aiden-runtime 4.9.0 → 4.9.1

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 CHANGED
@@ -94,7 +94,7 @@ Windows · Linux · WSL · macOS (API Mode)
94
94
  ![Built solo](https://img.shields.io/badge/Built-solo-B8A893?style=flat-square)
95
95
  ![By Taracod](https://img.shields.io/badge/By-Taracod-FF6B35?style=flat-square)
96
96
  ![White Lotus](https://img.shields.io/badge/Brand-White_Lotus-FFB088?style=flat-square)
97
- ![v4.9.0](https://img.shields.io/badge/Latest-v4.9.0-4ADE80?style=flat-square)
97
+ ![v4.9.1](https://img.shields.io/badge/Latest-v4.9.1-4ADE80?style=flat-square)
98
98
 
99
99
  </div>
100
100
 
@@ -568,7 +568,7 @@ async function main(argv, opts = {}) {
568
568
  });
569
569
  program
570
570
  .command('voice [args...]')
571
- .description('Voice diagnostics + one-shot TTS / transcribe (Phase v4.1-voice-cli). ' +
571
+ .description('Voice diagnostics + one-shot TTS / transcribe. ' +
572
572
  'Usage: aiden voice doctor | tts "<text>" | transcribe <file>')
573
573
  .allowUnknownOption()
574
574
  .action(async (args) => {
@@ -611,7 +611,7 @@ async function main(argv, opts = {}) {
611
611
  // v4.1 placeholders. (`tui` graduated to a real flag in Phase 15.)
612
612
  program
613
613
  .command('cron [args...]')
614
- .description('Cron diagnostics + one-shot list / run (Phase v4.1 hardened cron). ' +
614
+ .description('Cron diagnostics + one-shot list / run. ' +
615
615
  'Usage: aiden cron status | list | run <id>')
616
616
  .allowUnknownOption()
617
617
  .action(async (args) => {
@@ -1656,6 +1656,10 @@ class ChatSession {
1656
1656
  // .update_check.json cache so subsequent boots stay quiet until
1657
1657
  // a newer release ships.
1658
1658
  try {
1659
+ // v4.9.1 — modal sits BELOW the welcome banner with a blank
1660
+ // separator. Prevents the box from visually overlapping the
1661
+ // boot card on first-paint (smoke-reported regression).
1662
+ display.write('\n');
1659
1663
  await this.maybeShowBootUpdatePrompt();
1660
1664
  }
1661
1665
  catch { /* never let the update prompt crash boot */ }
@@ -1713,13 +1717,24 @@ class ChatSession {
1713
1717
  });
1714
1718
  if (choice === 'install') {
1715
1719
  if (method.inProcessInstallSupported) {
1716
- this.opts.display.write(`Installing aiden-runtime ${status.latest}…\n`);
1717
- const result = await ei.executeInstall({ packageSpec: `aiden-runtime@${status.latest}` });
1720
+ // v4.9.1 drive a live progress bar off the executor's
1721
+ // phase callback. The bar degrades cleanly on non-TTY, NO_COLOR,
1722
+ // and dumb terminals — see cli/v4/ui/progressBar.ts.
1723
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1724
+ const pb = require('./ui/progressBar');
1725
+ const bar = pb.startProgressBar({
1726
+ label: `Installing aiden-runtime ${status.latest}...`,
1727
+ phases: ['spawning', 'resolving', 'downloading', 'extracting', 'verifying', 'installed'],
1728
+ });
1729
+ const result = await ei.executeInstall({
1730
+ packageSpec: `aiden-runtime@${status.latest}`,
1731
+ onPhase: (p) => { bar.setPhase(p); bar.setPercent(pb.npmInstallPhasePercent(p)); },
1732
+ });
1718
1733
  if (result.success) {
1719
- this.opts.display.write(`aiden-runtime ${result.installedVersion ?? status.latest} installed.\n`);
1720
- this.opts.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
1734
+ bar.complete(`aiden-runtime ${result.installedVersion ?? status.latest} installed. Restart Aiden to apply: type /quit then re-run \`aiden\`.`);
1721
1735
  }
1722
1736
  else {
1737
+ bar.fail('Install failed.');
1723
1738
  this.opts.display.warn(result.error ?? 'Install failed (no error message).');
1724
1739
  }
1725
1740
  }
@@ -104,10 +104,15 @@ async function runDaemonSubcommand(action, args, opts = {}) {
104
104
  writeErr: err,
105
105
  });
106
106
  }
107
- default:
107
+ default: {
108
108
  err(`Unknown daemon action: ${action}\n`);
109
+ const { closestAction } = await Promise.resolve().then(() => __importStar(require('../util/closestAction')));
110
+ const m = closestAction(action, ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs', 'doctor']);
111
+ if (m)
112
+ err(`Did you mean: ${m}?\n\n`);
109
113
  err('Actions: install, uninstall, start, stop, restart, status, logs, doctor\n');
110
114
  return 2;
115
+ }
111
116
  }
112
117
  }
113
118
  const SYSTEMD_UNIT_NAME = 'aiden.service';
@@ -89,7 +89,7 @@ function collectDoctorChecks(rootDir) {
89
89
  ORDER BY started_at DESC LIMIT 1`).get();
90
90
  if (!inc) {
91
91
  checks.push({ name: 'recent incarnation', status: 'warn',
92
- detail: 'no daemon_incarnations rows (daemon never booted post-Slice-4)',
92
+ detail: 'no daemon_incarnations rows (daemon never started in this root)',
93
93
  fixable: false });
94
94
  }
95
95
  else {
@@ -22,44 +22,63 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.daemonStatus = void 0;
25
+ exports.daemonStatus = exports.DAEMON_SHELL_ONLY = void 0;
26
+ exports.dispatchDaemonSlash = dispatchDaemonSlash;
26
27
  const node_fs_1 = __importDefault(require("node:fs"));
27
28
  const node_os_1 = __importDefault(require("node:os"));
28
29
  const node_path_1 = __importDefault(require("node:path"));
29
- const daemon_1 = require("../../../core/v4/daemon");
30
+ const daemon_1 = require("./daemon");
31
+ const daemon_2 = require("../../../core/v4/daemon");
30
32
  const paths_1 = require("../../../core/v4/paths");
33
+ /**
34
+ * v4.9.1 amendment — `/daemon` defaults to `doctor` and routes
35
+ * `doctor` / `logs` to the existing `runDaemonSubcommand`. The pure-
36
+ * REPL `/daemon status` (in-process snapshot) stays inline. Lifecycle
37
+ * ops shell-hint — they need terminal control we can't grant inside chat.
38
+ */
39
+ exports.DAEMON_SHELL_ONLY = new Set(['install', 'uninstall', 'start', 'stop', 'restart']);
40
+ async function dispatchDaemonSlash(opts) {
41
+ const a = (opts.action || 'doctor').toLowerCase();
42
+ if (exports.DAEMON_SHELL_ONLY.has(a)) {
43
+ opts.write(`⚠ /daemon ${a} not available inside chat (requires terminal control)\n`);
44
+ opts.write(' Quit (/quit) and run from shell:\n\n');
45
+ const tail = opts.args.length > 0 ? ' ' + opts.args.join(' ') : '';
46
+ opts.write(` aiden daemon ${a}${tail}\n`);
47
+ return;
48
+ }
49
+ if (a === 'status' && opts.paintStatus) {
50
+ try {
51
+ opts.paintStatus();
52
+ }
53
+ catch (e) {
54
+ opts.warn(`/daemon status: failed to read state (${e instanceof Error ? e.message : String(e)})`);
55
+ }
56
+ return;
57
+ }
58
+ await opts.runDaemon(a, opts.args, { writeOut: opts.write, writeErr: opts.write });
59
+ }
31
60
  exports.daemonStatus = {
32
61
  name: 'daemon',
33
- description: 'Show daemon status (read-only). Use `aiden daemon` for lifecycle.',
62
+ description: 'Daemon diagnostics (doctor / status / logs).',
34
63
  category: 'system',
35
64
  icon: '⚙',
36
65
  handler: async (ctx) => {
37
- const sub = (ctx.args[0] ?? 'status').toLowerCase();
38
- if (sub !== 'status') {
39
- ctx.display.printError('Usage: /daemon status\n' +
40
- 'For lifecycle commands (install / start / stop / restart / logs), use the top-level CLI:\n' +
41
- ' aiden daemon install\n' +
42
- ' aiden daemon start\n' +
43
- ' aiden daemon stop\n' +
44
- ' aiden daemon status\n' +
45
- ' aiden daemon logs');
46
- return {};
47
- }
48
- try {
49
- const snapshot = readSnapshot();
50
- printSnapshot(snapshot, ctx);
51
- }
52
- catch (e) {
53
- ctx.display.warn(`/daemon status: failed to read state (${e instanceof Error ? e.message : String(e)})`);
54
- }
66
+ await dispatchDaemonSlash({
67
+ action: ctx.args[0] ?? 'doctor',
68
+ args: ctx.args.slice(1),
69
+ write: (s) => ctx.display.write(s),
70
+ warn: (s) => ctx.display.warn(s),
71
+ runDaemon: daemon_1.runDaemonSubcommand,
72
+ paintStatus: () => printSnapshot(readSnapshot(), ctx),
73
+ });
55
74
  return {};
56
75
  },
57
76
  };
58
77
  // ── Snapshot collector ─────────────────────────────────────────────────────
59
78
  function readSnapshot() {
60
79
  const aidenRoot = (0, paths_1.resolveAidenRoot)();
61
- const dbPath = (0, daemon_1.daemonDbPath)(aidenRoot);
62
- const lockPath = (0, daemon_1.daemonRuntimeLockPath)(aidenRoot);
80
+ const dbPath = (0, daemon_2.daemonDbPath)(aidenRoot);
81
+ const lockPath = (0, daemon_2.daemonRuntimeLockPath)(aidenRoot);
63
82
  // ── Liveness via the in-process bootstrap handle first, then fall
64
83
  // back to the runtime.lock PID check (covers the case where the
65
84
  // daemon is another process and we're a REPL inspecting its db).
@@ -67,11 +86,11 @@ function readSnapshot() {
67
86
  let instanceId = null;
68
87
  let port = null;
69
88
  let uptimeMs = null;
70
- const handle = (0, daemon_1.getDaemonHandle)();
89
+ const handle = (0, daemon_2.getDaemonHandle)();
71
90
  if (handle?.active && handle.instanceId) {
72
91
  running = true;
73
92
  instanceId = handle.instanceId;
74
- port = (0, daemon_1.getDaemonConfig)().port;
93
+ port = (0, daemon_2.getDaemonConfig)().port;
75
94
  if (handle.instanceTracker) {
76
95
  // instanceTracker has a `getStartedAt` if exposed; otherwise
77
96
  // derive from daemon_instances row below.
@@ -105,7 +124,7 @@ function readSnapshot() {
105
124
  dailyBudget: null,
106
125
  };
107
126
  }
108
- const db = (0, daemon_1.openDaemonDb)(dbPath);
127
+ const db = (0, daemon_2.openDaemonDb)(dbPath);
109
128
  // Uptime from the instance row when we have an instanceId.
110
129
  if (running && instanceId) {
111
130
  try {
@@ -76,6 +76,9 @@ exports.SUBSECTION_MAP = {
76
76
  recovery: 'System',
77
77
  // v4.6 ONB1 slice 10 — new-user guided tour.
78
78
  walkthrough: 'System',
79
+ // v4.9.1 amendment — REPL surfaces for memory + hooks (daemon already mapped).
80
+ memory: 'System',
81
+ hooks: 'System',
79
82
  // ── Authentication ──
80
83
  auth: 'Authentication',
81
84
  // ── Help ──
@@ -23,6 +23,39 @@
23
23
  * directory and runs a rescan, but NEVER auto-trusts anything (trust
24
24
  * remains an explicit, deliberate user action).
25
25
  */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
26
59
  var __importDefault = (this && this.__importDefault) || function (mod) {
27
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
61
  };
@@ -78,10 +111,15 @@ async function runHooksSubcommand(action, args, opts = {}) {
78
111
  case 'audit': return await cmdAudit(ctx);
79
112
  case '--help':
80
113
  case 'help': return cmdHelp(out);
81
- default:
114
+ default: {
82
115
  err(`Unknown hooks action: ${effective}\n`);
116
+ const { closestAction } = await Promise.resolve().then(() => __importStar(require('../util/closestAction')));
117
+ const m = closestAction(effective, ['list', 'show', 'trust', 'revoke', 'rescan', 'test', 'doctor', 'audit']);
118
+ if (m)
119
+ err(`Did you mean: ${m}?\n\n`);
83
120
  cmdHelp(err);
84
121
  return 2;
122
+ }
85
123
  }
86
124
  }
87
125
  finally {
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hooks = exports.HOOKS_SHELL_ONLY = void 0;
4
+ exports.dispatchHooksSlash = dispatchHooksSlash;
5
+ const hooks_1 = require("./hooks");
6
+ /** Actions that need an interactive confirmation prompt. */
7
+ exports.HOOKS_SHELL_ONLY = new Set(['trust', 'revoke']);
8
+ async function dispatchHooksSlash(opts) {
9
+ const a = (opts.action || 'list').toLowerCase();
10
+ if (exports.HOOKS_SHELL_ONLY.has(a)) {
11
+ opts.write(`⚠ /hooks ${a} not available inside chat (needs confirmation prompt)\n`);
12
+ opts.write(' Quit (/quit) and run from shell:\n\n');
13
+ const tail = opts.args.length > 0 ? ' ' + opts.args.join(' ') : '';
14
+ opts.write(` aiden hooks ${a}${tail}\n`);
15
+ return;
16
+ }
17
+ await opts.runHooks(a, opts.args, { writeOut: opts.write, writeErr: opts.write });
18
+ }
19
+ exports.hooks = {
20
+ name: 'hooks',
21
+ description: 'Manage hooks (list / show / rescan / test / doctor / audit).',
22
+ category: 'system',
23
+ icon: '🪝',
24
+ handler: async (ctx) => {
25
+ await dispatchHooksSlash({
26
+ action: ctx.args[0] ?? 'list',
27
+ args: ctx.args.slice(1),
28
+ write: (s) => ctx.display.write(s),
29
+ runHooks: hooks_1.runHooksSubcommand,
30
+ });
31
+ return {};
32
+ },
33
+ };
@@ -12,7 +12,7 @@
12
12
  * and registers each on the global CommandRegistry at boot.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.allCommands = exports.walkthrough = exports.recovery = exports.spawnPause = exports.plannerGuard = exports.suggestions = exports.daemonStatus = exports.browserDepth = exports.tce = exports.sandbox = exports.update = exports.reloadSoul = exports.history = exports.show = exports.status = exports.voice = exports.channel = exports.setup = exports.cron = exports.doctor = exports.license = exports.auth = exports.plugins = exports.streaming = exports.debugPrompt = exports.identity = exports.providers = exports.quit = exports.clear = exports.verbose = exports.reasoning = exports.reloadMcp = exports.skills = exports.theme = exports.skin = exports.yolo = exports.usage = exports.compress = exports.title = exports.save = exports.personality = exports.model = exports.tools = exports.help = void 0;
15
+ exports.allCommands = exports.hooks = exports.memory = exports.walkthrough = exports.recovery = exports.spawnPause = exports.plannerGuard = exports.suggestions = exports.daemonStatus = exports.browserDepth = exports.tce = exports.sandbox = exports.update = exports.reloadSoul = exports.history = exports.show = exports.status = exports.voice = exports.channel = exports.setup = exports.cron = exports.doctor = exports.license = exports.auth = exports.plugins = exports.streaming = exports.debugPrompt = exports.identity = exports.providers = exports.quit = exports.clear = exports.verbose = exports.reasoning = exports.reloadMcp = exports.skills = exports.theme = exports.skin = exports.yolo = exports.usage = exports.compress = exports.title = exports.save = exports.personality = exports.model = exports.tools = exports.help = void 0;
16
16
  const help_1 = require("./help");
17
17
  Object.defineProperty(exports, "help", { enumerable: true, get: function () { return help_1.help; } });
18
18
  const tools_1 = require("./tools");
@@ -106,6 +106,11 @@ Object.defineProperty(exports, "recovery", { enumerable: true, get: function ()
106
106
  // ONB1 slice 10 — new-user guided tour.
107
107
  const walkthrough_1 = require("./walkthrough");
108
108
  Object.defineProperty(exports, "walkthrough", { enumerable: true, get: function () { return walkthrough_1.walkthrough; } });
109
+ // v4.9.1 amendment — REPL slash surfaces for memory + hooks (mirrors CLI).
110
+ const memorySlash_1 = require("./memorySlash");
111
+ Object.defineProperty(exports, "memory", { enumerable: true, get: function () { return memorySlash_1.memory; } });
112
+ const hooksSlash_1 = require("./hooksSlash");
113
+ Object.defineProperty(exports, "hooks", { enumerable: true, get: function () { return hooksSlash_1.hooks; } });
109
114
  /** All built-in system commands, in canonical order. */
110
115
  exports.allCommands = [
111
116
  help_1.help,
@@ -158,6 +163,9 @@ exports.allCommands = [
158
163
  recovery_1.recovery,
159
164
  // ONB1 slice 10 — new-user guided tour.
160
165
  walkthrough_1.walkthrough,
166
+ // v4.9.1 amendment — REPL slash surfaces mirroring CLI subcommands.
167
+ memorySlash_1.memory,
168
+ hooksSlash_1.hooks,
161
169
  clear_1.clear,
162
170
  quit_1.quit,
163
171
  ];
@@ -155,10 +155,15 @@ async function runMemorySubcommand(action, args, opts = {}) {
155
155
  case 'review': return cmdReview(args, paths, opts, out, err, json);
156
156
  case '--help':
157
157
  case 'help': return cmdHelp(out);
158
- default:
158
+ default: {
159
159
  err(`Unknown memory action: ${effective}\n`);
160
+ const { closestAction } = await Promise.resolve().then(() => __importStar(require('../util/closestAction')));
161
+ const m = closestAction(effective, ['list', 'show', 'add', 'remove', 'edit', 'backup', 'restore', 'diff', 'namespaces', 'pending', 'approve', 'reject', 'review']);
162
+ if (m)
163
+ err(`Did you mean: ${m}?\n\n`);
160
164
  cmdHelp(err);
161
165
  return 2;
166
+ }
162
167
  }
163
168
  }
164
169
  function cmdHelp(write) {
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memory = exports.MEMORY_SHELL_ONLY = void 0;
4
+ exports.dispatchMemorySlash = dispatchMemorySlash;
5
+ const memory_1 = require("./memory");
6
+ /** Actions that need the full CLI surface (confirmation / destructive). */
7
+ exports.MEMORY_SHELL_ONLY = new Set(['remove', 'restore']);
8
+ /**
9
+ * Pure dispatch — exported for tests + reuse. Either prints a shell
10
+ * hint OR delegates to the provided `runMemory` runner. Side effects
11
+ * confined to the supplied `write` sink.
12
+ */
13
+ async function dispatchMemorySlash(opts) {
14
+ const a = (opts.action || 'list').toLowerCase();
15
+ if (exports.MEMORY_SHELL_ONLY.has(a)) {
16
+ opts.write(`⚠ /memory ${a} not available inside chat (destructive operation)\n`);
17
+ opts.write(' Quit (/quit) and run from shell:\n\n');
18
+ const tail = opts.args.length > 0 ? ' ' + opts.args.join(' ') : '';
19
+ opts.write(` aiden memory ${a}${tail}\n`);
20
+ return;
21
+ }
22
+ await opts.runMemory(a, opts.args, { writeOut: opts.write, writeErr: opts.write });
23
+ }
24
+ exports.memory = {
25
+ name: 'memory',
26
+ description: 'Manage memory (list / show / add / namespaces / pending / approve / review).',
27
+ category: 'system',
28
+ icon: '🧠',
29
+ handler: async (ctx) => {
30
+ await dispatchMemorySlash({
31
+ action: ctx.args[0] ?? 'list',
32
+ args: ctx.args.slice(1),
33
+ write: (s) => ctx.display.write(s),
34
+ runMemory: memory_1.runMemorySubcommand,
35
+ });
36
+ return {};
37
+ },
38
+ };
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/ui/progressBar.ts — v4.9.1 reusable progress animation.
10
+ * Auto-detects TTY / NO_COLOR / TERM=dumb / CI to pick render mode
11
+ * (block glyphs vs `#-`, color vs plain, animated vs once-per-second
12
+ * non-TTY lines). Cursor hidden during animation, restored on exit.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.detectRenderMode = detectRenderMode;
16
+ exports.renderLine = renderLine;
17
+ exports.startProgressBar = startProgressBar;
18
+ exports.npmInstallPhasePercent = npmInstallPhasePercent;
19
+ exports.detectNpmPhase = detectNpmPhase;
20
+ const DEFAULT_WIDTH = 28;
21
+ const DEFAULT_TICK_MS = 100;
22
+ /** Minimum elapsed before we paint anything — avoids flicker on sub-300ms ops. */
23
+ const PAINT_AFTER_MS = 300;
24
+ const ANSI_HIDE_CURSOR = '\x1b[?25l';
25
+ const ANSI_SHOW_CURSOR = '\x1b[?25h';
26
+ const ANSI_CLEAR_LINE = '\r\x1b[2K';
27
+ const ANSI_BRAND = '\x1b[38;2;255;107;53m'; // RGB 255,107,53 (Aiden orange)
28
+ const ANSI_MUTED = '\x1b[38;2;106;106;106m';
29
+ const ANSI_SUCCESS = '\x1b[38;2;127;194;139m';
30
+ const ANSI_ERROR = '\x1b[38;2;224;90;90m';
31
+ const ANSI_RESET = '\x1b[0m';
32
+ /** Detect the right render mode from TTY + env. Pure function. */
33
+ function detectRenderMode(isTTY, env = process.env) {
34
+ if (!isTTY)
35
+ return { color: false, blocks: false, animated: false };
36
+ const noColor = env.NO_COLOR !== undefined && env.NO_COLOR !== '';
37
+ const dumb = env.TERM === 'dumb' || env.CI === 'true' || env.CI === '1';
38
+ return {
39
+ color: !noColor && !dumb,
40
+ blocks: !dumb,
41
+ animated: true,
42
+ };
43
+ }
44
+ /**
45
+ * Build the rendered line (without trailing newline). Pure so tests
46
+ * can assert byte-for-byte without timing.
47
+ */
48
+ function renderLine(opts) {
49
+ const pct = Math.max(0, Math.min(100, Math.round(opts.percent)));
50
+ const filled = Math.round((pct / 100) * opts.width);
51
+ const empty = opts.width - filled;
52
+ const full = opts.mode.blocks ? '█' : '#';
53
+ const blank = opts.mode.blocks ? '░' : '-';
54
+ const elapsed = `${(opts.elapsedMs / 1000).toFixed(1)}s`;
55
+ const bar = full.repeat(filled) + blank.repeat(empty);
56
+ if (opts.mode.color) {
57
+ return `${ANSI_BRAND}[${bar}]${ANSI_RESET} ${pct}% ${ANSI_MUTED}${opts.phase}${ANSI_RESET} ${ANSI_MUTED}${elapsed}${ANSI_RESET}`;
58
+ }
59
+ return `[${bar}] ${pct}% ${opts.phase} ${elapsed}`;
60
+ }
61
+ /**
62
+ * Start a progress bar. Returns a controller object. Never throws —
63
+ * any I/O failure on output degrades the bar to a silent no-op while
64
+ * still honoring `complete` / `fail` semantics for the caller.
65
+ */
66
+ function startProgressBar(opts) {
67
+ const width = opts.width ?? DEFAULT_WIDTH;
68
+ const out = opts.out ?? process.stdout;
69
+ const env = opts.env ?? process.env;
70
+ const tickMs = opts.tickMs ?? DEFAULT_TICK_MS;
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ const isTTY = opts.isTTY ?? Boolean(out.isTTY);
73
+ const mode = detectRenderMode(isTTY, env);
74
+ const startedAt = Date.now();
75
+ let phase = opts.phases[0] ?? '';
76
+ let percent = 0;
77
+ let painted = false;
78
+ let closed = false;
79
+ const write = (s) => {
80
+ try {
81
+ out.write(s);
82
+ }
83
+ catch { /* swallow — never break caller */ }
84
+ };
85
+ // SIGINT: restore cursor + clear the partial line before bubbling.
86
+ const onSigint = () => {
87
+ try {
88
+ write(ANSI_CLEAR_LINE + ANSI_SHOW_CURSOR);
89
+ }
90
+ catch { /* noop */ }
91
+ };
92
+ if (mode.animated) {
93
+ try {
94
+ process.once('SIGINT', onSigint);
95
+ }
96
+ catch { /* noop */ }
97
+ }
98
+ // Label line paints once, immediately.
99
+ write(`${mode.color ? ANSI_MUTED : ''}${opts.label}${mode.color ? ANSI_RESET : ''}\n`);
100
+ const paint = () => {
101
+ if (closed)
102
+ return;
103
+ const elapsedMs = Date.now() - startedAt;
104
+ if (elapsedMs < PAINT_AFTER_MS)
105
+ return;
106
+ const line = renderLine({ width, percent, phase, elapsedMs, mode });
107
+ if (mode.animated) {
108
+ if (!painted) {
109
+ write(ANSI_HIDE_CURSOR);
110
+ painted = true;
111
+ }
112
+ write(ANSI_CLEAR_LINE + line);
113
+ }
114
+ else {
115
+ write(line + '\n');
116
+ }
117
+ };
118
+ let timer = null;
119
+ if (mode.animated) {
120
+ timer = setInterval(paint, tickMs);
121
+ if (typeof timer.unref === 'function')
122
+ timer.unref();
123
+ }
124
+ const close = (icon, color, message) => {
125
+ if (closed)
126
+ return;
127
+ closed = true;
128
+ if (timer)
129
+ clearInterval(timer);
130
+ try {
131
+ process.removeListener('SIGINT', onSigint);
132
+ }
133
+ catch { /* noop */ }
134
+ const finalLine = mode.color
135
+ ? `${color}${icon}${ANSI_RESET} ${message}`
136
+ : `${icon} ${message}`;
137
+ if (mode.animated && painted)
138
+ write(ANSI_CLEAR_LINE);
139
+ write(finalLine + '\n');
140
+ if (mode.animated)
141
+ write(ANSI_SHOW_CURSOR);
142
+ };
143
+ return {
144
+ setPhase(name) { phase = name; if (!mode.animated)
145
+ paint(); },
146
+ setPercent(p) { percent = p; if (!mode.animated)
147
+ paint(); },
148
+ complete(message) { close('✓', ANSI_SUCCESS, message); },
149
+ fail(message) { close('✗', ANSI_ERROR, message); },
150
+ };
151
+ }
152
+ /** npm install phase → default percent. Best-effort bar shaping. */
153
+ function npmInstallPhasePercent(phase) {
154
+ switch (phase) {
155
+ case 'spawning': return 3;
156
+ case 'resolving': return 15;
157
+ case 'downloading': return 50;
158
+ case 'extracting': return 85;
159
+ case 'verifying': return 97;
160
+ case 'installed': return 100;
161
+ case 'failed': return 100;
162
+ default: return 0;
163
+ }
164
+ }
165
+ /** Detect npm phase from a stdout/stderr line. Checks ordered for npm 9/10/11. */
166
+ function detectNpmPhase(line) {
167
+ const l = line.toLowerCase();
168
+ if (l.includes('added ') && l.includes('package'))
169
+ return 'verifying';
170
+ if (l.includes('http fetch'))
171
+ return 'downloading';
172
+ if (l.includes('extracting') || l.includes('extract'))
173
+ return 'extracting';
174
+ if (l.includes('reify:'))
175
+ return 'downloading';
176
+ if (l.includes('resolved') || l.includes('audit'))
177
+ return 'resolving';
178
+ return null;
179
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.closestAction = closestAction;
4
+ /**
5
+ * Copyright (c) 2026 Shiva Deore (Taracod).
6
+ * Licensed under AGPL-3.0. See LICENSE for details.
7
+ *
8
+ * Aiden — local-first agent.
9
+ */
10
+ /**
11
+ * cli/v4/util/closestAction.ts — v4.9.1 amendment.
12
+ * Suggest the closest known action when the user mis-types a subcommand.
13
+ * Matches if input is a substring of a known action OR Levenshtein
14
+ * distance ≤ 2. Returns null when nothing is reasonably close.
15
+ */
16
+ function lev(a, b) {
17
+ const m = a.length, n = b.length;
18
+ if (m === 0)
19
+ return n;
20
+ if (n === 0)
21
+ return m;
22
+ const row = Array.from({ length: n + 1 }, (_, i) => i);
23
+ for (let i = 1; i <= m; i++) {
24
+ let prev = i - 1;
25
+ row[0] = i;
26
+ for (let j = 1; j <= n; j++) {
27
+ const cur = row[j];
28
+ row[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, row[j - 1], row[j]) + 1;
29
+ prev = cur;
30
+ }
31
+ }
32
+ return row[n];
33
+ }
34
+ function closestAction(input, known) {
35
+ if (!input)
36
+ return null;
37
+ const lo = input.toLowerCase();
38
+ let best = null;
39
+ for (const k of known) {
40
+ const kl = k.toLowerCase();
41
+ if (kl.includes(lo) || lo.includes(kl))
42
+ return k;
43
+ const d = lev(lo, kl);
44
+ if (d <= 2 && (!best || d < best.d))
45
+ best = { name: k, d };
46
+ }
47
+ return best?.name ?? null;
48
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/update/depWarningFilter.ts — v4.9.1.
10
+ *
11
+ * Strip Node `DeprecationWarning` noise (DEP0190 and friends) from
12
+ * npm install stderr before it reaches the user. Filtered lines are
13
+ * preserved in `~/.aiden/logs/update.log` so diagnostics aren't lost.
14
+ *
15
+ * Conservative match — only filters lines that BOTH look like a Node
16
+ * deprecation header AND name a DEP code or the trace-deprecation hint.
17
+ * Legitimate npm errors (EACCES, ENOTFOUND, ENOENT, etc.) pass through.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.isDeprecationLine = isDeprecationLine;
24
+ exports.splitStderr = splitStderr;
25
+ exports.logFilteredWarnings = logFilteredWarnings;
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_os_1 = __importDefault(require("node:os"));
29
+ /** True iff the line is Node-deprecation chatter we should hide. */
30
+ function isDeprecationLine(line) {
31
+ // Node's deprecation banner header: "(node:NNNN) [DEP0190] DeprecationWarning: ..."
32
+ if (/^\s*\(node:\d+\)\s*(?:\[DEP\d+\]\s*)?DeprecationWarning:/.test(line))
33
+ return true;
34
+ // The follow-up hint Node emits underneath the header.
35
+ if (/Use `node --trace-deprecation/.test(line))
36
+ return true;
37
+ // Bare DEP code lines (some Node versions emit these stand-alone).
38
+ if (/^\s*\[DEP\d+\]/.test(line))
39
+ return true;
40
+ return false;
41
+ }
42
+ /**
43
+ * Split an stderr blob into `kept` (user-visible) and `filtered`
44
+ * (deprecation noise, routed to the diagnostic log).
45
+ */
46
+ function splitStderr(blob) {
47
+ if (!blob)
48
+ return { kept: '', filtered: '' };
49
+ const lines = blob.split(/\r?\n/);
50
+ const kept = [];
51
+ const filtered = [];
52
+ for (const ln of lines) {
53
+ if (isDeprecationLine(ln))
54
+ filtered.push(ln);
55
+ else
56
+ kept.push(ln);
57
+ }
58
+ return { kept: kept.join('\n'), filtered: filtered.join('\n') };
59
+ }
60
+ /**
61
+ * Append filtered lines to `~/.aiden/logs/update.log` with an ISO
62
+ * timestamp header. Fail-open: a log-write failure must NEVER crash
63
+ * the install path.
64
+ */
65
+ async function logFilteredWarnings(filtered, opts = {}) {
66
+ if (!filtered || !filtered.trim())
67
+ return;
68
+ try {
69
+ const root = opts.aidenRoot ?? node_path_1.default.join(node_os_1.default.homedir(), '.aiden');
70
+ const logDir = node_path_1.default.join(root, 'logs');
71
+ await node_fs_1.promises.mkdir(logDir, { recursive: true });
72
+ const entry = `[${new Date().toISOString()}] update.npm-deprecation:\n${filtered}\n\n`;
73
+ await node_fs_1.promises.appendFile(node_path_1.default.join(logDir, 'update.log'), entry, 'utf8');
74
+ }
75
+ catch { /* fail-open */ }
76
+ }
@@ -42,11 +42,18 @@
42
42
  * - No registry probe — call `checkForUpdate` first if you need to
43
43
  * know whether an install is warranted.
44
44
  */
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
45
48
  Object.defineProperty(exports, "__esModule", { value: true });
46
49
  exports.INSTALL_TIMEOUT_MS = void 0;
47
50
  exports.executeInstall = executeInstall;
48
51
  exports.parseInstalledVersion = parseInstalledVersion;
49
52
  const node_child_process_1 = require("node:child_process");
53
+ const node_os_1 = __importDefault(require("node:os"));
54
+ const depWarningFilter_1 = require("./depWarningFilter");
55
+ const platformInstructions_1 = require("./platformInstructions");
56
+ const progressBar_1 = require("../../../cli/v4/ui/progressBar");
50
57
  /** 90 s wall-clock cap. Generous on cold caches / slow networks. */
51
58
  exports.INSTALL_TIMEOUT_MS = 90000;
52
59
  const DEFAULT_PACKAGE_SPEC = 'aiden-runtime@latest';
@@ -64,6 +71,9 @@ async function executeInstall(opts = {}) {
64
71
  const timeoutMs = opts.timeoutMs ?? exports.INSTALL_TIMEOUT_MS;
65
72
  const packageSpec = opts.packageSpec ?? DEFAULT_PACKAGE_SPEC;
66
73
  const platform = opts.platform ?? process.platform;
74
+ const home = opts.home ?? node_os_1.default.homedir();
75
+ const env = opts.env ?? process.env;
76
+ const onPhase = opts.onPhase ?? ((_p) => { });
67
77
  return new Promise((resolve) => {
68
78
  const args = ['install', '-g', packageSpec];
69
79
  // v4.8.1 Slice 2 — drop `shell: true`. Node 20+ emits
@@ -78,6 +88,7 @@ async function executeInstall(opts = {}) {
78
88
  const spawnOpts = {
79
89
  stdio: ['ignore', 'pipe', 'pipe'],
80
90
  };
91
+ onPhase('spawning');
81
92
  let child;
82
93
  try {
83
94
  child = spawn(cmd, args, spawnOpts);
@@ -92,11 +103,23 @@ async function executeInstall(opts = {}) {
92
103
  }
93
104
  let stdoutBuf = '';
94
105
  let stderrBuf = '';
106
+ // v4.9.1 — parse phase signal off each chunk.
107
+ const tryEmitPhase = (chunk) => {
108
+ for (const ln of chunk.split(/\r?\n/)) {
109
+ const p = (0, progressBar_1.detectNpmPhase)(ln);
110
+ if (p)
111
+ onPhase(p);
112
+ }
113
+ };
95
114
  child.stdout?.on('data', (chunk) => {
96
- stdoutBuf += chunk.toString();
115
+ const s = chunk.toString();
116
+ stdoutBuf += s;
117
+ tryEmitPhase(s);
97
118
  });
98
119
  child.stderr?.on('data', (chunk) => {
99
- stderrBuf += chunk.toString();
120
+ const s = chunk.toString();
121
+ stderrBuf += s;
122
+ tryEmitPhase(s);
100
123
  });
101
124
  // Timeout — kill the child + resolve as a failure with the captured
102
125
  // output so the user sees what npm was doing.
@@ -121,7 +144,11 @@ async function executeInstall(opts = {}) {
121
144
  child.on('close', (code) => {
122
145
  clearTimeout(timer);
123
146
  const stdout = stdoutBuf;
124
- const stderr = stderrBuf;
147
+ // v4.9.1 — strip Node DEP* noise from stderr before any surfacing
148
+ // to the user. Filtered lines land in ~/.aiden/logs/update.log.
149
+ const { kept: stderr, filtered } = (0, depWarningFilter_1.splitStderr)(stderrBuf);
150
+ if (filtered)
151
+ void (0, depWarningFilter_1.logFilteredWarnings)(filtered);
125
152
  const exitCode = code ?? -1;
126
153
  if (timedOut) {
127
154
  resolve({
@@ -134,14 +161,16 @@ async function executeInstall(opts = {}) {
134
161
  }
135
162
  // Permission-denied: surface platform-specific remediations.
136
163
  if (isPermissionDenied(stdout, stderr, exitCode)) {
164
+ onPhase('failed');
137
165
  resolve({
138
166
  success: false,
139
- error: permissionDeniedMessage(platform),
167
+ error: permissionDeniedMessage(platform, home, env),
140
168
  stdout, stderr, exitCode,
141
169
  });
142
170
  return;
143
171
  }
144
172
  if (exitCode !== 0) {
173
+ onPhase('failed');
145
174
  resolve({
146
175
  success: false,
147
176
  error: `Install failed (npm exit ${exitCode}). ` +
@@ -153,6 +182,7 @@ async function executeInstall(opts = {}) {
153
182
  }
154
183
  // Success — parse installed version from npm output. Pattern:
155
184
  // "+ aiden-runtime@4.1.3" or "added 1 package ... aiden-runtime@4.1.3"
185
+ onPhase('installed');
156
186
  const installedVersion = parseInstalledVersion(stdout) ?? parseInstalledVersion(stderr) ?? undefined;
157
187
  resolve({
158
188
  success: true,
@@ -190,38 +220,14 @@ function isPermissionDenied(stdout, stderr, exitCode) {
190
220
  return false;
191
221
  }
192
222
  /**
193
- * Build the platform-specific copy-paste remediation. Provides three
194
- * distinct paths system-wide-with-elevation (Windows admin),
195
- * sudo (macOS/Linux), or user-local-prefix (cross-platform) so the
196
- * user has options without us trying to self-escalate to UAC/sudo
197
- * from inside the running REPL.
223
+ * v4.9.1 — Build the platform-specific copy-paste remediation. Delegates
224
+ * to `platformInstructions.ts` for the heavy lifting so the same builder
225
+ * powers both EPERM remediation + stale-prefix warnings + the future
226
+ * `aiden update --setup-user-prefix` helper.
198
227
  */
199
- function permissionDeniedMessage(platform) {
200
- const userLocal = 'Or use a user-local npm prefix to avoid privileges entirely:\n' +
201
- ' npm config set prefix ~/.npm-global\n' +
202
- ' export PATH=~/.npm-global/bin:$PATH # add to your shell profile\n' +
203
- ' npm install -g aiden-runtime@latest';
204
- if (platform === 'win32') {
205
- return [
206
- 'Install failed: permission denied (npm needs Administrator for global install).',
207
- '',
208
- 'To update manually:',
209
- ' Windows: Open PowerShell as Administrator, then:',
210
- ' npm install -g aiden-runtime@latest',
211
- '',
212
- userLocal,
213
- ].join('\n');
214
- }
215
- // darwin / linux / others — sudo path.
216
- return [
217
- 'Install failed: permission denied (npm needs sudo for global install).',
218
- '',
219
- 'To update manually:',
220
- ' macOS / Linux:',
221
- ' sudo npm install -g aiden-runtime@latest',
222
- '',
223
- userLocal,
224
- ].join('\n');
228
+ function permissionDeniedMessage(platform, home, env) {
229
+ const instr = (0, platformInstructions_1.permissionDeniedInstructions)({ platform, home, env });
230
+ return [instr.headline, '', ...instr.steps].join('\n');
225
231
  }
226
232
  /**
227
233
  * Find the installed version in npm output. Two common patterns:
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/update/platformInstructions.ts — v4.9.1.
10
+ *
11
+ * Per-platform copy-paste remediation text for:
12
+ * - EPERM / EACCES during global npm install
13
+ * - Stale / risky npm prefix detection
14
+ *
15
+ * Branches purely on `process.platform` (and `$SHELL` for unix shell-
16
+ * rc-file recommendations). Shell syntax MUST be correct per-platform:
17
+ * PowerShell on Windows, bash/zsh on Unix. Cross-contamination is a
18
+ * regression (the v4.9.0 bug we're hot-fixing).
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.detectShell = detectShell;
22
+ exports.permissionDeniedInstructions = permissionDeniedInstructions;
23
+ exports.detectStalePrefix = detectStalePrefix;
24
+ /**
25
+ * Detect the user's interactive shell on POSIX. Returns the basename
26
+ * (`zsh` / `bash` / `sh`) or null when undetectable. Pure — env is
27
+ * injected so tests pin a value.
28
+ */
29
+ function detectShell(env = process.env) {
30
+ const sh = env.SHELL;
31
+ if (!sh)
32
+ return null;
33
+ const last = sh.split(/[\\/]/).pop() || '';
34
+ return last.toLowerCase() || null;
35
+ }
36
+ /** rc-file path the user should edit, per shell. POSIX only. */
37
+ function rcFileFor(shell, home) {
38
+ if (shell === 'zsh')
39
+ return `${home}/.zshrc`;
40
+ if (shell === 'bash')
41
+ return `${home}/.bashrc (or ~/.bash_profile on macOS)`;
42
+ return `${home}/.profile`;
43
+ }
44
+ /**
45
+ * Build the EPERM/EACCES remediation. Two options per platform —
46
+ * elevation (one-time) and user-local prefix (permanent, no privs).
47
+ */
48
+ function permissionDeniedInstructions(opts) {
49
+ const env = opts.env ?? process.env;
50
+ if (opts.platform === 'win32') {
51
+ return {
52
+ headline: 'Install failed: permission denied. npm needs Administrator for a global install on Windows.',
53
+ shell: 'powershell',
54
+ steps: [
55
+ 'Option 1 — Run once with elevated privileges:',
56
+ ' • Open PowerShell as Administrator (right-click → "Run as administrator")',
57
+ ' • npm install -g aiden-runtime@latest',
58
+ '',
59
+ 'Option 2 — Permanent: switch npm to a user-local prefix (no admin needed ever again):',
60
+ ' • npm config set prefix "$env:USERPROFILE\\AppData\\Roaming\\npm"',
61
+ ' • [Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\\AppData\\Roaming\\npm;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")',
62
+ ' • Close + reopen PowerShell, then: npm install -g aiden-runtime@latest',
63
+ ],
64
+ };
65
+ }
66
+ // POSIX: darwin / linux / *bsd / etc.
67
+ const shell = detectShell(env);
68
+ const rcFile = rcFileFor(shell, opts.home);
69
+ return {
70
+ headline: `Install failed: permission denied. npm needs sudo for a global install on ${opts.platform}.`,
71
+ shell: shell ?? undefined,
72
+ rcFile,
73
+ steps: [
74
+ 'Option 1 — Run once with elevated privileges:',
75
+ ' • sudo npm install -g aiden-runtime@latest',
76
+ '',
77
+ 'Option 2 — Permanent: switch npm to a user-local prefix (no sudo needed ever again):',
78
+ ` • npm config set prefix "${opts.home}/.npm-global"`,
79
+ ` • echo 'export PATH="${opts.home}/.npm-global/bin:$PATH"' >> ${rcFile}`,
80
+ ` • source ${rcFile}`,
81
+ ' • npm install -g aiden-runtime@latest',
82
+ ],
83
+ };
84
+ }
85
+ /**
86
+ * Stale / risky prefix detection. Returns a warning when the npm
87
+ * `prefix` config points at a location that needs elevation OR is
88
+ * known to cause permission churn. `null` when the prefix is safe.
89
+ *
90
+ * `writable` is the result of a `fs.access` check the caller does
91
+ * before invoking us (we don't want to do filesystem I/O in a pure
92
+ * builder — caller controls the side effect).
93
+ */
94
+ function detectStalePrefix(opts) {
95
+ const env = opts.env ?? process.env;
96
+ const p = opts.prefix;
97
+ // Windows risk: Program Files.
98
+ if (opts.platform === 'win32') {
99
+ if (/^[a-zA-Z]:\\Program Files/i.test(p)) {
100
+ return {
101
+ warning: `npm prefix is "${p}" — global installs here require Administrator every time.`,
102
+ switchSteps: [
103
+ 'Switch to a user-local prefix to avoid the prompt forever:',
104
+ ' • npm config set prefix "$env:USERPROFILE\\AppData\\Roaming\\npm"',
105
+ ' • [Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\\AppData\\Roaming\\npm;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")',
106
+ ' • Close + reopen PowerShell.',
107
+ ],
108
+ };
109
+ }
110
+ return null;
111
+ }
112
+ // POSIX risk: /usr or /usr/local without write access.
113
+ const risky = p === '/usr' || p === '/usr/local' || p.startsWith('/usr/');
114
+ if (risky && !opts.writable) {
115
+ const shell = detectShell(env);
116
+ const rcFile = rcFileFor(shell, opts.home);
117
+ return {
118
+ warning: `npm prefix is "${p}" — global installs here require sudo every time.`,
119
+ switchSteps: [
120
+ 'Switch to a user-local prefix to avoid sudo forever:',
121
+ ` • npm config set prefix "${opts.home}/.npm-global"`,
122
+ ` • echo 'export PATH="${opts.home}/.npm-global/bin:$PATH"' >> ${rcFile}`,
123
+ ` • source ${rcFile}`,
124
+ ],
125
+ };
126
+ }
127
+ return null;
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.9.0",
3
+ "version": "4.9.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },