@shrkcrft/cli 0.1.0-alpha.11 → 0.1.0-alpha.12

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.
Files changed (41) hide show
  1. package/dist/commands/ask.command.d.ts.map +1 -1
  2. package/dist/commands/ask.command.js +10 -9
  3. package/dist/commands/command-catalog.d.ts.map +1 -1
  4. package/dist/commands/command-catalog.js +100 -1
  5. package/dist/commands/deps-audit.command.d.ts +23 -0
  6. package/dist/commands/deps-audit.command.d.ts.map +1 -0
  7. package/dist/commands/deps-audit.command.js +266 -0
  8. package/dist/commands/doctor.command.d.ts.map +1 -1
  9. package/dist/commands/doctor.command.js +60 -1
  10. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  11. package/dist/commands/graph-code-subverbs.js +144 -26
  12. package/dist/commands/graph.command.d.ts.map +1 -1
  13. package/dist/commands/graph.command.js +3 -2
  14. package/dist/commands/help.command.d.ts.map +1 -1
  15. package/dist/commands/help.command.js +22 -1
  16. package/dist/commands/impact.command.d.ts.map +1 -1
  17. package/dist/commands/impact.command.js +3 -2
  18. package/dist/commands/move-plan.command.d.ts +23 -0
  19. package/dist/commands/move-plan.command.d.ts.map +1 -0
  20. package/dist/commands/move-plan.command.js +360 -0
  21. package/dist/commands/scaffold-validate.command.d.ts +22 -0
  22. package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
  23. package/dist/commands/scaffold-validate.command.js +215 -0
  24. package/dist/commands/smart-context.command.d.ts +30 -0
  25. package/dist/commands/smart-context.command.d.ts.map +1 -0
  26. package/dist/commands/smart-context.command.js +3763 -0
  27. package/dist/commands/spike.command.d.ts +22 -0
  28. package/dist/commands/spike.command.d.ts.map +1 -0
  29. package/dist/commands/spike.command.js +235 -0
  30. package/dist/commands/watch.command.d.ts +26 -0
  31. package/dist/commands/watch.command.d.ts.map +1 -0
  32. package/dist/commands/watch.command.js +456 -0
  33. package/dist/env/load-dotenv.d.ts +15 -0
  34. package/dist/env/load-dotenv.d.ts.map +1 -0
  35. package/dist/env/load-dotenv.js +70 -0
  36. package/dist/main.d.ts.map +1 -1
  37. package/dist/main.js +83 -2
  38. package/dist/schemas/json-schemas.d.ts +384 -36
  39. package/dist/schemas/json-schemas.d.ts.map +1 -1
  40. package/dist/schemas/json-schemas.js +247 -36
  41. package/package.json +33 -31
