@elmundi/ship-cli 0.8.0 → 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.
- package/README.md +456 -32
- package/bin/shipctl.mjs +165 -0
- package/lib/adapters/_fs.mjs +165 -0
- package/lib/adapters/agents/index.mjs +26 -0
- package/lib/adapters/ci/azure-pipelines.mjs +23 -0
- package/lib/adapters/ci/buildkite.mjs +24 -0
- package/lib/adapters/ci/circleci.mjs +23 -0
- package/lib/adapters/ci/gh-actions.mjs +29 -0
- package/lib/adapters/ci/gitlab-ci.mjs +23 -0
- package/lib/adapters/ci/jenkins.mjs +23 -0
- package/lib/adapters/ci/manual.mjs +18 -0
- package/lib/adapters/index.mjs +122 -0
- package/lib/adapters/language/dart.mjs +23 -0
- package/lib/adapters/language/go.mjs +23 -0
- package/lib/adapters/language/java.mjs +27 -0
- package/lib/adapters/language/js.mjs +32 -0
- package/lib/adapters/language/kotlin.mjs +48 -0
- package/lib/adapters/language/py.mjs +34 -0
- package/lib/adapters/language/rust.mjs +23 -0
- package/lib/adapters/language/swift.mjs +37 -0
- package/lib/adapters/language/ts.mjs +35 -0
- package/lib/adapters/trackers/azure-boards.mjs +49 -0
- package/lib/adapters/trackers/clickup.mjs +43 -0
- package/lib/adapters/trackers/github-issues.mjs +52 -0
- package/lib/adapters/trackers/jira.mjs +72 -0
- package/lib/adapters/trackers/linear.mjs +62 -0
- package/lib/adapters/trackers/none.mjs +18 -0
- package/lib/adapters/trackers/spreadsheet.mjs +28 -0
- package/lib/artifacts/fs-index.mjs +230 -0
- package/lib/bootstrap/render.mjs +373 -0
- package/lib/cache/store.mjs +422 -0
- package/lib/commands/bootstrap.mjs +4 -0
- package/lib/commands/callback.mjs +302 -0
- package/lib/commands/config.mjs +257 -0
- package/lib/commands/docs.mjs +1 -1
- package/lib/commands/doctor.mjs +583 -0
- package/lib/commands/feedback.mjs +355 -0
- package/lib/commands/help.mjs +96 -21
- package/lib/commands/init.mjs +830 -158
- package/lib/commands/kickoff.mjs +192 -0
- package/lib/commands/knowledge.mjs +368 -0
- package/lib/commands/lanes.mjs +502 -0
- package/lib/commands/manifest-catalog.mjs +102 -38
- package/lib/commands/migrate.mjs +204 -0
- package/lib/commands/new.mjs +452 -0
- package/lib/commands/patterns.mjs +9 -43
- package/lib/commands/run.mjs +617 -0
- package/lib/commands/sync.mjs +749 -0
- package/lib/commands/telemetry.mjs +390 -0
- package/lib/commands/verify.mjs +187 -0
- package/lib/config/io.mjs +232 -0
- package/lib/config/migrate.mjs +215 -0
- package/lib/config/schema.mjs +650 -0
- package/lib/detect.mjs +162 -19
- package/lib/feedback/drafts.mjs +129 -0
- package/lib/find-ship-root.mjs +16 -10
- package/lib/http.mjs +237 -11
- package/lib/state/idempotency.mjs +183 -0
- package/lib/state/lockfile.mjs +180 -0
- package/lib/telemetry/outbox.mjs +224 -0
- package/lib/templates.mjs +53 -65
- package/lib/verify/checks/agents-on-disk.mjs +58 -0
- package/lib/verify/checks/api-reachable.mjs +39 -0
- package/lib/verify/checks/artifacts-up-to-date.mjs +78 -0
- package/lib/verify/checks/bootstrap-files.mjs +67 -0
- package/lib/verify/checks/cache-integrity.mjs +51 -0
- package/lib/verify/checks/ci-secrets.mjs +86 -0
- package/lib/verify/checks/config-present.mjs +39 -0
- package/lib/verify/checks/gitignore-cache.mjs +51 -0
- package/lib/verify/checks/rules-markers.mjs +135 -0
- package/lib/verify/checks/stack-enums.mjs +33 -0
- package/lib/verify/checks/tracker-labels.mjs +91 -0
- package/lib/verify/registry.mjs +120 -0
- package/lib/version.mjs +34 -0
- package/package.json +10 -3
- 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
|
+
}
|
package/lib/version.mjs
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Ship CLI —
|
|
5
|
+
"description": "Ship CLI — artifacts protocol (patterns/tools/workflows/collections), docs search/fetch/feedback, and agent init",
|
|
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
|
-
"
|
|
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
|
-
}
|