@elmundi/ship-cli 0.14.2 → 0.15.3

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 (39) hide show
  1. package/README.md +17 -16
  2. package/bin/shipctl.mjs +4 -80
  3. package/lib/commands/feedback.mjs +1 -1
  4. package/lib/commands/help.mjs +47 -131
  5. package/lib/commands/init.mjs +17 -250
  6. package/lib/commands/knowledge.mjs +25 -328
  7. package/lib/commands/preflight.mjs +213 -0
  8. package/lib/commands/run.mjs +266 -116
  9. package/lib/commands/trigger.mjs +95 -10
  10. package/lib/config/schema.mjs +68 -11
  11. package/lib/http.mjs +0 -2
  12. package/lib/runtime/routines.mjs +34 -0
  13. package/lib/templates.mjs +2 -2
  14. package/lib/verify/checks/agents-on-disk.mjs +5 -28
  15. package/lib/verify/registry.mjs +7 -8
  16. package/package.json +1 -1
  17. package/lib/artifacts/fs-index.mjs +0 -230
  18. package/lib/cache/store.mjs +0 -422
  19. package/lib/commands/bootstrap.mjs +0 -4
  20. package/lib/commands/callback.mjs +0 -742
  21. package/lib/commands/docs.mjs +0 -90
  22. package/lib/commands/kickoff.mjs +0 -192
  23. package/lib/commands/lanes.mjs +0 -566
  24. package/lib/commands/manifest-catalog.mjs +0 -251
  25. package/lib/commands/migrate.mjs +0 -204
  26. package/lib/commands/new.mjs +0 -452
  27. package/lib/commands/patterns.mjs +0 -160
  28. package/lib/commands/process.mjs +0 -388
  29. package/lib/commands/search.mjs +0 -43
  30. package/lib/commands/sync.mjs +0 -824
  31. package/lib/config/migrate.mjs +0 -223
  32. package/lib/find-ship-root.mjs +0 -75
  33. package/lib/process/specialist-prompt-contract.mjs +0 -171
  34. package/lib/state/lockfile.mjs +0 -180
  35. package/lib/vendor/run-agent.workflow.yml +0 -254
  36. package/lib/verify/checks/artifacts-up-to-date.mjs +0 -78
  37. package/lib/verify/checks/cache-integrity.mjs +0 -51
  38. package/lib/verify/checks/gitignore-cache.mjs +0 -51
  39. package/lib/verify/checks/rules-markers.mjs +0 -135
