@ghl-ai/aw 0.1.36-beta.1 → 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/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, readdirSync } 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,27 +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
- import { sparseCheckoutAsync, includeToSparsePaths, cleanup } from '../git.mjs';
18
- import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
19
16
  import { linkWorkspace } from '../link.mjs';
20
17
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
21
18
  import { setupMcp } from '../mcp.mjs';
22
19
  import { autoUpdate, promptUpdate } from '../update.mjs';
23
20
  import { installGlobalHooks } from '../hooks.mjs';
24
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';
25
32
 
26
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
34
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
28
35
 
29
36
  const HOME = homedir();
30
37
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
31
- const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
38
+ const AW_HOME = join(HOME, '.aw');
32
39
 
33
40
  // ── IDE tasks for auto-pull ─────────────────────────────────────────────
34
41
 
35
42
  function installIdeTasks() {
36
- // VS Code / Cursor task — runs aw pull on folder open
37
43
  const vscodeTask = {
38
44
  version: '2.0.0',
39
45
  tasks: [
@@ -48,18 +54,15 @@ function installIdeTasks() {
48
54
  ],
49
55
  };
50
56
 
51
- // Install globally for VS Code and Cursor
52
57
  for (const ide of ['Code', 'Cursor']) {
53
58
  const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
54
59
  if (!existsSync(userDir)) continue;
55
60
 
56
61
  const tasksPath = join(userDir, 'tasks.json');
57
62
  if (existsSync(tasksPath)) {
58
- // Don't override existing tasks — check if aw task already there
59
63
  try {
60
64
  const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
61
65
  if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
62
- // Add our task to existing
63
66
  existing.tasks = existing.tasks || [];
64
67
  existing.tasks.push(vscodeTask.tasks[0]);
65
68
  writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
@@ -72,29 +75,6 @@ function installIdeTasks() {
72
75
  }
73
76
  }
74
77
 
75
- function saveManifest(data) {
76
- writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
77
- }
78
-
79
-
80
- function printPullSummary(pattern, actions) {
81
- for (const type of ['agents', 'skills', 'commands', 'evals']) {
82
- const typeActions = actions.filter(a => a.type === type);
83
- if (typeActions.length === 0) continue;
84
-
85
- const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
86
- for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
87
-
88
- const parts = [];
89
- if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
90
- if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
91
- if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
92
- const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
93
-
94
- fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
95
- }
96
- }
97
-
98
78
  const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
99
79
 
100
80
  export async function initCommand(args) {
@@ -106,12 +86,11 @@ export async function initCommand(args) {
106
86
 
107
87
  // ── Validate ──────────────────────────────────────────────────────────
108
88
 
109
- // Parse team/sub-team
110
89
  let nsParts = namespace ? namespace.split('/') : [];
111
90
  let team = nsParts[0] || null;
112
91
  let subTeam = nsParts[1] || null;
113
- let teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
114
- let folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
92
+ let teamNS = subTeam ? `${team}-${subTeam}` : team;
93
+ let folderName = subTeam ? `${team}/${subTeam}` : team;
115
94
 
116
95
  if (team && !ALLOWED_NAMESPACES.includes(team)) {
117
96
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
@@ -142,10 +121,16 @@ export async function initCommand(args) {
142
121
  fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
143
122
  }
144
123
 
145
- // ── Probe remote registry to check if namespace exists ────────────────
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) ──
146
131
 
147
132
  let namespaceExistsInRemote = false;
148
- if (folderName && !silent) {
133
+ if (folderName && !silent && !isGitNative && !isLegacy) {
149
134
  try {
150
135
  const probePaths = includeToSparsePaths([folderName]);
151
136
  const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
@@ -158,18 +143,16 @@ export async function initCommand(args) {
158
143
  cleanup(probeDir);
159
144
  }
160
145
  } catch {
161
- // Network error — skip probe, proceed without prompt
162
146
  namespaceExistsInRemote = true;
163
147
  }
164
148
  }
165
149
 
166
- // If namespace does NOT exist in remote, ask user to confirm
167
- if (folderName && !silent && !namespaceExistsInRemote && process.stdin.isTTY) {
150
+ if (folderName && !silent && !isGitNative && !isLegacy && !namespaceExistsInRemote && process.stdin.isTTY) {
168
151
  const choice = await fmt.select({
169
- message: `The namespace '${folderName}' does not exist in the registry yet and will be created from [template].\nplatform/ includes shared agents, skills & commands that cover most use cases.\nHow would you like to proceed?`,
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?`,
170
153
  options: [
171
154
  { value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
172
- { value: 'create-namespace', label: `Create '${folderName}' namespace from template` },
155
+ { value: 'create-namespace', label: `Use '${folderName}' namespace (will be created)` },
173
156
  ],
174
157
  });
175
158
 
@@ -183,58 +166,34 @@ export async function initCommand(args) {
183
166
  }
184
167
  }
185
168
 
186
- const hasConfig = config.exists(GLOBAL_AW_DIR);
187
- const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
188
- const isExisting = hasConfig && hasPlatform;
189
169
  const cwd = process.cwd();
190
170
 
191
- // ── Fast path: already initialized just pull + link ─────────────────
171
+ // ── Re-init path: already set up with native git clone ────────────────
192
172
 
193
- if (isExisting) {
173
+ if (isGitNative) {
194
174
  const cfg = config.load(GLOBAL_AW_DIR);
195
175
 
196
- // Add new sub-team if not already tracked
197
176
  const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
198
177
  if (isNewSubTeam) {
199
178
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
200
- await pullAsync({
201
- ...args,
202
- _positional: ['[template]'],
203
- _renameNamespace: folderName,
204
- _teamNS: teamNS,
205
- _workspaceDir: GLOBAL_AW_DIR,
206
- _skipIntegrate: true,
207
- });
179
+ const newSparsePaths = [`.aw_registry/${folderName}`, `content`];
180
+ addToSparseCheckout(AW_HOME, newSparsePaths);
208
181
  config.addPattern(GLOBAL_AW_DIR, folderName);
209
182
  } else {
210
183
  if (!silent) fmt.logStep('Already initialized — syncing...');
211
184
  }
212
185
 
213
- // Pull latest (parallel)
214
- // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
215
- // only has '.aw_registry/[template]/' — remap non-platform entries back.
216
- // Platform is never stored in cfg.include but must always be pulled.
217
- const freshCfg = config.load(GLOBAL_AW_DIR);
218
- const pullJobs = [
219
- pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
220
- ];
221
- if (freshCfg && freshCfg.include.length > 0) {
222
- for (const p of freshCfg.include) {
223
- if (p === 'platform') continue; // already added above
224
- const derivedTeamNS = p.replace(/\//g, '-');
225
- pullJobs.push(pullAsync({
226
- ...args,
227
- _positional: ['[template]'],
228
- _workspaceDir: GLOBAL_AW_DIR,
229
- _skipIntegrate: true,
230
- _renameNamespace: p,
231
- _teamNS: derivedTeamNS,
232
- }));
233
- }
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)'));
234
193
  }
235
- await Promise.all(pullJobs);
236
194
 
237
- // Re-link IDE dirs + hooks (idempotent)
195
+ const freshCfg = config.load(GLOBAL_AW_DIR);
196
+
238
197
  linkWorkspace(HOME);
239
198
  await installAwEcc(cwd, { silent });
240
199
  generateCommands(HOME);
@@ -244,7 +203,6 @@ export async function initCommand(args) {
244
203
  if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
245
204
  installGlobalHooks();
246
205
 
247
- // Link current project if needed
248
206
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
249
207
  try {
250
208
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
@@ -266,52 +224,66 @@ export async function initCommand(args) {
266
224
  return;
267
225
  }
268
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
+
269
241
  // ── Full init: first time setup ───────────────────────────────────────
270
242
 
271
- // Auto-detect user
272
243
  if (!user) {
273
244
  try {
274
245
  user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
275
246
  } catch { /* git not configured */ }
276
247
  }
277
248
 
278
- // Step 1: Create global source of truth
279
- mkdirSync(GLOBAL_AW_DIR, { recursive: true });
280
-
281
- 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
+ }
282
254
 
283
255
  fmt.note([
284
- `${chalk.dim('source:')} ~/.aw_registry/`,
256
+ `${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
285
257
  folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
286
- user ? `${chalk.dim('user:')} ${cfg.user}` : null,
258
+ user ? `${chalk.dim('user:')} ${user}` : null,
287
259
  `${chalk.dim('version:')} v${VERSION}`,
288
- ].filter(Boolean).join('\n'), 'Config created');
260
+ ].filter(Boolean).join('\n'), 'Config');
289
261
 
290
- // Step 2: Pull registry content (parallel)
291
262
  const s = fmt.spinner();
292
- const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
293
- s.start(`Pulling ${pullTargets}...`);
294
-
295
- const pullJobs = [
296
- pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
297
- ];
298
- if (folderName) {
299
- pullJobs.push(
300
- pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
301
- );
302
- }
263
+ s.start(`Cloning registry...`);
303
264
 
304
- let pullResults;
305
265
  try {
306
- pullResults = await Promise.all(pullJobs);
307
- s.stop(`Pulled ${pullTargets}`);
266
+ initPersistentClone(repoUrl, AW_HOME, sparsePaths);
267
+ s.stop('Registry cloned');
308
268
  } catch (e) {
309
- s.stop(chalk.red('Pull failed'));
269
+ s.stop(chalk.red('Clone failed'));
310
270
  fmt.cancel(e.message);
311
271
  }
312
272
 
313
- for (const { pattern, actions } of pullResults) {
314
- 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);
315
287
  }
316
288
 
317
289
  // Step 3: Link IDE dirs + setup tasks
@@ -334,27 +306,14 @@ export async function initCommand(args) {
334
306
  } catch { /* best effort */ }
335
307
  }
336
308
 
337
- // Step 5: Write manifest for nuke cleanup
338
- const manifest = {
339
- version: 1,
340
- installedAt: new Date().toISOString(),
341
- globalDir: GLOBAL_AW_DIR,
342
- createdFiles: [
343
- ...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
344
- ...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
345
- ],
346
- globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
347
- };
348
- saveManifest(manifest);
349
-
350
309
  // Offer to update if a newer version is available
351
310
  await promptUpdate(await args._updateCheck);
352
311
 
353
- // Done
354
312
  fmt.outro([
355
313
  'Install complete',
356
314
  '',
357
- ` ${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/`,
358
317
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
359
318
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
360
319
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
package/commands/nuke.mjs CHANGED
@@ -290,7 +290,7 @@ export function nukeCommand(args) {
290
290
  } catch { /* not installed via npm or no permissions */ }
291
291
  }
292
292
 
293
- // 10. Remove ~/.aw_registry/ itself (source of truth last!)
293
+ // 10. Remove ~/.aw_registry/ now a symlink to ~/.aw/.aw_registry/
294
294
  try {
295
295
  rmSync(GLOBAL_AW_DIR, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
296
296
  } catch {
@@ -298,6 +298,17 @@ export function nukeCommand(args) {
298
298
  }
299
299
  fmt.logStep('Removed ~/.aw_registry/');
300
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
+
301
312
  fmt.outro([
302
313
  'Fully removed',
303
314
  '',
@@ -307,7 +318,7 @@ export function nukeCommand(args) {
307
318
  ` ${chalk.green('✓')} Project symlinks cleaned`,
308
319
  ` ${chalk.green('✓')} Git hooks removed`,
309
320
  ` ${chalk.green('✓')} IDE auto-sync tasks removed`,
310
- ` ${chalk.green('✓')} Source of truth deleted`,
321
+ ` ${chalk.green('✓')} Source of truth deleted (symlink + git clone)`,
311
322
  '',
312
323
  ` ${chalk.dim('No existing files were touched.')}`,
313
324
  ` ${chalk.dim('To reinstall:')} ${chalk.bold('aw init')}`,