@ghl-ai/aw 0.1.58-beta.0 → 0.1.58

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 CHANGED
@@ -168,13 +168,10 @@ function printHelp() {
168
168
  cmd('aw mcp status', 'Show global AW MCP mode for Claude/Cursor/Codex'),
169
169
  cmd('aw mcp disable', 'Remove AW-managed MCP server across Claude/Cursor/Codex'),
170
170
  cmd('aw mcp enable', 'Restore AW-managed MCP server across Claude/Cursor/Codex'),
171
- cmd('aw integrations', 'Manage third-party integrations (Codex, Caveman, etc)'),
172
- cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman)'),
171
+ cmd('aw integrations', 'Manage third-party integrations (Codex, Caveman, Context Mode, etc)'),
172
+ cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman, context-mode)'),
173
173
  cmd('aw integrations remove <key>', 'Remove a tool'),
174
174
  cmd('aw integrations bundle <name>', 'Install a preset bundle'),
175
- cmd('aw integration add <name>', 'Configure an installed local integration, e.g. context-mode'),
176
- cmd('aw integration status <name>', 'Show local integration health'),
177
- cmd('aw integration remove <name>', 'Remove AW-managed local integration entries'),
178
175
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
179
176
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
180
177
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
package/commands/init.mjs CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  import { execSync } from 'node:child_process';
19
19
  import { join, dirname, sep } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
- import { fileURLToPath } from 'node:url';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
  import * as p from '@clack/prompts';
23
23
  import * as config from '../config.mjs';
24
24
  import * as fmt from '../fmt.mjs';
@@ -26,7 +26,7 @@ import { chalk, setSilent } from '../fmt.mjs';
26
26
  import { linkWorkspace } from '../link.mjs';
27
27
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
28
28
  import { setupMcp } from '../mcp.mjs';
29
- import { ensureContextModeIntegration, isContextModeRequested } from '../integrations/context-mode.mjs';
29
+ import { isContextModeRequested } from '../integrations/context-mode.mjs';
30
30
  import { applyStoredStartupPreferences, ensureAwRuntimeHook, isDefaultRoutingEnabled } from '../startup.mjs';
31
31
  import { installLocalCommitHook } from '../hooks.mjs';
32
32
  import { autoUpdate, promptUpdate } from '../update.mjs';
@@ -35,7 +35,7 @@ import { loadConfig as ensureTelemetryConfig } from '../telemetry.mjs';
35
35
  import { installAwEcc, AW_ECC_TAG } from '../ecc.mjs';
36
36
  import { removeWorkspaceHookDefaults } from '../codex.mjs';
37
37
  import { readHookManifest, pruneStaleHooks, writeHookManifest } from '../hook-cleanup.mjs';
