@elmundi/ship-cli 0.11.2 → 0.12.0

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.
@@ -1,85 +1,140 @@
1
1
  export function printHelp() {
2
- console.log(`Ship CLI artifacts protocol on ship.elmundi.com (or SHIP_API_BASE) + init.
2
+ console.log(`shipctl — adopt Ship in a repo, sync the catalog, run lanes, report Runs.
3
3
 
4
- ARTIFACTS PROTOCOL (RFC-0001)
5
- 1) shipctl search <query> — vector search (POST /search) over docs + prompts
6
- 2) shipctl docs fetch <path> — full markdown body by repo-relative path
7
- shipctl pattern|tool|collection show|fetch <id>
8
- — versioned artifact body (POST /fetch { kind, id, version? })
9
- 3) shipctl docs feedback … — improvement / retro note (POST /feedback)
4
+ Bootstrap a new or existing repo (init / new / doctor), pull the
5
+ methodology catalog into .ship/cache (sync), execute one-shot lanes or
6
+ emit prompts for the workspace runner (run / lanes / kickoff /
7
+ callback). Talks to the methodology + orchestration APIs over HTTPS.
10
8
 
11
- Every consumed artifact should be recorded in the PR as \`<kind>:<id>@<version>\`.
9
+ VOCABULARY
10
+ lanes: (.ship/config.yml) → operator console: Automations
11
+ pattern: (artifact kind) → operator console: Plays
12
+ pipeline_runs (DB / API) → operator console: Runs
13
+ attention surface → operator console: Inbox
12
14
 
13
- COMMANDS
14
- shipctl help
15
- shipctl search <query> [--top-k N]
16
-
17
- shipctl docs fetch <repo-relative-path>
18
- shipctl docs feedback --title "..." --summary "..." [--recommendation "…"]... [--source-context "…"]
19
-
20
- shipctl pattern list | shipctl pattern show <id> | shipctl pattern fetch <id> | shipctl pattern search <query> [--top-k N]
21
- shipctl tool … | shipctl collection … (same subcommands; plural aliases: patterns, tools, …)
22
-
23
- shipctl init [--yes] [--force] [--dry-run] [--json] [--cwd <dir>]
24
- [--agents <csv>]
25
- [--tracker <name>] [--ci <name>] [--preset <name>]
26
- [--language <name>] [--channel stable|edge]
27
- [--copy-rules] [--copy-playbook] [--bootstrap]
28
- [--telemetry on|off|ask]
29
-
30
- shipctl doctor [--json] [--cwd <dir>] [--write-inventory] [--no-network]
31
- — inspect the repo, propose a stack, optionally write
32
- .ship/inventory.json for 'shipctl init --bootstrap'.
33
-
34
- shipctl config init|get|set|validate|show|path — .ship/config.yml management
35
- shipctl sync [--check-only] [--only <kind:id>...] [--channel <c>] [--force-unpin]
36
- [--dry-run] [--lock] [--json]
37
- — fetch artifacts into .ship/cache. With --lock,
38
- also writes .ship/shipctl.lock.json covering every
39
- pattern the declared lanes depend on (Phase 4).
40
-
41
- shipctl new <name> [--preset ...] [--tracker ...] [--ci ...] [--agents ...] [--here] [--yes]
42
- — bootstrap a fresh repo: git init + README + .ship/config.yml
43
- shipctl verify [--no-network] [--check <id,...>] [--severity warn|error|info] [--json]
44
- — post-adoption liveness checks (local + config + network)
45
- shipctl telemetry status|on|off|show-id|reset-id|flush|export|delete-my-data|buffer
46
- — opt-in anonymous usage (RFC-0003); default OFF.
47
- '--scope artifact_usage,improvement_drafts,errors' on 'on'
48
- '--dry-run' on 'flush'; '--out <file>' on 'export'
49
- shipctl feedback draft|list|show|edit|submit|remove
50
- — local markdown drafts; submit creates a GitHub issue
51
- via POST /feedback and moves the draft to sent/.
52
- shipctl callback --status <ok|fail|cancelled> [--summary "..."] [--metric k=v]...
53
- — report a pipeline run's terminal status to Ship.
54
- Used inside workflow.yml 'if: always()' steps;
55
- reads SHIP_RUN_TOKEN + SHIP_CALLBACK_URL from env.
56
- shipctl kickoff [--pattern kickoff] [--version …] [--raw] [--json] [--cwd …]
57
- — print the kickoff / workload pattern body for piping
58
- into the customer's agent in CI (see artifacts/patterns/kickoff).
59
- shipctl migrate [--dry-run] [--yes] [--json] [--cwd …]
60
- — upgrade .ship/config.yml from v1 to v2 (lanes-as-config).
61
- shipctl run --lane <id> [--trigger event|schedule|manual|once]
62
- [--dry-run] [--offline] [--json] [--cwd …]
63
- [--ship-run-id …] [--ship-callback-url …] [--ship-run-token …]
64
- — RFC-0007 entry-point: resolve a lane from
65
- .ship/config.yml, fetch its pattern, check idempotency,
66
- emit the prompt, and report the callback.
67
- shipctl lanes install [--only <csv>] [--ref <git-ref>] [--owner …] [--repo …]
68
- [--shipctl-version <v>] [--dry-run] [--force] [--json] [--cwd …]
69
- — generate .github/workflows/ship-<lane>.yml thin
70
- wrappers that delegate to the reusable run-agent.yml.
71
- shipctl lanes list [--json] [--cwd …]
72
- — print the lane map from .ship/config.yml.
73
- shipctl lanes remove [--only <csv>] [--dry-run] [--json] [--cwd …]
74
- — delete generated ship-<lane>.yml wrappers.
75
- shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>] [--only <csv>] [--json]
76
- — open a PR that seeds .ship/knowledge/*.md starter
77
- buckets (code-style, ui-runbook). Reads SHIP_API_TOKEN.
78
- shipctl bootstrap (stub)
15
+ The protocol-stable terms (lanes:, pattern:, pipeline_runs) stay
16
+ literal in YAML, CLI flags, and HTTP. Operator-facing prose uses the
17
+ console nouns.
79
18
 
80
19
  GLOBAL FLAGS
81
- --base-url URL Methodology API (default: SHIP_API_BASE or https://ship.elmundi.com/api/methodology)
82
- --json Machine-readable JSON
20
+ --base-url URL Methodology API (default: SHIP_API_BASE or
21
+ https://ship.elmundi.com/api/methodology)
22
+ --json Machine-readable JSON output where supported
23
+ --version, -v Print shipctl version and exit
24
+ --help, -h Print this help
25
+
26
+ COMMANDS
27
+
28
+ Setup
29
+ shipctl init [--yes] [--force] [--dry-run] [--json] [--cwd <dir>]
30
+ [--agents <csv>]
31
+ [--tracker <name>] [--ci <name>] [--preset <name>]
32
+ [--language <name>] [--channel stable|edge]
33
+ [--copy-rules] [--copy-playbook] [--bootstrap]
34
+ [--telemetry on|off|ask]
35
+ — bootstrap .ship/, fetch artifacts, install
36
+ agent rules in an existing repo.
37
+ shipctl new <name> [--preset ...] [--tracker ...] [--ci ...] [--agents ...]
38
+ [--here] [--yes]
39
+ — bootstrap a fresh repo: git init + README +
40
+ .ship/config.yml.
41
+ shipctl doctor [--json] [--cwd <dir>] [--write-inventory] [--no-network]
42
+ — inspect the repo, propose a stack, optionally
43
+ write .ship/inventory.json for
44
+ 'shipctl init --bootstrap'.
45
+ shipctl config init|get|set|validate|show|path
46
+ — .ship/config.yml management.
47
+
48
+ Catalog
49
+ shipctl search <query> [--top-k N]
50
+ — vector search over docs + prompts (POST /search).
51
+ shipctl docs fetch <repo-relative-path>
52
+ shipctl docs feedback --title "..." --summary "..." [--recommendation "..."]...
53
+ [--source-context "..."]
54
+ — fetch markdown bodies; submit improvement /
55
+ retro notes (POST /feedback).
56
+ shipctl pattern list | shipctl pattern show <id> | shipctl pattern fetch <id>
57
+ | shipctl pattern search <query> [--top-k N]
58
+ — versioned artifact bodies (POST /fetch
59
+ { kind, id, version? }). 'pattern' is the
60
+ protocol-stable artifact kind; in the operator
61
+ console it shows up as a Play.
62
+ shipctl tool … | shipctl collection …
63
+ — same subcommands; plural aliases:
64
+ patterns, tools, collections.
65
+ shipctl sync [--check-only] [--only <kind:id>]... [--channel <c>]
66
+ [--force-unpin] [--dry-run] [--lock] [--json] [--cwd <dir>]
67
+ — fetch artifacts into .ship/cache. With --lock,
68
+ also writes .ship/shipctl.lock.json covering
69
+ every pattern the declared lanes depend on.
70
+
71
+ Run
72
+ shipctl trigger --event schedule --repo <id|owner/name> [--workspace <id>] [--json]
73
+ — ask Ship which configured lanes are
74
+ due for the current GitHub trigger.
75
+ shipctl run --lane <id> [--pattern <id>] [--fanout matrix|sequential|concurrent]
76
+ [--trigger event|schedule|manual|once]
77
+ [--dry-run] [--offline] [--json] [--cwd <dir>]
78
+ [--ship-run-id <uuid>] [--ship-callback-url <url>] [--ship-run-token <jwt>]
79
+ — one-shot dispatch entry point. 'kind: once'
80
+ lanes execute fully here; 'kind: lane / event /
81
+ schedule' lanes are queued for the workspace
82
+ runner via .github/workflows/run-agent.yml.
83
+ Reports its terminal status via the callback URL
84
+ Ship injected into the workflow.
85
+ shipctl lanes install [--only <csv>] [--ref <git-ref>] [--owner <gh>] [--repo <name>]
86
+ [--shipctl-version <v>] [--dry-run] [--force] [--json] [--cwd <dir>]
87
+ shipctl lanes list [--json] [--cwd <dir>]
88
+ shipctl lanes remove [--only <csv>] [--dry-run] [--json] [--cwd <dir>]
89
+ — generate / inspect / delete the
90
+ .github/workflows/ship-<lane>.yml thin wrappers
91
+ that delegate to the reusable run-agent.yml.
92
+ shipctl kickoff [--pattern <id>] [--version <v>] [--raw] [--json] [--cwd <dir>]
93
+ — print a pattern body for piping into the
94
+ customer's agent in CI.
95
+ shipctl callback --status <ok|fail|cancelled> [--summary "..."] [--metric k=v]...
96
+ [--outcome-text "..."] [--findings-count N] [--severity high=N]...
97
+ [--artifact pr:"..."]... [--escalation clarification:"..."]...
98
+ — report a Run's terminal status (and
99
+ RunSummary outcome) back to Ship so it can
100
+ render the outcome row and route any
101
+ escalations into the Inbox.
102
+
103
+ Knowledge
104
+ shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>] [--only <csv>] [--json]
105
+ — compatibility: open a PR that seeds
106
+ .ship/knowledge starter docs.
107
+ shipctl knowledge fetch <bucket-slug> [--workspace <id>] [--json]
108
+ — read Ship-owned bucket articles and
109
+ source sync state.
110
+ shipctl knowledge bootstrap [--workspace <id>] [--repo <id|owner/name>] [--json]
111
+ — post-merge action entry point: analyze
112
+ repo and open generated knowledge PR.
113
+ shipctl knowledge refresh-intel [--workspace <id>] [--repo <id|owner/name>] [--json]
114
+ — refresh the generated repository-context
115
+ bucket for an activated repo.
116
+ Reads SHIP_API_TOKEN.
117
+
118
+ Telemetry & feedback
119
+ shipctl telemetry status|on|off|show-id|reset-id|flush|export|delete-my-data|buffer
120
+ — opt-in anonymous usage (RFC-0003); default OFF.
121
+ '--scope artifact_usage,improvement_drafts,errors'
122
+ on 'on'; '--dry-run' on 'flush';
123
+ '--out <file>' on 'export'.
124
+ shipctl feedback draft|list|show|edit|submit|remove
125
+ — local markdown drafts; submit creates a
126
+ GitHub issue via POST /feedback and moves the
127
+ draft to sent/.
128
+
129
+ Misc
130
+ shipctl verify [--no-network] [--check <id,...>] [--severity warn|error|info] [--json]
131
+ — post-adoption liveness checks
132
+ (local + config + network).
133
+ shipctl migrate [--dry-run] [--yes] [--json] [--cwd <dir>]
134
+ — upgrade .ship/config.yml from v1 to v2
135
+ (lanes-as-config).
136
+ shipctl bootstrap (stub)
137
+ shipctl help — show this help.
83
138
 
84
139
  LOCAL TREE
85
140
  pattern / tool / collection list|show|fetch scan
@@ -108,7 +163,12 @@ SUPPORTED AGENTS
108
163
  cursor, codex, claude, aider, cline, continue, windsurf, zed,
109
164
  gemini, opencode, copilot, cursor-cloud, agents-md, claude-md
110
165
 
111
- For HTTP schemas see artifacts/tools/methodology-api/ARTIFACT.md in the Ship repo.
166
+ REFERENCE
167
+ Artifacts protocol: RFC-0001 (POST /search, POST /fetch). Every consumed
168
+ artifact should be recorded in the PR as \`<kind>:<id>@<version>\`.
169
+ HTTP schemas: artifacts/tools/methodology-api/ARTIFACT.md in the Ship repo.
170
+ Operator IA (Plays / Automations / Runs / Inbox): RFC-0010.
171
+
112
172
  Package: @elmundi/ship-cli (binary: shipctl).
113
173
  `);
