@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/audit/prompts.js +1 -1
- package/cli/commands/cmd-router.js +20 -2
- package/cli/commands-schema.js +65 -6
- package/cli/generator-help.js +26 -6
- package/cli/generator-jig-cli.js +67 -0
- package/cli/generator-jig.js +479 -0
- package/cli/generator-jig.spec.js +148 -0
- package/cli/generator.js +15 -2
- package/cli/index.js +4 -1
- package/cli/installer-templates.js +8 -3
- package/cli/mcp/manifests.js +22 -4
- package/cli/mcp/tools-generate.js +22 -2
- package/cli/mcp/tools-read.js +50 -8
- package/cli/mcp/tools-verify.js +2 -0
- package/cli/mcp/tools.js +52 -3
- package/cli/project-detector.js +105 -2
- package/cli/reader-cli.js +146 -0
- package/cli/reader-logic.js +392 -0
- package/cli/reader-logic.spec.js +160 -0
- package/cli/reader.js +78 -171
- package/cli/search-commands-graph.js +125 -1
- package/cli/search-commands.js +7 -1
- package/cli/search-db.js +2 -0
- package/cli/search-queries-graph.js +294 -0
- package/cli/search-queries.js +6 -1
- package/cli/search.js +28 -30
- package/cli/team/team-memory.js +30 -7
- package/cli/team/team-memory.spec.js +24 -0
- package/cli/tesseract.spec.js +2 -2
- package/cli/trace.spec.js +92 -0
- package/cli/verify-helpers.js +46 -3
- package/cli/verify-lint.js +224 -0
- package/cli/verify-lint.spec.js +44 -0
- package/cli/verify.js +8 -1
- package/cli/verify.spec.js +17 -0
- package/package.json +1 -1
- package/scripts/pre-commit.sh +8 -3
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
package/cli/mcp/manifests.js
CHANGED
|
@@ -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({
|
package/cli/mcp/tools-read.js
CHANGED
|
@@ -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,
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
66
|
-
const
|
|
67
|
-
const
|
|
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
|
};
|
package/cli/mcp/tools-verify.js
CHANGED
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 === '
|
|
20
|
-
|
|
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 {
|
|
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') {
|
package/cli/project-detector.js
CHANGED
|
@@ -170,8 +170,73 @@ const TIER_DIRECTORY_MAPS = {
|
|
|
170
170
|
]
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
-
export const
|
|
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
|
-
|
|
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
|
+
};
|