@holmes-lab/holmes-kit 0.1.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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/holmes-hook-antigravity.js +31 -0
- package/bin/holmes-kit.js +23 -0
- package/bin/holmes-mcp.js +34 -0
- package/bin/holmes-stop-antigravity.js +29 -0
- package/dist/.build-id +1 -0
- package/dist/holmes/cli/agents.js +168 -0
- package/dist/holmes/cli/doctor.js +625 -0
- package/dist/holmes/cli/gitignore-merge.js +84 -0
- package/dist/holmes/cli/governed-precondition.js +157 -0
- package/dist/holmes/cli/index.js +384 -0
- package/dist/holmes/cli/init.js +462 -0
- package/dist/holmes/cli/playbook-skills.js +711 -0
- package/dist/holmes/cli/roles-readme.js +134 -0
- package/dist/holmes/cli/settings-merge.js +122 -0
- package/dist/holmes/config/config.js +70 -0
- package/dist/holmes/context/bundler.js +114 -0
- package/dist/holmes/context/render.js +29 -0
- package/dist/holmes/context/tiers.js +110 -0
- package/dist/holmes/context/tokens.js +8 -0
- package/dist/holmes/cpg/cpg-scanner.js +213 -0
- package/dist/holmes/cpg/hash-cache.js +86 -0
- package/dist/holmes/cpg/language-parser-walk.js +917 -0
- package/dist/holmes/cpg/language-parser-worker.js +81 -0
- package/dist/holmes/cpg/language-parser.js +234 -0
- package/dist/holmes/cpg/scan-cache.js +108 -0
- package/dist/holmes/cpg/source-path.js +44 -0
- package/dist/holmes/cpg/test-files.js +84 -0
- package/dist/holmes/governance/constitution-debt.js +73 -0
- package/dist/holmes/governance/constitution-report.js +25 -0
- package/dist/holmes/governance/constitution.js +129 -0
- package/dist/holmes/governance/identity.js +30 -0
- package/dist/holmes/governance/ledger-lock.js +165 -0
- package/dist/holmes/governance/ledger-store.conformance.js +90 -0
- package/dist/holmes/governance/ledger-store.js +106 -0
- package/dist/holmes/governance/progress-ledger.js +83 -0
- package/dist/holmes/governance/provenance-chain.js +365 -0
- package/dist/holmes/governance/provenance-ledger.js +0 -0
- package/dist/holmes/governance/provenance-schema.js +47 -0
- package/dist/holmes/governance/replica-id.js +106 -0
- package/dist/holmes/governance/role-policy.js +137 -0
- package/dist/holmes/governance/trust-score.js +43 -0
- package/dist/holmes/guardrail/anchors.js +31 -0
- package/dist/holmes/guardrail/blind-spots.js +38 -0
- package/dist/holmes/guardrail/decision-ledger.js +107 -0
- package/dist/holmes/guardrail/executable-artifact.js +129 -0
- package/dist/holmes/guardrail/governance-history.js +101 -0
- package/dist/holmes/guardrail/phase.js +169 -0
- package/dist/holmes/guardrail/risk-classifier.js +450 -0
- package/dist/holmes/guardrail/risk-gate.js +160 -0
- package/dist/holmes/guardrail/risk-types.js +6 -0
- package/dist/holmes/guardrail/tspec-state.js +392 -0
- package/dist/holmes/guardrail/write-target.js +224 -0
- package/dist/holmes/hooks/adapters/antigravity.js +194 -0
- package/dist/holmes/hooks/pre-tool-use.js +1262 -0
- package/dist/holmes/hooks/stop.js +416 -0
- package/dist/holmes/mcp/basis.js +162 -0
- package/dist/holmes/mcp/handlers.js +1831 -0
- package/dist/holmes/mcp/server.js +71 -0
- package/dist/holmes/mcp/stdio-client.js +165 -0
- package/dist/holmes/mcp/supervisor.js +178 -0
- package/dist/holmes/mcp/tool-schemas.js +394 -0
- package/dist/holmes/mcp/validate-args.js +281 -0
- package/dist/holmes/messages/registry.js +50 -0
- package/dist/holmes/project/baseline.js +210 -0
- package/dist/holmes/project/change-source.js +233 -0
- package/dist/holmes/project/ignore.js +145 -0
- package/dist/holmes/project/root.js +113 -0
- package/dist/holmes/reverse/anchor.js +162 -0
- package/dist/holmes/reverse/cluster.js +187 -0
- package/dist/holmes/reverse/draft.js +151 -0
- package/dist/holmes/reverse/dynamic-wiring.js +47 -0
- package/dist/holmes/reverse/scan.js +194 -0
- package/dist/holmes/reverse/surface.js +154 -0
- package/dist/holmes/reverse/test-map.js +263 -0
- package/dist/holmes/review/coverage.js +33 -0
- package/dist/holmes/review/findings.js +123 -0
- package/dist/holmes/review/package.js +40 -0
- package/dist/holmes/review/review-targets.js +92 -0
- package/dist/holmes/review/scope.js +57 -0
- package/dist/holmes/review/test-evidence.js +77 -0
- package/dist/holmes/review/test-runner.js +572 -0
- package/dist/holmes/rtm/dataflow-taint.js +262 -0
- package/dist/holmes/rtm/gap-analyzer.js +27 -0
- package/dist/holmes/rtm/git-changes.js +72 -0
- package/dist/holmes/rtm/incremental.js +45 -0
- package/dist/holmes/rtm/localize.js +100 -0
- package/dist/holmes/rtm/rtm-builder.js +191 -0
- package/dist/holmes/rtm/rtm-check.js +89 -0
- package/dist/holmes/rtm/rtm-graph.js +232 -0
- package/dist/holmes/rtm/taint.js +92 -0
- package/dist/holmes/rtm/test-scope.js +336 -0
- package/dist/holmes/spec/approval-blockers.js +204 -0
- package/dist/holmes/spec/breaking-change.js +89 -0
- package/dist/holmes/spec/legacy-format.js +87 -0
- package/dist/holmes/spec/spec-digest.js +71 -0
- package/dist/holmes/spec/spec-parser.js +106 -0
- package/dist/holmes/spec/spec-store.conformance.js +118 -0
- package/dist/holmes/spec/spec-store.js +331 -0
- package/dist/holmes/spec/spec-types.js +177 -0
- package/dist/holmes/spec/validator.js +280 -0
- package/package.json +76 -0
- package/playbooks/adopt/PLAYBOOK.md +125 -0
- package/playbooks/author-slice/PLAYBOOK.md +119 -0
- package/playbooks/promote-slice/PLAYBOOK.md +134 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-100.2
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.IGNORE_LINES = exports.END = exports.BEGIN = void 0;
|
|
5
|
+
exports.mergeGitignore = mergeGitignore;
|
|
6
|
+
exports.removeGitignoreBlock = removeGitignoreBlock;
|
|
7
|
+
exports.hasGitignoreBlock = hasGitignoreBlock;
|
|
8
|
+
/**
|
|
9
|
+
* PURE .gitignore block merge. holmes-kit writes derived state under `.ax/` in the TARGET project
|
|
10
|
+
* (ledger, cpg_cache, reports) that must never be committed. The block is delimited so it can be
|
|
11
|
+
* added, refreshed, and removed exactly — never touching the user's own lines.
|
|
12
|
+
*/
|
|
13
|
+
exports.BEGIN = '# >>> holmes-kit >>>';
|
|
14
|
+
exports.END = '# <<< holmes-kit <<<';
|
|
15
|
+
/** Derived/runtime paths holmes-kit creates in a target project. */
|
|
16
|
+
exports.IGNORE_LINES = [
|
|
17
|
+
// @implements A-SPEC-148
|
|
18
|
+
// ADR-012 decided the provenance and progress ledgers are SSOT and must be SHARED — "what cannot
|
|
19
|
+
// be reconstructed must be shared", because a colleague's clone otherwise has an empty chain and
|
|
20
|
+
// cannot answer who decided what, when, or why. That decision was only ever applied to this
|
|
21
|
+
// repository's hand-edited .gitignore; every project `init` touched still ignored the whole
|
|
22
|
+
// directory, so ADR-012 was silently not in force for anyone else.
|
|
23
|
+
//
|
|
24
|
+
// The wildcard covers the per-replica chains too (`provenance.<replicaId>.jsonl`). Without it the
|
|
25
|
+
// split would have made the shared artefact local again — the exact regression the split exists
|
|
26
|
+
// to enable collaboration around.
|
|
27
|
+
'.ax/ledger/*',
|
|
28
|
+
'!.ax/ledger/provenance*.jsonl',
|
|
29
|
+
'!.ax/ledger/progress.jsonl',
|
|
30
|
+
'.ax/cpg_cache/',
|
|
31
|
+
'.ax/cpg_cache.json',
|
|
32
|
+
'.ax/reports/',
|
|
33
|
+
'.ax/*.sqlite*',
|
|
34
|
+
// `init` snapshots every config it touches before writing. Those snapshots are recovery aids, not
|
|
35
|
+
// project content — left untracked they show up in `git status` and read as install damage.
|
|
36
|
+
'.ax/state/',
|
|
37
|
+
'*.holmes-bak-*',
|
|
38
|
+
// @implements A-SPEC-193 — 하네스 배선물은 이 **설치본의 절대 경로**를 담는다(훅은 hooks.json
|
|
39
|
+
// 이 있는 디렉터리를 cwd 로 돌고, 패키지가 어디 설치됐는지는 기계마다 다르다). 그 경로가
|
|
40
|
+
// VCS 에 들어가면 다른 기계에서 남의 설치를 부르거나 아무것도 부르지 못한다. 팀과 공유하고
|
|
41
|
+
// 싶다면 이 두 줄을 지우고 명령을 상대 경로로 바꾸면 된다 — 기본값은 새지 않는 쪽이다.
|
|
42
|
+
'.agents/hooks.json',
|
|
43
|
+
'.agents/mcp_config.json',
|
|
44
|
+
];
|
|
45
|
+
function findBlock(text) {
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const start = lines.findIndex((l) => l.trim() === exports.BEGIN);
|
|
48
|
+
if (start < 0)
|
|
49
|
+
return null;
|
|
50
|
+
const end = lines.findIndex((l, i) => i > start && l.trim() === exports.END);
|
|
51
|
+
return end < 0 ? null : { start, end };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add (or refresh) the holmes-kit block. Idempotent: a second call reproduces the same text.
|
|
55
|
+
* Preserves everything outside the block, and preserves whether the file ended with a newline.
|
|
56
|
+
*/
|
|
57
|
+
function mergeGitignore(existing, lines = exports.IGNORE_LINES) {
|
|
58
|
+
const block = [exports.BEGIN, ...lines, exports.END];
|
|
59
|
+
const found = findBlock(existing);
|
|
60
|
+
if (found) {
|
|
61
|
+
const src = existing.split('\n');
|
|
62
|
+
return [...src.slice(0, found.start), ...block, ...src.slice(found.end + 1)].join('\n');
|
|
63
|
+
}
|
|
64
|
+
if (existing.trim() === '')
|
|
65
|
+
return block.join('\n') + '\n';
|
|
66
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
67
|
+
return `${existing}${sep}\n${block.join('\n')}\n`;
|
|
68
|
+
}
|
|
69
|
+
/** Remove the block (and the blank line that preceded it, if we added one). Inverse of merge. */
|
|
70
|
+
function removeGitignoreBlock(existing) {
|
|
71
|
+
const found = findBlock(existing);
|
|
72
|
+
if (!found)
|
|
73
|
+
return existing;
|
|
74
|
+
const src = existing.split('\n');
|
|
75
|
+
let start = found.start;
|
|
76
|
+
if (start > 0 && src[start - 1].trim() === '')
|
|
77
|
+
start -= 1; // drop the separator blank line
|
|
78
|
+
const out = [...src.slice(0, start), ...src.slice(found.end + 1)];
|
|
79
|
+
return out.join('\n');
|
|
80
|
+
}
|
|
81
|
+
/** True iff the holmes-kit block is present. */
|
|
82
|
+
function hasGitignoreBlock(existing) {
|
|
83
|
+
return findBlock(existing) !== null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MAX_DIRS = void 0;
|
|
37
|
+
exports.hasGatedCode = hasGatedCode;
|
|
38
|
+
exports.governedWarning = governedWarning;
|
|
39
|
+
// @implements A-SPEC-172
|
|
40
|
+
/**
|
|
41
|
+
* What a governed transition is about to block, and which playbook undoes it.
|
|
42
|
+
*
|
|
43
|
+
* Measured 2026-08-12: `init --mode governed --force` on a target with no approved A-SPEC succeeded
|
|
44
|
+
* and printed nothing about the precondition — from that moment every code write was denied. Without
|
|
45
|
+
* `--force` the same situation is refused with three lines that explain it. The transition passing is
|
|
46
|
+
* defensible (starting greenfield in governed mode and authoring specs first is a supported path);
|
|
47
|
+
* saying nothing is not.
|
|
48
|
+
*/
|
|
49
|
+
const fs = __importStar(require("node:fs"));
|
|
50
|
+
const path = __importStar(require("node:path"));
|
|
51
|
+
const phase_1 = require("../guardrail/phase");
|
|
52
|
+
const playbook_skills_1 = require("./playbook-skills");
|
|
53
|
+
/**
|
|
54
|
+
* Directories that are never the user's own code.
|
|
55
|
+
*
|
|
56
|
+
* `.ax` and `.claude` are written by init itself, and `node_modules` is present in nearly every JS
|
|
57
|
+
* project — counting any of them makes the answer permanently `true` and the branch vacuous.
|
|
58
|
+
*/
|
|
59
|
+
const SKIP_DIRS = new Set([
|
|
60
|
+
'.ax', '.git', '.claude', 'node_modules', 'dist', 'build', 'vendor', 'target', '.venv', '__pycache__',
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* How many directories the walk may visit.
|
|
64
|
+
*
|
|
65
|
+
* The cap only bites on projects with NO code — the walk returns at the first gated file — and those
|
|
66
|
+
* are exactly the ones where exhausting it gives the wrong playbook. Measured 2026-08-12 at roughly
|
|
67
|
+
* 0.05ms per directory, so 2000 costs about 100ms inside a one-shot command that also writes files
|
|
68
|
+
* and installs skills.
|
|
69
|
+
*/
|
|
70
|
+
exports.MAX_DIRS = 2000;
|
|
71
|
+
/**
|
|
72
|
+
* Does this target contain a file the Write/Edit gate would refuse once governed mode is live?
|
|
73
|
+
*
|
|
74
|
+
* The judgement is `classifyAction` — the gate's own function. Re-deriving "is this code" from a
|
|
75
|
+
* second extension list would leave two sources of truth, and the guidance would be wrong the moment
|
|
76
|
+
* they drifted apart (the failure REQ-163 measured for path identity).
|
|
77
|
+
*
|
|
78
|
+
* Content is never read: `classifyAction`'s shebang signal needs the file open, which does not fit
|
|
79
|
+
* the cost ceiling, and missing it errs toward `adopt` — the harmless direction.
|
|
80
|
+
*/
|
|
81
|
+
function hasGatedCode(target) {
|
|
82
|
+
const queue = [target];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
let visited = 0;
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
// Budget exhausted before an answer: report the safe side. `adopt` on an empty project wastes a
|
|
87
|
+
// moment; `author-slice` on a real codebase is wrong advice.
|
|
88
|
+
if (visited >= exports.MAX_DIRS)
|
|
89
|
+
return true;
|
|
90
|
+
const dir = queue.shift();
|
|
91
|
+
visited++;
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue; // an unreadable subdirectory must not take init down with it
|
|
98
|
+
}
|
|
99
|
+
for (const e of entries) {
|
|
100
|
+
const full = path.join(dir, e.name);
|
|
101
|
+
let isDir = e.isDirectory();
|
|
102
|
+
let isFile = e.isFile();
|
|
103
|
+
// A symlinked source tree is still code the gate will deny writes to — REQ-163 made resolved
|
|
104
|
+
// identity the rule for paths, and skipping links here would put a second, disagreeing answer
|
|
105
|
+
// next to the gate's. One stat per link, not per file.
|
|
106
|
+
if (e.isSymbolicLink()) {
|
|
107
|
+
try {
|
|
108
|
+
const st = fs.statSync(full);
|
|
109
|
+
isDir = st.isDirectory();
|
|
110
|
+
isFile = st.isFile();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue; // dangling link
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (isDir) {
|
|
117
|
+
if (SKIP_DIRS.has(e.name))
|
|
118
|
+
continue;
|
|
119
|
+
let real;
|
|
120
|
+
try {
|
|
121
|
+
real = fs.realpathSync(full);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
real = full;
|
|
125
|
+
}
|
|
126
|
+
// Links make one real tree reachable under several names. A cycle is already bounded by the
|
|
127
|
+
// kernel's ELOOP and by MAX_DIRS; what this stops is spending the budget re-walking the same
|
|
128
|
+
// directory, which would trip the cap and report code in a project that has none.
|
|
129
|
+
if (seen.has(real))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(real);
|
|
132
|
+
queue.push(full);
|
|
133
|
+
}
|
|
134
|
+
else if (isFile) {
|
|
135
|
+
const rel = path.relative(target, full);
|
|
136
|
+
if ((0, phase_1.classifyAction)(rel) === 'WRITE_CODE')
|
|
137
|
+
return true; // the answer is boolean — stop here
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The three lines a forced governed transition owes the user: that the precondition was skipped,
|
|
145
|
+
* what is now denied (and what still works), and the one playbook that fits this target.
|
|
146
|
+
*
|
|
147
|
+
* Naming both playbooks would hand the judgement back to the user, which is the defect itself.
|
|
148
|
+
*/
|
|
149
|
+
function governedWarning(target) {
|
|
150
|
+
return [
|
|
151
|
+
'Warning: --force skipped the governed precondition — this target has no approved A-SPEC.',
|
|
152
|
+
'Until one is approved, the Write/Edit gate denies EVERY code edit here (spec authoring is allowed).',
|
|
153
|
+
hasGatedCode(target)
|
|
154
|
+
? `Existing code found — run the '${(0, playbook_skills_1.invocableSkillName)('adopt')}' skill to reverse-engineer specs from it.`
|
|
155
|
+
: `No code here yet — run the '${(0, playbook_skills_1.invocableSkillName)('author-slice')}' skill to write and approve a spec chain first.`,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.packageRoot = packageRoot;
|
|
37
|
+
exports.main = main;
|
|
38
|
+
// @implements A-SPEC-100.2
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const init_1 = require("./init");
|
|
42
|
+
const doctor_1 = require("./doctor");
|
|
43
|
+
/**
|
|
44
|
+
* `holmes-kit` CLI entry — argv dispatch, hand-rolled (no new dependency).
|
|
45
|
+
*
|
|
46
|
+
* Commands:
|
|
47
|
+
* init [--mode guardrail|governed] [--target dir] [--dry-run] [--remove] [--force] ...
|
|
48
|
+
* doctor [--target dir] [--json]
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The installed package root. From `dist/holmes/cli/index.js` that is three levels up. Node resolves
|
|
52
|
+
* a global bin symlink to its real path before setting __dirname, so this is correct even when the
|
|
53
|
+
* command is invoked through the PATH shim.
|
|
54
|
+
*/
|
|
55
|
+
function packageRoot() {
|
|
56
|
+
return path.resolve(__dirname, '..', '..', '..');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @implements A-SPEC-171
|
|
60
|
+
* What each command accepts. `help` is common to all.
|
|
61
|
+
*
|
|
62
|
+
* Declared because the parser used to accept ANY `--key` and drop it when nobody read it —
|
|
63
|
+
* measured 2026-08-12, `holmes-kit init --help` wired a clean directory instead of printing
|
|
64
|
+
* anything, because `--help` was stored and ignored like any other unknown key. Special-casing
|
|
65
|
+
* `--help` would only postpone the next typo, so the rule is the declaration itself: this is
|
|
66
|
+
* REQ-144's "cannot decide is not permission", applied to CLI arguments.
|
|
67
|
+
*/
|
|
68
|
+
const KNOWN_FLAGS = {
|
|
69
|
+
init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent'],
|
|
70
|
+
doctor: ['help', 'target', 'json'],
|
|
71
|
+
skills: ['help', 'target'],
|
|
72
|
+
};
|
|
73
|
+
class UnknownFlagError extends Error {
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Flags that take NO value. Declared, because the old parser stored the NEXT token as any flag's
|
|
77
|
+
* value: `init --dry-run .` swallowed `.` into dry-run, the boolean read false, and the "show me
|
|
78
|
+
* what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
|
|
79
|
+
* --force). A boolean flag must never consume a token.
|
|
80
|
+
*/
|
|
81
|
+
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json']);
|
|
82
|
+
function parseFlags(argv, known) {
|
|
83
|
+
const flags = {};
|
|
84
|
+
const positionals = [];
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const a = argv[i];
|
|
87
|
+
if (!a.startsWith('--')) {
|
|
88
|
+
positionals.push(a);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (a === '--') {
|
|
92
|
+
// The conventional options terminator. Naming it `unknown flag: --` was fail-closed but a
|
|
93
|
+
// misdiagnosis (round-4) — this CLI takes no positionals that would need one.
|
|
94
|
+
throw new UnknownFlagError("the '--' option terminator is not supported — this CLI takes flags only");
|
|
95
|
+
}
|
|
96
|
+
// GNU `--key=value` — refusing it as `unknown flag: --target=/path` was fail-closed but
|
|
97
|
+
// misdiagnosed (round-3).
|
|
98
|
+
let key = a.slice(2);
|
|
99
|
+
let inline;
|
|
100
|
+
const eq = key.indexOf('=');
|
|
101
|
+
if (eq >= 0) {
|
|
102
|
+
inline = key.slice(eq + 1);
|
|
103
|
+
key = key.slice(0, eq);
|
|
104
|
+
}
|
|
105
|
+
if (known && !known.includes(key))
|
|
106
|
+
throw new UnknownFlagError(`unknown flag: --${key}`);
|
|
107
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
108
|
+
if (inline !== undefined)
|
|
109
|
+
throw new UnknownFlagError(`--${key} takes no value`);
|
|
110
|
+
flags[key] = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// @implements A-SPEC-190 (round 7) — a repeated value flag silently took the LAST one:
|
|
114
|
+
// `--target A --target B` acted on B while the operator was looking at A. 'Cannot decide is not
|
|
115
|
+
// permission' applies to the operator's own contradiction too.
|
|
116
|
+
if (Object.prototype.hasOwnProperty.call(flags, key)) {
|
|
117
|
+
throw new UnknownFlagError(`--${key} was given more than once — say it once, or the run acts on a directory you did not read`);
|
|
118
|
+
}
|
|
119
|
+
if (inline !== undefined) {
|
|
120
|
+
flags[key] = inline;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const next = argv[i + 1];
|
|
124
|
+
if (next && !next.startsWith('--')) {
|
|
125
|
+
flags[key] = next;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
flags[key] = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { flags, positionals };
|
|
133
|
+
}
|
|
134
|
+
function readApproval() {
|
|
135
|
+
const raw = process.env.HOLMES_APPROVAL;
|
|
136
|
+
if (!raw)
|
|
137
|
+
return undefined;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
146
|
+
|
|
147
|
+
holmes-kit init [flags] wire holmes-kit into a target project
|
|
148
|
+
--mode guardrail|governed guardrail (default): gates only dangerous shell commands; safe on a
|
|
149
|
+
spec-less project. governed: adds the No-Spec-No-Code Write/Edit gate
|
|
150
|
+
(refused unless the target already has an approved A-SPEC).
|
|
151
|
+
--target <dir> target project (default: cwd)
|
|
152
|
+
--specs-dir <rel> spec directory (default: .ax/specs)
|
|
153
|
+
--settings local|project which settings file to merge (default: local)
|
|
154
|
+
--agent <list> also wire these harnesses (claude|antigravity|codex|all,
|
|
155
|
+
comma-separated). Default: claude only.
|
|
156
|
+
--no-mcp skip .mcp.json / MCP server registration
|
|
157
|
+
--matcher <str> override the PreToolUse matcher (escape hatch)
|
|
158
|
+
--dry-run print the diff, write nothing
|
|
159
|
+
--remove remove holmes-kit wiring (holmes-owned entries only)
|
|
160
|
+
--force re-wire an already-wired target (requires HOLMES_APPROVAL)
|
|
161
|
+
|
|
162
|
+
holmes-kit doctor [flags] diagnose the install
|
|
163
|
+
--target <dir> also verify that target's wiring points at this install
|
|
164
|
+
--json machine-readable output
|
|
165
|
+
|
|
166
|
+
holmes-kit skills refresh [flags] refresh the recovery skills holmes-kit owns
|
|
167
|
+
--target <dir> target project (default: cwd; must exist). Touches ONLY marker-owned
|
|
168
|
+
skill files (installs absent ones, REWRITES all marker-owned ones,
|
|
169
|
+
skips yours); never reads or writes governance wiring, so no
|
|
170
|
+
approval is needed.
|
|
171
|
+
|
|
172
|
+
holmes-kit --version | --help
|
|
173
|
+
`;
|
|
174
|
+
async function main(argv) {
|
|
175
|
+
const cmd = argv[0];
|
|
176
|
+
// @implements A-SPEC-190 (round 7) — `holmes-kit constructor` resolved through the prototype
|
|
177
|
+
// chain to a truthy non-array and crashed with an unhandled TypeError instead of saying
|
|
178
|
+
// 'unknown command' (same class the MCP boundary fixed in REQ-189).
|
|
179
|
+
const knownFlagsFor = (c) => c !== undefined && Object.prototype.hasOwnProperty.call(KNOWN_FLAGS, c) ? KNOWN_FLAGS[c] : undefined;
|
|
180
|
+
let flags;
|
|
181
|
+
let positionals;
|
|
182
|
+
try {
|
|
183
|
+
({ flags, positionals } = parseFlags(argv.slice(1), knownFlagsFor(cmd)));
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof UnknownFlagError) {
|
|
187
|
+
// Named, because a typo is the thing the user has to fix and a nameless refusal sends them
|
|
188
|
+
// nowhere (REQ-155).
|
|
189
|
+
process.stderr.write(`${e.message}\n\n${USAGE}`);
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
if (knownFlagsFor(cmd) && !flags.help) {
|
|
195
|
+
// A value-taking flag with no value parses as boolean true; silently substituting a default
|
|
196
|
+
// acts where the operator did NOT point ("cannot decide is not permission", REQ-144).
|
|
197
|
+
for (const [key, v] of Object.entries(flags)) {
|
|
198
|
+
// `--target` (true) AND `--target=` ('') both point at nothing — an empty inline value
|
|
199
|
+
// resolved to CWD and wired it silently (round-5 HIGH).
|
|
200
|
+
if (!BOOLEAN_FLAGS.has(key) && (v === true || (typeof v === 'string' && v.trim() === ''))) {
|
|
201
|
+
process.stderr.write(`--${key} requires a value\n\n${USAGE}`);
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Positionals these commands do not take are refused, not dropped: `skills refresh <dir>`
|
|
206
|
+
// silently wrote into CWD while the named target stayed stale (round-3 HIGH).
|
|
207
|
+
const expected = cmd === 'skills' ? ['refresh'] : [];
|
|
208
|
+
const unexpected = positionals.filter((p) => !expected.includes(p));
|
|
209
|
+
if (unexpected.length > 0) {
|
|
210
|
+
const hint = fs.existsSync(unexpected[0]) ? ` — did you mean --target ${unexpected[0]}?` : '';
|
|
211
|
+
process.stderr.write(`unexpected argument: ${unexpected[0]}${hint}\n\n${USAGE}`);
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// @implements A-SPEC-171
|
|
216
|
+
// BEFORE any side effect. The measured defect was not that help was missing but that the writing
|
|
217
|
+
// happened first, so the order here is the fix.
|
|
218
|
+
if (knownFlagsFor(cmd) && flags.help) {
|
|
219
|
+
process.stdout.write(USAGE);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
223
|
+
process.stdout.write(USAGE);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
227
|
+
let v = '0.0.0';
|
|
228
|
+
try {
|
|
229
|
+
v = JSON.parse(fs.readFileSync(path.join(packageRoot(), 'package.json'), 'utf8')).version;
|
|
230
|
+
}
|
|
231
|
+
catch { /* keep default */ }
|
|
232
|
+
process.stdout.write(`${v}\n`);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (cmd === 'doctor') {
|
|
236
|
+
// A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
|
|
237
|
+
// (round-6) — the typo is the diagnosis, so say it.
|
|
238
|
+
if (typeof flags.target === 'string') {
|
|
239
|
+
const t = path.resolve(flags.target);
|
|
240
|
+
if (!fs.existsSync(t) || !fs.statSync(t).isDirectory()) {
|
|
241
|
+
process.stderr.write(`--target ${t} is not an existing directory\n\n${USAGE}`);
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const checks = await (0, doctor_1.runDoctor)(packageRoot(), typeof flags.target === 'string' ? path.resolve(flags.target) : undefined);
|
|
246
|
+
process.stdout.write(flags.json ? JSON.stringify(checks, null, 2) + '\n' : (0, doctor_1.formatChecks)(checks) + '\n');
|
|
247
|
+
return checks.some((c) => c.level === 'FAIL') ? 1 : 0;
|
|
248
|
+
}
|
|
249
|
+
if (cmd === 'skills') {
|
|
250
|
+
// @implements A-SPEC-190
|
|
251
|
+
// Doctor's drift remedy must actually WORK: `init --target` on a wired target exits 2 (the
|
|
252
|
+
// self-disarm guard) and --force demands governance approval — a refusal loop for advisory
|
|
253
|
+
// files. This surface calls the same installer and touches ONLY marker-owned skills; wiring
|
|
254
|
+
// (settings, hooks, MCP, specs) is never read or written, so the guard's territory is intact.
|
|
255
|
+
// Subcommand comes from the parser's positionals (round-2/3: hand-rolled token scans misparsed
|
|
256
|
+
// both `--target /path refresh` and `refresh /path`); the unexpected-positional guard above has
|
|
257
|
+
// already refused anything beyond `refresh`.
|
|
258
|
+
if (!positionals.includes('refresh')) {
|
|
259
|
+
process.stderr.write(`unknown skills subcommand: (none) — expected \`skills refresh\`\n\n${USAGE}`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
const { installPlaybookSkills, invocableSkillName, skillLabel } = require('./playbook-skills');
|
|
263
|
+
const target = path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd());
|
|
264
|
+
// A nonexistent target would be FABRICATED wholesale (measured: a literal `~` directory) and
|
|
265
|
+
// reported as success — refresh repairs an install, it does not create projects.
|
|
266
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
267
|
+
process.stderr.write(`--target ${target} is not an existing directory\n\n${USAGE}`);
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
const r = installPlaybookSkills(packageRoot(), target);
|
|
271
|
+
if (r.written.length)
|
|
272
|
+
process.stdout.write(`Refreshed ${r.written.length} recovery skill(s) under ${path.join(target, '.claude', 'skills')}: ${r.written.map(invocableSkillName).join(', ')}\n`);
|
|
273
|
+
if (r.written.length === 0 && r.skipped.length === 0 && r.failed.length === 0) {
|
|
274
|
+
// 이 패키지가 플레이북을 하나도 싣지 않았다 — 'Refreshed 0' 은 성공처럼 읽힌다.
|
|
275
|
+
process.stderr.write(`This holmes-kit package ships no playbooks — nothing to refresh under ${path.join(target, '.claude', 'skills')}. Reinstall or upgrade holmes-kit itself.\n`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
// round-7: a skill DIRECTORY the user manages as a symlink has no SKILL.md inside, so naming
|
|
279
|
+
// that file sent the reader to a path that does not exist. Name what actually occupies the spot.
|
|
280
|
+
for (const s of r.skipped) {
|
|
281
|
+
const file = path.join(target, '.claude', 'skills', invocableSkillName(s), 'SKILL.md');
|
|
282
|
+
const occupied = fs.existsSync(file) ? file : path.dirname(file);
|
|
283
|
+
process.stdout.write(`Left your own ${occupied} untouched (not written by holmes-kit)\n`);
|
|
284
|
+
}
|
|
285
|
+
// Invocable name in failures too (A-SPEC-172): the bare name sends the reader to a directory
|
|
286
|
+
// that does not exist.
|
|
287
|
+
for (const b of r.backedUp)
|
|
288
|
+
process.stdout.write(`Kept the previous ${invocableSkillName(b.name)} content at ${b.path}\n`);
|
|
289
|
+
for (const f of r.failed)
|
|
290
|
+
process.stderr.write(`Could not refresh recovery skill ${skillLabel(f.name)}: ${f.reason}\n`);
|
|
291
|
+
return r.failed.length > 0 ? 1 : 0;
|
|
292
|
+
}
|
|
293
|
+
if (cmd === 'init') {
|
|
294
|
+
const mode = (typeof flags.mode === 'string' ? flags.mode : 'guardrail');
|
|
295
|
+
// 형제 열거 오류와 같은 모양(round 7): --settings·--target 은 USAGE 와 함께 exit 2 인데
|
|
296
|
+
// --mode 만 맨몸 exit 1 이었다 — 같은 실수의 진단이 두 벌이면 스크립트가 갈린다.
|
|
297
|
+
if (mode !== 'guardrail' && mode !== 'governed') {
|
|
298
|
+
process.stderr.write(`unknown --mode ${mode} (guardrail|governed)\n\n${USAGE}`);
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
// Same rule as --mode (round-6: an unknown --settings silently substituted 'local' — the very
|
|
302
|
+
// "cannot decide is not permission" this file's own comment cites).
|
|
303
|
+
if (typeof flags.settings === 'string' && flags.settings !== 'local' && flags.settings !== 'project') {
|
|
304
|
+
process.stderr.write(`unknown --settings ${flags.settings} (local|project)\n\n${USAGE}`);
|
|
305
|
+
return 2;
|
|
306
|
+
}
|
|
307
|
+
// Same guard `skills refresh` carries (round-5: a typo'd --target FABRICATED a deep bogus
|
|
308
|
+
// project tree and wired it, exit 0 — init wires an EXISTING project, it does not mint one).
|
|
309
|
+
if (typeof flags.target === 'string') {
|
|
310
|
+
const t = path.resolve(flags.target);
|
|
311
|
+
if (!fs.existsSync(t) || !fs.statSync(t).isDirectory()) {
|
|
312
|
+
process.stderr.write(`--target ${t} is not an existing directory\n\n${USAGE}`);
|
|
313
|
+
return 2;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// @implements A-SPEC-190 (round 7) — `--specs-dir` was the one value flag with no validation:
|
|
317
|
+
// init READ it as target-relative but WIRED it verbatim, so an absolute or escaping path left
|
|
318
|
+
// the hook and the wiring pointing at two different directories.
|
|
319
|
+
const specsDir = typeof flags['specs-dir'] === 'string' ? flags['specs-dir'] : '.ax/specs';
|
|
320
|
+
// round-9: the value is interpolated into a hook COMMAND STRING, so a quote or a shell
|
|
321
|
+
// metacharacter would break out of it. A spec directory is a plain relative path; anything that
|
|
322
|
+
// could change the meaning of the command line is refused rather than escaped.
|
|
323
|
+
if (path.isAbsolute(specsDir) || specsDir.split(/[\\/]/).includes('..') || specsDir.startsWith('~')
|
|
324
|
+
|| /["'`$\\;&|<>\n\r]/.test(specsDir)) {
|
|
325
|
+
// '~' 는 셸이 확장하지 못했을 때만 남는다 — 그대로 두면 대상 안에 리터럴 '~' 디렉터리를
|
|
326
|
+
// 만들고 배선까지 그 이름으로 간다(round 8).
|
|
327
|
+
process.stderr.write(`--specs-dir ${specsDir} must be a plain relative path inside the target (no '..', no '~', no quotes or shell metacharacters)\n\n${USAGE}`);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
// @implements A-SPEC-193 — 모르는 하네스 이름은 조용히 무시하지 않는다. 배선했다는 보고와
|
|
331
|
+
// 실제가 어긋나는 것이 이 파일이 --mode·--settings 에서 이미 두 번 고친 실패 유형이다.
|
|
332
|
+
const { AGENTS } = require('./agents');
|
|
333
|
+
let agents = [];
|
|
334
|
+
if (typeof flags.agent === 'string' && flags.agent !== '') {
|
|
335
|
+
const asked = flags.agent.split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
336
|
+
const wanted = asked.includes('all') ? [...AGENTS] : asked;
|
|
337
|
+
const unknown = wanted.filter((a) => !AGENTS.includes(a));
|
|
338
|
+
if (unknown.length > 0) {
|
|
339
|
+
process.stderr.write(`unknown --agent ${unknown.join(', ')} (${AGENTS.join('|')}|all)\n\n${USAGE}`);
|
|
340
|
+
return 2;
|
|
341
|
+
}
|
|
342
|
+
agents = wanted;
|
|
343
|
+
}
|
|
344
|
+
const opts = {
|
|
345
|
+
agents,
|
|
346
|
+
target: path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd()),
|
|
347
|
+
packageRoot: packageRoot(),
|
|
348
|
+
mode,
|
|
349
|
+
specsDir,
|
|
350
|
+
settingsFile: flags.settings === 'project' ? 'project' : 'local',
|
|
351
|
+
mcp: flags['no-mcp'] !== true,
|
|
352
|
+
matcher: typeof flags.matcher === 'string' ? flags.matcher : undefined,
|
|
353
|
+
dryRun: flags['dry-run'] === true,
|
|
354
|
+
force: flags.force === true,
|
|
355
|
+
remove: flags.remove === true,
|
|
356
|
+
approval: readApproval(),
|
|
357
|
+
};
|
|
358
|
+
const res = (0, init_1.runInit)(opts);
|
|
359
|
+
for (const m of res.messages)
|
|
360
|
+
process[res.ok ? 'stdout' : 'stderr'].write(`${m}\n`);
|
|
361
|
+
if (res.ok) {
|
|
362
|
+
// round-8: a planned DELETION printed under 'would write' contradicted the line above it.
|
|
363
|
+
for (const p of res.removals ?? [])
|
|
364
|
+
process.stdout.write(` would remove ${p}\n`);
|
|
365
|
+
for (const c of res.changes) {
|
|
366
|
+
const verb = opts.dryRun ? 'would write' : 'wrote';
|
|
367
|
+
process.stdout.write(` ${verb} ${c.path}\n`);
|
|
368
|
+
if (opts.dryRun && c.after) {
|
|
369
|
+
process.stdout.write(c.after.split('\n').map((l) => ` | ${l}`).join('\n') + '\n');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return res.exitCode;
|
|
374
|
+
}
|
|
375
|
+
// round-8: `holmes-kit --target x` 나 `holmes-kit --` 처럼 명령 자리에 플래그가 오면 exit 1,
|
|
376
|
+
// 같은 토큰이 하위 명령 뒤에 오면 exit 2 였다 — 같은 실수에 두 가지 진단.
|
|
377
|
+
process.stderr.write(cmd.startsWith('-')
|
|
378
|
+
? `unknown flag in command position: ${cmd}\n\n${USAGE}`
|
|
379
|
+
: `unknown command: ${cmd}\n\n${USAGE}`);
|
|
380
|
+
return cmd.startsWith('-') ? 2 : 1;
|
|
381
|
+
}
|
|
382
|
+
if (require.main === module) {
|
|
383
|
+
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
384
|
+
}
|