@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.
Files changed (76) hide show
  1. package/README.md +415 -22
  2. package/bin/shipctl.mjs +165 -0
  3. package/lib/adapters/_fs.mjs +165 -0
  4. package/lib/adapters/agents/index.mjs +26 -0
  5. package/lib/adapters/ci/azure-pipelines.mjs +23 -0
  6. package/lib/adapters/ci/buildkite.mjs +24 -0
  7. package/lib/adapters/ci/circleci.mjs +23 -0
  8. package/lib/adapters/ci/gh-actions.mjs +29 -0
  9. package/lib/adapters/ci/gitlab-ci.mjs +23 -0
  10. package/lib/adapters/ci/jenkins.mjs +23 -0
  11. package/lib/adapters/ci/manual.mjs +18 -0
  12. package/lib/adapters/index.mjs +122 -0
  13. package/lib/adapters/language/dart.mjs +23 -0
  14. package/lib/adapters/language/go.mjs +23 -0
  15. package/lib/adapters/language/java.mjs +27 -0
  16. package/lib/adapters/language/js.mjs +32 -0
  17. package/lib/adapters/language/kotlin.mjs +48 -0
  18. package/lib/adapters/language/py.mjs +34 -0
  19. package/lib/adapters/language/rust.mjs +23 -0
  20. package/lib/adapters/language/swift.mjs +37 -0
  21. package/lib/adapters/language/ts.mjs +35 -0
  22. package/lib/adapters/trackers/azure-boards.mjs +49 -0
  23. package/lib/adapters/trackers/clickup.mjs +43 -0
  24. package/lib/adapters/trackers/github-issues.mjs +52 -0
  25. package/lib/adapters/trackers/jira.mjs +72 -0
  26. package/lib/adapters/trackers/linear.mjs +62 -0
  27. package/lib/adapters/trackers/none.mjs +18 -0
  28. package/lib/adapters/trackers/spreadsheet.mjs +28 -0
  29. package/lib/artifacts/fs-index.mjs +230 -0
  30. package/lib/bootstrap/render.mjs +373 -0
  31. package/lib/cache/store.mjs +422 -0
  32. package/lib/commands/bootstrap.mjs +4 -0
  33. package/lib/commands/callback.mjs +302 -0
  34. package/lib/commands/config.mjs +257 -0
  35. package/lib/commands/docs.mjs +1 -1
  36. package/lib/commands/doctor.mjs +583 -0
  37. package/lib/commands/feedback.mjs +355 -0
  38. package/lib/commands/help.mjs +96 -21
  39. package/lib/commands/init.mjs +830 -158
  40. package/lib/commands/kickoff.mjs +192 -0
  41. package/lib/commands/knowledge.mjs +368 -0
  42. package/lib/commands/lanes.mjs +502 -0
  43. package/lib/commands/manifest-catalog.mjs +102 -38
  44. package/lib/commands/migrate.mjs +204 -0
  45. package/lib/commands/new.mjs +452 -0
  46. package/lib/commands/patterns.mjs +9 -43
  47. package/lib/commands/run.mjs +617 -0
  48. package/lib/commands/sync.mjs +749 -0
  49. package/lib/commands/telemetry.mjs +390 -0
  50. package/lib/commands/verify.mjs +187 -0
  51. package/lib/config/io.mjs +232 -0
  52. package/lib/config/migrate.mjs +215 -0
  53. package/lib/config/schema.mjs +650 -0
  54. package/lib/detect.mjs +162 -19
  55. package/lib/feedback/drafts.mjs +129 -0
  56. package/lib/find-ship-root.mjs +16 -10
  57. package/lib/http.mjs +237 -11
  58. package/lib/state/idempotency.mjs +183 -0
  59. package/lib/state/lockfile.mjs +180 -0
  60. package/lib/telemetry/outbox.mjs +224 -0
  61. package/lib/templates.mjs +53 -65
  62. package/lib/verify/checks/agents-on-disk.mjs +58 -0
  63. package/lib/verify/checks/api-reachable.mjs +39 -0
  64. package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
  65. package/lib/verify/checks/bootstrap-files.mjs +67 -0
  66. package/lib/verify/checks/cache-integrity.mjs +51 -0
  67. package/lib/verify/checks/ci-secrets.mjs +86 -0
  68. package/lib/verify/checks/config-present.mjs +39 -0
  69. package/lib/verify/checks/gitignore-cache.mjs +51 -0
  70. package/lib/verify/checks/rules-markers.mjs +135 -0
  71. package/lib/verify/checks/stack-enums.mjs +33 -0
  72. package/lib/verify/checks/tracker-labels.mjs +91 -0
  73. package/lib/verify/registry.mjs +120 -0
  74. package/lib/version.mjs +34 -0
  75. package/package.json +10 -3
  76. package/bin/ship.mjs +0 -68
