@ghl-ai/aw 0.1.57 → 0.1.58-beta.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/integrations.mjs CHANGED
@@ -19,18 +19,10 @@ import { join, basename } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import * as fmt from './fmt.mjs';
21
21
  import { chalk } from './fmt.mjs';
22
- import {
23
- ensureContextModeIntegration,
24
- getContextModeIntegrationSummary,
25
- removeContextModeIntegration,
26
- } from './integrations/context-mode.mjs';
27
22
 
28
23
  const execAsync = promisify(exec);
29
24
  const HOME = homedir();
30
-
31
- function manifestPath(home = HOME) {
32
- return join(home, '.aw_registry', '.integration-manifest.json');
33
- }
25
+ const MANIFEST_PATH = join(HOME, '.aw_registry', '.integration-manifest.json');
34
26
 
35
27
  function getErrorMessage(error) {
36
28
  return error instanceof Error ? error.message : String(error);
@@ -144,16 +136,6 @@ export const INTEGRATIONS = {
144
136
  requiresAuth: false,
145
137
  authNote: 'run /graphify . inside your IDE to build the graph whenever you need it',
146
138
  },
147
-
148
- 'context-mode': {
149
- type: 'context-mode',
150
- label: 'Context Mode',
151
- description: 'Local context-mode MCP server and hook-based context preservation',
152
- autoInstall: false,
153
- teams: [],
154
- requiresAuth: false,
155
- authNote: 'Run aw doctor to verify context-mode health.',
156
- },
157
139
  };
158
140
 
159
141
  // ────────────────────────────────────────────────────────────────────────────────
@@ -166,27 +148,25 @@ export const BUNDLES = {};
166
148
  // MANIFEST MANAGEMENT
167
149
  // ────────────────────────────────────────────────────────────────────────────────
168
150
 
