@elmundi/ship-cli 0.8.1 → 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.
Files changed (78) hide show
  1. package/README.md +651 -25
  2. package/bin/shipctl.mjs +168 -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 +422 -0
  31. package/lib/cache/store.mjs +422 -0
  32. package/lib/commands/bootstrap.mjs +4 -0
  33. package/lib/commands/callback.mjs +742 -0
  34. package/lib/commands/config.mjs +257 -0
  35. package/lib/commands/docs.mjs +4 -4
  36. package/lib/commands/doctor.mjs +583 -0
  37. package/lib/commands/feedback.mjs +355 -0
  38. package/lib/commands/help.mjs +159 -24
  39. package/lib/commands/init.mjs +830 -158
  40. package/lib/commands/kickoff.mjs +192 -0
  41. package/lib/commands/knowledge.mjs +562 -0
  42. package/lib/commands/lanes.mjs +527 -0
  43. package/lib/commands/manifest-catalog.mjs +106 -42
  44. package/lib/commands/migrate.mjs +204 -0
  45. package/lib/commands/new.mjs +452 -0
  46. package/lib/commands/patterns.mjs +14 -48
  47. package/lib/commands/run.mjs +857 -0
  48. package/lib/commands/search.mjs +2 -2
  49. package/lib/commands/sync.mjs +824 -0
  50. package/lib/commands/telemetry.mjs +390 -0
  51. package/lib/commands/trigger.mjs +196 -0
  52. package/lib/commands/verify.mjs +187 -0
  53. package/lib/config/io.mjs +232 -0
  54. package/lib/config/migrate.mjs +223 -0
  55. package/lib/config/schema.mjs +901 -0
  56. package/lib/detect.mjs +162 -19
  57. package/lib/feedback/drafts.mjs +129 -0
  58. package/lib/find-ship-root.mjs +16 -10
  59. package/lib/http.mjs +237 -11
  60. package/lib/state/idempotency.mjs +183 -0
  61. package/lib/state/lockfile.mjs +180 -0
  62. package/lib/telemetry/outbox.mjs +224 -0
  63. package/lib/templates.mjs +53 -65
  64. package/lib/verify/checks/agents-on-disk.mjs +58 -0
  65. package/lib/verify/checks/api-reachable.mjs +39 -0
  66. package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
  67. package/lib/verify/checks/bootstrap-files.mjs +67 -0
  68. package/lib/verify/checks/cache-integrity.mjs +51 -0
  69. package/lib/verify/checks/ci-secrets.mjs +86 -0
  70. package/lib/verify/checks/config-present.mjs +39 -0
  71. package/lib/verify/checks/gitignore-cache.mjs +51 -0
  72. package/lib/verify/checks/rules-markers.mjs +135 -0
  73. package/lib/verify/checks/stack-enums.mjs +33 -0
  74. package/lib/verify/checks/tracker-labels.mjs +91 -0
  75. package/lib/verify/registry.mjs +120 -0
  76. package/lib/version.mjs +34 -0
  77. package/package.json +10 -3
  78. package/bin/ship.mjs +0 -68
