@shrkcrft/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
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/dist/audit/knowledge-audit-llm.d.ts +19 -0
- package/dist/audit/knowledge-audit-llm.d.ts.map +1 -0
- package/dist/audit/knowledge-audit-llm.js +164 -0
- package/dist/audit/knowledge-audit.d.ts +61 -0
- package/dist/audit/knowledge-audit.d.ts.map +1 -0
- package/dist/audit/knowledge-audit.js +203 -0
- package/dist/audit/knowledge-fix-plan-llm.d.ts +11 -0
- package/dist/audit/knowledge-fix-plan-llm.d.ts.map +1 -0
- package/dist/audit/knowledge-fix-plan-llm.js +141 -0
- package/dist/audit/knowledge-fix-plan.d.ts +41 -0
- package/dist/audit/knowledge-fix-plan.d.ts.map +1 -0
- package/dist/audit/knowledge-fix-plan.js +125 -0
- package/dist/audit/pipeline-audit-llm.d.ts +11 -0
- package/dist/audit/pipeline-audit-llm.d.ts.map +1 -0
- package/dist/audit/pipeline-audit-llm.js +134 -0
- package/dist/audit/pipeline-audit.d.ts +69 -0
- package/dist/audit/pipeline-audit.d.ts.map +1 -0
- package/dist/audit/pipeline-audit.js +166 -0
- package/dist/audit/templates-audit-llm.d.ts +19 -0
- package/dist/audit/templates-audit-llm.d.ts.map +1 -0
- package/dist/audit/templates-audit-llm.js +207 -0
- package/dist/audit/templates-audit.d.ts +63 -0
- package/dist/audit/templates-audit.d.ts.map +1 -0
- package/dist/audit/templates-audit.js +171 -0
- package/dist/audit/templates-fix-plan-llm.d.ts +19 -0
- package/dist/audit/templates-fix-plan-llm.d.ts.map +1 -0
- package/dist/audit/templates-fix-plan-llm.js +162 -0
- package/dist/audit/templates-fix-plan.d.ts +37 -0
- package/dist/audit/templates-fix-plan.d.ts.map +1 -0
- package/dist/audit/templates-fix-plan.js +174 -0
- package/dist/command-registry.d.ts +28 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +91 -1
- package/dist/commands/ai-status.command.d.ts +19 -0
- package/dist/commands/ai-status.command.d.ts.map +1 -0
- package/dist/commands/ai-status.command.js +94 -0
- package/dist/commands/api-diff.command.d.ts +11 -0
- package/dist/commands/api-diff.command.d.ts.map +1 -0
- package/dist/commands/api-diff.command.js +144 -0
- package/dist/commands/apply.command.d.ts.map +1 -1
- package/dist/commands/apply.command.js +10 -2
- package/dist/commands/arch.command.d.ts +9 -0
- package/dist/commands/arch.command.d.ts.map +1 -0
- package/dist/commands/arch.command.js +186 -0
- package/dist/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +10 -9
- package/dist/commands/cache-align.command.d.ts +12 -0
- package/dist/commands/cache-align.command.d.ts.map +1 -0
- package/dist/commands/cache-align.command.js +78 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +26 -2
- package/dist/commands/code-intel.command.d.ts +18 -0
- package/dist/commands/code-intel.command.d.ts.map +1 -0
- package/dist/commands/code-intel.command.js +146 -0
- package/dist/commands/codemod.command.d.ts.map +1 -1
- package/dist/commands/codemod.command.js +27 -6
- package/dist/commands/command-catalog.d.ts +15 -3
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +407 -34
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +4 -4
- package/dist/commands/completion.command.d.ts +10 -0
- package/dist/commands/completion.command.d.ts.map +1 -0
- package/dist/commands/completion.command.js +121 -0
- package/dist/commands/compress.command.d.ts +8 -0
- package/dist/commands/compress.command.d.ts.map +1 -0
- package/dist/commands/compress.command.js +147 -0
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +89 -23
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +121 -1
- package/dist/commands/contract-gate.command.d.ts.map +1 -1
- package/dist/commands/contract-gate.command.js +5 -1
- package/dist/commands/delegate.command.d.ts +65 -0
- package/dist/commands/delegate.command.d.ts.map +1 -0
- package/dist/commands/delegate.command.js +657 -0
- package/dist/commands/deps-audit.command.d.ts +23 -0
- package/dist/commands/deps-audit.command.d.ts.map +1 -0
- package/dist/commands/deps-audit.command.js +270 -0
- package/dist/commands/dev.command.d.ts.map +1 -1
- package/dist/commands/dev.command.js +5 -2
- package/dist/commands/diff-check.command.d.ts +30 -0
- package/dist/commands/diff-check.command.d.ts.map +1 -0
- package/dist/commands/diff-check.command.js +210 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +162 -10
- package/dist/commands/export.command.d.ts.map +1 -1
- package/dist/commands/export.command.js +76 -3
- package/dist/commands/framework.command.d.ts +12 -0
- package/dist/commands/framework.command.d.ts.map +1 -0
- package/dist/commands/framework.command.js +180 -0
- package/dist/commands/gate.command.d.ts +15 -0
- package/dist/commands/gate.command.d.ts.map +1 -0
- package/dist/commands/gate.command.js +300 -0
- package/dist/commands/gen.command.d.ts.map +1 -1
- package/dist/commands/gen.command.js +13 -1
- package/dist/commands/graph-code-subverbs.d.ts +33 -0
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -0
- package/dist/commands/graph-code-subverbs.js +1385 -0
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +31 -2
- package/dist/commands/help.command.d.ts +4 -3
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +86 -18
- package/dist/commands/helper.command.js +1 -1
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +171 -1
- package/dist/commands/import.command.d.ts.map +1 -1
- package/dist/commands/import.command.js +121 -5
- package/dist/commands/ingest.command.d.ts.map +1 -1
- package/dist/commands/ingest.command.js +5 -1
- package/dist/commands/init.command.d.ts.map +1 -1
- package/dist/commands/init.command.js +174 -7
- package/dist/commands/knowledge-author.command.d.ts.map +1 -1
- package/dist/commands/knowledge-author.command.js +9 -0
- package/dist/commands/knowledge-propose.command.d.ts.map +1 -1
- package/dist/commands/knowledge-propose.command.js +4 -2
- package/dist/commands/knowledge.command.d.ts.map +1 -1
- package/dist/commands/knowledge.command.js +26 -3
- package/dist/commands/migrate.command.d.ts +13 -0
- package/dist/commands/migrate.command.d.ts.map +1 -0
- package/dist/commands/migrate.command.js +152 -0
- package/dist/commands/move-plan.command.d.ts +23 -0
- package/dist/commands/move-plan.command.d.ts.map +1 -0
- package/dist/commands/move-plan.command.js +360 -0
- package/dist/commands/packs-new.d.ts +1 -1
- package/dist/commands/packs-new.d.ts.map +1 -1
- package/dist/commands/packs-new.js +5 -36
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +2 -10
- package/dist/commands/plan-context.command.d.ts +11 -0
- package/dist/commands/plan-context.command.d.ts.map +1 -0
- package/dist/commands/plan-context.command.js +85 -0
- package/dist/commands/preflight.command.d.ts.map +1 -1
- package/dist/commands/preflight.command.js +15 -0
- package/dist/commands/profiles.command.js +4 -4
- package/dist/commands/recommend.command.d.ts +6 -0
- package/dist/commands/recommend.command.d.ts.map +1 -1
- package/dist/commands/recommend.command.js +119 -5
- package/dist/commands/release.command.js +13 -13
- package/dist/commands/rule-graph-subverbs.d.ts +3 -0
- package/dist/commands/rule-graph-subverbs.d.ts.map +1 -0
- package/dist/commands/rule-graph-subverbs.js +132 -0
- package/dist/commands/rules.command.d.ts.map +1 -1
- package/dist/commands/rules.command.js +20 -3
- package/dist/commands/scaffold-validate.command.d.ts +22 -0
- package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
- package/dist/commands/scaffold-validate.command.js +215 -0
- package/dist/commands/search-structural.command.d.ts +18 -0
- package/dist/commands/search-structural.command.d.ts.map +1 -0
- package/dist/commands/search-structural.command.js +376 -0
- package/dist/commands/search.command.js +1 -1
- package/dist/commands/smart-context.command.d.ts +67 -0
- package/dist/commands/smart-context.command.d.ts.map +1 -0
- package/dist/commands/smart-context.command.js +4728 -0
- package/dist/commands/spike.command.d.ts +22 -0
- package/dist/commands/spike.command.d.ts.map +1 -0
- package/dist/commands/spike.command.js +235 -0
- package/dist/commands/surface.command.d.ts +1 -0
- package/dist/commands/surface.command.d.ts.map +1 -1
- package/dist/commands/surface.command.js +10 -3
- package/dist/commands/task-context.command.d.ts.map +1 -1
- package/dist/commands/task-context.command.js +5 -17
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +8 -2
- package/dist/commands/template-quality.command.d.ts.map +1 -1
- package/dist/commands/template-quality.command.js +39 -3
- package/dist/commands/templates.command.d.ts.map +1 -1
- package/dist/commands/templates.command.js +37 -2
- package/dist/commands/tests.command.d.ts.map +1 -1
- package/dist/commands/tests.command.js +13 -2
- package/dist/commands/watch.command.d.ts +26 -0
- package/dist/commands/watch.command.d.ts.map +1 -0
- package/dist/commands/watch.command.js +456 -0
- package/dist/dashboard/code-intelligence-data.d.ts +33 -0
- package/dist/dashboard/code-intelligence-data.d.ts.map +1 -0
- package/dist/dashboard/code-intelligence-data.js +329 -0
- package/dist/dashboard/dashboard-api-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-api-server.js +256 -2
- package/dist/dashboard/knowledge-ask.d.ts +4 -0
- package/dist/dashboard/knowledge-ask.d.ts.map +1 -0
- package/dist/dashboard/knowledge-ask.js +112 -0
- package/dist/env/load-dotenv.d.ts +15 -0
- package/dist/env/load-dotenv.d.ts.map +1 -0
- package/dist/env/load-dotenv.js +70 -0
- package/dist/export/claude-commands-export.d.ts +60 -0
- package/dist/export/claude-commands-export.d.ts.map +1 -0
- package/dist/export/claude-commands-export.js +276 -0
- package/dist/export/export-formats.d.ts +1 -1
- package/dist/export/export-formats.d.ts.map +1 -1
- package/dist/export/export-formats.js +139 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/init/init-templates.d.ts.map +1 -1
- package/dist/init/init-templates.js +133 -113
- package/dist/init/paths-advisory.d.ts +20 -0
- package/dist/init/paths-advisory.d.ts.map +1 -0
- package/dist/init/paths-advisory.js +88 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +331 -17
- package/dist/output/ccr-store-config.d.ts +18 -0
- package/dist/output/ccr-store-config.d.ts.map +1 -0
- package/dist/output/ccr-store-config.js +41 -0
- package/dist/output/format-output.d.ts.map +1 -1
- package/dist/output/format-output.js +6 -1
- package/dist/output/output-compression.d.ts +15 -0
- package/dist/output/output-compression.d.ts.map +1 -0
- package/dist/output/output-compression.js +60 -0
- package/dist/output/resolve-compress-type.d.ts +22 -0
- package/dist/output/resolve-compress-type.d.ts.map +1 -0
- package/dist/output/resolve-compress-type.js +21 -0
- package/dist/output/watch-loop.d.ts +9 -1
- package/dist/output/watch-loop.d.ts.map +1 -1
- package/dist/output/watch-loop.js +13 -3
- package/dist/schemas/json-schemas.d.ts +384 -36
- package/dist/schemas/json-schemas.d.ts.map +1 -1
- package/dist/schemas/json-schemas.js +247 -36
- package/dist/surface/profiles.d.ts.map +1 -1
- package/dist/surface/profiles.js +54 -10
- package/dist/surface/surface-config-writer.d.ts.map +1 -1
- package/dist/surface/surface-config-writer.js +23 -11
- package/dist/validation/run-validation-loop.d.ts.map +1 -1
- package/dist/validation/run-validation-loop.js +5 -1
- package/package.json +35 -21
- package/dist/commands/plugin.command.d.ts +0 -11
- package/dist/commands/plugin.command.d.ts.map +0 -1
- package/dist/commands/plugin.command.js +0 -394
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { readPlanFromFile } from '@shrkcrft/generator';
|
|
4
|
+
import { flagBool, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { asJson, header } from "../output/format-output.js";
|
|
6
|
+
import { printError } from "../output/print-error.js";
|
|
7
|
+
/**
|
|
8
|
+
* `shrk scaffold-validate <plan-file>` — verify that the files
|
|
9
|
+
* recorded in a saved generation plan actually exist on disk and
|
|
10
|
+
* look like they came from the template.
|
|
11
|
+
*
|
|
12
|
+
* Read-only: never writes, never re-runs the template. Designed to
|
|
13
|
+
* run after `shrk apply` (or after a human manually copied a plan
|
|
14
|
+
* into place) to catch:
|
|
15
|
+
* - missing files (apply was interrupted, or somebody deleted)
|
|
16
|
+
* - shrunken files (someone replaced the body with `// TODO`)
|
|
17
|
+
* - mismatched operation type (plan said `create`, disk shows
|
|
18
|
+
* something else)
|
|
19
|
+
*
|
|
20
|
+
* NOT a full template-replay check — we can't know the exact
|
|
21
|
+
* expected body without re-rendering the template, which would
|
|
22
|
+
* be far more expensive and would couple this command to every
|
|
23
|
+
* template's variable resolution. Sizes + existence + type catches
|
|
24
|
+
* 95% of real-world failures.
|
|
25
|
+
*/
|
|
26
|
+
export const scaffoldValidateCommand = {
|
|
27
|
+
name: 'scaffold-validate',
|
|
28
|
+
description: 'Verify that the files recorded in a saved generation plan exist on disk and match the planned type/size envelope.',
|
|
29
|
+
usage: 'shrk scaffold-validate <plan-file> [--json] [--shrink-tolerance N]',
|
|
30
|
+
async run(args) {
|
|
31
|
+
const planArg = args.positional[0]?.trim();
|
|
32
|
+
if (!planArg) {
|
|
33
|
+
process.stderr.write('Usage: shrk scaffold-validate <plan-file> [--json] [--shrink-tolerance N]\n');
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
36
|
+
const cwd = resolveCwd(args);
|
|
37
|
+
const json = flagBool(args, 'json');
|
|
38
|
+
const shrinkTolerance = typeof args.flags.get('shrink-tolerance') === 'string'
|
|
39
|
+
? Number(args.flags.get('shrink-tolerance'))
|
|
40
|
+
: 0.25;
|
|
41
|
+
const planFile = nodePath.isAbsolute(planArg) ? planArg : nodePath.resolve(cwd, planArg);
|
|
42
|
+
if (!existsSync(planFile)) {
|
|
43
|
+
process.stderr.write(`Plan file not found: ${planFile}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const loaded = readPlanFromFile(planFile);
|
|
47
|
+
if (!loaded.ok) {
|
|
48
|
+
printError(loaded.error);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const plan = loaded.value;
|
|
52
|
+
const projectRoot = nodePath.isAbsolute(plan.projectRoot)
|
|
53
|
+
? plan.projectRoot
|
|
54
|
+
: nodePath.resolve(cwd, plan.projectRoot);
|
|
55
|
+
const expected = plan.expectedChanges ?? [];
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const change of expected) {
|
|
58
|
+
findings.push(checkChange(projectRoot, change, shrinkTolerance));
|
|
59
|
+
}
|
|
60
|
+
const folderFindings = (plan.folderOps ?? []).map((op) => {
|
|
61
|
+
const target = nodePath.join(projectRoot, op.targetPath);
|
|
62
|
+
if (op.kind === 'rename-folder') {
|
|
63
|
+
// After apply, the renamed folder should be at newPath, and
|
|
64
|
+
// the old path should not exist (unless human reverted).
|
|
65
|
+
const newPath = op.newPath ? nodePath.join(projectRoot, op.newPath) : '';
|
|
66
|
+
if (newPath && existsSync(newPath)) {
|
|
67
|
+
return { kind: op.kind, targetPath: op.targetPath, status: 'compliant' };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: op.kind,
|
|
71
|
+
targetPath: op.targetPath,
|
|
72
|
+
status: 'missing',
|
|
73
|
+
detail: `rename target "${op.newPath ?? '?'}" not found`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (op.kind === 'delete-folder') {
|
|
77
|
+
if (!existsSync(target)) {
|
|
78
|
+
return { kind: op.kind, targetPath: op.targetPath, status: 'compliant' };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
kind: op.kind,
|
|
82
|
+
targetPath: op.targetPath,
|
|
83
|
+
status: 'missing',
|
|
84
|
+
detail: 'folder still present (delete not applied)',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { kind: String(op.kind), targetPath: op.targetPath, status: 'missing' };
|
|
88
|
+
});
|
|
89
|
+
const totals = {
|
|
90
|
+
expected: expected.length,
|
|
91
|
+
compliant: findings.filter((f) => f.status === 'compliant').length,
|
|
92
|
+
missing: findings.filter((f) => f.status === 'missing').length,
|
|
93
|
+
shrunk: findings.filter((f) => f.status === 'shrunk').length,
|
|
94
|
+
unexpectedType: findings.filter((f) => f.status === 'unexpected-type').length,
|
|
95
|
+
};
|
|
96
|
+
const hasFailures = totals.missing > 0 || totals.unexpectedType > 0;
|
|
97
|
+
const hasWarnings = totals.shrunk > 0;
|
|
98
|
+
const status = hasFailures
|
|
99
|
+
? 'failed'
|
|
100
|
+
: hasWarnings || folderFindings.some((f) => f.status === 'missing')
|
|
101
|
+
? 'partial'
|
|
102
|
+
: 'ok';
|
|
103
|
+
const report = {
|
|
104
|
+
schema: 'sharkcraft.scaffold-validate/v1',
|
|
105
|
+
planFile,
|
|
106
|
+
templateId: plan.templateId,
|
|
107
|
+
...(plan.name !== undefined ? { name: plan.name } : {}),
|
|
108
|
+
projectRoot,
|
|
109
|
+
totals,
|
|
110
|
+
findings,
|
|
111
|
+
folderOpFindings: folderFindings,
|
|
112
|
+
status,
|
|
113
|
+
handoffForClaude: handoffFor(status, totals, folderFindings.length),
|
|
114
|
+
};
|
|
115
|
+
if (json) {
|
|
116
|
+
process.stdout.write(asJson(report) + '\n');
|
|
117
|
+
return status === 'failed' ? 1 : 0;
|
|
118
|
+
}
|
|
119
|
+
renderText(report);
|
|
120
|
+
return status === 'failed' ? 1 : 0;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
function checkChange(projectRoot, change, shrinkTolerance) {
|
|
124
|
+
const abs = nodePath.join(projectRoot, change.relativePath);
|
|
125
|
+
const base = {
|
|
126
|
+
relativePath: change.relativePath,
|
|
127
|
+
type: change.type,
|
|
128
|
+
status: 'compliant',
|
|
129
|
+
expectedSizeBytes: change.sizeBytes,
|
|
130
|
+
};
|
|
131
|
+
// delete-file plans expect the file NOT to exist after apply.
|
|
132
|
+
if (change.type === 'delete') {
|
|
133
|
+
if (existsSync(abs)) {
|
|
134
|
+
return { ...base, status: 'unexpected-type', detail: 'file is still present after delete plan' };
|
|
135
|
+
}
|
|
136
|
+
return base;
|
|
137
|
+
}
|
|
138
|
+
if (!existsSync(abs)) {
|
|
139
|
+
return { ...base, status: 'missing', detail: 'file not found on disk' };
|
|
140
|
+
}
|
|
141
|
+
let stat;
|
|
142
|
+
try {
|
|
143
|
+
stat = statSync(abs);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return { ...base, status: 'missing', detail: e.message };
|
|
147
|
+
}
|
|
148
|
+
if (!stat.isFile()) {
|
|
149
|
+
return { ...base, status: 'unexpected-type', detail: 'path is not a regular file' };
|
|
150
|
+
}
|
|
151
|
+
// Read first chunk to detect "the file was reduced to a stub by hand"
|
|
152
|
+
// (`// TODO`, `export {}`, empty). We compare against the planned size
|
|
153
|
+
// with a tolerance — humans naturally grow files, but shrinking past
|
|
154
|
+
// tolerance is suspicious.
|
|
155
|
+
const actualSize = stat.size;
|
|
156
|
+
base.actualSizeBytes = actualSize;
|
|
157
|
+
if (change.sizeBytes > 0) {
|
|
158
|
+
const ratio = actualSize / change.sizeBytes;
|
|
159
|
+
if (ratio < 1 - shrinkTolerance) {
|
|
160
|
+
let snippet = '';
|
|
161
|
+
try {
|
|
162
|
+
snippet = readFileSync(abs, 'utf8').slice(0, 200).replace(/\s+/g, ' ');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore */
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
status: 'shrunk',
|
|
170
|
+
detail: `actual ${actualSize}B is < ${Math.round((1 - shrinkTolerance) * 100)}% of expected ${change.sizeBytes}B${snippet ? ` (head: "${snippet}")` : ''}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (actualSize === 0) {
|
|
175
|
+
return { ...base, status: 'no-content', detail: 'file is empty' };
|
|
176
|
+
}
|
|
177
|
+
return base;
|
|
178
|
+
}
|
|
179
|
+
function handoffFor(status, totals, folderCount) {
|
|
180
|
+
if (status === 'failed') {
|
|
181
|
+
return `Scaffold integrity failed: ${totals.missing} missing, ${totals.unexpectedType} type mismatch. Re-run \`shrk apply <plan>\` or investigate manually.`;
|
|
182
|
+
}
|
|
183
|
+
if (status === 'partial') {
|
|
184
|
+
const bits = [];
|
|
185
|
+
if (totals.shrunk > 0)
|
|
186
|
+
bits.push(`${totals.shrunk} file(s) shrunk past tolerance`);
|
|
187
|
+
if (folderCount > 0)
|
|
188
|
+
bits.push(`folder op(s) inconsistent`);
|
|
189
|
+
return `Scaffold OK but with warnings: ${bits.join(', ')}. Probably fine but worth a glance.`;
|
|
190
|
+
}
|
|
191
|
+
return 'Scaffold looks intact — every planned file is on disk with the expected envelope.';
|
|
192
|
+
}
|
|
193
|
+
function renderText(r) {
|
|
194
|
+
process.stdout.write(header(`scaffold-validate — ${r.templateId}${r.name ? ` (${r.name})` : ''}`));
|
|
195
|
+
process.stdout.write(`plan: ${r.planFile}\n`);
|
|
196
|
+
process.stdout.write(`status: ${r.status}\n`);
|
|
197
|
+
process.stdout.write(`summary: ${r.totals.compliant}/${r.totals.expected} compliant, ` +
|
|
198
|
+
`${r.totals.missing} missing, ${r.totals.shrunk} shrunk, ${r.totals.unexpectedType} type-mismatch\n`);
|
|
199
|
+
process.stdout.write('\n');
|
|
200
|
+
for (const f of r.findings) {
|
|
201
|
+
const marker = f.status === 'compliant' ? '✓' : f.status === 'missing' ? '✗' : f.status === 'shrunk' ? '⚠' : '?';
|
|
202
|
+
process.stdout.write(` ${marker} ${f.relativePath} [${f.type}]`);
|
|
203
|
+
if (f.detail)
|
|
204
|
+
process.stdout.write(` — ${f.detail}`);
|
|
205
|
+
process.stdout.write('\n');
|
|
206
|
+
}
|
|
207
|
+
if (r.folderOpFindings.length > 0) {
|
|
208
|
+
process.stdout.write('\nfolder ops:\n');
|
|
209
|
+
for (const f of r.folderOpFindings) {
|
|
210
|
+
const marker = f.status === 'compliant' ? '✓' : '✗';
|
|
211
|
+
process.stdout.write(` ${marker} ${f.kind} ${f.targetPath}${f.detail ? ` — ${f.detail}` : ''}\n`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write(`\nhandoff: ${r.handoffForClaude}\n`);
|
|
215
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ICommandHandler } from '../command-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `shrk search structural` — declarative AST pattern matching with
|
|
4
|
+
* optional rewrite mode (Wave 8). Patterns are JSON (no executable
|
|
5
|
+
* predicates) — either inline via --pattern or from a file via
|
|
6
|
+
* --pattern-file.
|
|
7
|
+
*
|
|
8
|
+
* Rewrite flow:
|
|
9
|
+
* 1. Pass `--rewrite '<json>'` (a `RewriteRecipe`) to compute a
|
|
10
|
+
* rewrite plan.
|
|
11
|
+
* 2. Preview is shown by default (no fs writes).
|
|
12
|
+
* 3. Pass `--apply` to actually write the changes.
|
|
13
|
+
*
|
|
14
|
+
* The plan can be inspected as JSON via `--json` and replayed via
|
|
15
|
+
* `--plan-in <path>` (write the plan with `--plan-out <path>` first).
|
|
16
|
+
*/
|
|
17
|
+
export declare const searchStructuralCommand: ICommandHandler;
|
|
18
|
+
//# sourceMappingURL=search-structural.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-structural.command.d.ts","sourceRoot":"","sources":["../../src/commands/search-structural.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB,EAAE,eAkErC,CAAC"}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { applyRewritePlan, PatternRegistryStore, planRewrite, runSearch, signRewritePlan, STARTER_PATTERNS, verifySignedRewritePlan, } from '@shrkcrft/structural-search';
|
|
4
|
+
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
|
+
/**
|
|
7
|
+
* `shrk search structural` — declarative AST pattern matching with
|
|
8
|
+
* optional rewrite mode (Wave 8). Patterns are JSON (no executable
|
|
9
|
+
* predicates) — either inline via --pattern or from a file via
|
|
10
|
+
* --pattern-file.
|
|
11
|
+
*
|
|
12
|
+
* Rewrite flow:
|
|
13
|
+
* 1. Pass `--rewrite '<json>'` (a `RewriteRecipe`) to compute a
|
|
14
|
+
* rewrite plan.
|
|
15
|
+
* 2. Preview is shown by default (no fs writes).
|
|
16
|
+
* 3. Pass `--apply` to actually write the changes.
|
|
17
|
+
*
|
|
18
|
+
* The plan can be inspected as JSON via `--json` and replayed via
|
|
19
|
+
* `--plan-in <path>` (write the plan with `--plan-out <path>` first).
|
|
20
|
+
*/
|
|
21
|
+
export const searchStructuralCommand = {
|
|
22
|
+
name: 'search-structural',
|
|
23
|
+
description: 'Run an AST-shape pattern over the project. Patterns are declarative JSON. Optional rewrite mode: --rewrite <recipe-json> previews; --apply writes the change to disk.',
|
|
24
|
+
usage: 'shrk search-structural (--pattern <json> | --pattern-file <path>) [--limit N] [--rewrite <recipe-json>] [--rewrite-file <path>] [--apply] [--dry-run] [--plan-out <path>] [--plan-in <path>] [--sign] [--verify-signature] [--secret-env <VAR>] [--json]',
|
|
25
|
+
async run(args) {
|
|
26
|
+
const cwd = resolveCwd(args);
|
|
27
|
+
const wantJson = flagBool(args, 'json');
|
|
28
|
+
// `shrk search-structural registry ...` — pattern registry subverbs.
|
|
29
|
+
// Dispatch before any pattern parsing so a missing pattern file
|
|
30
|
+
// doesn't reject a registry-only call.
|
|
31
|
+
if (args.positional[0] === 'registry') {
|
|
32
|
+
return runRegistry({ ...args, positional: args.positional.slice(1) });
|
|
33
|
+
}
|
|
34
|
+
const planInPath = flagString(args, 'plan-in');
|
|
35
|
+
if (planInPath) {
|
|
36
|
+
return runFromSavedPlan(cwd, planInPath, args, wantJson);
|
|
37
|
+
}
|
|
38
|
+
const inline = flagString(args, 'pattern');
|
|
39
|
+
const patternFile = flagString(args, 'pattern-file');
|
|
40
|
+
if (!inline && !patternFile) {
|
|
41
|
+
process.stderr.write(this.usage + '\n');
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
let pattern;
|
|
45
|
+
try {
|
|
46
|
+
pattern = parseJson(inline, patternFile, cwd);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
process.stderr.write(`Pattern parse error: ${e.message}\n`);
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
const limit = Number(flagString(args, 'limit') ?? '200');
|
|
53
|
+
// Rewrite path?
|
|
54
|
+
const recipeInline = flagString(args, 'rewrite');
|
|
55
|
+
const recipeFile = flagString(args, 'rewrite-file');
|
|
56
|
+
if (recipeInline || recipeFile) {
|
|
57
|
+
let recipe;
|
|
58
|
+
try {
|
|
59
|
+
recipe = parseJson(recipeInline, recipeFile, cwd);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
process.stderr.write(`Recipe parse error: ${e.message}\n`);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
return runRewrite(cwd, pattern, recipe, args, wantJson);
|
|
66
|
+
}
|
|
67
|
+
// Plain match path.
|
|
68
|
+
const result = runSearch({ projectRoot: cwd, pattern, limit });
|
|
69
|
+
if (wantJson) {
|
|
70
|
+
process.stdout.write(asJson(result) + '\n');
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(header(`Structural search: ${result.pattern.summary}`));
|
|
74
|
+
process.stdout.write(kv('files scanned', String(result.filesScanned)) + '\n');
|
|
75
|
+
process.stdout.write(kv('matches', String(result.matchCount)) + '\n');
|
|
76
|
+
if (result.truncated)
|
|
77
|
+
process.stdout.write(kv('truncated', 'yes') + '\n');
|
|
78
|
+
for (const m of result.matches.slice(0, 50)) {
|
|
79
|
+
process.stdout.write(` ${m.file}:${m.line}:${m.column} ${m.nodeKind} ${m.excerpt}\n`);
|
|
80
|
+
}
|
|
81
|
+
for (const d of result.diagnostics.slice(0, 10)) {
|
|
82
|
+
process.stdout.write(`! ${d}\n`);
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
async function runRewrite(cwd, pattern, recipe, args, wantJson) {
|
|
88
|
+
const apply = flagBool(args, 'apply');
|
|
89
|
+
const dryRun = flagBool(args, 'dry-run');
|
|
90
|
+
const planOut = flagString(args, 'plan-out');
|
|
91
|
+
const wantSign = flagBool(args, 'sign');
|
|
92
|
+
const secretEnv = flagString(args, 'secret-env') ?? 'SHRKCRFT_REWRITE_SECRET';
|
|
93
|
+
const plan = planRewrite({ projectRoot: cwd, pattern, recipe });
|
|
94
|
+
if (planOut) {
|
|
95
|
+
const abs = nodePath.isAbsolute(planOut) ? planOut : nodePath.resolve(cwd, planOut);
|
|
96
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
97
|
+
mkdirSync(nodePath.dirname(abs), { recursive: true });
|
|
98
|
+
const payload = wantSign
|
|
99
|
+
? signPlanOrFail(plan, secretEnv)
|
|
100
|
+
: plan;
|
|
101
|
+
if (!payload)
|
|
102
|
+
return 2;
|
|
103
|
+
writeFileSync(abs, JSON.stringify(payload, null, 2), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
if (!apply) {
|
|
106
|
+
// Preview only.
|
|
107
|
+
if (wantJson) {
|
|
108
|
+
process.stdout.write(asJson(plan) + '\n');
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write(header(`Rewrite plan (preview): ${recipe.kind}`));
|
|
112
|
+
process.stdout.write(kv('files scanned', String(plan.filesScanned)) + '\n');
|
|
113
|
+
process.stdout.write(kv('files to change', String(plan.files.length)) + '\n');
|
|
114
|
+
process.stdout.write(kv('total edits', String(plan.totalEdits)) + '\n');
|
|
115
|
+
if (planOut)
|
|
116
|
+
process.stdout.write(kv('plan written to', planOut) + '\n');
|
|
117
|
+
for (const f of plan.files.slice(0, 20)) {
|
|
118
|
+
process.stdout.write(`\n ${f.path}:\n`);
|
|
119
|
+
for (const e of f.edits.slice(0, 5)) {
|
|
120
|
+
process.stdout.write(` L${e.line} ${truncate(e.before)} → ${truncate(e.replacement)}\n`);
|
|
121
|
+
}
|
|
122
|
+
if (f.edits.length > 5)
|
|
123
|
+
process.stdout.write(` … (${f.edits.length - 5} more edits)\n`);
|
|
124
|
+
}
|
|
125
|
+
if (plan.files.length > 20) {
|
|
126
|
+
process.stdout.write(`\n … (${plan.files.length - 20} more files)\n`);
|
|
127
|
+
}
|
|
128
|
+
for (const d of plan.diagnostics.slice(0, 10))
|
|
129
|
+
process.stdout.write(`! ${d}\n`);
|
|
130
|
+
process.stdout.write(`\nTo write these changes: rerun with --apply.\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
// Apply path.
|
|
134
|
+
const result = applyRewritePlan(plan, { projectRoot: cwd, dryRun });
|
|
135
|
+
if (wantJson) {
|
|
136
|
+
process.stdout.write(asJson({ plan, result, dryRun }) + '\n');
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(header(`Rewrite ${dryRun ? '(dry-run)' : '(applied)'}: ${recipe.kind}`));
|
|
140
|
+
process.stdout.write(kv('files attempted', String(result.filesAttempted)) + '\n');
|
|
141
|
+
process.stdout.write(kv('files changed', String(result.filesChanged)) + '\n');
|
|
142
|
+
process.stdout.write(kv('bytes written', String(result.bytesWritten)) + '\n');
|
|
143
|
+
if (result.conflicts.length > 0) {
|
|
144
|
+
process.stdout.write(kv('conflicts (skipped)', String(result.conflicts.length)) + '\n');
|
|
145
|
+
for (const c of result.conflicts.slice(0, 10))
|
|
146
|
+
process.stdout.write(` • ${c}\n`);
|
|
147
|
+
}
|
|
148
|
+
for (const d of result.diagnostics.slice(0, 10))
|
|
149
|
+
process.stdout.write(`! ${d}\n`);
|
|
150
|
+
return result.conflicts.length > 0 ? 1 : 0;
|
|
151
|
+
}
|
|
152
|
+
async function runFromSavedPlan(cwd, planPath, args, wantJson) {
|
|
153
|
+
const abs = nodePath.isAbsolute(planPath) ? planPath : nodePath.resolve(cwd, planPath);
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = JSON.parse(readFileSync(abs, 'utf8'));
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
process.stderr.write(`Plan file read error: ${e.message}\n`);
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
const apply = flagBool(args, 'apply');
|
|
163
|
+
const dryRun = flagBool(args, 'dry-run');
|
|
164
|
+
const verify = flagBool(args, 'verify-signature');
|
|
165
|
+
const secretEnv = flagString(args, 'secret-env') ?? 'SHRKCRFT_REWRITE_SECRET';
|
|
166
|
+
if (!apply) {
|
|
167
|
+
process.stderr.write('Replaying a saved plan requires --apply (or --apply --dry-run).\n');
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
const wrapper = raw;
|
|
171
|
+
const isSigned = wrapper.schema === 'sharkcraft.structural-rewrite-plan-signed/v1';
|
|
172
|
+
if (verify) {
|
|
173
|
+
if (!isSigned) {
|
|
174
|
+
process.stderr.write('--verify-signature passed but plan is not signed.\n');
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
const secret = process.env[secretEnv];
|
|
178
|
+
if (!secret) {
|
|
179
|
+
process.stderr.write(`Missing secret: env var ${secretEnv} not set.\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
const v = verifySignedRewritePlan(raw, { secret });
|
|
183
|
+
if (!v.ok) {
|
|
184
|
+
process.stderr.write(`Signature verification failed (${v.reason}): ${v.message ?? ''}\n`);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const plan = isSigned ? raw.plan : raw;
|
|
189
|
+
const result = applyRewritePlan(plan, { projectRoot: cwd, dryRun });
|
|
190
|
+
if (wantJson) {
|
|
191
|
+
process.stdout.write(asJson({ result, dryRun, verified: verify }) + '\n');
|
|
192
|
+
return result.conflicts.length > 0 ? 1 : 0;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(header(`Rewrite ${dryRun ? '(dry-run)' : '(applied)'} from saved plan`));
|
|
195
|
+
if (verify)
|
|
196
|
+
process.stdout.write(kv('signature', 'verified') + '\n');
|
|
197
|
+
process.stdout.write(kv('files changed', String(result.filesChanged)) + '\n');
|
|
198
|
+
process.stdout.write(kv('conflicts (skipped)', String(result.conflicts.length)) + '\n');
|
|
199
|
+
return result.conflicts.length > 0 ? 1 : 0;
|
|
200
|
+
}
|
|
201
|
+
function signPlanOrFail(plan, secretEnv) {
|
|
202
|
+
const secret = process.env[secretEnv];
|
|
203
|
+
if (!secret) {
|
|
204
|
+
process.stderr.write(`Missing secret: env var ${secretEnv} not set (required for --sign).\n`);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return signRewritePlan(plan, { secret });
|
|
208
|
+
}
|
|
209
|
+
function parseJson(inline, fileFlag, cwd) {
|
|
210
|
+
if (inline)
|
|
211
|
+
return JSON.parse(inline);
|
|
212
|
+
const abs = nodePath.isAbsolute(fileFlag)
|
|
213
|
+
? fileFlag
|
|
214
|
+
: nodePath.resolve(cwd, fileFlag);
|
|
215
|
+
return JSON.parse(readFileSync(abs, 'utf8'));
|
|
216
|
+
}
|
|
217
|
+
function truncate(s) {
|
|
218
|
+
const oneLine = s.replace(/\s+/g, ' ').trim();
|
|
219
|
+
return oneLine.length > 60 ? oneLine.slice(0, 57) + '…' : oneLine;
|
|
220
|
+
}
|
|
221
|
+
async function runRegistry(args) {
|
|
222
|
+
const cwd = resolveCwd(args);
|
|
223
|
+
const wantJson = flagBool(args, 'json');
|
|
224
|
+
const sub = args.positional[0] ?? 'list';
|
|
225
|
+
const store = new PatternRegistryStore(cwd);
|
|
226
|
+
if (sub === 'list') {
|
|
227
|
+
const reg = store.read();
|
|
228
|
+
if (wantJson) {
|
|
229
|
+
process.stdout.write(asJson({ path: store.absPath, registry: reg }) + '\n');
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
if (reg.patterns.length === 0) {
|
|
233
|
+
process.stdout.write('No patterns registered.\n');
|
|
234
|
+
process.stdout.write(`(file: ${store.absPath})\n`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
process.stdout.write(header(`Pattern registry (${reg.patterns.length})`));
|
|
238
|
+
process.stdout.write(kv('path', store.absPath) + '\n');
|
|
239
|
+
for (const p of reg.patterns) {
|
|
240
|
+
const err = p.lastValidationError ? ` ✗ ${p.lastValidationError}` : '';
|
|
241
|
+
const validated = p.lastValidatedAt ? ` [validated ${p.lastValidatedAt}]` : '';
|
|
242
|
+
process.stdout.write(` • ${p.id} (${p.pattern.kind})${validated}${err}\n`);
|
|
243
|
+
if (p.title)
|
|
244
|
+
process.stdout.write(` ${p.title}\n`);
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
if (sub === 'add') {
|
|
249
|
+
const id = flagString(args, 'id');
|
|
250
|
+
const inline = flagString(args, 'pattern');
|
|
251
|
+
const patternFile = flagString(args, 'pattern-file');
|
|
252
|
+
const title = flagString(args, 'title');
|
|
253
|
+
const description = flagString(args, 'description');
|
|
254
|
+
if (!id) {
|
|
255
|
+
process.stderr.write('Usage: shrk search-structural registry add --id <id> (--pattern <json> | --pattern-file <path>) [--title "..."] [--description "..."]\n');
|
|
256
|
+
return 2;
|
|
257
|
+
}
|
|
258
|
+
if (!inline && !patternFile) {
|
|
259
|
+
process.stderr.write('Missing pattern body (--pattern or --pattern-file).\n');
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
let pattern;
|
|
263
|
+
try {
|
|
264
|
+
pattern = parseJson(inline, patternFile, cwd);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
process.stderr.write(`Pattern parse error: ${e.message}\n`);
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
const envelope = {
|
|
271
|
+
schema: 'sharkcraft.structural-pattern/v1',
|
|
272
|
+
id,
|
|
273
|
+
...(title ? { title } : {}),
|
|
274
|
+
...(description ? { description } : {}),
|
|
275
|
+
pattern,
|
|
276
|
+
};
|
|
277
|
+
const { result, entry } = store.add(envelope);
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
if (wantJson) {
|
|
280
|
+
process.stdout.write(asJson({ ok: false, error: result.error }) + '\n');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
process.stderr.write(`Pattern rejected: ${result.error}\n`);
|
|
284
|
+
}
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
287
|
+
if (wantJson) {
|
|
288
|
+
process.stdout.write(asJson({ ok: true, entry, path: store.absPath }) + '\n');
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
process.stdout.write(`Pattern registered: ${entry?.id} (${entry?.pattern.kind})\n`);
|
|
292
|
+
process.stdout.write(` → ${store.absPath}\n`);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (sub === 'remove') {
|
|
296
|
+
const id = args.positional[1] ?? flagString(args, 'id');
|
|
297
|
+
if (!id) {
|
|
298
|
+
process.stderr.write('Usage: shrk search-structural registry remove <id>\n');
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
const removed = store.remove(id);
|
|
302
|
+
if (wantJson) {
|
|
303
|
+
process.stdout.write(asJson({ removed, id }) + '\n');
|
|
304
|
+
return removed ? 0 : 1;
|
|
305
|
+
}
|
|
306
|
+
process.stdout.write(removed ? `Removed pattern "${id}".\n` : `No pattern with id "${id}".\n`);
|
|
307
|
+
return removed ? 0 : 1;
|
|
308
|
+
}
|
|
309
|
+
if (sub === 'validate') {
|
|
310
|
+
const result = store.validateAll();
|
|
311
|
+
if (wantJson) {
|
|
312
|
+
process.stdout.write(asJson(result) + '\n');
|
|
313
|
+
return result.failed === 0 ? 0 : 1;
|
|
314
|
+
}
|
|
315
|
+
process.stdout.write(header('Pattern registry validation'));
|
|
316
|
+
process.stdout.write(kv('total', String(result.total)) + '\n');
|
|
317
|
+
process.stdout.write(kv('failed', String(result.failed)) + '\n');
|
|
318
|
+
if (result.failed > 0) {
|
|
319
|
+
process.stdout.write('\nFailures:\n');
|
|
320
|
+
for (const e of result.errors) {
|
|
321
|
+
process.stdout.write(` ✗ ${e.id} — ${e.error}\n`);
|
|
322
|
+
}
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
process.stdout.write('\nAll patterns valid.\n');
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
if (sub === 'clear') {
|
|
329
|
+
const cleared = store.clear();
|
|
330
|
+
if (wantJson) {
|
|
331
|
+
process.stdout.write(asJson({ cleared, path: store.absPath }) + '\n');
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
process.stdout.write(cleared ? `Cleared ${store.absPath}\n` : 'No registry file to clear.\n');
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
if (sub === 'seed') {
|
|
338
|
+
const force = flagBool(args, 'force');
|
|
339
|
+
const existing = store.read();
|
|
340
|
+
if (existing.patterns.length > 0 && !force) {
|
|
341
|
+
const msg = `Registry already has ${existing.patterns.length} pattern(s). Use --force to merge / overwrite seed entries.\n`;
|
|
342
|
+
if (wantJson) {
|
|
343
|
+
process.stdout.write(asJson({ ok: false, error: 'non-empty' }) + '\n');
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
process.stderr.write(msg);
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
const added = [];
|
|
350
|
+
const failed = [];
|
|
351
|
+
for (const envelope of STARTER_PATTERNS) {
|
|
352
|
+
const { result, entry } = store.add(envelope);
|
|
353
|
+
if (result.ok && entry) {
|
|
354
|
+
added.push(entry.id);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
failed.push({ id: envelope.id ?? '?', error: result.error ?? 'unknown' });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (wantJson) {
|
|
361
|
+
process.stdout.write(asJson({ ok: failed.length === 0, added, failed, path: store.absPath }) + '\n');
|
|
362
|
+
return failed.length === 0 ? 0 : 1;
|
|
363
|
+
}
|
|
364
|
+
process.stdout.write(`Seeded ${added.length} starter pattern(s) → ${store.absPath}\n`);
|
|
365
|
+
for (const id of added)
|
|
366
|
+
process.stdout.write(` + ${id}\n`);
|
|
367
|
+
if (failed.length > 0) {
|
|
368
|
+
process.stdout.write('\nFailures:\n');
|
|
369
|
+
for (const f of failed)
|
|
370
|
+
process.stdout.write(` ✗ ${f.id} — ${f.error}\n`);
|
|
371
|
+
}
|
|
372
|
+
return failed.length === 0 ? 0 : 1;
|
|
373
|
+
}
|
|
374
|
+
process.stderr.write('Usage: shrk search-structural registry <list|add|remove|validate|clear|seed> [--json]\n');
|
|
375
|
+
return 2;
|
|
376
|
+
}
|
|
@@ -27,7 +27,7 @@ function parseSources(args) {
|
|
|
27
27
|
}
|
|
28
28
|
export const searchCommand = {
|
|
29
29
|
name: 'search',
|
|
30
|
-
description: 'Universal search across commands, MCP tools, knowledge, rules, paths, conventions, templates, helpers, playbooks, constructs, policies, decisions, scaffold patterns, contract templates, migration profiles,
|
|
30
|
+
description: 'Universal search across commands, MCP tools, knowledge, rules, paths, conventions, templates, helpers, playbooks, constructs, policies, decisions, scaffold patterns, contract templates, migration profiles, feedback rules, task routing hints, docs, recent reports. Default emits the 7-section unified output; pass --legacy for the flat output.',
|
|
31
31
|
usage: 'shrk search <query> [--kind <kind>] [--source local|pack|...] [--limit N] [--explain] [--commands-only] [--actions-only] [--format text|markdown|json] [--legacy]',
|
|
32
32
|
async run(args) {
|
|
33
33
|
// Sub-dispatch for `shrk search tuning [list|doctor]`.
|