@chemx/starter-kit 26.9.25-943 → 26.9.26-253

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/cli/index.js CHANGED
@@ -68,6 +68,7 @@ export const runWriterCli = async (...a) => (await import('./patcher.js')).ru
68
68
  export const writeFile = async (...a) => (await import('./patcher.js')).writeFile(...a);
69
69
  export const runTeamCli = async (...a) => (await import('./team/index.js')).runTeamCli(...a);
70
70
  export const runTrend = async (...a) => (await import('./trend.js')).runTrend(...a);
71
+ export const runLintAudit = async (...a) => (await import('./verify-lint.js')).runLintAudit(...a);
71
72
  export const runPillarsWizard = async (...a) => (await import('./pillars-wizard.js')).runPillarsWizard(...a);
72
73
  export const runTesseract = async (...a) => (await import('./tesseract.js')).runTesseract(...a);
73
74
  export { handleError, withErrorCatcher, publishIssue };
@@ -78,10 +79,11 @@ export { handleError, withErrorCatcher, publishIssue };
78
79
 
79
80
  export const ALLOWED_COMMANDS = new Set([
80
81
  'search', 'q', 'query', 'find',
82
+ 'trace', 'backtrace',
81
83
  'read', 'view', 'r',
82
84
  'patch', 'edit',
83
85
  'write',
84
- 'generate', 'g', 'gen', 'capsule', 'add',
86
+ 'generate', 'g', 'gen', 'capsule', 'add', 'jig',
85
87
  'explode', 'unpack',
86
88
  'audit',
87
89
  'trend', 'trends',
@@ -93,6 +95,7 @@ export const ALLOWED_COMMANDS = new Set([
93
95
  'mcp', 'mcp-server', 'server', 'install-mcp', 'setup-mcp',
94
96
  'build', 'run', 'wrap',
95
97
  'typecheck', 'check:types', 'tsc',
98
+ 'lint', 'check:lint', 'eslint',
96
99
  'test', 'tests', 'check:test',
97
100
  'check',
98
101
  'badge', 'badges',
@@ -98,15 +98,20 @@ elif command -v npx >/dev/null 2>&1; then
98
98
  fi
99
99
 
100
100
  if [ -n "\$AUDIT_BIN" ]; then
101
- printf "%s[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...%s\\n" "\$C_BLUE" "\$MIN_GRADE" "\$MIN_SCORE" "\$C_RESET"
102
- if ! \$AUDIT_BIN audit --git --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
101
+ if [ "\$CHEMX_VERBOSE" = "1" ]; then
102
+ printf "%s[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...%s\\n" "\$C_BLUE" "\$MIN_GRADE" "\$MIN_SCORE" "\$C_RESET"
103
+ fi
104
+ if ! AUDIT_OUT=\$(eval "\$AUDIT_BIN audit --git --min-grade=\$MIN_GRADE --min-score=\$MIN_SCORE --non-interactive" < /dev/null 2>&1); then
103
105
  printf "\\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
106
+ printf "%s\\n\\n" "\$AUDIT_OUT"
104
107
  printf "%sšŸ’” Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol%s\\n\\n" "\$C_CYAN" "\$C_RESET"
105
108
  exit 1
106
109
  fi
107
110
  fi
108
111
 
109
- printf "%sāœ” [Chemical X] Pre-commit architectural guardrails passed.%s\\n" "\$C_GREEN" "\$C_RESET"
112
+ if [ "\$CHEMX_VERBOSE" = "1" ]; then
113
+ printf "%sāœ” [Chemical X] Pre-commit architectural guardrails passed.%s\\n" "\$C_GREEN" "\$C_RESET"
114
+ fi
110
115
  exit 0
111
116
  `;
112
117
 
@@ -34,16 +34,20 @@ export const MASTER_MCP_TOOL = {
34
34
  'patterns',
35
35
  'issue',
36
36
  'project',
37
- 'tesseract'
37
+ 'tesseract',
38
+ 'trace',
39
+ 'backtrace'
38
40
  ],
39
41
  description: 'The Chemical X subsystem action to execute.'
40
42
  },
41
43
  params: {
42
44
  type: 'object',
43
- description: 'Parameter payload for the specific action (e.g., { path, symbol, outline, connections } for read; { query, blastRadius, semantic, hybrid } for q; { path, search, replace } for patch; { dir, command } for test/build).',
45
+ description: 'Parameter payload for the specific action (e.g., { path, symbol, outline, logic, template, connections } for read; { query, blastRadius, trace, backtrace, semantic, hybrid } for q; { path, search, replace } for patch; { dir, command } for test/build).',
44
46
  properties: {
45
47
  query: { type: 'string', description: 'Search term, symbol name, or conceptual query (for q/search)' },
46
48
  blastRadius: { type: 'boolean', description: 'Map direct consumers, transitive dependents & impacted tiers (for q)' },
49
+ trace: { type: 'boolean', description: 'Compute forward call trace of downstream invocations (for q/search)' },
50
+ backtrace: { type: 'boolean', description: 'Compute reverse backtrace causal caller path (for q/search)' },
47
51
  semantic: { type: 'boolean', description: 'Search conceptually related components via vector cosine similarity (for q)' },
48
52
  hybrid: { type: 'boolean', description: 'Blend BM25 keyword matching + Vector RRF ranking (for q)' },
49
53
  connections: { type: 'boolean', description: 'Include caller graph and dependent references (for q, read)' },
@@ -54,7 +58,12 @@ export const MASTER_MCP_TOOL = {
54
58
  reindex: { type: 'boolean', description: 'Force re-index before running query (for q)' },
55
59
  path: { type: 'string', description: 'Target file path (for read, patch, write, check, lock)' },
56
60
  symbol: { type: 'string', description: 'Target symbol declaration to extract (for read)' },
57
- outline: { type: 'boolean', description: 'Extract AST signatures only (80%+ token reduction) (for read)' },
61
+ outline: { type: 'boolean', description: 'Extract AST signatures only (80%+ token reduction). Compose with enrich:true for free-lunch outline + logic in one call. (for read)' },
62
+ logic: { type: 'boolean', description: 'Extract AST logic skeleton preserving control flow, guards, and mutations (for read)' },
63
+ template: { type: 'boolean', description: 'Extract declarative template markup only (Vue/Svelte/JSX) (for read)' },
64
+ enrich: { type: 'boolean', description: 'Append compacted logic skeleton after outline block. Composable overlay: use with outline:true or symbol. Returns outline + logic in one token-compact response without boilerplate penalty. (for read)' },
65
+ traceSymbol: { type: 'string', description: 'Symbol name: appends forward call trace card inline when enrich:true (for read)' },
66
+ backtraceSymbol: { type: 'string', description: 'Symbol name: appends reverse caller chain card inline when enrich:true (for read)' },
58
67
  startLine: { type: 'number', description: 'Starting line number (1-indexed) (for read)' },
59
68
  endLine: { type: 'number', description: 'Ending line number (1-indexed) (for read)' },
60
69
  stripComments: { type: 'boolean', description: 'Remove comments to minimize tokens (for read)' },
@@ -69,6 +78,8 @@ export const MASTER_MCP_TOOL = {
69
78
  overwrite: { type: 'boolean', description: 'Allow overwriting existing file (for write)' },
70
79
  dir: { type: 'string', description: 'Target directory (for audit, test, build, patterns)' },
71
80
  command: { type: 'string', description: 'Explicit execution command (for build, test)' },
81
+ target: { type: 'string', description: 'Target test file or spec path (for test)' },
82
+ filter: { type: 'string', description: 'Filter test names by regex or string pattern (for test)' },
72
83
  subAction: { type: 'string', description: 'Sub-action for team operations (e.g. list, claim, done, triage, acquire, release)' },
73
84
  taskId: { type: 'number', description: 'Target task ID (for team task claim/done)' },
74
85
  agentId: { type: 'string', description: 'Agent handle (e.g. @antigravity, @coder)' },
@@ -78,6 +89,13 @@ export const MASTER_MCP_TOOL = {
78
89
  force: { type: 'boolean', description: 'Force complete task even if hazards remain (for team task done)' },
79
90
  tokens: { type: 'number', description: 'Prompt tokens consumed by task' },
80
91
  cost: { type: 'number', description: 'Estimated dollar cost for task' },
92
+ jig: { type: 'boolean', description: 'Enable universal programmatic jig generation (for generate)' },
93
+ kind: { type: 'string', enum: ['service', 'route', 'store', 'repo', 'util', 'spec'], description: 'Programmatic file kind for jig (for generate)' },
94
+ methods: { description: 'Methods definitions array or comma-separated string (for generate)' },
95
+ state: { description: 'State properties array or string (for generate)' },
96
+ routes: { description: 'Routes array or string (for generate)' },
97
+ schema: { description: 'JSON schema definition (for generate)' },
98
+ preset: { type: 'string', enum: ['src', 'app', 'root', 'lib'], description: 'Architecture directory preset (for generate)' },
81
99
  name: { type: 'string', description: 'Capsule name (for generate)' },
82
100
  desc: { type: 'string', description: 'Functional description to tailor archetype (for generate)' },
83
101
  framework: { type: 'string', enum: ['vue', 'react', 'svelte'], description: 'Framework flavor (for generate)' },
@@ -90,7 +108,7 @@ export const MASTER_MCP_TOOL = {
90
108
  },
91
109
  command: {
92
110
  type: 'string',
93
- description: 'Optional CLI command string format (e.g., "test", "build", "verify", "typecheck", "audit src", "q a-button --blast-radius --json", "q \\"button state\\" --semantic --json", "read src/foo.vue --outline", "check src/bar.ts", "team task list").'
111
+ description: 'Optional CLI command string format (e.g., "test", "build", "verify", "typecheck", "audit src", "q a-button --blast-radius --json", "q \\"button state\\" --semantic --json", "read src/foo.vue --outline", "read src/foo.vue --outline --enrich", "check src/bar.ts", "team task list").'
94
112
  }
95
113
  }
96
114
  }
@@ -1,4 +1,4 @@
1
- import { createCapsuleFiles } from '../generator.js';
1
+ import { createCapsuleFiles, createJigFiles, JIG_KINDS } from '../generator.js';
2
2
  import { openIndexDb } from '../search-schema.js';
3
3
  import { fetchScoreTrends, formatTrendReport, renderSparkline } from '../trend.js';
4
4
  import { resolveTargetCwd } from './tools-search.js';
@@ -6,12 +6,32 @@ import { resolveTargetCwd } from './tools-search.js';
6
6
  export const handleGenerateCapsule = (args = {}, cwd = process.cwd()) => {
7
7
  const { name, framework, tier = 'm', targetDir = null, lean = false, desc = '', description = '', dryRun = false } = args;
8
8
 
9
+ const isJig = Boolean(args.jig || args.kind || (args.tier && JIG_KINDS.has(args.tier.toLowerCase())));
10
+ if (isJig) {
11
+ const kind = args.kind || (typeof args.jig === 'string' ? args.jig : null) || (args.tier && JIG_KINDS.has(args.tier.toLowerCase()) ? args.tier : 'service');
12
+ const jigResult = createJigFiles({
13
+ kind,
14
+ name: (name || `sample-${kind}`).trim(),
15
+ targetParent: targetDir,
16
+ dir: args.dir,
17
+ preset: args.preset,
18
+ methods: args.methods,
19
+ state: args.state,
20
+ routes: args.routes,
21
+ schema: args.schema,
22
+ desc: desc || description,
23
+ dryRun: Boolean(dryRun),
24
+ cwd
25
+ });
26
+ return jigResult;
27
+ }
28
+
9
29
  const hasName = Boolean(name && name.trim());
10
30
  const hasValidFramework = Boolean(framework && ['react', 'vue', 'svelte'].includes(framework.toLowerCase()));
11
31
  const canGenerate = hasName && hasValidFramework;
12
32
 
13
33
  if (!canGenerate) {
14
- throw new Error('chemx_generate_capsule requires "name" and "framework" (react, vue, svelte).');
34
+ throw new Error('chemx_generate_capsule requires "name" and "framework" (react, vue, svelte), or pass "jig: true" / "kind".');
15
35
  }
16
36
 
17
37
  const result = createCapsuleFiles({
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { openIndexDb } from '../search-db.js';
3
3
  import {
4
- findSymbolReferences, findFileDependencies, findFileDependents, calculateBlastRadius
4
+ findSymbolReferences, findFileDependencies, findFileDependents, calculateBlastRadius,
5
+ calculateCallTrace, calculateBacktrace,
5
6
  } from '../search-queries.js';
6
7
  import { findSimilarSymbols } from '../search-queries-similar.js';
7
8
  import { readTokenOptimized } from '../reader.js';
@@ -35,6 +36,28 @@ const buildContextEnvelope = (db, targetPath) => {
35
36
  return '';
36
37
  };
37
38
 
39
+ const buildTraceCard = (db, symbol) => {
40
+ try {
41
+ const result = calculateCallTrace(db, symbol, { maxDepth: 3 });
42
+ if (!result || !result.callees || result.callees.length === 0) return '';
43
+ const lines = result.callees.slice(0, 6).map((c) => `// -> ${c.symbol || c} (${c.file ? path.basename(c.file) : '?'})`);
44
+ return `\n// --- Forward Trace: ${symbol} ---\n${lines.join('\n')}`;
45
+ } catch {
46
+ return '';
47
+ }
48
+ };
49
+
50
+ const buildBacktraceCard = (db, symbol) => {
51
+ try {
52
+ const result = calculateBacktrace(db, symbol, { maxDepth: 5 });
53
+ if (!result || !result.rootCallers || result.rootCallers.length === 0) return '';
54
+ const lines = result.rootCallers.slice(0, 5).map((c) => `// <- ${path.basename(c)}`);
55
+ return `\n// --- Backtrace: ${symbol} ---\n${lines.join('\n')}`;
56
+ } catch {
57
+ return '';
58
+ }
59
+ };
60
+
38
61
  export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
39
62
  if (!args.path) throw new Error('chemx_read requires "path" argument.');
40
63
  const targetCwd = resolveTargetCwd(args.cwd || cwd);
@@ -51,19 +74,38 @@ export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
51
74
 
52
75
  const targetPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(targetCwd, rawPath);
53
76
  const res = readTokenOptimized(targetPath, {
54
- outline: args.outline, symbol: args.symbol,
55
- stripComments: args.stripComments !== false, compact: args.compact !== false,
56
- startLine, endLine
77
+ outline: args.outline,
78
+ logic: args.logic,
79
+ template: args.template,
80
+ enrich: args.enrich,
81
+ traceSymbol: args.traceSymbol,
82
+ backtraceSymbol: args.backtraceSymbol,
83
+ symbol: args.symbol,
84
+ stripComments: args.stripComments !== false,
85
+ compact: args.compact !== false,
86
+ startLine,
87
+ endLine
57
88
  });
58
89
 
59
- const db = (args.connections || args.symbol) ? openIndexDb(targetCwd) : null;
90
+ const needsDb = args.connections || args.symbol || (args.enrich && (args.traceSymbol || args.backtraceSymbol));
91
+ const db = needsDb ? openIndexDb(targetCwd) : null;
60
92
  const connectionCard = (args.connections && db) ? buildConnectionCard(db, args.symbol, targetPath) : '';
61
93
  const contextEnvelope = (args.symbol && db) ? buildContextEnvelope(db, targetPath) : '';
62
94
 
95
+ const traceCard = (args.enrich && args.traceSymbol && db)
96
+ ? buildTraceCard(db, args.traceSymbol)
97
+ : '';
98
+ const backtraceCard = (args.enrich && args.backtraceSymbol && db)
99
+ ? buildBacktraceCard(db, args.backtraceSymbol)
100
+ : '';
101
+
63
102
  const ext = path.extname(targetPath).toLowerCase();
64
103
  const lang = EXT_LANG_MAP[ext] || '';
65
- const fence = res.content.includes('```') ? '~~~' : '```';
66
- const fenced = `${fence}${lang}\n${res.content}\n${fence}`;
67
- const header = `// ${res.file} (${res.lineCount || res.totalLines} lines, ~${res.tokensEst} tokens)${connectionCard}\n`;
104
+ const bodyParts = [res.content, res.enriched, traceCard, backtraceCard].filter(Boolean);
105
+ const body = bodyParts.join('\n');
106
+ const fence = body.includes('```') ? '~~~' : '```';
107
+ const fenced = `${fence}${lang}\n${body}\n${fence}`;
108
+ const enrichNote = res.tokensEnriched > 0 ? ` +${res.tokensEnriched} enriched` : '';
109
+ const header = `// ${res.file} (${res.lineCount || res.totalLines} lines, ~${res.tokensEst} tokens${enrichNote})${connectionCard}\n`;
68
110
  return header + contextEnvelope + fenced;
69
111
  };
@@ -30,6 +30,8 @@ export const handleChemxTest = async (args = {}, cwd = process.cwd()) => {
30
30
  return runTestAudit([], false, {
31
31
  json: true,
32
32
  command: args.command,
33
+ target: args.target,
34
+ filter: args.filter,
33
35
  print: false,
34
36
  cwd: targetCwd
35
37
  });
package/cli/mcp/tools.js CHANGED
@@ -16,16 +16,65 @@ const parseCommand = (command, params) => {
16
16
  const parts = command.trim().split(/\s+/);
17
17
  const subCmd = parts[0];
18
18
  if (subCmd === 'audit' || subCmd === 'check') return { action: subCmd, params: { path: parts[1] || 'src', ...params } };
19
- if (subCmd === 'build') return { action: 'build', params: { dir: parts[1] || '.', ...params } };
20
- if (['verify', 'typecheck', 'test'].includes(subCmd)) return { action: subCmd, params };
19
+ if (subCmd === 'test') {
20
+ const targetMatch = command.match(/--target=([^\s]+)/);
21
+ const filterMatch = command.match(/(?:--filter=|-t=|-t\s+)([^\s]+)/);
22
+ const positional = parts.slice(1).find((p) => !p.startsWith('-'));
23
+ return {
24
+ action: 'test',
25
+ params: {
26
+ target: targetMatch ? targetMatch[1] : (positional || params?.target),
27
+ filter: filterMatch ? filterMatch[1] : params?.filter,
28
+ ...params
29
+ }
30
+ };
31
+ }
32
+ if (subCmd === 'jig') {
33
+ const kind = parts[1];
34
+ const name = parts[2];
35
+ const methodsMatch = command.match(/--methods=([^\s]+)/);
36
+ const routesMatch = command.match(/--routes=([^\s]+)/);
37
+ return {
38
+ action: 'generate',
39
+ params: {
40
+ jig: true,
41
+ kind,
42
+ name,
43
+ methods: methodsMatch ? methodsMatch[1] : undefined,
44
+ routes: routesMatch ? routesMatch[1] : undefined,
45
+ ...params
46
+ }
47
+ };
48
+ }
49
+ if (['verify', 'typecheck'].includes(subCmd)) return { action: subCmd, params };
21
50
  if (subCmd === 'read' || subCmd === 'r') {
22
51
  const hasOutline = command.includes('--outline') || command.includes(' -o');
52
+ const hasLogic = command.includes('--logic') || command.includes(' -l');
53
+ const hasTemplate = command.includes('--template') || command.includes(' -t');
54
+ const hasEnrich = command.includes('--enrich');
23
55
  const symbolMatch = command.match(/(?:--symbol=|-s\s+|-s=)([^\s]+)/);
56
+ const traceMatch = command.match(/--trace=([^\s]+)/);
57
+ const backtraceMatch = command.match(/--backtrace=([^\s]+)/);
24
58
  const lineMatch = parts[1]?.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
25
59
  const targetPath = lineMatch ? lineMatch[1] : parts[1];
26
60
  const startLine = lineMatch ? parseInt(lineMatch[2], 10) : undefined;
27
61
  const endLine = lineMatch && lineMatch[3] ? parseInt(lineMatch[3], 10) : undefined;
28
- return { action: 'read', params: { path: targetPath, outline: hasOutline, symbol: symbolMatch ? symbolMatch[1] : undefined, startLine, endLine, ...params } };
62
+ return {
63
+ action: 'read',
64
+ params: {
65
+ path: targetPath,
66
+ outline: hasOutline || undefined,
67
+ logic: hasLogic || undefined,
68
+ template: hasTemplate || undefined,
69
+ enrich: hasEnrich || undefined,
70
+ symbol: symbolMatch ? symbolMatch[1] : undefined,
71
+ traceSymbol: traceMatch ? traceMatch[1] : undefined,
72
+ backtraceSymbol: backtraceMatch ? backtraceMatch[1] : undefined,
73
+ startLine,
74
+ endLine,
75
+ ...params
76
+ }
77
+ };
29
78
  }
30
79
  if (subCmd === 'q' || subCmd === 'search') return { action: 'q', params: { query: parts.slice(1).join(' '), ...params } };
31
80
  if (subCmd === 'team') {
@@ -170,8 +170,73 @@ const TIER_DIRECTORY_MAPS = {
170
170
  ]
171
171
  };
172
172
 
173
- export const detectTierBaseDir = (tier, cwd = process.cwd()) => {
173
+ export const PRESET_DIRECTORY_MAPS = {
174
+ src: {
175
+ molecule: 'src/components/molecules',
176
+ atom: 'src/components/atoms',
177
+ organism: 'src/components/organisms',
178
+ template: 'src/components/templates',
179
+ hook: 'src/hooks',
180
+ view: 'src/views',
181
+ service: 'src/services',
182
+ route: 'src/routes',
183
+ store: 'src/stores',
184
+ repo: 'src/repos',
185
+ util: 'src/utils',
186
+ spec: 'test/unit'
187
+ },
188
+ app: {
189
+ molecule: 'app/components/molecules',
190
+ atom: 'app/components/atoms',
191
+ organism: 'app/components/organisms',
192
+ template: 'app/components/templates',
193
+ hook: 'app/composables',
194
+ view: 'app/pages',
195
+ service: 'app/services',
196
+ route: 'server/api',
197
+ store: 'app/stores',
198
+ repo: 'server/repos',
199
+ util: 'app/utils',
200
+ spec: 'test/unit'
201
+ },
202
+ root: {
203
+ molecule: 'components/molecules',
204
+ atom: 'components/atoms',
205
+ organism: 'components/organisms',
206
+ template: 'components/templates',
207
+ hook: 'hooks',
208
+ view: 'pages',
209
+ service: 'services',
210
+ route: 'routes',
211
+ store: 'stores',
212
+ repo: 'repos',
213
+ util: 'utils',
214
+ spec: 'test'
215
+ },
216
+ lib: {
217
+ molecule: 'src/lib/components/molecules',
218
+ atom: 'src/lib/components/atoms',
219
+ organism: 'src/lib/components/organisms',
220
+ template: 'src/lib/components/templates',
221
+ hook: 'src/lib/hooks',
222
+ view: 'src/routes',
223
+ service: 'src/lib/services',
224
+ route: 'src/routes/api',
225
+ store: 'src/lib/stores',
226
+ repo: 'src/lib/repos',
227
+ util: 'src/lib/utils',
228
+ spec: 'test'
229
+ }
230
+ };
231
+
232
+ export const detectTierBaseDir = (tier, cwd = process.cwd(), presetOverride = null) => {
174
233
  const config = loadProjectConfig(cwd);
234
+ const activePreset = presetOverride || config.preset;
235
+
236
+ if (activePreset && PRESET_DIRECTORY_MAPS[activePreset]?.[tier]) {
237
+ return PRESET_DIRECTORY_MAPS[activePreset][tier];
238
+ }
239
+
175
240
  if (config.dirs && config.dirs[tier]) {
176
241
  const configuredPath = path.resolve(cwd, config.dirs[tier]);
177
242
  if (fs.existsSync(configuredPath)) return config.dirs[tier];
@@ -182,7 +247,45 @@ export const detectTierBaseDir = (tier, cwd = process.cwd()) => {
182
247
  if (fs.existsSync(path.resolve(cwd, c))) return c;
183
248
  }
184
249
 
185
- return '.';
250
+ if (fs.existsSync(path.resolve(cwd, 'app/components'))) {
251
+ return PRESET_DIRECTORY_MAPS.app[tier] || 'app/components';
252
+ }
253
+
254
+ return PRESET_DIRECTORY_MAPS.src[tier] || '.';
255
+ };
256
+
257
+ export const detectJigBaseDir = (kind, { cwd = process.cwd(), preset = null } = {}) => {
258
+ const config = loadProjectConfig(cwd);
259
+ const activePreset = preset || config.preset;
260
+
261
+ if (activePreset && PRESET_DIRECTORY_MAPS[activePreset]?.[kind]) {
262
+ return PRESET_DIRECTORY_MAPS[activePreset][kind];
263
+ }
264
+
265
+ if (config.dirs && config.dirs[kind]) {
266
+ const configuredPath = path.resolve(cwd, config.dirs[kind]);
267
+ if (fs.existsSync(configuredPath)) return config.dirs[kind];
268
+ }
269
+
270
+ const defaultDirMap = {
271
+ service: ['src/services', 'app/services', 'services', 'src/lib/services'],
272
+ route: ['src/routes', 'server/api', 'routes', 'src/routes/api'],
273
+ store: ['src/stores', 'app/stores', 'stores', 'src/lib/stores'],
274
+ repo: ['src/repos', 'server/repos', 'repos', 'src/lib/repos'],
275
+ util: ['src/utils', 'app/utils', 'utils', 'src/lib/utils'],
276
+ spec: ['test/unit', 'tests/unit', 'test', 'tests']
277
+ };
278
+
279
+ const candidates = defaultDirMap[kind] || [`src/${kind}s`];
280
+ for (const c of candidates) {
281
+ if (fs.existsSync(path.resolve(cwd, c))) return c;
282
+ }
283
+
284
+ if (fs.existsSync(path.resolve(cwd, 'app'))) {
285
+ return PRESET_DIRECTORY_MAPS.app[kind] || `app/${kind}s`;
286
+ }
287
+
288
+ return PRESET_DIRECTORY_MAPS.src[kind] || `src/${kind}s`;
186
289
  };
187
290
 
188
291
  const TAILWIND_PACKAGES = [
@@ -0,0 +1,146 @@
1
+ import { ANSI } from './theme.js';
2
+ import { readTokenOptimized } from './reader.js';
3
+
4
+ /**
5
+ * CLI command runner for chemx read / chemx view.
6
+ *
7
+ * @param {string[]} args CLI arguments.
8
+ * @param {boolean} isCli Whether invoked directly from CLI.
9
+ */
10
+ export const runReaderCli = (args, isCli = false) => {
11
+ if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
12
+ const isJson = args.includes('--json');
13
+ if (isJson) {
14
+ process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
15
+ } else {
16
+ process.stdout.write([
17
+ `${ANSI.BOLD}USAGE${ANSI.RESET}`,
18
+ ` chemx read <file> [options]`,
19
+ '',
20
+ `${ANSI.BOLD}OPTIONS${ANSI.RESET}`,
21
+ ` --outline AST structure outline only (80%+ token savings)`,
22
+ ` --logic, -l AST logic skeleton (control flow, state, mutations)`,
23
+ ` --template, -t Extract template markup only (Vue/Svelte/JSX)`,
24
+ ` --enrich Append compacted logic skeleton after outline (free-lunch overlay)`,
25
+ ` --trace=<name> Forward call trace card appended inline (requires --enrich)`,
26
+ ` --backtrace=<name> Reverse caller chain card appended inline (requires --enrich)`,
27
+ ` --symbol=<name> Extract specific function/hook/interface declaration`,
28
+ ` --start=<N> Start line number (1-indexed)`,
29
+ ` --end=<N> End line number (1-indexed)`,
30
+ ` --strip-comments Remove comments to minimize tokens`,
31
+ ` --compact Collapse empty whitespace lines`,
32
+ ` --json Output result as minified JSON`,
33
+ ` -h, --help Show this help message`,
34
+ '',
35
+ `${ANSI.BOLD}EXAMPLES${ANSI.RESET}`,
36
+ ` chemx read src/store.ts --outline`,
37
+ ` chemx read src/controller.ts --outline --enrich`,
38
+ ` chemx read src/card.vue --outline --enrich --trace=handleSubmit`,
39
+ ` chemx read src/controller.ts --logic`,
40
+ ` chemx read api.ts --symbol=login --connections`,
41
+ ''
42
+ ].join('\n'));
43
+ }
44
+ if (isCli) process.exit(0);
45
+ return { help: true, success: true };
46
+ }
47
+
48
+ const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
49
+ let filePath = nonFlagArgs[0];
50
+
51
+ if (!filePath) {
52
+ process.stderr.write(`${ANSI.RED}āœ• Missing file path. Usage: chemx r <file[:start-end]> [-o] [-l] [-s name]${ANSI.RESET}\n`);
53
+ if (isCli) process.exit(1);
54
+ return null;
55
+ }
56
+
57
+ let startLine;
58
+ let endLine;
59
+
60
+ const colonMatch = filePath.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
61
+ if (colonMatch) {
62
+ filePath = colonMatch[1];
63
+ startLine = parseInt(colonMatch[2], 10);
64
+ if (colonMatch[3]) endLine = parseInt(colonMatch[3], 10);
65
+ }
66
+
67
+ const isJson = args.includes('--json') || args.includes('-j');
68
+ const isOutline = args.includes('--outline') || args.includes('-o');
69
+ const isLogic = args.includes('--logic') || args.includes('-l');
70
+ const isTemplate = args.includes('--template') || args.includes('-t');
71
+ const isCompact = args.includes('--compact') || args.includes('-c');
72
+ const isStripComments = args.includes('--strip-comments') || args.includes('--no-comments') || args.includes('-sc');
73
+ const isEnrich = args.includes('--enrich');
74
+
75
+ const traceFlag = args.find((a) => a.startsWith('--trace='));
76
+ const traceSymbol = traceFlag ? traceFlag.split('=')[1] : null;
77
+
78
+ const backtraceFlag = args.find((a) => a.startsWith('--backtrace='));
79
+ const backtraceSymbol = backtraceFlag ? backtraceFlag.split('=')[1] : null;
80
+
81
+ let symbol = null;
82
+ const symEqFlag = args.find((a) => a.startsWith('--symbol=') || a.startsWith('-sym='));
83
+ if (symEqFlag) {
84
+ symbol = symEqFlag.split('=')[1];
85
+ } else {
86
+ const sIndex = args.findIndex((a) => a === '-s' || a === '--symbol');
87
+ if (sIndex !== -1 && args[sIndex + 1] && !args[sIndex + 1].startsWith('-')) {
88
+ symbol = args[sIndex + 1];
89
+ } else {
90
+ const sFlag = args.find((a) => a.startsWith('-s='));
91
+ if (sFlag) {
92
+ const val = sFlag.split('=')[1];
93
+ if (/^\d+$/.test(val)) {
94
+ if (startLine === undefined) startLine = parseInt(val, 10);
95
+ } else {
96
+ symbol = val;
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ if (startLine === undefined) {
103
+ const startFlag = args.find((a) => a.startsWith('--start='));
104
+ if (startFlag) startLine = parseInt(startFlag.split('=')[1], 10);
105
+ }
106
+
107
+ if (endLine === undefined) {
108
+ const endFlag = args.find((a) => a.startsWith('--end=') || a.startsWith('-e='));
109
+ if (endFlag) endLine = parseInt(endFlag.split('=')[1], 10);
110
+ }
111
+
112
+ try {
113
+ const res = readTokenOptimized(filePath, {
114
+ outline: isOutline,
115
+ logic: isLogic,
116
+ template: isTemplate,
117
+ compact: isCompact,
118
+ stripComments: isStripComments,
119
+ enrich: isEnrich,
120
+ traceSymbol,
121
+ backtraceSymbol,
122
+ symbol,
123
+ startLine,
124
+ endLine,
125
+ });
126
+
127
+ if (isJson) {
128
+ process.stdout.write(JSON.stringify(res, null, 2) + '\n');
129
+ } else {
130
+ const enrichSuffix = res.tokensEnriched > 0 ? ` +${res.tokensEnriched} enriched` : '';
131
+ process.stdout.write(`${ANSI.BOLD}${ANSI.CYAN}--- ${res.file} (${res.lineCount || res.totalLines} lines, ~${res.tokensEst} tokens${enrichSuffix}) ---${ANSI.RESET}\n`);
132
+ process.stdout.write(res.content + '\n');
133
+ if (res.enriched) {
134
+ process.stdout.write(res.enriched + '\n');
135
+ }
136
+ }
137
+
138
+ if (isCli) process.exit(0);
139
+ return res;
140
+ } catch (err) {
141
+ const msg = err instanceof Error ? err.message : String(err);
142
+ process.stderr.write(`${ANSI.RED}āœ• ${msg}${ANSI.RESET}\n`);
143
+ if (isCli) process.exit(1);
144
+ return null;
145
+ }
146
+ };