A Vibe Coding Journey with Augment

Background

Recently, our copy team decided to update all UI text from Title Case to sentence case — meaning only the first letter of the first word gets capitalized:

"Confirm Update" -> "Confirm update"
"Edit User" -> "Edit user"

Sounds simple, right? It wasn’t.

Every change came with hidden rules: preserve proper nouns and acronyms, skip variable names, avoid touching technical terms, and make it work across all UI components, labels, and headings.

First try

So I briefly describe my request in the augment VSCode plugin, with the help of “Enhance Prompt” feature, I got a solid, structured set of instructions:

Search through the entire codebase for text strings that are in title case (where each word is captialized, eg. "Create User", "Edit Profile", "Delete Account") and convert them to sentence case (where only the first word is capitalized, e.g. "Create user", "Edit profile", "Delete account").

Specifically:
1. Look for text strings in UI components, labels, buttons, headings, and user-facing messages
2. Focus on strings that appear to be user interface text rather than code identifiers or technical terms
3. Preserve proper nouns and acronyms that should remain capitalized
4. Make the changes across all file types in the repository (JavaScript, TypeScript, JSX, TSX, HTML, etc.)
5. After making changes, run prettier to maintain consistent code formatting

Examples of transformations:
- "Create User" -> "Create user"
- "Edit Profile Settings" -> "Edit profile settings"
- "Delete Account" -> "Delete account"
- "Save Changes" -> "Save changes"

Do not change:
- Code variable names, function names, or class names
- Technical terms or proper nouns that should remain capitalized
- Already correctly formatted sentence case text

So far, so good — except Augment’s first move was to try listing every possible UI string instead of just transforming them.

After a few nudges, it got the idea… but tapped out after about 20 files. Turns out agent is not designed to process such a tedious job.

Break down the task

Given the bad outcome from Augment in first round,I fell back to the classic codemod approach, which means I need to write a script to walk through the AST file by file, and handle the transformations myself.

If this is 5 years ago, normally I would open up https://astexplorer.net/, copy a random code snippet into the editor, and write the visitor function to get string literals in each file. This could take me around 2 hours to finish the job. But now since we are doing vibe coding, I’ll let Augment do the chores, I just send my instructions like this:

In codemod.js, use `typescript` as an AST parser and `fast-glob` to get all "*.ts(x)"files, get all kinds of strings in each file.

After ~ 5 rounds of talking, Augment handed me a working visitor script. I tightened it up with a couple of rules:

  1. Only match strings with at least one uppercase letter.
  2. Skip strings in import statements.

This is the script I get:

import {
  isImportDeclaration,
  isStringLiteral,
  isNoSubstitutionTemplateLiteral,
  isTemplateExpression,
  isJsxText,
  isJsxAttribute,
  forEachChild,
  createSourceFile,
  ScriptTarget,
  ScriptKind,
} from 'typescript';
import glob from 'fast-glob';
import { readFileSync, writeFileSync } from 'fs';

// Function to extract all string literals from a TypeScript AST node
function extractStrings(sourceFile) {
  const stringsWithLineNumbers = [];

  // Helper function to check if string contains at least one capital letter
  function hasCapitalLetter(str) {
    return /[A-Z]/.test(str);
  }

  // Helper function to check if a node is part of an import statement
  function isInImportStatement(node) {
    let parent = node.parent;
    while (parent) {
      if (isImportDeclaration(parent)) {
        return true;
      }
      parent = parent.parent;
    }
    return false;
  }

  // Helper function to get line number from node position
  function getLineNumber(node) {
    const lineAndChar = sourceFile.getLineAndCharacterOfPosition(
      node.getStart(),
    );
    return lineAndChar.line + 1;
  }

  function visit(node) {
    // Handle regular string literals
    if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
      if (hasCapitalLetter(node.text) && !isInImportStatement(node)) {
        stringsWithLineNumbers.push({
          text: node.text,
          line: getLineNumber(node),
        });
      }
    }
    // Handle template expressions (template literals with ${} expressions)
    else if (isTemplateExpression(node)) {
      if (node.head && hasCapitalLetter(node.head.text)) {
        stringsWithLineNumbers.push({
          text: node.head.text,
          line: getLineNumber(node.head),
        });
      }
      node.templateSpans.forEach(span => {
        if (span.literal && hasCapitalLetter(span.literal.text)) {
          stringsWithLineNumbers.push({
            text: span.literal.text,
            line: getLineNumber(span.literal),
          });
        }
      });
    }
    // Handle JSX text content
    else if (isJsxText(node)) {
      const text = node.text.trim();
      if (text && hasCapitalLetter(text)) {
        stringsWithLineNumbers.push({
          text: text,
          line: getLineNumber(node),
        });
      }
    }
    // Handle JSX string literal attributes
    else if (
      isJsxAttribute(node) &&
      node.initializer &&
      isStringLiteral(node.initializer)
    ) {
      if (hasCapitalLetter(node.initializer.text)) {
        stringsWithLineNumbers.push({
          text: node.initializer.text,
          line: getLineNumber(node.initializer),
        });
      }
    }

    // Continue traversing child nodes
    forEachChild(node, visit);
  }

  // Start the traversal from the source file
  visit(sourceFile);
  return stringsWithLineNumbers;
}

