@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,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,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readConfig, findShipRoot } from "../config/io.mjs";
|
|
4
|
+
import { allChecks, runChecks, summarize } from "../verify/registry.mjs";
|
|
5
|
+
|
|
6
|
+
const SEVERITY_ORDER = { info: 0, warn: 1, error: 2 };
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const out = {
|
|
10
|
+
cwd: null,
|
|
11
|
+
check: /** @type {string[]} */ ([]),
|
|
12
|
+
json: false,
|
|
13
|
+
noNetwork: false,
|
|
14
|
+
severity: "info",
|
|
15
|
+
help: false,
|
|
16
|
+
};
|
|
17
|
+
const copy = [...argv];
|
|
18
|
+
while (copy.length) {
|
|
19
|
+
const a = copy.shift();
|
|
20
|
+
if (a === "--help" || a === "-h") { out.help = true; continue; }
|
|
21
|
+
if (a === "--json") { out.json = true; continue; }
|
|
22
|
+
if (a === "--no-network") { out.noNetwork = true; continue; }
|
|
23
|
+
if (a === "--cwd" && copy.length) { out.cwd = copy.shift(); continue; }
|
|
24
|
+
if (a && a.startsWith("--cwd=")) { out.cwd = a.slice("--cwd=".length); continue; }
|
|
25
|
+
if (a === "--check" && copy.length) {
|
|
26
|
+
for (const s of String(copy.shift()).split(",")) {
|
|
27
|
+
const id = s.trim();
|
|
28
|
+
if (id) out.check.push(id);
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (a && a.startsWith("--check=")) {
|
|
33
|
+
for (const s of a.slice("--check=".length).split(",")) {
|
|
34
|
+
const id = s.trim();
|
|
35
|
+
if (id) out.check.push(id);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (a === "--severity" && copy.length) { out.severity = copy.shift(); continue; }
|
|
40
|
+
if (a && a.startsWith("--severity=")) { out.severity = a.slice("--severity=".length); continue; }
|
|
41
|
+
// Silently ignore unknown flags so globals (--base-url, --json) don't blow up.
|
|
42
|
+
}
|
|
43
|
+
if (!["info", "warn", "error"].includes(out.severity)) {
|
|
44
|
+
throw new Error(`verify: unknown --severity '${out.severity}'. Expected: info|warn|error`);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printHelp() {
|
|
50
|
+
console.log(`shipctl verify — post-adoption liveness check for a Ship repo.
|
|
51
|
+
|
|
52
|
+
USAGE
|
|
53
|
+
shipctl verify [--cwd DIR] [--check <id,...>] [--no-network]
|
|
54
|
+
[--severity info|warn|error] [--json]
|
|
55
|
+
|
|
56
|
+
OPTIONS
|
|
57
|
+
--cwd DIR Target repo root (defaults to cwd / nearest .ship/).
|
|
58
|
+
--check <id,...> Run only the listed check ids (csv or repeated).
|
|
59
|
+
--no-network Skip checks in the 'network' category.
|
|
60
|
+
--severity <level> Filter displayed rows:
|
|
61
|
+
info (default) — show all checks (pass/warn/fail/skip)
|
|
62
|
+
warn — show warn + fail only
|
|
63
|
+
error — show fail only
|
|
64
|
+
--json Machine-readable output: {checks:[...], summary:{...}}.
|
|
65
|
+
|
|
66
|
+
EXIT CODE
|
|
67
|
+
0 when no checks returned 'fail' (warnings do not fail).
|
|
68
|
+
1 when at least one check failed.
|
|
69
|
+
|
|
70
|
+
AVAILABLE CHECKS
|
|
71
|
+
${allChecks().map((c) => ` ${c.id.padEnd(22)} ${c.description}`).join("\n")}
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadConfig(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const { config } = readConfig(cwd);
|
|
78
|
+
return config;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadInventory(cwd) {
|
|
85
|
+
const root = findShipRoot(cwd) || cwd;
|
|
86
|
+
const invPath = path.join(root, ".ship", "inventory.json");
|
|
87
|
+
if (!fs.existsSync(invPath)) return null;
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(invPath, "utf8"));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pickByStatus(rows, severity) {
|
|
96
|
+
if (severity === "info") return rows;
|
|
97
|
+
if (severity === "warn") return rows.filter((r) => r.status === "warn" || r.status === "fail");
|
|
98
|
+
return rows.filter((r) => r.status === "fail");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function badge(status) {
|
|
102
|
+
return `[${status}]`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pad(s, n) {
|
|
106
|
+
s = String(s);
|
|
107
|
+
if (s.length >= n) return s;
|
|
108
|
+
return s + " ".repeat(n - s.length);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {{json:boolean, yes:boolean, force:boolean, dryRun:boolean, baseUrl?:string}} ctx
|
|
113
|
+
* @param {string[]} args
|
|
114
|
+
*/
|
|
115
|
+
export async function verifyCommand(ctx, args) {
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = parseArgs(args);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(e.message);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (parsed.help) { printHelp(); return; }
|
|
125
|
+
if (ctx && ctx.json) parsed.json = true;
|
|
126
|
+
|
|
127
|
+
const rawCwd = parsed.cwd || process.cwd();
|
|
128
|
+
const resolvedRoot = findShipRoot(rawCwd) || path.resolve(rawCwd);
|
|
129
|
+
|
|
130
|
+
const config = loadConfig(resolvedRoot);
|
|
131
|
+
const inventory = loadInventory(resolvedRoot);
|
|
132
|
+
const baseUrl = (ctx && ctx.baseUrl)
|
|
133
|
+
|| (config && config.api && config.api.base_url)
|
|
134
|
+
|| process.env.SHIP_API_BASE
|
|
135
|
+
|| "https://ship.elmundi.com";
|
|
136
|
+
|
|
137
|
+
const checkCtx = {
|
|
138
|
+
cwd: resolvedRoot,
|
|
139
|
+
config,
|
|
140
|
+
inventory,
|
|
141
|
+
baseUrl,
|
|
142
|
+
logger: (msg) => { if (!parsed.json) process.stderr.write(`${msg}\n`); },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const rows = await runChecks(checkCtx, {
|
|
146
|
+
filter: parsed.check.length ? parsed.check : null,
|
|
147
|
+
noNetwork: parsed.noNetwork,
|
|
148
|
+
});
|
|
149
|
+
const summary = summarize(rows);
|
|
150
|
+
const exitCode = summary.fail > 0 ? 1 : 0;
|
|
151
|
+
|
|
152
|
+
if (parsed.json) {
|
|
153
|
+
process.stdout.write(
|
|
154
|
+
JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
cwd: resolvedRoot,
|
|
157
|
+
checks: rows,
|
|
158
|
+
summary,
|
|
159
|
+
exit_code: exitCode,
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
2,
|
|
163
|
+
) + "\n",
|
|
164
|
+
);
|
|
165
|
+
process.exit(exitCode);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const header = [`Ship verify — ${resolvedRoot}`, ""];
|
|
170
|
+
const visible = pickByStatus(rows, parsed.severity);
|
|
171
|
+
const idWidth = Math.max(
|
|
172
|
+
14,
|
|
173
|
+
...visible.map((r) => r.id.length),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const body = visible.map((r) => `${badge(r.status)} ${pad(r.id, idWidth)} ${r.detail}`);
|
|
177
|
+
const footer = [
|
|
178
|
+
"",
|
|
179
|
+
`${summary.total} check${summary.total === 1 ? "" : "s"} total: ${summary.pass} pass, ${summary.warn} warn, ${summary.fail} fail, ${summary.skip} skip`,
|
|
180
|
+
`Exit code: ${exitCode}${summary.fail ? " (any fail)" : ""}`,
|
|
181
|
+
];
|
|
182
|
+
if (!visible.length) {
|
|
183
|
+
body.push(`(no checks match --severity ${parsed.severity})`);
|
|
184
|
+
}
|
|
185
|
+
process.stdout.write(`${header.concat(body, footer).join("\n")}\n`);
|
|
186
|
+
process.exit(exitCode);
|
|
187
|
+
}
|