@@ -1,452 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
-
6
- /**
7
- * Resolve the shipctl entry script (bin/shipctl.mjs) relative to this module.
8
- * Used when we fall back to spawning `shipctl <subcommand>` subprocesses.
9
- */
10
- function shipctlBinPath() {
11
- const here = path.dirname(fileURLToPath(import.meta.url));
12
- return path.resolve(here, "..", "..", "bin", "shipctl.mjs");
13
- }
14
-
15
- /**
16
- * @param {string[]} args
17
- */
18
- function parseNewArgs(args) {
19
- /** @type {Record<string, any>} */
20
- const out = {
21
- name: null,
22
- here: false,
23
- preset: null,
24
- tracker: null,
25
- ci: null,
26
- agents: /** @type {string[]} */ ([]),
27
- language: null,
28
- channel: "stable",
29
- baseUrl: null,
30
- yes: false,
31
- force: false,
32
- dryRun: false,
33
- json: false,
34
- help: false,
35
- // tri-state: null = default (copy-rules enabled iff agents non-empty),
36
- // true = forced on, false = opted out via --no-copy-rules.
37
- copyRules: null,
38
- // tri-state: null = default on, false = opted out via --no-bootstrap.
39
- bootstrap: null,
40
- telemetry: "off",
41
- extra: /** @type {string[]} */ ([]),
42
- };
43
-
44
- const copy = [...args];
45
- while (copy.length) {
46
- const a = copy.shift();
47
- if (a === "--here") { out.here = true; continue; }
48
- if (a === "--help" || a === "-h") { out.help = true; continue; }
49
- if (a === "--yes" || a === "-y") { out.yes = true; continue; }
50
- if (a === "--force") { out.force = true; continue; }
51
- if (a === "--dry-run") { out.dryRun = true; continue; }
52
- if (a === "--json") { out.json = true; continue; }
53
- if (a === "--copy-rules") { out.copyRules = true; continue; }
54
- if (a === "--no-copy-rules") { out.copyRules = false; continue; }
55
- if (a === "--bootstrap") { out.bootstrap = true; continue; }
56
- if (a === "--no-bootstrap") { out.bootstrap = false; continue; }
57
- if (a === "--preset" && copy.length) { out.preset = copy.shift(); continue; }
58
- if (a.startsWith("--preset=")) { out.preset = a.slice("--preset=".length); continue; }
59
- if (a === "--tracker" && copy.length) { out.tracker = copy.shift(); continue; }
60
- if (a.startsWith("--tracker=")) { out.tracker = a.slice("--tracker=".length); continue; }
61
- if (a === "--ci" && copy.length) { out.ci = copy.shift(); continue; }
62
- if (a.startsWith("--ci=")) { out.ci = a.slice("--ci=".length); continue; }
63
- if (a === "--base-url" && copy.length) { out.baseUrl = copy.shift(); continue; }
64
- if (a.startsWith("--base-url=")) { out.baseUrl = a.slice("--base-url=".length); continue; }
65
- if (a === "--agents" && copy.length) {
66
- for (const s of String(copy.shift()).split(",")) {
67
- const id = s.trim();
68
- if (id) out.agents.push(id);
69
- }
70
- continue;
71
- }
72
- if (a.startsWith("--agents=")) {
73
- for (const s of a.slice("--agents=".length).split(",")) {
74
- const id = s.trim();
75
- if (id) out.agents.push(id);
76
- }
77
- continue;
78
- }
79
- if (a === "--language" && copy.length) { out.language = copy.shift(); continue; }
80
- if (a.startsWith("--language=")) { out.language = a.slice("--language=".length); continue; }
81
- if (a === "--channel" && copy.length) { out.channel = copy.shift(); continue; }
82
- if (a.startsWith("--channel=")) { out.channel = a.slice("--channel=".length); continue; }
83
- if (a === "--telemetry" && copy.length) { out.telemetry = copy.shift(); continue; }
84
- if (a.startsWith("--telemetry=")) { out.telemetry = a.slice("--telemetry=".length); continue; }
85
- if (a && a.startsWith("--")) { out.extra.push(a); continue; }
86
- if (out.name == null) { out.name = a; continue; }
87
- out.extra.push(a);
88
- }
89
- return out;
90
- }
91
-
92
- function printNewHelp() {
93
- console.log(`shipctl new <name> — bootstrap a fresh repository with Ship wiring.
94
-
95
- USAGE
96
- shipctl new <name> [options]
97
- shipctl new [--here] [options]
98
-
99
- OPTIONS
100
- --here Initialise in the current directory instead of <name>/.
101
- --preset <id> adoption-minimum|web-app|api-backend|mobile-app|cli|monorepo
102
- --tracker <id> linear|jira|github-issues|azure-boards|clickup|spreadsheet|none
103
- --ci <id> gh-actions|gitlab-ci|buildkite|circleci|azure-pipelines|jenkins|manual
104
- --agents <csv> Comma-separated agent ids (e.g. cursor,codex,claude).
105
- --language <id> ts|js|py|go|rust|java|kotlin|swift|dart|multi
106
- --channel <id> stable|edge (written to api.channel; default stable).
107
- --base-url <url> Override Ship API base URL (forwarded to 'init').
108
- --copy-rules Forward to init. Default ON when agents are selected;
109
- use --no-copy-rules to opt out.
110
- --no-copy-rules Skip installing cached agent rule files on disk.
111
- --bootstrap Forward --bootstrap to init. Default ON; use
112
- --no-bootstrap to opt out.
113
- --no-bootstrap Skip rendering CI/tracker scaffolding.
114
- --telemetry <on|off> Default: off. Writes telemetry.share.
115
- --yes Non-interactive (assumed for --dry-run).
116
- --force Reuse a non-empty target directory.
117
- --dry-run Describe the plan without touching disk.
118
- --json Machine-readable summary.
119
-
120
- Creates <name>/ (or reuses cwd with --here), runs 'git init -q', writes a
121
- minimal README.md, seeds .ship/config.yml via 'shipctl config init', applies
122
- the provided stack flags via 'shipctl config set', and then runs
123
- 'shipctl init --yes' for any selected agents.
124
- `);
125
- }
126
-
127
- function dirIsEmpty(dir) {
128
- try {
129
- const entries = fs.readdirSync(dir);
130
- return entries.filter((e) => e !== ".DS_Store").length === 0;
131
- } catch {
132
- return true;
133
- }
134
- }
135
-
136
- function isGitRepo(dir) {
137
- return fs.existsSync(path.join(dir, ".git"));
138
- }
139
-
140
- function resolveTargetDir(args, cwd) {
141
- if (args.here) return path.resolve(cwd);
142
- if (!args.name) {
143
- throw new Error("new: missing <name>. Run 'shipctl new --help' for usage.");
144
- }
145
- return path.resolve(cwd, args.name);
146
- }
147
-
148
- /**
149
- * Run `shipctl <sub...>` via the same Node binary, capturing output for JSON mode.
150
- * @param {string[]} argv
151
- * @param {{capture?:boolean}} [opts]
152
- */
153
- function runShipctl(argv, opts = {}) {
154
- const bin = shipctlBinPath();
155
- const res = spawnSync(process.execPath, [bin, ...argv], {
156
- stdio: opts.capture ? ["ignore", "pipe", "pipe"] : "inherit",
157
- encoding: "utf8",
158
- });
159
- if (res.error) throw res.error;
160
- return {
161
- status: typeof res.status === "number" ? res.status : 1,
162
- stdout: res.stdout || "",
163
- stderr: res.stderr || "",
164
- };
165
- }
166
-
167
- /**
168
- * Apply stack-level settings via `shipctl config set`.
169
- * Missing values are skipped.
170
- * @param {string} newDir
171
- * @param {ReturnType<typeof parseNewArgs>} a
172
- * @param {boolean} capture
173
- * @returns {{ok:boolean, applied:string[], errors:string[]}}
174
- */
175
- function applyStackConfig(newDir, a, capture) {
176
- const applied = [];
177
- const errors = [];
178
- const set = (key, value) => {
179
- const res = runShipctl(["config", "set", key, String(value), "--cwd", newDir], {
180
- capture,
181
- });
182
- if (res.status !== 0) {
183
- errors.push(`${key}=${value}: ${(res.stderr || res.stdout).trim() || `exit ${res.status}`}`);
184
- return false;
185
- }
186
- applied.push(`${key}=${value}`);
187
- return true;
188
- };
189
-
190
- if (a.tracker) set("stack.tracker", a.tracker);
191
- if (a.ci) set("stack.ci", a.ci);
192
- if (a.preset) set("stack.preset", a.preset);
193
- if (a.language) set("stack.language", a.language);
194
- if (a.channel) set("api.channel", a.channel);
195
- if (a.agents.length) set("stack.agents", `[${a.agents.join(",")}]`);
196
- if (a.telemetry === "on") set("telemetry.share", "true");
197
- else if (a.telemetry === "off") set("telemetry.share", "false");
198
-
199
- return { ok: errors.length === 0, applied, errors };
200
- }
201
-
202
- /**
203
- * @param {{ baseUrl:string, yes:boolean, force:boolean, dryRun:boolean, json:boolean }} ctx
204
- * @param {string[]} args
205
- */
206
- export async function newCommand(ctx, args) {
207
- const a = parseNewArgs(args);
208
- if (a.help) { printNewHelp(); return; }
209
-
210
- if (ctx) {
211
- if (ctx.json) a.json = true;
212
- if (ctx.yes) a.yes = true;
213
- if (ctx.force) a.force = true;
214
- if (ctx.dryRun) a.dryRun = true;
215
- // extractGlobalArgv in bin/shipctl.mjs strips `--base-url` out of argv
216
- // and stashes it on ctx. Fold it in here so the init subprocess gets
217
- // the same URL the caller handed to `shipctl new`.
218
- if (!a.baseUrl && ctx.baseUrl) a.baseUrl = ctx.baseUrl;
219
- }
220
-
221
- const cwd = process.cwd();
222
- let newDir;
223
- try {
224
- newDir = resolveTargetDir(a, cwd);
225
- } catch (e) {
226
- console.error(e.message);
227
- process.exit(1);
228
- }
229
-
230
- const willCreate = !fs.existsSync(newDir);
231
- const alreadyGit = !willCreate && isGitRepo(newDir);
232
- const alreadyNonEmpty = !willCreate && !dirIsEmpty(newDir);
233
- const configAlready = fs.existsSync(path.join(newDir, ".ship", "config.yml"));
234
-
235
- if (!a.here && !willCreate && alreadyNonEmpty && !a.force) {
236
- console.error(
237
- `new: target directory is not empty: ${newDir}\n` +
238
- `Re-run with --force to reuse it, or choose a different <name>.`,
239
- );
240
- process.exit(1);
241
- }
242
-
243
- const plannedFiles = [
244
- { path: path.join(newDir, ".git"), reason: "git init" },
245
- { path: path.join(newDir, "README.md"), reason: "minimal stub" },
246
- { path: path.join(newDir, ".ship", "config.yml"), reason: "shipctl config init" },
247
- ];
248
-
249
- const initArgv = buildInitArgv(a, newDir);
250
- const runInit = shouldRunInit(a);
251
-
252
- const summary = {
253
- cwd,
254
- dir: newDir,
255
- created_dir: willCreate,
256
- reused_dir: !willCreate,
257
- here: a.here,
258
- git_init: !alreadyGit,
259
- readme: true,
260
- stack: {
261
- tracker: a.tracker,
262
- ci: a.ci,
263
- preset: a.preset,
264
- language: a.language,
265
- channel: a.channel,
266
- base_url: a.baseUrl,
267
- agents: a.agents,
268
- telemetry: a.telemetry,
269
- copy_rules:
270
- a.copyRules === true || (a.copyRules !== false && a.agents.length > 0),
271
- bootstrap: a.bootstrap !== false,
272
- },
273
- init_argv: initArgv,
274
- run_init: runInit,
275
- planned_files: plannedFiles.map((f) => path.relative(cwd, f.path) || f.path),
276
- next_steps: [
277
- `cd ${path.relative(cwd, newDir) || "."}`,
278
- "shipctl verify",
279
- ],
280
- };
281
-
282
- if (a.dryRun) {
283
- if (a.json) {
284
- console.log(JSON.stringify({ ...summary, dry_run: true }, null, 2));
285
- return;
286
- }
287
- console.log(
288
- `shipctl new (dry-run) — ${a.here ? "using current dir" : willCreate ? "would create" : "would reuse"}: ${newDir}`,
289
- );
290
- const show = (full) => {
291
- const rel = path.relative(cwd, full);
292
- return rel && !rel.startsWith("..") ? rel : full;
293
- };
294
- for (const f of plannedFiles) {
295
- console.log(` plan: write ${show(f.path)} (${f.reason})`);
296
- }
297
- if (runInit) {
298
- console.log(` plan: shipctl ${initArgv.join(" ")}`);
299
- }
300
- const stackLines = [];
301
- if (a.tracker) stackLines.push(`stack.tracker=${a.tracker}`);
302
- if (a.ci) stackLines.push(`stack.ci=${a.ci}`);
303
- if (a.preset) stackLines.push(`stack.preset=${a.preset}`);
304
- if (a.language) stackLines.push(`stack.language=${a.language}`);
305
- if (a.channel) stackLines.push(`api.channel=${a.channel}`);
306
- if (a.agents.length) stackLines.push(`stack.agents=[${a.agents.join(",")}]`);
307
- if (a.telemetry) stackLines.push(`telemetry.share=${a.telemetry === "on"}`);
308
- for (const s of stackLines) console.log(` plan: shipctl config set ${s}`);
309
- console.log("(dry-run: no files written)");
310
- return;
311
- }
312
-
313
- const createdFiles = [];
314
- if (willCreate) fs.mkdirSync(newDir, { recursive: true });
315
-
316
- if (!alreadyGit) {
317
- const gitInit = spawnSync("git", ["init", "-q"], { cwd: newDir, encoding: "utf8" });
318
- if (gitInit.status !== 0) {
319
- const reason = (gitInit.stderr || "").trim() || `exit ${gitInit.status}`;
320
- console.error(`new: 'git init' failed in ${newDir}: ${reason}`);
321
- process.exit(1);
322
- }
323
- createdFiles.push(path.join(newDir, ".git"));
324
- }
325
-
326
- const readmePath = path.join(newDir, "README.md");
327
- if (!fs.existsSync(readmePath)) {
328
- const displayName = a.name || path.basename(newDir);
329
- fs.writeFileSync(
330
- readmePath,
331
- `# ${displayName}\n\nBootstrapped with shipctl (Ship methodology kit).\n`,
332
- "utf8",
333
- );
334
- createdFiles.push(readmePath);
335
- }
336
-
337
- const capture = !!a.json;
338
- const log = (msg) => { if (!a.json) console.log(msg); };
339
-
340
- if (!configAlready) {
341
- const res = runShipctl(["config", "init", "--cwd", newDir], { capture });
342
- if (res.status !== 0) {
343
- const out = (res.stderr || res.stdout).trim();
344
- console.error(
345
- `new: 'shipctl config init' exited with code ${res.status}${out ? `\n${out}` : ""}`,
346
- );
347
- process.exit(res.status);
348
- }
349
- createdFiles.push(path.join(newDir, ".ship", "config.yml"));
350
- if (!a.json) process.stdout.write(res.stdout || "");
351
- } else {
352
- log(`config: reusing existing ${path.join(newDir, ".ship", "config.yml")}`);
353
- }
354
-
355
- const stackResult = applyStackConfig(newDir, a, capture);
356
- if (!stackResult.ok) {
357
- console.error("new: failed to apply stack flags:");
358
- for (const e of stackResult.errors) console.error(` - ${e}`);
359
- process.exit(10);
360
- }
361
- if (!a.json) {
362
- for (const s of stackResult.applied) console.log(`config set ${s}`);
363
- }
364
-
365
- let initStatus = 0;
366
- if (runInit) {
367
- const res = runShipctl(initArgv, { capture });
368
- initStatus = res.status;
369
- if (!a.json) process.stdout.write(res.stdout || "");
370
- if (res.status !== 0) {
371
- const out = (res.stderr || res.stdout).trim();
372
- console.error(
373
- `new: 'shipctl init' exited with code ${res.status}${out ? `\n${out}` : ""}\n` +
374
- `Re-run with the same flags to retry, or pass --no-bootstrap / --no-copy-rules to skip remote steps.`,
375
- );
376
- process.exit(res.status);
377
- }
378
- }
379
-
380
- const finalConfigPath = path.join(newDir, ".ship", "config.yml");
381
- const configExists = fs.existsSync(finalConfigPath);
382
-
383
- if (a.json) {
384
- console.log(
385
- JSON.stringify(
386
- {
387
- ...summary,
388
- dry_run: false,
389
- stack_set: stackResult.applied,
390
- created_files: createdFiles.map((p) => path.relative(cwd, p) || p),
391
- config_written: configExists,
392
- init_status: initStatus,
393
- },
394
- null,
395
- 2,
396
- ),
397
- );
398
- return;
399
- }
400
-
401
- console.log("");
402
- console.log(`Done. Ship scaffolding in ${newDir}`);
403
- console.log("Next:");
404
- console.log(` cd ${path.relative(cwd, newDir) || "."}`);
405
- console.log(" shipctl verify");
406
- }
407
-
408
- /**
409
- * Build the argv list for the `shipctl init` subprocess. Kept separate so
410
- * --dry-run can show the plan without executing. Forwards the full set of
411
- * stack flags (tracker, CI, preset, agents, language, channel, base-url,
412
- * telemetry) and defaults --copy-rules ON (when agents were selected) and
413
- * --bootstrap ON. Use --no-copy-rules / --no-bootstrap on `new` to opt out.
414
- *
415
- * @param {ReturnType<typeof parseNewArgs>} a
416
- * @param {string} newDir
417
- */
418
- export function buildInitArgv(a, newDir) {
419
- const argv = ["init", "--cwd", newDir, "--yes"];
420
- if (a.agents.length) argv.push("--agents", a.agents.join(","));
421
- if (a.tracker) argv.push("--tracker", a.tracker);
422
- if (a.ci) argv.push("--ci", a.ci);
423
- if (a.preset) argv.push("--preset", a.preset);
424
- if (a.force) argv.push("--force");
425
- if (a.language) argv.push("--language", a.language);
426
- if (a.channel) argv.push("--channel", a.channel);
427
- if (a.telemetry) argv.push("--telemetry", a.telemetry);
428
- if (a.baseUrl) argv.push("--base-url", a.baseUrl);
429
-
430
- const wantCopyRules =
431
- a.copyRules === true || (a.copyRules !== false && a.agents.length > 0);
432
- if (wantCopyRules) argv.push("--copy-rules");
433
-
434
- const wantBootstrap = a.bootstrap !== false;
435
- if (wantBootstrap) argv.push("--bootstrap");
436
-
437
- if (a.json) argv.push("--json");
438
- return argv;
439
- }
440
-
441
- /**
442
- * Decide whether `shipctl new` needs to spawn `shipctl init` at all. init
443
- * does the real work (rule files on disk, CI scaffolding, telemetry prompt,
444
- * initial sync). If the caller said --no-bootstrap and passed no agents,
445
- * we skip the subprocess entirely.
446
- * @param {ReturnType<typeof parseNewArgs>} a
447
- */
448
- function shouldRunInit(a) {
449
- if (a.agents.length > 0) return true;
450
- if (a.bootstrap !== false) return true;
451
- return false;
452
- }
@@ -1,160 +0,0 @@
1
- import { apiGet, apiPost } from "../http.mjs";
2
- import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
3
- import { searchCommand } from "./search.mjs";
4
- import { scanArtifacts, readArtifactFile } from "../artifacts/fs-index.mjs";
5
-
6
- /**
7
- * @param {Record<string, unknown>} p
8
- */
9
- function slimEntry(p) {
10
- return {
11
- id: p.id,
12
- title: p.title,
13
- summary: p.summary,
14
- path: p.path,
15
- tags: Array.isArray(p.tags) ? p.tags : [],
16
- group: p.group,
17
- };
18
- }
19
-
20
- /**
21
- * @param {string} root
22
- * @param {{ baseUrl: string; json: boolean }} ctx
23
- * @param {string} sub
24
- * @param {string[]} rest
25
- */
26
- async function patternsFromDisk(root, ctx, sub, rest) {
27
- const entries = scanArtifacts(root, "pattern");
28
-
29
- if (sub === "list") {
30
- const slim = entries.map((p) => slimEntry(p));
31
- const out = { version: 1, description: "Patterns", patterns: slim };
32
- if (ctx.json) console.log(JSON.stringify(out, null, 2));
33
- else {
34
- console.log(`Patterns\n`);
35
- for (const p of slim) {
36
- console.log(`- ${p.id}`);
37
- console.log(` ${p.title}`);
38
- const tags = (p.tags || []).join(", ");
39
- console.log(` path: ${p.path} tags: ${tags}\n`);
40
- }
41
- }
42
- return;
43
- }
44
-
45
- if (sub === "show" || sub === "fetch") {
46
- const id = rest[0];
47
- if (!id) {
48
- console.error(`${sub}: pattern id required.`);
49
- process.exit(1);
50
- }
51
- const entry = entries.find((e) => e.id === id);
52
- if (!entry) {
53
- console.error(`Unknown id: ${id}`);
54
- process.exit(1);
55
- }
56
- const file = readArtifactFile(root, "pattern", id);
57
- if (!file) {
58
- console.error(`Missing file: ${entry.path}`);
59
- process.exit(1);
60
- }
61
- const content = file.content;
62
- const full = { ...slimEntry(entry), content };
63
- if (ctx.json) console.log(JSON.stringify(full, null, 2));
64
- else {
65
- console.log(`# ${entry.title} (${entry.id})\n`);
66
- console.log(content);
67
- }
68
- return;
69
- }
70
-
71
- console.error(`Unknown pattern subcommand: ${sub}`);
72
- process.exit(1);
73
- }
74
-
75
- /**
76
- * @param {{ baseUrl: string; json: boolean }} ctx
77
- * @param {string} sub
78
- * @param {string[]} rest
79
- */
80
- async function patternsFromHosted(ctx, sub, rest) {
81
- const base = ctx.baseUrl;
82
- if (sub === "list") {
83
- const data = await apiGet(base, "/patterns");
84
- if (ctx.json) console.log(JSON.stringify(data, null, 2));
85
- else {
86
- console.log(`${data.description || "Patterns"}\n`);
87
- for (const p of data.patterns || []) {
88
- console.log(`- ${p.id}`);
89
- console.log(` ${p.title}`);
90
- console.log(` path: ${p.path} tags: ${(p.tags || []).join(", ")}\n`);
91
- }
92
- }
93
- return;
94
- }
95
- if (sub === "show") {
96
- const id = rest[0];
97
- if (!id) {
98
- console.error("show: pattern id required.");
99
- process.exit(1);
100
- }
101
- const data = await apiGet(base, `/patterns/${encodeURIComponent(id)}`);
102
- if (ctx.json) console.log(JSON.stringify(data, null, 2));
103
- else {
104
- console.log(`# ${data.title} (${data.id})\n`);
105
- console.log(data.content);
106
- }
107
- return;
108
- }
109
- if (sub === "fetch") {
110
- const id = rest[0];
111
- if (!id) {
112
- console.error("fetch: pattern id required.");
113
- process.exit(1);
114
- }
115
- const data = await apiPost(base, "/fetch", { kind: "pattern", id });
116
- if (ctx.json) console.log(JSON.stringify(data, null, 2));
117
- else {
118
- console.log(`# ${data.title} (${data.id})\n`);
119
- console.log(data.content);
120
- }
121
- return;
122
- }
123
- console.error(`Unknown pattern subcommand: ${sub}`);
124
- process.exit(1);
125
- }
126
-
127
- /**
128
- * @param {{ baseUrl: string; json: boolean }} ctx
129
- * @param {string[]} args
130
- */
131
- export async function patternCommand(ctx, args) {
132
- const [sub, ...rest] = args;
133
- if (!sub || sub === "help") {
134
- console.log(`Usage:
135
- shipctl pattern list
136
- shipctl pattern show <id>
137
- shipctl pattern fetch <id>
138
- shipctl pattern search <query> [--top-k N]
139
-
140
- With a local Ship tree (cwd or SHIP_REPO): list/show/fetch scan artifacts/patterns/<id>/ARTIFACT.md on disk.
141
- Otherwise: methodology API (GET /patterns, POST /fetch for fetch, POST /search for search).
142
-
143
- Plural alias: shipctl patterns …
144
-
145
- Global flags: --base-url URL --json`);
146
- return;
147
- }
148
-
149
- if (sub === "search") {
150
- await searchCommand(ctx, rest);
151
- return;
152
- }
153
-
154
- const root = resolveShipRepoRootForCatalog();
155
- if (root) {
156
- await patternsFromDisk(root, ctx, sub, rest);
157
- } else {
158
- await patternsFromHosted(ctx, sub, rest);
159
- }
160
- }