@shrkcrft/cli 0.1.0-alpha.10 → 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.
- package/dist/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +10 -9
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +100 -1
- 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 +266 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +60 -1
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +144 -26
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +3 -2
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +22 -1
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +3 -2
- 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/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/smart-context.command.d.ts +30 -0
- package/dist/commands/smart-context.command.d.ts.map +1 -0
- package/dist/commands/smart-context.command.js +3763 -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/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/dashboard-api-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-api-server.js +25 -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/main.d.ts.map +1 -1
- package/dist/main.js +83 -2
- 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/package.json +33 -30
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-api-server.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-api-server.ts"],"names":[],"mappings":"AA4DA,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2BD,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC1F;
|
|
1
|
+
{"version":3,"file":"dashboard-api-server.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-api-server.ts"],"names":[],"mappings":"AA4DA,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2BD,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC1F;AA2jBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -249,6 +249,12 @@ async function handle(req, res, ctx) {
|
|
|
249
249
|
if (!path.startsWith('/api/') && opts.staticDir) {
|
|
250
250
|
return serveStatic(res, opts.staticDir, path);
|
|
251
251
|
}
|
|
252
|
+
// Non-API request with no static UI installed. Common when @shrkcrft/cli
|
|
253
|
+
// is consumed without @shrkcrft/dashboard present. Return a helpful page
|
|
254
|
+
// instead of falling through to the cryptic "Unknown route: /".
|
|
255
|
+
if (!path.startsWith('/api/') && !opts.staticDir) {
|
|
256
|
+
return serveNoUiHint(res, path);
|
|
257
|
+
}
|
|
252
258
|
// Session live events (SSE). Subscribe via EventSource on the session detail page.
|
|
253
259
|
const sseMatch = path.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
254
260
|
if (sseMatch) {
|
|
@@ -505,6 +511,25 @@ function serveSessionReportHtml(res, cwd, sessionId) {
|
|
|
505
511
|
}
|
|
506
512
|
res.end(html);
|
|
507
513
|
}
|
|
514
|
+
function serveNoUiHint(res, urlPath) {
|
|
515
|
+
const accept = (res.req?.headers['accept'] ?? '');
|
|
516
|
+
const wantsHtml = accept.includes('text/html');
|
|
517
|
+
if (wantsHtml) {
|
|
518
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>SharkCraft dashboard — UI not installed</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;max-width:680px;margin:48px auto;padding:0 16px;color:#1f2937;line-height:1.55}code,pre{background:#f3f4f6;padding:2px 6px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}pre{padding:12px;overflow:auto}h1{font-size:20px;margin-top:0}a{color:#2563eb}</style></head><body><h1>SharkCraft dashboard</h1><p>The API is running, but the web UI bundle (<code>@shrkcrft/dashboard</code>) was not found next to <code>@shrkcrft/cli</code>.</p><p>Install or upgrade it alongside the CLI:</p><pre>npm i -D @shrkcrft/dashboard@alpha</pre><p>Or point the server at a local build:</p><pre>shrk dashboard --dev-assets path/to/dashboard/dist</pre><p>The API itself is available — try <a href="/api/health">/api/health</a> or <a href="/api/overview">/api/overview</a>.</p></body></html>`;
|
|
519
|
+
res.statusCode = 200;
|
|
520
|
+
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
521
|
+
res.setHeader('cache-control', 'no-store');
|
|
522
|
+
res.setHeader('x-content-type-options', 'nosniff');
|
|
523
|
+
res.setHeader('referrer-policy', 'no-referrer');
|
|
524
|
+
if ((res.req?.method ?? 'GET') === 'HEAD') {
|
|
525
|
+
res.end();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
res.end(html);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
respondError(res, 404, 'not-found', `No UI assets installed; ${urlPath} cannot be served. Install @shrkcrft/dashboard or pass --dev-assets.`);
|
|
532
|
+
}
|
|
508
533
|
function serveStatic(res, staticDir, urlPath) {
|
|
509
534
|
// Strip leading / and resolve against staticDir. Reject traversal.
|
|
510
535
|
const rel = urlPath.replace(/^\/+/, '');
|
|
@@ -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
|
+
}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";
|
|
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"}
|