@ghl-ai/aw 0.1.56 → 0.1.57
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/cli.mjs +2 -5
- package/commands/init.mjs +56 -13
- package/commands/integration.mjs +38 -77
- package/commands/integrations.mjs +84 -100
- package/ecc.mjs +14 -3
- package/git.mjs +2 -0
- package/integrations/context-mode.mjs +312 -89
- package/integrations.mjs +153 -40
- package/package-manager.mjs +72 -0
- package/package.json +4 -3
- package/update.mjs +29 -14
- package/integrations/index.mjs +0 -31
package/integrations.mjs
CHANGED
|
@@ -19,10 +19,18 @@ 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';
|
|
22
27
|
|
|
23
28
|
const execAsync = promisify(exec);
|
|
24
29
|
const HOME = homedir();
|
|
25
|
-
|
|
30
|
+
|
|
31
|
+
function manifestPath(home = HOME) {
|
|
32
|
+
return join(home, '.aw_registry', '.integration-manifest.json');
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
function getErrorMessage(error) {
|
|
28
36
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -136,6 +144,16 @@ export const INTEGRATIONS = {
|
|
|
136
144
|
requiresAuth: false,
|
|
137
145
|
authNote: 'run /graphify . inside your IDE to build the graph whenever you need it',
|
|
138
146
|
},
|
|
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
|
+
},
|
|
139
157
|
};
|
|
140
158
|
|
|
141
159
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -148,25 +166,27 @@ export const BUNDLES = {};
|
|
|
148
166
|
// MANIFEST MANAGEMENT
|
|
149
167
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
150
168
|
|
|
151
|
-
export function readManifest() {
|
|
152
|
-
|
|
169
|
+
export function readManifest(home = HOME) {
|
|
170
|
+
const filePath = manifestPath(home);
|
|
171
|
+
if (!existsSync(filePath)) {
|
|
153
172
|
return { version: 1, installed: {} };
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
try {
|
|
157
|
-
return JSON.parse(readFileSync(
|
|
176
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
158
177
|
} catch {
|
|
159
178
|
return { version: 1, installed: {} };
|
|
160
179
|
}
|
|
161
180
|
}
|
|
162
181
|
|
|
163
|
-
export function writeManifest(manifest) {
|
|
164
|
-
|
|
165
|
-
|
|
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');
|
|
166
186
|
}
|
|
167
187
|
|
|
168
|
-
export function isInstalled(key) {
|
|
169
|
-
const manifest = readManifest();
|
|
188
|
+
export function isInstalled(key, options = {}) {
|
|
189
|
+
const manifest = readManifest(options.home);
|
|
170
190
|
const entry = manifest.installed[key];
|
|
171
191
|
if (!entry) return false;
|
|
172
192
|
// Backward compat: pre-existing entries have no `status` field — treat as installed.
|
|
@@ -175,25 +195,35 @@ export function isInstalled(key) {
|
|
|
175
195
|
return entry.status !== 'skipped';
|
|
176
196
|
}
|
|
177
197
|
|
|
178
|
-
function recordInstalled(key, type) {
|
|
179
|
-
|
|
198
|
+
function recordInstalled(key, type, options = {}) {
|
|
199
|
+
if (options.dryRun) return;
|
|
200
|
+
const manifest = readManifest(options.home);
|
|
180
201
|
manifest.installed[key] = {
|
|
181
202
|
type,
|
|
182
203
|
status: 'installed',
|
|
183
204
|
installedAt: new Date().toISOString(),
|
|
205
|
+
...(options.extra || {}),
|
|
184
206
|
};
|
|
185
|
-
writeManifest(manifest);
|
|
207
|
+
writeManifest(manifest, options.home);
|
|
186
208
|
}
|
|
187
209
|
|
|
188
|
-
function recordSkipped(key, type, reason) {
|
|
189
|
-
|
|
210
|
+
function recordSkipped(key, type, reason, options = {}) {
|
|
211
|
+
if (options.dryRun) return;
|
|
212
|
+
const manifest = readManifest(options.home);
|
|
190
213
|
manifest.installed[key] = {
|
|
191
214
|
type,
|
|
192
215
|
status: 'skipped',
|
|
193
216
|
reason,
|
|
194
217
|
installedAt: new Date().toISOString(),
|
|
195
218
|
};
|
|
196
|
-
writeManifest(manifest);
|
|
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);
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -419,7 +449,7 @@ async function resolveCliInvocation(cliCommand, pythonCmd, installer) {
|
|
|
419
449
|
}
|
|
420
450
|
}
|
|
421
451
|
|
|
422
|
-
async function runPythonCli(integration, key, { silent = false } = {}) {
|
|
452
|
+
async function runPythonCli(integration, key, { silent = false, home = HOME } = {}) {
|
|
423
453
|
const spinner = silent ? null : fmt.spinner();
|
|
424
454
|
|
|
425
455
|
// 1. Detect Python ≥ minPython.
|
|
@@ -427,7 +457,7 @@ async function runPythonCli(integration, key, { silent = false } = {}) {
|
|
|
427
457
|
const py = await detectPython(integration.minPython);
|
|
428
458
|
if (!py) {
|
|
429
459
|
if (!silent) spinner.stop(chalk.yellow('Python not found'));
|
|
430
|
-
recordSkipped(key, 'python-cli', 'python-not-found');
|
|
460
|
+
recordSkipped(key, 'python-cli', 'python-not-found', { home });
|
|
431
461
|
if (!silent) {
|
|
432
462
|
fmt.logWarn(
|
|
433
463
|
`${integration.label} skipped — Python ${integration.minPython.major}.${integration.minPython.minor}+ required.`,
|
|
@@ -559,7 +589,7 @@ async function runClaudePlugin(installCmd, { silent = false, marketplaceSource =
|
|
|
559
589
|
// UNIVERSAL INSTALLER (OS-detected master script — covers all IDEs in one shot)
|
|
560
590
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
561
591
|
|
|
562
|
-
async function runUniversalInstaller(integration, key, { silent = false } = {}) {
|
|
592
|
+
async function runUniversalInstaller(integration, key, { silent = false, home = HOME } = {}) {
|
|
563
593
|
const spinner = silent ? null : fmt.spinner();
|
|
564
594
|
|
|
565
595
|
if (!silent) spinner.start(`Installing ${integration.label} for all detected IDEs...`);
|
|
@@ -606,7 +636,7 @@ async function runUniversalInstaller(integration, key, { silent = false } = {})
|
|
|
606
636
|
'Skipped',
|
|
607
637
|
);
|
|
608
638
|
}
|
|
609
|
-
recordSkipped(key, 'universal-installer', 'wsl-not-found');
|
|
639
|
+
recordSkipped(key, 'universal-installer', 'wsl-not-found', { home });
|
|
610
640
|
return false;
|
|
611
641
|
}
|
|
612
642
|
}
|
|
@@ -649,13 +679,60 @@ async function runUniversalInstaller(integration, key, { silent = false } = {})
|
|
|
649
679
|
// INTEGRATION INSTALLER
|
|
650
680
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
651
681
|
|
|
652
|
-
|
|
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;
|
|
653
698
|
const integration = INTEGRATIONS[key];
|
|
654
699
|
if (!integration) {
|
|
655
700
|
throw new Error(`Unknown integration: ${key}`);
|
|
656
701
|
}
|
|
657
702
|
|
|
658
|
-
if (
|
|
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 })) {
|
|
659
736
|
if (!silent) fmt.logWarn(`${integration.label} is already installed`);
|
|
660
737
|
return false;
|
|
661
738
|
}
|
|
@@ -664,7 +741,7 @@ export async function installIntegration(key, { silent = false } = {}) {
|
|
|
664
741
|
if (integration.type === 'plugin') {
|
|
665
742
|
// PLUGIN: Run claude plugin install
|
|
666
743
|
await runClaudePlugin(integration.installCmd, { silent, marketplaceSource: integration.marketplaceSource ?? null });
|
|
667
|
-
recordInstalled(key, 'plugin');
|
|
744
|
+
recordInstalled(key, 'plugin', { home });
|
|
668
745
|
|
|
669
746
|
if (!silent) {
|
|
670
747
|
fmt.logSuccess(`${integration.label} plugin installed`);
|
|
@@ -699,7 +776,7 @@ export async function installIntegration(key, { silent = false } = {}) {
|
|
|
699
776
|
integration.mcpUrl
|
|
700
777
|
);
|
|
701
778
|
|
|
702
|
-
recordInstalled(key, 'remote-mcp');
|
|
779
|
+
recordInstalled(key, 'remote-mcp', { home });
|
|
703
780
|
|
|
704
781
|
if (!silent) {
|
|
705
782
|
fmt.logSuccess(`${integration.label} added to MCP servers`);
|
|
@@ -709,10 +786,10 @@ export async function installIntegration(key, { silent = false } = {}) {
|
|
|
709
786
|
}
|
|
710
787
|
} else if (integration.type === 'python-cli') {
|
|
711
788
|
// PYTHON CLI: pip-install + run post-install + per-project hooks
|
|
712
|
-
const ok = await runPythonCli(integration, key, { silent });
|
|
789
|
+
const ok = await runPythonCli(integration, key, { silent, home });
|
|
713
790
|
if (!ok) return false; // skipped (e.g. Python missing) — manifest already recorded
|
|
714
791
|
|
|
715
|
-
recordInstalled(key, 'python-cli');
|
|
792
|
+
recordInstalled(key, 'python-cli', { home });
|
|
716
793
|
|
|
717
794
|
if (!silent) {
|
|
718
795
|
fmt.logSuccess(`${integration.label} installed`);
|
|
@@ -722,9 +799,9 @@ export async function installIntegration(key, { silent = false } = {}) {
|
|
|
722
799
|
}
|
|
723
800
|
} else if (integration.type === 'universal-installer') {
|
|
724
801
|
// UNIVERSAL INSTALLER: OS-detected master script, covers all IDEs in one shot
|
|
725
|
-
const ok = await runUniversalInstaller(integration, key, { silent });
|
|
802
|
+
const ok = await runUniversalInstaller(integration, key, { silent, home });
|
|
726
803
|
if (!ok) return false; // skipped (e.g. WSL not found) — manifest already recorded
|
|
727
|
-
recordInstalled(key, 'universal-installer');
|
|
804
|
+
recordInstalled(key, 'universal-installer', { home });
|
|
728
805
|
|
|
729
806
|
if (!silent) {
|
|
730
807
|
fmt.logSuccess(`${integration.label} installed across all detected IDEs`);
|
|
@@ -748,13 +825,22 @@ export async function installIntegration(key, { silent = false } = {}) {
|
|
|
748
825
|
// INTEGRATION REMOVER
|
|
749
826
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
750
827
|
|
|
751
|
-
export async function removeIntegration(key,
|
|
828
|
+
export async function removeIntegration(key, options = {}) {
|
|
829
|
+
const { silent = false } = options;
|
|
830
|
+
const home = options.home || HOME;
|
|
831
|
+
const dryRun = options.dryRun === true;
|
|
752
832
|
const integration = INTEGRATIONS[key];
|
|
753
833
|
if (!integration) {
|
|
754
834
|
throw new Error(`Unknown integration: ${key}`);
|
|
755
835
|
}
|
|
756
836
|
|
|
757
|
-
if (
|
|
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 })) {
|
|
758
844
|
if (!silent) fmt.logWarn(`${integration.label} is not installed`);
|
|
759
845
|
return false;
|
|
760
846
|
}
|
|
@@ -848,9 +934,7 @@ export async function removeIntegration(key, { silent = false } = {}) {
|
|
|
848
934
|
}
|
|
849
935
|
|
|
850
936
|
// Remove from manifest
|
|
851
|
-
|
|
852
|
-
delete manifest.installed[key];
|
|
853
|
-
writeManifest(manifest);
|
|
937
|
+
removeInstalled(key, { home });
|
|
854
938
|
|
|
855
939
|
return true;
|
|
856
940
|
} catch (e) {
|
|
@@ -865,8 +949,8 @@ export async function removeIntegration(key, { silent = false } = {}) {
|
|
|
865
949
|
// HELPERS
|
|
866
950
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
867
951
|
|
|
868
|
-
export function getInstalledList() {
|
|
869
|
-
const manifest = readManifest();
|
|
952
|
+
export function getInstalledList(options = {}) {
|
|
953
|
+
const manifest = readManifest(options.home);
|
|
870
954
|
// Only return entries actually installed — skipped entries (e.g. Python missing)
|
|
871
955
|
// shouldn't show up as "installed" to the rest of the system.
|
|
872
956
|
return Object.entries(manifest.installed)
|
|
@@ -874,6 +958,34 @@ export function getInstalledList() {
|
|
|
874
958
|
.map(([key]) => key);
|
|
875
959
|
}
|
|
876
960
|
|
|
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
|
+
|
|
877
989
|
export function suggestForTeam(namespace) {
|
|
878
990
|
if (!namespace) return [];
|
|
879
991
|
|
|
@@ -895,7 +1007,8 @@ export function suggestForTeam(namespace) {
|
|
|
895
1007
|
// AUTO-INSTALL (called from init.mjs - installs suggested integrations)
|
|
896
1008
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
897
1009
|
|
|
898
|
-
export async function autoInstallIntegrations(team,
|
|
1010
|
+
export async function autoInstallIntegrations(team, options = {}) {
|
|
1011
|
+
const { silent = false, installer = installIntegration } = options;
|
|
899
1012
|
// Get suggested integrations for this team
|
|
900
1013
|
const suggested = suggestForTeam(team);
|
|
901
1014
|
if (!suggested || suggested.length === 0) {
|
|
@@ -903,7 +1016,7 @@ export async function autoInstallIntegrations(team, { silent = false, installer
|
|
|
903
1016
|
}
|
|
904
1017
|
|
|
905
1018
|
// Only install if they're not already installed
|
|
906
|
-
const toInstall = suggested.filter((key) => !isInstalled(key));
|
|
1019
|
+
const toInstall = suggested.filter((key) => !isInstalled(key, options));
|
|
907
1020
|
if (toInstall.length === 0) {
|
|
908
1021
|
return [];
|
|
909
1022
|
}
|
|
@@ -914,8 +1027,8 @@ export async function autoInstallIntegrations(team, { silent = false, installer
|
|
|
914
1027
|
|
|
915
1028
|
const installed = [];
|
|
916
1029
|
for (const key of toInstall) {
|
|
917
|
-
const
|
|
918
|
-
if (
|
|
1030
|
+
const result = await installer(key, { ...options, silent });
|
|
1031
|
+
if (integrationSucceeded(result)) {
|
|
919
1032
|
installed.push(INTEGRATIONS[key].label);
|
|
920
1033
|
}
|
|
921
1034
|
}
|
|
@@ -1011,8 +1124,8 @@ export async function promptAndInstall(team, { silent = false } = {}) {
|
|
|
1011
1124
|
// Install all
|
|
1012
1125
|
const installed = [];
|
|
1013
1126
|
for (const key of toInstall) {
|
|
1014
|
-
const
|
|
1015
|
-
if (
|
|
1127
|
+
const result = await installIntegration(key, { silent: false });
|
|
1128
|
+
if (integrationSucceeded(result)) {
|
|
1016
1129
|
installed.push(INTEGRATIONS[key].label);
|
|
1017
1130
|
}
|
|
1018
1131
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const SELF_PATH = fileURLToPath(import.meta.url);
|
|
7
|
+
|
|
8
|
+
export function detectPackageManager(selfPath = SELF_PATH) {
|
|
9
|
+
const normalizedPath = String(selfPath || '').replace(/\\/g, '/');
|
|
10
|
+
if (normalizedPath.includes('/.volta/')) return 'volta';
|
|
11
|
+
if (normalizedPath.includes('/pnpm/')) return 'pnpm';
|
|
12
|
+
return 'npm';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildGlobalPackageInstall(packageSpec, options = {}) {
|
|
16
|
+
const packageManager = options.packageManager || detectPackageManager(options.selfPath);
|
|
17
|
+
if (packageManager === 'volta') {
|
|
18
|
+
return { packageManager, command: 'volta', args: ['install', packageSpec] };
|
|
19
|
+
}
|
|
20
|
+
if (packageManager === 'pnpm') {
|
|
21
|
+
return { packageManager, command: 'pnpm', args: ['add', '-g', packageSpec] };
|
|
22
|
+
}
|
|
23
|
+
return { packageManager: 'npm', command: 'npm', args: ['i', '-g', packageSpec] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatCommand({ command, args = [] }) {
|
|
27
|
+
return [command, ...args].join(' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function installGlobalPackage(packageSpec, options = {}) {
|
|
31
|
+
const plan = buildGlobalPackageInstall(packageSpec, options);
|
|
32
|
+
const result = spawnSync(plan.command, plan.args, {
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
env: options.env || process.env,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: options.stdio || 'pipe',
|
|
37
|
+
timeout: options.timeout || 5 * 60 * 1000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...plan,
|
|
42
|
+
ok: result.status === 0,
|
|
43
|
+
status: result.status,
|
|
44
|
+
stdout: result.stdout || '',
|
|
45
|
+
stderr: result.stderr || '',
|
|
46
|
+
error: result.error || null,
|
|
47
|
+
commandLine: formatCommand(plan),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getNpmPrefixBin(options = {}) {
|
|
52
|
+
const runner = options.runner || spawnSync;
|
|
53
|
+
const result = runner('npm', ['prefix', '-g'], {
|
|
54
|
+
env: options.env || process.env,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
stdio: 'pipe',
|
|
57
|
+
timeout: options.timeout || 10_000,
|
|
58
|
+
});
|
|
59
|
+
if (result.status !== 0) return null;
|
|
60
|
+
const prefix = String(result.stdout || '').trim();
|
|
61
|
+
if (!prefix) return null;
|
|
62
|
+
return join(prefix, process.platform === 'win32' ? '' : 'bin');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getAwPackageRoot(selfPath = SELF_PATH) {
|
|
66
|
+
if (selfPath.endsWith('package-manager.mjs')) return dirname(selfPath);
|
|
67
|
+
return dirname(selfPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function executableExists(path) {
|
|
71
|
+
return Boolean(path && existsSync(path));
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"link.mjs",
|
|
23
23
|
"manifest.mjs",
|
|
24
24
|
"mcp.mjs",
|
|
25
|
+
"package-manager.mjs",
|
|
25
26
|
"paths.mjs",
|
|
26
27
|
"plan.mjs",
|
|
27
28
|
"registry.mjs",
|
|
@@ -54,9 +55,9 @@
|
|
|
54
55
|
"license": "MIT",
|
|
55
56
|
"scripts": {
|
|
56
57
|
"test": "yarn test:vitest && yarn test:node",
|
|
57
|
-
"test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
|
|
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",
|
|
58
59
|
"test:node": "node tests/run-node-tests.mjs",
|
|
59
|
-
"test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.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",
|
|
60
61
|
"preuninstall": "node bin.js nuke 2>/dev/null || true"
|
|
61
62
|
},
|
|
62
63
|
"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 { readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, 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,15 +39,16 @@ const LOG_MAX_ENTRIES = 50;
|
|
|
39
39
|
|
|
40
40
|
const SELF_PATH = fileURLToPath(import.meta.url);
|
|
41
41
|
|
|
42
|
-
function detectPM() {
|
|
43
|
-
|
|
44
|
-
if (
|
|
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';
|
|
45
46
|
return 'npm';
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const PM = detectPM();
|
|
49
50
|
|
|
50
|
-
function getInstallCmd(version = 'latest') {
|
|
51
|
+
export function getInstallCmd(version = 'latest') {
|
|
51
52
|
const spec = `${PKG_NAME}@${version}`;
|
|
52
53
|
if (PM === 'volta') return `volta install ${spec}`;
|
|
53
54
|
if (PM === 'pnpm') return `pnpm add -g ${spec}`;
|
|
@@ -91,23 +92,36 @@ function compareVersions(a, b) {
|
|
|
91
92
|
// After install, re-resolve where the package actually landed and read
|
|
92
93
|
// its package.json. Falls back to getLocalVersion() (our own bundle).
|
|
93
94
|
|
|
94
|
-
function
|
|
95
|
+
export function getInstalledPackageRoot(options = {}) {
|
|
96
|
+
const packageManager = options.packageManager || PM;
|
|
97
|
+
const runner = options.execSync || execSync;
|
|
95
98
|
try {
|
|
96
|
-
if (
|
|
99
|
+
if (packageManager === 'volta') {
|
|
97
100
|
// Volta resolves via its own image; ask it directly
|
|
98
|
-
const resolved =
|
|
101
|
+
const resolved = runner('volta which aw', {
|
|
99
102
|
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
100
103
|
}).trim();
|
|
101
|
-
const
|
|
102
|
-
|
|
104
|
+
const imageRoot = join(resolved, '..', '..');
|
|
105
|
+
const packageRoot = join(imageRoot, 'lib', 'node_modules', PKG_NAME);
|
|
106
|
+
return existsSync(join(packageRoot, 'package.json')) ? packageRoot : imageRoot;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
// npm / pnpm: read from the global prefix
|
|
106
|
-
const prefix =
|
|
110
|
+
const prefix = runner('npm prefix -g', {
|
|
107
111
|
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
108
112
|
}).trim();
|
|
109
|
-
|
|
110
|
-
|
|
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;
|
|
111
125
|
} catch {
|
|
112
126
|
return getLocalVersion();
|
|
113
127
|
}
|
|
@@ -200,7 +214,8 @@ export function autoUpdate(result) {
|
|
|
200
214
|
execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
|
|
201
215
|
const installed = getInstalledVersion();
|
|
202
216
|
const ok = compareVersions(installed, result.current) > 0;
|
|
203
|
-
const
|
|
217
|
+
const packageRoot = getInstalledPackageRoot();
|
|
218
|
+
const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, packageRoot, ok };
|
|
204
219
|
appendLog(entry);
|
|
205
220
|
return { status: ok ? 'upgraded' : 'failed', ...entry };
|
|
206
221
|
} catch (e) {
|
package/integrations/index.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ensureContextModeIntegration,
|
|
3
|
-
getContextModeIntegrationSummary,
|
|
4
|
-
removeContextModeIntegration,
|
|
5
|
-
} from './context-mode.mjs';
|
|
6
|
-
|
|
7
|
-
export const KNOWN_INTEGRATIONS = [
|
|
8
|
-
{
|
|
9
|
-
name: 'context-mode',
|
|
10
|
-
description: 'Local context-mode MCP server and hook integration.',
|
|
11
|
-
add: ensureContextModeIntegration,
|
|
12
|
-
remove: removeContextModeIntegration,
|
|
13
|
-
status: getContextModeIntegrationSummary,
|
|
14
|
-
},
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
export function resolveIntegration(name) {
|
|
18
|
-
const normalized = String(name || '').trim().toLowerCase();
|
|
19
|
-
const integration = KNOWN_INTEGRATIONS.find(item => item.name === normalized);
|
|
20
|
-
if (integration) return integration;
|
|
21
|
-
|
|
22
|
-
const known = KNOWN_INTEGRATIONS.map(item => item.name).join(', ');
|
|
23
|
-
throw new Error(`Unknown integration: ${name || '<missing>'}. Known integrations: ${known}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function listIntegrations(home, options = {}) {
|
|
27
|
-
return KNOWN_INTEGRATIONS.map(integration => ({
|
|
28
|
-
...integration.status(home, options),
|
|
29
|
-
description: integration.description,
|
|
30
|
-
}));
|
|
31
|
-
}
|