@ghl-ai/aw 0.1.45 → 0.1.46
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 +7 -1
- package/commands/init.mjs +17 -0
- package/commands/integrations.mjs +254 -0
- package/integrations.mjs +954 -0
- package/package.json +4 -3
package/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const COMMANDS = {
|
|
|
26
26
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
27
27
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
28
28
|
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
29
|
+
integrations: () => import('./commands/integrations.mjs').then(m => m.integrationsCommand),
|
|
29
30
|
'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
|
|
30
31
|
c4: () => import('./commands/c4.mjs').then(m => m.c4Command),
|
|
31
32
|
'init-repo': () => import('./commands/init-repo.mjs').then(m => m.initRepoCommand),
|
|
@@ -82,8 +83,9 @@ function printHelp() {
|
|
|
82
83
|
const sec = (title) => `\n ${chalk.bold.underline(title)}`;
|
|
83
84
|
const help = [
|
|
84
85
|
sec('Setup'),
|
|
85
|
-
cmd('aw init', 'Initialize workspace (
|
|
86
|
+
cmd('aw init', 'Initialize workspace (auto-installs suggested integrations)'),
|
|
86
87
|
cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
|
|
88
|
+
cmd('aw init --no-integrations', 'Skip integration setup (Codex, Caveman, Graphify, etc)'),
|
|
87
89
|
` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
|
|
88
90
|
` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
|
|
89
91
|
cmd('aw init-repo', 'Scaffold cloud-bootstrap files (idempotent, --dry-run/--force/--diff)'),
|
|
@@ -109,6 +111,10 @@ function printHelp() {
|
|
|
109
111
|
cmd('aw routing status', 'Show global AW session-routing mode for Claude/Cursor/Codex'),
|
|
110
112
|
cmd('aw routing disable', 'Disable automatic AW session routing globally'),
|
|
111
113
|
cmd('aw routing enable', 'Re-enable automatic AW session routing globally'),
|
|
114
|
+
cmd('aw integrations', 'Manage third-party integrations (Codex, Caveman, etc)'),
|
|
115
|
+
cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman)'),
|
|
116
|
+
cmd('aw integrations remove <key>', 'Remove a tool'),
|
|
117
|
+
cmd('aw integrations bundle <name>', 'Install a preset bundle'),
|
|
112
118
|
cmd('aw drop <path>', 'Stop syncing or delete local content'),
|
|
113
119
|
cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
|
|
114
120
|
cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
|
package/commands/init.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import { loadConfig as ensureTelemetryConfig } from '../telemetry.mjs';
|
|
|
34
34
|
import { installAwEcc, AW_ECC_TAG } from '../ecc.mjs';
|
|
35
35
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
36
36
|
import { readHookManifest, pruneStaleHooks, writeHookManifest } from '../hook-cleanup.mjs';
|
|
37
|
+
import { promptAndInstall, autoInstallIntegrations } from '../integrations.mjs';
|
|
37
38
|
import {
|
|
38
39
|
initPersistentClone,
|
|
39
40
|
isValidClone,
|
|
@@ -212,6 +213,7 @@ export async function initCommand(args) {
|
|
|
212
213
|
let namespace = args['--namespace'] || null;
|
|
213
214
|
let user = args['--user'] || '';
|
|
214
215
|
const silent = args['--silent'] === true;
|
|
216
|
+
const skipIntegrations = args['--no-integrations'] === true;
|
|
215
217
|
|
|
216
218
|
// In silent mode, suppress ALL fmt output and show a single spinner.
|
|
217
219
|
// setSilent(true) makes every fmt.* call a no-op — internal functions
|
|
@@ -414,6 +416,12 @@ export async function initCommand(args) {
|
|
|
414
416
|
// Write hook manifest after all hook installation is complete
|
|
415
417
|
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
416
418
|
|
|
419
|
+
// Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
|
|
420
|
+
let installedIntegrations = [];
|
|
421
|
+
if (!silent && !skipIntegrations && !isNewSubTeam) {
|
|
422
|
+
installedIntegrations = await autoInstallIntegrations(freshCfg?.namespace || team, { silent });
|
|
423
|
+
}
|
|
424
|
+
|
|
417
425
|
if (silent) {
|
|
418
426
|
if (silentSpinner) { silentSpinner.stop('Done'); setSilent(false); }
|
|
419
427
|
autoUpdate(await args._updateCheck);
|
|
@@ -427,6 +435,7 @@ export async function initCommand(args) {
|
|
|
427
435
|
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
428
436
|
: null,
|
|
429
437
|
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
438
|
+
installedIntegrations.length > 0 ? ` ${chalk.green('✓')} Integrations: ${installedIntegrations.join(', ')}` : null,
|
|
430
439
|
].filter(Boolean).join('\n'));
|
|
431
440
|
}
|
|
432
441
|
return;
|
|
@@ -574,6 +583,12 @@ export async function initCommand(args) {
|
|
|
574
583
|
// Ensure telemetry config exists (generates machine_id on first run)
|
|
575
584
|
ensureTelemetryConfig();
|
|
576
585
|
|
|
586
|
+
// Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
|
|
587
|
+
let installedIntegrations = [];
|
|
588
|
+
if (!silent && !skipIntegrations) {
|
|
589
|
+
installedIntegrations = await autoInstallIntegrations(team, { silent });
|
|
590
|
+
}
|
|
591
|
+
|
|
577
592
|
// Offer to update if a newer version is available
|
|
578
593
|
if (!silent) await promptUpdate(await args._updateCheck);
|
|
579
594
|
|
|
@@ -593,10 +608,12 @@ export async function initCommand(args) {
|
|
|
593
608
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
594
609
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
|
595
610
|
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
|
|
611
|
+
installedIntegrations.length > 0 ? ` ${chalk.green('✓')} Integrations: ${installedIntegrations.join(', ')}` : null,
|
|
596
612
|
'',
|
|
597
613
|
` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
|
|
598
614
|
` ${chalk.dim('New clones:')} auto-linked via git hook`,
|
|
599
615
|
` ${chalk.dim('Update:')} ${chalk.bold('aw init')} ${chalk.dim('(or auto on pull/IDE open)')}`,
|
|
616
|
+
` ${chalk.dim('Integrations:')} ${chalk.bold('aw integrations')} ${chalk.dim('(manage Codex, Caveman, etc)')}`,
|
|
600
617
|
` ${chalk.dim('Uninstall:')} ${chalk.bold('aw nuke')}`,
|
|
601
618
|
].filter(Boolean).join('\n'));
|
|
602
619
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// commands/integrations.mjs — CLI command: aw integrations add/remove/list/bundle
|
|
2
|
+
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import * as fmt from '../fmt.mjs';
|
|
5
|
+
import { chalk } from '../fmt.mjs';
|
|
6
|
+
import {
|
|
7
|
+
INTEGRATIONS,
|
|
8
|
+
BUNDLES,
|
|
9
|
+
installIntegration,
|
|
10
|
+
removeIntegration,
|
|
11
|
+
getInstalledList,
|
|
12
|
+
} from '../integrations.mjs';
|
|
13
|
+
|
|
14
|
+
export async function integrationsCommand(args) {
|
|
15
|
+
const subcommand = args._positional[0];
|
|
16
|
+
|
|
17
|
+
switch (subcommand) {
|
|
18
|
+
case 'add':
|
|
19
|
+
return cmdAdd(args);
|
|
20
|
+
case 'remove':
|
|
21
|
+
return cmdRemove(args);
|
|
22
|
+
case 'bundle':
|
|
23
|
+
return cmdBundle(args);
|
|
24
|
+
case 'list':
|
|
25
|
+
case undefined:
|
|
26
|
+
return cmdList();
|
|
27
|
+
default:
|
|
28
|
+
fmt.cancel(`Unknown subcommand: ${subcommand}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// aw integrations list
|
|
34
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
async function cmdList() {
|
|
37
|
+
fmt.banner('Integrations', {
|
|
38
|
+
icon: '🔗',
|
|
39
|
+
subtitle: ' Available tools and MCP servers',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const installed = getInstalledList();
|
|
43
|
+
|
|
44
|
+
// Group by type
|
|
45
|
+
const plugins = Object.entries(INTEGRATIONS).filter(
|
|
46
|
+
([, i]) => i.type === 'plugin'
|
|
47
|
+
);
|
|
48
|
+
const remoteRcps = Object.entries(INTEGRATIONS).filter(
|
|
49
|
+
([, i]) => i.type === 'remote-mcp'
|
|
50
|
+
);
|
|
51
|
+
const universalInstallers = Object.entries(INTEGRATIONS).filter(
|
|
52
|
+
([, i]) => i.type === 'universal-installer'
|
|
53
|
+
);
|
|
54
|
+
const pythonClis = Object.entries(INTEGRATIONS).filter(
|
|
55
|
+
([, i]) => i.type === 'python-cli'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const typeIcon = (type) =>
|
|
59
|
+
type === 'plugin' ? '🔌' : type === 'remote-mcp' ? '🌐' : type === 'universal-installer' ? '🪨' : '⚙️';
|
|
60
|
+
|
|
61
|
+
// Installed section
|
|
62
|
+
if (installed.length > 0) {
|
|
63
|
+
fmt.logMessage(`\n${chalk.bold.underline('Installed')}`);
|
|
64
|
+
for (const key of installed) {
|
|
65
|
+
const integration = INTEGRATIONS[key];
|
|
66
|
+
if (!integration) continue;
|
|
67
|
+
fmt.logSuccess(` ${typeIcon(integration.type)} ${integration.label}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Available Plugins
|
|
72
|
+
fmt.logMessage(`\n${chalk.bold.underline('Available Plugins')}`);
|
|
73
|
+
for (const [key, integration] of plugins) {
|
|
74
|
+
if (!installed.includes(key)) {
|
|
75
|
+
fmt.logMessage(
|
|
76
|
+
` 🔌 ${integration.label.padEnd(25)} — ${integration.description}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Available Remote MCPs
|
|
82
|
+
fmt.logMessage(`\n${chalk.bold.underline('Available Remote MCPs')}`);
|
|
83
|
+
for (const [key, integration] of remoteRcps) {
|
|
84
|
+
if (!installed.includes(key)) {
|
|
85
|
+
fmt.logMessage(
|
|
86
|
+
` 🌐 ${integration.label.padEnd(25)} — ${integration.description}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Universal Installers
|
|
92
|
+
if (universalInstallers.length > 0) {
|
|
93
|
+
fmt.logMessage(`\n${chalk.bold.underline('Universal Tools')}`);
|
|
94
|
+
for (const [key, integration] of universalInstallers) {
|
|
95
|
+
if (!installed.includes(key)) {
|
|
96
|
+
fmt.logMessage(
|
|
97
|
+
` 🪨 ${integration.label.padEnd(25)} — ${integration.description}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Python CLIs
|
|
104
|
+
if (pythonClis.length > 0) {
|
|
105
|
+
fmt.logMessage(`\n${chalk.bold.underline('Available Python Tools')}`);
|
|
106
|
+
for (const [key, integration] of pythonClis) {
|
|
107
|
+
if (!installed.includes(key)) {
|
|
108
|
+
fmt.logMessage(
|
|
109
|
+
` 🐍 ${integration.label.padEnd(25)} — ${integration.description}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Bundles
|
|
116
|
+
fmt.logMessage(`\n${chalk.bold.underline('Bundles')}`);
|
|
117
|
+
for (const [bundleKey, bundle] of Object.entries(BUNDLES)) {
|
|
118
|
+
fmt.logMessage(
|
|
119
|
+
` 📦 ${bundle.label.padEnd(25)} — ${bundle.description}`
|
|
120
|
+
);
|
|
121
|
+
fmt.logMessage(
|
|
122
|
+
` Includes: ${bundle.includes.map((k) => INTEGRATIONS[k].label).join(', ')}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fmt.logMessage(`\n${chalk.dim('Commands:')}`);
|
|
127
|
+
fmt.logMessage(` aw integrations add <key> Install a specific tool`);
|
|
128
|
+
fmt.logMessage(` aw integrations remove <key> Remove a tool`);
|
|
129
|
+
fmt.logMessage(` aw integrations bundle <name> Install a preset bundle`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
// aw integrations add <key>
|
|
134
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async function cmdAdd(args) {
|
|
137
|
+
const key = args._positional[1];
|
|
138
|
+
|
|
139
|
+
if (!key) {
|
|
140
|
+
fmt.cancel('Usage: aw integrations add <key>');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!INTEGRATIONS[key]) {
|
|
144
|
+
// Suggest similar keys
|
|
145
|
+
const available = Object.keys(INTEGRATIONS);
|
|
146
|
+
fmt.cancel(
|
|
147
|
+
[
|
|
148
|
+
`Unknown integration: ${chalk.red(key)}`,
|
|
149
|
+
'',
|
|
150
|
+
`Available: ${available.join(', ')}`,
|
|
151
|
+
].join('\n')
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const integration = INTEGRATIONS[key];
|
|
156
|
+
fmt.intro(`Installing ${integration.label}`);
|
|
157
|
+
|
|
158
|
+
const success = await installIntegration(key, { silent: false });
|
|
159
|
+
|
|
160
|
+
if (success) {
|
|
161
|
+
fmt.outro(`✓ ${integration.label} installed successfully`);
|
|
162
|
+
} else {
|
|
163
|
+
fmt.cancel(`Failed to install ${integration.label}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// aw integrations remove <key>
|
|
169
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
async function cmdRemove(args) {
|
|
172
|
+
const key = args._positional[1];
|
|
173
|
+
|
|
174
|
+
if (!key) {
|
|
175
|
+
fmt.cancel('Usage: aw integrations remove <key>');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!INTEGRATIONS[key]) {
|
|
179
|
+
const available = Object.keys(INTEGRATIONS);
|
|
180
|
+
fmt.cancel(
|
|
181
|
+
[
|
|
182
|
+
`Unknown integration: ${chalk.red(key)}`,
|
|
183
|
+
'',
|
|
184
|
+
`Available: ${available.join(', ')}`,
|
|
185
|
+
].join('\n')
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const integration = INTEGRATIONS[key];
|
|
190
|
+
fmt.intro(`Removing ${integration.label}`);
|
|
191
|
+
|
|
192
|
+
const success = await removeIntegration(key, { silent: false });
|
|
193
|
+
|
|
194
|
+
if (success) {
|
|
195
|
+
fmt.outro(`✓ ${integration.label} removed successfully`);
|
|
196
|
+
} else {
|
|
197
|
+
fmt.cancel(`Failed to remove ${integration.label}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// aw integrations bundle <bundleName>
|
|
203
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
async function cmdBundle(args) {
|
|
206
|
+
const bundleName = args._positional[1];
|
|
207
|
+
|
|
208
|
+
if (!bundleName) {
|
|
209
|
+
fmt.cancel('Usage: aw integrations bundle <name>');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!BUNDLES[bundleName]) {
|
|
213
|
+
const available = Object.keys(BUNDLES);
|
|
214
|
+
fmt.cancel(
|
|
215
|
+
[
|
|
216
|
+
`Unknown bundle: ${chalk.red(bundleName)}`,
|
|
217
|
+
'',
|
|
218
|
+
`Available: ${available.join(', ')}`,
|
|
219
|
+
].join('\n')
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bundle = BUNDLES[bundleName];
|
|
224
|
+
fmt.intro(`Installing bundle: ${bundle.label}`);
|
|
225
|
+
|
|
226
|
+
fmt.logMessage(`${bundle.description}\n`);
|
|
227
|
+
fmt.logMessage(`Includes:`);
|
|
228
|
+
for (const key of bundle.includes) {
|
|
229
|
+
const integration = INTEGRATIONS[key];
|
|
230
|
+
fmt.logMessage(` • ${integration.label}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const confirm = await p.default.confirm({
|
|
234
|
+
message: `Continue installing ${bundle.includes.length} tool(s)?`,
|
|
235
|
+
initialValue: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (p.default.isCancel(confirm) || !confirm) {
|
|
239
|
+
fmt.cancel('Cancelled');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fmt.logMessage('');
|
|
243
|
+
|
|
244
|
+
// Install all
|
|
245
|
+
let successCount = 0;
|
|
246
|
+
for (const key of bundle.includes) {
|
|
247
|
+
const success = await installIntegration(key, { silent: false });
|
|
248
|
+
if (success) successCount++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fmt.outro(
|
|
252
|
+
`✓ Bundle installation complete (${successCount}/${bundle.includes.length} installed)`
|
|
253
|
+
);
|
|
254
|
+
}
|
package/integrations.mjs
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// integrations.mjs — Registry, manifest, and installation for third-party integrations.
|
|
2
|
+
// Handles four types:
|
|
3
|
+
// - plugin (claude plugin install)
|
|
4
|
+
// - remote-mcp (add to mcp.json / config.toml)
|
|
5
|
+
// - universal-installer (runs the integration's cross-IDE installer)
|
|
6
|
+
// - python-cli (pip-install a Python CLI tool, then run its setup commands)
|
|
7
|
+
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { exec } from 'node:child_process';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { join, basename } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import * as fmt from './fmt.mjs';
|
|
21
|
+
import { chalk } from './fmt.mjs';
|
|
22
|
+
|
|
23
|
+
const execAsync = promisify(exec);
|
|
24
|
+
const HOME = homedir();
|
|
25
|
+
const MANIFEST_PATH = join(HOME, '.aw_registry', '.integration-manifest.json');
|
|
26
|
+
|
|
27
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// INTEGRATION REGISTRY
|
|
29
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const INTEGRATIONS = {
|
|
32
|
+
// PLUGINS (installed via: claude plugin install)
|
|
33
|
+
'codex': {
|
|
34
|
+
type: 'plugin',
|
|
35
|
+
label: 'OpenAI Codex',
|
|
36
|
+
installCmd: 'codex@openai-codex',
|
|
37
|
+
marketplaceSource: 'openai/codex-plugin-cc',
|
|
38
|
+
description: '/codex:review, /codex:adversarial-review, /codex:rescue — delegate tasks or get code reviews from Codex',
|
|
39
|
+
teams: [], // universal
|
|
40
|
+
requiresAuth: true,
|
|
41
|
+
authNote: 'Run /codex:setup to verify Codex is ready. If not logged in yet, run: !codex login',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
'lean-ctx': {
|
|
45
|
+
type: 'universal-installer',
|
|
46
|
+
label: 'LeanCTX',
|
|
47
|
+
description: 'Context OS for AI — compresses file reads + shell output + memory (60-99% fewer input tokens)',
|
|
48
|
+
scripts: {
|
|
49
|
+
win32: null, // no PS1 — uses npm fallback (lean-ctx-bin)
|
|
50
|
+
posix: 'https://leanctx.com/install.sh',
|
|
51
|
+
},
|
|
52
|
+
npmPackage: 'lean-ctx-bin', // Windows fallback via npm
|
|
53
|
+
postInstall: ['lean-ctx setup'], // auto-detects + configures all IDEs in one shot
|
|
54
|
+
teams: [], // universal
|
|
55
|
+
requiresAuth: false,
|
|
56
|
+
authNote: 'Restart your IDE after install — lean-ctx setup auto-configures Claude Code, Cursor, Codex, Gemini, and more',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
rtk: {
|
|
60
|
+
type: 'universal-installer',
|
|
61
|
+
label: 'RTK (Rust Token Killer)',
|
|
62
|
+
description: 'CLI proxy that filters/compresses shell output (git, tests, logs) by 60-90%',
|
|
63
|
+
scripts: {
|
|
64
|
+
win32: null, // use WSL fallback
|
|
65
|
+
posix: 'https://raw.githubusercontent.com/rtk-ai/rtk/main/install.sh',
|
|
66
|
+
},
|
|
67
|
+
postInstall: [
|
|
68
|
+
'rtk init -g', // Claude Code
|
|
69
|
+
'rtk init -g --gemini', // Gemini CLI
|
|
70
|
+
'rtk init -g --codex', // Codex
|
|
71
|
+
'rtk init -g --agent cursor', // Cursor
|
|
72
|
+
],
|
|
73
|
+
teams: [], // universal — every team benefits
|
|
74
|
+
requiresAuth: false,
|
|
75
|
+
authNote: 'Restart Claude Code after install for the auto-rewrite hook to take effect',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
caveman: {
|
|
79
|
+
type: 'plugin',
|
|
80
|
+
label: 'Caveman',
|
|
81
|
+
installCmd: 'caveman@caveman',
|
|
82
|
+
marketplaceSource: 'JuliusBrussee/caveman',
|
|
83
|
+
description: 'Token-efficient responses — ~75% fewer output tokens (lite / full / ultra / wenyan modes)',
|
|
84
|
+
teams: [], // universal — every team benefits
|
|
85
|
+
requiresAuth: false,
|
|
86
|
+
authNote: 'Activate with /caveman in any session (lite / full / ultra modes)',
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
skills: {
|
|
90
|
+
type: 'universal-installer',
|
|
91
|
+
label: 'Agent Skills (Matt Pocock)',
|
|
92
|
+
description: 'Slash commands for real engineering: /tdd, /diagnose, /grill-me, /grill-with-docs, /triage, /to-prd, /zoom-out',
|
|
93
|
+
scripts: {
|
|
94
|
+
win32: null,
|
|
95
|
+
posix: null, // no binary — installed entirely via npx below
|
|
96
|
+
},
|
|
97
|
+
postInstall: [
|
|
98
|
+
'npx skills@latest add mattpocock/skills',
|
|
99
|
+
],
|
|
100
|
+
teams: [],
|
|
101
|
+
requiresAuth: false,
|
|
102
|
+
authNote: 'Interactive install — pick which skills + agents you want. Then run /setup-matt-pocock-skills once per repo.',
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// PYTHON CLIs (installed via uv / pipx / pip, then runs post-install hooks)
|
|
106
|
+
graphify: {
|
|
107
|
+
type: 'python-cli',
|
|
108
|
+
label: 'Graphify (Knowledge Graph)',
|
|
109
|
+
description: 'Builds a queryable knowledge graph of your codebase + docs',
|
|
110
|
+
pipPackage: 'graphifyy',
|
|
111
|
+
cliCommand: 'graphify',
|
|
112
|
+
minPython: { major: 3, minor: 10 },
|
|
113
|
+
postInstall: [
|
|
114
|
+
['install'], // global: registers the /graphify skill in ~/.claude/skills/
|
|
115
|
+
],
|
|
116
|
+
perProjectInstall: [
|
|
117
|
+
// IDE wiring — only runs if that IDE's config dir exists on this machine.
|
|
118
|
+
// Each command writes the IDE-specific CLAUDE.md/AGENTS.md section + hook.
|
|
119
|
+
{ args: ['claude', 'install'], requiresGit: false, requiresIde: '.claude' },
|
|
120
|
+
{ args: ['codex', 'install'], requiresGit: false, requiresIde: '.codex' },
|
|
121
|
+
{ args: ['cursor', 'install'], requiresGit: false, requiresIde: '.cursor' },
|
|
122
|
+
{ args: ['gemini', 'install'], requiresGit: false, requiresIde: '.gemini' },
|
|
123
|
+
// Git hooks — post-commit AST rebuild + post-checkout sync + merge driver
|
|
124
|
+
{ args: ['hook', 'install'], requiresGit: true },
|
|
125
|
+
// If a graph already exists, register it into the global graph immediately.
|
|
126
|
+
// appendCwdBasename appends the project folder name as the --as tag.
|
|
127
|
+
{ args: ['global', 'add', 'graphify-out/graph.json', '--as'], appendCwdBasename: true, requiresFile: 'graphify-out/graph.json' },
|
|
128
|
+
],
|
|
129
|
+
teams: [], // universal — every team benefits from a knowledge graph
|
|
130
|
+
requiresAuth: false,
|
|
131
|
+
authNote: 'run /graphify . inside your IDE to build the graph whenever you need it',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
// BUNDLES (preset groups for common use cases)
|
|
137
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export const BUNDLES = {};
|
|
140
|
+
|
|
141
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
// MANIFEST MANAGEMENT
|
|
143
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export function readManifest() {
|
|
146
|
+
if (!existsSync(MANIFEST_PATH)) {
|
|
147
|
+
return { version: 1, installed: {} };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
|
|
152
|
+
} catch {
|
|
153
|
+
return { version: 1, installed: {} };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function writeManifest(manifest) {
|
|
158
|
+
mkdirSync(join(HOME, '.aw_registry'), { recursive: true });
|
|
159
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isInstalled(key) {
|
|
163
|
+
const manifest = readManifest();
|
|
164
|
+
const entry = manifest.installed[key];
|
|
165
|
+
if (!entry) return false;
|
|
166
|
+
// Backward compat: pre-existing entries have no `status` field — treat as installed.
|
|
167
|
+
// Skipped entries (e.g. python-cli skipped because Python missing) return false so
|
|
168
|
+
// autoInstallIntegrations retries them on the next `aw init` run.
|
|
169
|
+
return entry.status !== 'skipped';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function recordInstalled(key, type) {
|
|
173
|
+
const manifest = readManifest();
|
|
174
|
+
manifest.installed[key] = {
|
|
175
|
+
type,
|
|
176
|
+
status: 'installed',
|
|
177
|
+
installedAt: new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
writeManifest(manifest);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recordSkipped(key, type, reason) {
|
|
183
|
+
const manifest = readManifest();
|
|
184
|
+
manifest.installed[key] = {
|
|
185
|
+
type,
|
|
186
|
+
status: 'skipped',
|
|
187
|
+
reason,
|
|
188
|
+
installedAt: new Date().toISOString(),
|
|
189
|
+
};
|
|
190
|
+
writeManifest(manifest);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
// JSON MCP CONFIG MERGE (Claude & Cursor)
|
|
195
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
function addToJsonMcp(filePath, serverName, config) {
|
|
198
|
+
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
199
|
+
|
|
200
|
+
let existing = {};
|
|
201
|
+
if (existsSync(filePath)) {
|
|
202
|
+
try {
|
|
203
|
+
existing = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
204
|
+
} catch {
|
|
205
|
+
// Corrupted file, start fresh
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
210
|
+
existing.mcpServers[serverName] = config;
|
|
211
|
+
|
|
212
|
+
writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// TOML MCP CONFIG MERGE (Codex)
|
|
217
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
function addToTomlMcp(filePath, serverName, url) {
|
|
220
|
+
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
221
|
+
|
|
222
|
+
let content = '';
|
|
223
|
+
if (existsSync(filePath)) {
|
|
224
|
+
content = readFileSync(filePath, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove any existing block for this server (idempotent)
|
|
228
|
+
const blockRegex = new RegExp(`\\[mcp_servers\\.${serverName}\\][^\\[]*`, 'g');
|
|
229
|
+
content = content.replace(blockRegex, '');
|
|
230
|
+
|
|
231
|
+
// Append new block
|
|
232
|
+
const newBlock = `[mcp_servers.${serverName}]\nurl = "${url}"\nstartup_timeout_sec = 30\n\n`;
|
|
233
|
+
content = content + newBlock;
|
|
234
|
+
|
|
235
|
+
writeFileSync(filePath, content);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
// PYTHON CLI INSTALLER (pip / pipx / uv tool, cross-platform)
|
|
240
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
243
|
+
const WHICH_CMD = IS_WINDOWS ? 'where' : 'which';
|
|
244
|
+
|
|
245
|
+
// Try interpreters in order; return the first that meets minPython, or null.
|
|
246
|
+
async function detectPython({ major, minor }) {
|
|
247
|
+
const candidates = IS_WINDOWS
|
|
248
|
+
? ['py -3', 'python', 'python3']
|
|
249
|
+
: ['python3', 'python'];
|
|
250
|
+
|
|
251
|
+
for (const cmd of candidates) {
|
|
252
|
+
try {
|
|
253
|
+
const { stdout } = await execAsync(`${cmd} --version`, { timeout: 10000 });
|
|
254
|
+
const m = stdout.match(/Python (\d+)\.(\d+)/);
|
|
255
|
+
if (!m) continue;
|
|
256
|
+
const [maj, min] = [parseInt(m[1], 10), parseInt(m[2], 10)];
|
|
257
|
+
if (maj > major || (maj === major && min >= minor)) {
|
|
258
|
+
return { cmd, version: `${maj}.${min}` };
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// Interpreter not present on PATH; try next.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Pick the best available Python package installer. Prefers tools that manage PATH.
|
|
268
|
+
async function pickPythonInstaller(pythonCmd) {
|
|
269
|
+
for (const tool of ['uv', 'pipx']) {
|
|
270
|
+
try {
|
|
271
|
+
await execAsync(`${WHICH_CMD} ${tool}`, { timeout: 5000 });
|
|
272
|
+
if (tool === 'uv') return { name: 'uv', build: pkg => `uv tool install "${pkg}"` };
|
|
273
|
+
if (tool === 'pipx') return { name: 'pipx', build: pkg => `pipx install "${pkg}"` };
|
|
274
|
+
} catch {
|
|
275
|
+
// Not on PATH; try next.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Fall back to user-site pip. Note: bin dir may not be on PATH after install
|
|
279
|
+
// — we work around that by falling back to `python -m <module>` when needed.
|
|
280
|
+
return { name: 'pip', build: pkg => `${pythonCmd} -m pip install --user "${pkg}"` };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Write a global SessionStart hook to ~/.claude/settings.json that registers the
|
|
284
|
+
// current project's graph into ~/.graphify/global-graph.json every time Claude Code
|
|
285
|
+
// opens in a project that has a built graph. Fast (JSON merge only, no LLM).
|
|
286
|
+
// Written globally so one hook covers all projects — guarded by graph.json existence.
|
|
287
|
+
export function installGraphifyGlobalAddHook(homeDir) {
|
|
288
|
+
const claudeDir = join(homeDir, '.claude');
|
|
289
|
+
if (!existsSync(claudeDir)) return;
|
|
290
|
+
|
|
291
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
292
|
+
let settings = {};
|
|
293
|
+
if (existsSync(settingsPath)) {
|
|
294
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { /* corrupt — start fresh */ }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!settings.hooks) settings.hooks = {};
|
|
298
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
299
|
+
|
|
300
|
+
const MARKER = 'graphify-global-add';
|
|
301
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
302
|
+
e => e?.description !== MARKER,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Always use `python -m graphify` (never the bare binary) so the hook is safe
|
|
306
|
+
// across future shell sessions where an old graphify binary might shadow the
|
|
307
|
+
// currently-installed version via PATH. Derived fresh here rather than baking
|
|
308
|
+
// in the cli token resolved at install time.
|
|
309
|
+
const pythonExe = IS_WINDOWS ? 'python' : 'python3';
|
|
310
|
+
const cmd = `[ -f graphify-out/graph.json ] && ${pythonExe} -m graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1 || true`;
|
|
311
|
+
|
|
312
|
+
settings.hooks.SessionStart.push({
|
|
313
|
+
description: MARKER,
|
|
314
|
+
hooks: [{ type: 'command', command: cmd }],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
318
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Write the graphify-global MCP server entry to ~/.claude/settings.json so Claude Code
|
|
322
|
+
// auto-starts it on launch. Uses `python -m graphify.serve` to avoid PATH issues with
|
|
323
|
+
// stale graphify.exe binaries that may shadow the currently installed version.
|
|
324
|
+
//
|
|
325
|
+
// The command is a single stable token — `python` on Windows, `python3` on POSIX —
|
|
326
|
+
// so the JSON args array is always parseable (py.cmd can be `py -3` which has a space
|
|
327
|
+
// and would break if used directly as the command field).
|
|
328
|
+
export function installGraphifyMcpServer(homeDir) {
|
|
329
|
+
const claudeDir = join(homeDir, '.claude');
|
|
330
|
+
if (!existsSync(claudeDir)) return;
|
|
331
|
+
|
|
332
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
333
|
+
let settings = {};
|
|
334
|
+
if (existsSync(settingsPath)) {
|
|
335
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { /* corrupt — start fresh */ }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
339
|
+
|
|
340
|
+
// Use join() so the path uses OS-correct separators on all platforms.
|
|
341
|
+
const globalGraphPath = join(homeDir, '.graphify', 'global-graph.json');
|
|
342
|
+
// `python` on Windows (Microsoft Store + standard installer both put it on PATH),
|
|
343
|
+
// `python3` on macOS/Linux (standard convention; `python` may not exist).
|
|
344
|
+
const pythonExe = IS_WINDOWS ? 'python' : 'python3';
|
|
345
|
+
|
|
346
|
+
settings.mcpServers['graphify-global'] = {
|
|
347
|
+
command: pythonExe,
|
|
348
|
+
args: ['-m', 'graphify.serve', globalGraphPath],
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
352
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// After pip install, the CLI binary may not be on PATH (pip --user puts it in a Scripts
|
|
356
|
+
// dir that is often missing from PATH, and on Windows an old binary can shadow the new
|
|
357
|
+
// one). When pip was used, always prefer `python -m <module>` to guarantee we run the
|
|
358
|
+
// just-installed version. For uv/pipx the binary is on PATH — probe with which/where
|
|
359
|
+
// (not --version, since some CLIs don't support that flag and exit non-zero).
|
|
360
|
+
async function resolveCliInvocation(cliCommand, pythonCmd, installer) {
|
|
361
|
+
if (installer?.name === 'pip') {
|
|
362
|
+
return `${pythonCmd} -m ${cliCommand}`;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await execAsync(`${WHICH_CMD} ${cliCommand}`, { timeout: 10000 });
|
|
366
|
+
return cliCommand;
|
|
367
|
+
} catch {
|
|
368
|
+
return `${pythonCmd} -m ${cliCommand}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function runPythonCli(integration, key, { silent = false } = {}) {
|
|
373
|
+
const spinner = silent ? null : fmt.spinner();
|
|
374
|
+
|
|
375
|
+
// 1. Detect Python ≥ minPython.
|
|
376
|
+
if (!silent) spinner.start(`Checking for Python ${integration.minPython.major}.${integration.minPython.minor}+...`);
|
|
377
|
+
const py = await detectPython(integration.minPython);
|
|
378
|
+
if (!py) {
|
|
379
|
+
if (!silent) spinner.stop(chalk.yellow('Python not found'));
|
|
380
|
+
recordSkipped(key, 'python-cli', 'python-not-found');
|
|
381
|
+
if (!silent) {
|
|
382
|
+
fmt.logWarn(
|
|
383
|
+
`${integration.label} skipped — Python ${integration.minPython.major}.${integration.minPython.minor}+ required.`,
|
|
384
|
+
);
|
|
385
|
+
fmt.note(
|
|
386
|
+
`Install Python from https://www.python.org/downloads/ and re-run \`aw init\` to retry.`,
|
|
387
|
+
'Skipped',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (!silent) spinner.stop(`✓ Python ${py.version} found`);
|
|
393
|
+
|
|
394
|
+
// 2. Pick installer (uv > pipx > pip --user).
|
|
395
|
+
const installer = await pickPythonInstaller(py.cmd);
|
|
396
|
+
|
|
397
|
+
// 3. Install the package.
|
|
398
|
+
if (!silent) spinner.start(`Installing ${integration.pipPackage} via ${installer.name}...`);
|
|
399
|
+
try {
|
|
400
|
+
await execAsync(installer.build(integration.pipPackage), {
|
|
401
|
+
timeout: 5 * 60 * 1000,
|
|
402
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
403
|
+
});
|
|
404
|
+
if (!silent) spinner.stop('✓ Package installed');
|
|
405
|
+
} catch (e) {
|
|
406
|
+
if (!silent) spinner.stop(chalk.yellow('Package install failed'));
|
|
407
|
+
throw new Error(`${installer.name} install failed: ${e.message}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 4. Resolve the CLI invocation (handles pip --user PATH gaps).
|
|
411
|
+
// Pass installer so pip-installed tools always use `python -m` (avoids Windows
|
|
412
|
+
// PATH shadowing where an old binary intercepts the command).
|
|
413
|
+
const cli = await resolveCliInvocation(integration.cliCommand, py.cmd, installer);
|
|
414
|
+
|
|
415
|
+
// 5. Run global post-install hooks (e.g. `graphify install` registers the skill).
|
|
416
|
+
for (const args of integration.postInstall || []) {
|
|
417
|
+
let resolvedArgs = [...args];
|
|
418
|
+
// graphify install needs --platform windows on Windows so the SKILL.md it writes
|
|
419
|
+
// uses PowerShell syntax instead of bash — without this the skill fails on first use.
|
|
420
|
+
if (integration.cliCommand === 'graphify' && resolvedArgs[0] === 'install' && IS_WINDOWS) {
|
|
421
|
+
resolvedArgs = [...resolvedArgs, '--platform', 'windows'];
|
|
422
|
+
}
|
|
423
|
+
const cmd = `${cli} ${resolvedArgs.join(' ')}`;
|
|
424
|
+
try {
|
|
425
|
+
await execAsync(cmd, { timeout: 60 * 1000, maxBuffer: 10 * 1024 * 1024 });
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (!silent) fmt.logWarn(`${cmd} failed: ${e.message}`);
|
|
428
|
+
// Non-fatal — global registration may already exist.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 5b. For graphify: wire up the global MCP server and the SessionStart hook that
|
|
433
|
+
// auto-registers any built graph into ~/.graphify/global-graph.json on IDE open.
|
|
434
|
+
// The graph itself is built manually via `/graphify .` — graphify's own per-IDE
|
|
435
|
+
// install (step 6 below) writes the CLAUDE.md / AGENTS.md sections for that.
|
|
436
|
+
if (integration.cliCommand === 'graphify') {
|
|
437
|
+
installGraphifyMcpServer(HOME);
|
|
438
|
+
installGraphifyGlobalAddHook(HOME);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 6. Run per-project hooks if cwd looks like a real project (not HOME).
|
|
442
|
+
const cwd = process.cwd();
|
|
443
|
+
const isHome = cwd === HOME;
|
|
444
|
+
const hasGit = existsSync(join(cwd, '.git'));
|
|
445
|
+
if (!isHome) {
|
|
446
|
+
for (const step of integration.perProjectInstall || []) {
|
|
447
|
+
if (step.requiresGit && !hasGit) continue;
|
|
448
|
+
// Skip IDE-specific steps when that IDE is not configured on this machine.
|
|
449
|
+
if (step.requiresIde && !existsSync(join(HOME, step.requiresIde))) continue;
|
|
450
|
+
// Skip steps that require a specific file to exist in the project (e.g. graph.json).
|
|
451
|
+
if (step.requiresFile && !existsSync(join(cwd, step.requiresFile))) continue;
|
|
452
|
+
const args = step.appendCwdBasename ? [...step.args, basename(cwd)] : step.args;
|
|
453
|
+
const cmd = `${cli} ${args.join(' ')}`;
|
|
454
|
+
try {
|
|
455
|
+
await execAsync(cmd, { cwd, timeout: 5 * 60 * 1000, maxBuffer: 10 * 1024 * 1024 });
|
|
456
|
+
} catch (e) {
|
|
457
|
+
if (!silent) fmt.logWarn(`${cmd} failed: ${e.message}`);
|
|
458
|
+
// Non-fatal — user can re-run later.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
467
|
+
// PLUGIN INSTALLER (claude plugin install)
|
|
468
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
async function runClaudePlugin(installCmd, { silent = false, marketplaceSource = null } = {}) {
|
|
471
|
+
const spinner = silent ? null : fmt.spinner();
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
if (!silent) spinner.start(`Installing ${installCmd}...`);
|
|
475
|
+
|
|
476
|
+
// Check if claude CLI is available (cross-platform)
|
|
477
|
+
try {
|
|
478
|
+
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { stdio: 'ignore' });
|
|
479
|
+
} catch {
|
|
480
|
+
if (!silent) {
|
|
481
|
+
spinner.stop(chalk.yellow('Claude CLI not found'));
|
|
482
|
+
}
|
|
483
|
+
throw new Error('claude: command not found (install Claude Code first)');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Some plugins require a marketplace add step before install
|
|
487
|
+
if (marketplaceSource) {
|
|
488
|
+
await execAsync(`claude plugin marketplace add ${marketplaceSource}`, {
|
|
489
|
+
timeout: 60000,
|
|
490
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Run the install
|
|
495
|
+
await execAsync(`claude plugin install ${installCmd}`, {
|
|
496
|
+
timeout: 120000,
|
|
497
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!silent) spinner.stop('✓ Installed');
|
|
501
|
+
return true;
|
|
502
|
+
} catch (e) {
|
|
503
|
+
if (!silent) spinner.stop(chalk.yellow('Installation failed'));
|
|
504
|
+
throw e;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
509
|
+
// UNIVERSAL INSTALLER (OS-detected master script — covers all IDEs in one shot)
|
|
510
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
async function runUniversalInstaller(integration, key, { silent = false } = {}) {
|
|
513
|
+
const spinner = silent ? null : fmt.spinner();
|
|
514
|
+
|
|
515
|
+
if (!silent) spinner.start(`Installing ${integration.label} for all detected IDEs...`);
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
let cmd;
|
|
519
|
+
|
|
520
|
+
// If no scripts defined at all, skip binary install and go straight to postInstall
|
|
521
|
+
const hasScript = integration.scripts?.win32 || integration.scripts?.posix;
|
|
522
|
+
|
|
523
|
+
if (!hasScript && IS_WINDOWS && integration.npmPackage) {
|
|
524
|
+
// npm-only tool on Windows (e.g. Skills)
|
|
525
|
+
if (!silent) fmt.logWarn(`No binary installer — using npm for ${integration.label}`);
|
|
526
|
+
cmd = `npm install -g ${integration.npmPackage}`;
|
|
527
|
+
} else if (!hasScript) {
|
|
528
|
+
// No binary, no npm — skip straight to postInstall (e.g. npx-only tools)
|
|
529
|
+
cmd = null;
|
|
530
|
+
} else if (IS_WINDOWS) {
|
|
531
|
+
if (integration.scripts.win32) {
|
|
532
|
+
cmd = `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "irm '${integration.scripts.win32}' | iex"`;
|
|
533
|
+
} else {
|
|
534
|
+
// No Windows script — try WSL first, then npm fallback
|
|
535
|
+
let wslOk = false;
|
|
536
|
+
try {
|
|
537
|
+
execSync('wsl --status', { stdio: 'ignore' });
|
|
538
|
+
// Verify bash is actually available inside WSL
|
|
539
|
+
execSync('wsl -- bash --version', { stdio: 'ignore' });
|
|
540
|
+
wslOk = true;
|
|
541
|
+
} catch { /* WSL absent or bash not installed */ }
|
|
542
|
+
|
|
543
|
+
if (wslOk) {
|
|
544
|
+
cmd = `wsl -- bash -c "curl -fsSL '${integration.scripts.posix}' | sh"`;
|
|
545
|
+
} else if (integration.npmPackage) {
|
|
546
|
+
if (!silent) fmt.logWarn('WSL/bash not available — installing via npm instead');
|
|
547
|
+
cmd = `npm install -g ${integration.npmPackage}`;
|
|
548
|
+
} else {
|
|
549
|
+
if (!silent) {
|
|
550
|
+
spinner.stop(chalk.yellow('WSL not found'));
|
|
551
|
+
fmt.logWarn(
|
|
552
|
+
`${integration.label} requires WSL on Windows for full support.\n` +
|
|
553
|
+
` Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install`,
|
|
554
|
+
'Skipped',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
recordSkipped(key, 'universal-installer', 'wsl-not-found');
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
cmd = `curl -fsSL '${integration.scripts.posix}' | bash`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (cmd) {
|
|
566
|
+
await execAsync(cmd, {
|
|
567
|
+
timeout: 5 * 60 * 1000,
|
|
568
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Run post-install commands (e.g. rtk init -g to wire the agent hook)
|
|
573
|
+
for (const postCmd of integration.postInstall || []) {
|
|
574
|
+
let finalPostCmd = postCmd;
|
|
575
|
+
// If on Windows and we used WSL for the main install, we must use it for the post command too
|
|
576
|
+
if (IS_WINDOWS && cmd && cmd.startsWith('wsl')) {
|
|
577
|
+
finalPostCmd = `wsl -- ${postCmd}`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
await execAsync(finalPostCmd, { timeout: 60 * 1000, maxBuffer: 10 * 1024 * 1024 });
|
|
582
|
+
} catch (e) {
|
|
583
|
+
if (!silent) fmt.logWarn(`Post-install step failed: ${finalPostCmd} — ${e.message}`);
|
|
584
|
+
// Non-fatal — user can re-run later
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!silent) spinner.stop('✓ Installed');
|
|
589
|
+
return true;
|
|
590
|
+
} catch (e) {
|
|
591
|
+
if (!silent) spinner.stop(chalk.yellow('Installation failed'));
|
|
592
|
+
throw e;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
597
|
+
// INTEGRATION INSTALLER
|
|
598
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
export async function installIntegration(key, { silent = false } = {}) {
|
|
601
|
+
const integration = INTEGRATIONS[key];
|
|
602
|
+
if (!integration) {
|
|
603
|
+
throw new Error(`Unknown integration: ${key}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (isInstalled(key)) {
|
|
607
|
+
if (!silent) fmt.logWarn(`${integration.label} is already installed`);
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
if (integration.type === 'plugin') {
|
|
613
|
+
// PLUGIN: Run claude plugin install
|
|
614
|
+
await runClaudePlugin(integration.installCmd, { silent, marketplaceSource: integration.marketplaceSource ?? null });
|
|
615
|
+
recordInstalled(key, 'plugin');
|
|
616
|
+
|
|
617
|
+
if (!silent) {
|
|
618
|
+
fmt.logSuccess(`${integration.label} plugin installed`);
|
|
619
|
+
if (integration.authNote) {
|
|
620
|
+
fmt.note(integration.authNote, 'Next Step');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} else if (integration.type === 'remote-mcp') {
|
|
624
|
+
// REMOTE MCP: Add to mcp.json files
|
|
625
|
+
if (!silent) fmt.logStep(`Adding ${integration.label} MCP server...`);
|
|
626
|
+
|
|
627
|
+
const config = { type: 'http', url: integration.mcpUrl };
|
|
628
|
+
|
|
629
|
+
// Claude Code: ~/.claude.json
|
|
630
|
+
addToJsonMcp(
|
|
631
|
+
join(HOME, '.claude.json'),
|
|
632
|
+
key,
|
|
633
|
+
config
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Cursor: ~/.cursor/mcp.json
|
|
637
|
+
addToJsonMcp(
|
|
638
|
+
join(HOME, '.cursor', 'mcp.json'),
|
|
639
|
+
key,
|
|
640
|
+
config
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// Codex: ~/.codex/config.toml
|
|
644
|
+
addToTomlMcp(
|
|
645
|
+
join(HOME, '.codex', 'config.toml'),
|
|
646
|
+
key,
|
|
647
|
+
integration.mcpUrl
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
recordInstalled(key, 'remote-mcp');
|
|
651
|
+
|
|
652
|
+
if (!silent) {
|
|
653
|
+
fmt.logSuccess(`${integration.label} added to MCP servers`);
|
|
654
|
+
if (integration.authNote) {
|
|
655
|
+
fmt.note(integration.authNote, 'Note');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} else if (integration.type === 'python-cli') {
|
|
659
|
+
// PYTHON CLI: pip-install + run post-install + per-project hooks
|
|
660
|
+
const ok = await runPythonCli(integration, key, { silent });
|
|
661
|
+
if (!ok) return false; // skipped (e.g. Python missing) — manifest already recorded
|
|
662
|
+
|
|
663
|
+
recordInstalled(key, 'python-cli');
|
|
664
|
+
|
|
665
|
+
if (!silent) {
|
|
666
|
+
fmt.logSuccess(`${integration.label} installed`);
|
|
667
|
+
if (integration.authNote) {
|
|
668
|
+
fmt.note(integration.authNote, 'Note');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} else if (integration.type === 'universal-installer') {
|
|
672
|
+
// UNIVERSAL INSTALLER: OS-detected master script, covers all IDEs in one shot
|
|
673
|
+
const ok = await runUniversalInstaller(integration, key, { silent });
|
|
674
|
+
if (!ok) return false; // skipped (e.g. WSL not found) — manifest already recorded
|
|
675
|
+
recordInstalled(key, 'universal-installer');
|
|
676
|
+
|
|
677
|
+
if (!silent) {
|
|
678
|
+
fmt.logSuccess(`${integration.label} installed across all detected IDEs`);
|
|
679
|
+
if (integration.authNote) {
|
|
680
|
+
fmt.note(integration.authNote, 'Note');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return true;
|
|
686
|
+
} catch (e) {
|
|
687
|
+
if (!silent) {
|
|
688
|
+
fmt.logError(`Failed to install ${integration.label}: ${e.message}`);
|
|
689
|
+
}
|
|
690
|
+
// Don't record as installed if it failed
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
696
|
+
// INTEGRATION REMOVER
|
|
697
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
698
|
+
|
|
699
|
+
export async function removeIntegration(key, { silent = false } = {}) {
|
|
700
|
+
const integration = INTEGRATIONS[key];
|
|
701
|
+
if (!integration) {
|
|
702
|
+
throw new Error(`Unknown integration: ${key}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (!isInstalled(key)) {
|
|
706
|
+
if (!silent) fmt.logWarn(`${integration.label} is not installed`);
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
if (integration.type === 'plugin') {
|
|
712
|
+
// PLUGIN: Manual removal instruction
|
|
713
|
+
if (!silent) {
|
|
714
|
+
fmt.logWarn(
|
|
715
|
+
`To remove the ${integration.label} plugin, run: /plugin in Claude Code and disable it`,
|
|
716
|
+
'Manual Removal'
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
} else if (integration.type === 'remote-mcp') {
|
|
720
|
+
// REMOTE MCP: Remove from mcp.json files
|
|
721
|
+
if (!silent) fmt.logStep(`Removing ${integration.label} MCP server...`);
|
|
722
|
+
|
|
723
|
+
// Claude Code
|
|
724
|
+
try {
|
|
725
|
+
const claudePath = join(HOME, '.claude.json');
|
|
726
|
+
if (existsSync(claudePath)) {
|
|
727
|
+
const config = JSON.parse(readFileSync(claudePath, 'utf8'));
|
|
728
|
+
delete config.mcpServers?.[key];
|
|
729
|
+
writeFileSync(claudePath, JSON.stringify(config, null, 2) + '\n');
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
// Best effort
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Cursor
|
|
736
|
+
try {
|
|
737
|
+
const cursorPath = join(HOME, '.cursor', 'mcp.json');
|
|
738
|
+
if (existsSync(cursorPath)) {
|
|
739
|
+
const config = JSON.parse(readFileSync(cursorPath, 'utf8'));
|
|
740
|
+
delete config.mcpServers?.[key];
|
|
741
|
+
writeFileSync(cursorPath, JSON.stringify(config, null, 2) + '\n');
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Best effort
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Codex
|
|
748
|
+
try {
|
|
749
|
+
const codexPath = join(HOME, '.codex', 'config.toml');
|
|
750
|
+
if (existsSync(codexPath)) {
|
|
751
|
+
let content = readFileSync(codexPath, 'utf8');
|
|
752
|
+
const blockRegex = new RegExp(
|
|
753
|
+
`\\[mcp_servers\\.${key}\\][^\\[]*`,
|
|
754
|
+
'g'
|
|
755
|
+
);
|
|
756
|
+
content = content.replace(blockRegex, '');
|
|
757
|
+
writeFileSync(codexPath, content);
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
// Best effort
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!silent) fmt.logSuccess(`${integration.label} removed from MCP servers`);
|
|
764
|
+
} else if (integration.type === 'python-cli') {
|
|
765
|
+
// PYTHON CLI: only remove the manifest entry — leave the pip package installed
|
|
766
|
+
// because the user may use the CLI outside of `aw`. Print manual cleanup hints.
|
|
767
|
+
if (!silent) {
|
|
768
|
+
fmt.logWarn(
|
|
769
|
+
`Removed ${integration.label} from the aw manifest. The Python package was left installed.\n` +
|
|
770
|
+
` To fully remove, run in each project: \`${integration.cliCommand} claude uninstall\` and \`${integration.cliCommand} hook uninstall\`\n` +
|
|
771
|
+
` Then uninstall the package: \`pip uninstall ${integration.pipPackage.split(/[<>=]/)[0]}\` (or \`uv tool uninstall\` / \`pipx uninstall\`)`,
|
|
772
|
+
'Manual Cleanup',
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
} else if (integration.type === 'universal-installer') {
|
|
776
|
+
if (!silent) {
|
|
777
|
+
fmt.logWarn(
|
|
778
|
+
`Removed ${integration.label} from the aw manifest.\n` +
|
|
779
|
+
` To fully uninstall: follow the uninstall instructions for ${integration.label} in its documentation.`,
|
|
780
|
+
'Manual Cleanup',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Remove from manifest
|
|
786
|
+
const manifest = readManifest();
|
|
787
|
+
delete manifest.installed[key];
|
|
788
|
+
writeManifest(manifest);
|
|
789
|
+
|
|
790
|
+
return true;
|
|
791
|
+
} catch (e) {
|
|
792
|
+
if (!silent) {
|
|
793
|
+
fmt.logError(`Failed to remove ${integration.label}: ${e.message}`);
|
|
794
|
+
}
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
800
|
+
// HELPERS
|
|
801
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
export function getInstalledList() {
|
|
804
|
+
const manifest = readManifest();
|
|
805
|
+
// Only return entries actually installed — skipped entries (e.g. Python missing)
|
|
806
|
+
// shouldn't show up as "installed" to the rest of the system.
|
|
807
|
+
return Object.entries(manifest.installed)
|
|
808
|
+
.filter(([, entry]) => entry.status !== 'skipped')
|
|
809
|
+
.map(([key]) => key);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function suggestForTeam(namespace) {
|
|
813
|
+
if (!namespace) return [];
|
|
814
|
+
|
|
815
|
+
const team = namespace.split('/')[0]; // e.g. 'platform' from 'platform/crm'
|
|
816
|
+
|
|
817
|
+
return Object.entries(INTEGRATIONS)
|
|
818
|
+
.filter(([, integration]) => {
|
|
819
|
+
// Show if: team is in the integration's teams list OR teams list is empty (universal)
|
|
820
|
+
return (
|
|
821
|
+
integration.teams.length === 0 || integration.teams.includes(team)
|
|
822
|
+
);
|
|
823
|
+
})
|
|
824
|
+
.map(([key]) => key);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
828
|
+
// AUTO-INSTALL (called from init.mjs - installs suggested integrations)
|
|
829
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
export async function autoInstallIntegrations(team, { silent = false, installer = installIntegration } = {}) {
|
|
832
|
+
// Get suggested integrations for this team
|
|
833
|
+
const suggested = suggestForTeam(team);
|
|
834
|
+
if (!suggested || suggested.length === 0) {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Only install if they're not already installed
|
|
839
|
+
const toInstall = suggested.filter((key) => !isInstalled(key));
|
|
840
|
+
if (toInstall.length === 0) {
|
|
841
|
+
return [];
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!silent) {
|
|
845
|
+
fmt.logStep(`Setting up integrations for ${team}...`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const installed = [];
|
|
849
|
+
for (const key of toInstall) {
|
|
850
|
+
const success = await installer(key, { silent });
|
|
851
|
+
if (success) {
|
|
852
|
+
installed.push(INTEGRATIONS[key].label);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return installed;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
860
|
+
// INTERACTIVE SETUP (can be called via: aw integrations)
|
|
861
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
export async function promptAndInstall(team, { silent = false } = {}) {
|
|
864
|
+
// Skip if: silent mode, no TTY, or all integrations already installed
|
|
865
|
+
if (silent || !process.stdin.isTTY) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const suggested = suggestForTeam(team);
|
|
870
|
+
if (!suggested || suggested.length === 0) {
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Check if any suggested integrations are NOT installed
|
|
875
|
+
const availableToInstall = suggested.filter((key) => !isInstalled(key));
|
|
876
|
+
if (availableToInstall.length === 0) {
|
|
877
|
+
// All already installed
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Prompt user
|
|
882
|
+
const p = await import('@clack/prompts');
|
|
883
|
+
const shouldSetup = await p.default.confirm({
|
|
884
|
+
message: `Your team (${team}) can use integrations like Codex, Caveman, etc. Set up any now?`,
|
|
885
|
+
initialValue: false,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (p.default.isCancel(shouldSetup) || !shouldSetup) {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Show bundles + individual tools
|
|
893
|
+
const bundleOptions = Object.entries(BUNDLES)
|
|
894
|
+
.filter(([, bundle]) => {
|
|
895
|
+
return (
|
|
896
|
+
bundle.teams.length === 0 || bundle.teams.includes(team.split('/')[0])
|
|
897
|
+
);
|
|
898
|
+
})
|
|
899
|
+
.map(([bundleKey, bundle]) => ({
|
|
900
|
+
value: `bundle:${bundleKey}`,
|
|
901
|
+
label: `📦 ${bundle.label}`,
|
|
902
|
+
description: bundle.description,
|
|
903
|
+
}));
|
|
904
|
+
|
|
905
|
+
const individualOptions = availableToInstall.map((key) => {
|
|
906
|
+
const integration = INTEGRATIONS[key];
|
|
907
|
+
const icon =
|
|
908
|
+
integration.type === 'plugin' ? '🔌'
|
|
909
|
+
: integration.type === 'remote-mcp' ? '🌐'
|
|
910
|
+
: integration.type === 'python-cli' ? '🐍'
|
|
911
|
+
: '⚙️';
|
|
912
|
+
return {
|
|
913
|
+
value: `integration:${key}`,
|
|
914
|
+
label: `${icon} ${integration.label}`,
|
|
915
|
+
description: integration.description,
|
|
916
|
+
};
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const choices = [...bundleOptions, ...individualOptions];
|
|
920
|
+
|
|
921
|
+
const selected = await p.default.multiselect({
|
|
922
|
+
message: 'Select integrations to install:',
|
|
923
|
+
options: choices,
|
|
924
|
+
required: false,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
if (p.default.isCancel(selected)) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Process selections
|
|
932
|
+
const toInstall = new Set();
|
|
933
|
+
for (const selection of selected) {
|
|
934
|
+
if (selection.startsWith('bundle:')) {
|
|
935
|
+
const bundleKey = selection.replace('bundle:', '');
|
|
936
|
+
const bundle = BUNDLES[bundleKey];
|
|
937
|
+
bundle.includes.forEach((key) => toInstall.add(key));
|
|
938
|
+
} else if (selection.startsWith('integration:')) {
|
|
939
|
+
const integrationKey = selection.replace('integration:', '');
|
|
940
|
+
toInstall.add(integrationKey);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Install all
|
|
945
|
+
const installed = [];
|
|
946
|
+
for (const key of toInstall) {
|
|
947
|
+
const success = await installIntegration(key, { silent: false });
|
|
948
|
+
if (success) {
|
|
949
|
+
installed.push(INTEGRATIONS[key].label);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return installed;
|
|
954
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"hooks/",
|
|
35
35
|
"startup.mjs",
|
|
36
36
|
"ecc.mjs",
|
|
37
|
+
"integrations.mjs",
|
|
37
38
|
"render-rules.mjs",
|
|
38
39
|
"telemetry.mjs"
|
|
39
40
|
],
|
|
@@ -51,9 +52,9 @@
|
|
|
51
52
|
"license": "MIT",
|
|
52
53
|
"scripts": {
|
|
53
54
|
"test": "yarn test:vitest && yarn test:node",
|
|
54
|
-
"test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4",
|
|
55
|
+
"test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
|
|
55
56
|
"test:node": "node tests/run-node-tests.mjs",
|
|
56
|
-
"test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4",
|
|
57
|
+
"test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
|
|
57
58
|
"preuninstall": "node bin.js nuke 2>/dev/null || true"
|
|
58
59
|
},
|
|
59
60
|
"publishConfig": {
|