@elmundi/ship-cli 0.14.2 → 0.15.3
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 +17 -16
- package/bin/shipctl.mjs +4 -80
- package/lib/commands/feedback.mjs +1 -1
- package/lib/commands/help.mjs +47 -131
- package/lib/commands/init.mjs +17 -250
- package/lib/commands/knowledge.mjs +25 -328
- package/lib/commands/preflight.mjs +213 -0
- package/lib/commands/run.mjs +266 -116
- package/lib/commands/trigger.mjs +95 -10
- package/lib/config/schema.mjs +68 -11
- package/lib/http.mjs +0 -2
- package/lib/runtime/routines.mjs +34 -0
- package/lib/templates.mjs +2 -2
- package/lib/verify/checks/agents-on-disk.mjs +5 -28
- package/lib/verify/registry.mjs +7 -8
- package/package.json +1 -1
- package/lib/artifacts/fs-index.mjs +0 -230
- package/lib/cache/store.mjs +0 -422
- package/lib/commands/bootstrap.mjs +0 -4
- package/lib/commands/callback.mjs +0 -742
- package/lib/commands/docs.mjs +0 -90
- package/lib/commands/kickoff.mjs +0 -192
- package/lib/commands/lanes.mjs +0 -566
- package/lib/commands/manifest-catalog.mjs +0 -251
- package/lib/commands/migrate.mjs +0 -204
- package/lib/commands/new.mjs +0 -452
- package/lib/commands/patterns.mjs +0 -160
- package/lib/commands/process.mjs +0 -388
- package/lib/commands/search.mjs +0 -43
- package/lib/commands/sync.mjs +0 -824
- package/lib/config/migrate.mjs +0 -223
- package/lib/find-ship-root.mjs +0 -75
- package/lib/process/specialist-prompt-contract.mjs +0 -171
- package/lib/state/lockfile.mjs +0 -180
- package/lib/vendor/run-agent.workflow.yml +0 -254
- package/lib/verify/checks/artifacts-up-to-date.mjs +0 -78
- package/lib/verify/checks/cache-integrity.mjs +0 -51
- package/lib/verify/checks/gitignore-cache.mjs +0 -51
- package/lib/verify/checks/rules-markers.mjs +0 -135
package/lib/commands/new.mjs
DELETED
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Resolve the shipctl entry script (bin/shipctl.mjs) relative to this module.
|
|
8
|
-
* Used when we fall back to spawning `shipctl <subcommand>` subprocesses.
|
|
9
|
-
*/
|
|
10
|
-
function shipctlBinPath() {
|
|
11
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
return path.resolve(here, "..", "..", "bin", "shipctl.mjs");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string[]} args
|
|
17
|
-
*/
|
|
18
|
-
function parseNewArgs(args) {
|
|
19
|
-
/** @type {Record<string, any>} */
|
|
20
|
-
const out = {
|
|
21
|
-
name: null,
|
|
22
|
-
here: false,
|
|
23
|
-
preset: null,
|
|
24
|
-
tracker: null,
|
|
25
|
-
ci: null,
|
|
26
|
-
agents: /** @type {string[]} */ ([]),
|
|
27
|
-
language: null,
|
|
28
|
-
channel: "stable",
|
|
29
|
-
baseUrl: null,
|
|
30
|
-
yes: false,
|
|
31
|
-
force: false,
|
|
32
|
-
dryRun: false,
|
|
33
|
-
json: false,
|
|
34
|
-
help: false,
|
|
35
|
-
// tri-state: null = default (copy-rules enabled iff agents non-empty),
|
|
36
|
-
// true = forced on, false = opted out via --no-copy-rules.
|
|
37
|
-
copyRules: null,
|
|
38
|
-
// tri-state: null = default on, false = opted out via --no-bootstrap.
|
|
39
|
-
bootstrap: null,
|
|
40
|
-
telemetry: "off",
|
|
41
|
-
extra: /** @type {string[]} */ ([]),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const copy = [...args];
|
|
45
|
-
while (copy.length) {
|
|
46
|
-
const a = copy.shift();
|
|
47
|
-
if (a === "--here") { out.here = true; continue; }
|
|
48
|
-
if (a === "--help" || a === "-h") { out.help = true; continue; }
|
|
49
|
-
if (a === "--yes" || a === "-y") { out.yes = true; continue; }
|
|
50
|
-
if (a === "--force") { out.force = true; continue; }
|
|
51
|
-
if (a === "--dry-run") { out.dryRun = true; continue; }
|
|
52
|
-
if (a === "--json") { out.json = true; continue; }
|
|
53
|
-
if (a === "--copy-rules") { out.copyRules = true; continue; }
|
|
54
|
-
if (a === "--no-copy-rules") { out.copyRules = false; continue; }
|
|
55
|
-
if (a === "--bootstrap") { out.bootstrap = true; continue; }
|
|
56
|
-
if (a === "--no-bootstrap") { out.bootstrap = false; continue; }
|
|
57
|
-
if (a === "--preset" && copy.length) { out.preset = copy.shift(); continue; }
|
|
58
|
-
if (a.startsWith("--preset=")) { out.preset = a.slice("--preset=".length); continue; }
|
|
59
|
-
if (a === "--tracker" && copy.length) { out.tracker = copy.shift(); continue; }
|
|
60
|
-
if (a.startsWith("--tracker=")) { out.tracker = a.slice("--tracker=".length); continue; }
|
|
61
|
-
if (a === "--ci" && copy.length) { out.ci = copy.shift(); continue; }
|
|
62
|
-
if (a.startsWith("--ci=")) { out.ci = a.slice("--ci=".length); continue; }
|
|
63
|
-
if (a === "--base-url" && copy.length) { out.baseUrl = copy.shift(); continue; }
|
|
64
|
-
if (a.startsWith("--base-url=")) { out.baseUrl = a.slice("--base-url=".length); continue; }
|
|
65
|
-
if (a === "--agents" && copy.length) {
|
|
66
|
-
for (const s of String(copy.shift()).split(",")) {
|
|
67
|
-
const id = s.trim();
|
|
68
|
-
if (id) out.agents.push(id);
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (a.startsWith("--agents=")) {
|
|
73
|
-
for (const s of a.slice("--agents=".length).split(",")) {
|
|
74
|
-
const id = s.trim();
|
|
75
|
-
if (id) out.agents.push(id);
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (a === "--language" && copy.length) { out.language = copy.shift(); continue; }
|
|
80
|
-
if (a.startsWith("--language=")) { out.language = a.slice("--language=".length); continue; }
|
|
81
|
-
if (a === "--channel" && copy.length) { out.channel = copy.shift(); continue; }
|
|
82
|
-
if (a.startsWith("--channel=")) { out.channel = a.slice("--channel=".length); continue; }
|
|
83
|
-
if (a === "--telemetry" && copy.length) { out.telemetry = copy.shift(); continue; }
|
|
84
|
-
if (a.startsWith("--telemetry=")) { out.telemetry = a.slice("--telemetry=".length); continue; }
|
|
85
|
-
if (a && a.startsWith("--")) { out.extra.push(a); continue; }
|
|
86
|
-
if (out.name == null) { out.name = a; continue; }
|
|
87
|
-
out.extra.push(a);
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function printNewHelp() {
|
|
93
|
-
console.log(`shipctl new <name> — bootstrap a fresh repository with Ship wiring.
|
|
94
|
-
|
|
95
|
-
USAGE
|
|
96
|
-
shipctl new <name> [options]
|
|
97
|
-
shipctl new [--here] [options]
|
|
98
|
-
|
|
99
|
-
OPTIONS
|
|
100
|
-
--here Initialise in the current directory instead of <name>/.
|
|
101
|
-
--preset <id> adoption-minimum|web-app|api-backend|mobile-app|cli|monorepo
|
|
102
|
-
--tracker <id> linear|jira|github-issues|azure-boards|clickup|spreadsheet|none
|
|
103
|
-
--ci <id> gh-actions|gitlab-ci|buildkite|circleci|azure-pipelines|jenkins|manual
|
|
104
|
-
--agents <csv> Comma-separated agent ids (e.g. cursor,codex,claude).
|
|
105
|
-
--language <id> ts|js|py|go|rust|java|kotlin|swift|dart|multi
|
|
106
|
-
--channel <id> stable|edge (written to api.channel; default stable).
|
|
107
|
-
--base-url <url> Override Ship API base URL (forwarded to 'init').
|
|
108
|
-
--copy-rules Forward to init. Default ON when agents are selected;
|
|
109
|
-
use --no-copy-rules to opt out.
|
|
110
|
-
--no-copy-rules Skip installing cached agent rule files on disk.
|
|
111
|
-
--bootstrap Forward --bootstrap to init. Default ON; use
|
|
112
|
-
--no-bootstrap to opt out.
|
|
113
|
-
--no-bootstrap Skip rendering CI/tracker scaffolding.
|
|
114
|
-
--telemetry <on|off> Default: off. Writes telemetry.share.
|
|
115
|
-
--yes Non-interactive (assumed for --dry-run).
|
|
116
|
-
--force Reuse a non-empty target directory.
|
|
117
|
-
--dry-run Describe the plan without touching disk.
|
|
118
|
-
--json Machine-readable summary.
|
|
119
|
-
|
|
120
|
-
Creates <name>/ (or reuses cwd with --here), runs 'git init -q', writes a
|
|
121
|
-
minimal README.md, seeds .ship/config.yml via 'shipctl config init', applies
|
|
122
|
-
the provided stack flags via 'shipctl config set', and then runs
|
|
123
|
-
'shipctl init --yes' for any selected agents.
|
|
124
|
-
`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function dirIsEmpty(dir) {
|
|
128
|
-
try {
|
|
129
|
-
const entries = fs.readdirSync(dir);
|
|
130
|
-
return entries.filter((e) => e !== ".DS_Store").length === 0;
|
|
131
|
-
} catch {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isGitRepo(dir) {
|
|
137
|
-
return fs.existsSync(path.join(dir, ".git"));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function resolveTargetDir(args, cwd) {
|
|
141
|
-
if (args.here) return path.resolve(cwd);
|
|
142
|
-
if (!args.name) {
|
|
143
|
-
throw new Error("new: missing <name>. Run 'shipctl new --help' for usage.");
|
|
144
|
-
}
|
|
145
|
-
return path.resolve(cwd, args.name);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Run `shipctl <sub...>` via the same Node binary, capturing output for JSON mode.
|
|
150
|
-
* @param {string[]} argv
|
|
151
|
-
* @param {{capture?:boolean}} [opts]
|
|
152
|
-
*/
|
|
153
|
-
function runShipctl(argv, opts = {}) {
|
|
154
|
-
const bin = shipctlBinPath();
|
|
155
|
-
const res = spawnSync(process.execPath, [bin, ...argv], {
|
|
156
|
-
stdio: opts.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
157
|
-
encoding: "utf8",
|
|
158
|
-
});
|
|
159
|
-
if (res.error) throw res.error;
|
|
160
|
-
return {
|
|
161
|
-
status: typeof res.status === "number" ? res.status : 1,
|
|
162
|
-
stdout: res.stdout || "",
|
|
163
|
-
stderr: res.stderr || "",
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Apply stack-level settings via `shipctl config set`.
|
|
169
|
-
* Missing values are skipped.
|
|
170
|
-
* @param {string} newDir
|
|
171
|
-
* @param {ReturnType<typeof parseNewArgs>} a
|
|
172
|
-
* @param {boolean} capture
|
|
173
|
-
* @returns {{ok:boolean, applied:string[], errors:string[]}}
|
|
174
|
-
*/
|
|
175
|
-
function applyStackConfig(newDir, a, capture) {
|
|
176
|
-
const applied = [];
|
|
177
|
-
const errors = [];
|
|
178
|
-
const set = (key, value) => {
|
|
179
|
-
const res = runShipctl(["config", "set", key, String(value), "--cwd", newDir], {
|
|
180
|
-
capture,
|
|
181
|
-
});
|
|
182
|
-
if (res.status !== 0) {
|
|
183
|
-
errors.push(`${key}=${value}: ${(res.stderr || res.stdout).trim() || `exit ${res.status}`}`);
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
applied.push(`${key}=${value}`);
|
|
187
|
-
return true;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
if (a.tracker) set("stack.tracker", a.tracker);
|
|
191
|
-
if (a.ci) set("stack.ci", a.ci);
|
|
192
|
-
if (a.preset) set("stack.preset", a.preset);
|
|
193
|
-
if (a.language) set("stack.language", a.language);
|
|
194
|
-
if (a.channel) set("api.channel", a.channel);
|
|
195
|
-
if (a.agents.length) set("stack.agents", `[${a.agents.join(",")}]`);
|
|
196
|
-
if (a.telemetry === "on") set("telemetry.share", "true");
|
|
197
|
-
else if (a.telemetry === "off") set("telemetry.share", "false");
|
|
198
|
-
|
|
199
|
-
return { ok: errors.length === 0, applied, errors };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* @param {{ baseUrl:string, yes:boolean, force:boolean, dryRun:boolean, json:boolean }} ctx
|
|
204
|
-
* @param {string[]} args
|
|
205
|
-
*/
|
|
206
|
-
export async function newCommand(ctx, args) {
|
|
207
|
-
const a = parseNewArgs(args);
|
|
208
|
-
if (a.help) { printNewHelp(); return; }
|
|
209
|
-
|
|
210
|
-
if (ctx) {
|
|
211
|
-
if (ctx.json) a.json = true;
|
|
212
|
-
if (ctx.yes) a.yes = true;
|
|
213
|
-
if (ctx.force) a.force = true;
|
|
214
|
-
if (ctx.dryRun) a.dryRun = true;
|
|
215
|
-
// extractGlobalArgv in bin/shipctl.mjs strips `--base-url` out of argv
|
|
216
|
-
// and stashes it on ctx. Fold it in here so the init subprocess gets
|
|
217
|
-
// the same URL the caller handed to `shipctl new`.
|
|
218
|
-
if (!a.baseUrl && ctx.baseUrl) a.baseUrl = ctx.baseUrl;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const cwd = process.cwd();
|
|
222
|
-
let newDir;
|
|
223
|
-
try {
|
|
224
|
-
newDir = resolveTargetDir(a, cwd);
|
|
225
|
-
} catch (e) {
|
|
226
|
-
console.error(e.message);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const willCreate = !fs.existsSync(newDir);
|
|
231
|
-
const alreadyGit = !willCreate && isGitRepo(newDir);
|
|
232
|
-
const alreadyNonEmpty = !willCreate && !dirIsEmpty(newDir);
|
|
233
|
-
const configAlready = fs.existsSync(path.join(newDir, ".ship", "config.yml"));
|
|
234
|
-
|
|
235
|
-
if (!a.here && !willCreate && alreadyNonEmpty && !a.force) {
|
|
236
|
-
console.error(
|
|
237
|
-
`new: target directory is not empty: ${newDir}\n` +
|
|
238
|
-
`Re-run with --force to reuse it, or choose a different <name>.`,
|
|
239
|
-
);
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const plannedFiles = [
|
|
244
|
-
{ path: path.join(newDir, ".git"), reason: "git init" },
|
|
245
|
-
{ path: path.join(newDir, "README.md"), reason: "minimal stub" },
|
|
246
|
-
{ path: path.join(newDir, ".ship", "config.yml"), reason: "shipctl config init" },
|
|
247
|
-
];
|
|
248
|
-
|
|
249
|
-
const initArgv = buildInitArgv(a, newDir);
|
|
250
|
-
const runInit = shouldRunInit(a);
|
|
251
|
-
|
|
252
|
-
const summary = {
|
|
253
|
-
cwd,
|
|
254
|
-
dir: newDir,
|
|
255
|
-
created_dir: willCreate,
|
|
256
|
-
reused_dir: !willCreate,
|
|
257
|
-
here: a.here,
|
|
258
|
-
git_init: !alreadyGit,
|
|
259
|
-
readme: true,
|
|
260
|
-
stack: {
|
|
261
|
-
tracker: a.tracker,
|
|
262
|
-
ci: a.ci,
|
|
263
|
-
preset: a.preset,
|
|
264
|
-
language: a.language,
|
|
265
|
-
channel: a.channel,
|
|
266
|
-
base_url: a.baseUrl,
|
|
267
|
-
agents: a.agents,
|
|
268
|
-
telemetry: a.telemetry,
|
|
269
|
-
copy_rules:
|
|
270
|
-
a.copyRules === true || (a.copyRules !== false && a.agents.length > 0),
|
|
271
|
-
bootstrap: a.bootstrap !== false,
|
|
272
|
-
},
|
|
273
|
-
init_argv: initArgv,
|
|
274
|
-
run_init: runInit,
|
|
275
|
-
planned_files: plannedFiles.map((f) => path.relative(cwd, f.path) || f.path),
|
|
276
|
-
next_steps: [
|
|
277
|
-
`cd ${path.relative(cwd, newDir) || "."}`,
|
|
278
|
-
"shipctl verify",
|
|
279
|
-
],
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
if (a.dryRun) {
|
|
283
|
-
if (a.json) {
|
|
284
|
-
console.log(JSON.stringify({ ...summary, dry_run: true }, null, 2));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
console.log(
|
|
288
|
-
`shipctl new (dry-run) — ${a.here ? "using current dir" : willCreate ? "would create" : "would reuse"}: ${newDir}`,
|
|
289
|
-
);
|
|
290
|
-
const show = (full) => {
|
|
291
|
-
const rel = path.relative(cwd, full);
|
|
292
|
-
return rel && !rel.startsWith("..") ? rel : full;
|
|
293
|
-
};
|
|
294
|
-
for (const f of plannedFiles) {
|
|
295
|
-
console.log(` plan: write ${show(f.path)} (${f.reason})`);
|
|
296
|
-
}
|
|
297
|
-
if (runInit) {
|
|
298
|
-
console.log(` plan: shipctl ${initArgv.join(" ")}`);
|
|
299
|
-
}
|
|
300
|
-
const stackLines = [];
|
|
301
|
-
if (a.tracker) stackLines.push(`stack.tracker=${a.tracker}`);
|
|
302
|
-
if (a.ci) stackLines.push(`stack.ci=${a.ci}`);
|
|
303
|
-
if (a.preset) stackLines.push(`stack.preset=${a.preset}`);
|
|
304
|
-
if (a.language) stackLines.push(`stack.language=${a.language}`);
|
|
305
|
-
if (a.channel) stackLines.push(`api.channel=${a.channel}`);
|
|
306
|
-
if (a.agents.length) stackLines.push(`stack.agents=[${a.agents.join(",")}]`);
|
|
307
|
-
if (a.telemetry) stackLines.push(`telemetry.share=${a.telemetry === "on"}`);
|
|
308
|
-
for (const s of stackLines) console.log(` plan: shipctl config set ${s}`);
|
|
309
|
-
console.log("(dry-run: no files written)");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const createdFiles = [];
|
|
314
|
-
if (willCreate) fs.mkdirSync(newDir, { recursive: true });
|
|
315
|
-
|
|
316
|
-
if (!alreadyGit) {
|
|
317
|
-
const gitInit = spawnSync("git", ["init", "-q"], { cwd: newDir, encoding: "utf8" });
|
|
318
|
-
if (gitInit.status !== 0) {
|
|
319
|
-
const reason = (gitInit.stderr || "").trim() || `exit ${gitInit.status}`;
|
|
320
|
-
console.error(`new: 'git init' failed in ${newDir}: ${reason}`);
|
|
321
|
-
process.exit(1);
|
|
322
|
-
}
|
|
323
|
-
createdFiles.push(path.join(newDir, ".git"));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const readmePath = path.join(newDir, "README.md");
|
|
327
|
-
if (!fs.existsSync(readmePath)) {
|
|
328
|
-
const displayName = a.name || path.basename(newDir);
|
|
329
|
-
fs.writeFileSync(
|
|
330
|
-
readmePath,
|
|
331
|
-
`# ${displayName}\n\nBootstrapped with shipctl (Ship methodology kit).\n`,
|
|
332
|
-
"utf8",
|
|
333
|
-
);
|
|
334
|
-
createdFiles.push(readmePath);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const capture = !!a.json;
|
|
338
|
-
const log = (msg) => { if (!a.json) console.log(msg); };
|
|
339
|
-
|
|
340
|
-
if (!configAlready) {
|
|
341
|
-
const res = runShipctl(["config", "init", "--cwd", newDir], { capture });
|
|
342
|
-
if (res.status !== 0) {
|
|
343
|
-
const out = (res.stderr || res.stdout).trim();
|
|
344
|
-
console.error(
|
|
345
|
-
`new: 'shipctl config init' exited with code ${res.status}${out ? `\n${out}` : ""}`,
|
|
346
|
-
);
|
|
347
|
-
process.exit(res.status);
|
|
348
|
-
}
|
|
349
|
-
createdFiles.push(path.join(newDir, ".ship", "config.yml"));
|
|
350
|
-
if (!a.json) process.stdout.write(res.stdout || "");
|
|
351
|
-
} else {
|
|
352
|
-
log(`config: reusing existing ${path.join(newDir, ".ship", "config.yml")}`);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const stackResult = applyStackConfig(newDir, a, capture);
|
|
356
|
-
if (!stackResult.ok) {
|
|
357
|
-
console.error("new: failed to apply stack flags:");
|
|
358
|
-
for (const e of stackResult.errors) console.error(` - ${e}`);
|
|
359
|
-
process.exit(10);
|
|
360
|
-
}
|
|
361
|
-
if (!a.json) {
|
|
362
|
-
for (const s of stackResult.applied) console.log(`config set ${s}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
let initStatus = 0;
|
|
366
|
-
if (runInit) {
|
|
367
|
-
const res = runShipctl(initArgv, { capture });
|
|
368
|
-
initStatus = res.status;
|
|
369
|
-
if (!a.json) process.stdout.write(res.stdout || "");
|
|
370
|
-
if (res.status !== 0) {
|
|
371
|
-
const out = (res.stderr || res.stdout).trim();
|
|
372
|
-
console.error(
|
|
373
|
-
`new: 'shipctl init' exited with code ${res.status}${out ? `\n${out}` : ""}\n` +
|
|
374
|
-
`Re-run with the same flags to retry, or pass --no-bootstrap / --no-copy-rules to skip remote steps.`,
|
|
375
|
-
);
|
|
376
|
-
process.exit(res.status);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const finalConfigPath = path.join(newDir, ".ship", "config.yml");
|
|
381
|
-
const configExists = fs.existsSync(finalConfigPath);
|
|
382
|
-
|
|
383
|
-
if (a.json) {
|
|
384
|
-
console.log(
|
|
385
|
-
JSON.stringify(
|
|
386
|
-
{
|
|
387
|
-
...summary,
|
|
388
|
-
dry_run: false,
|
|
389
|
-
stack_set: stackResult.applied,
|
|
390
|
-
created_files: createdFiles.map((p) => path.relative(cwd, p) || p),
|
|
391
|
-
config_written: configExists,
|
|
392
|
-
init_status: initStatus,
|
|
393
|
-
},
|
|
394
|
-
null,
|
|
395
|
-
2,
|
|
396
|
-
),
|
|
397
|
-
);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
console.log("");
|
|
402
|
-
console.log(`Done. Ship scaffolding in ${newDir}`);
|
|
403
|
-
console.log("Next:");
|
|
404
|
-
console.log(` cd ${path.relative(cwd, newDir) || "."}`);
|
|
405
|
-
console.log(" shipctl verify");
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Build the argv list for the `shipctl init` subprocess. Kept separate so
|
|
410
|
-
* --dry-run can show the plan without executing. Forwards the full set of
|
|
411
|
-
* stack flags (tracker, CI, preset, agents, language, channel, base-url,
|
|
412
|
-
* telemetry) and defaults --copy-rules ON (when agents were selected) and
|
|
413
|
-
* --bootstrap ON. Use --no-copy-rules / --no-bootstrap on `new` to opt out.
|
|
414
|
-
*
|
|
415
|
-
* @param {ReturnType<typeof parseNewArgs>} a
|
|
416
|
-
* @param {string} newDir
|
|
417
|
-
*/
|
|
418
|
-
export function buildInitArgv(a, newDir) {
|
|
419
|
-
const argv = ["init", "--cwd", newDir, "--yes"];
|
|
420
|
-
if (a.agents.length) argv.push("--agents", a.agents.join(","));
|
|
421
|
-
if (a.tracker) argv.push("--tracker", a.tracker);
|
|
422
|
-
if (a.ci) argv.push("--ci", a.ci);
|
|
423
|
-
if (a.preset) argv.push("--preset", a.preset);
|
|
424
|
-
if (a.force) argv.push("--force");
|
|
425
|
-
if (a.language) argv.push("--language", a.language);
|
|
426
|
-
if (a.channel) argv.push("--channel", a.channel);
|
|
427
|
-
if (a.telemetry) argv.push("--telemetry", a.telemetry);
|
|
428
|
-
if (a.baseUrl) argv.push("--base-url", a.baseUrl);
|
|
429
|
-
|
|
430
|
-
const wantCopyRules =
|
|
431
|
-
a.copyRules === true || (a.copyRules !== false && a.agents.length > 0);
|
|
432
|
-
if (wantCopyRules) argv.push("--copy-rules");
|
|
433
|
-
|
|
434
|
-
const wantBootstrap = a.bootstrap !== false;
|
|
435
|
-
if (wantBootstrap) argv.push("--bootstrap");
|
|
436
|
-
|
|
437
|
-
if (a.json) argv.push("--json");
|
|
438
|
-
return argv;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Decide whether `shipctl new` needs to spawn `shipctl init` at all. init
|
|
443
|
-
* does the real work (rule files on disk, CI scaffolding, telemetry prompt,
|
|
444
|
-
* initial sync). If the caller said --no-bootstrap and passed no agents,
|
|
445
|
-
* we skip the subprocess entirely.
|
|
446
|
-
* @param {ReturnType<typeof parseNewArgs>} a
|
|
447
|
-
*/
|
|
448
|
-
function shouldRunInit(a) {
|
|
449
|
-
if (a.agents.length > 0) return true;
|
|
450
|
-
if (a.bootstrap !== false) return true;
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { apiGet, apiPost } from "../http.mjs";
|
|
2
|
-
import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
|
|
3
|
-
import { searchCommand } from "./search.mjs";
|
|
4
|
-
import { scanArtifacts, readArtifactFile } from "../artifacts/fs-index.mjs";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {Record<string, unknown>} p
|
|
8
|
-
*/
|
|
9
|
-
function slimEntry(p) {
|
|
10
|
-
return {
|
|
11
|
-
id: p.id,
|
|
12
|
-
title: p.title,
|
|
13
|
-
summary: p.summary,
|
|
14
|
-
path: p.path,
|
|
15
|
-
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
16
|
-
group: p.group,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {string} root
|
|
22
|
-
* @param {{ baseUrl: string; json: boolean }} ctx
|
|
23
|
-
* @param {string} sub
|
|
24
|
-
* @param {string[]} rest
|
|
25
|
-
*/
|
|
26
|
-
async function patternsFromDisk(root, ctx, sub, rest) {
|
|
27
|
-
const entries = scanArtifacts(root, "pattern");
|
|
28
|
-
|
|
29
|
-
if (sub === "list") {
|
|
30
|
-
const slim = entries.map((p) => slimEntry(p));
|
|
31
|
-
const out = { version: 1, description: "Patterns", patterns: slim };
|
|
32
|
-
if (ctx.json) console.log(JSON.stringify(out, null, 2));
|
|
33
|
-
else {
|
|
34
|
-
console.log(`Patterns\n`);
|
|
35
|
-
for (const p of slim) {
|
|
36
|
-
console.log(`- ${p.id}`);
|
|
37
|
-
console.log(` ${p.title}`);
|
|
38
|
-
const tags = (p.tags || []).join(", ");
|
|
39
|
-
console.log(` path: ${p.path} tags: ${tags}\n`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (sub === "show" || sub === "fetch") {
|
|
46
|
-
const id = rest[0];
|
|
47
|
-
if (!id) {
|
|
48
|
-
console.error(`${sub}: pattern id required.`);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
const entry = entries.find((e) => e.id === id);
|
|
52
|
-
if (!entry) {
|
|
53
|
-
console.error(`Unknown id: ${id}`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
const file = readArtifactFile(root, "pattern", id);
|
|
57
|
-
if (!file) {
|
|
58
|
-
console.error(`Missing file: ${entry.path}`);
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
const content = file.content;
|
|
62
|
-
const full = { ...slimEntry(entry), content };
|
|
63
|
-
if (ctx.json) console.log(JSON.stringify(full, null, 2));
|
|
64
|
-
else {
|
|
65
|
-
console.log(`# ${entry.title} (${entry.id})\n`);
|
|
66
|
-
console.log(content);
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.error(`Unknown pattern subcommand: ${sub}`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {{ baseUrl: string; json: boolean }} ctx
|
|
77
|
-
* @param {string} sub
|
|
78
|
-
* @param {string[]} rest
|
|
79
|
-
*/
|
|
80
|
-
async function patternsFromHosted(ctx, sub, rest) {
|
|
81
|
-
const base = ctx.baseUrl;
|
|
82
|
-
if (sub === "list") {
|
|
83
|
-
const data = await apiGet(base, "/patterns");
|
|
84
|
-
if (ctx.json) console.log(JSON.stringify(data, null, 2));
|
|
85
|
-
else {
|
|
86
|
-
console.log(`${data.description || "Patterns"}\n`);
|
|
87
|
-
for (const p of data.patterns || []) {
|
|
88
|
-
console.log(`- ${p.id}`);
|
|
89
|
-
console.log(` ${p.title}`);
|
|
90
|
-
console.log(` path: ${p.path} tags: ${(p.tags || []).join(", ")}\n`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (sub === "show") {
|
|
96
|
-
const id = rest[0];
|
|
97
|
-
if (!id) {
|
|
98
|
-
console.error("show: pattern id required.");
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
const data = await apiGet(base, `/patterns/${encodeURIComponent(id)}`);
|
|
102
|
-
if (ctx.json) console.log(JSON.stringify(data, null, 2));
|
|
103
|
-
else {
|
|
104
|
-
console.log(`# ${data.title} (${data.id})\n`);
|
|
105
|
-
console.log(data.content);
|
|
106
|
-
}
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (sub === "fetch") {
|
|
110
|
-
const id = rest[0];
|
|
111
|
-
if (!id) {
|
|
112
|
-
console.error("fetch: pattern id required.");
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
const data = await apiPost(base, "/fetch", { kind: "pattern", id });
|
|
116
|
-
if (ctx.json) console.log(JSON.stringify(data, null, 2));
|
|
117
|
-
else {
|
|
118
|
-
console.log(`# ${data.title} (${data.id})\n`);
|
|
119
|
-
console.log(data.content);
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
console.error(`Unknown pattern subcommand: ${sub}`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {{ baseUrl: string; json: boolean }} ctx
|
|
129
|
-
* @param {string[]} args
|
|
130
|
-
*/
|
|
131
|
-
export async function patternCommand(ctx, args) {
|
|
132
|
-
const [sub, ...rest] = args;
|
|
133
|
-
if (!sub || sub === "help") {
|
|
134
|
-
console.log(`Usage:
|
|
135
|
-
shipctl pattern list
|
|
136
|
-
shipctl pattern show <id>
|
|
137
|
-
shipctl pattern fetch <id>
|
|
138
|
-
shipctl pattern search <query> [--top-k N]
|
|
139
|
-
|
|
140
|
-
With a local Ship tree (cwd or SHIP_REPO): list/show/fetch scan artifacts/patterns/<id>/ARTIFACT.md on disk.
|
|
141
|
-
Otherwise: methodology API (GET /patterns, POST /fetch for fetch, POST /search for search).
|
|
142
|
-
|
|
143
|
-
Plural alias: shipctl patterns …
|
|
144
|
-
|
|
145
|
-
Global flags: --base-url URL --json`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (sub === "search") {
|
|
150
|
-
await searchCommand(ctx, rest);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const root = resolveShipRepoRootForCatalog();
|
|
155
|
-
if (root) {
|
|
156
|
-
await patternsFromDisk(root, ctx, sub, rest);
|
|
157
|
-
} else {
|
|
158
|
-
await patternsFromHosted(ctx, sub, rest);
|
|
159
|
-
}
|
|
160
|
-
}
|