@@ -1,27 +1,20 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
- import { apiGet, apiPost } from "../http.mjs";
2
+ import { apiGet, apiPost, fetchArtifact } from "../http.mjs";
4
3
  import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
4
+ import { findShipRoot } from "../config/io.mjs";
5
+ import { writeCached, cachePath } from "../cache/store.mjs";
5
6
  import { searchCommand } from "./search.mjs";
7
+ import { scanArtifacts, readArtifactFile, pluralFor } from "../artifacts/fs-index.mjs";
6
8
 
7
- /** @type {Record<string, { manifestRel: string; arrayKey: string; name: string; apiPath: string; fetchKind: string }>} */
9
+ /** @type {Record<string, { arrayKey: string; name: string; apiPath: string; fetchKind: "tool"|"collection" }>} */
8
10
  const RESOURCES = {
9
11
  tool: {
10
- manifestRel: "tools/manifest.json",
11
12
  arrayKey: "tools",
12
13
  name: "Tools",
13
14
  apiPath: "tools",
14
15
  fetchKind: "tool",
15
16
  },
16
- workflow: {
17
- manifestRel: "workflows/manifest.json",
18
- arrayKey: "workflows",
19
- name: "Workflows",
20
- apiPath: "workflows",
21
- fetchKind: "workflow",
22
- },
23
17
  collection: {
24
- manifestRel: "collections/manifest.json",
25
18
  arrayKey: "collections",
26
19
  name: "Collections",
27
20
  apiPath: "collections",
@@ -30,7 +23,7 @@ const RESOURCES = {
30
23
  };
31
24
 
32
25
  /**
33
- * @param {"tool"|"workflow"|"collection"} resource
26
+ * @param {"tool"|"collection"} resource
34
27
  * @param {{ baseUrl: string; json: boolean }} ctx
35
28
  * @param {string[]} args
36
29
  */
@@ -40,15 +33,20 @@ export async function resourceManifestCommand(resource, ctx, args) {
40
33
 
41
34
  const [sub, ...rest] = args;
42
35
  if (!sub || sub === "help") {
36
+ const plural = pluralFor(spec.fetchKind);
43
37
  console.log(`Usage:
44
38
  ship ${resource} list
45
39
  ship ${resource} show <id>
46
- ship ${resource} fetch <id>
40
+ ship ${resource} fetch <id> [--version V] [--print]
47
41
  ship ${resource} search <query> [--top-k N]
48
42
 
49
- With a local Ship tree (cwd or SHIP_REPO): reads ${spec.manifestRel} on disk.
43
+ With a local Ship tree (cwd or SHIP_REPO): scans artifacts/${plural}/<id>/ARTIFACT.md on disk.
50
44
  Otherwise: methodology API (GET /${spec.apiPath}, POST /fetch for fetch, POST /search for search).
51
45
 
46
+ In a Ship workspace (.ship/config.yml), 'fetch' writes the artifact to
47
+ .ship/cache/<kind>/<id>@<version>/ARTIFACT.md and prints a 'cached:' line. Pass
48
+ --print to also echo the body on stdout.
49
+
52
50
  Plural alias: ship ${spec.apiPath} …
53
51
 
54
52
  Global flags: --base-url URL --json`);
@@ -69,7 +67,29 @@ Global flags: --base-url URL --json`);
69
67
  }
70
68
 
71
69
  /**
72
- * @param {"tool"|"workflow"|"collection"} resource
70
+ * Parse `fetch`-specific flags so the hosted catalog path can honour
71
+ * `--print`, `--version` and `--cwd` without polluting the global flag
72
+ * extractor. Unknown flags are silently preserved as positionals (no error),
73
+ * matching the rest of the manifest-catalog command.
74
+ * @param {string[]} rest
75
+ */
76
+ function parseFetchFlags(rest) {
77
+ const out = { positional: /** @type {string[]} */ ([]), print: false, version: null, cwd: null };
78
+ const copy = [...rest];
79
+ while (copy.length) {
80
+ const a = copy.shift();
81
+ if (a === "--print") { out.print = true; continue; }
82
+ if (a === "--version" && copy.length) { out.version = copy.shift(); continue; }
83
+ if (a && a.startsWith("--version=")) { out.version = a.slice("--version=".length); continue; }
84
+ if (a === "--cwd" && copy.length) { out.cwd = copy.shift(); continue; }
85
+ if (a && a.startsWith("--cwd=")) { out.cwd = a.slice("--cwd=".length); continue; }
86
+ out.positional.push(a);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ /**
92
+ * @param {"tool"|"collection"} resource
73
93
  */
74
94
  async function manifestFromHosted(resource, spec, ctx, sub, rest) {
75
95
  const base = ctx.baseUrl;
@@ -106,16 +126,67 @@ async function manifestFromHosted(resource, spec, ctx, sub, rest) {
106
126
  return;
107
127
  }
108
128
  if (sub === "fetch") {
109
- const id = rest[0];
129
+ const flags = parseFetchFlags(rest);
130
+ const id = flags.positional[0];
110
131
  if (!id) {
111
132
  console.error("fetch: id required.");
112
133
  process.exit(1);
113
134
  }
114
- const data = await apiPost(base, "/fetch", { kind: spec.fetchKind, id });
135
+
136
+ const shipRoot = findShipRoot(flags.cwd || process.cwd());
137
+ const wantCache = !!shipRoot;
138
+ const wantStdoutBody = flags.print || !wantCache || ctx.json;
139
+
140
+ if (wantCache) {
141
+ const { content, meta } = await fetchArtifact(
142
+ base,
143
+ spec.fetchKind,
144
+ id,
145
+ flags.version || undefined,
146
+ );
147
+ const version = meta.version || flags.version || "0.0.0";
148
+ const writeRes = writeCached(shipRoot, spec.fetchKind, id, version, content, {
149
+ content_sha256: meta.content_sha256,
150
+ updated_at: meta.updated_at,
151
+ channel: meta.channel,
152
+ version,
153
+ source_url: meta.source_url,
154
+ });
155
+ const rel = path.relative(shipRoot, writeRes.bodyPath) || cachePath(shipRoot, spec.fetchKind, id, version);
156
+ const relDisplay = path.isAbsolute(rel) ? writeRes.bodyPath : rel;
157
+ if (ctx.json) {
158
+ console.log(
159
+ JSON.stringify(
160
+ {
161
+ kind: spec.fetchKind,
162
+ id,
163
+ version,
164
+ content_sha256: meta.content_sha256,
165
+ cached_path: relDisplay,
166
+ content: wantStdoutBody ? content : undefined,
167
+ },
168
+ null,
169
+ 2,
170
+ ),
171
+ );
172
+ } else {
173
+ console.log(`cached: ${spec.fetchKind}/${id}@${version} \u2192 ${relDisplay}`);
174
+ if (wantStdoutBody) {
175
+ console.log(`# ${id}@${version}\n`);
176
+ console.log(content);
177
+ }
178
+ }
179
+ return;
180
+ }
181
+
182
+ const data = await apiPost(base, "/fetch", { kind: spec.fetchKind, id, ...(flags.version ? { version: flags.version } : {}) });
115
183
  if (ctx.json) {
116
184
  console.log(JSON.stringify(data, null, 2));
117
185
  } else {
118
- console.log(`# ${data.title} (${data.id})\n`);
186
+ console.error(
187
+ `note: not in a Ship workspace (no .ship/config.yml found); printing body only. Run 'shipctl config init' to enable caching.`,
188
+ );
189
+ console.log(`# ${data.title || data.id} (${data.id})\n`);
119
190
  console.log(data.content);
120
191
  }
121
192
  return;
@@ -125,22 +196,21 @@ async function manifestFromHosted(resource, spec, ctx, sub, rest) {
125
196
  }
126
197
 
127
198
  /**
128
- * @param {"tool"|"workflow"|"collection"} resource
199
+ * @param {"tool"|"collection"} resource
129
200
  */
130
201
  async function manifestFromDisk(resource, root, spec, ctx, sub, rest) {
131
- const manifestPath = path.join(root, spec.manifestRel);
132
- const raw = fs.readFileSync(manifestPath, "utf8");
133
- /** @type {Record<string, unknown>} */
134
- const data = JSON.parse(raw);
135
- const entries = /** @type {Array<{ id: string; title: string; summary?: string; path: string; tags?: string[] }>} */ (
136
- data[spec.arrayKey] || []
137
- );
202
+ const entries = scanArtifacts(root, spec.fetchKind);
138
203
 
139
204
  if (sub === "list") {
140
205
  if (ctx.json) {
141
- console.log(JSON.stringify(data, null, 2));
206
+ const payload = {
207
+ description: spec.name,
208
+ version: 1,
209
+ [spec.arrayKey]: entries,
210
+ };
211
+ console.log(JSON.stringify(payload, null, 2));
142
212
  } else {
143
- console.log(`${data.description || spec.name}\n`);
213
+ console.log(`${spec.name}\n`);
144
214
  for (const p of entries) {
145
215
  console.log(`- ${p.id}`);
146
216
  console.log(` ${p.title}`);
@@ -161,18 +231,12 @@ async function manifestFromDisk(resource, root, spec, ctx, sub, rest) {
161
231
  console.error(`Unknown id: ${id}`);
162
232
  process.exit(1);
163
233
  }
164
- const abs = path.resolve(root, entry.path);
165
- const rootNorm = root.endsWith(path.sep) ? root.slice(0, -1) : root;
166
- const absNorm = abs.endsWith(path.sep) ? abs.slice(0, -1) : abs;
167
- if (absNorm !== rootNorm && !abs.startsWith(root + path.sep)) {
168
- console.error("Manifest path escapes repository root.");
169
- process.exit(1);
170
- }
171
- if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
234
+ const file = readArtifactFile(root, spec.fetchKind, id);
235
+ if (!file) {
172
236
  console.error(`Missing file: ${entry.path}`);
173
237
  process.exit(1);
174
238
  }
175
- const content = fs.readFileSync(abs, "utf8");
239
+ const content = file.content;
176
240
  if (ctx.json) {
177
241
  console.log(JSON.stringify({ ...entry, content }, null, 2));
178
242
  } else {
@@ -0,0 +1,204 @@
1
+ /**
2
+ * `shipctl migrate` — upgrade `.ship/config.yml` from v1 to v2 (RFC-0007).
3
+ *
4
+ * The command is conservative:
5
+ * - Writes `.ship/config.yml.bak` before touching anything.
6
+ * - Refuses to overwrite without `--yes` unless `--dry-run` is set.
7
+ * - Emits a machine-readable summary under `--json` so adopters can
8
+ * wire it into their own migration runbooks.
9
+ *
10
+ * The actual migration rules live in `cli/lib/config/migrate.mjs`; this
11
+ * command is the I/O and UX layer.
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { readConfig, writeConfig } from "../config/io.mjs";
18
+ import { validateConfig } from "../config/schema.mjs";
19
+ import { migrateV1ToV2 } from "../config/migrate.mjs";
20
+
21
+ const EXIT_OK = 0;
22
+ const EXIT_USAGE = 2;
23
+ const EXIT_NOOP = 0;
24
+ const EXIT_VALIDATION = 1;
25
+
26
+ function printHelp() {
27
+ console.log(`shipctl migrate — upgrade .ship/config.yml to the current schema.
28
+
29
+ USAGE
30
+ shipctl migrate [--dry-run] [--yes] [--json] [--cwd <dir>]
31
+
32
+ FLAGS
33
+ --dry-run Print the proposed new config without writing to disk.
34
+ --yes Skip the interactive confirmation and overwrite in place
35
+ (a backup is always written to .ship/config.yml.bak first).
36
+ --json Emit a structured summary (path, backup, warnings, stubs).
37
+ --cwd <dir> Repo root (default: search upward for .ship/config.yml).
38
+ --help Show this help.
39
+
40
+ EXIT
41
+ 0 migration applied (or already at the latest schema)
42
+ 1 resulting config failed validation
43
+ 2 argument / IO error
44
+ `);
45
+ }
46
+
47
+ /**
48
+ * @param {{json?: boolean, yes?: boolean, dryRun?: boolean}} ctx
49
+ * @param {string[]} rest
50
+ */
51
+ export async function migrateCommand(ctx, rest) {
52
+ const args = parseArgs(rest);
53
+ if (args.help) {
54
+ printHelp();
55
+ process.exit(EXIT_OK);
56
+ }
57
+
58
+ const cwd = args.cwd || process.cwd();
59
+ let read;
60
+ try {
61
+ read = readConfig(cwd);
62
+ } catch (err) {
63
+ die(EXIT_USAGE, err instanceof Error ? err.message : String(err));
64
+ }
65
+
66
+ const { config: src, filePath } = read;
67
+
68
+ let result;
69
+ try {
70
+ result = migrateV1ToV2(src);
71
+ } catch (err) {
72
+ die(EXIT_USAGE, `migrate failed: ${err instanceof Error ? err.message : err}`);
73
+ }
74
+
75
+ if (!result.migrated) {
76
+ const payload = {
77
+ path: filePath,
78
+ migrated: false,
79
+ warnings: result.warnings,
80
+ stub_lanes: [],
81
+ };
82
+ if (ctx.json || args.json) {
83
+ console.log(JSON.stringify(payload, null, 2));
84
+ } else {
85
+ console.log(`${filePath}: already at the latest schema (no changes).`);
86
+ }
87
+ process.exit(EXIT_NOOP);
88
+ }
89
+
90
+ const validation = validateConfig(result.config);
91
+ if (!validation.ok) {
92
+ const msg = [
93
+ "migrate produced an invalid v2 config:",
94
+ ...validation.errors.map((e) => ` - ${e}`),
95
+ ...result.warnings.map((w) => ` (warn) ${w}`),
96
+ ].join("\n");
97
+ die(EXIT_VALIDATION, msg);
98
+ }
99
+
100
+ const yes = ctx.yes || args.yes;
101
+ const dryRun = ctx.dryRun || args.dryRun;
102
+ const backupPath = `${filePath}.bak`;
103
+
104
+ if (dryRun || !yes) {
105
+ const summary = {
106
+ path: filePath,
107
+ migrated: true,
108
+ backup: backupPath,
109
+ dry_run: Boolean(dryRun),
110
+ warnings: result.warnings,
111
+ stub_lanes: result.stub_lanes,
112
+ };
113
+ if (ctx.json || args.json) {
114
+ console.log(
115
+ JSON.stringify(
116
+ { ...summary, proposed_config: result.config },
117
+ null,
118
+ 2,
119
+ ),
120
+ );
121
+ } else {
122
+ console.log(`Proposed migration for ${filePath}:`);
123
+ for (const w of result.warnings) console.log(` - ${w}`);
124
+ if (result.stub_lanes.length) {
125
+ console.log(` - stub lanes (fill before shipping): ${result.stub_lanes.join(", ")}`);
126
+ }
127
+ console.log("");
128
+ console.log(serialiseForDisplay(result.config));
129
+ console.log("");
130
+ if (dryRun) {
131
+ console.log("--dry-run: not writing to disk.");
132
+ } else {
133
+ console.log("Re-run with --yes to apply the migration (writes .bak first).");
134
+ }
135
+ }
136
+ process.exit(EXIT_OK);
137
+ }
138
+
139
+ try {
140
+ fs.copyFileSync(filePath, backupPath);
141
+ writeConfig(filePath, result.config);
142
+ } catch (err) {
143
+ die(EXIT_USAGE, `migrate write failed: ${err instanceof Error ? err.message : err}`);
144
+ }
145
+
146
+ if (ctx.json || args.json) {
147
+ console.log(
148
+ JSON.stringify(
149
+ {
150
+ path: filePath,
151
+ migrated: true,
152
+ backup: backupPath,
153
+ warnings: result.warnings,
154
+ stub_lanes: result.stub_lanes,
155
+ },
156
+ null,
157
+ 2,
158
+ ),
159
+ );
160
+ } else {
161
+ console.log(`Wrote ${filePath} (backup at ${backupPath}).`);
162
+ for (const w of result.warnings) console.log(` - ${w}`);
163
+ if (result.stub_lanes.length) {
164
+ console.log(
165
+ ` - stub lanes to finish: ${result.stub_lanes.join(", ")} — edit the file or rerun 'shipctl init'.`,
166
+ );
167
+ }
168
+ }
169
+ process.exit(EXIT_OK);
170
+ }
171
+
172
+ function parseArgs(rest) {
173
+ const out = { dryRun: false, yes: false, json: false, help: false, cwd: null };
174
+ const copy = [...rest];
175
+ while (copy.length) {
176
+ const a = copy.shift();
177
+ if (a === "--help" || a === "-h") out.help = true;
178
+ else if (a === "--dry-run") out.dryRun = true;
179
+ else if (a === "--yes") out.yes = true;
180
+ else if (a === "--json") out.json = true;
181
+ else if (a === "--cwd" && copy[0] !== undefined) out.cwd = path.resolve(String(copy.shift()));
182
+ else if (a && a.startsWith("--cwd=")) out.cwd = path.resolve(a.slice("--cwd=".length));
183
+ else {
184
+ console.error(`unknown argument: ${a}\nRun: shipctl migrate --help`);
185
+ process.exit(EXIT_USAGE);
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+
191
+ /**
192
+ * Render the config as YAML-ish for display. We deliberately don't
193
+ * import the YAML module here — the real write path already
194
+ * normalises, and `--dry-run` is human-scan territory. JSON is good
195
+ * enough and shows every field unambiguously.
196
+ */
197
+ function serialiseForDisplay(config) {
198
+ return JSON.stringify(config, null, 2);
199
+ }
200
+
201
+ function die(code, msg) {
202
+ console.error(msg);
203
+ process.exit(code);
204
+ }