@contentrain/mcp 1.0.7 → 1.1.1
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 +13 -2
- package/dist/{apply-manager-BDtHM8z-.mjs → apply-manager-Cf5OWOJ7.mjs} +7 -7
- package/dist/{apply-manager-BDtHM8z-.mjs.map → apply-manager-Cf5OWOJ7.mjs.map} +1 -1
- package/dist/{branch-lifecycle-DB6sgeJi.mjs → branch-lifecycle-wGJg25LT.mjs} +31 -12
- package/dist/branch-lifecycle-wGJg25LT.mjs.map +1 -0
- package/dist/{config-CPlhDwsv.mjs → config-B5kc2oeV.mjs} +2 -2
- package/dist/{config-CPlhDwsv.mjs.map → config-B5kc2oeV.mjs.map} +1 -1
- package/dist/{context-CUFulSi2.mjs → context-DIDt_TxX.mjs} +4 -4
- package/dist/{context-CUFulSi2.mjs.map → context-DIDt_TxX.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +10 -5
- package/dist/core/config.mjs +3 -1
- package/dist/core/content-manager.d.mts +1 -9
- package/dist/core/content-manager.d.mts.map +1 -1
- package/dist/core/content-manager.mjs +4 -1
- package/dist/core/context.mjs +6 -2
- package/dist/core/graph-builder.mjs +4 -1
- package/dist/core/meta-manager.mjs +3 -1
- package/dist/core/model-manager.mjs +4 -1
- package/dist/core/scan-config.mjs +3 -1
- package/dist/core/scanner.mjs +4 -1
- package/dist/core/validator.d.mts.map +1 -1
- package/dist/core/validator.mjs +6 -2
- package/dist/{detect-C8XmJ4S6.mjs → detect-DinOQAmK.mjs} +2 -2
- package/dist/{detect-C8XmJ4S6.mjs.map → detect-DinOQAmK.mjs.map} +1 -1
- package/dist/{fs-BbuGnVcG.mjs → fs-BnIQ4dKj.mjs} +2 -2
- package/dist/{fs-BbuGnVcG.mjs.map → fs-BnIQ4dKj.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts.map +1 -1
- package/dist/git/branch-lifecycle.mjs +4 -1
- package/dist/git/transaction.d.mts +7 -3
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +9 -4
- package/dist/{graph-builder-fPrDDwnu.mjs → graph-builder-tM7zj-SX.mjs} +3 -3
- package/dist/{graph-builder-fPrDDwnu.mjs.map → graph-builder-tM7zj-SX.mjs.map} +1 -1
- package/dist/{id-BdD4HvMY.mjs → id-DV_T9Ic8.mjs} +3 -5
- package/dist/id-DV_T9Ic8.mjs.map +1 -0
- package/dist/index.mjs +16 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{meta-manager-CECrfsKm.mjs → meta-manager--0IwAPCl.mjs} +2 -2
- package/dist/{meta-manager-CECrfsKm.mjs.map → meta-manager--0IwAPCl.mjs.map} +1 -1
- package/dist/{model-manager-kl78AAMg.mjs → model-manager-DW_fOGRM.mjs} +16 -186
- package/dist/model-manager-DW_fOGRM.mjs.map +1 -0
- package/dist/{scan-config-AJGxCdNd.mjs → scan-config-QgnJ1-52.mjs} +2 -2
- package/dist/{scan-config-AJGxCdNd.mjs.map → scan-config-QgnJ1-52.mjs.map} +1 -1
- package/dist/{scanner-BdTiv8sw.mjs → scanner-DPrbU-oK.mjs} +3 -3
- package/dist/{scanner-BdTiv8sw.mjs.map → scanner-DPrbU-oK.mjs.map} +1 -1
- package/dist/serializer-CnHTtmc1.mjs +2 -0
- package/dist/{server-QgivXkem.mjs → server-BXguCuJb.mjs} +62 -47
- package/dist/server-BXguCuJb.mjs.map +1 -0
- package/dist/server.mjs +16 -9
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-3HWi-PTu.mjs → templates-bRPzYq4M.mjs} +1 -1
- package/dist/{templates-3HWi-PTu.mjs.map → templates-bRPzYq4M.mjs.map} +1 -1
- package/dist/transaction-B0V0_SE2.mjs +288 -0
- package/dist/transaction-B0V0_SE2.mjs.map +1 -0
- package/dist/util/detect.mjs +3 -1
- package/dist/util/fs.mjs +2 -1
- package/dist/util/id.d.mts +2 -1
- package/dist/util/id.d.mts.map +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/util/serializer.d.mts +2 -14
- package/dist/util/serializer.mjs +2 -2
- package/dist/{validator-CyckZAVr.mjs → validator-SQYUV2JP.mjs} +23 -197
- package/dist/validator-SQYUV2JP.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/branch-lifecycle-DB6sgeJi.mjs.map +0 -1
- package/dist/id-BdD4HvMY.mjs.map +0 -1
- package/dist/model-manager-kl78AAMg.mjs.map +0 -1
- package/dist/serializer-Dueih-i2.mjs +0 -33
- package/dist/serializer-Dueih-i2.mjs.map +0 -1
- package/dist/server-QgivXkem.mjs.map +0 -1
- package/dist/transaction-_AiXqGuD.mjs +0 -143
- package/dist/transaction-_AiXqGuD.mjs.map +0 -1
- package/dist/util/serializer.d.mts.map +0 -1
- package/dist/validator-CyckZAVr.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner-BdTiv8sw.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\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\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst CSS_CLASS_PREFIX_RE = /^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|p-|m-|w-|h-|bg-|text-|border-|rounded|shadow|gap-|space-|font-|leading-|tracking-|opacity-|z-|overflow-|cursor-|transition|transform|animate-|hover:|focus:|active:|dark:|sm:|md:|lg:|xl:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n if (/^(flex|grid|block|inline|hidden|container|relative|absolute|fixed|sticky|static|isolate|overflow-hidden|overflow-auto|overflow-scroll|underline|italic|uppercase|lowercase|capitalize|truncate|break-words|break-all|whitespace-nowrap|sr-only|not-sr-only)$/.test(token)) return true\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scanner-DPrbU-oK.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\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\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst CSS_CLASS_PREFIX_RE = /^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|p-|m-|w-|h-|bg-|text-|border-|rounded|shadow|gap-|space-|font-|leading-|tracking-|opacity-|z-|overflow-|cursor-|transition|transform|animate-|hover:|focus:|active:|dark:|sm:|md:|lg:|xl:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n if (/^(flex|grid|block|inline|hidden|container|relative|absolute|fixed|sticky|static|isolate|overflow-hidden|overflow-auto|overflow-scroll|underline|italic|uppercase|lowercase|capitalize|truncate|break-words|break-all|whitespace-nowrap|sr-only|not-sr-only)$/.test(token)) return true\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-
|
|
2
|
-
import { n as readVocabulary, t as readConfig } from "./config-
|
|
3
|
-
import { n as readMeta, r as writeMeta } from "./meta-manager
|
|
4
|
-
import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-
|
|
5
|
-
import { t as readContext } from "./context-
|
|
6
|
-
import { t as detectStack } from "./detect-
|
|
7
|
-
import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-
|
|
8
|
-
import { n as listTemplates, t as getTemplate } from "./templates-
|
|
9
|
-
import { n as createTransaction, t as buildBranchName } from "./transaction-
|
|
10
|
-
import { t as validateProject } from "./validator-
|
|
11
|
-
import { t as buildGraph } from "./graph-builder-
|
|
12
|
-
import { n as scanCandidates, r as scanSummary } from "./scanner-
|
|
13
|
-
import { n as applyExtract, r as applyReuse } from "./apply-manager-
|
|
1
|
+
import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BnIQ4dKj.mjs";
|
|
2
|
+
import { n as readVocabulary, t as readConfig } from "./config-B5kc2oeV.mjs";
|
|
3
|
+
import { n as readMeta, r as writeMeta } from "./meta-manager--0IwAPCl.mjs";
|
|
4
|
+
import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-DW_fOGRM.mjs";
|
|
5
|
+
import { t as readContext } from "./context-DIDt_TxX.mjs";
|
|
6
|
+
import { t as detectStack } from "./detect-DinOQAmK.mjs";
|
|
7
|
+
import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-wGJg25LT.mjs";
|
|
8
|
+
import { n as listTemplates, t as getTemplate } from "./templates-bRPzYq4M.mjs";
|
|
9
|
+
import { n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-B0V0_SE2.mjs";
|
|
10
|
+
import { t as validateProject } from "./validator-SQYUV2JP.mjs";
|
|
11
|
+
import { t as buildGraph } from "./graph-builder-tM7zj-SX.mjs";
|
|
12
|
+
import { n as scanCandidates, r as scanSummary } from "./scanner-DPrbU-oK.mjs";
|
|
13
|
+
import { n as applyExtract, r as applyReuse } from "./apply-manager-Cf5OWOJ7.mjs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
18
19
|
import { simpleGit } from "simple-git";
|
|
19
20
|
//#region src/tools/context.ts
|
|
20
21
|
function registerContextTools(server, projectRoot) {
|
|
@@ -403,12 +404,13 @@ function registerSetupTools(server, projectRoot) {
|
|
|
403
404
|
});
|
|
404
405
|
await updateGitignore(wt);
|
|
405
406
|
});
|
|
406
|
-
await tx.commit("[contentrain] init: project setup"
|
|
407
|
-
const gitResult = await tx.complete({
|
|
407
|
+
await tx.commit("[contentrain] init: project setup", {
|
|
408
408
|
tool: "contentrain_init",
|
|
409
409
|
model: "",
|
|
410
410
|
locale: supportedLocales[0] ?? "en"
|
|
411
411
|
});
|
|
412
|
+
const gitResult = await tx.complete();
|
|
413
|
+
await ensureContentBranch(projectRoot);
|
|
412
414
|
return { content: [{
|
|
413
415
|
type: "text",
|
|
414
416
|
text: JSON.stringify({
|
|
@@ -432,7 +434,8 @@ function registerSetupTools(server, projectRoot) {
|
|
|
432
434
|
git: {
|
|
433
435
|
branch,
|
|
434
436
|
action: gitResult.action,
|
|
435
|
-
commit: gitResult.commit
|
|
437
|
+
commit: gitResult.commit,
|
|
438
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
436
439
|
},
|
|
437
440
|
next_steps: ["Create models with contentrain_model_save or contentrain_scaffold", "Use contentrain_scan to find hardcoded strings"]
|
|
438
441
|
}, null, 2)
|
|
@@ -503,12 +506,12 @@ function registerSetupTools(server, projectRoot) {
|
|
|
503
506
|
if (with_sample_content && tmpl.sample_content) contentCreated = await writeSampleContent(wt, tmpl, effectiveLocales, config);
|
|
504
507
|
if (tmpl.vocabulary) vocabAdded = await mergeVocabulary(wt, tmpl.vocabulary, effectiveLocales);
|
|
505
508
|
});
|
|
506
|
-
await tx.commit(`[contentrain] scaffold: ${templateId} (${defaultLocale})
|
|
507
|
-
const gitResult = await tx.complete({
|
|
509
|
+
await tx.commit(`[contentrain] scaffold: ${templateId} (${defaultLocale})`, {
|
|
508
510
|
tool: "contentrain_scaffold",
|
|
509
511
|
model: tmpl.models.map((m) => m.id).join(", "),
|
|
510
512
|
locale: defaultLocale
|
|
511
513
|
});
|
|
514
|
+
const gitResult = await tx.complete();
|
|
512
515
|
return { content: [{
|
|
513
516
|
type: "text",
|
|
514
517
|
text: JSON.stringify({
|
|
@@ -520,7 +523,8 @@ function registerSetupTools(server, projectRoot) {
|
|
|
520
523
|
git: {
|
|
521
524
|
branch,
|
|
522
525
|
action: gitResult.action,
|
|
523
|
-
commit: gitResult.commit
|
|
526
|
+
commit: gitResult.commit,
|
|
527
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
524
528
|
},
|
|
525
529
|
context_updated: true,
|
|
526
530
|
next_steps: [
|
|
@@ -689,11 +693,11 @@ function registerModelTools(server, projectRoot) {
|
|
|
689
693
|
await tx.write(async (wt) => {
|
|
690
694
|
await writeModel(wt, model);
|
|
691
695
|
});
|
|
692
|
-
await tx.commit(`[contentrain] ${action}: ${input.id}
|
|
693
|
-
const gitResult = await tx.complete({
|
|
696
|
+
await tx.commit(`[contentrain] ${action}: ${input.id}`, {
|
|
694
697
|
tool: "contentrain_model_save",
|
|
695
698
|
model: input.id
|
|
696
699
|
});
|
|
700
|
+
const gitResult = await tx.complete();
|
|
697
701
|
const defaultLocale = config.locales.default;
|
|
698
702
|
const contentDir = resolveContentDir(projectRoot, model);
|
|
699
703
|
const contentPath = model.content_path ?? `.contentrain/content/${input.domain}/${input.id}`;
|
|
@@ -717,7 +721,8 @@ function registerModelTools(server, projectRoot) {
|
|
|
717
721
|
git: {
|
|
718
722
|
branch,
|
|
719
723
|
action: gitResult.action,
|
|
720
|
-
commit: gitResult.commit
|
|
724
|
+
commit: gitResult.commit,
|
|
725
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
721
726
|
},
|
|
722
727
|
context_updated: true,
|
|
723
728
|
content_path: contentPath + "/",
|
|
@@ -786,11 +791,11 @@ function registerModelTools(server, projectRoot) {
|
|
|
786
791
|
await tx.write(async (wt) => {
|
|
787
792
|
filesRemoved = await deleteModel(wt, modelId);
|
|
788
793
|
});
|
|
789
|
-
await tx.commit(`[contentrain] delete: ${modelId}
|
|
790
|
-
const gitResult = await tx.complete({
|
|
794
|
+
await tx.commit(`[contentrain] delete: ${modelId}`, {
|
|
791
795
|
tool: "contentrain_model_delete",
|
|
792
796
|
model: modelId
|
|
793
797
|
});
|
|
798
|
+
const gitResult = await tx.complete();
|
|
794
799
|
return { content: [{
|
|
795
800
|
type: "text",
|
|
796
801
|
text: JSON.stringify({
|
|
@@ -800,7 +805,8 @@ function registerModelTools(server, projectRoot) {
|
|
|
800
805
|
git: {
|
|
801
806
|
branch,
|
|
802
807
|
action: gitResult.action,
|
|
803
|
-
commit: gitResult.commit
|
|
808
|
+
commit: gitResult.commit,
|
|
809
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
804
810
|
},
|
|
805
811
|
files_removed: filesRemoved,
|
|
806
812
|
context_updated: true
|
|
@@ -912,13 +918,13 @@ function registerContentTools(server, projectRoot) {
|
|
|
912
918
|
results = await writeContent(wt, model, input.entries, config);
|
|
913
919
|
entryIds = results.map((r) => r.id ?? r.slug ?? r.locale).filter(Boolean);
|
|
914
920
|
});
|
|
915
|
-
await tx.commit(`[contentrain] content: ${input.model}
|
|
916
|
-
const gitResult = await tx.complete({
|
|
921
|
+
await tx.commit(`[contentrain] content: ${input.model}`, {
|
|
917
922
|
tool: "contentrain_content_save",
|
|
918
923
|
model: input.model,
|
|
919
924
|
locale: input.entries[0]?.locale,
|
|
920
925
|
entries: entryIds
|
|
921
926
|
});
|
|
927
|
+
const gitResult = await tx.complete();
|
|
922
928
|
const validationResult = await validateProject(projectRoot, { model: input.model });
|
|
923
929
|
return { content: [{
|
|
924
930
|
type: "text",
|
|
@@ -929,7 +935,8 @@ function registerContentTools(server, projectRoot) {
|
|
|
929
935
|
git: {
|
|
930
936
|
branch,
|
|
931
937
|
action: gitResult.action,
|
|
932
|
-
commit: gitResult.commit
|
|
938
|
+
commit: gitResult.commit,
|
|
939
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
933
940
|
},
|
|
934
941
|
validation: {
|
|
935
942
|
valid: validationResult.valid,
|
|
@@ -999,12 +1006,12 @@ function registerContentTools(server, projectRoot) {
|
|
|
999
1006
|
keys: input.keys
|
|
1000
1007
|
});
|
|
1001
1008
|
});
|
|
1002
|
-
await tx.commit(`[contentrain] delete content: ${input.model}
|
|
1003
|
-
const gitResult = await tx.complete({
|
|
1009
|
+
await tx.commit(`[contentrain] delete content: ${input.model}`, {
|
|
1004
1010
|
tool: "contentrain_content_delete",
|
|
1005
1011
|
model: input.model,
|
|
1006
1012
|
locale: input.locale
|
|
1007
1013
|
});
|
|
1014
|
+
const gitResult = await tx.complete();
|
|
1008
1015
|
return { content: [{
|
|
1009
1016
|
type: "text",
|
|
1010
1017
|
text: JSON.stringify({
|
|
@@ -1015,7 +1022,8 @@ function registerContentTools(server, projectRoot) {
|
|
|
1015
1022
|
git: {
|
|
1016
1023
|
branch,
|
|
1017
1024
|
action: gitResult.action,
|
|
1018
|
-
commit: gitResult.commit
|
|
1025
|
+
commit: gitResult.commit,
|
|
1026
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
1019
1027
|
},
|
|
1020
1028
|
context_updated: true
|
|
1021
1029
|
}, null, 2)
|
|
@@ -1119,11 +1127,11 @@ function registerWorkflowTools(server, projectRoot) {
|
|
|
1119
1127
|
});
|
|
1120
1128
|
});
|
|
1121
1129
|
if (result.fixed > 0) {
|
|
1122
|
-
await tx.commit(`[contentrain] validate: auto-fix ${result.fixed} issue(s)
|
|
1123
|
-
const gitResult = await tx.complete({
|
|
1130
|
+
await tx.commit(`[contentrain] validate: auto-fix ${result.fixed} issue(s)`, {
|
|
1124
1131
|
tool: "contentrain_validate",
|
|
1125
1132
|
model: input.model ?? "*"
|
|
1126
1133
|
});
|
|
1134
|
+
const gitResult = await tx.complete();
|
|
1127
1135
|
const nextSteps = [];
|
|
1128
1136
|
if (result.summary.errors > 0) nextSteps.push("Fix remaining errors manually");
|
|
1129
1137
|
if (result.summary.warnings > 0) nextSteps.push("Review warnings");
|
|
@@ -1137,7 +1145,8 @@ function registerWorkflowTools(server, projectRoot) {
|
|
|
1137
1145
|
git: {
|
|
1138
1146
|
branch,
|
|
1139
1147
|
action: gitResult.action,
|
|
1140
|
-
commit: gitResult.commit
|
|
1148
|
+
commit: gitResult.commit,
|
|
1149
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
1141
1150
|
},
|
|
1142
1151
|
context_updated: true,
|
|
1143
1152
|
next_steps: nextSteps
|
|
@@ -1234,12 +1243,12 @@ function registerWorkflowTools(server, projectRoot) {
|
|
|
1234
1243
|
isError: true
|
|
1235
1244
|
};
|
|
1236
1245
|
branchesToPush = branchesToPush.filter((b) => unmergedSet.has(b));
|
|
1237
|
-
} else branchesToPush = branchSummary.all.filter((b) => b.startsWith("
|
|
1246
|
+
} else branchesToPush = branchSummary.all.filter((b) => b.startsWith("cr/") && unmergedSet.has(b));
|
|
1238
1247
|
if (branchesToPush.length === 0) return {
|
|
1239
1248
|
content: [{
|
|
1240
1249
|
type: "text",
|
|
1241
1250
|
text: JSON.stringify({
|
|
1242
|
-
error: "No unmerged
|
|
1251
|
+
error: "No unmerged cr/* branches found to push.",
|
|
1243
1252
|
next_steps: ["Make changes first with contentrain_content_save or contentrain_model_save"]
|
|
1244
1253
|
})
|
|
1245
1254
|
}],
|
|
@@ -1256,6 +1265,9 @@ function registerWorkflowTools(server, projectRoot) {
|
|
|
1256
1265
|
error: error instanceof Error ? error.message : String(error)
|
|
1257
1266
|
});
|
|
1258
1267
|
}
|
|
1268
|
+
try {
|
|
1269
|
+
await git.push(remoteName, CONTENTRAIN_BRANCH);
|
|
1270
|
+
} catch {}
|
|
1259
1271
|
const cleanup = await cleanupMergedBranches(projectRoot);
|
|
1260
1272
|
const nextSteps = [];
|
|
1261
1273
|
if (pushed.length > 0) nextSteps.push("Create PRs on your git platform for review");
|
|
@@ -1649,12 +1661,12 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1649
1661
|
copiedCount = 1;
|
|
1650
1662
|
}
|
|
1651
1663
|
});
|
|
1652
|
-
await tx.commit(`[contentrain] bulk: copy ${input.source_locale} → ${input.target_locale} for ${input.model}
|
|
1653
|
-
const gitResult = await tx.complete({
|
|
1664
|
+
await tx.commit(`[contentrain] bulk: copy ${input.source_locale} → ${input.target_locale} for ${input.model}`, {
|
|
1654
1665
|
tool: "contentrain_bulk",
|
|
1655
1666
|
model: input.model,
|
|
1656
1667
|
locale: input.target_locale
|
|
1657
1668
|
});
|
|
1669
|
+
const gitResult = await tx.complete();
|
|
1658
1670
|
return { content: [{
|
|
1659
1671
|
type: "text",
|
|
1660
1672
|
text: JSON.stringify({
|
|
@@ -1665,7 +1677,8 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1665
1677
|
git: {
|
|
1666
1678
|
branch,
|
|
1667
1679
|
action: gitResult.action,
|
|
1668
|
-
commit: gitResult.commit
|
|
1680
|
+
commit: gitResult.commit,
|
|
1681
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
1669
1682
|
},
|
|
1670
1683
|
context_updated: true
|
|
1671
1684
|
}, null, 2)
|
|
@@ -1731,12 +1744,12 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1731
1744
|
await Promise.all(updateOps);
|
|
1732
1745
|
}
|
|
1733
1746
|
});
|
|
1734
|
-
await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}
|
|
1735
|
-
const gitResult = await tx.complete({
|
|
1747
|
+
await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`, {
|
|
1736
1748
|
tool: "contentrain_bulk",
|
|
1737
1749
|
model: input.model,
|
|
1738
1750
|
entries: input.entry_ids
|
|
1739
1751
|
});
|
|
1752
|
+
const gitResult = await tx.complete();
|
|
1740
1753
|
return { content: [{
|
|
1741
1754
|
type: "text",
|
|
1742
1755
|
text: JSON.stringify({
|
|
@@ -1748,7 +1761,8 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1748
1761
|
git: {
|
|
1749
1762
|
branch,
|
|
1750
1763
|
action: gitResult.action,
|
|
1751
|
-
commit: gitResult.commit
|
|
1764
|
+
commit: gitResult.commit,
|
|
1765
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
1752
1766
|
},
|
|
1753
1767
|
context_updated: true
|
|
1754
1768
|
}, null, 2)
|
|
@@ -1798,12 +1812,12 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1798
1812
|
allRemoved.push(...removed);
|
|
1799
1813
|
}
|
|
1800
1814
|
});
|
|
1801
|
-
await tx.commit(`[contentrain] bulk: delete ${input.entry_ids.length} entries from ${input.model}
|
|
1802
|
-
const gitResult = await tx.complete({
|
|
1815
|
+
await tx.commit(`[contentrain] bulk: delete ${input.entry_ids.length} entries from ${input.model}`, {
|
|
1803
1816
|
tool: "contentrain_bulk",
|
|
1804
1817
|
model: input.model,
|
|
1805
1818
|
entries: input.entry_ids
|
|
1806
1819
|
});
|
|
1820
|
+
const gitResult = await tx.complete();
|
|
1807
1821
|
return { content: [{
|
|
1808
1822
|
type: "text",
|
|
1809
1823
|
text: JSON.stringify({
|
|
@@ -1815,7 +1829,8 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1815
1829
|
git: {
|
|
1816
1830
|
branch,
|
|
1817
1831
|
action: gitResult.action,
|
|
1818
|
-
commit: gitResult.commit
|
|
1832
|
+
commit: gitResult.commit,
|
|
1833
|
+
...gitResult.sync ? { sync: gitResult.sync } : {}
|
|
1819
1834
|
},
|
|
1820
1835
|
context_updated: true
|
|
1821
1836
|
}, null, 2)
|
|
@@ -1845,7 +1860,7 @@ function registerBulkTools(server, projectRoot) {
|
|
|
1845
1860
|
}
|
|
1846
1861
|
//#endregion
|
|
1847
1862
|
//#region package.json
|
|
1848
|
-
var version = "1.
|
|
1863
|
+
var version = "1.1.1";
|
|
1849
1864
|
//#endregion
|
|
1850
1865
|
//#region src/server.ts
|
|
1851
1866
|
function createServer(projectRoot) {
|
|
@@ -1865,4 +1880,4 @@ function createServer(projectRoot) {
|
|
|
1865
1880
|
//#endregion
|
|
1866
1881
|
export { createServer as t };
|
|
1867
1882
|
|
|
1868
|
-
//# sourceMappingURL=server-
|
|
1883
|
+
//# sourceMappingURL=server-BXguCuJb.mjs.map
|