@elmundi/ship-cli 0.8.1 → 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.
- package/README.md +651 -25
- package/bin/shipctl.mjs +168 -0
- package/lib/adapters/_fs.mjs +165 -0
- package/lib/adapters/agents/index.mjs +26 -0
- package/lib/adapters/ci/azure-pipelines.mjs +23 -0
- package/lib/adapters/ci/buildkite.mjs +24 -0
- package/lib/adapters/ci/circleci.mjs +23 -0
- package/lib/adapters/ci/gh-actions.mjs +29 -0
- package/lib/adapters/ci/gitlab-ci.mjs +23 -0
- package/lib/adapters/ci/jenkins.mjs +23 -0
- package/lib/adapters/ci/manual.mjs +18 -0
- package/lib/adapters/index.mjs +122 -0
- package/lib/adapters/language/dart.mjs +23 -0
- package/lib/adapters/language/go.mjs +23 -0
- package/lib/adapters/language/java.mjs +27 -0
- package/lib/adapters/language/js.mjs +32 -0
- package/lib/adapters/language/kotlin.mjs +48 -0
- package/lib/adapters/language/py.mjs +34 -0
- package/lib/adapters/language/rust.mjs +23 -0
- package/lib/adapters/language/swift.mjs +37 -0
- package/lib/adapters/language/ts.mjs +35 -0
- package/lib/adapters/trackers/azure-boards.mjs +49 -0
- package/lib/adapters/trackers/clickup.mjs +43 -0
- package/lib/adapters/trackers/github-issues.mjs +52 -0
- package/lib/adapters/trackers/jira.mjs +72 -0
- package/lib/adapters/trackers/linear.mjs +62 -0
- package/lib/adapters/trackers/none.mjs +18 -0
- package/lib/adapters/trackers/spreadsheet.mjs +28 -0
- package/lib/artifacts/fs-index.mjs +230 -0
- package/lib/bootstrap/render.mjs +422 -0
- package/lib/cache/store.mjs +422 -0
- package/lib/commands/bootstrap.mjs +4 -0
- package/lib/commands/callback.mjs +742 -0
- package/lib/commands/config.mjs +257 -0
- package/lib/commands/docs.mjs +4 -4
- package/lib/commands/doctor.mjs +583 -0
- package/lib/commands/feedback.mjs +355 -0
- package/lib/commands/help.mjs +159 -24
- package/lib/commands/init.mjs +830 -158
- package/lib/commands/kickoff.mjs +192 -0
- package/lib/commands/knowledge.mjs +562 -0
- package/lib/commands/lanes.mjs +527 -0
- package/lib/commands/manifest-catalog.mjs +106 -42
- package/lib/commands/migrate.mjs +204 -0
- package/lib/commands/new.mjs +452 -0
- package/lib/commands/patterns.mjs +14 -48
- package/lib/commands/run.mjs +857 -0
- package/lib/commands/search.mjs +2 -2
- package/lib/commands/sync.mjs +824 -0
- package/lib/commands/telemetry.mjs +390 -0
- package/lib/commands/trigger.mjs +196 -0
- package/lib/commands/verify.mjs +187 -0
- package/lib/config/io.mjs +232 -0
- package/lib/config/migrate.mjs +223 -0
- package/lib/config/schema.mjs +901 -0
- package/lib/detect.mjs +162 -19
- package/lib/feedback/drafts.mjs +129 -0
- package/lib/find-ship-root.mjs +16 -10
- package/lib/http.mjs +237 -11
- package/lib/state/idempotency.mjs +183 -0
- package/lib/state/lockfile.mjs +180 -0
- package/lib/telemetry/outbox.mjs +224 -0
- package/lib/templates.mjs +53 -65
- package/lib/verify/checks/agents-on-disk.mjs +58 -0
- package/lib/verify/checks/api-reachable.mjs +39 -0
- package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
- package/lib/verify/checks/bootstrap-files.mjs +67 -0
- package/lib/verify/checks/cache-integrity.mjs +51 -0
- package/lib/verify/checks/ci-secrets.mjs +86 -0
- package/lib/verify/checks/config-present.mjs +39 -0
- package/lib/verify/checks/gitignore-cache.mjs +51 -0
- package/lib/verify/checks/rules-markers.mjs +135 -0
- package/lib/verify/checks/stack-enums.mjs +33 -0
- package/lib/verify/checks/tracker-labels.mjs +91 -0
- package/lib/verify/registry.mjs +120 -0
- package/lib/version.mjs +34 -0
- package/package.json +10 -3
- package/bin/ship.mjs +0 -68
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shipctl knowledge` — manage workspace knowledge buckets.
|
|
3
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
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>
|
|
16
|
+
*
|
|
17
|
+
* Auth: bearer token from ``SHIP_API_TOKEN`` (the same env var the
|
|
18
|
+
* console docs describe for CLI sessions minted under Settings →
|
|
19
|
+
* "Mint a CLI token"). We deliberately don't read ``SHIP_RUN_TOKEN`` —
|
|
20
|
+
* that's a short-lived pipeline handle, not a user PAT.
|
|
21
|
+
*
|
|
22
|
+
* Base URL resolution:
|
|
23
|
+
*
|
|
24
|
+
* 1. ``--base-url`` flag (explicit wins)
|
|
25
|
+
* 2. ``SHIP_WORKSPACE_API_BASE`` (workspace control plane)
|
|
26
|
+
* 3. ``SHIP_API_BASE`` (methodology API; only usable if the caller
|
|
27
|
+
* ran their own reverse-proxy that co-locates both)
|
|
28
|
+
* 4. ``https://api.ship.elmundi.com`` as the canonical production
|
|
29
|
+
* workspace API.
|
|
30
|
+
*
|
|
31
|
+
* Workspace + repo resolution:
|
|
32
|
+
*
|
|
33
|
+
* - ``--workspace`` pins a workspace id; otherwise we fetch
|
|
34
|
+
* ``GET /v1/workspaces`` and pick the only row. If there are
|
|
35
|
+
* multiple rows we abort with a helpful message so the caller
|
|
36
|
+
* either supplies ``--workspace`` or narrows their PAT.
|
|
37
|
+
* - ``--repo`` pins a repo id (uuid) or a full_name like
|
|
38
|
+
* ``owner/name``; otherwise we fetch
|
|
39
|
+
* ``GET /v1/workspaces/{ws}/repos`` and pick the most-recently
|
|
40
|
+
* activated row — the same heuristic the wizard uses, so
|
|
41
|
+
* ``shipctl knowledge init`` on a freshly-onboarded workspace
|
|
42
|
+
* seeds the repo the user just activated.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const VERSION = "v1";
|
|
46
|
+
|
|
47
|
+
/** Ship ships two starter buckets today — keep in lockstep with
|
|
48
|
+
* ``backend.app.services.catalog.KNOWLEDGE_STARTERS`` and
|
|
49
|
+
* ``console/src/lib/api/client.ts#KNOWLEDGE_STARTERS``. */
|
|
50
|
+
const KNOWN_SLUGS = ["code-style", "ui-runbook"];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
54
|
+
* @param {string[]} rest
|
|
55
|
+
*/
|
|
56
|
+
export async function knowledgeCommand(ctx, rest) {
|
|
57
|
+
const [sub, ...args] = rest;
|
|
58
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
59
|
+
printKnowledgeHelp();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (sub === "init") {
|
|
63
|
+
await knowledgeInitCommand(ctx, args);
|
|
64
|
+
return;
|
|
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
|
+
}
|
|
78
|
+
console.error(
|
|
79
|
+
`Unknown 'shipctl knowledge' subcommand: ${sub}\nRun: shipctl knowledge --help`,
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printKnowledgeHelp() {
|
|
85
|
+
console.log(`shipctl knowledge — manage workspace knowledge buckets (${VERSION})
|
|
86
|
+
|
|
87
|
+
SUBCOMMANDS
|
|
88
|
+
shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>]
|
|
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]
|
|
95
|
+
|
|
96
|
+
INIT FLAGS
|
|
97
|
+
--workspace <id> Workspace UUID. Defaults to the only workspace
|
|
98
|
+
the caller's PAT can see.
|
|
99
|
+
--repo <ref> Workspace repo UUID, or GitHub 'owner/name'.
|
|
100
|
+
Defaults to the most-recently activated repo in
|
|
101
|
+
the resolved workspace.
|
|
102
|
+
--only <csv> Comma-separated starter slugs. Defaults to the
|
|
103
|
+
full catalog (${KNOWN_SLUGS.join(", ")}).
|
|
104
|
+
--base-url URL Workspace control-plane API. See env fallbacks.
|
|
105
|
+
--json Emit a machine-readable JSON summary.
|
|
106
|
+
|
|
107
|
+
ENV
|
|
108
|
+
SHIP_API_TOKEN Required. Bearer PAT minted at /settings.
|
|
109
|
+
SHIP_WORKSPACE_API_BASE Optional override for the control plane.
|
|
110
|
+
SHIP_API_BASE Fallback only (co-located proxies).
|
|
111
|
+
|
|
112
|
+
EXIT
|
|
113
|
+
0 PR opened (or idempotently already present)
|
|
114
|
+
1 arg / config error
|
|
115
|
+
2 auth error (401)
|
|
116
|
+
3 network / HTTP 5xx
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
122
|
+
* @param {string[]} args
|
|
123
|
+
*/
|
|
124
|
+
async function knowledgeInitCommand(ctx, args) {
|
|
125
|
+
const opts = parseInitArgs(args);
|
|
126
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
|
|
127
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
128
|
+
if (!token) {
|
|
129
|
+
console.error(
|
|
130
|
+
"SHIP_API_TOKEN is required. Mint one at /settings in the Ship console.",
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const selection = opts.only;
|
|
136
|
+
if (selection !== null) {
|
|
137
|
+
const unknown = selection.filter((s) => !KNOWN_SLUGS.includes(s));
|
|
138
|
+
if (unknown.length) {
|
|
139
|
+
console.error(
|
|
140
|
+
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown: ${KNOWN_SLUGS.join(", ")}`,
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let workspaceId = opts.workspace;
|
|
147
|
+
if (!workspaceId) {
|
|
148
|
+
workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
152
|
+
|
|
153
|
+
const body = selection === null ? {} : { selection };
|
|
154
|
+
const result = await apiPostJson(
|
|
155
|
+
baseUrl,
|
|
156
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/knowledge_seed`,
|
|
157
|
+
body,
|
|
158
|
+
token,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (ctx.json || opts.json) {
|
|
162
|
+
console.log(JSON.stringify(result, null, 2));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
166
|
+
console.log(
|
|
167
|
+
`Seeded compatibility knowledge files for workspace ${workspaceId} / repo ${repoId}:\n` +
|
|
168
|
+
` PR #${result.pr_number}: ${result.pr_url}\n` +
|
|
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` +
|
|
277
|
+
` Files: ${files.join(", ") || "(none)"}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {string[]} args
|
|
283
|
+
* @returns {{
|
|
284
|
+
* workspace: string|null,
|
|
285
|
+
* repo: string|null,
|
|
286
|
+
* only: string[]|null,
|
|
287
|
+
* baseUrl: string|null,
|
|
288
|
+
* json: boolean,
|
|
289
|
+
* }}
|
|
290
|
+
*/
|
|
291
|
+
function parseInitArgs(args) {
|
|
292
|
+
const out = {
|
|
293
|
+
workspace: null,
|
|
294
|
+
repo: null,
|
|
295
|
+
only: null,
|
|
296
|
+
baseUrl: null,
|
|
297
|
+
json: false,
|
|
298
|
+
};
|
|
299
|
+
const copy = [...args];
|
|
300
|
+
const consume = (flag, key) => {
|
|
301
|
+
if (copy[0] === flag && copy[1] !== undefined) {
|
|
302
|
+
copy.shift();
|
|
303
|
+
out[key] = String(copy.shift());
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
const p = `${flag}=`;
|
|
307
|
+
if (copy[0] && copy[0].startsWith(p)) {
|
|
308
|
+
out[key] = copy[0].slice(p.length);
|
|
309
|
+
copy.shift();
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
};
|
|
314
|
+
while (copy.length) {
|
|
315
|
+
if (
|
|
316
|
+
consume("--workspace", "workspace") ||
|
|
317
|
+
consume("--repo", "repo") ||
|
|
318
|
+
consume("--only", "only") ||
|
|
319
|
+
consume("--base-url", "baseUrl")
|
|
320
|
+
) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (copy[0] === "--json") {
|
|
324
|
+
out.json = true;
|
|
325
|
+
copy.shift();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
329
|
+
printKnowledgeHelp();
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
console.error(`Unknown flag: ${copy[0]}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
if (out.only !== null) {
|
|
336
|
+
out.only = String(out.only)
|
|
337
|
+
.split(",")
|
|
338
|
+
.map((s) => s.trim())
|
|
339
|
+
.filter(Boolean);
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
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
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @param {string|null|undefined} explicit
|
|
423
|
+
* @returns {string}
|
|
424
|
+
*/
|
|
425
|
+
function resolveBaseUrl(explicit) {
|
|
426
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
427
|
+
const envWorkspace = process.env.SHIP_WORKSPACE_API_BASE;
|
|
428
|
+
if (envWorkspace) return envWorkspace.replace(/\/+$/, "");
|
|
429
|
+
const envGeneric = process.env.SHIP_API_BASE;
|
|
430
|
+
if (envGeneric) return envGeneric.replace(/\/+$/, "");
|
|
431
|
+
return "https://api.ship.elmundi.com";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {string} baseUrl
|
|
436
|
+
* @param {string} token
|
|
437
|
+
* @returns {Promise<string>}
|
|
438
|
+
*/
|
|
439
|
+
async function resolveSoleWorkspace(baseUrl, token) {
|
|
440
|
+
const rows = await apiGetJson(baseUrl, "/v1/workspaces", token);
|
|
441
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
442
|
+
console.error("No workspaces visible to this token.");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
if (rows.length > 1) {
|
|
446
|
+
const ids = rows.map((r) => `${r.id} (${r.name ?? "?"})`).join("\n ");
|
|
447
|
+
console.error(
|
|
448
|
+
`Token has access to more than one workspace; pass --workspace <id>.\n ${ids}`,
|
|
449
|
+
);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
return String(rows[0].id);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {string} baseUrl
|
|
457
|
+
* @param {string} token
|
|
458
|
+
* @param {string} workspaceId
|
|
459
|
+
* @param {string|null} hint
|
|
460
|
+
* @returns {Promise<string>}
|
|
461
|
+
*/
|
|
462
|
+
async function resolveRepoId(baseUrl, token, workspaceId, hint) {
|
|
463
|
+
// Direct UUID? Accept it verbatim — avoids a list call.
|
|
464
|
+
if (hint && /^[0-9a-fA-F-]{32,36}$/.test(hint) && hint.includes("-")) {
|
|
465
|
+
return hint;
|
|
466
|
+
}
|
|
467
|
+
const rows = await apiGetJson(
|
|
468
|
+
baseUrl,
|
|
469
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos`,
|
|
470
|
+
token,
|
|
471
|
+
);
|
|
472
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
473
|
+
console.error(
|
|
474
|
+
`Workspace ${workspaceId} has no activated repos. Activate one in the console first.`,
|
|
475
|
+
);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
if (hint) {
|
|
479
|
+
const match = rows.find(
|
|
480
|
+
(r) =>
|
|
481
|
+
r.full_name === hint ||
|
|
482
|
+
`${r.owner ?? ""}/${r.name ?? ""}` === hint ||
|
|
483
|
+
r.id === hint,
|
|
484
|
+
);
|
|
485
|
+
if (!match) {
|
|
486
|
+
const known = rows.map((r) => r.full_name ?? r.id).join(", ");
|
|
487
|
+
console.error(
|
|
488
|
+
`--repo ${hint} doesn't match any activated repo in workspace ${workspaceId}.\nKnown: ${known}`,
|
|
489
|
+
);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
return String(match.id);
|
|
493
|
+
}
|
|
494
|
+
const sorted = [...rows].sort((a, b) => {
|
|
495
|
+
const ax = a.activated_at ? Date.parse(a.activated_at) : 0;
|
|
496
|
+
const bx = b.activated_at ? Date.parse(b.activated_at) : 0;
|
|
497
|
+
return bx - ax;
|
|
498
|
+
});
|
|
499
|
+
return String(sorted[0].id);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {string} baseUrl
|
|
504
|
+
* @param {string} path
|
|
505
|
+
* @param {string} token
|
|
506
|
+
*/
|
|
507
|
+
async function apiGetJson(baseUrl, path, token) {
|
|
508
|
+
return apiRequest(baseUrl, path, "GET", token, null);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @param {string} baseUrl
|
|
513
|
+
* @param {string} path
|
|
514
|
+
* @param {Record<string, unknown>} body
|
|
515
|
+
* @param {string} token
|
|
516
|
+
*/
|
|
517
|
+
async function apiPostJson(baseUrl, path, body, token) {
|
|
518
|
+
return apiRequest(baseUrl, path, "POST", token, body);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* @param {string} baseUrl
|
|
523
|
+
* @param {string} path
|
|
524
|
+
* @param {string} method
|
|
525
|
+
* @param {string} token
|
|
526
|
+
* @param {Record<string, unknown>|null} body
|
|
527
|
+
*/
|
|
528
|
+
async function apiRequest(baseUrl, path, method, token, body) {
|
|
529
|
+
const url = `${baseUrl}${path}`;
|
|
530
|
+
let res;
|
|
531
|
+
try {
|
|
532
|
+
res = await fetch(url, {
|
|
533
|
+
method,
|
|
534
|
+
headers: {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
Accept: "application/json",
|
|
537
|
+
Authorization: `Bearer ${token}`,
|
|
538
|
+
},
|
|
539
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
540
|
+
});
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.error(`Network error calling ${url}: ${err instanceof Error ? err.message : err}`);
|
|
543
|
+
process.exit(3);
|
|
544
|
+
}
|
|
545
|
+
const text = await res.text();
|
|
546
|
+
let data = null;
|
|
547
|
+
try {
|
|
548
|
+
data = text ? JSON.parse(text) : null;
|
|
549
|
+
} catch {
|
|
550
|
+
data = text;
|
|
551
|
+
}
|
|
552
|
+
if (res.ok) return data;
|
|
553
|
+
if (res.status === 401) {
|
|
554
|
+
console.error(
|
|
555
|
+
`HTTP 401 on ${method} ${url} — SHIP_API_TOKEN is missing, expired, or lacks workspace access.`,
|
|
556
|
+
);
|
|
557
|
+
process.exit(2);
|
|
558
|
+
}
|
|
559
|
+
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
560
|
+
console.error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
|
|
561
|
+
process.exit(res.status >= 500 ? 3 : 1);
|
|
562
|
+
}
|