@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,390 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
findShipRoot,
|
|
6
|
+
readConfig,
|
|
7
|
+
writeConfig,
|
|
8
|
+
readState,
|
|
9
|
+
writeState,
|
|
10
|
+
} from "../config/io.mjs";
|
|
11
|
+
import { validateConfig } from "../config/schema.mjs";
|
|
12
|
+
import {
|
|
13
|
+
outboxPath,
|
|
14
|
+
listEvents,
|
|
15
|
+
countEvents,
|
|
16
|
+
clearEvents,
|
|
17
|
+
writeAllEvents,
|
|
18
|
+
ALLOWED_EVENT_TYPES,
|
|
19
|
+
} from "../telemetry/outbox.mjs";
|
|
20
|
+
import {
|
|
21
|
+
postTelemetry,
|
|
22
|
+
exportTelemetry,
|
|
23
|
+
deleteTelemetry,
|
|
24
|
+
} from "../http.mjs";
|
|
25
|
+
|
|
26
|
+
const KNOWN_SCOPES = ["artifact_usage", "improvement_drafts", "errors"];
|
|
27
|
+
|
|
28
|
+
function parseArgs(rest) {
|
|
29
|
+
const out = {
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
yes: false,
|
|
32
|
+
dryRun: false,
|
|
33
|
+
out: null,
|
|
34
|
+
scope: null,
|
|
35
|
+
limit: null,
|
|
36
|
+
positional: [],
|
|
37
|
+
};
|
|
38
|
+
const copy = [...rest];
|
|
39
|
+
while (copy.length) {
|
|
40
|
+
const a = copy[0];
|
|
41
|
+
if (a === "--cwd" && copy[1]) {
|
|
42
|
+
copy.shift();
|
|
43
|
+
out.cwd = String(copy.shift());
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (a.startsWith("--cwd=")) {
|
|
47
|
+
out.cwd = a.slice("--cwd=".length);
|
|
48
|
+
copy.shift();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (a === "--yes" || a === "-y") {
|
|
52
|
+
out.yes = true;
|
|
53
|
+
copy.shift();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (a === "--dry-run") {
|
|
57
|
+
out.dryRun = true;
|
|
58
|
+
copy.shift();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (a === "--out" && copy[1]) {
|
|
62
|
+
copy.shift();
|
|
63
|
+
out.out = String(copy.shift());
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a.startsWith("--out=")) {
|
|
67
|
+
out.out = a.slice("--out=".length);
|
|
68
|
+
copy.shift();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (a === "--scope" && copy[1]) {
|
|
72
|
+
copy.shift();
|
|
73
|
+
out.scope = String(copy.shift());
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (a.startsWith("--scope=")) {
|
|
77
|
+
out.scope = a.slice("--scope=".length);
|
|
78
|
+
copy.shift();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (a === "--limit" && copy[1]) {
|
|
82
|
+
copy.shift();
|
|
83
|
+
out.limit = parseInt(copy.shift(), 10);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (a.startsWith("--limit=")) {
|
|
87
|
+
out.limit = parseInt(a.slice("--limit=".length), 10);
|
|
88
|
+
copy.shift();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
out.positional.push(copy.shift());
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requireShipRoot(cwd) {
|
|
97
|
+
const root = findShipRoot(cwd);
|
|
98
|
+
if (!root) {
|
|
99
|
+
console.error(".ship/ not found. Run 'shipctl config init' first.");
|
|
100
|
+
process.exit(10);
|
|
101
|
+
}
|
|
102
|
+
return root;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveBaseUrl(ctx, config) {
|
|
106
|
+
return (
|
|
107
|
+
ctx?.baseUrl ||
|
|
108
|
+
process.env.SHIP_API_BASE ||
|
|
109
|
+
config?.api?.base_url ||
|
|
110
|
+
"https://ship.elmundi.com"
|
|
111
|
+
).replace(/\/$/, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function promptConfirm(msg) {
|
|
115
|
+
if (!process.stdin.isTTY) return false;
|
|
116
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
117
|
+
try {
|
|
118
|
+
const answer = await new Promise((resolve) => rl.question(`${msg} [y/N] `, resolve));
|
|
119
|
+
return /^y(es)?$/i.test(String(answer || "").trim());
|
|
120
|
+
} finally {
|
|
121
|
+
rl.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function promptLine(msg) {
|
|
126
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
127
|
+
try {
|
|
128
|
+
return await new Promise((resolve) => rl.question(msg, resolve));
|
|
129
|
+
} finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function saveConfig(root, config) {
|
|
135
|
+
const { filePath } = readConfig(root);
|
|
136
|
+
const valid = validateConfig(config);
|
|
137
|
+
if (!valid.ok) {
|
|
138
|
+
for (const e of valid.errors) console.error(e);
|
|
139
|
+
process.exit(10);
|
|
140
|
+
}
|
|
141
|
+
writeConfig(filePath, config);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function printStatus(root) {
|
|
145
|
+
const { config } = readConfig(root);
|
|
146
|
+
const { state } = readState(root);
|
|
147
|
+
const share = config.telemetry?.share === true;
|
|
148
|
+
const id = config.telemetry?.anonymous_id || "(none)";
|
|
149
|
+
const scope = config.telemetry?.scope || {};
|
|
150
|
+
const scopeStr = KNOWN_SCOPES.map((k) => `${k}=${scope[k] === true}`).join(",");
|
|
151
|
+
const pending = countEvents(root);
|
|
152
|
+
const last = state.last_flush_at || "never";
|
|
153
|
+
console.log(`share=${share}`);
|
|
154
|
+
console.log(`anonymous_id=${id}`);
|
|
155
|
+
console.log(`scope=${scopeStr}`);
|
|
156
|
+
console.log(`outbox_pending=${pending}`);
|
|
157
|
+
console.log(`last_flush_at=${last}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cmdOn(root, args) {
|
|
161
|
+
const { config } = readConfig(root);
|
|
162
|
+
config.telemetry = config.telemetry || {};
|
|
163
|
+
config.telemetry.scope = config.telemetry.scope || {
|
|
164
|
+
artifact_usage: true,
|
|
165
|
+
improvement_drafts: true,
|
|
166
|
+
errors: false,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (args.scope) {
|
|
170
|
+
const parts = args.scope.split(",").map((s) => s.trim()).filter(Boolean);
|
|
171
|
+
for (const p of parts) {
|
|
172
|
+
if (!KNOWN_SCOPES.includes(p)) {
|
|
173
|
+
console.error(`unknown scope: ${p}; allowed=${KNOWN_SCOPES.join(",")}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const p of parts) config.telemetry.scope[p] = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!args.yes) {
|
|
181
|
+
const ok = await promptConfirm(
|
|
182
|
+
"Enable anonymous telemetry (artifact usage + feedback metadata)?",
|
|
183
|
+
);
|
|
184
|
+
if (!ok) {
|
|
185
|
+
console.error("aborted.");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
config.telemetry.share = true;
|
|
190
|
+
if (!config.telemetry.anonymous_id) {
|
|
191
|
+
config.telemetry.anonymous_id = randomUUID();
|
|
192
|
+
}
|
|
193
|
+
saveConfig(root, config);
|
|
194
|
+
console.log(
|
|
195
|
+
`telemetry.share=true anonymous_id=${config.telemetry.anonymous_id} scope=${KNOWN_SCOPES.map(
|
|
196
|
+
(k) => `${k}=${config.telemetry.scope[k] === true}`,
|
|
197
|
+
).join(",")}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cmdOff(root) {
|
|
202
|
+
const { config } = readConfig(root);
|
|
203
|
+
config.telemetry = config.telemetry || {};
|
|
204
|
+
config.telemetry.share = false;
|
|
205
|
+
config.telemetry.scope = {
|
|
206
|
+
artifact_usage: false,
|
|
207
|
+
improvement_drafts: false,
|
|
208
|
+
errors: false,
|
|
209
|
+
};
|
|
210
|
+
saveConfig(root, config);
|
|
211
|
+
console.log("telemetry.share=false");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cmdShowId(root) {
|
|
215
|
+
const { config } = readConfig(root);
|
|
216
|
+
const id = config.telemetry?.anonymous_id;
|
|
217
|
+
if (!id) {
|
|
218
|
+
console.error("no anonymous_id set. Run 'shipctl telemetry on' to generate one.");
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
console.log(id);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cmdResetId(root) {
|
|
225
|
+
const { config } = readConfig(root);
|
|
226
|
+
config.telemetry = config.telemetry || {};
|
|
227
|
+
config.telemetry.anonymous_id = randomUUID();
|
|
228
|
+
saveConfig(root, config);
|
|
229
|
+
console.log(config.telemetry.anonymous_id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function cmdFlush(ctx, root, args) {
|
|
233
|
+
const { config } = readConfig(root);
|
|
234
|
+
if (config.telemetry?.share !== true) {
|
|
235
|
+
console.log("telemetry disabled; nothing to send");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const events = listEvents(root);
|
|
239
|
+
if (events.length === 0) {
|
|
240
|
+
console.log("flushed 0 events, 0 failed");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (args.dryRun) {
|
|
244
|
+
console.log(`would flush ${events.length} events to ${resolveBaseUrl(ctx, config)}/telemetry`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const baseUrl = resolveBaseUrl(ctx, config);
|
|
249
|
+
let flushed = 0;
|
|
250
|
+
let failed = 0;
|
|
251
|
+
const pending = [];
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < events.length; i += 100) {
|
|
254
|
+
const batch = events.slice(i, i + 100);
|
|
255
|
+
const stripped = batch.map((ev) => ({
|
|
256
|
+
type: ev.type,
|
|
257
|
+
anonymous_id: ev.anonymous_id || config.telemetry.anonymous_id,
|
|
258
|
+
timestamp: ev.timestamp,
|
|
259
|
+
payload: ev.payload || {},
|
|
260
|
+
}));
|
|
261
|
+
const res = await postTelemetry(baseUrl, stripped);
|
|
262
|
+
if (res.ok) {
|
|
263
|
+
flushed += batch.length;
|
|
264
|
+
} else {
|
|
265
|
+
failed += batch.length;
|
|
266
|
+
pending.push(...batch);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
writeAllEvents(root, pending);
|
|
271
|
+
|
|
272
|
+
if (flushed > 0) {
|
|
273
|
+
try {
|
|
274
|
+
const { state } = readState(root);
|
|
275
|
+
writeState(root, { ...state, last_flush_at: new Date().toISOString() });
|
|
276
|
+
} catch {
|
|
277
|
+
// non-fatal
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`flushed ${flushed} events, ${failed} failed`);
|
|
282
|
+
if (failed > 0) process.exit(20);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function cmdExport(ctx, root, args) {
|
|
286
|
+
const { config } = readConfig(root);
|
|
287
|
+
const id = config.telemetry?.anonymous_id;
|
|
288
|
+
if (!id) {
|
|
289
|
+
console.error("no anonymous_id set.");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const baseUrl = resolveBaseUrl(ctx, config);
|
|
293
|
+
const data = await exportTelemetry(baseUrl, id);
|
|
294
|
+
const json = JSON.stringify(data, null, 2);
|
|
295
|
+
if (args.out) {
|
|
296
|
+
fs.writeFileSync(args.out, json + "\n", "utf8");
|
|
297
|
+
console.log(`wrote ${args.out}`);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(json);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function cmdDeleteMyData(ctx, root) {
|
|
304
|
+
const { config } = readConfig(root);
|
|
305
|
+
const id = config.telemetry?.anonymous_id;
|
|
306
|
+
if (!id) {
|
|
307
|
+
console.error("no anonymous_id set; nothing to delete.");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
if (!process.stdin.isTTY) {
|
|
311
|
+
console.error("delete-my-data requires an interactive terminal.");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
const typed = (
|
|
315
|
+
await promptLine(`Are you sure? Type the anonymous_id to confirm: `)
|
|
316
|
+
).trim();
|
|
317
|
+
if (typed !== id) {
|
|
318
|
+
console.error("mismatch; aborted.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
const baseUrl = resolveBaseUrl(ctx, config);
|
|
322
|
+
const data = await deleteTelemetry(baseUrl, id);
|
|
323
|
+
console.log(JSON.stringify({ deleted: data?.deleted ?? 0 }));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function summarizePayload(payload) {
|
|
327
|
+
if (!payload || typeof payload !== "object") return "";
|
|
328
|
+
const parts = [];
|
|
329
|
+
for (const k of ["kind", "id", "version", "agent", "source", "updates_count", "failures_count"]) {
|
|
330
|
+
if (payload[k] !== undefined) parts.push(`${k}=${JSON.stringify(payload[k])}`);
|
|
331
|
+
}
|
|
332
|
+
if (parts.length === 0) {
|
|
333
|
+
const first = Object.keys(payload).slice(0, 3);
|
|
334
|
+
for (const k of first) parts.push(`${k}=${JSON.stringify(payload[k])}`);
|
|
335
|
+
}
|
|
336
|
+
return parts.join(" ");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function cmdBuffer(root, args) {
|
|
340
|
+
const limit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 20;
|
|
341
|
+
const events = listEvents(root);
|
|
342
|
+
const tail = events.slice(-limit);
|
|
343
|
+
for (const ev of tail) {
|
|
344
|
+
console.log(
|
|
345
|
+
`${ev.timestamp || "-"} ${ev.type || "-"} ${summarizePayload(ev.payload)}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
console.log(`(${tail.length}/${events.length} shown; outbox=${outboxPath(root)})`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function telemetryCommand(ctx, rest) {
|
|
352
|
+
const args = parseArgs(rest);
|
|
353
|
+
const sub = args.positional[0];
|
|
354
|
+
const root = requireShipRoot(args.cwd);
|
|
355
|
+
|
|
356
|
+
switch (sub) {
|
|
357
|
+
case "status":
|
|
358
|
+
printStatus(root);
|
|
359
|
+
return;
|
|
360
|
+
case "on":
|
|
361
|
+
await cmdOn(root, args);
|
|
362
|
+
return;
|
|
363
|
+
case "off":
|
|
364
|
+
cmdOff(root);
|
|
365
|
+
return;
|
|
366
|
+
case "show-id":
|
|
367
|
+
cmdShowId(root);
|
|
368
|
+
return;
|
|
369
|
+
case "reset-id":
|
|
370
|
+
cmdResetId(root);
|
|
371
|
+
return;
|
|
372
|
+
case "flush":
|
|
373
|
+
await cmdFlush(ctx, root, args);
|
|
374
|
+
return;
|
|
375
|
+
case "export":
|
|
376
|
+
await cmdExport(ctx, root, args);
|
|
377
|
+
return;
|
|
378
|
+
case "delete-my-data":
|
|
379
|
+
await cmdDeleteMyData(ctx, root);
|
|
380
|
+
return;
|
|
381
|
+
case "buffer":
|
|
382
|
+
cmdBuffer(root, args);
|
|
383
|
+
return;
|
|
384
|
+
default:
|
|
385
|
+
console.error(
|
|
386
|
+
`usage: shipctl telemetry <status|on|off|show-id|reset-id|flush|export|delete-my-data|buffer>\nallowed event types: ${ALLOWED_EVENT_TYPES.join(", ")}`,
|
|
387
|
+
);
|
|
388
|
+
process.exit(2);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readConfig } from "../config/io.mjs";
|
|
2
|
+
|
|
3
|
+
const VERSION = "v1";
|
|
4
|
+
|
|
5
|
+
export async function triggerCommand(ctx, rest) {
|
|
6
|
+
const opts = parseArgs(rest);
|
|
7
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl || ctx.baseUrl);
|
|
8
|
+
const token = requireToken();
|
|
9
|
+
let workspaceId = opts.workspace;
|
|
10
|
+
if (!workspaceId) workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
11
|
+
const repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
12
|
+
const { config } = readConfig(opts.cwd || process.cwd());
|
|
13
|
+
|
|
14
|
+
const result = await apiPostJson(
|
|
15
|
+
baseUrl,
|
|
16
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/trigger`,
|
|
17
|
+
{
|
|
18
|
+
event: opts.event,
|
|
19
|
+
config,
|
|
20
|
+
github: {
|
|
21
|
+
event_name: process.env.SHIP_EVENT_NAME || process.env.GITHUB_EVENT_NAME || "",
|
|
22
|
+
ref: process.env.SHIP_REF || process.env.GITHUB_REF || "",
|
|
23
|
+
sha: process.env.SHIP_SHA || process.env.GITHUB_SHA || "",
|
|
24
|
+
run_id: process.env.GITHUB_RUN_ID || "",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
token,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (ctx.json || opts.json) {
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const due = Array.isArray(result.due_lanes) ? result.due_lanes : [];
|
|
35
|
+
if (!due.length) {
|
|
36
|
+
console.log(`Ship trigger ${opts.event}: no lanes due.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(`Ship trigger ${opts.event}: ${due.length} lane(s) due`);
|
|
40
|
+
for (const lane of due) console.log(` - ${lane.lane_id}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`shipctl trigger — ask Ship which lanes are due (${VERSION})
|
|
45
|
+
|
|
46
|
+
USAGE
|
|
47
|
+
shipctl trigger --event schedule --repo <id|owner/name> [--workspace <id>] [--json]
|
|
48
|
+
|
|
49
|
+
ENV
|
|
50
|
+
SHIP_API_TOKEN Required.
|
|
51
|
+
SHIP_WORKSPACE_API_BASE Optional API base override.
|
|
52
|
+
SHIP_API_BASE Fallback API base override.
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(args) {
|
|
57
|
+
const out = {
|
|
58
|
+
event: null,
|
|
59
|
+
workspace: null,
|
|
60
|
+
repo: null,
|
|
61
|
+
baseUrl: null,
|
|
62
|
+
cwd: null,
|
|
63
|
+
json: false,
|
|
64
|
+
};
|
|
65
|
+
const copy = [...args];
|
|
66
|
+
const consume = (flag, key) => {
|
|
67
|
+
if (copy[0] === flag && copy[1] !== undefined) {
|
|
68
|
+
copy.shift();
|
|
69
|
+
out[key] = String(copy.shift());
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const p = `${flag}=`;
|
|
73
|
+
if (copy[0] && copy[0].startsWith(p)) {
|
|
74
|
+
out[key] = copy[0].slice(p.length);
|
|
75
|
+
copy.shift();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
while (copy.length) {
|
|
81
|
+
if (
|
|
82
|
+
consume("--event", "event") ||
|
|
83
|
+
consume("--workspace", "workspace") ||
|
|
84
|
+
consume("--repo", "repo") ||
|
|
85
|
+
consume("--base-url", "baseUrl") ||
|
|
86
|
+
consume("--cwd", "cwd")
|
|
87
|
+
) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (copy[0] === "--json") {
|
|
91
|
+
out.json = true;
|
|
92
|
+
copy.shift();
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
96
|
+
printHelp();
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
console.error(`Unknown flag: ${copy[0]}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
if (!out.event) {
|
|
103
|
+
console.error("Usage: shipctl trigger --event <schedule> --repo <id|owner/name>");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (!["schedule", "manual", "pull_request", "push"].includes(out.event)) {
|
|
107
|
+
console.error("--event must be one of: schedule, manual, pull_request, push");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireToken() {
|
|
114
|
+
const token = process.env.SHIP_API_TOKEN || "";
|
|
115
|
+
if (!token) {
|
|
116
|
+
console.error("SHIP_API_TOKEN is required.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
return token;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveBaseUrl(explicit) {
|
|
123
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
124
|
+
if (process.env.SHIP_WORKSPACE_API_BASE) return process.env.SHIP_WORKSPACE_API_BASE.replace(/\/+$/, "");
|
|
125
|
+
if (process.env.SHIP_API_BASE) return process.env.SHIP_API_BASE.replace(/\/+$/, "");
|
|
126
|
+
return "https://api.ship.elmundi.com";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resolveSoleWorkspace(baseUrl, token) {
|
|
130
|
+
const rows = await apiGetJson(baseUrl, "/v1/workspaces", token);
|
|
131
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
132
|
+
console.error("No workspaces visible to this token.");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (rows.length > 1) {
|
|
136
|
+
console.error("Token has access to more than one workspace; pass --workspace <id>.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
return String(rows[0].id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function resolveRepoId(baseUrl, token, workspaceId, hint) {
|
|
143
|
+
if (hint && /^[0-9a-fA-F-]{32,36}$/.test(hint) && hint.includes("-")) return hint;
|
|
144
|
+
const rows = await apiGetJson(baseUrl, `/v1/workspaces/${encodeURIComponent(workspaceId)}/repos`, token);
|
|
145
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
146
|
+
console.error(`Workspace ${workspaceId} has no activated repos.`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (hint) {
|
|
150
|
+
const match = rows.find((r) => r.full_name === hint || `${r.owner ?? ""}/${r.name ?? ""}` === hint || r.id === hint);
|
|
151
|
+
if (!match) {
|
|
152
|
+
console.error(`--repo ${hint} doesn't match any activated repo in workspace ${workspaceId}.`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
return String(match.id);
|
|
156
|
+
}
|
|
157
|
+
return String(rows[0].id);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function apiGetJson(baseUrl, path, token) {
|
|
161
|
+
return apiRequest(baseUrl, path, "GET", token, null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function apiPostJson(baseUrl, path, body, token) {
|
|
165
|
+
return apiRequest(baseUrl, path, "POST", token, body);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function apiRequest(baseUrl, path, method, token, body) {
|
|
169
|
+
const url = `${baseUrl}${path}`;
|
|
170
|
+
let res;
|
|
171
|
+
try {
|
|
172
|
+
res = await fetch(url, {
|
|
173
|
+
method,
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
Authorization: `Bearer ${token}`,
|
|
178
|
+
},
|
|
179
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`Network error calling ${url}: ${err instanceof Error ? err.message : err}`);
|
|
183
|
+
process.exit(3);
|
|
184
|
+
}
|
|
185
|
+
const text = await res.text();
|
|
186
|
+
let data = null;
|
|
187
|
+
try {
|
|
188
|
+
data = text ? JSON.parse(text) : null;
|
|
189
|
+
} catch {
|
|
190
|
+
data = text;
|
|
191
|
+
}
|
|
192
|
+
if (res.ok) return data;
|
|
193
|
+
const msg = typeof data === "string" ? data : JSON.stringify(data);
|
|
194
|
+
console.error(`HTTP ${res.status} ${res.statusText} on ${method} ${url}\n${msg}`);
|
|
195
|
+
process.exit(res.status >= 500 ? 3 : 1);
|
|
196
|
+
}
|