@ghl-ai/aw 0.1.11 → 0.1.13

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/nuke.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // commands/nuke.mjs — Safe cleanup: reads manifest, removes only what AW created
1
+ // commands/nuke.mjs — Safe cleanup: removes only what AW created
2
2
  //
3
3
  // Safety guarantee: NEVER deletes files that AW didn't create.
4
4
 
@@ -13,31 +13,28 @@ const HOME = homedir();
13
13
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
14
14
  const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
15
15
 
16
- // IDE dirs where AW creates symlinks
17
16
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
18
17
  const CONTENT_TYPES = ['agents', 'skills', 'commands', 'blueprints', 'evals'];
19
18
 
20
19
  function loadManifest() {
21
20
  if (!existsSync(MANIFEST_PATH)) return null;
22
- try {
23
- return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
24
- } catch { return null; }
21
+ try { return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')); }
22
+ catch { return null; }
25
23
  }
26
24
 
27
- // Remove AW-created files listed in manifest
25
+ // 1. Remove AW-created files listed in manifest
28
26
  function removeCreatedFiles(manifest) {
29
27
  if (!manifest?.createdFiles?.length) return;
30
28
  let removed = 0;
31
29
  for (const rel of manifest.createdFiles) {
32
30
  const p = join(HOME, rel);
33
- try {
34
- if (existsSync(p)) { rmSync(p); removed++; }
35
- } catch { /* best effort */ }
31
+ try { if (existsSync(p)) { rmSync(p); removed++; } }
32
+ catch { /* best effort */ }
36
33
  }
37
34
  if (removed > 0) fmt.logStep(`Removed ${removed} generated file${removed > 1 ? 's' : ''}`);
38
35
  }
39
36
 
40
- // Remove symlinks from IDE dirs that point into .aw_registry
37
+ // 2. Remove symlinks from IDE dirs that point into .aw_registry
41
38
  function removeIdeSymlinks() {
42
39
  let removed = 0;
43
40
 
@@ -60,42 +57,118 @@ function removeIdeSymlinks() {
60
57
  // Remove dir if now empty
61
58
  try { if (readdirSync(dir).length === 0) rmSync(dir); } catch {}
62
59
  }
60
+
61
+ // Also clean CLAUDE.md / AGENTS.md if they're AW-generated
62
+ for (const file of ['CLAUDE.md', 'AGENTS.md']) {
63
+ const p = join(HOME, ide, file);
64
+ try {
65
+ if (lstatSync(p).isSymbolicLink()) {
66
+ const target = readlinkSync(p);
67
+ if (target.includes('.aw_registry') || target.includes('aw_registry')) {
68
+ unlinkSync(p); removed++;
69
+ }
70
+ }
71
+ } catch { /* not a symlink or doesn't exist */ }
72
+ }
63
73
  }
64
74
 
65
75
  if (removed > 0) fmt.logStep(`Removed ${removed} IDE symlink${removed > 1 ? 's' : ''}`);
66
76
  }
67
77
 