@@ -0,0 +1,360 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
4
+ import { flagBool, resolveCwd } from "../command-registry.js";
5
+ import { asJson, header } from "../output/format-output.js";
6
+ /**
7
+ * `shrk move-plan <source> <target>` — emit a structured plan for
8
+ * moving a source file to a new location. Read-only: NEVER moves
9
+ * anything itself, never rewrites imports. The agent (or a human)
10
+ * uses the plan to do the actual work.
11
+ *
12
+ * Covers:
13
+ * - graph-traced importer rewrites (relative + package-name imports)
14
+ * - exports to revisit if the source was re-exported from an index
15
+ * - cross-package warnings (layer-order risks)
16
+ * - a rollback plan that's the literal reverse of the move
17
+ * - validation commands to run after applying
18
+ *
19
+ * Limitations:
20
+ * - single-file only. Directory moves can be approximated by
21
+ * listing the directory and running this command per file, or
22
+ * wait for a `--folder` follow-up.
23
+ * - `from '@shrkcrft/pkg/sub'` deep-import rewrites are best-effort;
24
+ * ambiguous cases get reported as `review-manually`.
25
+ */
26
+ export const movePlanCommand = {
27
+ name: 'move-plan',
28
+ description: 'Plan a file move: graph-traced importer rewrites, export touch-ups, cross-package warnings. Read-only.',
29
+ usage: 'shrk move-plan <source-file> <target-file> [--json]',
30
+ async run(args) {
31
+ const sourceArg = args.positional[0]?.trim();
32
+ const targetArg = args.positional[1]?.trim();
33
+ if (!sourceArg || !targetArg) {
34
+ process.stderr.write('Usage: shrk move-plan <source-file> <target-file> [--json]\n');
35
+ return 2;
36
+ }
37
+ const cwd = resolveCwd(args);
38
+ const json = flagBool(args, 'json');
39
+ const sourceRel = relativize(cwd, sourceArg);
40
+ const targetRel = relativize(cwd, targetArg);
41
+ if (!existsSync(nodePath.join(cwd, sourceRel))) {
42
+ process.stderr.write(`Source file does not exist: ${sourceRel}\n`);
43
+ return 1;
44
+ }
45
+ if (existsSync(nodePath.join(cwd, targetRel))) {
46
+ process.stderr.write(`Target already exists (won't propose moving on top of it): ${targetRel}\n`);
47
+ return 1;
48
+ }
49
+ const store = new GraphStore(cwd);
50
+ if (!store.exists()) {
51
+ process.stderr.write('No SharkCraft graph found. Run `shrk graph build` so move-plan can trace importers.\n');
52
+ return 1;
53
+ }
54
+ const api = GraphQueryApi.fromStore(cwd);
55
+ const sourcePackage = resolveOwningPackage(cwd, sourceRel);
56
+ const targetPackage = resolveOwningPackage(cwd, targetRel);
57
+ const crossesPackages = sourcePackage?.name !== targetPackage?.name;
58
+ const importerNodes = (() => {
59
+ const fileNode = api.findFile(sourceRel);
60
+ if (!fileNode)
61
+ return [];
62
+ return api.importersOf(fileNode.id)
63
+ .map((n) => n.path)
64
+ .filter((p) => typeof p === 'string' && p.length > 0);
65
+ })();
66
+ const importsToRewrite = [];
67
+ const exportsToUpdate = [];
68
+ for (const importerPath of importerNodes) {
69
+ const abs = nodePath.join(cwd, importerPath);
70
+ if (!existsSync(abs))
71
+ continue;
72
+ let body;
73
+ try {
74
+ body = readFileSync(abs, 'utf8');
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ const rewrites = computeRewritesForFile({
80
+ importerPath,
81
+ importerBody: body,
82
+ sourceRel,
83
+ targetRel,
84
+ sourcePackage,
85
+ targetPackage,
86
+ });
87
+ importsToRewrite.push(...rewrites);
88
+ // If the importer is `<pkg>/src/index.ts` and re-exports the source,
89
+ // it likely needs to drop / move that export.
90
+ if (/(^|\/)index\.[jt]sx?$/.test(importerPath) && body.includes(sourceBaseSpecifier(sourceRel))) {
91
+ exportsToUpdate.push({
92
+ file: importerPath,
93
+ action: 'review-manually',
94
+ reason: `index file re-exports something from "${sourceRel}"; relocate the re-export to point at "${targetRel}" or drop it.`,
95
+ });
96
+ }
97
+ }
98
+ // Also check if the source file itself is referenced from its own
99
+ // package's index.ts.
100
+ if (sourcePackage) {
101
+ const sourceIndex = nodePath.join(cwd, sourcePackage.dir, 'src/index.ts');
102
+ const sourceIndexRel = nodePath.relative(cwd, sourceIndex);
103
+ const alreadyFlagged = exportsToUpdate.some((e) => e.file === sourceIndexRel);
104
+ if (!alreadyFlagged && existsSync(sourceIndex)) {
105
+ try {
106
+ const body = readFileSync(sourceIndex, 'utf8');
107
+ if (body.includes(sourceBaseSpecifier(sourceRel))) {
108
+ exportsToUpdate.push({
109
+ file: sourceIndexRel,
110
+ action: 'edit',
111
+ reason: crossesPackages
112
+ ? `index re-exports the moved file; drop the export (the file now lives in another package) or replace with a forwarding re-export.`
113
+ : `index re-exports the moved file; update the relative path to point at the new location.`,
114
+ });
115
+ }
116
+ }
117
+ catch {
118
+ /* ignore */
119
+ }
120
+ }
121
+ }
122
+ const affectedPackages = unique([
123
+ ...importerNodes.map((p) => resolveOwningPackage(cwd, p)?.name ?? null),
124
+ sourcePackage?.name ?? null,
125
+ targetPackage?.name ?? null,
126
+ ].filter((n) => typeof n === 'string'));
127
+ const risks = [];
128
+ if (crossesPackages) {
129
+ risks.push(`Cross-package move: ${sourcePackage?.name ?? '(no package)'} → ${targetPackage?.name ?? '(no package)'}. Run \`shrk check boundaries\` after the move to confirm the new home is layered correctly.`);
130
+ }
131
+ if (importerNodes.length > 30) {
132
+ risks.push(`${importerNodes.length} importer(s) — large blast radius. Consider scripting the rewrites or moving in smaller chunks.`);
133
+ }
134
+ if (exportsToUpdate.some((e) => e.action === 'review-manually')) {
135
+ risks.push('Some index re-exports were flagged for manual review — automatic rewriting could lose intent.');
136
+ }
137
+ const movePlan = [
138
+ {
139
+ order: 1,
140
+ title: 'Move the file',
141
+ details: `git mv "${sourceRel}" "${targetRel}" (then verify content unchanged)`,
142
+ },
143
+ ...importsToRewrite.map((rw, i) => ({
144
+ order: 2 + i,
145
+ title: `Rewrite import in ${rw.file}`,
146
+ details: `Replace \`from '${rw.fromImport}'\` with \`from '${rw.toImport}'\`. Reason: ${rw.reason}`,
147
+ })),
148
+ ...(exportsToUpdate.length > 0
149
+ ? [
150
+ {
151
+ order: 2 + importsToRewrite.length,
152
+ title: 'Reconcile index re-exports',
153
+ details: exportsToUpdate
154
+ .map((e) => `${e.file} (${e.action}): ${e.reason}`)
155
+ .join(' || '),
156
+ },
157
+ ]
158
+ : []),
159
+ ];
160
+ const rollbackPlan = [
161
+ { order: 1, title: 'Restore file', details: `git mv "${targetRel}" "${sourceRel}"` },
162
+ {
163
+ order: 2,
164
+ title: 'Revert import rewrites',
165
+ details: importsToRewrite.length === 0
166
+ ? 'No importers to revert.'
167
+ : `git restore ${unique(importsToRewrite.map((r) => r.file)).join(' ')}`,
168
+ },
169
+ ];
170
+ const validationCommands = [
171
+ 'bun x tsc -p tsconfig.base.json --noEmit',
172
+ 'shrk check boundaries',
173
+ 'shrk check imports',
174
+ 'bun test',
175
+ ];
176
+ const status = importsToRewrite.length === 0 && exportsToUpdate.length === 0
177
+ ? 'no-importers'
178
+ : crossesPackages
179
+ ? 'cross-package'
180
+ : 'in-package';
181
+ const handoff = status === 'no-importers'
182
+ ? `Move is trivial — no importers reference "${sourceRel}". Just run the file move + validation.`
183
+ : status === 'cross-package'
184
+ ? `Move crosses packages. Apply each importsToRewrite[] edit, then revisit exportsToUpdate[], then run validationCommands. Re-run \`shrk check boundaries\` to confirm layering.`
185
+ : `In-package move with ${importsToRewrite.length} importer rewrite(s). Safe but worth applying mechanically.`;
186
+ const report = {
187
+ schema: 'sharkcraft.move-plan/v1',
188
+ source: { path: sourceRel, package: sourcePackage?.name ?? null },
189
+ target: { path: targetRel, package: targetPackage?.name ?? null },
190
+ crossesPackages,
191
+ movePlan,
192
+ importsToRewrite,
193
+ exportsToUpdate,
194
+ affectedPackages,
195
+ risks,
196
+ rollbackPlan,
197
+ validationCommands,
198
+ handoffForClaude: handoff,
199
+ };
200
+ if (json) {
201
+ process.stdout.write(asJson(report) + '\n');
202
+ return 0;
203
+ }
204
+ renderText(report);
205
+ return 0;
206
+ },
207
+ };
208
+ function relativize(cwd, raw) {
209
+ const cleaned = raw.replace(/\\/g, '/').replace(/^\.\//, '');
210
+ if (nodePath.isAbsolute(cleaned)) {
211
+ return nodePath.relative(cwd, cleaned).replace(/\\/g, '/');
212
+ }
213
+ return cleaned;
214
+ }
215
+ const PACKAGE_CACHE = new Map();
216
+ function listPackages(cwd) {
217
+ const cached = PACKAGE_CACHE.get(cwd);
218
+ if (cached)
219
+ return cached;
220
+ const out = [];
221
+ for (const root of ['packages', 'libs', 'apps']) {
222
+ const rootAbs = nodePath.join(cwd, root);
223
+ if (!existsSync(rootAbs))
224
+ continue;
225
+ let entries;
226
+ try {
227
+ entries = readdirSync(rootAbs);
228
+ }
229
+ catch {
230
+ continue;
231
+ }
232
+ for (const entry of entries) {
233
+ const dir = nodePath.join(root, entry);
234
+ const pkgJson = nodePath.join(cwd, dir, 'package.json');
235
+ if (!existsSync(pkgJson))
236
+ continue;
237
+ try {
238
+ const parsed = JSON.parse(readFileSync(pkgJson, 'utf8'));
239
+ if (parsed.name)
240
+ out.push({ name: parsed.name, dir });
241
+ }
242
+ catch {
243
+ // skip
244
+ }
245
+ }
246
+ }
247
+ PACKAGE_CACHE.set(cwd, out);
248
+ return out;
249
+ }
250
+ function resolveOwningPackage(cwd, fileRel) {
251
+ const packages = listPackages(cwd);
252
+ let best = null;
253
+ for (const p of packages) {
254
+ const dirSlash = p.dir.replace(/\\/g, '/') + '/';
255
+ if (fileRel.startsWith(dirSlash)) {
256
+ if (!best || p.dir.length > best.dir.length)
257
+ best = p;
258
+ }
259
+ }
260
+ return best;
261
+ }
262
+ function sourceBaseSpecifier(sourceRel) {
263
+ // The plain basename without extension — usable as a fuzzy substring
264
+ // match against `from '../foo'` re-exports.
265
+ const ext = nodePath.extname(sourceRel);
266
+ return nodePath.basename(sourceRel, ext);
267
+ }
268
+ function computeRewritesForFile(input) {
269
+ const out = [];
270
+ const seen = new Set();
271
+ const importerDir = nodePath.dirname(input.importerPath);
272
+ const expectedRelativeBefore = toRelativeSpecifier(importerDir, input.sourceRel);
273
+ const expectedRelativeAfter = toRelativeSpecifier(importerDir, input.targetRel);
274
+ // Pattern 1: relative imports that resolve to the source file.
275
+ // Try both with-extension and without-extension forms.
276
+ const candidates = [expectedRelativeBefore, stripJsTsExt(expectedRelativeBefore)];
277
+ for (const cand of candidates) {
278
+ if (cand.length === 0)
279
+ continue;
280
+ if (!input.importerBody.includes(`'${cand}'`) && !input.importerBody.includes(`"${cand}"`))
281
+ continue;
282
+ const targetCand = stripJsTsExt(expectedRelativeAfter);
283
+ const key = `rel:${cand}`;
284
+ if (seen.has(key))
285
+ continue;
286
+ seen.add(key);
287
+ out.push({
288
+ file: input.importerPath,
289
+ fromImport: cand,
290
+ toImport: targetCand,
291
+ reason: 'relative-path rewrite (importer-relative)',
292
+ });
293
+ }
294
+ // Pattern 2: package-name imports `@shrkcrft/xxx` when the source is
295
+ // inside that package and the target is in a different one.
296
+ if (input.sourcePackage && input.targetPackage && input.sourcePackage.name !== input.targetPackage.name) {
297
+ const fromSpec = input.sourcePackage.name;
298
+ const toSpec = input.targetPackage.name;
299
+ if ((input.importerBody.includes(`'${fromSpec}'`) || input.importerBody.includes(`"${fromSpec}"`)) &&
300
+ // and the package-name import is actually exposing the moved symbol
301
+ // (we can't know for sure without re-resolving — flag as review).
302
+ input.importerPath !== fromIndexPath(input.sourcePackage)) {
303
+ const key = `pkg:${fromSpec}->${toSpec}`;
304
+ if (!seen.has(key)) {
305
+ seen.add(key);
306
+ out.push({
307
+ file: input.importerPath,
308
+ fromImport: fromSpec,
309
+ toImport: toSpec,
310
+ reason: 'package-name import: importer likely consumes the symbol via the old package. Verify the target package re-exports the symbol from its public index before applying.',
311
+ });
312
+ }
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+ function fromIndexPath(pkg) {
318
+ return nodePath.join(pkg.dir, 'src/index.ts');
319
+ }
320
+ function toRelativeSpecifier(fromDir, toRel) {
321
+ const rel = nodePath.relative(fromDir, toRel).replace(/\\/g, '/');
322
+ if (!rel.startsWith('.'))
323
+ return `./${rel}`;
324
+ return rel;
325
+ }
326
+ function stripJsTsExt(p) {
327
+ return p.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
328
+ }
329
+ function unique(items) {
330
+ return [...new Set(items)];
331
+ }
332
+ function renderText(r) {
333
+ process.stdout.write(header(`move-plan: ${r.source.path} → ${r.target.path}${r.crossesPackages ? ' (cross-package)' : ''}`));
334
+ process.stdout.write(`affected packages: ${r.affectedPackages.join(', ') || '(none)'}\n\n`);
335
+ process.stdout.write(`move plan (${r.movePlan.length} step(s)):\n`);
336
+ for (const s of r.movePlan) {
337
+ process.stdout.write(` ${s.order}. ${s.title} — ${s.details}\n`);
338
+ }
339
+ if (r.importsToRewrite.length > 0) {
340
+ process.stdout.write(`\nimport rewrites (${r.importsToRewrite.length}):\n`);
341
+ for (const rw of r.importsToRewrite) {
342
+ process.stdout.write(` ${rw.file}\n from '${rw.fromImport}' → to '${rw.toImport}'\n (${rw.reason})\n`);
343
+ }
344
+ }
345
+ if (r.exportsToUpdate.length > 0) {
346
+ process.stdout.write('\nexports to update:\n');
347
+ for (const e of r.exportsToUpdate) {
348
+ process.stdout.write(` ${e.file} [${e.action}] — ${e.reason}\n`);
349
+ }
350
+ }
351
+ if (r.risks.length > 0) {
352
+ process.stdout.write('\nrisks:\n');
353
+ for (const r2 of r.risks)
354
+ process.stdout.write(` - ${r2}\n`);
355
+ }
356
+ process.stdout.write('\nvalidation commands:\n');
357
+ for (const c of r.validationCommands)
358
+ process.stdout.write(` $ ${c}\n`);
359
+ process.stdout.write(`\nhandoff: ${r.handoffForClaude}\n`);
360
+ }
@@ -0,0 +1,22 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ /**
3
+ * `shrk scaffold-validate <plan-file>` — verify that the files
4
+ * recorded in a saved generation plan actually exist on disk and
5
+ * look like they came from the template.
6
+ *
7
+ * Read-only: never writes, never re-runs the template. Designed to
8
+ * run after `shrk apply` (or after a human manually copied a plan
9
+ * into place) to catch:
10
+ * - missing files (apply was interrupted, or somebody deleted)
11
+ * - shrunken files (someone replaced the body with `// TODO`)
12
+ * - mismatched operation type (plan said `create`, disk shows
13
+ * something else)
14
+ *
15
+ * NOT a full template-replay check — we can't know the exact
16
+ * expected body without re-rendering the template, which would
17
+ * be far more expensive and would couple this command to every
18
+ * template's variable resolution. Sizes + existence + type catches
19
+ * 95% of real-world failures.
20
+ */
21
+ export declare const scaffoldValidateCommand: ICommandHandler;
22
+ //# sourceMappingURL=scaffold-validate.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold-validate.command.d.ts","sourceRoot":"","sources":["../../src/commands/scaffold-validate.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAgChC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,EAAE,eAkGrC,CAAC"}
@@ -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,30 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ /**
3
+ * Gemini-backed context enrichment.
4
+ *
5
+ * Sits next to `shrk ask` as an explicit, opt-in AI surface — the
6
+ * deterministic engine (`shrk context`, `shrk brief`, MCP tools) stays
7
+ * AI-free. See docs/smart-context.md and the
8
+ * `.claude/skills/shrk-smart-context/` skill for the agent workflow.
9
+ *
10
+ * Verbs:
11
+ * - `smart-context "<task>"` — single brief (default).
12
+ * - `smart-context "<task>" --plan` — single structured plan.
13
+ * - `smart-context "<task>" --ai-plan` — two-stage AI-assisted plan.
14
+ * - `smart-context "<task>" --save` — persist under .sharkcraft/smart-context/.
15
+ * - `smart-context plan-ahead "t1" "t2"` — batch-save plans for an upcoming queue.
16
+ * - `smart-context list` — list saved entries.
17
+ * - `smart-context show <slug>` — print a saved entry.
18
+ */
19
+ export declare const smartContextCommand: ICommandHandler;
20
+ /** `shrk smart-context plan-ahead "task1" "task2" ...` — batch-saves plans. */
21
+ export declare const smartContextPlanAheadCommand: ICommandHandler;
22
+ /** `shrk smart-context list` — list saved entries. */
23
+ export declare const smartContextListCommand: ICommandHandler;
24
+ /** `shrk smart-context show <slug>` — print a saved entry. */
25
+ export declare const smartContextShowCommand: ICommandHandler;
26
+ /** `shrk smart-context embeddings build` — (re)build the semantic index. */
27
+ export declare const smartContextEmbeddingsBuildCommand: ICommandHandler;
28
+ /** `shrk smart-context embeddings-status` — freshness report (no model load). */
29
+ export declare const smartContextEmbeddingsStatusCommand: ICommandHandler;
30
+ //# sourceMappingURL=smart-context.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6BhC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,eA+JjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAmF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAgHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC"}