169
- export function readManifest(home = HOME) {
170
- const filePath = manifestPath(home);
171
- if (!existsSync(filePath)) {
151
+ export function readManifest() {
152
+ if (!existsSync(MANIFEST_PATH)) {
172
153
  return { version: 1, installed: {} };
173
154
  }
174
155
 
175
156
  try {
176
- return JSON.parse(readFileSync(filePath, 'utf8'));
157
+ return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
177
158
  } catch {
178
159
  return { version: 1, installed: {} };
179
160
  }
180
161
  }
181
162
 
182
- export function writeManifest(manifest, home = HOME) {
183
- const filePath = manifestPath(home);
184
- mkdirSync(join(filePath, '..'), { recursive: true });
185
- writeFileSync(filePath, JSON.stringify(manifest, null, 2) + '\n');
163
+ export function writeManifest(manifest) {
164
+ mkdirSync(join(HOME, '.aw_registry'), { recursive: true });
165
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
186
166
  }
187
167
 
188
- export function isInstalled(key, options = {}) {
189
- const manifest = readManifest(options.home);
168
+ export function isInstalled(key) {
169
+ const manifest = readManifest();
190
170
  const entry = manifest.installed[key];
191
171
  if (!entry) return false;
192
172
  // Backward compat: pre-existing entries have no `status` field — treat as installed.
@@ -195,35 +175,25 @@ export function isInstalled(key, options = {}) {
195
175
  return entry.status !== 'skipped';
196
176
  }
197
177
 
198
- function recordInstalled(key, type, options = {}) {
199
- if (options.dryRun) return;
200
- const manifest = readManifest(options.home);
178
+ function recordInstalled(key, type) {
179
+ const manifest = readManifest();
201
180
  manifest.installed[key] = {
202
181
  type,
203
182
  status: 'installed',
204
183
  installedAt: new Date().toISOString(),
205
- ...(options.extra || {}),
206
184
  };
207
- writeManifest(manifest, options.home);
185
+ writeManifest(manifest);
208
186
  }
209
187
 
210
- function recordSkipped(key, type, reason, options = {}) {
211
- if (options.dryRun) return;
212
- const manifest = readManifest(options.home);
188
+ function recordSkipped(key, type, reason) {
189
+ const manifest = readManifest();
213
190
  manifest.installed[key] = {
214
191
  type,
215
192
  status: 'skipped',
216
193
  reason,
217
194
  installedAt: new Date().toISOString(),
218
195
  };
219
- writeManifest(manifest, options.home);
220
- }
221
-
222
- function removeInstalled(key, options = {}) {
223
- if (options.dryRun) return;
224
- const manifest = readManifest(options.home);
225
- delete manifest.installed[key];
226
- writeManifest(manifest, options.home);
196
+ writeManifest(manifest);
227
197
  }
228
198
 
229
199
  // ────────────────────────────────────────────────────────────────────────────────
@@ -449,7 +419,7 @@ async function resolveCliInvocation(cliCommand, pythonCmd, installer) {
449
419
  }
450
420
  }
451
421
 
452
- async function runPythonCli(integration, key, { silent = false, home = HOME } = {}) {
422
+ async function runPythonCli(integration, key, { silent = false } = {}) {
453
423
  const spinner = silent ? null : fmt.spinner();
454
424
 
455
425
  // 1. Detect Python ≥ minPython.
@@ -457,7 +427,7 @@ async function runPythonCli(integration, key, { silent = false, home = HOME } =
457
427
  const py = await detectPython(integration.minPython);
458
428
  if (!py) {
459
429
  if (!silent) spinner.stop(chalk.yellow('Python not found'));
460
- recordSkipped(key, 'python-cli', 'python-not-found', { home });
430
+ recordSkipped(key, 'python-cli', 'python-not-found');
461
431
  if (!silent) {
462
432
  fmt.logWarn(
463
433
  `${integration.label} skipped — Python ${integration.minPython.major}.${integration.minPython.minor}+ required.`,
@@ -589,7 +559,7 @@ async function runClaudePlugin(installCmd, { silent = false, marketplaceSource =
589
559
  // UNIVERSAL INSTALLER (OS-detected master script — covers all IDEs in one shot)
590
560
  // ────────────────────────────────────────────────────────────────────────────────
591
561
 
592
- async function runUniversalInstaller(integration, key, { silent = false, home = HOME } = {}) {
562
+ async function runUniversalInstaller(integration, key, { silent = false } = {}) {
593
563
  const spinner = silent ? null : fmt.spinner();
594
564
 
595
565
  if (!silent) spinner.start(`Installing ${integration.label} for all detected IDEs...`);
@@ -636,7 +606,7 @@ async function runUniversalInstaller(integration, key, { silent = false, home =
636
606
  'Skipped',
637
607
  );
638
608
  }
639
- recordSkipped(key, 'universal-installer', 'wsl-not-found', { home });
609
+ recordSkipped(key, 'universal-installer', 'wsl-not-found');
640
610
  return false;
641
611
  }
642
612
  }
@@ -679,60 +649,13 @@ async function runUniversalInstaller(integration, key, { silent = false, home =
679
649
  // INTEGRATION INSTALLER
680
650
  // ────────────────────────────────────────────────────────────────────────────────
681
651
 
682
- function successResult(key, integration, extra = {}) {
683
- return { success: true, key, integration, ...extra };
684
- }
685
-
686
- function failureResult(key, integration, error, extra = {}) {
687
- return { success: false, key, integration, error, ...extra };
688
- }
689
-
690
- export function integrationSucceeded(result) {
691
- return result === true || result?.success === true;
692
- }
693
-
694
- export async function installIntegration(key, options = {}) {
695
- const { silent = false } = options;
696
- const home = options.home || HOME;
697
- const dryRun = options.dryRun === true;
652
+ export async function installIntegration(key, { silent = false } = {}) {
698
653
  const integration = INTEGRATIONS[key];
699
654
  if (!integration) {
700
655
  throw new Error(`Unknown integration: ${key}`);
701
656
  }
702
657
 
703
- if (integration.type === 'context-mode') {
704
- const result = ensureContextModeIntegration(home, {
705
- ...options,
706
- home,
707
- env: options.env || process.env,
708
- dryRun,
709
- });
710
- if (dryRun) {
711
- return successResult(key, integration, result);
712
- }
713
- if (result.binary.present && result.complete) {
714
- recordInstalled(key, integration.type, {
715
- home,
716
- extra: {
717
- binaryPath: result.binary.path,
718
- version: result.binary.version,
719
- },
720
- });
721
- return successResult(key, integration, result);
722
- }
723
- return failureResult(
724
- key,
725
- integration,
726
- result.binary.present ? 'partial-config' : (result.warnings?.[0] || 'binary-missing'),
727
- result,
728
- );
729
- }
730
-
731
- if (dryRun) {
732
- return successResult(key, integration, { plannedInstall: true });
733
- }
734
-
735
- if (isInstalled(key, { home })) {
658
+ if (isInstalled(key)) {
736
659
  if (!silent) fmt.logWarn(`${integration.label} is already installed`);
737
660
  return false;
738
661
  }
@@ -741,7 +664,7 @@ export async function installIntegration(key, options = {}) {
741
664
  if (integration.type === 'plugin') {
742
665
  // PLUGIN: Run claude plugin install
743
666
  await runClaudePlugin(integration.installCmd, { silent, marketplaceSource: integration.marketplaceSource ?? null });
744
- recordInstalled(key, 'plugin', { home });
667
+ recordInstalled(key, 'plugin');
745
668
 
746
669
  if (!silent) {
747
670
  fmt.logSuccess(`${integration.label} plugin installed`);
@@ -776,7 +699,7 @@ export async function installIntegration(key, options = {}) {
776
699
  integration.mcpUrl
777
700
  );
778
701
 
779
- recordInstalled(key, 'remote-mcp', { home });
702
+ recordInstalled(key, 'remote-mcp');
780
703
 
781
704
  if (!silent) {
782
705
  fmt.logSuccess(`${integration.label} added to MCP servers`);
@@ -786,10 +709,10 @@ export async function installIntegration(key, options = {}) {
786
709
  }
787
710
  } else if (integration.type === 'python-cli') {
788
711
  // PYTHON CLI: pip-install + run post-install + per-project hooks
789
- const ok = await runPythonCli(integration, key, { silent, home });
712
+ const ok = await runPythonCli(integration, key, { silent });
790
713
  if (!ok) return false; // skipped (e.g. Python missing) — manifest already recorded
791
714
 
792
- recordInstalled(key, 'python-cli', { home });
715
+ recordInstalled(key, 'python-cli');
793
716
 
794
717
  if (!silent) {
795
718
  fmt.logSuccess(`${integration.label} installed`);
@@ -799,9 +722,9 @@ export async function installIntegration(key, options = {}) {
799
722
  }
800
723
  } else if (integration.type === 'universal-installer') {
801
724
  // UNIVERSAL INSTALLER: OS-detected master script, covers all IDEs in one shot
802
- const ok = await runUniversalInstaller(integration, key, { silent, home });
725
+ const ok = await runUniversalInstaller(integration, key, { silent });
803
726
  if (!ok) return false; // skipped (e.g. WSL not found) — manifest already recorded
804
- recordInstalled(key, 'universal-installer', { home });
727
+ recordInstalled(key, 'universal-installer');
805
728
 
806
729
  if (!silent) {
807
730
  fmt.logSuccess(`${integration.label} installed across all detected IDEs`);
@@ -825,22 +748,13 @@ export async function installIntegration(key, options = {}) {
825
748
  // INTEGRATION REMOVER
826
749
  // ────────────────────────────────────────────────────────────────────────────────
827
750
 
828
- export async function removeIntegration(key, options = {}) {
829
- const { silent = false } = options;
830
- const home = options.home || HOME;
831
- const dryRun = options.dryRun === true;
751
+ export async function removeIntegration(key, { silent = false } = {}) {
832
752
  const integration = INTEGRATIONS[key];
833
753
  if (!integration) {
834
754
  throw new Error(`Unknown integration: ${key}`);
835
755
  }
836
756
 
837
- if (integration.type === 'context-mode') {
838
- const result = removeContextModeIntegration(home, { dryRun });
839
- removeInstalled(key, { home, dryRun });
840
- return successResult(key, integration, result);
841
- }
842
-
843
- if (!isInstalled(key, { home })) {
757
+ if (!isInstalled(key)) {
844
758
  if (!silent) fmt.logWarn(`${integration.label} is not installed`);
845
759
  return false;
846
760
  }
@@ -934,7 +848,9 @@ export async function removeIntegration(key, options = {}) {
934
848
  }
935
849
 
936
850
  // Remove from manifest
937
- removeInstalled(key, { home });
851
+ const manifest = readManifest();
852
+ delete manifest.installed[key];
853
+ writeManifest(manifest);
938
854
 
939
855
  return true;
940
856
  } catch (e) {
@@ -949,8 +865,8 @@ export async function removeIntegration(key, options = {}) {
949
865
  // HELPERS
950
866
  // ────────────────────────────────────────────────────────────────────────────────
951
867
 
952
- export function getInstalledList(options = {}) {
953
- const manifest = readManifest(options.home);
868
+ export function getInstalledList() {
869
+ const manifest = readManifest();
954
870
  // Only return entries actually installed — skipped entries (e.g. Python missing)
955
871
  // shouldn't show up as "installed" to the rest of the system.
956
872
  return Object.entries(manifest.installed)
@@ -958,34 +874,6 @@ export function getInstalledList(options = {}) {
958
874
  .map(([key]) => key);
959
875
  }
960
876
 
961
- export function getIntegrationSummary(key, options = {}) {
962
- const integration = INTEGRATIONS[key];
963
- if (!integration) return null;
964
- if (integration.type === 'context-mode') {
965
- return {
966
- ...getContextModeIntegrationSummary(options.home || HOME, options),
967
- label: integration.label,
968
- description: integration.description,
969
- type: integration.type,
970
- };
971
- }
972
- const installed = isInstalled(key, options);
973
- return {
974
- name: key,
975
- label: integration.label,
976
- description: integration.description,
977
- type: integration.type,
978
- installed,
979
- configured: installed,
980
- state: installed ? 'installed' : 'available',
981
- summary: installed ? `${integration.label} installed` : `${integration.label} available`,
982
- };
983
- }
984
-
985
- export function getIntegrationSummaries(options = {}) {
986
- return Object.keys(INTEGRATIONS).map(key => getIntegrationSummary(key, options));
987
- }
988
-
989
877
  export function suggestForTeam(namespace) {
990
878
  if (!namespace) return [];
991
879
 
@@ -1007,8 +895,7 @@ export function suggestForTeam(namespace) {
1007
895
  // AUTO-INSTALL (called from init.mjs - installs suggested integrations)
1008
896
  // ────────────────────────────────────────────────────────────────────────────────
1009
897
 
1010
- export async function autoInstallIntegrations(team, options = {}) {
1011
- const { silent = false, installer = installIntegration } = options;
898
+ export async function autoInstallIntegrations(team, { silent = false, installer = installIntegration } = {}) {
1012
899
  // Get suggested integrations for this team
1013
900
  const suggested = suggestForTeam(team);
1014
901
  if (!suggested || suggested.length === 0) {
@@ -1016,7 +903,7 @@ export async function autoInstallIntegrations(team, options = {}) {
1016
903
  }
1017
904
 
1018
905
  // Only install if they're not already installed
1019
- const toInstall = suggested.filter((key) => !isInstalled(key, options));
906
+ const toInstall = suggested.filter((key) => !isInstalled(key));
1020
907
  if (toInstall.length === 0) {
1021
908
  return [];
1022
909
  }
@@ -1027,8 +914,8 @@ export async function autoInstallIntegrations(team, options = {}) {
1027
914
 
1028
915
  const installed = [];
1029
916
  for (const key of toInstall) {
1030
- const result = await installer(key, { ...options, silent });
1031
- if (integrationSucceeded(result)) {
917
+ const success = await installer(key, { silent });
918
+ if (success) {
1032
919
  installed.push(INTEGRATIONS[key].label);
1033
920
  }
1034
921
  }
@@ -1124,8 +1011,8 @@ export async function promptAndInstall(team, { silent = false } = {}) {
1124
1011
  // Install all
1125
1012
  const installed = [];
1126
1013
  for (const key of toInstall) {
1127
- const result = await installIntegration(key, { silent: false });
1128
- if (integrationSucceeded(result)) {
1014
+ const success = await installIntegration(key, { silent: false });
1015
+ if (success) {
1129
1016
  installed.push(INTEGRATIONS[key].label);
1130
1017
  }
1131
1018
  }
package/link.mjs CHANGED
@@ -11,11 +11,29 @@ function forceSymlink(target, linkPath) {
11
11
  symlinkSync(target, linkPath);
12
12
  }
13
13
 
14
+ function linkNestedSkillRoot(target, linkPath, { silent }) {
15
+ try {
16
+ forceSymlink(target, linkPath);
17
+ return true;
18
+ } catch (error) {
19
+ if (!silent) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ throw new Error(`Could not link nested skill ${linkPath} -> ${target}: ${message}`);
22
+ }
23
+ return false;
24
+ }
25
+ }
26
+
14
27
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
15
28
  // Per-file symlink types
16
29
  const FILE_TYPES = ['agents'];
17
30
  const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', 'references', 'docs']);
18
31
 
32
+ function realHomeDir() {
33
+ const HOME = homedir();
34
+ try { return realpathSync(HOME); } catch { return HOME; }
35
+ }
36
+
19
37
  /**
20
38
  * List namespace directories inside .aw_registry/ (skip dotfiles).
21
39
  */
@@ -36,6 +54,32 @@ function listDirs(dir) {
36
54
  .map(d => d.name);
37
55
  }
38
56
 
57
+ /**
58
+ * Recursively find directories below a skills/ folder that are real skill roots.
59
+ * A real skill root is a directory containing SKILL.md. Once found, do not
60
+ * descend further so bundled references/scripts inside that skill are preserved.
61
+ */
62
+ function findSkillRootDirs(skillsDir) {
63
+ const results = [];
64
+ function walk(dir, segments) {
65
+ if (!existsSync(dir)) return;
66
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
67
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
68
+
69
+ const childDir = join(dir, entry.name);
70
+ const childSegments = [...segments, entry.name];
71
+ if (existsSync(join(childDir, 'SKILL.md'))) {
72
+ results.push({ skillDirPath: childDir, segments: childSegments });
73
+ continue;
74
+ }
75
+
76
+ walk(childDir, childSegments);
77
+ }
78
+ }
79
+ walk(skillsDir, []);
80
+ return results;
81
+ }
82
+
39
83
  /**
40
84
  * Recursively find directories named `typeName` within `nsDir`,
41
85
  * skipping other known type directories to avoid false matches.
@@ -77,7 +121,7 @@ function cleanIdeSymlinks(cwd) {
77
121
  cleanSymlinksRecursive(ideDir);
78
122
  }
79
123
  // Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
80
- const HOME = homedir();
124
+ const HOME = realHomeDir();
81
125
  if (cwd === HOME) {
82
126
  const agentsSkillsDir = join(cwd, '.agents', 'skills');
83
127
  if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
@@ -140,7 +184,8 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
140
184
  // where $HOME may be /var/... but process.cwd() resolves to /private/var/...
141
185
  try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
142
186
 
143
- const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
187
+ const HOME = realHomeDir();
188
+ const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
144
189
  let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
145
190
  try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
146
191
  if (!existsSync(awDir)) return 0;
@@ -185,6 +230,19 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
185
230
  try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
186
231
  }
187
232
  }
233
+
234
+ for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
235
+ if (skillSegments.length <= 1) continue;
236
+ const flat = [ns, ...segments, ...skillSegments].join('-');
237
+
238
+ for (const ide of IDE_DIRS) {
239
+ const linkDir = join(cwd, ide, 'skills');
240
+ mkdirSync(linkDir, { recursive: true });
241
+ const linkPath = join(linkDir, flat);
242
+ const relTarget = relative(linkDir, skillDirPath);
243
+ if (linkNestedSkillRoot(relTarget, linkPath, { silent })) created++;
244
+ }
245
+ }
188
246
  }
189
247
  }
190
248
 
@@ -246,7 +304,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
246
304
  }
247
305
 
248
306
  // Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
249
- if (cwd === homedir()) {
307
+ if (cwd === HOME) {
250
308
  const agentsSkillsDir = join(cwd, '.agents/skills');
251
309
  for (const ns of namespaces) {
252
310
  for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
@@ -258,6 +316,14 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
258
316
  const relTarget = relative(agentsSkillsDir, targetPath);
259
317
  try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
260
318
  }
319
+
320
+ for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
321
+ if (skillSegments.length <= 1) continue;
322
+ const flat = [ns, ...segments, ...skillSegments].join('-');
323
+ const linkPath = join(agentsSkillsDir, flat);
324
+ const relTarget = relative(agentsSkillsDir, skillDirPath);
325
+ if (linkNestedSkillRoot(relTarget, linkPath, { silent })) created++;
326
+ }
261
327
  }
262
328
  }
263
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.57",
3
+ "version": "0.1.58-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,6 @@
22
22
  "link.mjs",
23
23
  "manifest.mjs",
24
24
  "mcp.mjs",
25
- "package-manager.mjs",
26
25
  "paths.mjs",
27
26
  "plan.mjs",
28
27
  "registry.mjs",
@@ -55,9 +54,9 @@
55
54
  "license": "MIT",
56
55
  "scripts": {
57
56
  "test": "yarn test:vitest && yarn test:node",
58
- "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs tests/integrations tests/package-manager.test.mjs",
57
+ "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
59
58
  "test:node": "node tests/run-node-tests.mjs",
60
- "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs tests/integrations tests/package-manager.test.mjs",
59
+ "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
61
60
  "preuninstall": "node bin.js nuke 2>/dev/null || true"
62
61
  },
63
62
  "publishConfig": {
package/update.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  // .volta/ → volta install pnpm/ → pnpm add -g
10
10
  // else → npm i -g
11
11
 
12
- import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
12
+ import { readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
13
13
  import { exec as execCb, execSync } from 'node:child_process';
14
14
  import { promisify } from 'node:util';
15
15
  import { join, dirname } from 'node:path';
@@ -39,16 +39,15 @@ const LOG_MAX_ENTRIES = 50;
39
39
 
40
40
  const SELF_PATH = fileURLToPath(import.meta.url);
41
41
 
42
- function detectPM(selfPath = SELF_PATH) {
43
- const normalizedPath = String(selfPath || '').replace(/\\/g, '/');
44
- if (normalizedPath.includes('/.volta/')) return 'volta';
45
- if (normalizedPath.includes('/pnpm/')) return 'pnpm';
42
+ function detectPM() {
43
+ if (SELF_PATH.includes('/.volta/')) return 'volta';
44
+ if (SELF_PATH.includes('/pnpm/')) return 'pnpm';
46
45
  return 'npm';
47
46
  }
48
47
 
49
48
  const PM = detectPM();
50
49
 
51
- export function getInstallCmd(version = 'latest') {
50
+ function getInstallCmd(version = 'latest') {
52
51
  const spec = `${PKG_NAME}@${version}`;
53
52
  if (PM === 'volta') return `volta install ${spec}`;
54
53
  if (PM === 'pnpm') return `pnpm add -g ${spec}`;
@@ -92,36 +91,23 @@ function compareVersions(a, b) {
92
91
  // After install, re-resolve where the package actually landed and read
93
92
  // its package.json. Falls back to getLocalVersion() (our own bundle).
94
93
 
95
- export function getInstalledPackageRoot(options = {}) {
96
- const packageManager = options.packageManager || PM;
97
- const runner = options.execSync || execSync;
94
+ function getInstalledVersion() {
98
95
  try {
99
- if (packageManager === 'volta') {
96
+ if (PM === 'volta') {
100
97
  // Volta resolves via its own image; ask it directly
101
- const resolved = runner('volta which aw', {
98
+ const resolved = execSync('volta which aw', {
102
99
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
103
100
  }).trim();
104
- const imageRoot = join(resolved, '..', '..');
105
- const packageRoot = join(imageRoot, 'lib', 'node_modules', PKG_NAME);
106
- return existsSync(join(packageRoot, 'package.json')) ? packageRoot : imageRoot;
101
+ const pkgRoot = join(resolved, '..', '..');
102
+ return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version;
107
103
  }
108
104
 
109
105
  // npm / pnpm: read from the global prefix
110
- const prefix = runner('npm prefix -g', {
106
+ const prefix = execSync('npm prefix -g', {
111
107
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
112
108
  }).trim();
113
- if (packageManager === 'pnpm') {
114
- return join(prefix, 'node_modules', PKG_NAME);
115
- }
116
- return join(prefix, 'lib', 'node_modules', PKG_NAME);
117
- } catch {
118
- return dirname(SELF_PATH);
119
- }
120
- }
121
-
122
- function getInstalledVersion() {
123
- try {
124
- return JSON.parse(readFileSync(join(getInstalledPackageRoot(), 'package.json'), 'utf8')).version;
109
+ const pkgPath = join(prefix, 'lib', 'node_modules', PKG_NAME, 'package.json');
110
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
125
111
  } catch {
126
112
  return getLocalVersion();
127
113
  }
@@ -214,8 +200,7 @@ export function autoUpdate(result) {
214
200
  execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
215
201
  const installed = getInstalledVersion();
216
202
  const ok = compareVersions(installed, result.current) > 0;
217
- const packageRoot = getInstalledPackageRoot();
218
- const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, packageRoot, ok };
203
+ const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, ok };
219
204
  appendLog(entry);
220
205
  return { status: ok ? 'upgraded' : 'failed', ...entry };
221
206
  } catch (e) {