68
- // Find and remove .aw_registry symlinks from project directories
78
+ // 3. Find and remove ALL .aw_registry symlinks from project directories
69
79
  function removeProjectSymlinks() {
70
80
  let removed = 0;
71
- const dirsToCheck = new Set([process.cwd()]);
72
81
 
73
- // Scan ~/Documents, ~/Projects, ~/Desktop, ~/dev and their children
74
- for (const parent of ['Documents', 'Projects', 'Desktop', 'dev', 'repos', 'src', 'code', 'work']) {
75
- const p = join(HOME, parent);
76
- if (!existsSync(p)) continue;
77
- try {
78
- for (const sub of readdirSync(p)) {
79
- dirsToCheck.add(join(p, sub));
82
+ // Use find command for thorough search much more reliable than manual scanning
83
+ try {
84
+ const result = execSync(
85
+ `find "${HOME}" -maxdepth 4 -name ".aw_registry" -type l 2>/dev/null`,
86
+ { encoding: 'utf8', timeout: 10000 }
87
+ ).trim();
88
+
89
+ if (result) {
90
+ for (const linkPath of result.split('\n').filter(Boolean)) {
91
+ try {
92
+ unlinkSync(linkPath);
93
+ removed++;
94
+ } catch { /* best effort */ }
80
95
  }
81
- } catch {}
82
- }
96
+ }
97
+ } catch {
98
+ // Fallback: manual scan of common dirs
99
+ const dirsToCheck = new Set([process.cwd()]);
100
+ for (const parent of ['Documents', 'Projects', 'Desktop', 'dev', 'repos', 'src', 'code', 'work']) {
101
+ const p = join(HOME, parent);
102
+ if (!existsSync(p)) continue;
103
+ try {
104
+ for (const sub of readdirSync(p)) {
105
+ const sp = join(p, sub);
106
+ dirsToCheck.add(sp);
107
+ // Also check one level deeper
108
+ try {
109
+ for (const sub2 of readdirSync(sp)) {
110
+ dirsToCheck.add(join(sp, sub2));
111
+ }
112
+ } catch {}
113
+ }
114
+ } catch {}
115
+ }
83
116
 
84
- for (const dir of dirsToCheck) {
85
- const link = join(dir, '.aw_registry');
86
- try {
87
- if (lstatSync(link).isSymbolicLink()) { unlinkSync(link); removed++; }
88
- } catch { /* doesn't exist or not a symlink */ }
117
+ for (const dir of dirsToCheck) {
118
+ const link = join(dir, '.aw_registry');
119
+ try {
120
+ if (lstatSync(link).isSymbolicLink()) { unlinkSync(link); removed++; }
121
+ } catch {}
122
+ }
89
123
  }
90
124
 
91
125
  if (removed > 0) fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
92
126
  }
93
127
 
128
+ // 4. Remove git template hook
129
+ function removeGitTemplate(manifest) {
130
+ const gitTemplateDir = manifest?.gitTemplate || join(HOME, '.git-templates', 'aw');
131
+ if (!existsSync(gitTemplateDir)) return;
132
+
133
+ rmSync(gitTemplateDir, { recursive: true, force: true });
134
+ try {
135
+ const current = execSync('git config --global init.templateDir', {
136
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
137
+ }).trim();
138
+ if (current === gitTemplateDir) {
139
+ execSync('git config --global --unset init.templateDir', { stdio: 'pipe' });
140
+ }
141
+ } catch { /* not set */ }
142
+ fmt.logStep('Removed git template hook');
143
+ }
144
+
145
+ // 5. Remove IDE auto-init tasks
146
+ function removeIdeTasks() {
147
+ for (const ide of ['Code', 'Cursor']) {
148
+ const tasksPath = join(HOME, 'Library', 'Application Support', ide, 'User', 'tasks.json');
149
+ if (!existsSync(tasksPath)) continue;
150
+ try {
151
+ const data = JSON.parse(readFileSync(tasksPath, 'utf8'));
152
+ const before = data.tasks?.length || 0;
153
+ data.tasks = (data.tasks || []).filter(t =>
154
+ t.label !== 'aw: sync registry' && t.label !== 'aw: pull registry'
155
+ );
156
+ if (data.tasks.length < before) {
157
+ if (data.tasks.length === 0) {
158
+ unlinkSync(tasksPath);
159
+ } else {
160
+ writeFileSync(tasksPath, JSON.stringify(data, null, 2) + '\n');
161
+ }
162
+ fmt.logStep(`Removed auto-sync task from ${ide}`);
163
+ }
164
+ } catch { /* best effort */ }
165
+ }
166
+ }
167
+
94
168
  export function nukeCommand(args) {
95
169
  fmt.intro('aw nuke');
96
170
 
97
171
  if (!existsSync(GLOBAL_AW_DIR)) {
98
- // Check cwd for local symlink
99
172
  const local = join(process.cwd(), '.aw_registry');
100
173
  if (existsSync(local) && lstatSync(local).isSymbolicLink()) {
101
174
  unlinkSync(local);
@@ -114,71 +187,46 @@ export function nukeCommand(args) {
114
187
  manifest ? `${chalk.dim('installed:')} ${manifest.installedAt}` : null,
115
188
  ].filter(Boolean).join('\n'), 'Cleaning up');
116
189
 
190
+ // Order matters: remove symlinks BEFORE removing the source
191
+
117
192
  // 1. Remove AW-created files (instruction files, MCP configs)
118
193
  removeCreatedFiles(manifest);
119
194
 
120
195
  // 2. Remove IDE symlinks (only those pointing to .aw_registry)
121
196
  removeIdeSymlinks();
122
197
 
123
- // 3. Remove .aw_registry symlinks from project directories
198
+ // 3. Remove .aw_registry symlinks from ALL project directories
124
199
  removeProjectSymlinks();
125
200
 
126
201
  // 4. Remove git template hook
127
- const gitTemplateDir = manifest?.gitTemplate || join(HOME, '.git-templates', 'aw');
128
- if (existsSync(gitTemplateDir)) {
129
- rmSync(gitTemplateDir, { recursive: true, force: true });
130
- // Unset git config if it pointed to our template
131
- try {
132
- const current = execSync('git config --global init.templateDir', {
133
- encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
134
- }).trim();
135
- if (current === gitTemplateDir) {
136
- execSync('git config --global --unset init.templateDir', { stdio: 'pipe' });
137
- }
138
- } catch { /* not set */ }
139
- fmt.logStep('Removed git template hook');
140
- }
202
+ removeGitTemplate(manifest);
141
203
 
142
- // 5. Remove IDE auto-pull tasks
143
- for (const ide of ['Code', 'Cursor']) {
144
- const tasksPath = join(HOME, 'Library', 'Application Support', ide, 'User', 'tasks.json');
145
- if (!existsSync(tasksPath)) continue;
146
- try {
147
- const data = JSON.parse(readFileSync(tasksPath, 'utf8'));
148
- const before = data.tasks?.length || 0;
149
- data.tasks = (data.tasks || []).filter(t => t.label !== 'aw: sync registry' && t.label !== 'aw: pull registry');
150
- if (data.tasks.length < before) {
151
- if (data.tasks.length === 0) {
152
- unlinkSync(tasksPath);
153
- } else {
154
- writeFileSync(tasksPath, JSON.stringify(data, null, 2) + '\n');
155
- }
156
- fmt.logStep(`Removed auto-pull task from ${ide}`);
157
- }
158
- } catch { /* best effort */ }
159
- }
204
+ // 5. Remove IDE auto-init tasks
205
+ removeIdeTasks();
160
206
 
161
- // 7. Remove ~/.aw_docs/
207
+ // 6. Remove ~/.aw_docs/
162
208
  const awDocs = join(HOME, '.aw_docs');
163
209
  if (existsSync(awDocs)) {
164
210
  rmSync(awDocs, { recursive: true, force: true });
165
211
  fmt.logStep('Removed ~/.aw_docs/');
166
212
  }
167
213
 
168
- // 8. Remove ~/.aw_registry/ itself
214
+ // 7. Remove ~/.aw_registry/ itself (source of truth — last!)
169
215
  rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
170
216
  fmt.logStep('Removed ~/.aw_registry/');
171
217
 
172
218
  fmt.outro([
173
219
  'Fully removed',
174
220
  '',
221
+ ` ${chalk.green('✓')} Generated files cleaned`,
175
222
  ` ${chalk.green('✓')} IDE symlinks cleaned`,
176
223
  ` ${chalk.green('✓')} Project symlinks cleaned`,
177
224
  ` ${chalk.green('✓')} Git template hook removed`,
178
- ` ${chalk.green('✓')} IDE auto-pull tasks removed`,
225
+ ` ${chalk.green('✓')} IDE auto-sync tasks removed`,
179
226
  ` ${chalk.green('✓')} Source of truth deleted`,
180
227
  '',
181
228
  ` ${chalk.dim('No existing files were touched.')}`,
182
- ` ${chalk.dim('Run')} ${chalk.bold('aw init')} ${chalk.dim('to reinstall.')}`,
229
+ ` ${chalk.dim('To reinstall:')} ${chalk.bold('aw init')}`,
230
+ ` ${chalk.dim('To uninstall CLI:')} ${chalk.bold('npm uninstall -g @ghl-ai/aw')}`,
183
231
  ].join('\n'));
184
232
  }
package/commands/pull.mjs CHANGED
@@ -28,28 +28,27 @@ export function pullCommand(args) {
28
28
  const silent = args['--silent'] === true || args._silent === true;
29
29
  const renameNamespace = args._renameNamespace || null;
30
30
 
31
- // Silent mode: suppress all output and exit cleanly on errors
32
- if (silent) {
33
- const origCancel = fmt.cancel;
34
- fmt.cancel = () => { process.exit(0); };
35
- fmt.logInfo = () => {};
36
- fmt.logSuccess = () => {};
37
- fmt.logStep = () => {};
38
- fmt.logWarn = () => {};
39
- fmt.logMessage = () => {};
40
- fmt.note = () => {};
41
- fmt.outro = () => {};
42
- fmt.spinner = () => ({ start: () => {}, stop: () => {} });
43
- }
31
+ // Silent mode: wrap fmt to suppress all output and exit cleanly on errors
32
+ const log = {
33
+ cancel: silent ? () => { process.exit(0); } : fmt.cancel,
34
+ logInfo: silent ? () => {} : fmt.logInfo,
35
+ logSuccess: silent ? () => {} : fmt.logSuccess,
36
+ logStep: silent ? () => {} : fmt.logStep,
37
+ logWarn: silent ? () => {} : fmt.logWarn,
38
+ logMessage: silent ? () => {} : fmt.logMessage,
39
+ note: silent ? () => {} : fmt.note,
40
+ outro: silent ? () => {} : fmt.outro,
41
+ spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
42
+ };
44
43
 
45
44
  // No args = re-pull everything in sync config
46
45
  if (!input) {
47
46
  const cfg = config.load(workspaceDir);
48
- if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
47
+ if (!cfg) log.cancel('No .sync-config.json found. Run: aw init');
49
48
  if (cfg.include.length === 0) {
50
- fmt.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
49
+ log.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
51
50
  }
52
- fmt.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
51
+ log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
53
52
  for (const p of cfg.include) {
54
53
  pullCommand({ ...args, _positional: [p], _skipIntegrate: true });
55
54
  }
@@ -65,7 +64,7 @@ export function pullCommand(args) {
65
64
  let pattern = resolved.registryPath;
66
65
 
67
66
  if (!pattern) {
68
- fmt.cancel(`Could not resolve "${input}" to a registry path`);
67
+ log.cancel(`Could not resolve "${input}" to a registry path`);
69
68
  }
70
69
 
71
70
  // Ensure workspace exists
@@ -76,11 +75,11 @@ export function pullCommand(args) {
76
75
  // Load config
77
76
  const cfg = config.load(workspaceDir);
78
77
  if (!cfg) {
79
- fmt.cancel('No .sync-config.json found. Run: aw init');
78
+ log.cancel('No .sync-config.json found. Run: aw init');
80
79
  }
81
80
 
82
81
  // Fetch from registry
83
- const s = fmt.spinner();
82
+ const s = log.spinner();
84
83
  s.start('Fetching from registry...');
85
84
 
86
85
  const sparsePaths = includeToSparsePaths([pattern]);
@@ -89,7 +88,7 @@ export function pullCommand(args) {
89
88
  tempDir = sparseCheckout(cfg.repo, sparsePaths);
90
89
  } catch (e) {
91
90
  s.stop(chalk.red('Fetch failed'));
92
- fmt.cancel(e.message);
91
+ log.cancel(e.message);
93
92
  }
94
93
 
95
94
  try {
@@ -104,8 +103,8 @@ export function pullCommand(args) {
104
103
 
105
104
  if (registryDirs.length === 0) {
106
105
  s.stop(chalk.red('Not found'));
107
- if (args._silent) return;
108
- fmt.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}`);
106
+ if (silent) return;
107
+ log.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}`);
109
108
  }
110
109
 
111
110
  // Rename namespace if requested (e.g., [template] → dev)
@@ -132,8 +131,8 @@ export function pullCommand(args) {
132
131
  if (!hasMatch) {
133
132
  s.stop(chalk.red('Not found'));
134
133
  cleanup(tempDir);
135
- if (args._silent) return;
136
- fmt.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}\n\n Check the pattern exists in the registry repo.`);
134
+ if (silent) return;
135
+ log.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}\n\n Check the pattern exists in the registry repo.`);
137
136
  }
138
137
 
139
138
  const fetched = registryDirs.map(d => chalk.cyan(d.name)).join(', ');
@@ -142,7 +141,7 @@ export function pullCommand(args) {
142
141
  // Add to config if not already there
143
142
  if (!cfg.include.includes(pattern)) {
144
143
  config.addPattern(workspaceDir, pattern);
145
- fmt.logSuccess(`Added ${chalk.cyan(pattern)} to config`);
144
+ log.logSuccess(`Added ${chalk.cyan(pattern)} to config`);
146
145
  }
147
146
 
148
147
  // Compute plan
@@ -156,7 +155,7 @@ export function pullCommand(args) {
156
155
  }
157
156
 
158
157
  // Apply
159
- const s2 = fmt.spinner();
158
+ const s2 = log.spinner();
160
159
  s2.start('Applying changes...');
161
160
  const conflictCount = applyActions(actions);
162
161
  updateManifest(workspaceDir, actions, cfg.namespace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {