@holmes-lab/holmes-kit 0.1.10 → 0.1.11

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 CHANGED
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.1.11] - 2026-08-23
9
+
10
+ ### Added
11
+ - **`doctor` diagnoses the global npm prefix (REQ-243)**: after a Windows adopter hit `EPERM` on
12
+ `npm install -g` twice — once elevated — `doctor` now reads the npm global prefix, computes where
13
+ a global install would land (win32: `<prefix>\\node_modules`; POSIX: `<prefix>/lib/node_modules`),
14
+ and checks writability on the deepest existing ancestor, which is what decides the real mkdir's
15
+ fate. An unwritable prefix is a **WARN** naming the exact directory the install would die in, with
16
+ a user-space prefix prescription — never elevation, which npm's own error text suggests and which
17
+ would run better-sqlite3's install script with system privileges. An unreadable probe is a WARN
18
+ carrying the reason, never a silent pass. The check cannot FAIL: a red doctor would tell a user
19
+ with a healthy local install that the install is broken.
20
+
21
+ ### Unchanged
22
+ - The `-g` EPERM itself is not fixable by any package: it occurs while npm creates the scope
23
+ directory, before a single package file is transferred. The supported regular-account path remains
24
+ the local install (`npm install --save-dev`), which touches no protected directory.
25
+
7
26
  <!-- @implements A-SPEC-209 -->
8
27
  ## [0.1.10] - 2026-08-23
