@elmundi/ship-cli 0.12.1 → 0.14.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 +70 -653
- package/bin/shipctl.mjs +9 -1
- package/lib/agents/cursor.mjs +143 -0
- package/lib/agents/index.mjs +51 -0
- package/lib/commands/help.mjs +31 -23
- package/lib/commands/knowledge.mjs +18 -7
- package/lib/commands/lanes.mjs +74 -35
- package/lib/commands/process.mjs +388 -0
- package/lib/commands/run.mjs +456 -763
- package/lib/commands/sync.mjs +1 -1
- package/lib/commands/trigger.mjs +73 -40
- package/lib/config/schema.mjs +196 -10
- package/lib/process/specialist-prompt-contract.mjs +171 -0
- package/lib/runtime/routines.mjs +264 -0
- package/lib/vendor/run-agent.workflow.yml +254 -0
- package/package.json +4 -4
package/bin/shipctl.mjs
CHANGED
|
@@ -140,13 +140,21 @@ try {
|
|
|
140
140
|
process.exit(0);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
if (cmd === "process") {
|
|
144
|
+
const { processCommand } = await import("../lib/commands/process.mjs");
|
|
145
|
+
await processCommand(ctx, rest);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
143
149
|
if (cmd === "migrate") {
|
|
144
150
|
const { migrateCommand } = await import("../lib/commands/migrate.mjs");
|
|
145
151
|
await migrateCommand(ctx, rest);
|
|
146
152
|
process.exit(0);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
if (cmd === "run") {
|
|
155
|
+
if (cmd === "run" || cmd === "agent-run") {
|
|
156
|
+
// ``agent-run`` is a back-compat alias for ``run`` — older trigger
|
|
157
|
+
// workflows still spell it that way until they re-seed.
|
|
150
158
|
const { runCommand } = await import("../lib/commands/run.mjs");
|
|
151
159
|
await runCommand(ctx, rest);
|
|
152
160
|
process.exit(0);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Cloud Agent runtime adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the public ``POST https://api.cursor.com/v0/agents`` API the
|
|
5
|
+
* ElMundi sibling repo uses (``tools/linear-agent/scripts/cloud-agent-launch.mjs``).
|
|
6
|
+
* The shape Ship needs:
|
|
7
|
+
*
|
|
8
|
+
* 1. Launch an agent against ``repo`` at ``ref`` with ``prompt``.
|
|
9
|
+
* 2. Poll until the agent's status is one of the terminal values
|
|
10
|
+
* (``FINISHED`` / ``ERRORED`` / ``CANCELLED``).
|
|
11
|
+
* 3. Return the branch name + final status to ``shipctl run``. Side
|
|
12
|
+
* effects (tracker writes / inbox rows) happen via the agent's
|
|
13
|
+
* own ``POST /agent-runs/finish`` call from inside Cursor — the
|
|
14
|
+
* CLI no longer reads a state file off the branch.
|
|
15
|
+
*
|
|
16
|
+
* Auth: ``CURSOR_API_KEY`` env var, sent as Basic ``<key>:`` per Cursor's
|
|
17
|
+
* docs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Buffer } from "node:buffer";
|
|
21
|
+
|
|
22
|
+
const BASE = "https://api.cursor.com";
|
|
23
|
+
const TERMINAL_STATUSES = new Set(["FINISHED", "ERRORED", "CANCELLED"]);
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
function authHeader() {
|
|
27
|
+
const key = (process.env.CURSOR_API_KEY || "").trim();
|
|
28
|
+
if (!key) {
|
|
29
|
+
throw new Error("CURSOR_API_KEY is not set");
|
|
30
|
+
}
|
|
31
|
+
return "Basic " + Buffer.from(`${key}:`).toString("base64");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async function postJson(path, body) {
|
|
36
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
Authorization: authHeader(),
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`Cursor API ${path} ${res.status}: ${text.slice(0, 500)}`);
|
|
47
|
+
}
|
|
48
|
+
return text ? JSON.parse(text) : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async function getJson(path) {
|
|
53
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
54
|
+
headers: { Authorization: authHeader() },
|
|
55
|
+
});
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`Cursor API ${path} ${res.status}: ${text.slice(0, 500)}`);
|
|
59
|
+
}
|
|
60
|
+
return text ? JSON.parse(text) : {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Launch a single Cursor agent and poll until it terminates.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} opts
|
|
68
|
+
* @param {string} opts.repoUrl e.g. ``https://github.com/owner/repo``
|
|
69
|
+
* @param {string} opts.ref branch the agent checks out (default ``main``)
|
|
70
|
+
* @param {string} opts.branchName branch the agent commits onto
|
|
71
|
+
* @param {string} opts.prompt full prompt body
|
|
72
|
+
* @param {boolean} [opts.autoCreatePr] open a PR when done (default false)
|
|
73
|
+
* @param {number} [opts.pollIntervalMs] poll cadence (default 15s)
|
|
74
|
+
* @param {number} [opts.timeoutMs] total deadline (default 30 min)
|
|
75
|
+
* @param {(line: string) => void} [opts.onLog] streaming log hook
|
|
76
|
+
* @returns {Promise<{ agentId: string, branchName: string, status: string, raw: object }>}
|
|
77
|
+
*/
|
|
78
|
+
export async function runCursorAgent({
|
|
79
|
+
repoUrl,
|
|
80
|
+
ref = "main",
|
|
81
|
+
branchName,
|
|
82
|
+
prompt,
|
|
83
|
+
autoCreatePr = false,
|
|
84
|
+
pollIntervalMs = 15_000,
|
|
85
|
+
timeoutMs = 30 * 60 * 1000,
|
|
86
|
+
onLog = (l) => console.error(`[cursor] ${l}`),
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (!repoUrl) throw new Error("runCursorAgent: repoUrl required");
|
|
89
|
+
if (!branchName) throw new Error("runCursorAgent: branchName required");
|
|
90
|
+
if (!prompt || typeof prompt !== "string") {
|
|
91
|
+
throw new Error("runCursorAgent: prompt required");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const launch = await postJson("/v0/agents", {
|
|
95
|
+
prompt: { text: prompt },
|
|
96
|
+
source: { repository: repoUrl, ref },
|
|
97
|
+
target: {
|
|
98
|
+
branchName,
|
|
99
|
+
autoCreatePr,
|
|
100
|
+
openAsCursorGithubApp: false,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const agentId = launch.id || launch.agentId || launch.agent_id;
|
|
104
|
+
if (!agentId) {
|
|
105
|
+
throw new Error(`Cursor launch returned no agent id: ${JSON.stringify(launch).slice(0, 300)}`);
|
|
106
|
+
}
|
|
107
|
+
onLog(`launched agent=${agentId} branch=${branchName} ref=${ref}`);
|
|
108
|
+
|
|
109
|
+
const deadline = Date.now() + timeoutMs;
|
|
110
|
+
let lastStatus = launch.status || "CREATING";
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
if (TERMINAL_STATUSES.has(String(lastStatus).toUpperCase())) {
|
|
113
|
+
onLog(`agent terminal: status=${lastStatus}`);
|
|
114
|
+
return { agentId, branchName, status: lastStatus, raw: launch };
|
|
115
|
+
}
|
|
116
|
+
await sleep(pollIntervalMs);
|
|
117
|
+
let snapshot;
|
|
118
|
+
try {
|
|
119
|
+
snapshot = await getJson(`/v0/agents/${encodeURIComponent(agentId)}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
onLog(`poll error (continuing): ${err.message}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const next = (snapshot.status || "").toString();
|
|
125
|
+
if (next && next !== lastStatus) {
|
|
126
|
+
onLog(`status: ${lastStatus} → ${next}`);
|
|
127
|
+
lastStatus = next;
|
|
128
|
+
}
|
|
129
|
+
if (TERMINAL_STATUSES.has(next.toUpperCase())) {
|
|
130
|
+
return { agentId, branchName, status: next, raw: snapshot };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Cursor agent ${agentId} did not reach a terminal state within ${Math.round(
|
|
135
|
+
timeoutMs / 1000,
|
|
136
|
+
)}s (last status: ${lastStatus})`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
function sleep(ms) {
|
|
142
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
143
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent runtime dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* E14 picks the runtime per ``.ship/config.yml`` ``agent.default.provider``
|
|
5
|
+
* (and per-routine override under ``agent.overrides.<routine>``). Today
|
|
6
|
+
* Cursor Cloud is the only one wired in; ``claude`` / ``codex`` slots
|
|
7
|
+
* exist so adding them later is a single new file plus an entry in
|
|
8
|
+
* ``RUNTIMES`` here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { runCursorAgent } from "./cursor.mjs";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const RUNTIMES = {
|
|
15
|
+
cursor: runCursorAgent,
|
|
16
|
+
// claude: runClaudeCodeAgent, // TODO when we add it
|
|
17
|
+
// codex: runCodexAgent, // TODO when we add it
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_PROVIDER = "cursor";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function resolveProvider(config, routineId) {
|
|
24
|
+
const override = config?.agent?.overrides?.[routineId]?.provider;
|
|
25
|
+
if (override) return String(override).toLowerCase();
|
|
26
|
+
const def = config?.agent?.default?.provider;
|
|
27
|
+
if (def) return String(def).toLowerCase();
|
|
28
|
+
return DEFAULT_PROVIDER;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run the configured agent runtime.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} provider — one of ``RUNTIMES``' keys.
|
|
36
|
+
* @param {object} opts — passed straight through to the runtime.
|
|
37
|
+
* @returns {Promise<{ agentId: string, branchName: string, status: string, raw: object }>}
|
|
38
|
+
*/
|
|
39
|
+
export async function runAgent(provider, opts) {
|
|
40
|
+
const fn = RUNTIMES[provider];
|
|
41
|
+
if (!fn) {
|
|
42
|
+
const known = Object.keys(RUNTIMES).join(", ") || "(none)";
|
|
43
|
+
throw new Error(
|
|
44
|
+
`agent runtime '${provider}' is not wired in this build. Known: ${known}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return fn(opts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export const SUPPORTED_PROVIDERS = Object.freeze(Object.keys(RUNTIMES));
|
package/lib/commands/help.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
export function printHelp() {
|
|
2
|
-
console.log(`shipctl — adopt Ship in a repo, sync the catalog, run
|
|
2
|
+
console.log(`shipctl — adopt Ship in a repo, sync the catalog, run routines, report outcomes.
|
|
3
3
|
|
|
4
4
|
Bootstrap a new or existing repo (init / new / doctor), pull the
|
|
5
|
-
methodology catalog into .ship/cache (sync), execute one-shot
|
|
6
|
-
emit prompts for the workspace runner (
|
|
5
|
+
methodology catalog into .ship/cache (sync), execute one-shot routines or
|
|
6
|
+
emit prompts for the workspace runner (trigger / run / kickoff /
|
|
7
7
|
callback). Talks to the methodology + orchestration APIs over HTTPS.
|
|
8
8
|
|
|
9
9
|
VOCABULARY
|
|
10
|
-
|
|
11
|
-
pattern:
|
|
12
|
-
|
|
13
|
-
attention surface
|
|
10
|
+
process.routines: (.ship/config.yml) → repo-local scheduled/manual work
|
|
11
|
+
pattern: (artifact kind) → cached role/playbook prompt body
|
|
12
|
+
routine claim: (Ship API) → idempotent schedule-window claim
|
|
13
|
+
attention surface → operator console: Inbox
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Legacy 'lanes:' configs and '--lane' flags are still accepted as aliases
|
|
16
|
+
so already-seeded repositories keep working while new repos use
|
|
17
|
+
'process.routines'.
|
|
18
18
|
|
|
19
19
|
GLOBAL FLAGS
|
|
20
20
|
--base-url URL Methodology API (default: SHIP_API_BASE or
|
|
@@ -66,22 +66,19 @@ COMMANDS
|
|
|
66
66
|
[--force-unpin] [--dry-run] [--lock] [--json] [--cwd <dir>]
|
|
67
67
|
— fetch artifacts into .ship/cache. With --lock,
|
|
68
68
|
also writes .ship/shipctl.lock.json covering
|
|
69
|
-
|
|
69
|
+
every pattern the declared routines depend on.
|
|
70
70
|
|
|
71
71
|
Run
|
|
72
72
|
shipctl trigger --event schedule --repo <id|owner/name> [--workspace <id>] [--json]
|
|
73
|
-
—
|
|
74
|
-
|
|
75
|
-
shipctl run --
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
runner via .github/workflows/run-agent.yml.
|
|
83
|
-
Reports its terminal status via the callback URL
|
|
84
|
-
Ship injected into the workflow.
|
|
73
|
+
— compute due routines locally, then claim
|
|
74
|
+
the schedule window in Ship.
|
|
75
|
+
shipctl run --routine <id> [--dry-run] [--json] [--cwd <dir>]
|
|
76
|
+
— execute one routine end-to-end:
|
|
77
|
+
resolve pattern, fetch a ticket
|
|
78
|
+
(if FSM-staged), launch the agent
|
|
79
|
+
runtime, exit on terminal status.
|
|
80
|
+
'shipctl agent-run' is a back-compat
|
|
81
|
+
alias.
|
|
85
82
|
shipctl lanes install [--only <csv>] [--ref <git-ref>] [--owner <gh>] [--repo <name>]
|
|
86
83
|
[--shipctl-version <v>] [--dry-run] [--force] [--json] [--cwd <dir>]
|
|
87
84
|
shipctl lanes list [--json] [--cwd <dir>]
|
|
@@ -99,6 +96,17 @@ COMMANDS
|
|
|
99
96
|
RunSummary outcome) back to Ship so it can
|
|
100
97
|
render the outcome row and route any
|
|
101
98
|
escalations into the Inbox.
|
|
99
|
+
shipctl process prompt --state <id> [--ticket-json <json>] [--policies-file <path>]
|
|
100
|
+
[--cwd <dir>] [--json]
|
|
101
|
+
— assemble a Process/FSM specialist prompt
|
|
102
|
+
bundle with ticket context, allowed
|
|
103
|
+
transitions, policies, and mandatory
|
|
104
|
+
knowledge-first guardrails.
|
|
105
|
+
shipctl process tickets --workspace <id> [--query <text>] [--tracker <kind>] [--json]
|
|
106
|
+
— read-only tracker picker for selecting
|
|
107
|
+
ticket context before building a process
|
|
108
|
+
prompt. Does not create, comment, or
|
|
109
|
+
transition tickets.
|
|
102
110
|
|
|
103
111
|
Knowledge
|
|
104
112
|
shipctl knowledge init [--workspace <id>] [--repo <id|owner/name>] [--only <csv>] [--json]
|
|
@@ -44,10 +44,12 @@
|
|
|
44
44
|
|
|
45
45
|
const VERSION = "v1";
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* ``
|
|
50
|
-
|
|
47
|
+
/** Static starters with source markdown under ``artifacts/knowledge-starters``.
|
|
48
|
+
* Procedural catalog recipes are exposed by the backend under the
|
|
49
|
+
* ``ship-recipes/<pattern-id>`` prefix because that list is generated from
|
|
50
|
+
* on-disk pattern artifacts at runtime. */
|
|
51
|
+
export const STATIC_KNOWLEDGE_SLUGS = ["code-style", "ui-runbook"];
|
|
52
|
+
export const RECIPE_KNOWLEDGE_PREFIX = "ship-recipes/";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* @param {{baseUrl?: string, json?: boolean}} ctx
|
|
@@ -100,7 +102,9 @@ INIT FLAGS
|
|
|
100
102
|
Defaults to the most-recently activated repo in
|
|
101
103
|
the resolved workspace.
|
|
102
104
|
--only <csv> Comma-separated starter slugs. Defaults to the
|
|
103
|
-
full catalog
|
|
105
|
+
full backend catalog, including static starters
|
|
106
|
+
(${STATIC_KNOWLEDGE_SLUGS.join(", ")}) and generated
|
|
107
|
+
recipe starters under ${RECIPE_KNOWLEDGE_PREFIX}<pattern-id>.
|
|
104
108
|
--base-url URL Workspace control-plane API. See env fallbacks.
|
|
105
109
|
--json Emit a machine-readable JSON summary.
|
|
106
110
|
|
|
@@ -134,10 +138,10 @@ async function knowledgeInitCommand(ctx, args) {
|
|
|
134
138
|
|
|
135
139
|
const selection = opts.only;
|
|
136
140
|
if (selection !== null) {
|
|
137
|
-
const unknown = selection.filter((s) => !
|
|
141
|
+
const unknown = selection.filter((s) => !isKnownKnowledgeStarterSlug(s));
|
|
138
142
|
if (unknown.length) {
|
|
139
143
|
console.error(
|
|
140
|
-
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown: ${
|
|
144
|
+
`Unknown knowledge slug(s): ${unknown.join(", ")}\nKnown static slugs: ${STATIC_KNOWLEDGE_SLUGS.join(", ")}; recipe slugs must start with ${RECIPE_KNOWLEDGE_PREFIX}`,
|
|
141
145
|
);
|
|
142
146
|
process.exit(1);
|
|
143
147
|
}
|
|
@@ -173,6 +177,13 @@ async function knowledgeInitCommand(ctx, args) {
|
|
|
173
177
|
);
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
export function isKnownKnowledgeStarterSlug(slug) {
|
|
181
|
+
return (
|
|
182
|
+
STATIC_KNOWLEDGE_SLUGS.includes(slug) ||
|
|
183
|
+
slug.startsWith(RECIPE_KNOWLEDGE_PREFIX)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
176
187
|
async function knowledgeFetchCommand(ctx, args) {
|
|
177
188
|
const opts = parseFetchArgs(args);
|
|
178
189
|
const baseUrl = resolveBaseUrl(opts.baseUrl || explicitGlobalBaseUrl(ctx));
|
package/lib/commands/lanes.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `shipctl lanes` — generate and manage the thin GitHub Actions caller
|
|
3
|
-
* workflows that delegate to
|
|
3
|
+
* workflows that delegate to a **vendored** reusable `run-agent.yml` in the
|
|
4
|
+
* same repository (RFC-0007 Phase 3).
|
|
4
5
|
*
|
|
5
6
|
* Each lane in `.ship/config.yml` (v2) gets one file at
|
|
6
7
|
* .github/workflows/ship-<lane_id>.yml
|
|
7
|
-
* whose body is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* single ref bump.
|
|
8
|
+
* whose body is the `on:` triggers (derived from the lane kind) and
|
|
9
|
+
* `uses: ./.github/workflows/run-agent.yml`
|
|
10
|
+
* `shipctl lanes install` also writes `.github/workflows/run-agent.yml`
|
|
11
|
+
* from the template bundled inside `@elmundi/ship-cli`.
|
|
12
12
|
*
|
|
13
13
|
* Subcommands:
|
|
14
14
|
* install — render wrappers for every declared lane (or just --only X).
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
24
25
|
import YAML from "yaml";
|
|
25
26
|
|
|
26
27
|
import { findShipRoot, readConfig } from "../config/io.mjs";
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
validateConfig,
|
|
29
30
|
CONFIG_SCHEMA_VERSION,
|
|
30
31
|
lanePatterns,
|
|
31
|
-
lanePrimaryPattern,
|
|
32
32
|
laneFanout,
|
|
33
33
|
} from "../config/schema.mjs";
|
|
34
34
|
|
|
@@ -42,9 +42,16 @@ const BANNER_HEADER = `${BANNER_MARKER}
|
|
|
42
42
|
# Regenerate via: shipctl lanes install
|
|
43
43
|
# Hand edits OUTSIDE the \`ship-cli:\` markers will NOT be overwritten.`;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
45
|
+
/** Same-repo reusable workflow path (GitHub Actions). */
|
|
46
|
+
const LOCAL_REUSABLE_USES = "./.github/workflows/run-agent.yml";
|
|
47
|
+
|
|
48
|
+
const RUN_AGENT_MARKER = "# ship-cli: run-agent v1";
|
|
49
|
+
|
|
50
|
+
function readRunAgentTemplate() {
|
|
51
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const vendor = path.join(dir, "..", "vendor", "run-agent.workflow.yml");
|
|
53
|
+
return fs.readFileSync(vendor, "utf8");
|
|
54
|
+
}
|
|
48
55
|
|
|
49
56
|
function printHelp() {
|
|
50
57
|
console.log(`shipctl lanes — manage GitHub Actions caller workflows for the
|
|
@@ -62,11 +69,8 @@ USAGE
|
|
|
62
69
|
|
|
63
70
|
FLAGS (install)
|
|
64
71
|
--only <ids> Comma-separated lane ids to render (default: all).
|
|
65
|
-
--ref
|
|
66
|
-
|
|
67
|
-
prefixed with 'v' — e.g. v0.12.0).
|
|
68
|
-
--owner <gh-owner> GitHub owner of the ship repo (default: ElMundiUA).
|
|
69
|
-
--repo <name> GitHub repo name (default: ship).
|
|
72
|
+
--ref, --owner, --repo Ignored (legacy); reusable workflow is always
|
|
73
|
+
${LOCAL_REUSABLE_USES} in the caller repo.
|
|
70
74
|
--shipctl-version <v> Pin the @elmundi/ship-cli version the reusable
|
|
71
75
|
workflow installs on the runner (default: latest).
|
|
72
76
|
--force Overwrite wrappers that exist but were not generated
|
|
@@ -97,6 +101,45 @@ export async function lanesCommand(ctx, rest) {
|
|
|
97
101
|
|
|
98
102
|
/* ─────────────────────────── install ─────────────────────────── */
|
|
99
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Copy vendored `run-agent.workflow.yml` into `.github/workflows/run-agent.yml`.
|
|
106
|
+
*/
|
|
107
|
+
function syncRunAgentTemplate({ runAgentFile, runAgentRel, dryRun, force, installed, skipped }) {
|
|
108
|
+
let template;
|
|
109
|
+
try {
|
|
110
|
+
template = readRunAgentTemplate();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`cannot read bundled run-agent template: ${err instanceof Error ? err.message : err}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const existed = fs.existsSync(runAgentFile);
|
|
117
|
+
if (existed) {
|
|
118
|
+
const cur = fs.readFileSync(runAgentFile, "utf8");
|
|
119
|
+
if (!cur.includes(RUN_AGENT_MARKER) && !force) {
|
|
120
|
+
skipped.push({ path: runAgentRel, reason: "run-agent-exists-without-banner" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (cur === template) {
|
|
124
|
+
skipped.push({ path: runAgentRel, reason: "run-agent-up-to-date" });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (dryRun) {
|
|
129
|
+
installed.push({
|
|
130
|
+
path: runAgentRel,
|
|
131
|
+
action: existed ? "would-update-run-agent" : "would-write-run-agent",
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
fs.mkdirSync(path.dirname(runAgentFile), { recursive: true });
|
|
136
|
+
fs.writeFileSync(runAgentFile, template, "utf8");
|
|
137
|
+
installed.push({
|
|
138
|
+
path: runAgentRel,
|
|
139
|
+
action: existed ? "updated-run-agent" : "wrote-run-agent",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
async function installCmd(ctx, rest) {
|
|
101
144
|
const args = parseInstallArgs(rest);
|
|
102
145
|
args.dryRun = args.dryRun || Boolean(ctx.dryRun);
|
|
@@ -112,20 +155,26 @@ async function installCmd(ctx, rest) {
|
|
|
112
155
|
);
|
|
113
156
|
}
|
|
114
157
|
|
|
115
|
-
const
|
|
116
|
-
const reusable = formatReusableRef({
|
|
117
|
-
owner: args.owner || DEFAULT_REUSABLE_OWNER,
|
|
118
|
-
repo: args.repo || DEFAULT_REUSABLE_REPO,
|
|
119
|
-
ref,
|
|
120
|
-
});
|
|
121
|
-
|
|
158
|
+
const reusable = LOCAL_REUSABLE_USES;
|
|
122
159
|
const targetDir = path.join(shipRoot, WORKFLOW_DIR);
|
|
123
|
-
|
|
160
|
+
const runAgentFile = path.join(targetDir, "run-agent.yml");
|
|
161
|
+
const runAgentRel = path.relative(shipRoot, runAgentFile) || runAgentFile;
|
|
124
162
|
|
|
125
163
|
const installed = [];
|
|
126
164
|
const skipped = [];
|
|
127
165
|
const errors = [];
|
|
128
166
|
|
|
167
|
+
syncRunAgentTemplate({
|
|
168
|
+
runAgentFile,
|
|
169
|
+
runAgentRel,
|
|
170
|
+
dryRun: args.dryRun,
|
|
171
|
+
force: args.force,
|
|
172
|
+
installed,
|
|
173
|
+
skipped,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!args.dryRun) fs.mkdirSync(targetDir, { recursive: true });
|
|
177
|
+
|
|
129
178
|
for (const [laneId, lane] of wanted) {
|
|
130
179
|
const file = path.join(targetDir, `ship-${laneId}.yml`);
|
|
131
180
|
const rel = path.relative(shipRoot, file) || file;
|
|
@@ -169,7 +218,7 @@ async function installCmd(ctx, rest) {
|
|
|
169
218
|
};
|
|
170
219
|
|
|
171
220
|
emitSummary(ctx, args, payload, () => {
|
|
172
|
-
console.log(`Ship lanes → ${reusable}`);
|
|
221
|
+
console.log(`Ship lanes → reusable ${reusable}`);
|
|
173
222
|
for (const row of installed) console.log(` ${row.action}: ${row.path}`);
|
|
174
223
|
for (const row of skipped) console.log(` skipped (${row.reason}): ${row.path}`);
|
|
175
224
|
for (const row of errors) console.log(` ERROR: ${row.lane}: ${row.error}`);
|
|
@@ -191,7 +240,7 @@ function listCmd(ctx, rest) {
|
|
|
191
240
|
kind: lane.kind,
|
|
192
241
|
// ``pattern`` keeps the single-string shape for humans/scripts
|
|
193
242
|
// that eyeball the first pattern; ``patterns`` always lists all
|
|
194
|
-
// so multi-pattern
|
|
243
|
+
// so multi-pattern routines (RFC-0008 C3.1) surface correctly.
|
|
195
244
|
pattern: pats[0] || null,
|
|
196
245
|
patterns: pats,
|
|
197
246
|
// ``fanout`` resolves to the runtime default (``matrix``) when
|
|
@@ -310,16 +359,6 @@ function selectLanes(config, onlyCsv) {
|
|
|
310
359
|
return all.filter(([id]) => wanted.has(id));
|
|
311
360
|
}
|
|
312
361
|
|
|
313
|
-
function deriveDefaultRef(config) {
|
|
314
|
-
const min = config.shipctl_min;
|
|
315
|
-
if (typeof min === "string" && /^\d+\.\d+\.\d+/.test(min)) return `v${min}`;
|
|
316
|
-
return "main";
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function formatReusableRef({ owner, repo, ref }) {
|
|
320
|
-
return `${owner}/${repo}/${DEFAULT_REUSABLE_PATH}@${ref}`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
362
|
/**
|
|
324
363
|
* Render the caller workflow YAML. The body is structured first as a JS
|
|
325
364
|
* object so the output is guaranteed to be syntactically valid; we then
|