114
174
  }
@@ -25,7 +25,7 @@ USAGE
25
25
  shipctl kickoff [--pattern <id>] [--version <semver>] [--raw] [--json] [--cwd <dir>]
26
26
 
27
27
  DEFAULTS
28
- --pattern kickoff
28
+ --pattern common-kickoff
29
29
 
30
30
  FLAGS
31
31
  --pattern Catalog pattern id (folder under artifacts/patterns/).
@@ -39,7 +39,7 @@ When .ship/config.yml sets stack.agent.provider, a one-line hint is written
39
39
  to stderr so logs show which agent the repo is wired for — unless --json.
40
40
 
41
41
  EXAMPLE (workflow step)
42
- shipctl kickoff --pattern kickoff > kickoff.md
42
+ shipctl kickoff --pattern common-kickoff > kickoff.md
43
43
  # …concatenate workload pattern + kickoff.md into your agent invocation…
44
44
  `);
45
45
  }
@@ -64,7 +64,7 @@ function resolveMethodologyBase(ctx, config) {
64
64
 
65
65
  function parseKickoffArgs(rest) {
66
66
  const out = {
67
- patternId: "kickoff",
67
+ patternId: "common-kickoff",
68
68
  version: null,
69
69
  raw: false,
70
70
  json: false,
@@ -1,25 +1,18 @@
1
1
  /**
2
2
  * `shipctl knowledge` — manage workspace knowledge buckets.
3
3
  *
4
- * Today this is a thin wrapper around the backend's
5
- * ``POST /v1/workspaces/{ws}/repos/{repo}/knowledge_seed`` endpoint —
6
- * the same one the onboarding wizard's step 4 hits. It opens a single
7
- * PR in the tenant repo that drops starter markdown under
8
- * ``.ship/knowledge/``:
9
- *
10
- * - ``code-style.md`` — languages, naming, imports, tests, review checklist
11
- * - ``ui-runbook.md`` — design-system usage, states, perf budgets
12
- *
13
- * The CLI exists so CI pipelines (and re-adoption flows where the UI
14
- * wizard isn't the natural entry point) can wire the buckets without a
15
- * browser round-trip.
4
+ * The canonical knowledge surface is now Ship-owned:
5
+ * ``knowledge_buckets`` contain ``bucket_articles`` and
6
+ * ``knowledge_sources`` records where each article came from. The
7
+ * historical ``init`` command remains as a compatibility wrapper for
8
+ * starter PRs, while ``bootstrap`` is the GitHub Actions entry point
9
+ * that opens the generated knowledge PR after wizard seed merge.
16
10
  *
17
11
  * Usage:
18
12
  *
19
- * shipctl knowledge init [--workspace <id>] [--repo <id>]
20
- * [--only code-style,ui-runbook]
21
- * [--base-url https://api.ship.example.com]
22
- * [--json]
13
+ * shipctl knowledge fetch repository-context --workspace <id>
14
+ * shipctl knowledge bootstrap --workspace <id> --repo <id|owner/name>
15
+ * shipctl knowledge refresh-intel --workspace <id> --repo <id|owner/name>
23
16
  *
24
17
  * Auth: bearer token from ``SHIP_API_TOKEN`` (the same env var the
25
18
  * console docs describe for CLI sessions minted under Settings →
@@ -70,6 +63,18 @@ export async function knowledgeCommand(ctx, rest) {
70
63
  await knowledgeInitCommand(ctx, args);
71
64
  return;
72
65
  }
66
+ if (sub === "fetch") {
67
+ await knowledgeFetchCommand(ctx, args);
68
+ return;
69
+ }
70
+ if (sub === "bootstrap") {
71
+ await knowledgeBootstrapCommand(ctx, args);
72
+ return;
73
+ }
74
+ if (sub === "refresh-intel" || sub === "refresh-context") {
75
+ await knowledgeRefreshIntelCommand(ctx, args);
76
+ return;
77
+ }
73
78
  console.error(
74
79
  `Unknown 'shipctl knowledge' subcommand: ${sub}\nRun: shipctl knowledge --help`,
75
80
  );
@@ -82,6 +87,11 @@ function printKnowledgeHelp() {
82
87
  SUBCOMMANDS
83
88
  shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>]
84
89
  [--only <csv>] [--json]
90
+ shipctl knowledge fetch <bucket-slug> [--workspace <id>] [--json]
91
+ shipctl knowledge bootstrap [--workspace <id>] [--repo <id|owner/name>]
92
+ [--json]
93
+ shipctl knowledge refresh-intel [--workspace <id>] [--repo <id|owner/name>]
94
+ [--json]
85
95
 
86
96
  INIT FLAGS
87
97
  --workspace <id> Workspace UUID. Defaults to the only workspace
@@ -154,9 +164,116 @@ async function knowledgeInitCommand(ctx, args) {
154
164
  }
155
165
  const files = Array.isArray(result.files) ? result.files : [];
156
166
  console.log(
157
- `Seeded knowledge buckets for workspace ${workspaceId} / repo ${repoId}:\n` +
167
+ `Seeded compatibility knowledge files for workspace ${workspaceId} / repo ${repoId}:\n` +
158
168
  ` PR #${result.pr_number}: ${result.pr_url}\n` +
