@ghl-ai/aw 0.1.35 → 0.1.36-beta.2

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
@@ -72,7 +72,8 @@ function printHelp() {
72
72
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
73
73
  const help = [
74
74
  sec('Setup'),
75
- cmd('aw init --namespace <team/sub-team>', 'Initialize workspace (required)'),
75
+ cmd('aw init', 'Initialize workspace (platform/ only)'),
76
+ cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
76
77
  ` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
77
78
  ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
78
79
 
package/commands/drop.mjs CHANGED
@@ -2,87 +2,85 @@
2
2
 
3
3
  import { join, resolve } from 'node:path';
4
4
  import { rmSync, existsSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
5
6
  import * as config from '../config.mjs';
6
7
  import * as fmt from '../fmt.mjs';
7
8
  import { chalk } from '../fmt.mjs';
8
- import { matchesAny } from '../glob.mjs';
9
9
  import { resolveInput } from '../paths.mjs';
10
- import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
10
+ import { removeFromSparseCheckout, isValidClone } from '../git.mjs';
11
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
12
+ import { linkWorkspace } from '../link.mjs';
11
13
 
12
14
  export function dropCommand(args) {
13
15
  const input = args._positional?.[0];
14
16
  const cwd = process.cwd();
17
+
18
+ const HOME = homedir();
19
+ const AW_HOME = join(HOME, '.aw');
20
+ const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
15
21
  const workspaceDir = join(cwd, '.aw_registry');
16
22
 
17
23
  fmt.intro('aw drop');
18
24
 
19
25
  if (!input) {
20
- fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)\n aw drop example-team/skills/example-skill/SKILL (delete single file)');
26
+ fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)');
27
+ return;
21
28
  }
22
29
 
23
- const cfg = config.load(workspaceDir);
24
- if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
30
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
31
+ if (!isValidClone(AW_HOME, repoUrl)) {
32
+ fmt.cancel('Registry not initialized. Run: aw init');
33
+ return;
34
+ }
25
35
 
26
- // Resolve to registry path (accepts both local and registry paths)
36
+ const cfg = config.load(GLOBAL_AW_DIR);
37
+ if (!cfg) {
38
+ fmt.cancel('No .sync-config.json found. Run: aw init');
39
+ return;
40
+ }
41
+
42
+ // Resolve to registry path
27
43
  const resolved = resolveInput(input, workspaceDir);
28
44
  const regPath = resolved.registryPath;
29
45
 
30
46
  if (!regPath) {
31
47
  fmt.cancel(`Could not resolve "${input}" to a registry path`);
48
+ return;
32
49
  }
33
50
 
34
- // Check if this path (or a parent) is in config → remove from config + delete files
51
+ // Check if this path (or a parent) is in config
35
52
  const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
36
53
 
37
54
  if (isConfigPath) {
38
- config.removePattern(workspaceDir, regPath);
55
+ // Remove from sparse checkout
56
+ try {
57
+ removeFromSparseCheckout(AW_HOME, [`${REGISTRY_DIR}/${regPath}`]);
58
+ } catch (e) {
59
+ fmt.logWarn(`Could not update sparse checkout: ${e.message}`);
60
+ }
61
+
62
+ config.removePattern(GLOBAL_AW_DIR, regPath);
39
63
  fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
40
64
  }
41
65
 
42
- // Delete matching local files
43
- const removed = deleteMatchingFiles(workspaceDir, regPath);
66
+ // Count removed files (they disappear from working tree via sparse checkout)
67
+ const registryAbsPath = join(AW_HOME, REGISTRY_DIR, regPath);
68
+ let removed = 0;
69
+ if (!existsSync(registryAbsPath)) {
70
+ removed = 1; // sparse checkout removed it
71
+ }
44
72
 
45
- if (removed > 0) {
46
- fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
47
- } else if (!isConfigPath) {
73
+ if (!isConfigPath && removed === 0) {
48
74
  fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
75
+ return;
49
76
  }
50
77
 
51
- if (!isConfigPath && removed > 0) {
52
- fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
78
+ if (!isConfigPath) {
79
+ fmt.logWarn(`Path was not in sync config — no sparse checkout change made`);
53
80
  }
54
81
 
55
- fmt.outro('Done');
56
- }
82
+ // Re-link to remove dead symlinks
83
+ linkWorkspace(HOME);
57
84
 
58
- /**
59
- * Find and delete local files whose registry path matches the given path.
60
- */
61
- function deleteMatchingFiles(workspaceDir, path) {
62
- const manifest = loadManifest(workspaceDir);
63
- let removed = 0;
64
-
65
- for (const [manifestKey] of Object.entries(manifest.files)) {
66
- const registryPath = manifestKeyToRegistryPath(manifestKey);
67
-
68
- if (matchesAny(registryPath, [path])) {
69
- const filePath = join(workspaceDir, manifestKey);
70
- if (existsSync(filePath)) {
71
- rmSync(filePath, { recursive: true, force: true });
72
- removed++;
73
- }
74
- delete manifest.files[manifestKey];
75
- }
76
- }
77
-
78
- saveManifest(workspaceDir, manifest);
79
- return removed;
80
- }
81
-
82
- /**
83
- * Convert manifest key to registry path.
84
- * Manifest key now mirrors registry: "platform/agents/architecture-reviewer.md" → "platform/agents/architecture-reviewer"
85
- */
86
- function manifestKeyToRegistryPath(manifestKey) {
87
- return manifestKey.replace(/\.md$/, '');
85
+ fmt.outro('Done');
88
86
  }
package/commands/init.mjs CHANGED
@@ -1,10 +1,10 @@
1
- // commands/init.mjs — Clean init: clone registry, link IDEs, global git hooks.
1
+ // commands/init.mjs — Clean init: persistent git clone of registry, link IDEs, global git hooks.
2
2
  //
3
3
  // No shell profile modifications. No daemons. No background processes.
4
4
  // Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
5
5
  // Uses IDE tasks for auto-pull on workspace open.
6
6
 
7
- import { mkdirSync, existsSync, writeFileSync, symlinkSync } from 'node:fs';
7
+ import { existsSync, writeFileSync, symlinkSync, lstatSync, readdirSync } from 'node:fs';
8
8
  import { execSync } from 'node:child_process';
9
9
  import { join, dirname } from 'node:path';
10
10
  import { homedir } from 'node:os';
@@ -13,24 +13,33 @@ import { readFileSync } from 'node:fs';
13
13
  import * as config from '../config.mjs';
14
14
  import * as fmt from '../fmt.mjs';
15
15
  import { chalk } from '../fmt.mjs';
16
- import { pullCommand, pullAsync } from './pull.mjs';
17
16
  import { linkWorkspace } from '../link.mjs';
18
17
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
19
18
  import { setupMcp } from '../mcp.mjs';
20
19
  import { autoUpdate, promptUpdate } from '../update.mjs';
21
20
  import { installGlobalHooks } from '../hooks.mjs';
21
+ import { installAwEcc } from '../ecc.mjs';
22
+ import {
23
+ initPersistentClone,
24
+ isValidClone,
25
+ fetchAndMerge,
26
+ addToSparseCheckout,
27
+ includeToSparsePaths,
28
+ sparseCheckoutAsync,
29
+ cleanup,
30
+ } from '../git.mjs';
31
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
22
32
 
23
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
34
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
25
35
 
26
36
  const HOME = homedir();
27
37
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
28
- const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
38
+ const AW_HOME = join(HOME, '.aw');
29
39
 
30
40
  // ── IDE tasks for auto-pull ─────────────────────────────────────────────
31
41
 
32
42
  function installIdeTasks() {
33
- // VS Code / Cursor task — runs aw pull on folder open
34
43
  const vscodeTask = {
35
44
  version: '2.0.0',
36
45
  tasks: [
@@ -45,18 +54,15 @@ function installIdeTasks() {
45
54
  ],
46
55
  };
47
56
 
48
- // Install globally for VS Code and Cursor
49
57
  for (const ide of ['Code', 'Cursor']) {
50
58
  const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
51
59
  if (!existsSync(userDir)) continue;
52
60
 
53
61
  const tasksPath = join(userDir, 'tasks.json');
54
62
  if (existsSync(tasksPath)) {
55
- // Don't override existing tasks — check if aw task already there
56
63
  try {
57
64
  const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
58
65
  if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
59
- // Add our task to existing
60
66
  existing.tasks = existing.tasks || [];
61
67
  existing.tasks.push(vscodeTask.tasks[0]);
62
68
  writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
@@ -69,33 +75,10 @@ function installIdeTasks() {
69
75
  }
70
76
  }
71
77
 
72
- function saveManifest(data) {
73
- writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
74
- }
75
-
76
-
77
- function printPullSummary(pattern, actions) {
78
- for (const type of ['agents', 'skills', 'commands', 'evals']) {
79
- const typeActions = actions.filter(a => a.type === type);
80
- if (typeActions.length === 0) continue;
81
-
82
- const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
83
- for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
84
-
85
- const parts = [];
86
- if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
87
- if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
88
- if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
89
- const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
90
-
91
- fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
92
- }
93
- }
94
-
95
78
  const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
96
79
 
97
80
  export async function initCommand(args) {
98
- const namespace = args['--namespace'] || null;
81
+ let namespace = args['--namespace'] || null;
99
82
  let user = args['--user'] || '';
100
83
  const silent = args['--silent'] === true;
101
84
 
@@ -103,24 +86,11 @@ export async function initCommand(args) {
103
86
 
104
87
  // ── Validate ──────────────────────────────────────────────────────────
105
88
 
106
- if (!namespace && !silent) {
107
- const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
108
- fmt.cancel([
109
- `Missing required ${chalk.bold('--namespace')} flag`,
110
- '',
111
- ` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
112
- ` ${chalk.dim('Teams:')} ${list}`,
113
- '',
114
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
115
- ].join('\n'));
116
- }
117
-
118
- // Parse team/sub-team
119
- const nsParts = namespace ? namespace.split('/') : [];
120
- const team = nsParts[0] || null;
121
- const subTeam = nsParts[1] || null;
122
- const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
123
- const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
89
+ let nsParts = namespace ? namespace.split('/') : [];
90
+ let team = nsParts[0] || null;
91
+ let subTeam = nsParts[1] || null;
92
+ let teamNS = subTeam ? `${team}-${subTeam}` : team;
93
+ let folderName = subTeam ? `${team}/${subTeam}` : team;
124
94
 
