@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
@@ -0,0 +1,373 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Bootstrap renderer for `shipctl init --bootstrap`.
6
+ *
7
+ * This is intentionally a v1: full preset-body interpretation (parsing
8
+ * `## Bootstrap (files to write)` blocks from adapter artifacts) is TODO
9
+ * and tracked as a templating engine in RFC-0004. For now we:
10
+ *
11
+ * - Always emit a SHIP_BOOTSTRAP_PLAN.md summary so the user has a
12
+ * single actionable next-step document.
13
+ * - For the common `mobile-app + gh-actions + linear` triple we also
14
+ * write minimal CI workflow skeleton, label contract YAML, and
15
+ * `.env.example` placeholders. Other combos fall back to plan-only.
16
+ *
17
+ * @typedef {Object} PlanFile
18
+ * @property {string} path Relative to cwd.
19
+ * @property {string} content
20
+ * @property {"create"|"append"|"patch"} mode
21
+ *
22
+ * @typedef {Object} PlanSummary
23
+ * @property {string[]} notes
24
+ * @property {Array<{path:string, mode:string, detail?:string}>} files
25
+ *
26
+ * @typedef {Object} RenderedPlan
27
+ * @property {PlanFile[]} files
28
+ * @property {PlanSummary} summary
29
+ */
30
+
31
+ const MOBILE_LABELS = [
32
+ "platform:ios",
33
+ "platform:android",
34
+ "store:review",
35
+ "flag:behind",
36
+ "flag:ahead",
37
+ "change-record",
38
+ "blocked",
39
+ "preview:ready",
40
+ ];
41
+
42
+ const ENV_EXAMPLE_MARKER_START = "# --- ship-managed ---";
43
+ const ENV_EXAMPLE_MARKER_END = "# --- end ship-managed ---";
44
+
45
+ /**
46
+ * @param {object} cfg
47
+ * @returns {RenderedPlan}
48
+ */
49
+ export function renderMobileAppGhActionsLinear(cfg) {
50
+ const preset = cfg.stack?.preset || "mobile-app";
51
+ const tracker = cfg.stack?.tracker || "linear";
52
+ const ci = cfg.stack?.ci || "gh-actions";
53
+ const agents = Array.isArray(cfg.stack?.agents) ? cfg.stack.agents : [];
54
+
55
+ const workflow = `# ship-managed: workflow
56
+ # Skeleton written by \`shipctl init --bootstrap\`.
57
+ # shipctl sync (Epic 7) will fill in job bodies from preset:preset-${preset}.
58
+ name: ship-pilot
59
+ on:
60
+ pull_request:
61
+ push:
62
+ branches: [main]
63
+
64
+ jobs:
65
+ # ship-managed: workflow
66
+ lint:
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ # TODO: language-specific lint wired by shipctl sync
71
+ - run: echo "lint: placeholder"
72
+
73
+ # ship-managed: workflow
74
+ build-ios:
75
+ runs-on: macos-latest
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ # TODO: EAS / Fastlane build steps wired by shipctl sync
79
+ - run: echo "build-ios: placeholder"
80
+
81
+ # ship-managed: workflow
82
+ build-android:
83
+ runs-on: ubuntu-latest
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ # TODO: Gradle / EAS build steps wired by shipctl sync
87
+ - run: echo "build-android: placeholder"
88
+ `;
89
+
90
+ const labelsYml = `# ship-managed: labels
91
+ # Synced to the tracker (${tracker}) by \`shipctl verify\`.
92
+ version: 1
93
+ preset: ${preset}
94
+ labels:
95
+ ${MOBILE_LABELS.map((l) => ` - name: "${l}"`).join("\n")}
96
+ `;
97
+
98
+ const envBlock = `${ENV_EXAMPLE_MARKER_START}
99
+ # Placeholders for ${preset} / ${tracker} / ${ci}.
100
+ # Fill these in .env.local (not committed) or your platform secret store.
101
+ LINEAR_API_KEY=
102
+ LINEAR_TEAM_ID=
103
+ GITHUB_TOKEN=
104
+ EXPO_TOKEN=
105
+ SENTRY_AUTH_TOKEN=
106
+ ${ENV_EXAMPLE_MARKER_END}
107
+ `;
108
+
109
+ const plan = renderAdoptionMinimum(cfg, {
110
+ extraNotes: [
111
+ "Mobile-app pilot scaffolding was emitted (gh-actions + linear).",
112
+ "See `.github/workflows/ship-pilot.yml`, `.ship/labels.yml`, `.env.example`.",
113
+ ],
114
+ });
115
+
116
+ const files = [
117
+ ...plan.files,
118
+ {
119
+ path: ".github/workflows/ship-pilot.yml",
120
+ content: workflow,
121
+ mode: /** @type {"create"} */ ("create"),
122
+ },
123
+ {
124
+ path: ".ship/labels.yml",
125
+ content: labelsYml,
126
+ mode: /** @type {"create"} */ ("create"),
127
+ },
128
+ {
129
+ path: ".env.example",
130
+ content: envBlock,
131
+ mode: /** @type {"append"} */ ("append"),
132
+ },
133
+ ];
134
+
135
+ const summary = {
136
+ notes: [
137
+ ...plan.summary.notes,
138
+ `bootstrap: mobile-app + ${ci} + ${tracker} triple rendered`,
139
+ `agents: ${agents.join(", ") || "(none)"}`,
140
+ ],
141
+ files: files.map((f) => ({
142
+ path: f.path,
143
+ mode: f.mode,
144
+ detail:
145
+ f.path === ".ship/labels.yml"
146
+ ? `${MOBILE_LABELS.length} labels`
147
+ : f.path === ".env.example"
148
+ ? "5 placeholders"
149
+ : undefined,
150
+ })),
151
+ };
152
+
153
+ return { files, summary };
154
+ }
155
+
156
+ /**
157
+ * Generate just the SHIP_BOOTSTRAP_PLAN.md summary. Used as the v1
158
+ * fallback for preset / CI / tracker combos that don't have a specific
159
+ * renderer yet.
160
+ *
161
+ * @param {object} cfg
162
+ * @param {{extraNotes?:string[]}} [opts]
163
+ * @returns {RenderedPlan}
164
+ */
165
+ export function renderAdoptionMinimum(cfg, opts = {}) {
166
+ const stack = cfg.stack || {};
167
+ const preset = stack.preset || "adoption-minimum";
168
+ const tracker = stack.tracker || "none";
169
+ const ci = stack.ci || "manual";
170
+ const agents = Array.isArray(stack.agents) ? stack.agents : [];
171
+ const language = stack.language || "multi";
172
+ const channel = cfg.api?.channel || "stable";
173
+ const telemetry = cfg.telemetry?.share === true ? "on" : "off";
174
+ const extraNotes = opts.extraNotes || [];
175
+
176
+ const todos = buildTodoList({ preset, ci, tracker, agents });
177
+ const recommendedTools = buildRecommendedTools({ preset });
178
+ const recommendedSecrets = buildRecommendedSecrets({ tracker, ci });
179
+
180
+ const body = `# Ship bootstrap plan
181
+
182
+ _Generated by \`shipctl init --bootstrap\` on ${new Date().toISOString()}._
183
+
184
+ ## Chosen stack
185
+
186
+ - **preset**: \`${preset}\`
187
+ - **tracker**: \`${tracker}\`
188
+ - **ci**: \`${ci}\`
189
+ - **language**: \`${language}\`
190
+ - **agents**: ${agents.length ? agents.map((a) => `\`${a}\``).join(", ") : "_(none)_"}
191
+ - **channel**: \`${channel}\`
192
+ - **telemetry**: \`${telemetry}\`
193
+
194
+ ## Recommended tools
195
+
196
+ ${recommendedTools.map((t) => `- ${t}`).join("\n") || "_(none for this preset yet — fill manually.)_"}
197
+
198
+ ## Recommended secrets / env
199
+
200
+ ${recommendedSecrets.map((s) => `- \`${s}\``).join("\n") || "_(none required.)_"}
201
+
202
+ ## Files to create / review
203
+
204
+ ${todos.map((t) => `- [ ] ${t}`).join("\n")}
205
+
206
+ ## Next steps
207
+
208
+ 1. \`shipctl sync\` to refresh \`.ship/cache/\` against the Ship API.
209
+ 2. \`shipctl verify\` to confirm tracker labels / CI secrets / rules markers.
210
+ 3. Open the preset artifact for full details:
211
+ \`shipctl collection show preset-${preset}\`.
212
+
213
+ ${
214
+ extraNotes.length
215
+ ? `## Notes\n\n${extraNotes.map((n) => `- ${n}`).join("\n")}\n`
216
+ : ""
217
+ }`;
218
+
219
+ return {
220
+ files: [
221
+ {
222
+ path: "SHIP_BOOTSTRAP_PLAN.md",
223
+ content: body,
224
+ mode: /** @type {"create"} */ ("create"),
225
+ },
226
+ ],
227
+ summary: {
228
+ notes: ["bootstrap: plan-only fallback rendered (SHIP_BOOTSTRAP_PLAN.md)"],
229
+ files: [
230
+ {
231
+ path: "SHIP_BOOTSTRAP_PLAN.md",
232
+ mode: "create",
233
+ detail: `${todos.length} todo items`,
234
+ },
235
+ ],
236
+ },
237
+ };
238
+ }
239
+
240
+ function buildTodoList({ preset, ci, tracker, agents }) {
241
+ const todos = [];
242
+ if (ci === "gh-actions") {
243
+ todos.push("Confirm `.github/workflows/ship-pilot.yml` skeleton (shipctl sync will fill the job bodies).");
244
+ } else {
245
+ todos.push(`Author the CI workflow skeleton for \`${ci}\` manually (no renderer yet).`);
246
+ }
247
+ if (tracker !== "none") {
248
+ todos.push(`Create the label contract for \`${tracker}\` (see preset:preset-${preset} for the label set).`);
249
+ }
250
+ for (const a of agents) {
251
+ todos.push(`Agent rules for \`${a}\`: install via \`shipctl init --copy-rules --agents ${a}\`.`);
252
+ }
253
+ todos.push("Populate `.env.example` / secret store with the secrets listed above.");
254
+ todos.push("Run `shipctl verify` after the above to confirm the stack.");
255
+ return todos;
256
+ }
257
+
258
+ function buildRecommendedTools({ preset }) {
259
+ const common = ["`shipctl doctor` — inspect repo and reconcile stack"];
260
+ const byPreset = {
261
+ "mobile-app": [
262
+ "EAS Build / Fastlane for iOS + Android signed builds",
263
+ "Detox or Maestro for device-farm E2E",
264
+ "Expo Updates or CodePush for OTA patches",
265
+ ],
266
+ "web-app": [
267
+ "Playwright (hosted) for PR preview E2E",
268
+ "Preview deployments (Vercel / Netlify / Fly) per PR",
269
+ ],
270
+ "api-backend": [
271
+ "Contract tests (Pact / OpenAPI diff)",
272
+ "Migration discipline (Atlas / Liquibase)",
273
+ ],
274
+ cli: ["Cross-platform release matrix (GoReleaser / pkg / esbuild)"],
275
+ monorepo: ["Turborepo / Nx / pnpm workspaces for per-package CI"],
276
+ "adoption-minimum": [],
277
+ };
278
+ return [...common, ...(byPreset[preset] || [])];
279
+ }
280
+
281
+ function buildRecommendedSecrets({ tracker, ci }) {
282
+ const secrets = new Set();
283
+ if (tracker === "linear") secrets.add("LINEAR_API_KEY").add("LINEAR_TEAM_ID");
284
+ if (tracker === "jira") secrets.add("JIRA_API_TOKEN").add("JIRA_EMAIL");
285
+ if (tracker === "github-issues") secrets.add("GITHUB_TOKEN");
286
+ if (ci === "gh-actions") secrets.add("GITHUB_TOKEN");
287
+ if (ci === "circleci") secrets.add("CIRCLE_TOKEN");
288
+ return [...secrets];
289
+ }
290
+
291
+ /**
292
+ * Pick the right renderer for this stack. v1 only special-cases
293
+ * `mobile-app + gh-actions + linear`.
294
+ *
295
+ * @param {object} cfg
296
+ * @returns {RenderedPlan}
297
+ */
298
+ export function renderPlan(cfg /*, presetArtifact */) {
299
+ const preset = cfg.stack?.preset;
300
+ const tracker = cfg.stack?.tracker;
301
+ const ci = cfg.stack?.ci;
302
+
303
+ if (preset === "mobile-app" && ci === "gh-actions" && tracker === "linear") {
304
+ return renderMobileAppGhActionsLinear(cfg);
305
+ }
306
+ return renderAdoptionMinimum(cfg);
307
+ }
308
+
309
+ /**
310
+ * Apply a plan to disk. Append-mode files use marker-guarded idempotency.
311
+ * Create-mode files are skipped when they already exist unless `force`
312
+ * is set (we never silently stomp a user's file).
313
+ *
314
+ * @param {string} cwd
315
+ * @param {RenderedPlan} plan
316
+ * @param {{dryRun?:boolean, force?:boolean}} [opts]
317
+ * @returns {Array<{path:string, action:"wrote"|"skipped"|"appended"|"would_write"|"would_skip"|"would_append"}>}
318
+ */
319
+ export function applyPlan(cwd, plan, opts = {}) {
320
+ const { dryRun = false, force = false } = opts;
321
+ /** @type {Array<{path:string, action:string}>} */
322
+ const results = [];
323
+
324
+ for (const file of plan.files) {
325
+ const abs = path.join(cwd, file.path);
326
+
327
+ if (file.mode === "append") {
328
+ const current = fs.existsSync(abs) ? fs.readFileSync(abs, "utf8") : "";
329
+ if (current.includes(ENV_EXAMPLE_MARKER_START)) {
330
+ results.push({ path: file.path, action: dryRun ? "would_skip" : "skipped" });
331
+ continue;
332
+ }
333
+ if (dryRun) {
334
+ results.push({ path: file.path, action: "would_append" });
335
+ continue;
336
+ }
337
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
338
+ const prefix = current.length && !current.endsWith("\n") ? "\n" : "";
339
+ fs.writeFileSync(abs, current + prefix + file.content, "utf8");
340
+ results.push({ path: file.path, action: "appended" });
341
+ continue;
342
+ }
343
+
344
+ if (fs.existsSync(abs) && !force) {
345
+ results.push({ path: file.path, action: dryRun ? "would_skip" : "skipped" });
346
+ continue;
347
+ }
348
+ if (dryRun) {
349
+ results.push({ path: file.path, action: "would_write" });
350
+ continue;
351
+ }
352
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
353
+ fs.writeFileSync(abs, file.content, "utf8");
354
+ results.push({ path: file.path, action: "wrote" });
355
+ }
356
+
357
+ return results;
358
+ }
359
+
360
+ /**
361
+ * Top-level entry point used by `shipctl init --bootstrap`.
362
+ *
363
+ * @param {string} cwd
364
+ * @param {object} config
365
+ * @param {object|null} presetArtifact Reserved for v2 when we parse the preset body.
366
+ * @param {Array<object>} _adapters Reserved for v2.
367
+ * @param {{dryRun?:boolean, force?:boolean}} [opts]
368
+ */
369
+ export function renderBootstrap(cwd, config, presetArtifact, _adapters, opts = {}) {
370
+ const plan = renderPlan(config, presetArtifact);
371
+ const results = applyPlan(cwd, plan, opts);
372
+ return { plan, results };
373
+ }