@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,456 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, watch as fsWatch, writeFileSync, } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+ import { buildTaskPacket, inspectSharkcraft } from '@shrkcrft/inspector';
5
+ import { SemanticIndex, buildFocusedContext, classifyTask, parseTaskTypeOverride, } from '@shrkcrft/embeddings';
6
+ import { flagBool, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
7
+ import { asJson } from "../output/format-output.js";
8
+ const FEED_DIR = nodePath.join('.sharkcraft', 'feed');
9
+ const DEFAULT_DEBOUNCE_MS = 750;
10
+ const DEFAULT_INTERVAL_MS = 0; // off by default; --interval to enable
11
+ const MANIFEST_SCHEMA = 'sharkcraft.shrk-watch-manifest/v1';
12
+ function manifestPath(cwd, slug) {
13
+ return nodePath.join(cwd, FEED_DIR, `${slug}.pid.json`);
14
+ }
15
+ function readManifest(path) {
16
+ if (!existsSync(path))
17
+ return null;
18
+ try {
19
+ const m = JSON.parse(readFileSync(path, 'utf8'));
20
+ if (m.schema !== MANIFEST_SCHEMA || typeof m.pid !== 'number')
21
+ return null;
22
+ return m;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function isProcessAlive(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * `shrk watch "<task>"` — emit a fresh task-focused context bundle on
39
+ * stdout JSONL each time the workspace changes.
40
+ *
41
+ * Designed to run in a terminal *next to* Claude Code so the agent has
42
+ * a continuously-refreshed "feed" of the most relevant code for the
43
+ * current task — no extra LLM calls, just BGE-ranked deltas.
44
+ *
45
+ * Surfaces:
46
+ * - stdout: one JSON line per packet, NDJSON-style.
47
+ * - filesystem: same packet appended to
48
+ * `.sharkcraft/feed/<slug>.jsonl` so other tools (or the user)
49
+ * can tail it from elsewhere.
50
+ *
51
+ * Cost: every emission is O(BGE-search + re-rank) — typically
52
+ * 100–300 ms for ~10 candidate files. We never call a generative LLM.
53
+ */
54
+ export const watchCommand = {
55
+ name: 'watch',
56
+ description: 'Emit a focused-context packet on stdout JSONL each time the workspace changes (or every --interval seconds). No LLM calls.',
57
+ usage: 'shrk watch "<task>" [--interval N] [--debounce ms] [--task-type <type>] [--once] [--quiet] [--json]',
58
+ async run(args) {
59
+ const task = args.positional.join(' ').trim();
60
+ if (!task) {
61
+ process.stderr.write('Usage: shrk watch "<task>" [--interval N] [--once] [--debounce ms]\n');
62
+ return 2;
63
+ }
64
+ const cwd = resolveCwd(args);
65
+ const once = flagBool(args, 'once');
66
+ const quiet = flagBool(args, 'quiet') || flagBool(args, 'json');
67
+ const debounceMs = flagNumber(args, 'debounce') ?? DEFAULT_DEBOUNCE_MS;
68
+ const intervalSec = flagNumber(args, 'interval') ?? 0;
69
+ const intervalMs = intervalSec > 0 ? intervalSec * 1000 : DEFAULT_INTERVAL_MS;
70
+ const overrideRaw = flagString(args, 'task-type');
71
+ const taskTypeOverride = parseTaskTypeOverride(overrideRaw);
72
+ const index = await SemanticIndex.tryLoad(cwd);
73
+ if (!index) {
74
+ process.stderr.write('[shrk watch] no semantic index — run `shrk smart-context embeddings-build` first.\n');
75
+ return 1;
76
+ }
77
+ const slug = slugify(task);
78
+ const feedDir = nodePath.join(cwd, FEED_DIR);
79
+ mkdirSync(feedDir, { recursive: true });
80
+ const feedPath = nodePath.join(feedDir, `${slug}.jsonl`);
81
+ const manifestFilePath = manifestPath(cwd, slug);
82
+ // Daemon manifest check — one watcher per (cwd, slug). Allows the agent
83
+ // and the human to know which watcher owns which feed, and prevents two
84
+ // processes from racing on the same JSONL.
85
+ if (!once) {
86
+ const existing = readManifest(manifestFilePath);
87
+ if (existing && isProcessAlive(existing.pid)) {
88
+ if (flagBool(args, 'replace')) {
89
+ try {
90
+ process.kill(existing.pid, 'SIGTERM');
91
+ }
92
+ catch {
93
+ // best effort
94
+ }
95
+ // Give the old process a moment to clean up.
96
+ await new Promise((r) => setTimeout(r, 250));
97
+ }
98
+ else {
99
+ process.stderr.write(`[shrk watch] another watcher is already running for slug "${slug}" (pid ${existing.pid}).\n` +
100
+ ` → Use \`shrk watch list\` to see active feeds.\n` +
101
+ ` → Use \`shrk watch stop ${slug}\` to stop it.\n` +
102
+ ` → Pass --replace to take over.\n`);
103
+ return 1;
104
+ }
105
+ }
106
+ else if (existing) {
107
+ // Stale manifest (pid not alive) — clean up.
108
+ try {
109
+ rmSync(manifestFilePath, { force: true });
110
+ }
111
+ catch {
112
+ /* ignore */
113
+ }
114
+ }
115
+ const manifest = {
116
+ schema: MANIFEST_SCHEMA,
117
+ pid: process.pid,
118
+ slug,
119
+ task,
120
+ startedAt: new Date().toISOString(),
121
+ feedPath,
122
+ intervalMs,
123
+ debounceMs,
124
+ };
125
+ writeFileSync(manifestFilePath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
126
+ }
127
+ if (!quiet) {
128
+ process.stderr.write(`[shrk watch] task: "${task}"\n[shrk watch] feed: ${feedPath}\n[shrk watch] press Ctrl+C to stop.\n`);
129
+ }
130
+ let lastHash = '';
131
+ let lastEmittedAt = 0;
132
+ const emit = async (reason) => {
133
+ const packet = await buildPacket({
134
+ cwd,
135
+ task,
136
+ index,
137
+ taskTypeOverride,
138
+ reason,
139
+ });
140
+ const fingerprint = packet.fingerprint;
141
+ if (fingerprint === lastHash) {
142
+ // Same content — skip noisy duplicate emissions.
143
+ return;
144
+ }
145
+ lastHash = fingerprint;
146
+ lastEmittedAt = Date.now();
147
+ const line = asJson(packet);
148
+ process.stdout.write(line + '\n');
149
+ try {
150
+ appendFileSync(feedPath, line + '\n', 'utf8');
151
+ }
152
+ catch (e) {
153
+ process.stderr.write(`[shrk watch] feed write failed: ${e.message}\n`);
154
+ }
155
+ };
156
+ // Initial emission.
157
+ await emit('start');
158
+ if (once)
159
+ return 0;
160
+ let debounceTimer = null;
161
+ const scheduleEmit = (reason) => {
162
+ if (debounceTimer)
163
+ clearTimeout(debounceTimer);
164
+ debounceTimer = setTimeout(() => {
165
+ debounceTimer = null;
166
+ void emit(reason);
167
+ }, debounceMs);
168
+ };
169
+ const watchers = [];
170
+ const roots = ['packages', 'examples', 'sharkcraft', 'docs', 'libs']
171
+ .map((p) => nodePath.join(cwd, p))
172
+ .filter((abs) => existsSync(abs));
173
+ for (const root of roots) {
174
+ try {
175
+ const w = fsWatch(root, { recursive: true }, (_event, filename) => {
176
+ if (!filename)
177
+ return;
178
+ if (!shouldReactTo(String(filename)))
179
+ return;
180
+ scheduleEmit(`change:${filename}`);
181
+ });
182
+ watchers.push(w);
183
+ }
184
+ catch (e) {
185
+ process.stderr.write(`[shrk watch] could not watch ${root}: ${e.message}\n`);
186
+ }
187
+ }
188
+ // Optional interval-based emission so an idle workspace still
189
+ // produces fresh packets (handy for "every N seconds, refresh").
190
+ const intervalHandle = intervalMs > 0
191
+ ? setInterval(() => {
192
+ if (Date.now() - lastEmittedAt < intervalMs / 2)
193
+ return;
194
+ scheduleEmit('interval');
195
+ }, intervalMs)
196
+ : null;
197
+ return new Promise((resolve) => {
198
+ const shutdown = (code) => {
199
+ if (debounceTimer)
200
+ clearTimeout(debounceTimer);
201
+ if (intervalHandle)
202
+ clearInterval(intervalHandle);
203
+ for (const w of watchers) {
204
+ try {
205
+ w.close();
206
+ }
207
+ catch {
208
+ /* ignore */
209
+ }
210
+ }
211
+ try {
212
+ if (existsSync(manifestFilePath)) {
213
+ const cur = readManifest(manifestFilePath);
214
+ if (cur && cur.pid === process.pid)
215
+ rmSync(manifestFilePath, { force: true });
216
+ }
217
+ }
218
+ catch {
219
+ /* ignore */
220
+ }
221
+ if (!quiet)
222
+ process.stderr.write('\n[shrk watch] stopped.\n');
223
+ resolve(code);
224
+ };
225
+ const onSignal = () => shutdown(0);
226
+ process.once('SIGINT', onSignal);
227
+ process.once('SIGTERM', onSignal);
228
+ });
229
+ },
230
+ };
231
+ /** `shrk watch list` — show all active and stale watch manifests. */
232
+ export const watchListCommand = {
233
+ name: 'list',
234
+ description: 'List active shrk-watch daemons by reading manifests in .sharkcraft/feed/.',
235
+ usage: 'shrk watch list [--json]',
236
+ async run(args) {
237
+ const cwd = resolveCwd(args);
238
+ const json = flagBool(args, 'json');
239
+ const dir = nodePath.join(cwd, FEED_DIR);
240
+ if (!existsSync(dir)) {
241
+ if (json)
242
+ process.stdout.write(asJson({ active: [], stale: [] }) + '\n');
243
+ else
244
+ process.stdout.write('No watch feeds yet.\n');
245
+ return 0;
246
+ }
247
+ const entries = readdirSync(dir).filter((n) => n.endsWith('.pid.json'));
248
+ const active = [];
249
+ const stale = [];
250
+ for (const name of entries) {
251
+ const m = readManifest(nodePath.join(dir, name));
252
+ if (!m)
253
+ continue;
254
+ if (isProcessAlive(m.pid)) {
255
+ let feedSize;
256
+ try {
257
+ feedSize = statSync(m.feedPath).size;
258
+ }
259
+ catch {
260
+ /* ignore */
261
+ }
262
+ active.push({ ...m, ...(feedSize !== undefined ? { feedSize } : {}) });
263
+ }
264
+ else {
265
+ stale.push({ slug: m.slug, pid: m.pid, startedAt: m.startedAt });
266
+ }
267
+ }
268
+ if (json) {
269
+ process.stdout.write(asJson({ active, stale }) + '\n');
270
+ return 0;
271
+ }
272
+ if (active.length === 0 && stale.length === 0) {
273
+ process.stdout.write('No watch feeds.\n');
274
+ return 0;
275
+ }
276
+ if (active.length > 0) {
277
+ process.stdout.write(`Active watchers (${active.length}):\n`);
278
+ for (const w of active) {
279
+ process.stdout.write(` ${w.slug.padEnd(50)} pid ${String(w.pid).padEnd(8)} ${w.startedAt}\n → ${w.feedPath}${w.feedSize !== undefined ? ` (${w.feedSize} bytes)` : ''}\n`);
280
+ }
281
+ }
282
+ if (stale.length > 0) {
283
+ process.stdout.write(`\nStale manifests (process not alive):\n`);
284
+ for (const w of stale) {
285
+ process.stdout.write(` ${w.slug} (pid ${w.pid}, started ${w.startedAt})\n`);
286
+ }
287
+ process.stdout.write(' → Run `shrk watch prune` to clean them up.\n');
288
+ }
289
+ return 0;
290
+ },
291
+ };
292
+ /** `shrk watch stop <slug>` — SIGTERM the matching watcher. */
293
+ export const watchStopCommand = {
294
+ name: 'stop',
295
+ description: 'Stop a running shrk-watch daemon by slug (sends SIGTERM and waits for it to exit).',
296
+ usage: 'shrk watch stop <slug> [--json]',
297
+ async run(args) {
298
+ const slug = args.positional[0]?.trim();
299
+ if (!slug) {
300
+ process.stderr.write('Usage: shrk watch stop <slug>\n');
301
+ return 2;
302
+ }
303
+ const cwd = resolveCwd(args);
304
+ const json = flagBool(args, 'json');
305
+ const path = manifestPath(cwd, slug);
306
+ const m = readManifest(path);
307
+ if (!m) {
308
+ if (json)
309
+ process.stdout.write(asJson({ status: 'not-found', slug }) + '\n');
310
+ else
311
+ process.stderr.write(`No watcher manifest found for slug "${slug}".\n`);
312
+ return 1;
313
+ }
314
+ if (!isProcessAlive(m.pid)) {
315
+ try {
316
+ rmSync(path, { force: true });
317
+ }
318
+ catch {
319
+ /* ignore */
320
+ }
321
+ if (json)
322
+ process.stdout.write(asJson({ status: 'stale', slug, pid: m.pid }) + '\n');
323
+ else
324
+ process.stdout.write(`Watcher for "${slug}" was stale (pid ${m.pid}); cleaned up manifest.\n`);
325
+ return 0;
326
+ }
327
+ try {
328
+ process.kill(m.pid, 'SIGTERM');
329
+ }
330
+ catch (e) {
331
+ process.stderr.write(`Failed to signal pid ${m.pid}: ${e.message}\n`);
332
+ return 1;
333
+ }
334
+ // Wait briefly for the process to exit and clean its own manifest.
335
+ for (let i = 0; i < 20; i += 1) {
336
+ await new Promise((r) => setTimeout(r, 100));
337
+ if (!isProcessAlive(m.pid))
338
+ break;
339
+ }
340
+ if (json)
341
+ process.stdout.write(asJson({ status: 'stopped', slug, pid: m.pid }) + '\n');
342
+ else
343
+ process.stdout.write(`Stopped watcher "${slug}" (pid ${m.pid}).\n`);
344
+ return 0;
345
+ },
346
+ };
347
+ /** `shrk watch prune` — remove stale manifests. */
348
+ export const watchPruneCommand = {
349
+ name: 'prune',
350
+ description: 'Remove stale shrk-watch manifests (pid not alive). Safe to run anytime.',
351
+ usage: 'shrk watch prune [--json]',
352
+ async run(args) {
353
+ const cwd = resolveCwd(args);
354
+ const json = flagBool(args, 'json');
355
+ const dir = nodePath.join(cwd, FEED_DIR);
356
+ if (!existsSync(dir)) {
357
+ if (json)
358
+ process.stdout.write(asJson({ removed: [] }) + '\n');
359
+ else
360
+ process.stdout.write('No feed directory; nothing to prune.\n');
361
+ return 0;
362
+ }
363
+ const removed = [];
364
+ for (const name of readdirSync(dir).filter((n) => n.endsWith('.pid.json'))) {
365
+ const path = nodePath.join(dir, name);
366
+ const m = readManifest(path);
367
+ if (!m || !isProcessAlive(m.pid)) {
368
+ try {
369
+ rmSync(path, { force: true });
370
+ removed.push(m?.slug ?? name);
371
+ }
372
+ catch {
373
+ /* ignore */
374
+ }
375
+ }
376
+ }
377
+ if (json)
378
+ process.stdout.write(asJson({ removed }) + '\n');
379
+ else
380
+ process.stdout.write(removed.length === 0 ? 'No stale manifests.\n' : `Pruned: ${removed.join(', ')}\n`);
381
+ return 0;
382
+ },
383
+ };
384
+ async function buildPacket(input) {
385
+ const inspection = await inspectSharkcraft({ cwd: input.cwd });
386
+ const packet = buildTaskPacket(inspection, input.task, { maxTokens: 3500 });
387
+ const focused = await buildFocusedContext({
388
+ cwd: input.cwd,
389
+ task: input.task,
390
+ index: input.index,
391
+ rules: packet.relevantRules,
392
+ verificationCommands: packet.verificationCommands,
393
+ });
394
+ const taskType = input.taskTypeOverride ?? classifyTask(input.task).type;
395
+ const summarisedFiles = focused.files.map((f) => ({
396
+ path: f.path,
397
+ fileSimilarity: f.fileSimilarity,
398
+ summary: f.summary,
399
+ blocks: f.blocks.map((b) => ({
400
+ name: b.name,
401
+ kind: b.kind,
402
+ startLine: b.startLine,
403
+ similarity: b.similarity,
404
+ })),
405
+ }));
406
+ // Fingerprint = which files + their similarities + which blocks ranked.
407
+ // Same diff round-trip = same fingerprint = no re-emission.
408
+ const fp = createHash('sha1')
409
+ .update(JSON.stringify({ taskType, files: summarisedFiles }))
410
+ .digest('hex')
411
+ .slice(0, 16);
412
+ return {
413
+ schema: 'sharkcraft.shrk-watch-packet/v1',
414
+ task: input.task,
415
+ taskSlug: slugify(input.task),
416
+ taskType,
417
+ emittedAt: new Date().toISOString(),
418
+ reason: input.reason,
419
+ fingerprint: fp,
420
+ focused: {
421
+ model: focused.model,
422
+ approxTokens: focused.approxTokens,
423
+ files: focused.files,
424
+ rules: focused.rules,
425
+ docHits: focused.docHits,
426
+ verificationCommands: focused.verificationCommands,
427
+ },
428
+ hints: {
429
+ pull: 'shrk smart-context "<task>" --focused --tiny-only --json',
430
+ plan: 'shrk smart-context "<task>" --focused --plan --save',
431
+ spike: 'shrk spike <slug>',
432
+ },
433
+ };
434
+ }
435
+ function shouldReactTo(filename) {
436
+ if (filename.startsWith('.git/'))
437
+ return false;
438
+ if (filename.includes('node_modules/'))
439
+ return false;
440
+ if (filename.includes('/dist/') || filename.endsWith('/dist'))
441
+ return false;
442
+ if (filename.includes('.sharkcraft/'))
443
+ return false; // never feed our own writes back in
444
+ if (filename.includes('.next/'))
445
+ return false;
446
+ if (/\.(ts|tsx|js|jsx|mjs|cjs|md|json|yml|yaml)$/.test(filename))
447
+ return true;
448
+ return false;
449
+ }
450
+ function slugify(s) {
451
+ return (s
452
+ .toLowerCase()
453
+ .replace(/[^a-z0-9]+/g, '-')
454
+ .replace(/(^-+|-+$)/g, '')
455
+ .slice(0, 60) || 'task');
456
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal `.env` loader for the Node-based `shrk` binary.
3
+ *
4
+ * Bun auto-loads `.env`; Node does not. This walks from `cwd` up to the
5
+ * filesystem root looking for a `.env` file and merges any KEY=VALUE
6
+ * pairs into `process.env` — but only when the key is not already set,
7
+ * so an actual shell export always wins.
8
+ *
9
+ * No dependency on `dotenv`. Parser is intentionally small: lines that
10
+ * start with `#` are comments, blank lines are skipped, surrounding
11
+ * single/double quotes on the value are stripped, and escaped `\n`
12
+ * sequences inside double-quoted values become real newlines.
13
+ */
14
+ export declare function loadDotenv(startDir: string): void;
15
+ //# sourceMappingURL=load-dotenv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-dotenv.d.ts","sourceRoot":"","sources":["../../src/env/load-dotenv.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAmBjD"}
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ /**
4
+ * Minimal `.env` loader for the Node-based `shrk` binary.
5
+ *
6
+ * Bun auto-loads `.env`; Node does not. This walks from `cwd` up to the
7
+ * filesystem root looking for a `.env` file and merges any KEY=VALUE
8
+ * pairs into `process.env` — but only when the key is not already set,
9
+ * so an actual shell export always wins.
10
+ *
11
+ * No dependency on `dotenv`. Parser is intentionally small: lines that
12
+ * start with `#` are comments, blank lines are skipped, surrounding
13
+ * single/double quotes on the value are stripped, and escaped `\n`
14
+ * sequences inside double-quoted values become real newlines.
15
+ */
16
+ export function loadDotenv(startDir) {
17
+ const envPath = findEnvFile(startDir);
18
+ if (!envPath)
19
+ return;
20
+ let body;
21
+ try {
22
+ body = readFileSync(envPath, 'utf8');
23
+ }
24
+ catch {
25
+ return;
26
+ }
27
+ for (const rawLine of body.split(/\r?\n/)) {
28
+ const line = rawLine.trim();
29
+ if (line.length === 0 || line.startsWith('#'))
30
+ continue;
31
+ const eq = line.indexOf('=');
32
+ if (eq <= 0)
33
+ continue;
34
+ const key = line.slice(0, eq).trim();
35
+ if (!isValidKey(key))
36
+ continue;
37
+ if (process.env[key] !== undefined)
38
+ continue;
39
+ process.env[key] = unquote(line.slice(eq + 1).trim());
40
+ }
41
+ }
42
+ function isValidKey(key) {
43
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
44
+ }
45
+ function unquote(value) {
46
+ if (value.length >= 2) {
47
+ const first = value[0];
48
+ const last = value[value.length - 1];
49
+ if (first === '"' && last === '"') {
50
+ return value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t');
51
+ }
52
+ if (first === "'" && last === "'") {
53
+ return value.slice(1, -1);
54
+ }
55
+ }
56
+ const hash = value.indexOf(' #');
57
+ return hash === -1 ? value : value.slice(0, hash).trimEnd();
58
+ }
59
+ function findEnvFile(startDir) {
60
+ let dir = nodePath.resolve(startDir);
61
+ while (true) {
62
+ const candidate = nodePath.join(dir, '.env');
63
+ if (existsSync(candidate))
64
+ return candidate;
65
+ const parent = nodePath.dirname(dir);
66
+ if (parent === dir)
67
+ return null;
68
+ dir = parent;
69
+ }
70
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EACL,eAAe,EAIhB,MAAM,uBAAuB,CAAC;AAmW/B,wBAAgB,aAAa,IAAI,eAAe,CAqW/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAkGD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA4CxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAIhB,MAAM,uBAAuB,CAAC;AAgX/B,wBAAgB,aAAa,IAAI,eAAe,CAmX/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAkGD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA4CxE"}
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { loadDotenv } from "./env/load-dotenv.js";
2
3
  import { CommandRegistry, extractGlobalCwd, parseArgs, } from "./command-registry.js";
3
4
  import { initCommand } from "./commands/init.command.js";
4
5
  import { inspectCommand } from "./commands/inspect.command.js";
@@ -55,6 +56,12 @@ import { dashboardCommand } from "./commands/dashboard.command.js";
55
56
  import { dashboardDiffCommand, dashboardExportCommand, } from "./commands/dashboard-export.command.js";
56
57
  import { importCommand } from "./commands/import.command.js";
57
58
  import { askCommand } from "./commands/ask.command.js";
59
+ import { smartContextCommand, smartContextEmbeddingsBuildCommand, smartContextEmbeddingsStatusCommand, smartContextListCommand, smartContextPlanAheadCommand, smartContextShowCommand, } from "./commands/smart-context.command.js";
60
+ import { spikeCommand } from "./commands/spike.command.js";
61
+ import { depsAuditCommand } from "./commands/deps-audit.command.js";
62
+ import { scaffoldValidateCommand } from "./commands/scaffold-validate.command.js";
63
+ import { movePlanCommand } from "./commands/move-plan.command.js";
64
+ import { watchCommand, watchListCommand, watchPruneCommand, watchStopCommand } from "./commands/watch.command.js";
58
65
  import { mcpCommand } from "./commands/mcp.command.js";
59
66
  import { versionCommand } from "./commands/version.command.js";
60
67
  import { makeHelpCommand } from "./commands/help.command.js";
@@ -204,6 +211,20 @@ export function buildRegistry() {
204
211
  registry.register(planParentCommand);
205
212
  registry.register(devCommand);
206
213
  registry.register(askCommand);
214
+ registry.register(smartContextCommand);
215
+ registry.registerSubcommand('smart-context', smartContextPlanAheadCommand);
216
+ registry.registerSubcommand('smart-context', smartContextListCommand);
217
+ registry.registerSubcommand('smart-context', smartContextShowCommand);
218
+ registry.registerSubcommand('smart-context', smartContextEmbeddingsBuildCommand);
219
+ registry.registerSubcommand('smart-context', smartContextEmbeddingsStatusCommand);
220
+ registry.register(spikeCommand);
221
+ registry.register(depsAuditCommand);
222
+ registry.register(scaffoldValidateCommand);
223
+ registry.register(movePlanCommand);
224
+ registry.register(watchCommand);
225
+ registry.registerSubcommand('watch', watchListCommand);
226
+ registry.registerSubcommand('watch', watchStopCommand);
227
+ registry.registerSubcommand('watch', watchPruneCommand);
207
228
  registry.register(mcpCommand);
208
229
  registry.register(versionCommand);
209
230
  registry.register(qualityCommand);
@@ -804,9 +825,69 @@ if (isMain ||
804
825
  entryPath.endsWith('shrk') ||
805
826
  entryPath.endsWith('shrk.js') ||
806
827
  entryPath.endsWith('shrk.cmd')) {
828
+ loadDotenv(process.cwd());
807
829
  const argv = process.argv.slice(2);
808
- runCli(argv).then((code) => process.exit(code), (err) => {
830
+ const cleanShutdown = async (code) => {
831
+ // Best-effort teardown of shared native runtimes. Without this,
832
+ // commands that loaded native libs (ONNX via embeddings; Metal
833
+ // via node-llama-cpp) abort during `process.exit` AFTER the
834
+ // work completed — the user sees their result then `zsh: abort`.
835
+ // Dynamic imports keep these off the hot path for commands that
836
+ // never touched them.
837
+ try {
838
+ const mod = (await import('@shrkcrft/embeddings'));
839
+ if (typeof mod.disposeSemanticIndexPipeline === 'function') {
840
+ await mod.disposeSemanticIndexPipeline();
841
+ }
842
+ }
843
+ catch {
844
+ // Best-effort; never block the exit on teardown failure.
845
+ }
846
+ try {
847
+ const mod = (await import('@shrkcrft/ai'));
848
+ if (typeof mod.disposeLlamaCppRuntime === 'function') {
849
+ await mod.disposeLlamaCppRuntime();
850
+ }
851
+ }
852
+ catch {
853
+ // Best-effort.
854
+ }
855
+ // Flush stdio synchronously before bypassing C++ destructors.
856
+ // `_exit` skips all libc finalizers, which is exactly what we
857
+ // need (see below), but it also doesn't wait for buffered
858
+ // writes to drain. Two synchronous write callbacks force the
859
+ // current buffers through.
860
+ try {
861
+ await new Promise((resolve) => process.stdout.write('', () => resolve()));
862
+ await new Promise((resolve) => process.stderr.write('', () => resolve()));
863
+ }
864
+ catch {
865
+ // ignore flush failures
866
+ }
867
+ // Prefer `_exit` over `exit` on Node. Even after our explicit
868
+ // disposes above, node-llama-cpp's libggml-metal destructor
869
+ // still aborts in `__cxa_finalize_ranges` because the Metal
870
+ // device list isn't drained by the current dispose API
871
+ // (`GGML_ASSERT([rsets->data count] == 0)` fires from
872
+ // `ggml_metal_device_free`, surfacing as `zsh: abort` AFTER
873
+ // the user's result has already printed). `process._exit`
874
+ // skips the libc++ static-destructor pass entirely, which is
875
+ // safe here because we have already torn down the runtimes
876
+ // we care about above and the OS will reclaim the rest.
877
+ //
878
+ // Bun's process object doesn't expose `_exit`, but Bun also
879
+ // doesn't drive shutdown through libuv + libc++ static
880
+ // destructors the same way Node does, so the Metal crash
881
+ // doesn't reproduce there. Fall back to `process.exit` when
882
+ // `_exit` is unavailable.
883
+ const lowLevelExit = process._exit;
884
+ if (typeof lowLevelExit === 'function') {
885
+ lowLevelExit(code);
886
+ }
887
+ process.exit(code);
888
+ };
889
+ runCli(argv).then((code) => cleanShutdown(code), (err) => {
809
890
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
810
- process.exit(1);
891
+ return cleanShutdown(1);
811
892
  });
812
893
  }