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

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 (81) hide show
  1. package/dist/audit/knowledge-audit-llm.d.ts +19 -0
  2. package/dist/audit/knowledge-audit-llm.d.ts.map +1 -0
  3. package/dist/audit/knowledge-audit-llm.js +164 -0
  4. package/dist/audit/knowledge-audit.d.ts +61 -0
  5. package/dist/audit/knowledge-audit.d.ts.map +1 -0
  6. package/dist/audit/knowledge-audit.js +203 -0
  7. package/dist/audit/knowledge-fix-plan-llm.d.ts +11 -0
  8. package/dist/audit/knowledge-fix-plan-llm.d.ts.map +1 -0
  9. package/dist/audit/knowledge-fix-plan-llm.js +141 -0
  10. package/dist/audit/knowledge-fix-plan.d.ts +41 -0
  11. package/dist/audit/knowledge-fix-plan.d.ts.map +1 -0
  12. package/dist/audit/knowledge-fix-plan.js +125 -0
  13. package/dist/audit/pipeline-audit-llm.d.ts +11 -0
  14. package/dist/audit/pipeline-audit-llm.d.ts.map +1 -0
  15. package/dist/audit/pipeline-audit-llm.js +134 -0
  16. package/dist/audit/pipeline-audit.d.ts +69 -0
  17. package/dist/audit/pipeline-audit.d.ts.map +1 -0
  18. package/dist/audit/pipeline-audit.js +166 -0
  19. package/dist/audit/templates-audit-llm.d.ts +19 -0
  20. package/dist/audit/templates-audit-llm.d.ts.map +1 -0
  21. package/dist/audit/templates-audit-llm.js +207 -0
  22. package/dist/audit/templates-audit.d.ts +63 -0
  23. package/dist/audit/templates-audit.d.ts.map +1 -0
  24. package/dist/audit/templates-audit.js +171 -0
  25. package/dist/audit/templates-fix-plan-llm.d.ts +19 -0
  26. package/dist/audit/templates-fix-plan-llm.d.ts.map +1 -0
  27. package/dist/audit/templates-fix-plan-llm.js +162 -0
  28. package/dist/audit/templates-fix-plan.d.ts +37 -0
  29. package/dist/audit/templates-fix-plan.d.ts.map +1 -0
  30. package/dist/audit/templates-fix-plan.js +174 -0
  31. package/dist/commands/ai-status.command.d.ts +19 -0
  32. package/dist/commands/ai-status.command.d.ts.map +1 -0
  33. package/dist/commands/ai-status.command.js +94 -0
  34. package/dist/commands/ask.command.d.ts.map +1 -1
  35. package/dist/commands/ask.command.js +10 -9
  36. package/dist/commands/command-catalog.d.ts.map +1 -1
  37. package/dist/commands/command-catalog.js +110 -1
  38. package/dist/commands/deps-audit.command.d.ts +23 -0
  39. package/dist/commands/deps-audit.command.d.ts.map +1 -0
  40. package/dist/commands/deps-audit.command.js +266 -0
  41. package/dist/commands/doctor.command.d.ts.map +1 -1
  42. package/dist/commands/doctor.command.js +100 -3
  43. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  44. package/dist/commands/graph-code-subverbs.js +144 -26
  45. package/dist/commands/graph.command.d.ts.map +1 -1
  46. package/dist/commands/graph.command.js +3 -2
  47. package/dist/commands/help.command.d.ts.map +1 -1
  48. package/dist/commands/help.command.js +22 -1
  49. package/dist/commands/impact.command.d.ts.map +1 -1
  50. package/dist/commands/impact.command.js +3 -2
  51. package/dist/commands/move-plan.command.d.ts +23 -0
  52. package/dist/commands/move-plan.command.d.ts.map +1 -0
  53. package/dist/commands/move-plan.command.js +360 -0
  54. package/dist/commands/scaffold-validate.command.d.ts +22 -0
  55. package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
  56. package/dist/commands/scaffold-validate.command.js +215 -0
  57. package/dist/commands/smart-context.command.d.ts +58 -0
  58. package/dist/commands/smart-context.command.d.ts.map +1 -0
  59. package/dist/commands/smart-context.command.js +4524 -0
  60. package/dist/commands/spike.command.d.ts +22 -0
  61. package/dist/commands/spike.command.d.ts.map +1 -0
  62. package/dist/commands/spike.command.js +235 -0
  63. package/dist/commands/surface.command.d.ts +1 -0
  64. package/dist/commands/surface.command.d.ts.map +1 -1
  65. package/dist/commands/surface.command.js +10 -3
  66. package/dist/commands/template-quality.command.d.ts.map +1 -1
  67. package/dist/commands/template-quality.command.js +39 -3
  68. package/dist/commands/templates.command.d.ts.map +1 -1
  69. package/dist/commands/templates.command.js +37 -2
  70. package/dist/commands/watch.command.d.ts +26 -0
  71. package/dist/commands/watch.command.d.ts.map +1 -0
  72. package/dist/commands/watch.command.js +456 -0
  73. package/dist/env/load-dotenv.d.ts +15 -0
  74. package/dist/env/load-dotenv.d.ts.map +1 -0
  75. package/dist/env/load-dotenv.js +70 -0
  76. package/dist/main.d.ts.map +1 -1
  77. package/dist/main.js +105 -2
  78. package/dist/schemas/json-schemas.d.ts +384 -36
  79. package/dist/schemas/json-schemas.d.ts.map +1 -1
  80. package/dist/schemas/json-schemas.js +247 -36
  81. 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;AAoX/B,wBAAgB,aAAa,IAAI,eAAe,CAuX/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"}