125
95
  if (team && !ALLOWED_NAMESPACES.includes(team)) {
126
96
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
@@ -151,55 +121,81 @@ export async function initCommand(args) {
151
121
  fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
152
122
  }
153
123
 
154
- const hasConfig = config.exists(GLOBAL_AW_DIR);
155
- const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
156
- const isExisting = hasConfig && hasPlatform;
124
+ // ── Detect installation state ─────────────────────────────────────────
125
+
126
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
127
+ const isGitNative = isValidClone(AW_HOME, repoUrl);
128
+ const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
129
+
130
+ // ── Probe remote registry to check if namespace exists (fresh install only) ──
131
+
132
+ let namespaceExistsInRemote = false;
133
+ if (folderName && !silent && !isGitNative && !isLegacy) {
134
+ try {
135
+ const probePaths = includeToSparsePaths([folderName]);
136
+ const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
137
+ try {
138
+ const fullNsPath = join(probeDir, REGISTRY_DIR, ...folderName.split('/'));
139
+ namespaceExistsInRemote = existsSync(fullNsPath) &&
140
+ readdirSync(fullNsPath, { withFileTypes: true })
141
+ .some(d => d.isDirectory() && !d.name.startsWith('.'));
142
+ } finally {
143
+ cleanup(probeDir);
144
+ }
145
+ } catch {
146
+ namespaceExistsInRemote = true;
147
+ }
148
+ }
149
+
150
+ if (folderName && !silent && !isGitNative && !isLegacy && !namespaceExistsInRemote && process.stdin.isTTY) {
151
+ const choice = await fmt.select({
152
+ message: `The namespace '${folderName}' does not exist in the registry yet.\nplatform/ includes shared agents, skills & commands that cover most use cases.\nHow would you like to proceed?`,
153
+ options: [
154
+ { value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
155
+ { value: 'create-namespace', label: `Use '${folderName}' namespace (will be created)` },
156
+ ],
157
+ });
158
+
159
+ if (fmt.isCancel(choice)) {
160
+ fmt.cancel('Operation cancelled.');
161
+ process.exit(0);
162
+ }
163
+
164
+ if (choice === 'platform-only') {
165
+ namespace = null; team = null; subTeam = null; teamNS = null; folderName = null;
166
+ }
167
+ }
168
+
157
169
  const cwd = process.cwd();
158
170
 
159
- // ── Fast path: already initialized just pull + link ─────────────────
171
+ // ── Re-init path: already set up with native git clone ────────────────
160
172
 
161
- if (isExisting) {
173
+ if (isGitNative) {
162
174
  const cfg = config.load(GLOBAL_AW_DIR);
163
175
 
164
- // Add new sub-team if not already tracked
165
176
  const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
166
177
  if (isNewSubTeam) {
167
178
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
168
- await pullAsync({
169
- ...args,
170
- _positional: ['[template]'],
171
- _renameNamespace: folderName,
172
- _teamNS: teamNS,
173
- _workspaceDir: GLOBAL_AW_DIR,
174
- _skipIntegrate: true,
175
- });
179
+ const newSparsePaths = [`.aw_registry/${folderName}`, `content`];
180
+ addToSparseCheckout(AW_HOME, newSparsePaths);
176
181
  config.addPattern(GLOBAL_AW_DIR, folderName);
177
182
  } else {
178
183
  if (!silent) fmt.logStep('Already initialized — syncing...');
179
184
  }
180
185
 
181
- // Pull latest (parallel)
182
- // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
183
- // only has '.aw_registry/[template]/' — remap non-platform entries back.
184
- const freshCfg = config.load(GLOBAL_AW_DIR);
185
- if (freshCfg && freshCfg.include.length > 0) {
186
- const pullJobs = freshCfg.include.map(p => {
187
- const isTeamNs = p !== 'platform';
188
- const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
189
- return pullAsync({
190
- ...args,
191
- _positional: [isTeamNs ? '[template]' : p],
192
- _workspaceDir: GLOBAL_AW_DIR,
193
- _skipIntegrate: true,
194
- _renameNamespace: isTeamNs ? p : undefined,
195
- _teamNS: derivedTeamNS,
196
- });
197
- });
198
- await Promise.all(pullJobs);
186
+ const s = fmt.spinner ? fmt.spinner() : { start: () => {}, stop: () => {} };
187
+ if (!silent) s.start('Fetching latest...');
188
+ try {
189
+ fetchAndMerge(AW_HOME);
190
+ if (!silent) s.stop('Registry updated');
191
+ } catch (e) {
192
+ if (!silent) s.stop(chalk.yellow('Fetch failed (continuing with local)'));
199
193
  }
200
194
 
201
- // Re-link IDE dirs + hooks (idempotent)
195
+ const freshCfg = config.load(GLOBAL_AW_DIR);
196
+
202
197
  linkWorkspace(HOME);
198
+ await installAwEcc(cwd, { silent });
203
199
  generateCommands(HOME);
204
200
  copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
205
201
  initAwDocs(HOME);
@@ -207,7 +203,6 @@ export async function initCommand(args) {
207
203
  if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
208
204
  installGlobalHooks();
209
205
 
210
- // Link current project if needed
211
206
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
212
207
  try {
213
208
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
@@ -229,57 +224,72 @@ export async function initCommand(args) {
229
224
  return;
230
225
  }
231
226
 
227
+ // ── Legacy migration: old ~/.aw_registry/ dir → notify user ──────────
228
+
229
+ if (isLegacy) {
230
+ if (!silent) {
231
+ fmt.logWarn([
232
+ 'Legacy installation detected (~/.aw_registry/ is a plain directory).',
233
+ '',
234
+ ` Run ${chalk.bold('aw nuke')} first to remove the old install, then ${chalk.bold('aw init')} again.`,
235
+ ` This will migrate to the new native git clone at ~/.aw/`,
236
+ ].join('\n'));
237
+ }
238
+ // Fall through to full init — create AW_HOME fresh
239
+ }
240
+
232
241
  // ── Full init: first time setup ───────────────────────────────────────
233
242
 
234
- // Auto-detect user
235
243
  if (!user) {
236
244
  try {
237
245
  user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
238
246
  } catch { /* git not configured */ }
239
247
  }
240
248
 
241
- // Step 1: Create global source of truth
242
- mkdirSync(GLOBAL_AW_DIR, { recursive: true });
243
-
244
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
249
+ // Determine sparse paths
250
+ const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`];
251
+ if (folderName) {
252
+ sparsePaths.push(`.aw_registry/${folderName}`);
253
+ }
245
254
 
246
255
  fmt.note([
247
- `${chalk.dim('source:')} ~/.aw_registry/`,
256
+ `${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
248
257
  folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
249
- user ? `${chalk.dim('user:')} ${cfg.user}` : null,
258
+ user ? `${chalk.dim('user:')} ${user}` : null,
250
259
  `${chalk.dim('version:')} v${VERSION}`,
251
- ].filter(Boolean).join('\n'), 'Config created');
260
+ ].filter(Boolean).join('\n'), 'Config');
252
261
 
253
- // Step 2: Pull registry content (parallel)
254
262
  const s = fmt.spinner();
255
- const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
256
- s.start(`Pulling ${pullTargets}...`);
257
-
258
- const pullJobs = [
259
- pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
260
- ];
261
- if (folderName) {
262
- pullJobs.push(
263
- pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
264
- );
265
- }
263
+ s.start(`Cloning registry...`);
266
264
 
267
- let pullResults;
268
265
  try {
269
- pullResults = await Promise.all(pullJobs);
270
- s.stop(`Pulled ${pullTargets}`);
266
+ initPersistentClone(repoUrl, AW_HOME, sparsePaths);
267
+ s.stop('Registry cloned');
271
268
  } catch (e) {
272
- s.stop(chalk.red('Pull failed'));
269
+ s.stop(chalk.red('Clone failed'));
273
270
  fmt.cancel(e.message);
274
271
  }
275
272
 
276
- for (const { pattern, actions } of pullResults) {
277
- printPullSummary(pattern, actions);
273
+ // Create backward-compat symlink: ~/.aw_registry/ ~/.aw/.aw_registry/
274
+ if (!existsSync(GLOBAL_AW_DIR)) {
275
+ try {
276
+ symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
277
+ fmt.logStep('Created ~/.aw_registry/ symlink');
278
+ } catch (e) {
279
+ fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
280
+ }
281
+ }
282
+
283
+ // Create sync config
284
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
285
+ if (folderName) {
286
+ config.addPattern(GLOBAL_AW_DIR, folderName);
278
287
  }
279
288
 
280
289
  // Step 3: Link IDE dirs + setup tasks
281
290
  fmt.logStep('Linking IDE symlinks...');
282
291
  linkWorkspace(HOME);
292
+ await installAwEcc(cwd, { silent });
283
293
  generateCommands(HOME);
284
294
  const instructionFiles = copyInstructions(HOME, null, team) || [];
285
295
  initAwDocs(HOME);
@@ -296,27 +306,14 @@ export async function initCommand(args) {
296
306
  } catch { /* best effort */ }
297
307
  }
298
308
 
299
- // Step 5: Write manifest for nuke cleanup
300
- const manifest = {
301
- version: 1,
302
- installedAt: new Date().toISOString(),
303
- globalDir: GLOBAL_AW_DIR,
304
- createdFiles: [
305
- ...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
306
- ...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
307
- ],
308
- globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
309
- };
310
- saveManifest(manifest);
311
-
312
309
  // Offer to update if a newer version is available
313
310
  await promptUpdate(await args._updateCheck);
314
311
 
315
- // Done
316
312
  fmt.outro([
317
313
  'Install complete',
318
314
  '',
319
- ` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
315
+ ` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
316
+ ` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
320
317
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
321
318
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
322
319
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
package/commands/nuke.mjs CHANGED
@@ -9,6 +9,7 @@ import { execSync } from 'node:child_process';
9
9
  import * as fmt from '../fmt.mjs';
10
10
  import { chalk } from '../fmt.mjs';
11
11
  import { removeGlobalHooks } from '../hooks.mjs';
12
+ import { uninstallAwEcc } from '../ecc.mjs';
12
13
 
13
14
  const HOME = homedir();
14
15
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
@@ -243,28 +244,31 @@ export function nukeCommand(args) {
243
244
  // 2. Remove IDE symlinks (only those pointing to .aw_registry)
244
245
  removeIdeSymlinks();
245
246
 
246
- // 3. Remove .aw_registry symlinks from ALL project directories
247
+ // 3. Remove aw-ecc installed files (agents, commands, rules, skills, hooks)
248
+ uninstallAwEcc();
249
+
250
+ // 4. Remove .aw_registry symlinks from ALL project directories
247
251
  removeProjectSymlinks();
248
252
 
249
- // 4. Remove git hooks (core.hooksPath + legacy template)
253
+ // 5. Remove git hooks (core.hooksPath + legacy template)
250
254
  removeGitHooks(manifest);
251
255
 
252
- // 5. Remove IDE auto-init tasks
256
+ // 6. Remove IDE auto-init tasks
253
257
  removeIdeTasks();
254
258
 
255
- // 5b. Remove upgrade lock/log (inside .aw_registry, must happen before dir removal)
259
+ // Remove upgrade lock/log (inside .aw_registry, must happen before dir removal)
256
260
  for (const p of [join(GLOBAL_AW_DIR, '.aw-upgrade.lock'), join(GLOBAL_AW_DIR, '.aw-upgrade.log')]) {
257
261
  try { if (existsSync(p)) rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ }
258
262
  }
259
263
 
260
- // 6. Remove ~/.aw_docs/
264
+ // 7. Remove ~/.aw_docs/
261
265
  const awDocs = join(HOME, '.aw_docs');
262
266
  if (existsSync(awDocs)) {
263
267
  rmSync(awDocs, { recursive: true, force: true });
264
268
  fmt.logStep('Removed ~/.aw_docs/');
265
269
  }
266
270
 
267
- // 7. Remove any manual `aw` symlinks (e.g. ~/.local/bin/aw)
271
+ // 8. Remove any manual `aw` symlinks (e.g. ~/.local/bin/aw)
268
272
  const manualBins = [
269
273
  join(HOME, '.local', 'bin', 'aw'),
270
274
  join(HOME, 'bin', 'aw'),
@@ -278,7 +282,7 @@ export function nukeCommand(args) {
278
282
  } catch { /* doesn't exist */ }
279
283
  }
280
284
 
281
- // 8. Uninstall npm global package (skip if already in npm uninstall lifecycle)
285
+ // 9. Uninstall npm global package (skip if already in npm uninstall lifecycle)
282
286
  if (!process.env.npm_lifecycle_event) {
283
287
  try {
284
288
  execSync('npm uninstall -g @ghl-ai/aw', { stdio: 'pipe', timeout: 15000 });
@@ -286,19 +290,35 @@ export function nukeCommand(args) {
286
290
  } catch { /* not installed via npm or no permissions */ }
287
291
  }
288
292
 
289
- // 9. Remove ~/.aw_registry/ itself (source of truth last!)
290
- rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
293
+ // 10. Remove ~/.aw_registry/ now a symlink to ~/.aw/.aw_registry/
294
+ try {
295
+ rmSync(GLOBAL_AW_DIR, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
296
+ } catch {
297
+ try { execSync(`rm -rf "${GLOBAL_AW_DIR}"`, { stdio: 'pipe' }); } catch { /* best effort */ }
298
+ }
291
299
  fmt.logStep('Removed ~/.aw_registry/');
292
300
 
301
+ // 11. Remove ~/.aw/ (persistent git clone)
302
+ const AW_HOME = join(HOME, '.aw');
303
+ if (existsSync(AW_HOME)) {
304
+ try {
305
+ rmSync(AW_HOME, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
306
+ } catch {
307
+ try { execSync(`rm -rf "${AW_HOME}"`, { stdio: 'pipe' }); } catch { /* best effort */ }
308
+ }
309
+ fmt.logStep('Removed ~/.aw/');
310
+ }
311
+
293
312
  fmt.outro([
294
313
  'Fully removed',
295
314
  '',
296
315
  ` ${chalk.green('✓')} Generated files cleaned`,
297
316
  ` ${chalk.green('✓')} IDE symlinks cleaned`,
317
+ ` ${chalk.green('✓')} aw-ecc engine removed`,
298
318
  ` ${chalk.green('✓')} Project symlinks cleaned`,
299
319
  ` ${chalk.green('✓')} Git hooks removed`,
300
320
  ` ${chalk.green('✓')} IDE auto-sync tasks removed`,
301
- ` ${chalk.green('✓')} Source of truth deleted`,
321
+ ` ${chalk.green('✓')} Source of truth deleted (symlink + git clone)`,
302
322
  '',
303
323
  ` ${chalk.dim('No existing files were touched.')}`,
304
324
  ` ${chalk.dim('To reinstall:')} ${chalk.bold('aw init')}`,