@@ -0,0 +1,135 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { KNOWN_AGENTS } from "../../detect.mjs";
4
+ import { listCached, readCachedArtifact } from "../../cache/store.mjs";
5
+
6
+ export const id = "rules-markers";
7
+ export const category = "local";
8
+ export const description = "Agent rule files contain current artifacts-protocol markers";
9
+
10
+ const RULE_MARKER = "<!-- ship-cli: artifacts-protocol v1 -->";
11
+ const FOOTER_PREFIX = "<!-- ship-cli: installed-from ";
12
+ const FOOTER_SUFFIX = " -->";
13
+
14
+ function readFooter(body) {
15
+ const idx = body.lastIndexOf(FOOTER_PREFIX);
16
+ if (idx < 0) return null;
17
+ const end = body.indexOf(FOOTER_SUFFIX, idx);
18
+ if (end < 0) return null;
19
+ return body.slice(idx + FOOTER_PREFIX.length, end).trim();
20
+ }
21
+
22
+ /**
23
+ * @param {import("../registry.mjs").CheckContext} ctx
24
+ */
25
+ export async function run(ctx) {
26
+ const agents = (ctx.config && ctx.config.stack && Array.isArray(ctx.config.stack.agents))
27
+ ? ctx.config.stack.agents
28
+ : [];
29
+ if (!agents.length) {
30
+ return { status: "skip", detail: "stack.agents is empty" };
31
+ }
32
+
33
+ let cache = [];
34
+ try {
35
+ cache = listCached(ctx.cwd);
36
+ } catch {
37
+ cache = [];
38
+ }
39
+
40
+ const rows = [];
41
+ let hasFail = false;
42
+ let hasWarn = false;
43
+
44
+ for (const agent of agents) {
45
+ const spec = KNOWN_AGENTS[agent];
46
+ // Prefer the install_target declared by the cached agent-rules artifact;
47
+ // fall back to the hardcoded KNOWN_AGENTS mapping for graceful
48
+ // degradation when the cache hasn't been populated yet (RFC-0004).
49
+ let rel = null;
50
+ let cachedFm = null;
51
+ try {
52
+ cachedFm = readCachedArtifact(ctx.cwd, "collection", `agent-rules-${agent}`);
53
+ } catch {
54
+ cachedFm = null;
55
+ }
56
+ if (!cachedFm) {
57
+ rows.push({
58
+ agent,
59
+ status: "warn",
60
+ detail: `no cached agent-rules-${agent}; run shipctl sync`,
61
+ });
62
+ hasWarn = true;
63
+ continue;
64
+ }
65
+ // v1 single-file artifacts kept install_target at the top level; v2
66
+ // moved it under `spec:`. Honour both.
67
+ const topLevel = cachedFm.fm && typeof cachedFm.fm.install_target === "string"
68
+ ? cachedFm.fm.install_target.trim()
69
+ : "";
70
+ const nested = cachedFm.spec && typeof cachedFm.spec.install_target === "string"
71
+ ? cachedFm.spec.install_target.trim()
72
+ : "";
73
+ const installTarget = topLevel || nested;
74
+ if (installTarget) {
75
+ rel = installTarget;
76
+ } else if (spec) {
77
+ rel = path.join(...spec.targetRel);
78
+ } else {
79
+ rows.push({ agent, status: "warn", detail: `unknown agent id '${agent}' (no install_target and no KNOWN_AGENTS entry)` });
80
+ hasWarn = true;
81
+ continue;
82
+ }
83
+
84
+ const abs = path.join(ctx.cwd, rel);
85
+ if (!fs.existsSync(abs)) {
86
+ rows.push({ agent, status: "fail", detail: `missing rule file ${rel}` });
87
+ hasFail = true;
88
+ continue;
89
+ }
90
+ const body = fs.readFileSync(abs, "utf8");
91
+ if (!body.includes(RULE_MARKER)) {
92
+ rows.push({ agent, status: "fail", detail: `${rel} has no '${RULE_MARKER}' marker` });
93
+ hasFail = true;
94
+ continue;
95
+ }
96
+ const footer = readFooter(body);
97
+ if (!footer) {
98
+ rows.push({ agent, status: "fail", detail: `${rel} has no 'installed-from' footer` });
99
+ hasFail = true;
100
+ continue;
101
+ }
102
+
103
+ // footer looks like "collection/agent-rules-cursor@1.0.1"
104
+ const m = /^collection\/agent-rules-[a-z0-9-]+@([0-9A-Za-z.\-+]+)$/.exec(footer);
105
+ if (!m) {
106
+ rows.push({ agent, status: "warn", detail: `${rel} footer is '${footer}' (non-standard format)` });
107
+ hasWarn = true;
108
+ continue;
109
+ }
110
+ const installedVersion = m[1];
111
+ const cached = cache.find(
112
+ (c) => c.kind === "collection" && c.id === `agent-rules-${agent}`,
113
+ );
114
+ if (cached && cached.version && cached.version !== installedVersion) {
115
+ rows.push({
116
+ agent,
117
+ status: "warn",
118
+ detail: `${rel} footer @${installedVersion}, cache has @${cached.version} — run 'shipctl init --copy-rules'`,
119
+ });
120
+ hasWarn = true;
121
+ } else {
122
+ rows.push({ agent, status: "pass", detail: `${rel} @${installedVersion}` });
123
+ }
124
+ }
125
+
126
+ const overall = hasFail ? "fail" : hasWarn ? "warn" : "pass";
127
+ const summary = rows
128
+ .filter((r) => r.status !== "pass")
129
+ .map((r) => `${r.agent}: ${r.detail}`)
130
+ .join("; ");
131
+ const detail = overall === "pass"
132
+ ? `all ${rows.length} agent rule files have correct markers`
133
+ : summary || `${rows.length} agent rule files inspected`;
134
+ return { status: overall, detail, data: { rows } };
135
+ }
@@ -0,0 +1,33 @@
1
+ import { validateConfig } from "../../config/schema.mjs";
2
+
3
+ export const id = "stack-enums";
4
+ export const category = "config";
5
+ export const description = "stack.* values are valid enum members";
6
+
7
+ /**
8
+ * @param {import("../registry.mjs").CheckContext} ctx
9
+ */
10
+ export async function run(ctx) {
11
+ if (!ctx.config) {
12
+ return { status: "skip", detail: "no config loaded" };
13
+ }
14
+ const res = validateConfig(ctx.config);
15
+ if (!res.ok) {
16
+ const stackErrors = res.errors.filter((e) => e.startsWith("stack."));
17
+ const display = stackErrors.length ? stackErrors : res.errors;
18
+ return {
19
+ status: "fail",
20
+ detail: display[0],
21
+ data: { errors: res.errors, warnings: res.warnings },
22
+ };
23
+ }
24
+ const warns = (res.warnings || []).filter((w) => w.startsWith("stack."));
25
+ if (warns.length) {
26
+ return { status: "warn", detail: warns[0], data: { warnings: res.warnings } };
27
+ }
28
+ const stack = ctx.config.stack || {};
29
+ return {
30
+ status: "pass",
31
+ detail: `tracker=${stack.tracker || "?"}, ci=${stack.ci || "?"}, preset=${stack.preset || "?"}, language=${stack.language || "?"}, agents=[${(stack.agents || []).join(",") || ""}]`,
32
+ };
33
+ }
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+
5
+ export const id = "tracker-labels";
6
+ export const category = "network";
7
+ export const description = "Tracker labels match .ship/labels.yml";
8
+
9
+ async function fetchLinearLabels(apiKey) {
10
+ const body = JSON.stringify({
11
+ query: "query { issueLabels(first: 250) { nodes { id name } } }",
12
+ });
13
+ const res = await fetch("https://api.linear.app/graphql", {
14
+ method: "POST",
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ Accept: "application/json",
18
+ Authorization: apiKey,
19
+ },
20
+ body,
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(`Linear API ${res.status} ${res.statusText}`);
24
+ }
25
+ const data = await res.json();
26
+ const nodes = data?.data?.issueLabels?.nodes || [];
27
+ return nodes.map((n) => String(n.name));
28
+ }
29
+
30
+ function readLabelsYaml(cwd) {
31
+ const rel = path.join(".ship", "labels.yml");
32
+ const abs = path.join(cwd, rel);
33
+ if (!fs.existsSync(abs)) return null;
34
+ try {
35
+ const doc = YAML.parse(fs.readFileSync(abs, "utf8"));
36
+ const arr = Array.isArray(doc?.labels) ? doc.labels : [];
37
+ return arr.map((l) => (typeof l === "string" ? l : l && l.name)).filter(Boolean);
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @param {import("../registry.mjs").CheckContext} ctx
45
+ */
46
+ export async function run(ctx) {
47
+ const tracker = ctx.config && ctx.config.stack && ctx.config.stack.tracker;
48
+ if (!tracker || tracker === "none") {
49
+ return { status: "skip", detail: "stack.tracker is none" };
50
+ }
51
+ if (tracker !== "linear") {
52
+ return {
53
+ status: "skip",
54
+ detail: `tracker=${tracker} label verification not implemented yet`,
55
+ };
56
+ }
57
+
58
+ const declared = readLabelsYaml(ctx.cwd);
59
+ if (declared == null) {
60
+ return { status: "skip", detail: ".ship/labels.yml not present" };
61
+ }
62
+
63
+ const apiKey = process.env.LINEAR_API_KEY;
64
+ if (!apiKey) {
65
+ return {
66
+ status: "skip",
67
+ detail: "LINEAR_API_KEY not set — skipping live Linear label fetch",
68
+ };
69
+ }
70
+
71
+ let remote;
72
+ try {
73
+ remote = await fetchLinearLabels(apiKey);
74
+ } catch (e) {
75
+ return { status: "warn", detail: `Linear API call failed: ${e.message}` };
76
+ }
77
+
78
+ const remoteSet = new Set(remote);
79
+ const missing = declared.filter((n) => !remoteSet.has(n));
80
+ if (!missing.length) {
81
+ return {
82
+ status: "pass",
83
+ detail: `all ${declared.length} declared labels exist on Linear`,
84
+ };
85
+ }
86
+ return {
87
+ status: "warn",
88
+ detail: `missing labels on Linear: ${missing.join(", ")}`,
89
+ data: { missing, declared, remote_count: remote.length },
90
+ };
91
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Verify registry: central import point for all Ship verify checks.
3
+ *
4
+ * @typedef {Object} CheckContext
5
+ * @property {string} cwd
6
+ * @property {object|null} config Parsed .ship/config.yml (or null if missing).
7
+ * @property {object|null} [inventory] Optional .ship/inventory.json snapshot.
8
+ * @property {string} [baseUrl] Effective methodology API base URL.
9
+ * @property {(msg:string)=>void} [logger]
10
+ *
11
+ * @typedef {"pass"|"warn"|"fail"|"skip"} CheckStatus
12
+ *
13
+ * @typedef {Object} CheckResult
14
+ * @property {CheckStatus} status
15
+ * @property {string} detail
16
+ * @property {object} [data]
17
+ *
18
+ * @typedef {Object} Check
19
+ * @property {string} id
20
+ * @property {"local"|"config"|"network"} category
21
+ * @property {string} description
22
+ * @property {(ctx:CheckContext)=>Promise<CheckResult>} run
23
+ */
24
+
25
+ import * as configPresent from "./checks/config-present.mjs";
26
+ import * as gitignoreCache from "./checks/gitignore-cache.mjs";
27
+ import * as rulesMarkers from "./checks/rules-markers.mjs";
28
+ import * as cacheIntegrity from "./checks/cache-integrity.mjs";
29
+ import * as bootstrapFiles from "./checks/bootstrap-files.mjs";
30
+ import * as stackEnums from "./checks/stack-enums.mjs";
31
+ import * as agentsOnDisk from "./checks/agents-on-disk.mjs";
32
+ import * as apiReachable from "./checks/api-reachable.mjs";
33
+ import * as artifactsUpToDate from "./checks/artifacts-up-to-date.mjs";
34
+ import * as trackerLabels from "./checks/tracker-labels.mjs";
35
+ import * as ciSecrets from "./checks/ci-secrets.mjs";
36
+
37
+ /**
38
+ * Ordered list of checks. Order governs how they appear in `shipctl verify`
39
+ * output; within a category we keep a stable human-friendly grouping.
40
+ * @type {Check[]}
41
+ */
42
+ const CHECKS = [
43
+ configPresent,
44
+ gitignoreCache,
45
+ stackEnums,
46
+ rulesMarkers,
47
+ cacheIntegrity,
48
+ bootstrapFiles,
49
+ agentsOnDisk,
50
+ apiReachable,
51
+ artifactsUpToDate,
52
+ trackerLabels,
53
+ ciSecrets,
54
+ ];
55
+
56
+ export function allChecks() {
57
+ return CHECKS.slice();
58
+ }
59
+
60
+ /**
61
+ * @param {CheckContext} ctx
62
+ * @param {{filter?:string[]|null, noNetwork?:boolean}} [opts]
63
+ * @returns {Promise<Array<{id:string, category:string, description:string, status:CheckStatus, detail:string, data?:object, duration_ms:number}>>}
64
+ */
65
+ export async function runChecks(ctx, opts = {}) {
66
+ const { filter = null, noNetwork = false } = opts;
67
+ const wantSet = filter && filter.length
68
+ ? new Set(filter.map((s) => String(s).trim()).filter(Boolean))
69
+ : null;
70
+
71
+ const out = [];
72
+ for (const check of CHECKS) {
73
+ if (wantSet && !wantSet.has(check.id)) continue;
74
+ if (noNetwork && check.category === "network") {
75
+ out.push({
76
+ id: check.id,
77
+ category: check.category,
78
+ description: check.description,
79
+ status: "skip",
80
+ detail: "skipped (--no-network)",
81
+ duration_ms: 0,
82
+ });
83
+ continue;
84
+ }
85
+ const started = Date.now();
86
+ let res;
87
+ try {
88
+ res = await check.run(ctx);
89
+ } catch (e) {
90
+ res = {
91
+ status: "fail",
92
+ detail: `check threw: ${e instanceof Error ? e.message : String(e)}`,
93
+ };
94
+ }
95
+ out.push({
96
+ id: check.id,
97
+ category: check.category,
98
+ description: check.description,
99
+ status: res.status || "fail",
100
+ detail: res.detail || "",
101
+ data: res.data,
102
+ duration_ms: Date.now() - started,
103
+ });
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Summarise a list of CheckResult rows.
110
+ */
111
+ export function summarize(rows) {
112
+ const summary = { total: rows.length, pass: 0, warn: 0, fail: 0, skip: 0 };
113
+ for (const r of rows) {
114
+ if (r.status === "pass") summary.pass += 1;
115
+ else if (r.status === "warn") summary.warn += 1;
116
+ else if (r.status === "fail") summary.fail += 1;
117
+ else summary.skip += 1;
118
+ }
119
+ return summary;
120
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Read the CLI version straight from the published package.json so we never
3
+ * end up with a hardcoded constant that drifts from npm. The value is also
4
+ * the canonical Ship release version (kept in sync with the root VERSION
5
+ * file by `scripts/version.mjs` — see top-level README "Versioning").
6
+ *
7
+ * Used by:
8
+ * - `shipctl --version` / `shipctl -v` / `shipctl version`
9
+ * - the `User-Agent` header on every outbound HTTP request, so the
10
+ * methodology API can correlate adoption metrics with the client release.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const pkgPath = join(__dirname, "..", "package.json");
18
+
19
+ let cached = null;
20
+
21
+ export function getCliVersion() {
22
+ if (cached) return cached;
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
25
+ cached = String(pkg.version || "0.0.0-unknown");
26
+ } catch {
27
+ cached = "0.0.0-unknown";
28
+ }
29
+ return cached;
30
+ }
31
+
32
+ export function getUserAgent() {
33
+ return `shipctl/${getCliVersion()} (+https://github.com/ElMundiUA/ship)`;
34
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@elmundi/ship-cli",
3
- "version": "0.8.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
- "description": "Ship CLI methodology search, docs fetch/feedback, catalog commands (pattern/tool/workflow/collection), and agent init",
5
+ "description": "Ship CLI: bootstrap a repo, sync the Plays catalog, run Automations, report Runs.",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Denys Kuzin",
8
8
  "repository": {
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "keywords": [
21
21
  "ship",
22
+ "shipctl",
22
23
  "cli",
23
24
  "agents",
24
25
  "methodology"
@@ -31,6 +32,12 @@
31
32
  "access": "public"
32
33
  },
33
34
  "bin": {
34
- "ship": "./bin/ship.mjs"
35
+ "shipctl": "./bin/shipctl.mjs"
36
+ },
37
+ "scripts": {
38
+ "test": "node --test tests/*.test.mjs"
39
+ },
40
+ "dependencies": {
41
+ "yaml": "^2.8.3"
35
42
  }
36
43
  }
package/bin/ship.mjs DELETED
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env node
2
- import { extractGlobalArgv } from "../lib/config.mjs";
3
- import { docsCommand } from "../lib/commands/docs.mjs";
4
- import { searchCommand } from "../lib/commands/search.mjs";
5
- import { patternCommand } from "../lib/commands/patterns.mjs";
6
- import { resourceManifestCommand } from "../lib/commands/manifest-catalog.mjs";
7
- import { printHelp } from "../lib/commands/help.mjs";
8
- import { initCommand } from "../lib/commands/init.mjs";
9
-
10
- const raw = process.argv.slice(2);
11
- const { _, ...g } = extractGlobalArgv(raw);
12
- const ctx = {
13
- baseUrl: g.baseUrl,
14
- json: g.json,
15
- yes: g.yes,
16
- force: g.force,
17
- dryRun: g.dryRun,
18
- };
19
-
20
- const [cmd, ...rest] = _;
21
-
22
- try {
23
- if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") {
24
- printHelp();
25
- process.exit(0);
26
- }
27
-
28
- if (cmd === "search") {
29
- await searchCommand(ctx, rest);
30
- process.exit(0);
31
- }
32
-
33
- if (cmd === "docs") {
34
- await docsCommand(ctx, rest);
35
- process.exit(0);
36
- }
37
-
38
- if (cmd === "pattern" || cmd === "patterns") {
39
- await patternCommand(ctx, rest);
40
- process.exit(0);
41
- }
42
-
43
- if (cmd === "tool" || cmd === "tools") {
44
- await resourceManifestCommand("tool", ctx, rest);
45
- process.exit(0);
46
- }
47
-
48
- if (cmd === "workflow" || cmd === "workflows") {
49
- await resourceManifestCommand("workflow", ctx, rest);
50
- process.exit(0);
51
- }
52
-
53
- if (cmd === "collection" || cmd === "collections") {
54
- await resourceManifestCommand("collection", ctx, rest);
55
- process.exit(0);
56
- }
57
-
58
- if (cmd === "init") {
59
- await initCommand(ctx, rest);
60
- process.exit(0);
61
- }
62
-
63
- console.error(`Unknown command: ${cmd}\nRun: ship help`);
64
- process.exit(1);
65
- } catch (err) {
66
- console.error(err instanceof Error ? err.message : err);
67
- process.exit(1);
68
- }