@phren/cli 0.0.10 → 0.0.12

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 (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -2,7 +2,7 @@ import * as crypto from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "./shared.js";
5
- import { withFileLock } from "./governance-locks.js";
5
+ import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
6
6
  import { errorMessage } from "./utils.js";
7
7
  const GOVERNANCE_SCHEMA_VERSION = 1;
8
8
  const DEFAULT_MEMORY_SCORES_FILE = {
@@ -15,12 +15,6 @@ function usageLogFile(phrenPath) {
15
15
  function scoresJournalFile(phrenPath) {
16
16
  return runtimeFile(phrenPath, "scores.jsonl");
17
17
  }
18
- function hasValidSchemaVersion(data) {
19
- return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
20
- }
21
- function isFiniteNumber(value) {
22
- return typeof value === "number" && Number.isFinite(value);
23
- }
24
18
  function isEntryScore(value) {
25
19
  if (!isRecord(value))
26
20
  return false;
@@ -120,7 +114,7 @@ function readScoreJournal(phrenPath) {
120
114
  return JSON.parse(line);
121
115
  }
122
116
  catch (err) {
123
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
117
+ if ((process.env.PHREN_DEBUG))
124
118
  process.stderr.write(`[phren] readScoreJournal parseLine: ${errorMessage(err)}\n`);
125
119
  return null;
126
120
  }
@@ -153,7 +147,7 @@ function claimScoreJournal(phrenPath) {
153
147
  return JSON.parse(line);
154
148
  }
155
149
  catch (err) {
156
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
150
+ if ((process.env.PHREN_DEBUG))
157
151
  process.stderr.write(`[phren] claimScoreJournal parseLine: ${errorMessage(err)}\n`);
158
152
  return null;
159
153
  }
@@ -169,7 +163,7 @@ function claimScoreJournal(phrenPath) {
169
163
  fs.unlinkSync(claimedFile);
170
164
  }
171
165
  catch (err) {
172
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
166
+ if ((process.env.PHREN_DEBUG))
173
167
  process.stderr.write(`[phren] claimScoreJournal unlinkClaim: ${errorMessage(err)}\n`);
174
168
  }
175
169
  }
package/mcp/dist/hooks.js CHANGED
@@ -1,18 +1,12 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { createHmac, randomUUID } from "crypto";
3
+ import { createHmac } from "crypto";
4
4
  import { execFileSync } from "child_process";
5
5
  import { fileURLToPath } from "url";
6
- import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, installPreferencesFile } from "./shared.js";
6
+ import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, installPreferencesFile, atomicWriteText } from "./shared.js";
7
7
  import { errorMessage } from "./utils.js";
8
8
  import { hookConfigPath } from "./provider-adapters.js";
9
9
  import { PACKAGE_SPEC } from "./package-metadata.js";
10
- function atomicWriteText(filePath, content) {
11
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
12
- const tmpPath = `${filePath}.tmp-${randomUUID()}`;
13
- fs.writeFileSync(tmpPath, content);
14
- fs.renameSync(tmpPath, filePath);
15
- }
16
10
  export function commandExists(cmd) {
17
11
  try {
18
12
  const whichCmd = process.platform === "win32" ? "where.exe" : "which";
@@ -215,10 +209,34 @@ function validateCodexConfig(config) {
215
209
  Array.isArray(config.hooks?.UserPromptSubmit) &&
216
210
  Array.isArray(config.hooks?.Stop));
217
211
  }
212
+ // ── mtime-based install-preferences cache (shared by readHookPreferences + readCustomHooks) ──
213
+ const _installPrefsJsonCache = new Map();
214
+ export function clearHookPrefsCache() {
215
+ _installPrefsJsonCache.clear();
216
+ }
217
+ function cachedReadInstallPrefsJson(phrenPath) {
218
+ const prefsPath = installPreferencesFile(phrenPath);
219
+ let mtimeMs;
220
+ try {
221
+ mtimeMs = fs.statSync(prefsPath).mtimeMs;
222
+ }
223
+ catch {
224
+ _installPrefsJsonCache.delete(prefsPath);
225
+ return null;
226
+ }
227
+ const cached = _installPrefsJsonCache.get(prefsPath);
228
+ if (cached && cached.mtimeMs === mtimeMs) {
229
+ return cached.parsed;
230
+ }
231
+ const parsed = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
232
+ _installPrefsJsonCache.set(prefsPath, { mtimeMs, parsed });
233
+ return parsed;
234
+ }
218
235
  function readHookPreferences(phrenPath) {
219
236
  try {
220
- const prefsPath = installPreferencesFile(phrenPath);
221
- const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
237
+ const prefs = cachedReadInstallPrefsJson(phrenPath);
238
+ if (!prefs)
239
+ return { enabled: true, toolPrefs: {} };
222
240
  const enabled = prefs.hooksEnabled !== false;
223
241
  const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object"
224
242
  ? prefs.hookTools
@@ -250,20 +268,38 @@ const VALID_HOOK_EVENTS = new Set(HOOK_EVENT_VALUES);
250
268
  export function getHookTarget(h) {
251
269
  return "webhook" in h ? h.webhook : h.command;
252
270
  }
271
+ /** Re-validate a command hook at execution time (mirrors mcp-hooks.ts validateHookCommand). */
272
+ function validateCommandAtExecution(command) {
273
+ const trimmed = command.trim();
274
+ if (!trimmed)
275
+ return "Command cannot be empty.";
276
+ if (trimmed.length > 1000)
277
+ return "Command too long (max 1000 characters).";
278
+ if (/[`$(){}&|;<>]/.test(trimmed))
279
+ return "Command contains disallowed shell characters.";
280
+ if (/\b(eval|source)\b/.test(trimmed))
281
+ return "eval and source are not permitted in hook commands.";
282
+ if (!/^[\w./~"'"]/.test(trimmed))
283
+ return "Command must begin with an executable name or path.";
284
+ return null;
285
+ }
253
286
  const DEFAULT_CUSTOM_HOOK_TIMEOUT = 5000;
254
287
  const HOOK_TIMEOUT_MS = parseInt(process.env.PHREN_HOOK_TIMEOUT_MS || '14000', 10);
255
288
  const HOOK_ERROR_LOG_MAX_LINES = 1000;
256
289
  export function readCustomHooks(phrenPath) {
257
290
  try {
258
- const prefsPath = installPreferencesFile(phrenPath);
259
- const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
260
- if (!Array.isArray(prefs.customHooks))
291
+ const prefs = cachedReadInstallPrefsJson(phrenPath);
292
+ if (!prefs || !Array.isArray(prefs.customHooks))
261
293
  return [];
262
- return prefs.customHooks.filter((h) => h &&
263
- typeof h.event === "string" &&
264
- VALID_HOOK_EVENTS.has(h.event) &&
265
- ((typeof h.command === "string" && h.command.trim().length > 0) ||
266
- (typeof h.webhook === "string" && h.webhook.trim().length > 0)));
294
+ return prefs.customHooks.filter((h) => {
295
+ if (!h || typeof h !== "object")
296
+ return false;
297
+ const rec = h;
298
+ return (typeof rec.event === "string" &&
299
+ VALID_HOOK_EVENTS.has(rec.event) &&
300
+ ((typeof rec.command === "string" && rec.command.trim().length > 0) ||
301
+ (typeof rec.webhook === "string" && rec.webhook.trim().length > 0)));
302
+ });
267
303
  }
268
304
  catch (err) {
269
305
  debugLog(`readCustomHooks: ${errorMessage(err)}`);
@@ -321,6 +357,14 @@ export function runCustomHooks(phrenPath, event, env = {}) {
321
357
  });
322
358
  continue;
323
359
  }
360
+ const cmdErr = validateCommandAtExecution(hook.command);
361
+ if (cmdErr) {
362
+ const message = `${event}: skipped hook (re-validation failed): ${cmdErr}`;
363
+ debugLog(`runCustomHooks: ${message}`);
364
+ errors.push({ code: PhrenError.VALIDATION_ERROR, message });
365
+ appendHookErrorLog(phrenPath, event, message);
366
+ continue;
367
+ }
324
368
  const shellArgs = isWindows ? ["/c", hook.command] : ["-c", hook.command];
325
369
  try {
326
370
  execFileSync(shellCmd, shellArgs, {
package/mcp/dist/index.js CHANGED
@@ -48,13 +48,13 @@ function cleanStaleLocks(phrenPath) {
48
48
  }
49
49
  }
50
50
  catch (err) {
51
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
51
+ if ((process.env.PHREN_DEBUG))
52
52
  process.stderr.write(`[phren] cleanStaleLocks statFile: ${errorMessage(err)}\n`);
53
53
  }
54
54
  }
55
55
  }
56
56
  catch (err) {
57
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
57
+ if ((process.env.PHREN_DEBUG))
58
58
  process.stderr.write(`[phren] cleanStaleLocks readdir: ${errorMessage(err)}\n`);
59
59
  }
60
60
  }
@@ -88,7 +88,7 @@ async function main() {
88
88
  db?.close();
89
89
  }
90
90
  catch (err) {
91
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
91
+ if ((process.env.PHREN_DEBUG))
92
92
  process.stderr.write(`[phren] rebuildIndex dbClose: ${errorMessage(err)}\n`);
93
93
  }
94
94
  db = await buildIndex(phrenPath, profile);
@@ -158,7 +158,7 @@ async function main() {
158
158
  trackToolCall(phrenPath, registeredName);
159
159
  }
160
160
  catch (err) {
161
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
161
+ if ((process.env.PHREN_DEBUG))
162
162
  process.stderr.write(`[phren] trackToolCall: ${errorMessage(err)}\n`);
163
163
  }
164
164
  return handler(...args);
@@ -4,27 +4,16 @@
4
4
  */
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
- import { randomUUID } from "crypto";
8
- import { execFileSync } from "child_process";
9
- import { buildLifecycleCommands } from "./hooks.js";
10
- import { EXEC_TIMEOUT_QUICK_MS, isRecord, hookConfigPath, homePath, readRootManifest, } from "./shared.js";
7
+ import { buildLifecycleCommands, commandExists } from "./hooks.js";
8
+ import { isRecord, hookConfigPath, homePath, readRootManifest, atomicWriteText, } from "./shared.js";
11
9
  import { isFeatureEnabled, errorMessage } from "./utils.js";
12
10
  import { probeVsCodeConfig, resolveCodexMcpConfig, resolveCopilotMcpConfig, resolveCursorMcpConfig, } from "./provider-adapters.js";
13
11
  import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init-preferences.js";
14
- import { resolveEntryScript, VERSION } from "./init-shared.js";
15
- function log(msg) {
16
- process.stdout.write(msg + "\n");
17
- }
12
+ import { resolveEntryScript, log, VERSION } from "./init-shared.js";
18
13
  function getObjectProp(value, key) {
19
14
  const candidate = value[key];
20
15
  return isRecord(candidate) ? candidate : undefined;
21
16
  }
22
- function atomicWriteText(filePath, content) {
23
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
- const tmpPath = `${filePath}.tmp-${randomUUID()}`;
25
- fs.writeFileSync(tmpPath, content);
26
- fs.renameSync(tmpPath, filePath);
27
- }
28
17
  export function patchJsonFile(filePath, patch) {
29
18
  let data = {};
30
19
  if (fs.existsSync(filePath)) {
@@ -44,16 +33,6 @@ export function patchJsonFile(filePath, patch) {
44
33
  patch(data);
45
34
  atomicWriteText(filePath, JSON.stringify(data, null, 2) + "\n");
46
35
  }
47
- function commandExists(cmd) {
48
- try {
49
- const whichCmd = process.platform === "win32" ? "where.exe" : "which";
50
- execFileSync(whichCmd, [cmd], { stdio: ["ignore", "ignore", "ignore"], timeout: EXEC_TIMEOUT_QUICK_MS });
51
- return true;
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
36
  function buildMcpServerConfig(phrenPath) {
58
37
  const entryScript = resolveEntryScript();
59
38
  if (entryScript && fs.existsSync(entryScript)) {
@@ -228,7 +207,7 @@ export function configureClaude(phrenPath, opts = {}) {
228
207
  eventHooks.push({ matcher: "", hooks: [hookBody] });
229
208
  }
230
209
  };
231
- const toolHookEnabled = hooksEnabled && (isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false) || isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false));
210
+ const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false);
232
211
  if (hooksEnabled) {
233
212
  upsertPhrenHook("UserPromptSubmit", {
234
213
  type: "command",
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  import * as crypto from "crypto";
7
7
  import { debugLog, installPreferencesFile } from "./phren-paths.js";
8
8
  import { errorMessage } from "./utils.js";
9
- import { withFileLock } from "./governance-locks.js";
9
+ import { withFileLock } from "./shared-governance.js";
10
10
  function preferencesFile(phrenPath) {
11
11
  return installPreferencesFile(phrenPath);
12
12
  }
@@ -11,7 +11,7 @@ import { getMachineName } from "./machine-identity.js";
11
11
  import { execFileSync } from "child_process";
12
12
  import { GOVERNANCE_SCHEMA_VERSION, } from "./shared-governance.js";
13
13
  import { STOP_WORDS, errorMessage } from "./utils.js";
14
- import { ROOT, STARTER_DIR, VERSION, resolveEntryScript } from "./init-shared.js";
14
+ import { ROOT, STARTER_DIR, VERSION, resolveEntryScript, commandVersion, versionAtLeast, nearestWritableTarget } from "./init-shared.js";
15
15
  import { readInstallPreferences } from "./init-preferences.js";
16
16
  import { TASKS_FILENAME } from "./data-tasks.js";
17
17
  import { getProjectOwnershipDefault, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, } from "./project-config.js";
@@ -311,21 +311,6 @@ export function getVerifyOutcomeNote(phrenPath, checks) {
311
311
  }
312
312
  return "Some reported issues are optional for your chosen install mode; review git-remote / MCP failures separately from hard failures.";
313
313
  }
314
- function commandVersion(cmd, args = ["--version"]) {
315
- const effectiveCmd = process.platform === "win32" && (cmd === "npm" || cmd === "npx") ? `${cmd}.cmd` : cmd;
316
- try {
317
- return execFileSync(effectiveCmd, args, {
318
- encoding: "utf8",
319
- stdio: ["ignore", "pipe", "ignore"],
320
- shell: process.platform === "win32" && effectiveCmd.endsWith(".cmd"),
321
- timeout: EXEC_TIMEOUT_QUICK_MS,
322
- }).trim();
323
- }
324
- catch (err) {
325
- debugLog(`commandVersion ${effectiveCmd} failed: ${errorMessage(err)}`);
326
- return null;
327
- }
328
- }
329
314
  export function getHookEntrypointCheck(deps = {}) {
330
315
  const pathExists = deps.pathExists ?? fs.existsSync;
331
316
  const versionReader = deps.versionReader ?? commandVersion;
@@ -344,40 +329,6 @@ export function getHookEntrypointCheck(deps = {}) {
344
329
  fix: hookEntrypointOk ? undefined : "Rebuild phren: `npm run build` or reinstall the package, and ensure npm/npx is available for hook fallbacks",
345
330
  };
346
331
  }
347
- function parseSemverTriple(raw) {
348
- const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
349
- if (!match)
350
- return null;
351
- return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
352
- }
353
- function versionAtLeast(raw, major, minor = 0) {
354
- if (!raw)
355
- return false;
356
- const parsed = parseSemverTriple(raw);
357
- if (!parsed)
358
- return false;
359
- const [m, n] = parsed;
360
- if (m !== major)
361
- return m > major;
362
- return n >= minor;
363
- }
364
- function nearestWritableTarget(filePath) {
365
- let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
366
- while (!fs.existsSync(probe)) {
367
- const parent = path.dirname(probe);
368
- if (parent === probe)
369
- return false;
370
- probe = parent;
371
- }
372
- try {
373
- fs.accessSync(probe, fs.constants.W_OK);
374
- return true;
375
- }
376
- catch (err) {
377
- debugLog(`nearestWritableTarget failed for ${filePath}: ${errorMessage(err)}`);
378
- return false;
379
- }
380
- }
381
332
  function gitRemoteStatus(phrenPath) {
382
333
  try {
383
334
  execFileSync("git", ["-C", phrenPath, "rev-parse", "--is-inside-work-tree"], {
@@ -1087,14 +1038,14 @@ export function updateMachinesYaml(phrenPath, machine, profile) {
1087
1038
  }
1088
1039
  }
1089
1040
  catch (err) {
1090
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1091
- process.stderr.write(`[phren] updateMachinesYaml parse: ${err instanceof Error ? err.message : String(err)}\n`);
1041
+ if ((process.env.PHREN_DEBUG))
1042
+ process.stderr.write(`[phren] updateMachinesYaml parse: ${errorMessage(err)}\n`);
1092
1043
  }
1093
1044
  // Passive init/link refreshes should keep an existing mapping; explicit overrides can remap.
1094
1045
  if (hasExistingMapping && !machine && !profile)
1095
1046
  return;
1096
1047
  const mapping = setMachineProfile(phrenPath, machineName, profileName);
1097
- if (!mapping.ok && (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
1048
+ if (!mapping.ok && (process.env.PHREN_DEBUG)) {
1098
1049
  process.stderr.write(`[phren] updateMachinesYaml setMachineProfile: ${mapping.error}\n`);
1099
1050
  }
1100
1051
  }
@@ -1289,8 +1240,8 @@ export function runPostInitVerify(phrenPath) {
1289
1240
  ftsOk = entries.some(d => d.isDirectory() && !d.name.startsWith("."));
1290
1241
  }
1291
1242
  catch (err) {
1292
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1293
- process.stderr.write(`[phren] runPostInitVerify projectScan: ${err instanceof Error ? err.message : String(err)}\n`);
1243
+ if ((process.env.PHREN_DEBUG))
1244
+ process.stderr.write(`[phren] runPostInitVerify projectScan: ${errorMessage(err)}\n`);
1294
1245
  ftsOk = false;
1295
1246
  }
1296
1247
  checks.push({
@@ -2,8 +2,11 @@
2
2
  * Shared constants and utilities for init modules.
3
3
  * Kept separate to avoid circular dependencies between init-config and init-setup.
4
4
  */
5
+ import * as fs from "fs";
5
6
  import * as path from "path";
6
- import { homePath } from "./shared.js";
7
+ import { execFileSync } from "child_process";
8
+ import { homePath, EXEC_TIMEOUT_QUICK_MS, debugLog } from "./shared.js";
9
+ import { errorMessage } from "./utils.js";
7
10
  import { ROOT as PACKAGE_ROOT, VERSION } from "./package-metadata.js";
8
11
  export const ROOT = PACKAGE_ROOT;
9
12
  export { VERSION };
@@ -15,6 +18,55 @@ export function resolveEntryScript() {
15
18
  export function log(msg) {
16
19
  process.stdout.write(msg + "\n");
17
20
  }
21
+ export function commandVersion(cmd, args = ["--version"]) {
22
+ const effectiveCmd = process.platform === "win32" && (cmd === "npm" || cmd === "npx") ? `${cmd}.cmd` : cmd;
23
+ try {
24
+ return execFileSync(effectiveCmd, args, {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ shell: process.platform === "win32" && effectiveCmd.endsWith(".cmd"),
28
+ timeout: EXEC_TIMEOUT_QUICK_MS,
29
+ }).trim();
30
+ }
31
+ catch (err) {
32
+ debugLog(`commandVersion ${effectiveCmd} failed: ${errorMessage(err)}`);
33
+ return null;
34
+ }
35
+ }
36
+ export function parseSemverTriple(raw) {
37
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
38
+ if (!match)
39
+ return null;
40
+ return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
41
+ }
42
+ export function versionAtLeast(raw, major, minor = 0) {
43
+ if (!raw)
44
+ return false;
45
+ const parsed = parseSemverTriple(raw);
46
+ if (!parsed)
47
+ return false;
48
+ const [m, n] = parsed;
49
+ if (m !== major)
50
+ return m > major;
51
+ return n >= minor;
52
+ }
53
+ export function nearestWritableTarget(filePath) {
54
+ let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
55
+ while (!fs.existsSync(probe)) {
56
+ const parent = path.dirname(probe);
57
+ if (parent === probe)
58
+ return false;
59
+ probe = parent;
60
+ }
61
+ try {
62
+ fs.accessSync(probe, fs.constants.W_OK);
63
+ return true;
64
+ }
65
+ catch (err) {
66
+ debugLog(`nearestWritableTarget failed for ${filePath}: ${errorMessage(err)}`);
67
+ return false;
68
+ }
69
+ }
18
70
  export async function confirmPrompt(message) {
19
71
  if (process.env.CI === "true" || !process.stdin.isTTY)
20
72
  return true;