159
169
  ` Branch: ${result.branch}\n` +
170
+ ` Files: ${files.join(", ") || "(none)"}\n` +
171
+ `\nShip-owned repository context is refreshed separately with:\n` +
172
+ ` shipctl knowledge refresh-intel --workspace ${workspaceId} --repo ${repoId}`,
173
+ );
174
+ }
175
+
176
+ async function knowledgeFetchCommand(ctx, args) {
177
+ const opts = parseFetchArgs(args);
178
+ const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
179
+ const token = requireToken();
180
+ let workspaceId = opts.workspace;
181
+ if (!workspaceId) {
182
+ workspaceId = await resolveSoleWorkspace(baseUrl, token);
183
+ }
184
+
185
+ const [bucket, articles, sources] = await Promise.all([
186
+ apiGetJson(
187
+ baseUrl,
188
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/buckets/${encodeURIComponent(opts.slug)}`,
189
+ token,
190
+ ),
191
+ apiGetJson(
192
+ baseUrl,
193
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/buckets/${encodeURIComponent(opts.slug)}/articles`,
194
+ token,
195
+ ),
196
+ apiGetJson(
197
+ baseUrl,
198
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/buckets/${encodeURIComponent(opts.slug)}/sources`,
199
+ token,
200
+ ),
201
+ ]);
202
+
203
+ const result = { bucket, articles, sources };
204
+ if (ctx.json || opts.json) {
205
+ console.log(JSON.stringify(result, null, 2));
206
+ return;
207
+ }
208
+
209
+ console.log(`${bucket.name} (${bucket.slug})`);
210
+ console.log(` scope: ${bucket.scope_kind} source: ${bucket.source_kind}`);
211
+ console.log(` articles: ${Array.isArray(articles) ? articles.length : 0}`);
212
+ console.log(` sources: ${Array.isArray(sources) ? sources.length : 0}`);
213
+ for (const article of Array.isArray(articles) ? articles : []) {
214
+ console.log(`\n## ${article.title} (${article.slug})`);
215
+ console.log(String(article.body_md || "").trim());
216
+ }
217
+ }
218
+
219
+ async function knowledgeRefreshIntelCommand(ctx, args) {
220
+ const opts = parseRefreshArgs(args);
221
+ const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
222
+ const token = requireToken();
223
+ let workspaceId = opts.workspace;
224
+ if (!workspaceId) {
225
+ workspaceId = await resolveSoleWorkspace(baseUrl, token);
226
+ }
227
+ const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
228
+ const result = await apiPostJson(
229
+ baseUrl,
230
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/intel/harvest`,
231
+ {},
232
+ token,
233
+ );
234
+ if (ctx.json || opts.json) {
235
+ console.log(JSON.stringify(result, null, 2));
236
+ return;
237
+ }
238
+ const where = result.enqueued
239
+ ? `queued as job ${result.job_id || "(unknown)"}`
240
+ : `completed inline, intel_id=${result.intel_id || "(none)"}`;
241
+ console.log(
242
+ `Repository context refresh for workspace ${workspaceId} / repo ${repoId}: ${where}\n` +
243
+ `Fetch it with: shipctl knowledge fetch repository-context --workspace ${workspaceId}`,
244
+ );
245
+ }
246
+
247
+ async function knowledgeBootstrapCommand(ctx, args) {
248
+ const opts = parseBootstrapArgs(args);
249
+ const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
250
+ const token = requireToken();
251
+ let workspaceId = opts.workspace;
252
+ if (!workspaceId) {
253
+ workspaceId = await resolveSoleWorkspace(baseUrl, token);
254
+ }
255
+ const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
256
+ const result = await apiPostJson(
257
+ baseUrl,
258
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/knowledge/bootstrap`,
259
+ {},
260
+ token,
261
+ );
262
+ if (ctx.json || opts.json) {
263
+ console.log(JSON.stringify(result, null, 2));
264
+ return;
265
+ }
266
+ const files = Array.isArray(result.files) ? result.files : [];
267
+ if (result.status === "already_done") {
268
+ console.log(
269
+ `Knowledge bootstrap already completed for workspace ${workspaceId} / repo ${repoId}:\n` +
270
+ ` PR #${result.pr_number || "?"}: ${result.pr_url || "(unknown)"}`,
271
+ );
272
+ return;
273
+ }
274
+ console.log(
275
+ `Knowledge bootstrap opened PR #${result.pr_number} for workspace ${workspaceId} / repo ${repoId}:\n` +
276
+ ` ${result.pr_url}\n` +
160
277
  ` Files: ${files.join(", ") || "(none)"}`,
161
278
  );
162
279
  }
@@ -224,6 +341,83 @@ function parseInitArgs(args) {
224
341
  return out;
225
342
  }
226
343
 
344
+ function parseFetchArgs(args) {
345
+ const out = parseCommonArgs(args, { slug: null });
346
+ if (!out.slug) {
347
+ console.error("Usage: shipctl knowledge fetch <bucket-slug> [--workspace <id>] [--json]");
348
+ process.exit(1);
349
+ }
350
+ return out;
351
+ }
352
+
353
+ function parseRefreshArgs(args) {
354
+ return parseCommonArgs(args, { repo: null });
355
+ }
356
+
357
+ function parseBootstrapArgs(args) {
358
+ return parseCommonArgs(args, { repo: null });
359
+ }
360
+
361
+ function parseCommonArgs(args, extra) {
362
+ const out = {
363
+ workspace: null,
364
+ baseUrl: null,
365
+ json: false,
366
+ ...extra,
367
+ };
368
+ const copy = [...args];
369
+ const consume = (flag, key) => {
370
+ if (copy[0] === flag && copy[1] !== undefined) {
371
+ copy.shift();
372
+ out[key] = String(copy.shift());
373
+ return true;
374
+ }
375
+ const p = `${flag}=`;
376
+ if (copy[0] && copy[0].startsWith(p)) {
377
+ out[key] = copy[0].slice(p.length);
378
+ copy.shift();
379
+ return true;
380
+ }
381
+ return false;
382
+ };
383
+ while (copy.length) {
384
+ if (
385
+ consume("--workspace", "workspace") ||
386
+ consume("--repo", "repo") ||
387
+ consume("--base-url", "baseUrl")
388
+ ) {
389
+ continue;
390
+ }
391
+ if (copy[0] === "--json") {
392
+ out.json = true;
393
+ copy.shift();
394
+ continue;
395
+ }
396
+ if (!String(copy[0]).startsWith("-") && "slug" in out && out.slug === null) {
397
+ out.slug = String(copy.shift());
398
+ continue;
399
+ }
400
+ if (copy[0] === "--help" || copy[0] === "-h") {
401
+ printKnowledgeHelp();
402
+ process.exit(0);
403
+ }
404
+ console.error(`Unknown flag: ${copy[0]}`);
405
+ process.exit(1);
406
+ }
407
+ return out;
408
+ }
409
+
410
+ function requireToken() {
411
+ const token = process.env.SHIP_API_TOKEN || "";
412
+ if (!token) {
413
+ console.error(
414
+ "SHIP_API_TOKEN is required. Mint one at /settings in the Ship console.",
415
+ );
416
+ process.exit(1);
417
+ }
418
+ return token;
419
+ }
420
+
227
421
  /**
228
422
  * @param {string|null|undefined} explicit
229
423
  * @returns {string}
@@ -24,7 +24,13 @@ import path from "node:path";
24
24
  import YAML from "yaml";
25
25
 
26
26
  import { findShipRoot, readConfig } from "../config/io.mjs";
27
- import { validateConfig, CONFIG_SCHEMA_VERSION } from "../config/schema.mjs";
27
+ import {
28
+ validateConfig,
29
+ CONFIG_SCHEMA_VERSION,
30
+ lanePatterns,
31
+ lanePrimaryPattern,
32
+ laneFanout,
33
+ } from "../config/schema.mjs";
28
34
 
29
35
  const EXIT_OK = 0;
30
36
  const EXIT_USAGE = 2;
@@ -41,7 +47,11 @@ const DEFAULT_REUSABLE_REPO = "ship";
41
47
  const DEFAULT_REUSABLE_PATH = ".github/workflows/run-agent.yml";
42
48
 
43
49
  function printHelp() {
44
- console.log(`shipctl lanes — manage GitHub Actions caller workflows.
50
+ console.log(`shipctl lanes — manage GitHub Actions caller workflows for the
51
+ lanes declared in .ship/config.yml (each lane shows up as an
52
+ Automation in the operator console; one Run is recorded per dispatch).
53
+
54
+ Aliases: shipctl automations <subcmd> (operator-friendly name; both work)
45
55
 
46
56
  USAGE
47
57
  shipctl lanes install [--only <id,id>] [--ref <git-ref>] [--owner <gh-owner>]
@@ -174,14 +184,25 @@ async function installCmd(ctx, rest) {
174
184
  function listCmd(ctx, rest) {
175
185
  const args = parseListArgs(rest);
176
186
  const { config } = loadConfig(args.cwd);
177
- const rows = Object.entries(config.lanes || {}).map(([id, lane]) => ({
178
- lane: id,
179
- kind: lane.kind,
180
- pattern: lane.pattern || null,
181
- on: lane.kind === "event" ? lane.on || null : null,
182
- cron: lane.kind === "schedule" ? lane.cron || null : null,
183
- idempotency_key: lane.kind === "once" ? lane.idempotency?.key || null : null,
184
- }));
187
+ const rows = Object.entries(config.lanes || {}).map(([id, lane]) => {
188
+ const pats = lanePatterns(lane);
189
+ return {
190
+ lane: id,
191
+ kind: lane.kind,
192
+ // ``pattern`` keeps the single-string shape for humans/scripts
193
+ // that eyeball the first pattern; ``patterns`` always lists all
194
+ // so multi-pattern lanes (RFC-0008 C3.1) surface correctly.
195
+ pattern: pats[0] || null,
196
+ patterns: pats,
197
+ // ``fanout`` resolves to the runtime default (``matrix``) when
198
+ // the lane doesn't declare one, so the workflow plan step
199
+ // doesn't have to branch on "is this key missing?" (RFC-0008 C3.2).
200
+ fanout: laneFanout(lane),
201
+ on: lane.kind === "event" ? lane.on || null : null,
202
+ cron: lane.kind === "schedule" ? lane.cron || null : null,
203
+ idempotency_key: lane.kind === "once" ? lane.idempotency?.key || null : null,
204
+ };
205
+ });
185
206
  if (ctx.json || args.json) {
186
207
  console.log(JSON.stringify({ ok: true, lanes: rows }, null, 2));
187
208
  return;
@@ -197,8 +218,12 @@ function listCmd(ctx, rest) {
197
218
  : row.kind === "schedule"
198
219
  ? `cron ${JSON.stringify(row.cron)}`
199
220
  : `once ${row.idempotency_key || ""}`;
221
+ const patternLabel =
222
+ row.patterns.length > 1
223
+ ? `patterns=[${row.patterns.join(", ")}]`
224
+ : `pattern=${row.pattern || "-"}`;
200
225
  console.log(
201
- ` ${row.lane.padEnd(28)} kind=${row.kind.padEnd(9)} ${trigger} (pattern=${row.pattern || "-"})`,
226
+ ` ${row.lane.padEnd(28)} kind=${row.kind.padEnd(9)} ${trigger} (${patternLabel})`,
202
227
  );
203
228
  }
204
229
  }
@@ -35,10 +35,10 @@ export async function resourceManifestCommand(resource, ctx, args) {
35
35
  if (!sub || sub === "help") {
36
36
  const plural = pluralFor(spec.fetchKind);
37
37
  console.log(`Usage:
38
- ship ${resource} list
39
- ship ${resource} show <id>
40
- ship ${resource} fetch <id> [--version V] [--print]
41
- ship ${resource} search <query> [--top-k N]
38
+ shipctl ${resource} list
39
+ shipctl ${resource} show <id>
40
+ shipctl ${resource} fetch <id> [--version V] [--print]
41
+ shipctl ${resource} search <query> [--top-k N]
42
42
 
43
43
  With a local Ship tree (cwd or SHIP_REPO): scans artifacts/${plural}/<id>/ARTIFACT.md on disk.
44
44
  Otherwise: methodology API (GET /${spec.apiPath}, POST /fetch for fetch, POST /search for search).
@@ -47,7 +47,7 @@ In a Ship workspace (.ship/config.yml), 'fetch' writes the artifact to
47
47
  .ship/cache/<kind>/<id>@<version>/ARTIFACT.md and prints a 'cached:' line. Pass
48
48
  --print to also echo the body on stdout.
49
49
 
50
- Plural alias: ship ${spec.apiPath} …
50
+ Plural alias: shipctl ${spec.apiPath} …
51
51
 
52
52
  Global flags: --base-url URL --json`);
53
53
  return;
@@ -132,15 +132,15 @@ export async function patternCommand(ctx, args) {
132
132
  const [sub, ...rest] = args;
133
133
  if (!sub || sub === "help") {
134
134
  console.log(`Usage:
135
- ship pattern list
136
- ship pattern show <id>
137
- ship pattern fetch <id>
138
- ship pattern search <query> [--top-k N]
135
+ shipctl pattern list
136
+ shipctl pattern show <id>
137
+ shipctl pattern fetch <id>
138
+ shipctl pattern search <query> [--top-k N]
139
139
 
140
140
  With a local Ship tree (cwd or SHIP_REPO): list/show/fetch scan artifacts/patterns/<id>/ARTIFACT.md on disk.
141
141
  Otherwise: methodology API (GET /patterns, POST /fetch for fetch, POST /search for search).
142
142
 
143
- Plural alias: ship patterns …
143
+ Plural alias: shipctl patterns …
144
144
 
145
145
  Global flags: --base-url URL --json`);
146
146
  return;