38
- import { promptAndInstall, autoInstallIntegrations } from '../integrations.mjs';
38
+ import { installIntegration, autoInstallIntegrations } from '../integrations.mjs';
39
39
  import {
40
40
  initPersistentClone,
41
41
  isValidClone,
@@ -65,6 +65,41 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
65
65
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
66
66
  const AW_HOME = join(HOME, '.aw');
67
67
 
68
+ function errorMessage(error) {
69
+ return error instanceof Error ? error.message : String(error);
70
+ }
71
+
72
+ function writeHookManifestBestEffort(manifest, context) {
73
+ try {
74
+ writeHookManifest(manifest);
75
+ } catch (error) {
76
+ fmt.logWarn(`Could not persist hook manifest${context ? ` ${context}` : ''}: ${errorMessage(error)}`);
77
+ }
78
+ }
79
+
80
+ export async function syncEccAfterAwUpdate(updateResult, cwd, { silent = false } = {}) {
81
+ if (updateResult?.status !== 'upgraded' || !updateResult.packageRoot) {
82
+ return { synced: false, reason: updateResult?.reason || updateResult?.status || 'not-upgraded' };
83
+ }
84
+
85
+ const eccModulePath = join(updateResult.packageRoot, 'ecc.mjs');
86
+ if (!existsSync(eccModulePath)) {
87
+ return { synced: false, reason: 'missing-ecc-module' };
88
+ }
89
+
90
+ try {
91
+ const moduleUrl = `${pathToFileURL(eccModulePath).href}?aw-post-update=${Date.now()}`;
92
+ const ecc = await import(moduleUrl);
93
+ await ecc.installAwEcc(cwd, { silent });
94
+ return { synced: true, eccVersion: ecc.AW_ECC_TAG || null };
95
+ } catch (err) {
96
+ if (!silent) {
97
+ fmt.logWarn(`AW updated, but post-update ECC sync failed: ${err.message}`);
98
+ }
99
+ return { synced: false, reason: err.message };
100
+ }
101
+ }
102
+
68
103
  function syncRulesTargets(targetDir) {
69
104
  const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
70
105
  if (!existsSync(rulesSrc)) return false;
@@ -94,23 +129,24 @@ function syncHomeAndProjectInstructions(cwd, namespace) {
94
129
  }
95
130
  }
96
131
 
97
- function maybeConfigureContextMode(args, { silent = false } = {}) {
132
+ async function maybeConfigureContextMode(args, { silent = false } = {}) {
98
133
  if (!isContextModeRequested(args, process.env)) {
99
134
  return null;
100
135
  }
101
136
 
102
- const result = ensureContextModeIntegration(HOME, {
137
+ const result = await installIntegration('context-mode', {
138
+ home: HOME,
103
139
  env: process.env,
104
140
  silent,
105
141
  });
106
142
 
107
143
  if (!silent) {
108
- for (const warning of result.warnings) {
144
+ for (const warning of result.warnings || []) {
109
145
  fmt.logWarn(warning);
110
146
  }
111
- if (result.changedFiles.length > 0) {
147
+ if ((result.changedFiles || []).length > 0) {
112
148
  fmt.logStep(`Context Mode configured (${result.changedFiles.length} file${result.changedFiles.length === 1 ? '' : 's'})`);
113
- } else if (result.warnings.length > 0) {
149
+ } else if ((result.warnings || []).length > 0) {
114
150
  fmt.logWarn('Context Mode was requested but not configured');
115
151
  } else {
116
152
  fmt.logStep('Context Mode already configured');
@@ -404,7 +440,7 @@ export async function initCommand(args) {
404
440
  ensureAwRuntimeHook(HOME);
405
441
  syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
406
442
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
407
- maybeConfigureContextMode(args, { silent });
443
+ await maybeConfigureContextMode(args, { silent });
408
444
  applyStoredStartupPreferences(HOME);
409
445
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
410
446
  installGlobalHooks();
@@ -444,7 +480,7 @@ export async function initCommand(args) {
444
480
  if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
445
481
 
446
482
  // Write hook manifest after all hook installation is complete
447
- try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
483
+ writeHookManifestBestEffort({ eccVersion: AW_ECC_TAG, awVersion: VERSION });
448
484
 
449
485
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
450
486
  let installedIntegrations = [];
@@ -454,7 +490,14 @@ export async function initCommand(args) {
454
490
 
455
491
  if (silent) {
456
492
  if (silentSpinner) { silentSpinner.stop('Done'); setSilent(false); }
457
- autoUpdate(await args._updateCheck);
493
+ const updateResult = autoUpdate(await args._updateCheck);
494
+ const postUpdateEcc = await syncEccAfterAwUpdate(updateResult, cwd, { silent: true });
495
+ if (postUpdateEcc.synced) {
496
+ writeHookManifestBestEffort(
497
+ { eccVersion: postUpdateEcc.eccVersion, awVersion: updateResult.to },
498
+ 'after AW update',
499
+ );
500
+ }
458
501
  } else {
459
502
  fmt.outro([
460
503
  `⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
@@ -565,7 +608,7 @@ export async function initCommand(args) {
565
608
  Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
566
609
  setupMcp(HOME, team, { silent }),
567
610
  ]);
568
- maybeConfigureContextMode(args, { silent });
611
+ await maybeConfigureContextMode(args, { silent });
569
612
  // applyStoredStartupPreferences reads settings written by ECC — keep after batch B
570
613
  applyStoredStartupPreferences(HOME);
571
614
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
@@ -630,7 +673,7 @@ export async function initCommand(args) {
630
673
 
631
674
  // Write hook manifest after all hook installation is complete, including
632
675
  // bundled usage hooks, so `aw nuke` can prune AW-managed settings entries.
633
- try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
676
+ writeHookManifestBestEffort({ eccVersion: AW_ECC_TAG, awVersion: VERSION });
634
677
 
635
678
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
636
679
  let installedIntegrations = [];
@@ -1,43 +1,52 @@
1
+ // commands/integration.mjs — Deprecated compatibility wrapper for `aw integration`.
2
+
1
3
  import { homedir } from 'node:os';
2
4
 
3
5
  import * as fmt from '../fmt.mjs';
4
- import { listIntegrations, resolveIntegration } from '../integrations/index.mjs';
6
+ import { getIntegrationSummary, INTEGRATIONS } from '../integrations.mjs';
7
+ import { integrationsCommand } from './integrations.mjs';
5
8
 
6
- function printWarnings(warnings = []) {
7
- for (const warning of warnings) {
8
- fmt.logWarn(warning);
9
+ function requireKnownIntegration(key) {
10
+ if (!key) {
11
+ fmt.cancel(`Missing integration name. Known integrations: ${Object.keys(INTEGRATIONS).join(', ')}`);
12
+ }
13
+ if (!INTEGRATIONS[key]) {
14
+ fmt.cancel(`Unknown integration: ${key}`);
9
15
  }
10
16
  }
11
17
 
12
- function printChangedFiles(changedFiles = []) {
13
- if (changedFiles.length === 0) return;
14
- fmt.note(changedFiles.map(file => file.replace(homedir(), '~')).join('\n'), 'Changed files');
15
- }
16
-
17
- function requireIntegration(name) {
18
- if (!name) {
19
- fmt.cancel('Missing integration name. Known integrations: context-mode');
18
+ function printStatus(key, home) {
19
+ const status = getIntegrationSummary(key, { home, env: process.env });
20
+ if (!status) {
21
+ fmt.cancel(`Unknown integration: ${key}`);
20
22
  }
21
23
 
22
- try {
23
- return resolveIntegration(name);
24
- } catch (err) {
25
- fmt.cancel(err.message);
26
- }
24
+ fmt.note(
25
+ [
26
+ `state: ${status.state}`,
27
+ `installed: ${status.installed}`,
28
+ `configured: ${status.configured}`,
29
+ status.version ? `version: ${status.version}` : null,
30
+ status.path ? `path: ${status.path}` : null,
31
+ `summary: ${status.summary}`,
32
+ ].filter(Boolean).join('\n'),
33
+ key,
34
+ );
27
35
  }
28
36
 
29
37
  export async function integrationCommand(args) {
30
- const [subcommand = 'list', name] = args._positional;
38
+ const [subcommand = 'list', key] = args._positional;
31
39
  const home = homedir();
32
40
 
33
41
  if (args['--help']) {
34
42
  fmt.intro('aw integration');
43
+ fmt.logWarn('`aw integration` is deprecated. Use `aw integrations` for new automation.');
35
44
  fmt.note(
36
45
  [
37
46
  'aw integration list',
38
- 'aw integration add context-mode [--dry-run]',
39
- 'aw integration status context-mode',
40
- 'aw integration remove context-mode [--dry-run]',
47
+ 'aw integration add <key> [--dry-run] [--strict]',
48
+ 'aw integration status <key>',
49
+ 'aw integration remove <key> [--dry-run]',
41
50
  ].join('\n'),
42
51
  'Usage',
43
52
  );
@@ -45,67 +54,19 @@ export async function integrationCommand(args) {
45
54
  return;
46
55
  }
47
56
 
48
- if (subcommand === 'list') {
49
- fmt.intro('aw integration list');
50
- const rows = listIntegrations(home, { env: process.env });
51
- fmt.note(
52
- rows.map(row => `${row.name} ${row.state} ${row.summary}`).join('\n'),
53
- 'Known integrations',
54
- );
55
- fmt.outro('Done');
56
- return;
57
- }
58
-
59
- const integration = requireIntegration(name);
60
-
61
- if (subcommand === 'add') {
62
- fmt.intro(`aw integration add ${integration.name}`);
63
- const result = integration.add(home, {
64
- env: process.env,
65
- dryRun: args['--dry-run'] === true,
66
- });
67
- printWarnings(result.warnings);
68
- printChangedFiles(result.changedFiles);
69
- if (result.changedFiles.length > 0) {
70
- fmt.outro(`${integration.name} configured`);
71
- } else if (result.warnings.length > 0) {
72
- fmt.outro(`${integration.name} not configured; binary is missing or config was unsafe to edit`);
73
- } else {
74
- fmt.outro(`${integration.name} already configured`);
75
- }
76
- return;
77
- }
78
-
79
- if (subcommand === 'remove') {
80
- fmt.intro(`aw integration remove ${integration.name}`);
81
- const result = integration.remove(home, {
82
- dryRun: args['--dry-run'] === true,
83
- });
84
- printWarnings(result.warnings);
85
- printChangedFiles(result.changedFiles);
86
- fmt.outro(result.changedFiles.length > 0
87
- ? `${integration.name} removed`
88
- : `${integration.name} not configured`);
89
- return;
90
- }
57
+ fmt.logWarn('`aw integration` is deprecated. Use `aw integrations` instead.');
91
58
 
92
59
  if (subcommand === 'status') {
93
- fmt.intro(`aw integration status ${integration.name}`);
94
- const status = integration.status(home, { env: process.env });
95
- fmt.note(
96
- [
97
- `state: ${status.state}`,
98
- `installed: ${status.installed}`,
99
- `configured: ${status.configured}`,
100
- status.version ? `version: ${status.version}` : null,
101
- status.path ? `path: ${status.path}` : null,
102
- `summary: ${status.summary}`,
103
- ].filter(Boolean).join('\n'),
104
- integration.name,
105
- );
60
+ requireKnownIntegration(key);
61
+ fmt.intro(`aw integration status ${key}`);
62
+ printStatus(key, home);
106
63
  fmt.outro('Done');
107
64
  return;
108
65
  }
109
66
 
67
+ if (['list', 'add', 'remove', 'bundle'].includes(subcommand)) {
68
+ return integrationsCommand(args);
69
+ }
70
+
110
71
  fmt.cancel(`Unknown integration subcommand: ${subcommand}. Use add / remove / status / list`);
111
72
  }
@@ -1,16 +1,53 @@
1
1
  // commands/integrations.mjs — CLI command: aw integrations add/remove/list/bundle
2
2
 
3
+ import { homedir } from 'node:os';
4
+
3
5
  import * as p from '@clack/prompts';
4
6
  import * as fmt from '../fmt.mjs';
5
7
  import { chalk } from '../fmt.mjs';
6
8
  import {
7
9
  INTEGRATIONS,
8
10
  BUNDLES,
11
+ getIntegrationSummaries,
9
12
  installIntegration,
13
+ integrationSucceeded,
10
14
  removeIntegration,
11
- getInstalledList,
12
15
  } from '../integrations.mjs';
13
16
 
17
+ function availableKeys() {
18
+ return Object.keys(INTEGRATIONS).join(', ');
19
+ }
20
+
21
+ function printWarnings(warnings = []) {
22
+ for (const warning of warnings) fmt.logWarn(warning);
23
+ }
24
+
25
+ function printChangedFiles(changedFiles = []) {
26
+ if (!changedFiles.length) return;
27
+ fmt.note(changedFiles.map(file => file.replace(homedir(), '~')).join('\n'), 'Changed files');
28
+ }
29
+
30
+ function printContextModeAddResult(result) {
31
+ printWarnings(result.warnings);
32
+ if (result.plannedInstall) {
33
+ fmt.note(result.install?.commandLine || 'install context-mode@latest', 'Planned package install');
34
+ }
35
+ if (result.binary?.present) {
36
+ fmt.note(
37
+ [
38
+ `binary: ${result.binary.path}`,
39
+ `source: ${result.binary.source}`,
40
+ result.installedPackage ? 'package: installed during this run' : 'package: reused existing binary',
41
+ ].filter(Boolean).join('\n'),
42
+ 'Context Mode',
43
+ );
44
+ }
45
+ printChangedFiles(result.changedFiles);
46
+ if (result.configuredHarnesses?.length) {
47
+ fmt.note(result.configuredHarnesses.join('\n'), 'Configured surfaces');
48
+ }
49
+ }
50
+
14
51
  export async function integrationsCommand(args) {
15
52
  const subcommand = args._positional[0];
16
53
 
@@ -39,92 +76,16 @@ async function cmdList() {
39
76
  subtitle: ' Available tools and MCP servers',
40
77
  });
41
78
 
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'
79
+ const rows = getIntegrationSummaries({ home: homedir(), env: process.env });
80
+ fmt.note(
81
+ rows.map(row => `${row.name.padEnd(14)} ${row.state.padEnd(11)} ${row.summary}`).join('\n'),
82
+ 'Available integrations',
56
83
  );
57
84
 
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
85
  fmt.logMessage(`\n${chalk.dim('Commands:')}`);
127
86
  fmt.logMessage(` aw integrations add <key> Install a specific tool`);
87
+ fmt.logMessage(` aw integrations add <key> --dry-run`);
88
+ fmt.logMessage(` aw integrations add context-mode --strict`);
128
89
  fmt.logMessage(` aw integrations remove <key> Remove a tool`);
129
90
  fmt.logMessage(` aw integrations bundle <name> Install a preset bundle`);
130
91
  }
@@ -142,12 +103,11 @@ async function cmdAdd(args) {
142
103
 
143
104
  if (!INTEGRATIONS[key]) {
144
105
  // Suggest similar keys
145
- const available = Object.keys(INTEGRATIONS);
146
106
  fmt.cancel(
147
107
  [
148
108
  `Unknown integration: ${chalk.red(key)}`,
149
109
  '',
150
- `Available: ${available.join(', ')}`,
110
+ `Available: ${availableKeys()}`,
151
111
  ].join('\n')
152
112
  );
153
113
  }
@@ -155,13 +115,33 @@ async function cmdAdd(args) {
155
115
  const integration = INTEGRATIONS[key];
156
116
  fmt.intro(`Installing ${integration.label}`);
157
117
 
158
- const success = await installIntegration(key, { silent: false });
118
+ const result = await installIntegration(key, {
119
+ home: homedir(),
120
+ env: process.env,
121
+ dryRun: args['--dry-run'] === true,
122
+ silent: false,
123
+ });
159
124
 
160
- if (success) {
161
- fmt.outro(`✓ ${integration.label} installed successfully`);
162
- } else {
163
- fmt.cancel(`Failed to install ${integration.label}`);
125
+ if (integration.type === 'context-mode') {
126
+ printContextModeAddResult(result);
127
+ if (!result.success) {
128
+ const message = result.error === 'partial-config'
129
+ ? `${integration.label} was only partially configured`
130
+ : `${integration.label} was not configured`;
131
+ if (args['--strict'] === true) {
132
+ fmt.cancel(message);
133
+ }
134
+ fmt.outro(message);
135
+ return;
136
+ }
137
+ fmt.outro(args['--dry-run'] === true
138
+ ? `${integration.label} install/config dry run complete`
139
+ : `${integration.label} configured`);
140
+ return;
164
141
  }
142
+
143
+ if (!integrationSucceeded(result)) fmt.cancel(`Failed to install ${integration.label}`);
144
+ fmt.outro(`✓ ${integration.label} installed successfully`);
165
145
  }
166
146
 
167
147
  // ────────────────────────────────────────────────────────────────────────────────
@@ -176,12 +156,11 @@ async function cmdRemove(args) {
176
156
  }
177
157
 
178
158
  if (!INTEGRATIONS[key]) {
179
- const available = Object.keys(INTEGRATIONS);
180
159
  fmt.cancel(
181
160
  [
182
161
  `Unknown integration: ${chalk.red(key)}`,
183
162
  '',
184
- `Available: ${available.join(', ')}`,
163
+ `Available: ${availableKeys()}`,
185
164
  ].join('\n')
186
165
  );
187
166
  }
@@ -189,13 +168,18 @@ async function cmdRemove(args) {
189
168
  const integration = INTEGRATIONS[key];
190
169
  fmt.intro(`Removing ${integration.label}`);
191
170
 
192
- const success = await removeIntegration(key, { silent: false });
171
+ const result = await removeIntegration(key, {
172
+ home: homedir(),
173
+ dryRun: args['--dry-run'] === true,
174
+ silent: false,
175
+ });
193
176
 
194
- if (success) {
195
- fmt.outro(`✓ ${integration.label} removed successfully`);
196
- } else {
197
- fmt.cancel(`Failed to remove ${integration.label}`);
198
- }
177
+ printWarnings(result.warnings);
178
+ printChangedFiles(result.changedFiles);
179
+ if (!integrationSucceeded(result)) fmt.cancel(`Failed to remove ${integration.label}`);
180
+ fmt.outro(args['--dry-run'] === true
181
+ ? `${integration.label} removal dry run complete`
182
+ : `✓ ${integration.label} removed successfully`);
199
183
  }
200
184
 
201
185
  // ────────────────────────────────────────────────────────────────────────────────
@@ -244,8 +228,8 @@ async function cmdBundle(args) {
244
228
  // Install all
245
229
  let successCount = 0;
246
230
  for (const key of bundle.includes) {
247
- const success = await installIntegration(key, { silent: false });
248
- if (success) successCount++;
231
+ const result = await installIntegration(key, { silent: false });
232
+ if (integrationSucceeded(result)) successCount++;
249
233
  }
250
234
 
251
235
  fmt.outro(
package/ecc.mjs CHANGED
@@ -12,7 +12,12 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
12
12
 
13
13
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
14
14
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
15
- export const AW_ECC_TAG = "v1.4.62";
15
+ export const AW_ECC_TAG = "v1.4.63";
16
+ const REQUIRED_ECC_FILES = [
17
+ "package.json",
18
+ "scripts/install-apply.js",
19
+ "scripts/sync-ecc-to-codex.sh",
20
+ ];
16
21
 
17
22
  const MARKETPLACE_NAME = "aw-marketplace";
18
23
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
@@ -85,11 +90,15 @@ async function cloneWithRefAsync(url, ref, dest) {
85
90
  }
86
91
  }
87
92
 
93
+ function hasRequiredEccFiles(dest) {
94
+ return REQUIRED_ECC_FILES.every((relPath) => existsSync(join(dest, relPath)));
95
+ }
96
+
88
97
  async function cloneOrUpdateAsync(tag, dest) {
89
98
  const overrideUrl = process.env.AW_ECC_CLONE_URL;
90
99
  const overrideRef = process.env.AW_ECC_CLONE_REF?.trim();
91
100
 
92
- if (existsSync(join(dest, ".git"))) {
101
+ if (existsSync(join(dest, ".git")) && hasRequiredEccFiles(dest)) {
93
102
  try {
94
103
  if (!overrideUrl && !overrideRef) {
95
104
  await runA(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
@@ -102,6 +111,7 @@ async function cloneOrUpdateAsync(tag, dest) {
102
111
  }
103
112
  // Restore working tree — pruneStaleHooks may have deleted tracked files
104
113
  await runA(`git -C ${dest} checkout -- .`);
114
+ if (!hasRequiredEccFiles(dest)) throw new Error("aw-ecc checkout is missing required files");
105
115
  return;
106
116
  } catch { /* fall through to fresh clone */ }
107
117
  }
@@ -188,7 +198,7 @@ function cloneOrUpdate(tag, dest) {
188
198
  // AW_ECC_CLONE_REF lets aw init install ECC from a non-default branch or tag.
189
199
  const overrideRef = process.env.AW_ECC_CLONE_REF?.trim();
190
200
 
191
- if (existsSync(join(dest, ".git"))) {
201
+ if (existsSync(join(dest, ".git")) && hasRequiredEccFiles(dest)) {
192
202
  try {
193
203
  if (!overrideUrl && !overrideRef) {
194
204
  run(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
@@ -201,6 +211,7 @@ function cloneOrUpdate(tag, dest) {
201
211
  }
202
212
  // Restore working tree — pruneStaleHooks may have deleted tracked files
203
213
  run(`git -C ${dest} checkout -- .`);
214
+ if (!hasRequiredEccFiles(dest)) throw new Error("aw-ecc checkout is missing required files");
204
215
  return;
205
216
  } catch { /* fall through to fresh clone */ }
206
217
  }
package/git.mjs CHANGED
@@ -453,6 +453,7 @@ export function detectChanges(awHome, registryDir) {
453
453
  if (!line || line.length < 3) continue;
454
454
  const xy = line.slice(0, 2);
455
455
  const filePath = line.slice(3).trim();
456
+ if (filePath === `${registryDir}/.aw-upgrade.log`) continue;
456
457
  const registryPath = filePath.startsWith(registryDir + '/') ? filePath.slice(registryDir.length + 1) : filePath;
457
458
  const entry = { path: filePath, registryPath };
458
459
 
@@ -485,6 +486,7 @@ export function getStagedFiles(awHome, registryDir) {
485
486
  if (!line) continue;
486
487
  const [status, filePath] = line.split('\t');
487
488
  if (!filePath?.startsWith(registryDir + '/')) continue;
489
+ if (filePath === `${registryDir}/.aw-upgrade.log`) continue;
488
490
  result.push({
489
491
  path: filePath,
490
492
  registryPath: filePath.slice(registryDir.length + 1),