9
28
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- a400784-mt568ove
1
+ c3422e5-mt570gca
@@ -28,6 +28,45 @@ export interface DoctorOptions {
28
28
  * real key-related failure.
29
29
  */
30
30
  export declare const STRIPPED_FOR_PROBE: readonly ["HOLMES_APPROVAL", "HOLMES_SPECS"];
31
+ /**
32
+ * Where a global install actually lands for a given prefix.
33
+ *
34
+ * @implements A-SPEC-243
35
+ * The shapes differ, not just the separators: win32 puts global modules directly under the prefix,
36
+ * POSIX puts them under `lib/`. The win32 result is the exact directory the reported EPERM died in
37
+ * — npm mkdir-ing `@holmes-lab` inside `C:\Program Files\nodejs\node_modules`, before a single
38
+ * package file was transferred. That is why no package content can fix that failure, and why the
39
+ * only lever left is naming the condition here.
40
+ */
41
+ export declare function globalInstallDir(prefix: string, platform: string): string;
42
+ /** What the global-prefix probe saw. `unreadable` carries WHY npm could not be consulted. */
43
+ export interface PrefixProbe {
44
+ prefix?: unknown;
45
+ dir?: string;
46
+ writable?: boolean;
47
+ platform?: string;
48
+ unreadable?: string;
49
+ }
50
+ /**
51
+ * Judge the global prefix. Pure — the probing lives with the caller.
52
+ *
53
+ * @implements A-SPEC-243
54
+ * Never FAIL: the `-g` path is optional, and a red doctor tells a user with a healthy local
55
+ * install that the install is broken — a false signal that comes back as distrust of the check.
56
+ *
57
+ * Never elevation: npm's own EPERM text ends with "try running the command again as
58
+ * root/Administrator", and pointing the opposite way is this check's reason to exist — elevation
59
+ * runs better-sqlite3's install script (`prebuild-install || node-gyp rebuild`) with system
60
+ * privileges. The prescription is a user-space prefix, per platform.
61
+ *
62
+ * An unreadable probe is a WARN carrying the reason, not a silent pass: a silent pass reads as
63
+ * "checked, no problem", which translates what was not seen into a verdict.
64
+ */
65
+ export declare function prefixVerdict(input: PrefixProbe): {
66
+ level: Level;
67
+ detail: string;
68
+ fix?: string;
69
+ };
31
70
  /** The parent environment minus the variables that legitimately change a gate decision. Pure. */
32
71
  export declare function probeEnv(parent: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
33
72
  /** Seam for the gate probe's child process. Default is a real `spawnSync`; tests inject a fake. */
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.STRIPPED_FOR_PROBE = void 0;
37
+ exports.globalInstallDir = globalInstallDir;
38
+ exports.prefixVerdict = prefixVerdict;
37
39
  exports.probeEnv = probeEnv;
38
40
  exports.runDoctor = runDoctor;
39
41
  exports.formatChecks = formatChecks;
@@ -63,6 +65,56 @@ const GRAMMARS = [
63
65
  * real key-related failure.
64
66
  */
65
67
  exports.STRIPPED_FOR_PROBE = ['HOLMES_APPROVAL', 'HOLMES_SPECS'];
68
+ /**
69
+ * Where a global install actually lands for a given prefix.
70
+ *
71
+ * @implements A-SPEC-243
72
+ * The shapes differ, not just the separators: win32 puts global modules directly under the prefix,
73
+ * POSIX puts them under `lib/`. The win32 result is the exact directory the reported EPERM died in
74
+ * — npm mkdir-ing `@holmes-lab` inside `C:\Program Files\nodejs\node_modules`, before a single
75
+ * package file was transferred. That is why no package content can fix that failure, and why the
76
+ * only lever left is naming the condition here.
77
+ */
78
+ function globalInstallDir(prefix, platform) {
79
+ return platform === 'win32'
80
+ ? `${prefix.replace(/[\\/]+$/, '')}\\node_modules`
81
+ : `${prefix.replace(/\/+$/, '')}/lib/node_modules`;
82
+ }
83
+ /**
84
+ * Judge the global prefix. Pure — the probing lives with the caller.
85
+ *
86
+ * @implements A-SPEC-243
87
+ * Never FAIL: the `-g` path is optional, and a red doctor tells a user with a healthy local
88
+ * install that the install is broken — a false signal that comes back as distrust of the check.
89
+ *
90
+ * Never elevation: npm's own EPERM text ends with "try running the command again as
91
+ * root/Administrator", and pointing the opposite way is this check's reason to exist — elevation
92
+ * runs better-sqlite3's install script (`prebuild-install || node-gyp rebuild`) with system
93
+ * privileges. The prescription is a user-space prefix, per platform.
94
+ *
95
+ * An unreadable probe is a WARN carrying the reason, not a silent pass: a silent pass reads as
96
+ * "checked, no problem", which translates what was not seen into a verdict.
97
+ */
98
+ function prefixVerdict(input) {
99
+ const reason = input.unreadable ?? (typeof input.prefix !== 'string' || input.prefix === ''
100
+ ? `npm 이 돌려준 prefix 를 해석할 수 없습니다 (${String(input.prefix)})`
101
+ : undefined);
102
+ if (reason !== undefined) {
103
+ return { level: 'WARN', detail: `전역 prefix 를 판독하지 못했습니다 — ${reason}. 로컬 설치(--save-dev)는 이 판정과 무관하게 동작합니다.` };
104
+ }
105
+ const prefix = input.prefix;
106
+ if (input.writable) {
107
+ return { level: 'PASS', detail: `전역 prefix(${prefix})가 현재 계정으로 쓰기 가능합니다 — npm install -g 가 권한 문제 없이 동작합니다.` };
108
+ }
109
+ const fix = input.platform === 'win32'
110
+ ? 'npm config set prefix "%APPDATA%\\npm" 실행 후 %APPDATA%\\npm 을 PATH 에 추가하고 터미널을 다시 여세요. 로컬 설치(npm install --save-dev)는 이 설정 없이도 동작합니다.'
111
+ : 'npm config set prefix "$HOME/.npm-global" 실행 후 $HOME/.npm-global/bin 을 PATH 에 추가하세요. 로컬 설치(npm install --save-dev)는 이 설정 없이도 동작합니다.';
112
+ return {
113
+ level: 'WARN',
114
+ detail: `전역 prefix(${prefix})가 현재 계정으로 쓰기 불가입니다 — npm install -g 는 ${input.dir ?? ''} 에 디렉터리를 만들다 EPERM 으로 죽습니다. 패키지가 도착하기 전 단계라 어떤 패키지도 이를 고칠 수 없습니다.`,
115
+ fix,
116
+ };
117
+ }
66
118
  /** The parent environment minus the variables that legitimately change a gate decision. Pure. */
67
119
  function probeEnv(parent) {
68
120
  const out = { ...parent };
@@ -451,6 +503,33 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
451
503
  catch { /* an unreadable settings file is `target wiring`'s business, not this check's */ }
452
504
  }
453
505
  add('gate blind spots', 'PASS', (0, blind_spots_1.blindSpotSummary)());
506
+ // @implements A-SPEC-243 — the writability of the deepest EXISTING ancestor is what decides the
507
+ // real mkdir's fate, since the scope directory being created does not exist yet.
508
+ {
509
+ let verdict;
510
+ try {
511
+ const prefix = (0, node_child_process_1.execFileSync)('npm', ['config', 'get', 'prefix'], { encoding: 'utf8' }).trim();
512
+ const dir = globalInstallDir(prefix, process.platform);
513
+ let probe = dir;
514
+ while (!fs.existsSync(probe)) {
515
+ const parent = path.dirname(probe);
516
+ if (parent === probe)
517
+ break;
518
+ probe = parent;
519
+ }
520
+ let writable = false;
521
+ try {
522
+ fs.accessSync(probe, fs.constants.W_OK);
523
+ writable = true;
524
+ }
525
+ catch { /* not writable */ }
526
+ verdict = prefixVerdict({ prefix, dir, writable, platform: process.platform });
527
+ }
528
+ catch (e) {
529
+ verdict = prefixVerdict({ unreadable: e instanceof Error ? e.message.split('\n')[0] : String(e) });
530
+ }
531
+ add('global prefix', verdict.level, verdict.detail, verdict.fix);
532
+ }
454
533
  add('environment', 'PASS', present);
455
534
  if (extraChecks) {
456
535
  checks.push(...extraChecks);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.1.10",
4
+ "version": "0.1.11",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",