@contentrain/mcp 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -20
- package/dist/{apply-manager-Cf5OWOJ7.mjs → apply-manager-BkPMfpsN.mjs} +8 -3
- package/dist/apply-manager-BkPMfpsN.mjs.map +1 -0
- package/dist/{astro-parser-RofKmKQi.mjs → astro-parser-BJ-54JGe.mjs} +3 -3
- package/dist/{astro-parser-RofKmKQi.mjs.map → astro-parser-BJ-54JGe.mjs.map} +1 -1
- package/dist/core/apply-manager.d.mts.map +1 -1
- package/dist/core/apply-manager.mjs +2 -2
- package/dist/core/scanner.d.mts +3 -9
- package/dist/core/scanner.d.mts.map +1 -1
- package/dist/core/scanner.mjs +3 -3
- package/dist/git/transaction.d.mts +6 -1
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +2 -2
- package/dist/index.mjs +5 -5
- package/dist/{scanner-DPrbU-oK.mjs → scanner-CQOxARUU.mjs} +275 -308
- package/dist/scanner-CQOxARUU.mjs.map +1 -0
- package/dist/{server-BXguCuJb.mjs → server-DfWRpzdP.mjs} +172 -20
- package/dist/server-DfWRpzdP.mjs.map +1 -0
- package/dist/server.mjs +5 -5
- package/dist/{svelte-parser-COTNG92w.mjs → svelte-parser-1bBcMx3T.mjs} +16 -6
- package/dist/{svelte-parser-COTNG92w.mjs.map → svelte-parser-1bBcMx3T.mjs.map} +1 -1
- package/dist/{transaction-B0V0_SE2.mjs → transaction-C_Pvux78.mjs} +102 -2
- package/dist/{transaction-B0V0_SE2.mjs.map → transaction-C_Pvux78.mjs.map} +1 -1
- package/dist/tsx-parser-BFLv292Y.mjs +2 -0
- package/dist/{tsx-parser-CYVyBWjn.mjs → tsx-parser-BWiMGAIR.mjs} +3 -2
- package/dist/tsx-parser-BWiMGAIR.mjs.map +1 -0
- package/dist/{vue-parser-CjS4YAoQ.mjs → vue-parser-DTPTN3Yb.mjs} +16 -5
- package/dist/{vue-parser-CjS4YAoQ.mjs.map → vue-parser-DTPTN3Yb.mjs.map} +1 -1
- package/package.json +2 -2
- package/dist/apply-manager-Cf5OWOJ7.mjs.map +0 -1
- package/dist/scanner-DPrbU-oK.mjs.map +0 -1
- package/dist/server-BXguCuJb.mjs.map +0 -1
- package/dist/tsx-parser-Bf8Firrq.mjs +0 -2
- package/dist/tsx-parser-CYVyBWjn.mjs.map +0 -1
|
@@ -271,6 +271,106 @@ async function createTransaction(projectRoot, branchName, options) {
|
|
|
271
271
|
}
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
|
+
async function mergeBranch(projectRoot, branchName) {
|
|
275
|
+
const git = simpleGit(projectRoot);
|
|
276
|
+
const config = await readConfig(projectRoot);
|
|
277
|
+
const remoteName = process.env["CONTENTRAIN_REMOTE"] ?? "origin";
|
|
278
|
+
const baseBranch = process.env["CONTENTRAIN_BRANCH"] ?? config?.repository?.default_branch ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
|
|
279
|
+
await ensureContentBranch(projectRoot);
|
|
280
|
+
let hasRemote = false;
|
|
281
|
+
try {
|
|
282
|
+
hasRemote = (await git.getRemotes()).some((r) => r.name === remoteName);
|
|
283
|
+
} catch {
|
|
284
|
+
hasRemote = false;
|
|
285
|
+
}
|
|
286
|
+
const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
|
|
287
|
+
await git.raw([
|
|
288
|
+
"worktree",
|
|
289
|
+
"add",
|
|
290
|
+
worktreePath,
|
|
291
|
+
CONTENTRAIN_BRANCH
|
|
292
|
+
]);
|
|
293
|
+
const wtGit = simpleGit(worktreePath);
|
|
294
|
+
const authorName = process.env["CONTENTRAIN_AUTHOR_NAME"] ?? "Contentrain";
|
|
295
|
+
const authorEmail = process.env["CONTENTRAIN_AUTHOR_EMAIL"] ?? "mcp@contentrain.io";
|
|
296
|
+
await wtGit.addConfig("user.name", authorName);
|
|
297
|
+
await wtGit.addConfig("user.email", authorEmail);
|
|
298
|
+
try {
|
|
299
|
+
try {
|
|
300
|
+
await wtGit.merge([branchName, "--no-edit"]);
|
|
301
|
+
} catch {
|
|
302
|
+
try {
|
|
303
|
+
await wtGit.merge(["--abort"]);
|
|
304
|
+
} catch {}
|
|
305
|
+
throw Object.assign(/* @__PURE__ */ new Error(`Merge conflict when merging branch "${branchName}" into "${CONTENTRAIN_BRANCH}". The branch still exists with your changes intact. Resolve the conflict manually, or delete the branch and retry.`), {
|
|
306
|
+
code: "CONTENT_BRANCH_MERGE_CONFLICT",
|
|
307
|
+
agent_hint: "The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.",
|
|
308
|
+
developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branchName}`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([
|
|
312
|
+
wtGit.raw(["rev-parse", "HEAD"]).then((s) => s.trim()),
|
|
313
|
+
git.raw(["rev-parse", baseBranch]).then((s) => s.trim()),
|
|
314
|
+
git.status()
|
|
315
|
+
]);
|
|
316
|
+
const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map((f) => f.path));
|
|
317
|
+
try {
|
|
318
|
+
await git.raw([
|
|
319
|
+
"merge-base",
|
|
320
|
+
"--is-ancestor",
|
|
321
|
+
previousBaseRef,
|
|
322
|
+
contentrainTip
|
|
323
|
+
]);
|
|
324
|
+
} catch {
|
|
325
|
+
throw Object.assign(/* @__PURE__ */ new Error(`Cannot fast-forward "${baseBranch}" to contentrain tip. The base branch has diverged. Merge "${baseBranch}" into "${CONTENTRAIN_BRANCH}" first.`), {
|
|
326
|
+
code: "BASE_UPDATE_FAILED",
|
|
327
|
+
agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,
|
|
328
|
+
developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
await git.raw([
|
|
332
|
+
"update-ref",
|
|
333
|
+
`refs/heads/${baseBranch}`,
|
|
334
|
+
contentrainTip
|
|
335
|
+
]);
|
|
336
|
+
try {
|
|
337
|
+
await git.raw(["read-tree", "HEAD"]);
|
|
338
|
+
} catch {
|
|
339
|
+
try {
|
|
340
|
+
await git.raw(["reset", "HEAD"]);
|
|
341
|
+
} catch {}
|
|
342
|
+
}
|
|
343
|
+
const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate);
|
|
344
|
+
if (hasRemote) {
|
|
345
|
+
try {
|
|
346
|
+
await git.push(remoteName, CONTENTRAIN_BRANCH);
|
|
347
|
+
} catch {
|
|
348
|
+
try {
|
|
349
|
+
await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH);
|
|
350
|
+
await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, "--no-edit"]);
|
|
351
|
+
await git.push(remoteName, CONTENTRAIN_BRANCH);
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
await git.push(remoteName, baseBranch);
|
|
356
|
+
} catch {}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
action: "merged",
|
|
360
|
+
commit: contentrainTip,
|
|
361
|
+
sync
|
|
362
|
+
};
|
|
363
|
+
} finally {
|
|
364
|
+
try {
|
|
365
|
+
await git.raw([
|
|
366
|
+
"worktree",
|
|
367
|
+
"remove",
|
|
368
|
+
worktreePath,
|
|
369
|
+
"--force"
|
|
370
|
+
]);
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
274
374
|
function buildBranchName(scope, target, locale) {
|
|
275
375
|
const ts = branchTimestamp();
|
|
276
376
|
const parts = [
|
|
@@ -283,6 +383,6 @@ function buildBranchName(scope, target, locale) {
|
|
|
283
383
|
return parts.join("/");
|
|
284
384
|
}
|
|
285
385
|
//#endregion
|
|
286
|
-
export { createTransaction as n, ensureContentBranch as r, buildBranchName as t };
|
|
386
|
+
export { mergeBranch as i, createTransaction as n, ensureContentBranch as r, buildBranchName as t };
|
|
287
387
|
|
|
288
|
-
//# sourceMappingURL=transaction-
|
|
388
|
+
//# sourceMappingURL=transaction-C_Pvux78.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction-B0V0_SE2.mjs","names":["removeFile"],"sources":["../src/git/transaction.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { rm as removeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { branchTimestamp } from '../util/id.js'\nimport type { SyncResult } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string; sync?: SyncResult; warning?: string }>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Check if contentrain branch exists locally\n const branches = await git.branchLocal()\n if (branches.all.includes(CONTENTRAIN_BRANCH)) return\n\n // Detect base branch\n const baseBranch = config?.repository?.default_branch\n || (await git.raw(['branch', '--show-current'])).trim()\n || 'main'\n\n // Migration: old system used `contentrain/*` feature branches which conflict\n // with creating a `contentrain` branch (git ref namespace: path can't be both\n // file and directory). Clean up merged old-prefix branches before creating.\n const oldPrefixBranches = branches.all.filter(b => b.startsWith('contentrain/'))\n if (oldPrefixBranches.length > 0) {\n // Get merged branches (safe to delete)\n let mergedBranches: string[] = []\n try {\n const mergedOutput = await git.raw(['branch', '--merged', baseBranch])\n mergedBranches = mergedOutput.split('\\n')\n .map(b => b.trim().replace(/^\\*\\s*/, ''))\n .filter(b => b.startsWith('contentrain/'))\n } catch { /* ignore */ }\n\n // Delete merged old-prefix branches\n for (const b of mergedBranches) {\n try { await git.raw(['branch', '-d', b]) } catch { /* skip if protected */ }\n }\n\n // Check if unmerged old-prefix branches still block creation\n const remaining = (await git.branchLocal()).all.filter(b => b.startsWith('contentrain/'))\n if (remaining.length > 0) {\n // Force-delete remaining old-prefix branches (they use the old system,\n // content is already on main via previous auto-merges)\n for (const b of remaining) {\n try { await git.raw(['branch', '-D', b]) } catch { /* skip */ }\n }\n }\n }\n\n // Create contentrain branch from base\n await git.branch([CONTENTRAIN_BRANCH, baseBranch])\n\n // Push to remote if exists\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n try {\n const remotes = await git.getRemotes()\n if (remotes.some(r => r.name === remoteName)) {\n await git.push(['-u', remoteName, CONTENTRAIN_BRANCH])\n }\n } catch {\n // Remote push is best-effort\n }\n}\n\nasync function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = simpleGit(projectRoot)\n const synced: string[] = []\n const skipped: string[] = []\n\n // Use git plumbing to find ALL files that differ between old and new commits.\n // diff-tree is fast and ignores working tree / index state entirely.\n // Not limited to .contentrain/ — some ops also modify .gitignore, etc.\n const compareRef = _previousBaseRef ?? contentrainTip\n let changedFiles: string[] = []\n try {\n const diffOutput = await git.raw([\n 'diff-tree', '--name-only', '-r', '--no-commit-id',\n compareRef, contentrainTip,\n ])\n changedFiles = diffOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n // Fallback: list .contentrain/ files from the contentrainTip commit\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '.contentrain/'])\n changedFiles = lsOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n return { synced, skipped }\n }\n }\n\n if (changedFiles.length === 0) return { synced, skipped }\n\n // Use pre-captured dirty files (before update-ref) to avoid false positives.\n // After update-ref, files appear as \"modified\" in status even though the developer\n // didn't touch them. We use the pre-update state to know what was truly dirty.\n const dirtyFiles = dirtyFilesBeforeUpdate ?? new Set<string>()\n\n // Determine which changed files still exist in contentrainTip (HEAD after update-ref)\n // Check each changed file individually — `ls-tree` on specific paths is efficient\n const filesInTip = new Set<string>()\n for (const file of changedFiles) {\n try {\n await git.raw(['cat-file', '-e', `${contentrainTip}:${file}`])\n filesInTip.add(file)\n } catch {\n // File does not exist in tip (was deleted)\n }\n }\n\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) {\n skipped.push(file)\n } else if (filesInTip.has(file)) {\n // File exists in new HEAD — checkout from HEAD\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n } else {\n // File was deleted in new HEAD — remove from working tree\n try {\n const filePath = join(projectRoot, file)\n await removeFile(filePath, { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n\n const warning = skipped.length > 0\n ? `${skipped.length} file(s) skipped due to local changes: ${skipped.join(', ')}. Commit your changes, then run: git checkout HEAD -- ${skipped.join(' ')}`\n : undefined\n\n return { synced, skipped, warning }\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: 'review' | 'auto-merge' },\n): Promise<GitTransaction> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch + current branch + remote in ONE batch\n // (reduces subprocess spawns from 4 to 2)\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n let currentBranch = ''\n let hasRemote = false\n\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n currentBranch = branchResult.trim()\n if (!baseBranch) baseBranch = currentBranch || 'main'\n hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n\n // Check if developer is on contentrain branch\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Contentrain manages this branch automatically. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch (e.g., main or a feature branch), then retry the operation.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n // Ensure contentrain branch exists (with migration for old contentrain/* branches)\n await ensureContentBranch(projectRoot)\n\n // Fetch latest from remote (parallel fetch for both branches)\n if (hasRemote) {\n await Promise.all([\n git.fetch(remoteName, baseBranch).catch(() => {}),\n git.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree on contentrain branch\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author (sequential — git config uses lock file)\n const authorName = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? 'Contentrain'\n const authorEmail = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? 'mcp@contentrain.io'\n await wtGit.addConfig('user.name', authorName)\n await wtGit.addConfig('user.email', authorEmail)\n\n // Sync contentrain with base branch (bring main changes into contentrain)\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n\n // Sync with remote contentrain if exists\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Create feature branch from contentrain\n await wtGit.checkout(['-b', branch])\n\n let commitHash = ''\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message, contextUpdate?) {\n // Write context.json together with content (no separate commit)\n if (contextUpdate) {\n await writeContext(worktreePath, contextUpdate)\n }\n await wtGit.add('.')\n const result = await wtGit.commit(message, { '--allow-empty': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete() {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge feature branch into contentrain, then advance base\n\n // Switch to contentrain branch in worktree\n await wtGit.checkout(CONTENTRAIN_BRANCH)\n\n // Merge feature branch into contentrain\n try {\n await wtGit.merge([branch, '--no-edit'])\n } catch {\n try {\n await wtGit.merge(['--abort'])\n } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branch}`,\n })\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n // (guaranteed by the merge above, but verify for safety)\n try {\n await git.raw(['merge-base', '--is-ancestor', previousBaseRef, contentrainTip])\n } catch {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD.\n // update-ref moves the branch pointer but leaves the index stale.\n // read-tree updates the index to match HEAD without touching the working tree.\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n // fallback: try reset for older git versions\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort with retry)\n if (hasRemote) {\n // Push contentrain branch\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Retry: fetch, merge, push\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n // Push base branch\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n ...(sync.warning ? { warning: sync.warning } : {}),\n }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n },\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['cr', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n"],"mappings":";;;;;;;;;;AA2BA,eAAsB,oBAAoB,aAAoC;CAC5E,MAAM,MAAM,UAAU,YAAY;CAClC,MAAM,SAAS,MAAM,WAAW,YAAY;CAG5C,MAAM,WAAW,MAAM,IAAI,aAAa;AACxC,KAAI,SAAS,IAAI,SAAS,mBAAmB,CAAE;CAG/C,MAAM,aAAa,QAAQ,YAAY,mBACjC,MAAM,IAAI,IAAI,CAAC,UAAU,iBAAiB,CAAC,EAAE,MAAM,IACpD;AAML,KAD0B,SAAS,IAAI,QAAO,MAAK,EAAE,WAAW,eAAe,CAAC,CAC1D,SAAS,GAAG;EAEhC,IAAI,iBAA2B,EAAE;AACjC,MAAI;AAEF,qBADqB,MAAM,IAAI,IAAI;IAAC;IAAU;IAAY;IAAW,CAAC,EACxC,MAAM,KAAK,CACtC,KAAI,MAAK,EAAE,MAAM,CAAC,QAAQ,UAAU,GAAG,CAAC,CACxC,QAAO,MAAK,EAAE,WAAW,eAAe,CAAC;UACtC;AAGR,OAAK,MAAM,KAAK,eACd,KAAI;AAAE,SAAM,IAAI,IAAI;IAAC;IAAU;IAAM;IAAE,CAAC;UAAS;EAInD,MAAM,aAAa,MAAM,IAAI,aAAa,EAAE,IAAI,QAAO,MAAK,EAAE,WAAW,eAAe,CAAC;AACzF,MAAI,UAAU,SAAS,EAGrB,MAAK,MAAM,KAAK,UACd,KAAI;AAAE,SAAM,IAAI,IAAI;IAAC;IAAU;IAAM;IAAE,CAAC;UAAS;;AAMvD,OAAM,IAAI,OAAO,CAAC,oBAAoB,WAAW,CAAC;CAGlD,MAAM,aAAa,QAAQ,IAAI,yBAAyB;AACxD,KAAI;AAEF,OADgB,MAAM,IAAI,YAAY,EAC1B,MAAK,MAAK,EAAE,SAAS,WAAW,CAC1C,OAAM,IAAI,KAAK;GAAC;GAAM;GAAY;GAAmB,CAAC;SAElD;;AAKV,eAAe,cACb,aACA,eACA,gBACA,kBACA,wBACqB;CACrB,MAAM,MAAM,UAAU,YAAY;CAClC,MAAM,SAAmB,EAAE;CAC3B,MAAM,UAAoB,EAAE;CAK5B,MAAM,aAAa,oBAAoB;CACvC,IAAI,eAAyB,EAAE;AAC/B,KAAI;AAKF,kBAJmB,MAAM,IAAI,IAAI;GAC/B;GAAa;GAAe;GAAM;GAClC;GAAY;GACb,CAAC,EACwB,MAAM,KAAK,CAAC,QAAO,MAAK,EAAE,MAAM,CAAC,SAAS,EAAE;SAChE;AAEN,MAAI;AAEF,mBADiB,MAAM,IAAI,IAAI;IAAC;IAAW;IAAM;IAAe;IAAgB;IAAgB,CAAC,EACzE,MAAM,KAAK,CAAC,QAAO,MAAK,EAAE,MAAM,CAAC,SAAS,EAAE;UAC9D;AACN,UAAO;IAAE;IAAQ;IAAS;;;AAI9B,KAAI,aAAa,WAAW,EAAG,QAAO;EAAE;EAAQ;EAAS;CAKzD,MAAM,aAAa,0CAA0B,IAAI,KAAa;CAI9D,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,QAAQ,aACjB,KAAI;AACF,QAAM,IAAI,IAAI;GAAC;GAAY;GAAM,GAAG,eAAe,GAAG;GAAO,CAAC;AAC9D,aAAW,IAAI,KAAK;SACd;AAKV,MAAK,MAAM,QAAQ,aACjB,KAAI,WAAW,IAAI,KAAK,CACtB,SAAQ,KAAK,KAAK;UACT,WAAW,IAAI,KAAK,CAE7B,KAAI;AACF,QAAM,IAAI,SAAS;GAAC;GAAQ;GAAM;GAAK,CAAC;AACxC,SAAO,KAAK,KAAK;SACX;AACN,UAAQ,KAAK,KAAK;;KAIpB,KAAI;AAEF,QAAMA,GADW,KAAK,aAAa,KAAK,EACb,EAAE,OAAO,MAAM,CAAC;AAC3C,SAAO,KAAK,KAAK;SACX;AACN,UAAQ,KAAK,KAAK;;AASxB,QAAO;EAAE;EAAQ;EAAS,SAJV,QAAQ,SAAS,IAC7B,GAAG,QAAQ,OAAO,yCAAyC,QAAQ,KAAK,KAAK,CAAC,wDAAwD,QAAQ,KAAK,IAAI,KACvJ,KAAA;EAE+B;;AAGrC,eAAsB,kBACpB,aACA,YACA,SACyB;CACzB,MAAM,MAAM,UAAU,YAAY;CAClC,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,WAAW,SAAS,oBAAoB,QAAQ,YAAY;CAElE,MAAM,aAAa,QAAQ,IAAI,yBAAyB;CAIxD,IAAI,aAAa,QAAQ,IAAI,yBAAyB,QAAQ,YAAY,kBAAkB;CAC5F,IAAI,gBAAgB;CACpB,IAAI,YAAY;CAEhB,MAAM,CAAC,cAAc,WAAW,MAAM,QAAQ,IAAI,CAChD,IAAI,IAAI,CAAC,UAAU,iBAAiB,CAAC,CAAC,YAAY,GAAG,EACrD,IAAI,YAAY,CAAC,YAAY,EAAE,CAAC,CACjC,CAAC;AACF,iBAAgB,aAAa,MAAM;AACnC,KAAI,CAAC,WAAY,cAAa,iBAAiB;AAC/C,aAAa,QAA+B,MAAK,MAAK,EAAE,SAAS,WAAW;AAG5E,KAAI,kBAAkB,mBACpB,OAAM,OAAO,uBAAO,IAAI,MACtB,QAAQ,mBAAmB,4IAG5B,EAAE;EACD,MAAM;EACN,YAAY;EACZ,kBAAkB,gBAAgB;EACnC,CAAC;AAIJ,OAAM,oBAAoB,YAAY;AAGtC,KAAI,UACF,OAAM,QAAQ,IAAI,CAChB,IAAI,MAAM,YAAY,WAAW,CAAC,YAAY,GAAG,EACjD,IAAI,MAAM,YAAY,mBAAmB,CAAC,YAAY,GAAG,CAC1D,CAAC;CAGJ,MAAM,eAAe,KAAK,QAAQ,EAAE,MAAM,YAAY,GAAG;CACzD,MAAM,SAAS;AAGf,OAAM,IAAI,IAAI;EAAC;EAAY;EAAO;EAAc;EAAmB,CAAC;CAEpE,MAAM,QAAQ,UAAU,aAAa;CAGrC,MAAM,aAAa,QAAQ,IAAI,8BAA8B;CAC7D,MAAM,cAAc,QAAQ,IAAI,+BAA+B;AAC/D,OAAM,MAAM,UAAU,aAAa,WAAW;AAC9C,OAAM,MAAM,UAAU,cAAc,YAAY;AAGhD,KAAI;AACF,QAAM,MAAM,MAAM,CAAC,YAAY,YAAY,CAAC;SACtC;AACN,MAAI;AAAE,SAAM,MAAM,MAAM,CAAC,UAAU,CAAC;UAAS;AAC7C,MAAI,UACF,KAAI;AACF,SAAM,MAAM,MAAM,CAAC,GAAG,WAAW,GAAG,cAAc,YAAY,CAAC;UACzD;AACN,OAAI;AAAE,UAAM,MAAM,MAAM,CAAC,UAAU,CAAC;WAAS;;;AAMnD,KAAI,UACF,KAAI;AACF,QAAM,MAAM,MAAM,CAAC,GAAG,WAAW,GAAG,sBAAsB,YAAY,CAAC;SACjE;AACN,MAAI;AAAE,SAAM,MAAM,MAAM,CAAC,UAAU,CAAC;UAAS;;AAKjD,OAAM,MAAM,SAAS,CAAC,MAAM,OAAO,CAAC;CAEpC,IAAI,aAAa;AAEjB,QAAO;EACL,UAAU;EACV;EAEA,MAAM,MAAM,UAAU;AACpB,SAAM,SAAS,aAAa;;EAG9B,MAAM,OAAO,SAAS,eAAgB;AAEpC,OAAI,cACF,OAAM,aAAa,cAAc,cAAc;AAEjD,SAAM,MAAM,IAAI,IAAI;AAEpB,iBADe,MAAM,MAAM,OAAO,SAAS,EAAE,iBAAiB,MAAM,CAAC,EACjD,UAAU;AAC9B,UAAO;;EAGT,MAAM,WAAW;AACf,OAAI,aAAa,UAAU;AACzB,QAAI,UACF,OAAM,IAAI,KAAK,YAAY,OAAO;AAEpC,WAAO;KAAE,QAAQ;KAAkB,QAAQ;KAAY;;AAMzD,SAAM,MAAM,SAAS,mBAAmB;AAGxC,OAAI;AACF,UAAM,MAAM,MAAM,CAAC,QAAQ,YAAY,CAAC;WAClC;AACN,QAAI;AACF,WAAM,MAAM,MAAM,CAAC,UAAU,CAAC;YACxB;AACR,UAAM,OAAO,uBAAO,IAAI,MACtB,uCAAuC,OAAO,UAAU,mBAAmB,qHAG5E,EAAE;KACD,MAAM;KACN,YAAY;KACZ,kBAAkB,gBAAgB,mBAAmB,gBAAgB;KACtE,CAAC;;GAIJ,MAAM,CAAC,gBAAgB,iBAAiB,sBAAsB,MAAM,QAAQ,IAAI;IAC9E,MAAM,IAAI,CAAC,aAAa,OAAO,CAAC,CAAC,MAAK,MAAK,EAAE,MAAM,CAAC;IACpD,IAAI,IAAI,CAAC,aAAa,WAAW,CAAC,CAAC,MAAK,MAAK,EAAE,MAAM,CAAC;IACtD,IAAI,QAAQ;IACb,CAAC;GACF,MAAM,yBAAyB,IAAI,IAAI,mBAAmB,MAAM,KAAI,MAAK,EAAE,KAAK,CAAC;AAIjF,OAAI;AACF,UAAM,IAAI,IAAI;KAAC;KAAc;KAAiB;KAAiB;KAAe,CAAC;WACzE;AACN,UAAM,OAAO,uBAAO,IAAI,MACtB,wBAAwB,WAAW,6DACO,WAAW,UAAU,mBAAmB,UACnF,EAAE;KACD,MAAM;KACN,YAAY,yDAAyD,WAAW,QAAQ,mBAAmB;KAC3G,kBAAkB,gBAAgB,mBAAmB,gBAAgB,WAAW,mBAAmB;KACpG,CAAC;;AAIJ,SAAM,IAAI,IAAI;IAAC;IAAc,cAAc;IAAc;IAAe,CAAC;AAKzE,OAAI;AACF,UAAM,IAAI,IAAI,CAAC,aAAa,OAAO,CAAC;WAC9B;AAEN,QAAI;AAAE,WAAM,IAAI,IAAI,CAAC,SAAS,OAAO,CAAC;YAAS;;GAIjD,MAAM,OAAO,MAAM,cAAc,aAAa,cAAc,gBAAgB,iBAAiB,uBAAuB;AAGpH,OAAI,WAAW;AAEb,QAAI;AACF,WAAM,IAAI,KAAK,YAAY,mBAAmB;YACxC;AAEN,SAAI;AACF,YAAM,MAAM,MAAM,YAAY,mBAAmB;AACjD,YAAM,MAAM,MAAM,CAAC,GAAG,WAAW,GAAG,sBAAsB,YAAY,CAAC;AACvE,YAAM,IAAI,KAAK,YAAY,mBAAmB;aACxC;;AAMV,QAAI;AACF,WAAM,IAAI,KAAK,YAAY,WAAW;YAChC;;AAKV,UAAO;IACL,QAAQ;IACR,QAAQ;IACR;IACA,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,SAAS,GAAG,EAAE;IAClD;;EAGH,MAAM,UAAU;AACd,OAAI;AACF,UAAM,IAAI,IAAI;KAAC;KAAY;KAAU;KAAc;KAAU,CAAC;WACxD;;EAIX;;AAGH,SAAgB,gBAAgB,OAAe,QAAgB,QAAyB;CACtF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,QAAQ;EAAC;EAAM;EAAO;EAAO;AACnC,KAAI,OAAQ,OAAM,KAAK,OAAO;AAC9B,OAAM,KAAK,GAAG;AACd,QAAO,MAAM,KAAK,IAAI"}
|
|
1
|
+
{"version":3,"file":"transaction-C_Pvux78.mjs","names":["removeFile"],"sources":["../src/git/transaction.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { rm as removeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { branchTimestamp } from '../util/id.js'\nimport type { SyncResult } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string; sync?: SyncResult; warning?: string }>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Check if contentrain branch exists locally\n const branches = await git.branchLocal()\n if (branches.all.includes(CONTENTRAIN_BRANCH)) return\n\n // Detect base branch\n const baseBranch = config?.repository?.default_branch\n || (await git.raw(['branch', '--show-current'])).trim()\n || 'main'\n\n // Migration: old system used `contentrain/*` feature branches which conflict\n // with creating a `contentrain` branch (git ref namespace: path can't be both\n // file and directory). Clean up merged old-prefix branches before creating.\n const oldPrefixBranches = branches.all.filter(b => b.startsWith('contentrain/'))\n if (oldPrefixBranches.length > 0) {\n // Get merged branches (safe to delete)\n let mergedBranches: string[] = []\n try {\n const mergedOutput = await git.raw(['branch', '--merged', baseBranch])\n mergedBranches = mergedOutput.split('\\n')\n .map(b => b.trim().replace(/^\\*\\s*/, ''))\n .filter(b => b.startsWith('contentrain/'))\n } catch { /* ignore */ }\n\n // Delete merged old-prefix branches\n for (const b of mergedBranches) {\n try { await git.raw(['branch', '-d', b]) } catch { /* skip if protected */ }\n }\n\n // Check if unmerged old-prefix branches still block creation\n const remaining = (await git.branchLocal()).all.filter(b => b.startsWith('contentrain/'))\n if (remaining.length > 0) {\n // Force-delete remaining old-prefix branches (they use the old system,\n // content is already on main via previous auto-merges)\n for (const b of remaining) {\n try { await git.raw(['branch', '-D', b]) } catch { /* skip */ }\n }\n }\n }\n\n // Create contentrain branch from base\n await git.branch([CONTENTRAIN_BRANCH, baseBranch])\n\n // Push to remote if exists\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n try {\n const remotes = await git.getRemotes()\n if (remotes.some(r => r.name === remoteName)) {\n await git.push(['-u', remoteName, CONTENTRAIN_BRANCH])\n }\n } catch {\n // Remote push is best-effort\n }\n}\n\nasync function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = simpleGit(projectRoot)\n const synced: string[] = []\n const skipped: string[] = []\n\n // Use git plumbing to find ALL files that differ between old and new commits.\n // diff-tree is fast and ignores working tree / index state entirely.\n // Not limited to .contentrain/ — some ops also modify .gitignore, etc.\n const compareRef = _previousBaseRef ?? contentrainTip\n let changedFiles: string[] = []\n try {\n const diffOutput = await git.raw([\n 'diff-tree', '--name-only', '-r', '--no-commit-id',\n compareRef, contentrainTip,\n ])\n changedFiles = diffOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n // Fallback: list .contentrain/ files from the contentrainTip commit\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '.contentrain/'])\n changedFiles = lsOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n return { synced, skipped }\n }\n }\n\n if (changedFiles.length === 0) return { synced, skipped }\n\n // Use pre-captured dirty files (before update-ref) to avoid false positives.\n // After update-ref, files appear as \"modified\" in status even though the developer\n // didn't touch them. We use the pre-update state to know what was truly dirty.\n const dirtyFiles = dirtyFilesBeforeUpdate ?? new Set<string>()\n\n // Determine which changed files still exist in contentrainTip (HEAD after update-ref)\n // Check each changed file individually — `ls-tree` on specific paths is efficient\n const filesInTip = new Set<string>()\n for (const file of changedFiles) {\n try {\n await git.raw(['cat-file', '-e', `${contentrainTip}:${file}`])\n filesInTip.add(file)\n } catch {\n // File does not exist in tip (was deleted)\n }\n }\n\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) {\n skipped.push(file)\n } else if (filesInTip.has(file)) {\n // File exists in new HEAD — checkout from HEAD\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n } else {\n // File was deleted in new HEAD — remove from working tree\n try {\n const filePath = join(projectRoot, file)\n await removeFile(filePath, { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n\n const warning = skipped.length > 0\n ? `${skipped.length} file(s) skipped due to local changes: ${skipped.join(', ')}. Commit your changes, then run: git checkout HEAD -- ${skipped.join(' ')}`\n : undefined\n\n return { synced, skipped, warning }\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: 'review' | 'auto-merge' },\n): Promise<GitTransaction> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch + current branch + remote in ONE batch\n // (reduces subprocess spawns from 4 to 2)\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n let currentBranch = ''\n let hasRemote = false\n\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n currentBranch = branchResult.trim()\n if (!baseBranch) baseBranch = currentBranch || 'main'\n hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n\n // Check if developer is on contentrain branch\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Contentrain manages this branch automatically. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch (e.g., main or a feature branch), then retry the operation.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n // Ensure contentrain branch exists (with migration for old contentrain/* branches)\n await ensureContentBranch(projectRoot)\n\n // Fetch latest from remote (parallel fetch for both branches)\n if (hasRemote) {\n await Promise.all([\n git.fetch(remoteName, baseBranch).catch(() => {}),\n git.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree on contentrain branch\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author (sequential — git config uses lock file)\n const authorName = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? 'Contentrain'\n const authorEmail = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? 'mcp@contentrain.io'\n await wtGit.addConfig('user.name', authorName)\n await wtGit.addConfig('user.email', authorEmail)\n\n // Sync contentrain with base branch (bring main changes into contentrain)\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n\n // Sync with remote contentrain if exists\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Create feature branch from contentrain\n await wtGit.checkout(['-b', branch])\n\n let commitHash = ''\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message, contextUpdate?) {\n // Write context.json together with content (no separate commit)\n if (contextUpdate) {\n await writeContext(worktreePath, contextUpdate)\n }\n await wtGit.add('.')\n const result = await wtGit.commit(message, { '--allow-empty': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete() {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge feature branch into contentrain, then advance base\n\n // Switch to contentrain branch in worktree\n await wtGit.checkout(CONTENTRAIN_BRANCH)\n\n // Merge feature branch into contentrain\n try {\n await wtGit.merge([branch, '--no-edit'])\n } catch {\n try {\n await wtGit.merge(['--abort'])\n } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branch}`,\n })\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n // (guaranteed by the merge above, but verify for safety)\n try {\n await git.raw(['merge-base', '--is-ancestor', previousBaseRef, contentrainTip])\n } catch {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD.\n // update-ref moves the branch pointer but leaves the index stale.\n // read-tree updates the index to match HEAD without touching the working tree.\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n // fallback: try reset for older git versions\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort with retry)\n if (hasRemote) {\n // Push contentrain branch\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Retry: fetch, merge, push\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n // Push base branch\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n ...(sync.warning ? { warning: sync.warning } : {}),\n }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n },\n }\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branchName: string,\n): Promise<{ action: 'merged'; commit: string; sync: SyncResult }> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Ensure contentrain branch exists\n await ensureContentBranch(projectRoot)\n\n // Check remote\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n\n // Create temp worktree on contentrain branch\n const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author\n const authorName = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? 'Contentrain'\n const authorEmail = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? 'mcp@contentrain.io'\n await wtGit.addConfig('user.name', authorName)\n await wtGit.addConfig('user.email', authorEmail)\n\n try {\n // Merge the feature branch into contentrain\n try {\n await wtGit.merge([branchName, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branchName}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branchName}`,\n })\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n try {\n await git.raw(['merge-base', '--is-ancestor', previousBaseRef, contentrainTip])\n } catch {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort)\n if (hasRemote) {\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n return {\n action: 'merged' as const,\n commit: contentrainTip,\n sync,\n }\n } finally {\n // Cleanup worktree\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['cr', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n"],"mappings":"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"}
|
|
@@ -121,7 +121,8 @@ function classifyByParentChain(node, sourceFile) {
|
|
|
121
121
|
};
|
|
122
122
|
return {
|
|
123
123
|
context: "jsx_attribute",
|
|
124
|
-
parent: attrName
|
|
124
|
+
parent: attrName,
|
|
125
|
+
parentProperty: attrName
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
if (ts.isVariableDeclaration(current)) return {
|
|
@@ -231,4 +232,4 @@ function buildSurrounding(lines, lineIdx) {
|
|
|
231
232
|
//#endregion
|
|
232
233
|
export { parseTsx as t };
|
|
233
234
|
|
|
234
|
-
//# sourceMappingURL=tsx-parser-
|
|
235
|
+
//# sourceMappingURL=tsx-parser-BWiMGAIR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsx-parser-BWiMGAIR.mjs","names":[],"sources":["../src/core/ast-scanner/tsx-parser.ts"],"sourcesContent":["import ts from 'typescript'\nimport type { ExtractedString, StructuralContext } from './types.js'\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n// ─── Public API ───\n\n/**\n * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context.\n *\n * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed).\n * Walks the AST and classifies each string by its parent chain.\n */\nexport function parseTsx(content: string, fileName: string): ExtractedString[] {\n const scriptKind = getScriptKind(fileName)\n const sourceFile = ts.createSourceFile(\n fileName,\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n scriptKind,\n )\n\n const results: ExtractedString[] = []\n const lines = content.split('\\n')\n\n visit(sourceFile, sourceFile, lines, results)\n\n return results\n}\n\n// ─── Script kind detection ───\n\nfunction getScriptKind(fileName: string): ts.ScriptKind {\n const lower = fileName.toLowerCase()\n if (lower.endsWith('.tsx')) return ts.ScriptKind.TSX\n if (lower.endsWith('.jsx')) return ts.ScriptKind.JSX\n if (lower.endsWith('.ts')) return ts.ScriptKind.TS\n return ts.ScriptKind.JS\n}\n\n// ─── AST Walker ───\n\nfunction visit(\n node: ts.Node,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n // Handle JsxText nodes\n if (ts.isJsxText(node)) {\n const text = node.text.trim()\n // Skip whitespace-only or empty JsxText\n if (text.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const parent = getJsxParentTag(node)\n results.push({\n value: text,\n line: lineIdx + 1,\n column: character + 1,\n context: 'jsx_text',\n scope: 'script',\n parent,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n return\n }\n\n // Handle string literals\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {\n const value = node.text\n // Skip empty strings\n if (value.length === 0) return\n\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const classification = classifyByParentChain(node, sourceFile)\n\n results.push({\n value,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n return\n }\n\n // Handle template literals with expressions — extract static head/spans\n if (ts.isTemplateExpression(node)) {\n extractTemplateParts(node, sourceFile, lines, results)\n return\n }\n\n ts.forEachChild(node, (child) => {\n visit(child, sourceFile, lines, results)\n })\n}\n\n// ─── Template literal parts extraction ───\n\nfunction extractTemplateParts(\n node: ts.TemplateExpression,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n const classification = classifyByParentChain(node, sourceFile)\n\n // Head: text before first ${...}\n const headText = node.head.text\n if (headText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile))\n results.push({\n value: headText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Template spans: text after each ${...} expression\n for (const span of node.templateSpans) {\n const spanText = span.literal.text\n if (spanText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile))\n results.push({\n value: spanText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Visit the expression inside ${...} for nested strings\n visit(span.expression, sourceFile, lines, results)\n }\n}\n\n// ─── Parent chain classification ───\n\ninterface Classification {\n context: StructuralContext\n parent: string\n parentProperty?: string\n}\n\nfunction classifyByParentChain(node: ts.Node, sourceFile: ts.SourceFile): Classification {\n let current = node.parent\n\n while (current) {\n // Import / Export declaration → import_path\n if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // Import specifier module path (dynamic import)\n if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // require() calls\n if (\n ts.isCallExpression(current)\n && ts.isIdentifier(current.expression)\n && current.expression.text === 'require'\n ) {\n return { context: 'import_path', parent: 'require' }\n }\n\n // JSX attribute → jsx_attribute or css_class\n if (ts.isJsxAttribute(current)) {\n const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n // className / class / style → CSS, not content\n if (attrName === 'className' || attrName === 'class' || attrName === 'style') {\n return { context: 'css_class', parent: attrName }\n }\n return { context: 'jsx_attribute', parent: attrName, parentProperty: attrName }\n }\n\n // Variable declaration → variable_assignment\n if (ts.isVariableDeclaration(current)) {\n const varName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'variable_assignment', parent: varName }\n }\n\n // Property assignment → object_property\n if (ts.isPropertyAssignment(current)) {\n const key = ts.isIdentifier(current.name)\n ? current.name.text\n : ts.isStringLiteral(current.name)\n ? current.name.text\n : current.name.getText(sourceFile)\n return { context: 'object_property', parent: key, parentProperty: key }\n }\n\n // Enum member → enum_value\n if (ts.isEnumMember(current)) {\n const enumName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'enum_value', parent: enumName }\n }\n\n // Call expression → function_argument or css_utility_call or console_call or test_assertion\n if (ts.isCallExpression(current)) {\n const callee = getCalleeName(current.expression, sourceFile)\n // CSS utility functions\n if (['cn', 'clsx', 'classNames', 'twMerge', 'twJoin', 'cva', 'cx'].includes(callee)) {\n return { context: 'css_utility_call', parent: callee }\n }\n // Console calls\n if (callee.startsWith('console.')) {\n return { context: 'console_call', parent: callee }\n }\n // Test assertions\n if (['describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(callee)) {\n return { context: 'test_assertion', parent: callee }\n }\n return { context: 'function_argument', parent: callee }\n }\n\n // Array literal → array_element\n if (ts.isArrayLiteralExpression(current)) {\n return { context: 'array_element', parent: 'array' }\n }\n\n // Case clause → switch_case\n if (ts.isCaseClause(current)) {\n return { context: 'switch_case', parent: 'case' }\n }\n\n // Type contexts → type_annotation\n if (\n ts.isTypeAliasDeclaration(current)\n || ts.isTypeReferenceNode(current)\n || ts.isInterfaceDeclaration(current)\n || ts.isTypeLiteralNode(current)\n || ts.isLiteralTypeNode(current)\n || ts.isUnionTypeNode(current)\n || ts.isIntersectionTypeNode(current)\n ) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n // Property declaration with type context (e.g., `as const` typed properties)\n // Check if we're inside a type annotation specifically\n if (ts.isPropertySignature(current)) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n current = current.parent\n }\n\n return { context: 'other', parent: '' }\n}\n\n// ─── Callee name extraction ───\n\nfunction getCalleeName(expr: ts.Expression, sourceFile: ts.SourceFile): string {\n if (ts.isIdentifier(expr)) {\n return expr.text\n }\n if (ts.isPropertyAccessExpression(expr)) {\n // e.g., console.log → \"console.log\"\n const obj = getCalleeName(expr.expression, sourceFile)\n return obj ? `${obj}.${expr.name.text}` : expr.name.text\n }\n return expr.getText(sourceFile)\n}\n\n// ─── JSX parent tag extraction ───\n\nfunction getJsxParentTag(node: ts.Node): string {\n let current = node.parent\n\n while (current) {\n if (ts.isJsxElement(current)) {\n const tagName = current.openingElement.tagName.getText()\n return tagName\n }\n if (ts.isJsxFragment(current)) {\n return 'Fragment'\n }\n current = current.parent\n }\n\n return ''\n}\n\n// ─── Surrounding code builder ───\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n"],"mappings":"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"}
|
|
@@ -178,14 +178,25 @@ let _tsxParser = null;
|
|
|
178
178
|
async function loadTsxParser() {
|
|
179
179
|
if (_tsxParser) return _tsxParser;
|
|
180
180
|
try {
|
|
181
|
-
_tsxParser = (await import("./tsx-parser-
|
|
181
|
+
_tsxParser = (await import("./tsx-parser-BFLv292Y.mjs")).parseTsx;
|
|
182
182
|
return _tsxParser;
|
|
183
183
|
} catch {
|
|
184
184
|
return null;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Map script block lang to a file extension that tsx-parser's getScriptKind understands.
|
|
189
|
+
* Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations
|
|
190
|
+
* (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.
|
|
191
|
+
*/
|
|
192
|
+
function resolveScriptFileName(vueFileName, lang) {
|
|
193
|
+
if (lang === "tsx") return vueFileName.replace(/\.vue$/, ".tsx");
|
|
194
|
+
if (lang === "ts") return vueFileName.replace(/\.vue$/, ".ts");
|
|
195
|
+
if (lang === "jsx") return vueFileName.replace(/\.vue$/, ".jsx");
|
|
196
|
+
return vueFileName.replace(/\.vue$/, ".js");
|
|
197
|
+
}
|
|
198
|
+
function parseScriptBlock(scriptContent, scriptStartLine, fileName, parseTsx, lang) {
|
|
199
|
+
return parseTsx(scriptContent, resolveScriptFileName(fileName, lang)).map((r) => {
|
|
189
200
|
r.line = r.line + scriptStartLine - 1;
|
|
190
201
|
r.scope = "script";
|
|
191
202
|
return r;
|
|
@@ -213,7 +224,7 @@ async function parseVue(content, fileName) {
|
|
|
213
224
|
const parseTsx = await loadTsxParser();
|
|
214
225
|
if (parseTsx) {
|
|
215
226
|
const scriptStartLine = scriptBlock.loc.start.line;
|
|
216
|
-
const scriptResults = parseScriptBlock(scriptBlock.content, scriptStartLine, fileName, parseTsx);
|
|
227
|
+
const scriptResults = parseScriptBlock(scriptBlock.content, scriptStartLine, fileName, parseTsx, scriptBlock.lang);
|
|
217
228
|
results.push(...scriptResults);
|
|
218
229
|
}
|
|
219
230
|
}
|
|
@@ -222,4 +233,4 @@ async function parseVue(content, fileName) {
|
|
|
222
233
|
//#endregion
|
|
223
234
|
export { parseVue };
|
|
224
235
|
|
|
225
|
-
//# sourceMappingURL=vue-parser-
|
|
236
|
+
//# sourceMappingURL=vue-parser-DTPTN3Yb.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-parser-CjS4YAoQ.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n): ExtractedString[] {\n // Parse script content using tsx-parser\n const scriptResults = parseTsx(scriptContent, fileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue-parser-DTPTN3Yb.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\n/**\n * Map script block lang to a file extension that tsx-parser's getScriptKind understands.\n * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations\n * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.\n */\nfunction resolveScriptFileName(vueFileName: string, lang?: string): string {\n if (lang === 'tsx') return vueFileName.replace(/\\.vue$/, '.tsx')\n if (lang === 'ts') return vueFileName.replace(/\\.vue$/, '.ts')\n if (lang === 'jsx') return vueFileName.replace(/\\.vue$/, '.jsx')\n return vueFileName.replace(/\\.vue$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n // Resolve filename with correct extension for TypeScript parser\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n scriptBlock.lang,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":";;AA0CA,MAAM,YAAY;CAChB,MAAM;CACN,SAAS;CACT,MAAM;CACN,SAAS;CACT,mBAAmB;CACnB,eAAe;CACf,WAAW;CACX,WAAW;CACX,qBAAqB;CACrB,IAAI;CACJ,WAAW;CACX,KAAK;CACL,WAAW;CACX,YAAY;CACZ,oBAAoB;CACrB;AAuCD,IAAI,YAAmC;AAEvC,eAAe,eAAwC;AACrD,KAAI,UAAW,QAAO;AACtB,KAAI;AAGF,cADY,MAAM,OAAO,mCAAA,MAAA,MAAA,wBAAA,EAAA,SAAA,EAAA,CAAA;AAEzB,SAAO;SACD;AACN,QAAM,IAAI,MACR,oGAED;;;AAML,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CAAM;CAAW;CAAQ;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAM,sBAAsB,IAAI,IAAI;CAClC;CAAS;CACT;CACA;CACA;CACD,CAAC;AAGF,MAAM,iBAAiB,IAAI,IAAI,CAC7B,SAAS,QACV,CAAC;AAIF,MAAM,kBAAkB;AAexB,SAAS,qBAAqB,SAAiB,MAAsB;CACnE,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,MAAM,MAAM,OAAO;AACnB,KAAI,OAAO,KAAK,MAAM,MAAM,OAC1B,SAAQ,MAAM,QAAQ,IAAI,MAAM,GAAG,gBAAgB;AAErD,QAAO;;AAKT,SAAS,aACP,MACA,oBACA,YACA,SACA,YAAoB,IACd;AACN,SAAQ,KAAK,MAAb;EACE,KAAK,UAAU,MAAM;GAKnB,MAAM,WAHO,OAAO,KAAK,YAAY,WACjC,KAAK,UACJ,KAAK,KAAK,UAAU,IACJ,MAAM;AAC3B,OAAI,QAAQ,SAAS,KAAK,KAAK,KAAK,QAAQ,CAC1C,SAAQ,KAAK;IACX,OAAO;IACP,MAAM,KAAK,IAAI,MAAM,OAAO,qBAAqB;IACjD,QAAQ,KAAK,IAAI,MAAM;IACvB,SAAS;IACT,OAAO;IACP,QAAQ;IACR,aAAa,qBAAqB,YAAY,KAAK,IAAI,MAAM,OAAO,qBAAqB,EAAE;IAC5F,CAAC;AAEJ;;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK,WAAW,OAAO,KAAK,YAAY,YAAY,KAAK,QAAQ,SAAS,UAAU,mBAAmB;IAMzG,MAAM,eALO,OAAO,KAAK,QAAQ,YAAY,WACzC,KAAK,QAAQ,UACb,IAGqB,MAAM,kBAAkB;AACjD,QAAI,cAAc,GAChB,SAAQ,KAAK;KACX,OAAO,YAAY;KACnB,MAAM,KAAK,IAAI,MAAM,OAAO,qBAAqB;KACjD,QAAQ,KAAK,IAAI,MAAM;KACvB,SAAS;KACT,OAAO;KACP,QAAQ;KACR,aAAa,qBAAqB,YAAY,KAAK,IAAI,MAAM,OAAO,qBAAqB,EAAE;KAC5F,CAAC;;AAIN;EAGF,KAAK,UAAU,SAAS;GACtB,MAAM,MAAM,KAAK,OAAO;AAGxB,OAAI,KAAK,MACP,MAAK,MAAM,QAAQ,KAAK,MACtB,aAAY,MAAM,KAAK,oBAAoB,YAAY,QAAQ;AAKnE,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,IAAI;AAGrE;;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK;SACF,MAAM,UAAU,KAAK,SACxB,KAAI,OAAO,SACT,MAAK,MAAM,SAAS,OAAO,SACzB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;;AAK/E;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;AAG3E;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;AAG3E;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;AAG3E;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK;SACF,MAAM,SAAS,KAAK,SACvB,KAAI,OAAO,UAAU,SACnB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;;AAI7E;EAGF,KAAK,UAAU;AAEb,OAAI,KAAK,WAAW,OAAO,KAAK,YAAY,SAC1C,cAAa,KAAK,SAAS,oBAAoB,YAAY,SAAS,UAAU;AAGhF,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;AAG3E;EAGF;AAEE,OAAI,KAAK,SACP,MAAK,MAAM,SAAS,KAAK,SACvB,cAAa,OAAO,oBAAoB,YAAY,SAAS,UAAU;AAG3E;;;AAKN,SAAS,YACP,MACA,WACA,oBACA,YACA,SACM;AACN,KAAI,KAAK,SAAS,UAAU,WAAW;AAErC,MAAI,CAAC,KAAK,MAAO;EAEjB,MAAM,WAAW,KAAK;EACtB,MAAM,YAAY,KAAK,MAAM;AAE7B,MAAI,CAAC,aAAa,UAAU,MAAM,CAAC,WAAW,EAAG;AAGjD,MAAI,eAAe,IAAI,SAAS,EAAE;AAChC,WAAQ,KAAK;IACX,OAAO;IACP,MAAM,KAAK,MAAM,IAAI,MAAM,OAAO,qBAAqB;IACvD,QAAQ,KAAK,MAAM,IAAI,MAAM;IAC7B,SAAS;IACT,OAAO;IACP,QAAQ;IACR,gBAAgB;IAChB,aAAa,qBAAqB,YAAY,KAAK,MAAM,IAAI,MAAM,OAAO,qBAAqB,EAAE;IAClG,CAAC;AACF;;AAGF,UAAQ,KAAK;GACX,OAAO;GACP,MAAM,KAAK,MAAM,IAAI,MAAM,OAAO,qBAAqB;GACvD,QAAQ,KAAK,MAAM,IAAI,MAAM;GAC7B,SAAS;GACT,OAAO;GACP,QAAQ;GACR,gBAAgB;GAChB,aAAa,qBAAqB,YAAY,KAAK,MAAM,IAAI,MAAM,OAAO,qBAAqB,EAAE;GAClG,CAAC;YACO,KAAK,SAAS,UAAU,WAAW;EAE5C,MAAM,gBAAgB,KAAK;AAG3B,MAAI,gBAAgB,IAAI,cAAc,CAAE;AAGxC,MAAI,kBAAkB,UAAU,KAAK,KAAK;GACxC,MAAM,UAAU,OAAO,KAAK,IAAI,YAAY,WAAW,KAAK,IAAI,UAAU;AAC1E,OAAI,oBAAoB,IAAI,QAAQ,CAAE;;AAIxC,MAAI,KAAK,KAAK;GACZ,MAAM,OAAO,OAAO,KAAK,IAAI,YAAY,WAAW,KAAK,IAAI,UAAU;GACvE,MAAM,UAAU,KAAK,OAAO,OAAO,KAAK,IAAI,YAAY,WAAW,KAAK,IAAI,UAAU;GAGtF,MAAM,cAAc,KAAK,MAAM,kBAAkB;AACjD,OAAI,cAAc,GAChB,SAAQ,KAAK;IACX,OAAO,YAAY;IACnB,MAAM,KAAK,IAAI,IAAI,MAAM,OAAO,qBAAqB;IACrD,QAAQ,KAAK,IAAI,IAAI,MAAM;IAC3B,SAAS;IACT,OAAO;IACP,QAAQ;IACR,gBAAgB,WAAW;IAC3B,aAAa,qBAAqB,YAAY,KAAK,IAAI,IAAI,MAAM,OAAO,qBAAqB,EAAE;IAChG,CAAC;;;;AAYV,IAAI,aAAiC;AAErC,eAAe,gBAA6C;AAC1D,KAAI,WAAY,QAAO;AACvB,KAAI;AAEF,gBADY,MAAM,OAAO,8BACR;AACjB,SAAO;SACD;AAEN,SAAO;;;;;;;;AASX,SAAS,sBAAsB,aAAqB,MAAuB;AACzE,KAAI,SAAS,MAAO,QAAO,YAAY,QAAQ,UAAU,OAAO;AAChE,KAAI,SAAS,KAAM,QAAO,YAAY,QAAQ,UAAU,MAAM;AAC9D,KAAI,SAAS,MAAO,QAAO,YAAY,QAAQ,UAAU,OAAO;AAChE,QAAO,YAAY,QAAQ,UAAU,MAAM;;AAG7C,SAAS,iBACP,eACA,iBACA,UACA,UACA,MACmB;AAMnB,QAHsB,SAAS,eADN,sBAAsB,UAAU,KAAK,CACC,CAG1C,KAAI,MAAK;AAC5B,IAAE,OAAO,EAAE,OAAO,kBAAkB;AACpC,IAAE,QAAQ;AACV,SAAO;GACP;;AAKJ,eAAsB,SAAS,SAAiB,UAA8C;CAC5F,MAAM,WAAW,MAAM,cAAc;CACrC,MAAM,UAA6B,EAAE;CAErC,MAAM,EAAE,eAAe,SAAS,MAAM,SAAS,EAAE,UAAU,UAAU,CAAC;AAGtE,KAAI,WAAW,UAAU;EACvB,MAAM,qBAAqB,WAAW,SAAS,IAAI,MAAM;EACzD,MAAM,cAAc,WAAW,SAAS;AAExC,MAAI,YACF,cAAa,aAAa,oBAAoB,SAAS,QAAQ;MAG/D,KAAI;GACF,MAAM,WAAW,SAAS,gBAAgB;IACxC,QAAQ,WAAW,SAAS;IAC5B,UAAU;IACV,IAAI;IACL,CAAC;AACF,OAAI,SAAS,IACX,cAAa,SAAS,KAAK,oBAAoB,SAAS,QAAQ;UAE5D;;CAQZ,MAAM,cAAc,WAAW,eAAe,WAAW;AACzD,KAAI,aAAa;EACf,MAAM,WAAW,MAAM,eAAe;AACtC,MAAI,UAAU;GACZ,MAAM,kBAAkB,YAAY,IAAI,MAAM;GAC9C,MAAM,gBAAgB,iBACpB,YAAY,SACZ,iBACA,UACA,UACA,YAAY,KACb;AACD,WAAQ,KAAK,GAAG,cAAc;;;AAMlC,QAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"mcpName": "io.github.Contentrain/contentrain",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Local-first MCP server for AI-generated content governance — 13 deterministic tools for any platform",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"simple-git": "^3.27.0",
|
|
108
108
|
"typescript": "^5.7.0",
|
|
109
109
|
"zod": "^3.24.0",
|
|
110
|
-
"@contentrain/types": "0.4.
|
|
110
|
+
"@contentrain/types": "0.4.2"
|
|
111
111
|
},
|
|
112
112
|
"optionalDependencies": {
|
|
113
113
|
"@astrojs/compiler": "^2.10.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apply-manager-Cf5OWOJ7.mjs","names":[],"sources":["../src/core/apply-manager.ts"],"sourcesContent":["import type { ModelDefinition, FieldDef, FileFramework } from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText, writeText, pathExists } from '../util/fs.js'\nimport { readModel, writeModel, listModels, validateModelDefinition } from './model-manager.js'\nimport { writeContent, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, type ContentEntry } from './content-manager.js'\nimport { readConfig } from './config.js'\nimport { writeContext } from './context.js'\nimport { createTransaction, buildBranchName } from '../git/transaction.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\n\n// ─── Types ───\n\nexport interface ExtractionEntry {\n model: string\n kind: 'singleton' | 'collection' | 'dictionary' | 'document'\n domain: string\n i18n?: boolean\n fields?: Record<string, FieldDef>\n entries: Array<{\n locale?: string\n slug?: string\n data: Record<string, unknown>\n source?: { file: string; line: number; value: string }\n sources?: Array<{ file: string; line: number; key: string; value: string }>\n }>\n}\n\nexport interface ExtractionInput {\n extractions: ExtractionEntry[]\n dry_run?: boolean\n}\n\nexport interface ExtractionPreview {\n models_to_create: string[]\n models_to_update: string[]\n total_entries: number\n content_files: string[]\n}\n\nexport interface ExtractionResult {\n dry_run: boolean\n preview?: ExtractionPreview\n error?: string\n validation_errors?: string[]\n results?: {\n models_created: string[]\n models_updated: string[]\n entries_written: number\n source_map: Array<{ model: string; locale: string; value: string; file: string; line: number }>\n }\n git?: { branch: string; action: string; commit: string }\n context_updated?: boolean\n next_steps: string[]\n}\n\nexport interface PatchEntry {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\nexport interface ReuseInput {\n scope: { model?: string; domain?: string }\n patches: PatchEntry[]\n dry_run?: boolean\n}\n\nexport interface SyntaxError {\n file: string\n error: string\n}\n\nexport interface ReuseResult {\n dry_run: boolean\n error?: string\n scope_warnings?: string[]\n preview?: {\n files_to_modify: string[]\n patches_count: number\n imports_to_add: number\n }\n results?: {\n files_modified: string[]\n patches_applied: number\n patches_skipped: Array<{ file: string; line: number; reason: string }>\n imports_added: number\n framework_warnings?: Array<{ file: string; warning: string }>\n syntax_errors?: SyntaxError[]\n }\n git?: { branch: string; action: string; commit: string }\n next_steps: string[]\n}\n\n// ─── Constants ───\n\nconst MAX_PATCHES = 100\n\n/** File extensions allowed for patching — scannable source files only */\nexport const PATCHABLE_EXTENSIONS = new Set([\n '.vue', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directories that must never be patched */\nconst FORBIDDEN_PATH_SEGMENTS = new Set([\n '.contentrain', 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',\n])\n\n// ─── Framework Detection (Guardrail #2) ───\n\nexport type { FileFramework } from '@contentrain/types'\n\nexport function detectFileFramework(filePath: string): FileFramework {\n const ext = extname(filePath).toLowerCase()\n switch (ext) {\n case '.vue': return 'vue'\n case '.svelte': return 'svelte'\n case '.tsx':\n case '.jsx': return 'jsx'\n case '.astro': return 'astro'\n case '.ts':\n case '.js':\n case '.mjs': return 'script'\n default: return 'script'\n }\n}\n\n/**\n * Validate that a replacement expression uses the correct template syntax\n * for the target file's framework. Returns a warning string or null.\n */\nexport function validateFrameworkExpression(\n filePath: string,\n newExpression: string,\n context: 'tag_text' | 'other',\n): string | null {\n if (context !== 'tag_text') return null\n\n const framework = detectFileFramework(filePath)\n\n switch (framework) {\n case 'vue':\n if (!newExpression.includes('{{')) {\n return `Vue file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{{\" — expected Vue template syntax like {{ $t('key') }}`\n }\n break\n case 'jsx':\n if (!newExpression.includes('{')) {\n return `JSX file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected JSX syntax like {t('key')}`\n }\n break\n case 'svelte':\n if (!newExpression.includes('{')) {\n return `Svelte file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Svelte syntax like {$t('key')}`\n }\n break\n case 'astro':\n if (!newExpression.includes('{')) {\n return `Astro file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Astro syntax like {t('key')}`\n }\n break\n case 'script':\n // Script files don't have template interpolation — warn if attempting tag text replacement\n return `Script file \"${filePath}\": tag text replacement not applicable for .ts/.js files`\n }\n\n return null\n}\n\n// ─── Scope Validation (Guardrail #1) ───\n\n/**\n * Validate that a patch file path is safe and within allowed scope.\n * Returns an error message or null if valid.\n */\nexport function validatePatchPath(filePath: string): string | null {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Reject path traversal\n if (normalizedPath.includes('..')) {\n return `Path traversal detected: \"${filePath}\"`\n }\n\n // Reject absolute paths\n if (normalizedPath.startsWith('/')) {\n return `Absolute path not allowed: \"${filePath}\"`\n }\n\n // Reject forbidden directories\n const segments = normalizedPath.split('/')\n for (const seg of segments) {\n if (FORBIDDEN_PATH_SEGMENTS.has(seg)) {\n return `Patching files inside \"${seg}/\" is not allowed: \"${filePath}\"`\n }\n }\n\n // Reject non-scannable extensions\n const ext = extname(filePath).toLowerCase()\n if (!PATCHABLE_EXTENSIONS.has(ext)) {\n return `File extension \"${ext}\" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(', ')}. Path: \"${filePath}\"`\n }\n\n return null\n}\n\n// ─── Syntax Check (Guardrail #5) ───\n\n/**\n * Perform a basic syntax check on a patched file.\n * Returns an error message or null if syntax appears valid.\n */\nexport function checkSyntax(filePath: string, content: string): string | null {\n const ext = extname(filePath).toLowerCase()\n\n switch (ext) {\n case '.ts':\n case '.tsx':\n case '.js':\n case '.jsx':\n case '.mjs':\n return checkJsSyntax(content)\n case '.vue':\n return checkVueSyntax(content)\n case '.svelte':\n case '.astro':\n return checkTagBalance(content)\n default:\n return null\n }\n}\n\n/**\n * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure.\n * This is intentionally conservative — it catches obvious breakage without\n * requiring a full parser.\n */\nfunction checkJsSyntax(content: string): string | null {\n const stack: string[] = []\n const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }\n let inString: string | null = null\n let escaped = false\n let inLineComment = false\n let inBlockComment = false\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i]!\n const next = content[i + 1]\n\n // Handle escape sequences inside strings\n if (escaped) {\n escaped = false\n continue\n }\n\n if (ch === '\\\\' && inString !== null) {\n escaped = true\n continue\n }\n\n // Line comment\n if (!inString && !inBlockComment && ch === '/' && next === '/') {\n inLineComment = true\n continue\n }\n if (inLineComment) {\n if (ch === '\\n') inLineComment = false\n continue\n }\n\n // Block comment\n if (!inString && !inBlockComment && ch === '/' && next === '*') {\n inBlockComment = true\n i++ // skip *\n continue\n }\n if (inBlockComment) {\n if (ch === '*' && next === '/') {\n inBlockComment = false\n i++ // skip /\n }\n continue\n }\n\n // String handling\n if (inString !== null) {\n if (ch === inString) {\n // Template literal allows multi-line, others don't\n inString = null\n } else if (inString !== '`' && ch === '\\n') {\n return `Unterminated string literal near offset ${i}`\n }\n continue\n }\n\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n continue\n }\n\n // Bracket matching\n if (ch === '(' || ch === '[' || ch === '{') {\n stack.push(ch)\n } else if (ch === ')' || ch === ']' || ch === '}') {\n const expected = pairs[ch]!\n if (stack.length === 0) {\n return `Unmatched closing \"${ch}\" near offset ${i}`\n }\n const top = stack.pop()!\n if (top !== expected) {\n return `Mismatched bracket: expected closing for \"${top}\" but found \"${ch}\" near offset ${i}`\n }\n }\n }\n\n if (inString !== null) {\n return `Unterminated string literal (opened with ${inString})`\n }\n\n if (stack.length > 0) {\n return `Unclosed bracket \"${stack[stack.length - 1]}\" — ${stack.length} unclosed bracket(s)`\n }\n\n return null\n}\n\n/**\n * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced.\n */\nfunction checkVueSyntax(content: string): string | null {\n const tagBalance = checkTagBalance(content)\n if (tagBalance) return tagBalance\n\n // Vue-specific: check that SFC root tags are present and balanced\n for (const tag of ['template', 'script']) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n/**\n * Basic tag balance check for HTML-like files.\n * Checks that self-closing tags are handled and major structural tags are balanced.\n */\nfunction checkTagBalance(content: string): string | null {\n // Check for common structural tags balance\n const structuralTags = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'ul', 'ol', 'table']\n\n for (const tag of structuralTags) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n// ─── Extract Mode ───\n\nexport async function applyExtract(\n projectRoot: string,\n input: ExtractionInput,\n): Promise<ExtractionResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { extractions, dry_run } = input\n\n // Analyze what will happen\n const existingModels = await listModels(projectRoot)\n const existingIds = new Set(existingModels.map(m => m.id))\n\n const modelsToCreate: string[] = []\n const modelsToUpdate: string[] = []\n const contentFiles: string[] = []\n let totalEntries = 0\n\n const validationErrors: string[] = []\n\n for (const ext of extractions) {\n // Validate model definition with same rules as model_save\n const modelErrors = validateModelDefinition({\n id: ext.model,\n kind: ext.kind,\n fields: ext.fields as Record<string, unknown> | undefined,\n })\n if (modelErrors.length > 0) {\n validationErrors.push(...modelErrors.map(e => `[${ext.model}] ${e}`))\n }\n\n for (const entry of ext.entries) {\n if (ext.kind === 'dictionary') {\n if (entry.data['id'] !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`)\n }\n for (const [key, val] of Object.entries(entry.data)) {\n if (typeof val !== 'string') {\n validationErrors.push(`[${ext.model}] Dictionary entry value for key \"${key}\" must be a string, got ${typeof val}`)\n }\n }\n } else if (ext.kind === 'document') {\n if (!entry.slug && !entry.data['slug']) {\n validationErrors.push(`[${ext.model}] Document entries must have a slug`)\n }\n } else if (ext.kind === 'collection') {\n if (entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Collection entries should not have slug`)\n }\n } else if (ext.kind === 'singleton') {\n if (entry.data['id'] !== undefined || entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`)\n }\n }\n }\n\n if (existingIds.has(ext.model)) {\n modelsToUpdate.push(ext.model)\n } else {\n modelsToCreate.push(ext.model)\n }\n totalEntries += ext.entries.length\n\n // Guardrail #3: Preview-Execute Parity — use real model metadata if it exists\n let previewModel: ModelDefinition\n if (existingIds.has(ext.model)) {\n const real = await readModel(projectRoot, ext.model)\n if (real) {\n previewModel = real\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n\n const cDir = resolveContentDir(projectRoot, previewModel)\n for (const entry of ext.entries) {\n const locale = entry.locale ?? config.locales.default\n if (ext.kind === 'document' && entry.slug) {\n contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug))\n } else {\n contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale))\n }\n }\n }\n\n const preview: ExtractionPreview = {\n models_to_create: modelsToCreate,\n models_to_update: modelsToUpdate,\n total_entries: totalEntries,\n content_files: [...new Set(contentFiles)],\n }\n\n // Dry run — return preview only (include validation errors if any)\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview,\n ...(validationErrors.length > 0 ? { validation_errors: validationErrors } : {}),\n next_steps: [\n ...(validationErrors.length > 0\n ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`]\n : []),\n 'Review the preview above',\n 'Call contentrain_apply with mode:extract and dry_run:false to execute',\n ],\n }\n }\n\n // Block execute if validation errors exist\n if (validationErrors.length > 0) {\n return {\n dry_run: false,\n error: 'Model validation failed — cannot execute extract with invalid model definitions',\n validation_errors: validationErrors,\n next_steps: ['Fix the validation errors and retry'],\n }\n }\n\n // Branch health gate\n const health = await checkBranchHealth(projectRoot)\n if (health.blocked) {\n return {\n error: health.message,\n action: 'blocked' as const,\n hint: 'Merge or delete old contentrain/* branches before executing normalize.',\n } as unknown as ExtractionResult\n }\n\n // Execute — git transaction (always review mode for normalize)\n const branchName = buildBranchName('normalize', 'extract')\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n const sourceMap: Array<{ model: string; locale: string; value: string; file: string; line: number }> = []\n const modelsCreated: string[] = []\n const modelsUpdated: string[] = []\n let entriesWritten = 0\n\n try {\n await tx.write(async (wt) => {\n for (const ext of extractions) {\n // Create or merge model\n const existing = await readModel(wt, ext.model)\n if (existing) {\n // Merge fields: add new, keep existing\n if (ext.fields) {\n const merged = { ...existing.fields, ...ext.fields }\n // Only overwrite if new fields were actually added\n const newFieldNames = Object.keys(ext.fields).filter(k => !(k in (existing.fields ?? {})))\n if (newFieldNames.length > 0) {\n existing.fields = merged\n await writeModel(wt, existing)\n modelsUpdated.push(ext.model)\n }\n }\n } else {\n // Create new model\n const newModel: ModelDefinition = {\n id: ext.model,\n name: ext.model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),\n kind: ext.kind,\n domain: ext.domain,\n i18n: ext.i18n ?? true,\n fields: ext.fields,\n }\n await writeModel(wt, newModel)\n modelsCreated.push(ext.model)\n }\n\n // Write content entries\n const model = (await readModel(wt, ext.model))!\n const entries: ContentEntry[] = ext.entries.map(e => ({\n locale: e.locale,\n slug: e.slug,\n data: e.data,\n }))\n\n const wtConfig = await readConfig(wt) ?? config\n await writeContent(wt, model, entries, wtConfig)\n entriesWritten += entries.length\n\n // Track source map\n for (const entry of ext.entries) {\n // For dictionary entries with per-key source tracking\n if (ext.kind === 'dictionary' && entry.sources) {\n for (const s of entry.sources) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: s.value,\n file: s.file,\n line: s.line,\n })\n }\n } else if (entry.source) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: entry.source.value,\n file: entry.source.file,\n line: entry.source.line,\n })\n }\n }\n }\n\n // Write source map for reuse scope enforcement\n if (sourceMap.length > 0) {\n const sourcesByModel: Record<string, { source_files: string[]; entry_count: number }> = {}\n for (const s of sourceMap) {\n if (!sourcesByModel[s.model]) {\n sourcesByModel[s.model] = { source_files: [], entry_count: 0 }\n }\n const modelEntry = sourcesByModel[s.model]!\n if (!modelEntry.source_files.includes(s.file)) {\n modelEntry.source_files.push(s.file)\n }\n modelEntry.entry_count++\n }\n const sourcesJson = JSON.stringify({\n version: 1,\n created_at: new Date().toISOString(),\n models: sourcesByModel,\n }, null, 2) + '\\n'\n // Write to worktree only — merge brings it to main\n // Phase 2 (reuse) must wait for extract branch to be merged first\n await writeText(join(wt, '.contentrain', 'normalize-sources.json'), sourcesJson)\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: extractions.map(e => e.model).join(','),\n locale: config.locales.default,\n entries: extractions.flatMap(e => e.entries.map(en => en.slug ?? 'entry')),\n })\n })\n\n const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`\n await tx.commit(commitMsg)\n\n const gitResult = { branch: branchName, action: 'pending-review', commit: '' }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n } catch {\n gitResult.action = 'pending-review'\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n models_created: modelsCreated,\n models_updated: modelsUpdated,\n entries_written: entriesWritten,\n source_map: sourceMap,\n },\n git: gitResult,\n context_updated: true,\n next_steps: [\n 'Run contentrain_validate to check the extracted content',\n 'Run contentrain_submit to push the branch for review',\n 'After review, proceed with mode:reuse to patch source files',\n ],\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Reuse Mode ───\n\nexport async function applyReuse(\n projectRoot: string,\n input: ReuseInput,\n): Promise<ReuseResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { scope, patches, dry_run } = input\n\n // Validate scope\n if (!scope.model && !scope.domain) {\n throw new Error('Scope required: provide model or domain. Whole-project patching is not allowed.')\n }\n\n // Validate patch count\n if (patches.length > MAX_PATCHES) {\n throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`)\n }\n\n // Check content exists for scope (soft warning)\n if (scope.model) {\n const model = await readModel(projectRoot, scope.model)\n if (!model) {\n throw new Error(`Model \"${scope.model}\" not found. Run extract phase first.`)\n }\n }\n\n const scopeWarnings: string[] = []\n\n // Guardrail #1: Scope Real Enforcement\n // Step 1: Path safety — every patch must target a valid, patchable source file\n for (const patch of patches) {\n const pathError = validatePatchPath(patch.file)\n if (pathError) {\n throw new Error(`Invalid patch path: ${pathError}`)\n }\n }\n\n // Step 2: Semantic scope — verify scope model/domain exists and cross-check patch files\n if (scope.model || scope.domain) {\n const models = await listModels(projectRoot)\n const scopeModels = scope.model\n ? models.filter(m => m.id === scope.model)\n : scope.domain\n ? models.filter(m => m.domain === scope.domain)\n : models\n\n if (scopeModels.length === 0) {\n throw new Error(`No models found for scope ${scope.model ? `model=\"${scope.model}\"` : `domain=\"${scope.domain}\"`}`)\n }\n\n // Step 3: Verify patch files are source files (not content/config/meta files)\n // and belong to detectable source directories (not random locations)\n const { autoDetectSourceDirs } = await import('./scan-config.js')\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n\n // Build allowed file prefixes from source dirs\n const allowedPrefixes = sourceDirs.map(d => d === '.' ? '' : d + '/')\n\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n\n // Reject patches targeting .contentrain/ directory (content files should never be patched by reuse)\n if (normalizedPath.startsWith('.contentrain/') || normalizedPath.includes('/.contentrain/')) {\n throw new Error(`Cannot patch content/config files directly: \"${patch.file}\". Reuse patches source files only.`)\n }\n\n // If source dirs were detected (not just \".\"), verify patch files are within them\n if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {\n const inSourceDir = allowedPrefixes.some(prefix =>\n prefix === '' || normalizedPath.startsWith(prefix),\n )\n if (!inSourceDir) {\n throw new Error(\n `Patch file \"${patch.file}\" is outside detected source directories (${sourceDirs.join(', ')}). ` +\n `Reuse patches must target source files within the project's source tree.`,\n )\n }\n }\n }\n\n // Step 4: Semantic source→model cross-check via normalize-sources.json\n // Source map is written by extract into the review branch worktree.\n // It only exists on base after extract branch is merged.\n // If missing: dry_run proceeds with warning, execute is blocked.\n if (scope.model || scope.domain) {\n const sourcesPath = join(projectRoot, '.contentrain', 'normalize-sources.json')\n const sourcesRaw = await readText(sourcesPath)\n\n if (!sourcesRaw) {\n // Source map not found — check if scoped model is a dictionary\n // Dictionaries store all keys in one entry, so per-file source tracking\n // is not available. Allow reuse with a warning instead of blocking.\n const scopedModel = scopeModels.find(m => m.id === scope.model)\n const isDictionary = scopedModel?.kind === 'dictionary'\n\n if (isDictionary) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Dictionary models do not generate per-file source maps. ' +\n 'Scope enforcement is based on source-tree locality only.',\n )\n } else if (dry_run !== false) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Semantic scope enforcement is unavailable. ' +\n 'Merge the extract branch first, then reuse will have full scope protection.',\n )\n } else {\n throw new Error(\n 'Cannot execute reuse: normalize-sources.json not found on base branch. ' +\n 'The extract branch must be merged before reuse can execute. ' +\n 'This ensures semantic scope enforcement protects against out-of-scope patching.',\n )\n }\n } else {\n const sourcesData = JSON.parse(sourcesRaw) as {\n models?: Record<string, { source_files?: string[] }>\n }\n\n // Collect all allowed source files for the scope\n let allowedSourceFiles: string[] = []\n let scopeLabel = ''\n\n if (scope.model) {\n const modelSources = sourcesData.models?.[scope.model]?.source_files\n if (modelSources) allowedSourceFiles = modelSources\n scopeLabel = `model \"${scope.model}\"`\n } else if (scope.domain) {\n const domainModelIds = scopeModels.map(m => m.id)\n for (const modelId of domainModelIds) {\n const modelSources = sourcesData.models?.[modelId]?.source_files\n if (modelSources) allowedSourceFiles.push(...modelSources)\n }\n allowedSourceFiles = [...new Set(allowedSourceFiles)]\n scopeLabel = `domain \"${scope.domain}\"`\n }\n\n if (allowedSourceFiles.length > 0) {\n const outOfScopePatches: string[] = []\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n if (!allowedSourceFiles.includes(normalizedPath)) {\n outOfScopePatches.push(patch.file)\n }\n }\n if (outOfScopePatches.length > 0) {\n throw new Error(\n `Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. ` +\n `Out-of-scope files: ${outOfScopePatches.join(', ')}. ` +\n `Known source files: ${allowedSourceFiles.join(', ')}.`,\n )\n }\n }\n }\n }\n }\n\n // Group patches by file\n const patchesByFile = new Map<string, PatchEntry[]>()\n for (const patch of patches) {\n if (!patchesByFile.has(patch.file)) {\n patchesByFile.set(patch.file, [])\n }\n patchesByFile.get(patch.file)!.push(patch)\n }\n\n const filesToModify = [...patchesByFile.keys()]\n const importsToAdd = patches.filter(p => p.import_statement).length\n\n // Dry run — return preview only\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview: {\n files_to_modify: filesToModify,\n patches_count: patches.length,\n imports_to_add: importsToAdd,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n next_steps: [\n ...(scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : []),\n 'Review the files and patches above',\n 'Call contentrain_apply with mode:reuse and dry_run:false to execute',\n ],\n }\n }\n\n // Branch health gate\n const reuseHealth = await checkBranchHealth(projectRoot)\n if (reuseHealth.blocked) {\n return {\n dry_run: false,\n error: `Branch blocked: ${reuseHealth.message}`,\n next_steps: ['Merge or delete old contentrain/* branches before executing reuse.'],\n }\n }\n\n // Execute — git transaction\n const scopeTarget = scope.model ?? scope.domain!\n const branchName = buildBranchName('normalize/reuse', scopeTarget)\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n\n const filesModified: string[] = []\n let patchesApplied = 0\n let importsAdded = 0\n const patchesSkipped: Array<{ file: string; line: number; reason: string }> = []\n const frameworkWarnings: Array<{ file: string; warning: string }> = []\n const syntaxErrors: SyntaxError[] = []\n\n try {\n await tx.write(async (wt) => {\n for (const [relFile, filePatches] of patchesByFile) {\n const absPath = join(wt, relFile)\n\n if (!(await pathExists(absPath))) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file not found' })\n }\n continue\n }\n\n const content = await readText(absPath)\n if (content === null) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file unreadable' })\n }\n continue\n }\n\n // Sort patches by line DESC (bottom-up to avoid line shifts)\n const sorted = [...filePatches].toSorted((a, b) => b.line - a.line)\n\n const lines = content.split('\\n')\n let fileModified = false\n\n for (const patch of sorted) {\n // Determine replacement context before applying\n const targetLine = lines[patch.line - 1] ?? ''\n const isTagTextContext = targetLine.includes(`>${patch.old_value}<`)\n const replacementContext = isTagTextContext ? 'tag_text' as const : 'other' as const\n\n // Guardrail #2: Framework-aware validation — only warn for tag text replacements\n if (isTagTextContext) {\n const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext)\n if (fwWarning) {\n frameworkWarnings.push({ file: relFile, warning: fwWarning })\n }\n }\n\n const applied = applyPatchToLines(lines, patch)\n if (applied) {\n patchesApplied++\n fileModified = true\n } else {\n patchesSkipped.push({ file: relFile, line: patch.line, reason: 'old_value not found at or near specified line' })\n }\n }\n\n // Add imports (deduplicate)\n const importStatements = new Set(\n filePatches\n .filter(p => p.import_statement)\n .map(p => p.import_statement!),\n )\n\n if (importStatements.size > 0) {\n const added = addImportsToLines(lines, importStatements)\n importsAdded += added\n if (added > 0) fileModified = true\n }\n\n if (fileModified) {\n const newContent = lines.join('\\n')\n await writeText(absPath, newContent)\n filesModified.push(relFile)\n\n // Guardrail #5: Syntax check after patching\n const syntaxError = checkSyntax(relFile, newContent)\n if (syntaxError) {\n syntaxErrors.push({ file: relFile, error: syntaxError })\n }\n }\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: scopeTarget,\n locale: config.locales.default,\n })\n })\n\n if (filesModified.length === 0) {\n await tx.cleanup()\n return {\n dry_run: false,\n results: {\n files_modified: [],\n patches_applied: 0,\n patches_skipped: patchesSkipped,\n imports_added: 0,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n },\n next_steps: ['No files were modified. Check patch definitions and try again.'],\n }\n }\n\n const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`\n await tx.commit(commitMsg)\n\n const gitResult = { branch: branchName, action: 'pending-review', commit: '' }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n } catch {\n gitResult.action = 'pending-review'\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n files_modified: filesModified,\n patches_applied: patchesApplied,\n patches_skipped: patchesSkipped,\n imports_added: importsAdded,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : undefined,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n git: gitResult,\n next_steps: [\n 'Run contentrain_validate to verify the patched files',\n patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : '',\n syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : '',\n scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : '',\n 'Run contentrain_submit to push the branch for review',\n ].filter(Boolean),\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Patch Helpers ───\n\n/**\n * Apply a single patch to a lines array. Mutates lines in place.\n * Uses line hint for proximity matching — searches ±10 lines from hint.\n */\nfunction applyPatchToLines(lines: string[], patch: PatchEntry): boolean {\n const { line, old_value, new_expression } = patch\n const lineIdx = line - 1 // 0-based\n\n // Search range: ±10 lines from hint\n const searchStart = Math.max(0, lineIdx - 10)\n const searchEnd = Math.min(lines.length, lineIdx + 11)\n\n // First pass: exact line match\n if (lineIdx >= 0 && lineIdx < lines.length) {\n const replaced = replaceInLine(lines[lineIdx]!, old_value, new_expression)\n if (replaced !== null) {\n lines[lineIdx] = replaced\n return true\n }\n }\n\n // Second pass: proximity search\n for (let i = searchStart; i < searchEnd; i++) {\n if (i === lineIdx) continue // already tried\n const replaced = replaceInLine(lines[i]!, old_value, new_expression)\n if (replaced !== null) {\n lines[i] = replaced\n return true\n }\n }\n\n return false\n}\n\n/**\n * Replace old_value with new_expression in a single line.\n * Matches the string literal (quoted or unquoted tag text).\n * Returns the modified line, or null if not found.\n *\n * Guardrail #4: Safer patch matching — word boundary awareness and\n * ambiguity rejection for plain text fallback.\n */\nexport function replaceInLine(line: string, oldValue: string, newExpression: string): string | null {\n // Try exact match of the value in quoted strings\n // Match: \"old_value\", 'old_value', `old_value`\n for (const quote of ['\"', \"'\", '`']) {\n const quoted = `${quote}${oldValue}${quote}`\n if (line.includes(quoted)) {\n return line.replace(quoted, newExpression)\n }\n }\n\n // Try unquoted tag text match: >old_value<\n // The agent provides the complete new_expression with correct framework syntax:\n // JSX: {t('key')}\n // Vue: {{ $t('key') }}\n // Svelte: {$t('key')}\n // So we insert the expression as-is between > and <, without wrapping in braces.\n if (line.includes(`>${oldValue}<`)) {\n return line.replace(`>${oldValue}<`, `>${newExpression}<`)\n }\n\n // Guardrail #4: Safer plain text fallback\n // Only match if old_value appears at a word boundary and is unambiguous\n if (line.includes(oldValue)) {\n // Count occurrences — if multiple, it's ambiguous\n const occurrences = countOccurrences(line, oldValue)\n if (occurrences > 1) {\n return null // ambiguous — let proximity search handle it\n }\n\n // Word boundary check: don't replace \"Submit\" inside \"SubmitButton\"\n // Reject if the old_value is a substring of a larger word — check if\n // adjacent characters are word characters that extend the token.\n const idx = line.indexOf(oldValue)\n const charBefore = idx > 0 ? line[idx - 1]! : ''\n const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length]! : ''\n\n const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]!)\n const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]!)\n\n // If old_value starts with a word char and char before is also word char, it's a partial match\n if (oldStartsWithWord && isWordChar(charBefore)) {\n return null\n }\n // If old_value ends with a word char and char after is also word char, it's a partial match\n if (oldEndsWithWord && isWordChar(charAfter)) {\n return null\n }\n\n return line.replace(oldValue, newExpression)\n }\n\n return null\n}\n\n/** Count non-overlapping occurrences of a substring */\nfunction countOccurrences(str: string, sub: string): number {\n let count = 0\n let pos = 0\n while (pos <= str.length - sub.length) {\n const idx = str.indexOf(sub, pos)\n if (idx === -1) break\n count++\n pos = idx + sub.length\n }\n return count\n}\n\n/** Check if a character is a word character (letter, digit, underscore) */\nfunction isWordChar(ch: string): boolean {\n return /\\w/.test(ch)\n}\n\n/**\n * Add import statements to the top of a file (after existing imports).\n * Deduplicates — won't add if the import already exists.\n * Returns number of imports actually added.\n */\nfunction addImportsToLines(lines: string[], imports: Set<string>): number {\n let added = 0\n const existingContent = lines.join('\\n')\n\n // Find the last import line position (handles multi-line imports)\n let lastImportIdx = -1\n let inMultiLineImport = false\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (inMultiLineImport) {\n lastImportIdx = i\n if (trimmed.includes('}')) inMultiLineImport = false\n continue\n }\n if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {\n lastImportIdx = i\n // Check if this is a multi-line import (has { but no closing })\n if (trimmed.includes('{') && !trimmed.includes('}')) {\n inMultiLineImport = true\n }\n continue\n }\n // Stop searching after a non-import, non-empty, non-comment line following imports\n if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {\n break\n }\n }\n\n let insertAt: number\n if (lastImportIdx >= 0) {\n insertAt = lastImportIdx + 1\n } else {\n // No existing imports found — insert after shebang and directive lines\n insertAt = 0\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (i === 0 && trimmed.startsWith('#!')) {\n insertAt = i + 1\n continue\n }\n if (trimmed === \"'use client'\" || trimmed === '\"use client\"'\n || trimmed === \"'use server'\" || trimmed === '\"use server\"'\n || trimmed === \"'use client';\" || trimmed === '\"use client\";'\n || trimmed === \"'use server';\" || trimmed === '\"use server\";') {\n insertAt = i + 1\n continue\n }\n if (insertAt > 0 && trimmed.length > 0) break\n if (insertAt === 0 && trimmed.length > 0) break\n }\n }\n const toInsert: string[] = []\n\n for (const imp of imports) {\n // Check if this import already exists (by checking the from clause)\n if (!existingContent.includes(imp)) {\n toInsert.push(imp)\n added++\n }\n }\n\n if (toInsert.length > 0) {\n lines.splice(insertAt, 0, ...toInsert)\n }\n\n return added\n}\n"],"mappings":"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"}
|