@elmundi/ship-cli 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -22
- package/bin/shipctl.mjs +10 -7
- package/lib/bootstrap/render.mjs +49 -0
- package/lib/commands/callback.mjs +457 -17
- package/lib/commands/config.mjs +1 -1
- package/lib/commands/docs.mjs +4 -4
- package/lib/commands/help.mjs +137 -77
- package/lib/commands/kickoff.mjs +3 -3
- package/lib/commands/knowledge.mjs +211 -17
- package/lib/commands/lanes.mjs +36 -11
- package/lib/commands/manifest-catalog.mjs +5 -5
- package/lib/commands/patterns.mjs +5 -5
- package/lib/commands/run.mjs +329 -89
- package/lib/commands/search.mjs +2 -2
- package/lib/commands/sync.mjs +83 -8
- package/lib/commands/trigger.mjs +196 -0
- package/lib/config/migrate.mjs +13 -5
- package/lib/config/schema.mjs +253 -2
- package/lib/state/idempotency.mjs +1 -1
- package/lib/templates.mjs +3 -3
- package/package.json +2 -2
package/lib/commands/sync.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
writeState,
|
|
8
8
|
findShipRoot,
|
|
9
9
|
} from "../config/io.mjs";
|
|
10
|
-
import { validateConfig } from "../config/schema.mjs";
|
|
10
|
+
import { validateConfig, lanePatterns as lanePatternList } from "../config/schema.mjs";
|
|
11
11
|
import { fetchManifest, fetchArtifact } from "../http.mjs";
|
|
12
12
|
import {
|
|
13
13
|
readCached,
|
|
@@ -26,6 +26,52 @@ import {
|
|
|
26
26
|
} from "../state/lockfile.mjs";
|
|
27
27
|
import { getCliVersion } from "../version.mjs";
|
|
28
28
|
|
|
29
|
+
function printSyncHelp() {
|
|
30
|
+
console.log(`shipctl sync — fetch the catalog into .ship/cache (and optionally lock).
|
|
31
|
+
|
|
32
|
+
WHAT THIS COMMAND DOES
|
|
33
|
+
Pulls the artifacts your repo declares — pins, the active preset,
|
|
34
|
+
per-agent rule collections, and any pattern referenced by your lanes
|
|
35
|
+
(Automations in the operator console) — from the methodology API
|
|
36
|
+
into .ship/cache/<kind>/<id>@<version>/. Verifies content_sha256,
|
|
37
|
+
writes meta, optionally produces a lockfile so 'shipctl run --offline'
|
|
38
|
+
is reproducible.
|
|
39
|
+
|
|
40
|
+
USAGE
|
|
41
|
+
shipctl sync [--check-only] [--only <kind:id>]... [--channel <c>]
|
|
42
|
+
[--force-unpin] [--dry-run] [--lock] [--json] [--cwd <dir>]
|
|
43
|
+
|
|
44
|
+
FLAGS
|
|
45
|
+
--check-only Report what would change; do not write to disk.
|
|
46
|
+
--only <kind:id> Restrict to one or more artifacts (repeatable).
|
|
47
|
+
Example: --only pattern:role-developer --only collection:preset-web-app
|
|
48
|
+
--channel <c> Override config.api.channel for this invocation
|
|
49
|
+
(stable|edge).
|
|
50
|
+
--force-unpin Ignore artifacts.pins[] and pull the manifest
|
|
51
|
+
version. Use when intentionally bumping a pin.
|
|
52
|
+
--dry-run Print the resolution plan; do not write or fetch.
|
|
53
|
+
--lock After sync, materialise every pattern referenced
|
|
54
|
+
by the declared lanes and write
|
|
55
|
+
.ship/shipctl.lock.json (used by
|
|
56
|
+
'shipctl run --offline').
|
|
57
|
+
--json Emit a structured JSON summary on stdout.
|
|
58
|
+
--cwd <dir> Repo root. Defaults to the current directory;
|
|
59
|
+
searches upward for .ship/.
|
|
60
|
+
--help, -h Show this help.
|
|
61
|
+
|
|
62
|
+
EXAMPLES
|
|
63
|
+
shipctl sync # baseline pull
|
|
64
|
+
shipctl sync --check-only --json # CI guard
|
|
65
|
+
shipctl sync --only pattern:role-developer --only tool:methodology-api
|
|
66
|
+
shipctl sync --lock # produce a reproducible lockfile
|
|
67
|
+
|
|
68
|
+
EXIT CODE
|
|
69
|
+
0 when everything resolved.
|
|
70
|
+
20 when at least one artifact failed to fetch (or --lock left
|
|
71
|
+
unresolved entries).
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
function parseSyncArgs(rest) {
|
|
30
76
|
const out = {
|
|
31
77
|
cwd: process.cwd(),
|
|
@@ -36,10 +82,17 @@ function parseSyncArgs(rest) {
|
|
|
36
82
|
only: [],
|
|
37
83
|
lock: false,
|
|
38
84
|
json: false,
|
|
85
|
+
help: false,
|
|
86
|
+
unknown: [],
|
|
39
87
|
};
|
|
40
88
|
const copy = [...rest];
|
|
41
89
|
while (copy.length) {
|
|
42
90
|
const a = copy[0];
|
|
91
|
+
if (a === "--help" || a === "-h") {
|
|
92
|
+
out.help = true;
|
|
93
|
+
copy.shift();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
43
96
|
if (a === "--check-only") {
|
|
44
97
|
out.checkOnly = true;
|
|
45
98
|
copy.shift();
|
|
@@ -95,6 +148,12 @@ function parseSyncArgs(rest) {
|
|
|
95
148
|
copy.shift();
|
|
96
149
|
continue;
|
|
97
150
|
}
|
|
151
|
+
/* Previously we silently dropped unrecognised tokens here. That
|
|
152
|
+
* hid bashisms like a misspelt `--cheek-only`, so we now collect
|
|
153
|
+
* them and warn from `syncCommand` once parsing is complete.
|
|
154
|
+
* Stays non-fatal because existing CI scripts may rely on the old
|
|
155
|
+
* permissive behaviour. */
|
|
156
|
+
out.unknown.push(a);
|
|
98
157
|
copy.shift();
|
|
99
158
|
}
|
|
100
159
|
return out;
|
|
@@ -457,19 +516,26 @@ export async function buildLockfile({ shipRoot, config, baseUrl, channel, verbos
|
|
|
457
516
|
const notes = [];
|
|
458
517
|
|
|
459
518
|
const pins = config.artifacts?.pins || {};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
519
|
+
/* Flatten each lane into one (laneId, patternId) row per pattern so
|
|
520
|
+
* lanes that declare ``patterns: [a, b]`` (RFC-0008 C3.1) feed both
|
|
521
|
+
* into the sync/lockfile pipeline. Legacy ``pattern: <id>`` lanes
|
|
522
|
+
* normalise to a single-element list via lanePatternList(). */
|
|
523
|
+
const laneRows = [];
|
|
524
|
+
for (const [laneId, lane] of Object.entries(config.lanes || {})) {
|
|
525
|
+
for (const pid of lanePatternList(lane)) {
|
|
526
|
+
laneRows.push({ laneId, patternId: pid });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const pinRows = Object.keys(pins)
|
|
464
530
|
.filter((k) => k.startsWith("pattern/"))
|
|
465
|
-
.map((k) => ({ laneId: null,
|
|
531
|
+
.map((k) => ({ laneId: null, patternId: k.slice("pattern/".length) }));
|
|
466
532
|
|
|
467
533
|
/* De-duplicate on pattern id while preserving lane provenance (useful
|
|
468
534
|
* for the `notes` field — operators want to know which lane pinned a
|
|
469
535
|
* given pattern when they read the diff). */
|
|
470
536
|
const seen = new Map();
|
|
471
|
-
for (const row of [...
|
|
472
|
-
const pid = row.
|
|
537
|
+
for (const row of [...laneRows, ...pinRows]) {
|
|
538
|
+
const pid = row.patternId;
|
|
473
539
|
if (!seen.has(pid)) seen.set(pid, { id: pid, by: [] });
|
|
474
540
|
seen.get(pid).by.push(row.laneId || "config.artifacts.pins");
|
|
475
541
|
}
|
|
@@ -637,8 +703,17 @@ function cmpSemver(a, b) {
|
|
|
637
703
|
|
|
638
704
|
export async function syncCommand(ctx, rest) {
|
|
639
705
|
const args = parseSyncArgs(rest);
|
|
706
|
+
if (args.help) {
|
|
707
|
+
printSyncHelp();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
640
710
|
if (ctx?.dryRun) args.dryRun = true;
|
|
641
711
|
if (ctx?.json) args.json = true;
|
|
712
|
+
for (const tok of args.unknown) {
|
|
713
|
+
console.warn(
|
|
714
|
+
`warn: shipctl sync: ignoring unknown argument '${tok}'. Run 'shipctl sync --help'.`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
642
717
|
|
|
643
718
|
let result;
|
|
644
719
|
try {
|
|
@@ -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
|
+
}
|
package/lib/config/migrate.mjs
CHANGED
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
* the input untouched.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
CONFIG_SCHEMA_VERSION,
|
|
24
|
+
DEFAULT_PROCESS_CONFIG,
|
|
25
|
+
LEGACY_CONFIG_SCHEMA_VERSION,
|
|
26
|
+
} from "./schema.mjs";
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Default lane translations for the well-known v1 `lanes:` list entries.
|
|
@@ -34,23 +38,23 @@ import { CONFIG_SCHEMA_VERSION, LEGACY_CONFIG_SCHEMA_VERSION } from "./schema.mj
|
|
|
34
38
|
const V1_LANE_DEFAULTS = Object.freeze({
|
|
35
39
|
pr_review: {
|
|
36
40
|
kind: "event",
|
|
37
|
-
pattern: "
|
|
41
|
+
pattern: "flow-pr-self-review",
|
|
38
42
|
on: "pull_request",
|
|
39
43
|
permissions: { contents: "read", "pull-requests": "write" },
|
|
40
44
|
},
|
|
41
45
|
daily_standup: {
|
|
42
46
|
kind: "schedule",
|
|
43
|
-
pattern: "
|
|
47
|
+
pattern: "flow-daily-retro",
|
|
44
48
|
cron: "0 9 * * 1-5",
|
|
45
49
|
},
|
|
46
50
|
tech_debt: {
|
|
47
51
|
kind: "schedule",
|
|
48
|
-
pattern: "
|
|
52
|
+
pattern: "flow-learning-capture",
|
|
49
53
|
cron: "0 10 * * 1",
|
|
50
54
|
},
|
|
51
55
|
self_heal: {
|
|
52
56
|
kind: "event",
|
|
53
|
-
pattern: "
|
|
57
|
+
pattern: "op-workflow-self-heal",
|
|
54
58
|
on: "workflow_run",
|
|
55
59
|
when: { conclusion: "failure" },
|
|
56
60
|
permissions: { contents: "read", actions: "read", "pull-requests": "write" },
|
|
@@ -132,6 +136,10 @@ export function migrateV1ToV2(input) {
|
|
|
132
136
|
out.agent.default.provider = liftedProvider;
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
if (!out.process || typeof out.process !== "object" || Array.isArray(out.process)) {
|
|
140
|
+
out.process = cloneDefault(DEFAULT_PROCESS_CONFIG());
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
/* lanes: translate from the legacy list-of-strings shape. */
|
|
136
144
|
out.lanes = {};
|
|
137
145
|
const srcLanes = src.lanes;
|