// Function to process a single TypeScript file
function processFile(filePath) {
  try {
    const sourceCode = readFileSync(filePath, 'utf8');

    // Debug: Log first few characters to see if file is read correctly
    console.log(`Processing ${filePath} {${sourceCode.length} chars}`);

    // Create a TypeScript source file with more explicit options
    const sourceFile = createSourceFile(
      filePath,
      sourceCode,
      ScriptTarget.Latest,
      true,
      filePath.endsWith('tsx') ? ScriptKind.TSX : ScriptKind.TS,
    );

    // Check if parsing was successful
    if (sourceFile.parseDiagnostics && sourceFile.parseDiagnostics.length > 0) {
      console.warn(
        `Parse warnings/errors in ${filePath}:`,
        sourceFile.parseDiagnostics.map(d => d.messageText),
      );
    }

    // Extract all strings from the file
    const stringsWithLineNumbers = extractStrings(sourceFile);

    console.log(` -> Found ${stringsWithLineNumbers.length} strings`);

    return {
      filePath,
      strings: stringsWithLineNumbers,
      stringCount: stringsWithLineNumbers.length,
    };
  } catch (error) {
    console.error(`Error processing file ${filePath}:`, error.message);
    console.error('Stack trace:', error.stack);
    return {
      filePath,
      strings: [],
      stringCount: 0,
      error: error.message,
    };
  }
}

// Main function to process all TypeScript files
async function processAllTypeScriptFiles() {
  try {
    console.log('Starting file discovery...');

    // Get all TypeScript files using fast-glob
    const files = await glob(['**/*.ts', '**/*.tsx'], {
      ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.d.ts'],
      cwd: 'app',
      absolute: true,
    });

    console.log(`Found ${files.length} TypeScript files:`);
    files.slice(0, 5).forEach(file => console.log(` - ${file}`));
    if (files.length > 5) {
      console.log(` ... and ${files.length - 5} more`);
    }

    const results = [];
    let totalStrings = 0;

    // Process each file
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);

      const result = processFile(file);
      if (result.stringCount > 0) {
        results.push(result);
      }
      totalStrings += result.stringCount;
    }

    console.log(`\n=== SUMMARY ===`);
    console.log(`Total files processed: ${files.length}`);
    console.log(`Total strings found: ${totalStrings}`);

    return results;
  } catch (error) {
    console.error('Error processing TypeScript files:', error);
    console.error('Stack trace:', error.stack);
    throw error;
  }
}

// Run the codemod
processAllTypeScriptFiles()
  .then(results => {
    console.log('\nProcessing complete!');

    // Optional: Save results to a JSON file
    writeFileSync(
      'string-extraction-results.json',
      JSON.stringify(results, null, 2),
    );
    console.log('Results saved to string-extraction-results.json');
  })
  .catch(error => {
    console.error('Codemod failed:', error);
    process.exit(1);
  });

After running the script, it generated a manifest file of all string occurrence like this:

[
  {
    "filePath": "/src/app/login/route.ts",
    "strings": [
      {
        "text": "Invalid Login Request",
        "line": 9
      },
      {
        "text": "&.Mui-selected",
        "line": 21
      }
    ],
    "stringCount": 2
   }
 ]

“Smart” replacement

The rough AST scan still yields false positives (e.g., CSS selectors). To safely convert strings, I used Auggie CLI to process each file individually. This allowed AI to decide case changes file by file without overwhelming context.

Auggie: The agentic CLI that goes where your code does

The most powerful AI software development platform with the industry-leading context engine.

Install the auggie cli by running:

npm install -g @augmentcode/auggie

Then loop through files:

fs.writeFileSync(tempFileName, JSON.stringify([fileResult], null, 2));

try {
  // Use Auggie CLI to convert title case to sentence case
  const auggieCMD = `auggie 'check this file ${tempFileName} and do ...' --print`;
  
  console.log(Running Auggie for file ${i + 1}/${testFiles}...);

  await new Promise((resolve, reject) => {
    const cp = spawn("sh", ["-c", auggieCMD], { stdio: "inherit" });
    cp.on("exit", (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(Auggie process exited with code ${code}));
      }
    });
    cp.on("error", reject);
  });
  // Clean up temp file
  fs.unlinkSync(tempFileName);
} catch (error) {
  console.error(Error processing ${filePath}:, error.message);
  // Clean up temp file even on error
  if (fs.existsSync(tempFileName)) {
    fs.unlinkSync(tempFileName);
  }
}

The —print flag can instruct auggie to run in non-interactive mode.

Running the script by:

node ./scripts/codemd.js

Now enjoy the smart agent working for you.

We can see Auggie completely understand the requirement and handles case conversion intelligently. It successfully ignores false positives, leaving only valid UI text updated.

Summary

Auggie not only saved hours of manual work but also ensured consistency across the entire application. Sometimes, all it takes is breaking a task into smaller steps and using old-school scripts to orchestrate AI tools across a series of sub-tasks.