@elmundi/ship-cli 0.8.1 → 0.11.2
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 +415 -22
- package/bin/shipctl.mjs +165 -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 +373 -0
- package/lib/cache/store.mjs +422 -0
- package/lib/commands/bootstrap.mjs +4 -0
- package/lib/commands/callback.mjs +302 -0
- package/lib/commands/config.mjs +257 -0
- package/lib/commands/docs.mjs +1 -1
- package/lib/commands/doctor.mjs +583 -0
- package/lib/commands/feedback.mjs +355 -0
- package/lib/commands/help.mjs +96 -21
- package/lib/commands/init.mjs +830 -158
- package/lib/commands/kickoff.mjs +192 -0
- package/lib/commands/knowledge.mjs +368 -0
- package/lib/commands/lanes.mjs +502 -0
- package/lib/commands/manifest-catalog.mjs +102 -38
- package/lib/commands/migrate.mjs +204 -0
- package/lib/commands/new.mjs +452 -0
- package/lib/commands/patterns.mjs +9 -43
- package/lib/commands/run.mjs +617 -0
- package/lib/commands/sync.mjs +749 -0
- package/lib/commands/telemetry.mjs +390 -0
- package/lib/commands/verify.mjs +187 -0
- package/lib/config/io.mjs +232 -0
- package/lib/config/migrate.mjs +215 -0
- package/lib/config/schema.mjs +650 -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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shipctl kickoff` — print the markdown body of the `kickoff` pattern
|
|
3
|
+
* (or another pattern id) for piping into the customer’s agent in CI.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order for the methodology host:
|
|
6
|
+
* 1. Global `--base-url` (methodology API root, same as `pattern fetch`).
|
|
7
|
+
* 2. `.ship/config.yml` → `api.base_url` with `/api/methodology` appended
|
|
8
|
+
* when absent.
|
|
9
|
+
* 3. `SHIP_API_BASE` / default public host.
|
|
10
|
+
*
|
|
11
|
+
* When the process cwd (or `--cwd`) is inside the Ship monorepo, we read
|
|
12
|
+
* `artifacts/patterns/<id>/ARTIFACT.md` from disk so local dev matches prod.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fetchArtifact } from "../http.mjs";
|
|
17
|
+
import { readConfig, findShipRoot } from "../config/io.mjs";
|
|
18
|
+
import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
|
|
19
|
+
import { readArtifactFile } from "../artifacts/fs-index.mjs";
|
|
20
|
+
|
|
21
|
+
function printKickoffHelp() {
|
|
22
|
+
console.log(`shipctl kickoff — print a pattern body for piping into your agent (CI).
|
|
23
|
+
|
|
24
|
+
USAGE
|
|
25
|
+
shipctl kickoff [--pattern <id>] [--version <semver>] [--raw] [--json] [--cwd <dir>]
|
|
26
|
+
|
|
27
|
+
DEFAULTS
|
|
28
|
+
--pattern kickoff
|
|
29
|
+
|
|
30
|
+
FLAGS
|
|
31
|
+
--pattern Catalog pattern id (folder under artifacts/patterns/).
|
|
32
|
+
--version Optional pinned version (POST /fetch).
|
|
33
|
+
--raw Print the full ARTIFACT.md including YAML front matter.
|
|
34
|
+
--json Emit { pattern_id, body, agent_provider? } JSON.
|
|
35
|
+
--cwd Repo root to find .ship/config.yml (default: search upward).
|
|
36
|
+
|
|
37
|
+
The default output is markdown body only (front matter stripped) on stdout.
|
|
38
|
+
When .ship/config.yml sets stack.agent.provider, a one-line hint is written
|
|
39
|
+
to stderr so logs show which agent the repo is wired for — unless --json.
|
|
40
|
+
|
|
41
|
+
EXAMPLE (workflow step)
|
|
42
|
+
shipctl kickoff --pattern kickoff > kickoff.md
|
|
43
|
+
# …concatenate workload pattern + kickoff.md into your agent invocation…
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripFrontmatter(full) {
|
|
48
|
+
if (!full || !full.startsWith("---\n")) return full;
|
|
49
|
+
const end = full.indexOf("\n---\n", 4);
|
|
50
|
+
if (end === -1) return full;
|
|
51
|
+
return full.slice(end + "\n---\n".length);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} methodologyBase */
|
|
55
|
+
function resolveMethodologyBase(ctx, config) {
|
|
56
|
+
const fromFlag = ctx.baseUrl;
|
|
57
|
+
const raw = config?.api?.base_url;
|
|
58
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
59
|
+
const u = raw.replace(/\/$/, "");
|
|
60
|
+
return u.includes("/api/methodology") ? u : `${u}/api/methodology`;
|
|
61
|
+
}
|
|
62
|
+
return fromFlag;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseKickoffArgs(rest) {
|
|
66
|
+
const out = {
|
|
67
|
+
patternId: "kickoff",
|
|
68
|
+
version: null,
|
|
69
|
+
raw: false,
|
|
70
|
+
json: false,
|
|
71
|
+
help: false,
|
|
72
|
+
cwd: process.cwd(),
|
|
73
|
+
};
|
|
74
|
+
const copy = [...rest];
|
|
75
|
+
while (copy.length) {
|
|
76
|
+
const a = copy[0];
|
|
77
|
+
if (a === "--help" || a === "-h") {
|
|
78
|
+
out.help = true;
|
|
79
|
+
copy.shift();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a === "--raw") {
|
|
83
|
+
out.raw = true;
|
|
84
|
+
copy.shift();
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (a === "--json") {
|
|
88
|
+
out.json = true;
|
|
89
|
+
copy.shift();
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (a === "--pattern" && copy[1]) {
|
|
93
|
+
copy.shift();
|
|
94
|
+
out.patternId = String(copy.shift());
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (a.startsWith("--pattern=")) {
|
|
98
|
+
out.patternId = a.slice("--pattern=".length);
|
|
99
|
+
copy.shift();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (a === "--version" && copy[1]) {
|
|
103
|
+
copy.shift();
|
|
104
|
+
out.version = String(copy.shift());
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (a.startsWith("--version=")) {
|
|
108
|
+
out.version = a.slice("--version=".length);
|
|
109
|
+
copy.shift();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (a === "--cwd" && copy[1]) {
|
|
113
|
+
copy.shift();
|
|
114
|
+
out.cwd = path.resolve(String(copy.shift()));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (a.startsWith("--cwd=")) {
|
|
118
|
+
out.cwd = path.resolve(a.slice("--cwd=".length));
|
|
119
|
+
copy.shift();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
console.error(`unknown argument: ${a}\nRun: shipctl kickoff --help`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function kickoffCommand(ctx, rest) {
|
|
129
|
+
const args = parseKickoffArgs(rest);
|
|
130
|
+
if (args.help) {
|
|
131
|
+
printKickoffHelp();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ctx2 = ctx;
|
|
136
|
+
|
|
137
|
+
/** @type {object|null} */
|
|
138
|
+
let config = null;
|
|
139
|
+
const root = findShipRoot(args.cwd);
|
|
140
|
+
if (root) {
|
|
141
|
+
try {
|
|
142
|
+
config = readConfig(root).config;
|
|
143
|
+
} catch {
|
|
144
|
+
config = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const methodologyBase = resolveMethodologyBase(ctx2, config);
|
|
149
|
+
const agentProvider =
|
|
150
|
+
config?.stack?.agent && typeof config.stack.agent === "object"
|
|
151
|
+
? config.stack.agent.provider
|
|
152
|
+
: null;
|
|
153
|
+
|
|
154
|
+
/** @type {string|undefined} */
|
|
155
|
+
let fullText;
|
|
156
|
+
const shipRepo = resolveShipRepoRootForCatalog();
|
|
157
|
+
if (shipRepo) {
|
|
158
|
+
const file = readArtifactFile(shipRepo, "pattern", args.patternId);
|
|
159
|
+
if (file) fullText = file.content;
|
|
160
|
+
}
|
|
161
|
+
if (fullText === undefined) {
|
|
162
|
+
const { content } = await fetchArtifact(
|
|
163
|
+
methodologyBase,
|
|
164
|
+
"pattern",
|
|
165
|
+
args.patternId,
|
|
166
|
+
args.version || undefined,
|
|
167
|
+
);
|
|
168
|
+
fullText = content;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const body = args.raw ? fullText : stripFrontmatter(fullText);
|
|
172
|
+
|
|
173
|
+
if (args.json) {
|
|
174
|
+
console.log(
|
|
175
|
+
JSON.stringify(
|
|
176
|
+
{
|
|
177
|
+
pattern_id: args.patternId,
|
|
178
|
+
body,
|
|
179
|
+
agent_provider: agentProvider || null,
|
|
180
|
+
},
|
|
181
|
+
null,
|
|
182
|
+
2,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (agentProvider && typeof agentProvider === "string") {
|
|
189
|
+
console.error(`# ship: stack.agent.provider=${agentProvider}`);
|
|
190
|
+
}
|
|
191
|
+
process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
192
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shipctl knowledge` — manage workspace knowledge buckets.
|
|
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.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
*
|
|
19
|
+
* shipctl knowledge init [--workspace <id>] [--repo <id>]
|
|
20
|
+
* [--only code-style,ui-runbook]
|
|
21
|
+
* [--base-url https://api.ship.example.com]
|
|
22
|
+
* [--json]
|
|
23
|
+
*
|
|
24
|
+
* Auth: bearer token from ``SHIP_API_TOKEN`` (the same env var the
|
|
25
|
+
* console docs describe for CLI sessions minted under Settings →
|
|
26
|
+
* "Mint a CLI token"). We deliberately don't read ``SHIP_RUN_TOKEN`` —
|
|
27
|
+
* that's a short-lived pipeline handle, not a user PAT.
|
|
28
|
+
*
|
|
29
|
+
* Base URL resolution:
|
|
30
|
+
*
|
|
31
|
+
* 1. ``--base-url`` flag (explicit wins)
|
|
32
|
+
* 2. ``SHIP_WORKSPACE_API_BASE`` (workspace control plane)
|
|
33
|
+
* 3. ``SHIP_API_BASE`` (methodology API; only usable if the caller
|
|
34
|
+
* ran their own reverse-proxy that co-locates both)
|
|
35
|
+
* 4. ``https://api.ship.elmundi.com`` as the canonical production
|
|
36
|
+
* workspace API.
|
|
37
|
+
*
|
|
38
|
+
* Workspace + repo resolution:
|
|
39
|
+
*
|
|
40
|
+
* - ``--workspace`` pins a workspace id; otherwise we fetch
|
|
41
|
+
* ``GET /v1/workspaces`` and pick the only row. If there are
|
|
42
|
+
* multiple rows we abort with a helpful message so the caller
|
|
43
|
+
* either supplies ``--workspace`` or narrows their PAT.
|
|
44
|
+
* - ``--repo`` pins a repo id (uuid) or a full_name like
|
|
45
|
+
* ``owner/name``; otherwise we fetch
|
|
46
|
+
* ``GET /v1/workspaces/{ws}/repos`` and pick the most-recently
|
|
47
|
+
* activated row — the same heuristic the wizard uses, so
|
|
48
|
+
* ``shipctl knowledge init`` on a freshly-onboarded workspace
|
|
49
|
+
* seeds the repo the user just activated.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const VERSION = "v1";
|
|
53
|
+
|
|
54
|
+
/** Ship ships two starter buckets today — keep in lockstep with
|
|
55
|
+
* ``backend.app.services.catalog.KNOWLEDGE_STARTERS`` and
|
|
56
|
+
* ``console/src/lib/api/client.ts#KNOWLEDGE_STARTERS``. */
|
|
57
|
+
const KNOWN_SLUGS = ["code-style", "ui-runbook"];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
61
|
+
* @param {string[]} rest
|
|
62
|
+
*/
|
|
63
|
+
export async function knowledgeCommand(ctx, rest) {
|
|
64
|
+
const [sub, ...args] = rest;
|
|
65
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
|
|
66
|
+
printKnowledgeHelp();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (sub === "init") {
|
|
70
|
+
await knowledgeInitCommand(ctx, args);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.error(
|
|
74
|
+
`Unknown 'shipctl knowledge' subcommand: ${sub}\nRun: shipctl knowledge --help`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printKnowledgeHelp() {
|
|
80
|
+
console.log(`shipctl knowledge — manage workspace knowledge buckets (${VERSION})
|
|
81
|
+
|
|
82
|
+
SUBCOMMANDS
|
|
83
|
+
shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>]
|
|
84
|
+
[--only <csv>] [--json]
|
|
85
|
+
|
|
86
|
+
INIT FLAGS
|
|
87
|
+
--workspace <id> Workspace UUID. Defaults to the only workspace
|
|
88
|
+
the caller's PAT can see.
|
|
89
|
+
--repo <ref> Workspace repo UUID, or GitHub 'owner/name'.
|
|
90
|
+
Defaults to the most-recently activated repo in
|
|
91
|
+
the resolved workspace.
|
|
92
|
+
--only <csv> Comma-separated starter slugs. Defaults to the
|
|
93
|
+
full catalog (${KNOWN_SLUGS.join(", ")}).
|
|
94
|
+
--base-url URL Workspace control-plane API. See env fallbacks.
|
|
95
|
+
--json Emit a machine-readable JSON summary.
|
|
96
|
+
|
|
97
|
+
ENV
|
|
98
|
+
SHIP_API_TOKEN Required. Bearer PAT minted at /settings.
|
|
99
|
+
SHIP_WORKSPACE_API_BASE Optional override for the control plane.
|
|
100
|
+
SHIP_API_BASE Fallback only (co-located proxies).
|
|
101
|
+
|
|
102
|
+
EXIT
|
|
103
|
+
0 PR opened (or idempotently already present)
|
|
104
|
+
1 arg / config error
|
|
105
|
+
2 auth error (401)
|
|
106
|
+
3 network / HTTP 5xx
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
112
|
+
* @param {string[]} args
|
|
113
|
+
*/
|
|
114
|
+
async function knowledgeInitCommand(ctx, args) {
|
|
115
|
+
const opts = parseInitArgs(args);
|
|
116
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
|
|
117
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
118
|
+
if (!token) {
|
|
119
|
+
console.error(
|
|
120
|
+
"SHIP_API_TOKEN is required. Mint one at /settings in the Ship console.",
|
|
121
|
+
);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const selection = opts.only;
|
|
126
|
+
if (selection !== null) {
|
|
127
|
+
const unknown = selection.filter((s) => !KNOWN_SLUGS.includes(s));
|
|
128
|
+
if (unknown.length) {
|
|
129
|
+
console.error(
|
|
130
|
+
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown: ${KNOWN_SLUGS.join(", ")}`,
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let workspaceId = opts.workspace;
|
|
137
|
+
if (!workspaceId) {
|
|
138
|
+
workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
142
|
+
|
|
143
|
+
const body = selection === null ? {} : { selection };
|
|
144
|
+
const result = await apiPostJson(
|
|
145
|
+
baseUrl,
|
|
146
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/knowledge_seed`,
|
|
147
|
+
body,
|
|
148
|
+
token,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (ctx.json || opts.json) {
|
|
152
|
+
console.log(JSON.stringify(result, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
156
|
+
console.log(
|
|
157
|
+
`Seeded knowledge buckets for workspace ${workspaceId} / repo ${repoId}:\n` +
|
|
158
|
+
` PR #${result.pr_number}: ${result.pr_url}\n` +
|
|
159
|
+
` Branch: ${result.branch}\n` +
|
|
160
|
+
` Files: ${files.join(", ") || "(none)"}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string[]} args
|
|
166
|
+
* @returns {{
|
|
167
|
+
* workspace: string|null,
|
|
168
|
+
* repo: string|null,
|
|
169
|
+
* only: string[]|null,
|
|
170
|
+
* baseUrl: string|null,
|
|
171
|
+
* json: boolean,
|
|
172
|
+
* }}
|
|
173
|
+
*/
|
|
174
|
+
function parseInitArgs(args) {
|
|
175
|
+
const out = {
|
|
176
|
+
workspace: null,
|
|
177
|
+
repo: null,
|
|
178
|
+
only: null,
|
|
179
|
+
baseUrl: null,
|
|
180
|
+
json: false,
|
|
181
|
+
};
|
|
182
|
+
const copy = [...args];
|
|
183
|
+
const consume = (flag, key) => {
|
|
184
|
+
if (copy[0] === flag && copy[1] !== undefined) {
|
|
185
|
+
copy.shift();
|
|
186
|
+
out[key] = String(copy.shift());
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
const p = `${flag}=`;
|
|
190
|
+
if (copy[0] && copy[0].startsWith(p)) {
|
|
191
|
+
out[key] = copy[0].slice(p.length);
|
|
192
|
+
copy.shift();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
while (copy.length) {
|
|
198
|
+
if (
|
|
199
|
+
consume("--workspace", "workspace") ||
|
|
200
|
+
consume("--repo", "repo") ||
|
|
201
|
+
consume("--only", "only") ||
|
|
202
|
+
consume("--base-url", "baseUrl")
|
|
203
|
+
) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (copy[0] === "--json") {
|
|
207
|
+
out.json = true;
|
|
208
|
+
copy.shift();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
212
|
+
printKnowledgeHelp();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
console.error(`Unknown flag: ${copy[0]}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
if (out.only !== null) {
|
|
219
|
+
out.only = String(out.only)
|
|
220
|
+
.split(",")
|
|
221
|
+
.map((s) => s.trim())
|
|
222
|
+
.filter(Boolean);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {string|null|undefined} explicit
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
function resolveBaseUrl(explicit) {
|
|
232
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
233
|
+
const envWorkspace = process.env.SHIP_WORKSPACE_API_BASE;
|
|
234
|
+
if (envWorkspace) return envWorkspace.replace(/\/+$/, "");
|
|
235
|
+
const envGeneric = process.env.SHIP_API_BASE;
|
|
236
|
+
if (envGeneric) return envGeneric.replace(/\/+$/, "");
|
|
237
|
+
return "https://api.ship.elmundi.com";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} baseUrl
|
|
242
|
+
* @param {string} token
|
|
243
|
+
* @returns {Promise<string>}
|
|
244
|
+
*/
|
|
245
|
+
async function resolveSoleWorkspace(baseUrl, token) {
|
|
246
|
+
const rows = await apiGetJson(baseUrl, "/v1/workspaces", token);
|
|
247
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
248
|
+
console.error("No workspaces visible to this token.");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (rows.length > 1) {
|
|
252
|
+
const ids = rows.map((r) => `${r.id} (${r.name ?? "?"})`).join("\n ");
|
|
253
|
+
console.error(
|
|
254
|
+
`Token has access to more than one workspace; pass --workspace <id>.\n ${ids}`,
|
|
255
|
+
);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
return String(rows[0].id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {string} baseUrl
|
|
263
|
+
* @param {string} token
|
|
264
|
+
* @param {string} workspaceId
|
|
265
|
+
* @param {string|null} hint
|
|
266
|
+
* @returns {Promise<string>}
|
|
267
|
+
*/
|
|
268
|
+
async function resolveRepoId(baseUrl, token, workspaceId, hint) {
|
|
269
|
+
// Direct UUID? Accept it verbatim — avoids a list call.
|
|
270
|
+
if (hint && /^[0-9a-fA-F-]{32,36}$/.test(hint) && hint.includes("-")) {
|
|
271
|
+
return hint;
|
|
272
|
+
}
|
|
273
|
+
const rows = await apiGetJson(
|
|
274
|
+
baseUrl,
|
|
275
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos`,
|
|
276
|
+
token,
|
|
277
|
+
);
|
|
278
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
279
|
+
console.error(
|
|
280
|
+
`Workspace ${workspaceId} has no activated repos. Activate one in the console first.`,
|
|
281
|
+
);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
if (hint) {
|
|
285
|
+
const match = rows.find(
|
|
286
|
+
(r) =>
|
|
287
|
+
r.full_name === hint ||
|
|
288
|
+
`${r.owner ?? ""}/${r.name ?? ""}` === hint ||
|
|
289
|
+
r.id === hint,
|
|
290
|
+
);
|
|
291
|
+
if (!match) {
|
|
292
|
+
const known = rows.map((r) => r.full_name ?? r.id).join(", ");
|
|
293
|
+
console.error(
|
|
294
|
+
`--repo ${hint} doesn't match any activated repo in workspace ${workspaceId}.\nKnown: ${known}`,
|
|
295
|
+
);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
return String(match.id);
|
|
299
|
+
}
|
|
300
|
+
const sorted = [...rows].sort((a, b) => {
|
|
301
|
+
const ax = a.activated_at ? Date.parse(a.activated_at) : 0;
|
|
302
|
+
const bx = b.activated_at ? Date.parse(b.activated_at) : 0;
|
|
303
|
+
return bx - ax;
|
|
304
|
+
});
|
|
305
|
+
return String(sorted[0].id);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {string} baseUrl
|
|
310
|
+
* @param {string} path
|
|
311
|
+
* @param {string} token
|
|
312
|
+
*/
|
|
313
|
+
async function apiGetJson(baseUrl, path, token) {
|
|
314
|
+
return apiRequest(baseUrl, path, "GET", token, null);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} baseUrl
|
|
319
|
+
* @param {string} path
|
|
320
|
+
* @param {Record<string, unknown>} body
|
|
321
|
+
* @param {string} token
|
|
322
|
+
*/
|
|
323
|
+
async function apiPostJson(baseUrl, path, body, token) {
|
|
324
|
+
return apiRequest(baseUrl, path, "POST", token, body);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} baseUrl
|
|
329
|
+
* @param {string} path
|
|
330
|
+
* @param {string} method
|
|
331
|
+
* @param {string} token
|
|
332
|
+
* @param {Record<string, unknown>|null} body
|
|
333
|
+
*/
|
|
334
|
+
async function apiRequest(baseUrl, path, method, token, body) {
|
|
335
|
+
const url = `${baseUrl}${path}`;
|
|
336
|
+
let res;
|
|
337
|
+
try {
|
|
338
|
+
res = await fetch(url, {
|
|
339
|
+
method,
|
|
340
|
+
headers: {
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
Accept: "application/json",
|
|
343
|
+
Authorization: `Bearer ${token}`,
|
|
344
|
+
},
|
|
345
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error(`Network error calling ${url}: ${err instanceof Error ? err.message : err}`);
|
|
349
|
+
process.exit(3);
|
|
350
|
+
}
|
|
351
|
+
const text = await res.text();
|
|
352
|
+
let data = null;
|
|
353
|
+
try {
|
|
354
|
+
data = text ? JSON.parse(text) : null;
|
|
355
|
+
} catch {
|
|
356
|
+
data = text;
|
|
357
|
+
}
|
|
358
|
+
if (res.ok) return data;
|
|
359
|
+
if (res.status === 401) {
|
|
360
|
+
console.error(
|
|
361
|
+
`HTTP 401 on ${method} ${url} — SHIP_API_TOKEN is missing, expired, or lacks workspace access.`,
|
|
362
|
+
);
|
|
363
|
+
process.exit(2);
|
|
364
|
+
}
|
|
365
|
+
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
366
|
+
console.error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
|
|
367
|
+
process.exit(res.status >= 500 ? 3 : 1);
|
|
368
|
+
}
|