@ghl-ai/aw 0.1.7 → 0.1.9

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
@@ -16,6 +16,7 @@ const COMMANDS = {
16
16
  drop: () => import('./commands/drop.mjs').then(m => m.dropCommand),
17
17
  status: () => import('./commands/status.mjs').then(m => m.statusCommand),
18
18
  search: () => import('./commands/search.mjs').then(m => m.searchCommand),
19
+ link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
19
20
  nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
20
21
  daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
21
22
  };
package/commands/init.mjs CHANGED
@@ -1,98 +1,110 @@
1
- // commands/init.mjs — True omnipresent init: one install, works everywhere
1
+ // commands/init.mjs — Clean init: clone registry, link IDEs, git hooks for omnipresence.
2
2
  //
3
- // How it works:
4
- // 1. Installs the source of truth at ~/.aw_registry/
5
- // 2. Links IDE config to ~/.claude/, ~/.cursor/, ~/.codex/
6
- // 3. Adds a chpwd hook to ~/.zshrc so every `cd` into a project
7
- // auto-symlinks .aw_registry ~/.aw_registry (omnipresence)
8
- // 4. Starts a background daemon for hourly silent pulls
9
- // 5. Writes a manifest of everything touched so `aw nuke` can do perfect cleanup
10
-
11
- import { mkdirSync, existsSync, writeFileSync, readFileSync, chmodSync, symlinkSync, lstatSync } from 'node:fs';
3
+ // No shell profile modifications. No daemons. No background processes.
4
+ // Uses git's built-in template hooks for automatic linking on clone/checkout.
5
+ // Uses IDE tasks for auto-pull on workspace open.
6
+
7
+ import { mkdirSync, existsSync, writeFileSync, symlinkSync, chmodSync } from 'node:fs';
12
8
  import { execSync } from 'node:child_process';
13
- import { join, resolve, relative } from 'node:path';
14
- import { homedir, platform } from 'node:os';
9
+ import { join, dirname } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { readFileSync } from 'node:fs';
15
13
  import * as config from '../config.mjs';
16
14
  import * as fmt from '../fmt.mjs';
17
15
  import { chalk } from '../fmt.mjs';
18
16
  import { pullCommand } from './pull.mjs';
19
17
  import { linkWorkspace } from '../link.mjs';
20
- import { daemonCommand } from './daemon.mjs';
21
18
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
22
19
  import { setupMcp } from '../mcp.mjs';
23
- import { readFileSync as readFileSyncPkg } from 'node:fs';
24
- import { dirname } from 'node:path';
25
- import { fileURLToPath } from 'node:url';
26
20
 
27
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
- const VERSION = JSON.parse(readFileSyncPkg(join(__dirname, '..', 'package.json'), 'utf8')).version;
22
+ const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
29
23
 
30
24
  const HOME = homedir();
31
25
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
32
26
  const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
27
+ const GIT_TEMPLATE_DIR = join(HOME, '.git-templates', 'aw');
33
28
 
34
- // Shell hook that auto-symlinks .aw_registry into every directory you cd into
35
- const CHPWD_MARKER = '# aw-omnipresence';
36
- const CHPWD_HOOK = `
37
- ${CHPWD_MARKER}
38
- _aw_chpwd() {
39
- # Only act inside git repos (has .git/) and skip home dir
40
- [ "$PWD" = "$HOME" ] && return
41
- [ ! -d ".git" ] && return
42
- # Skip if .aw_registry already exists (real dir or symlink)
43
- [ -e ".aw_registry" ] && return
44
- # Create symlink to global source of truth
45
- ln -s "$HOME/.aw_registry" ".aw_registry" 2>/dev/null
46
- }
47
- # Fire on every directory change (zsh)
48
- if [ -n "$ZSH_VERSION" ]; then
49
- autoload -U add-zsh-hook
50
- add-zsh-hook chpwd _aw_chpwd
29
+ // ── Git template hook for omnipresence ──────────────────────────────────
30
+ // Git's init.templateDir copies hooks into every new clone/init.
31
+ // post-checkout fires on clone and branch switch — perfect for auto-linking.
32
+
33
+ const POST_CHECKOUT_HOOK = `#!/bin/sh
34
+ # aw: auto-link registry on clone/checkout (installed by aw init)
35
+ AW_REGISTRY="$HOME/.aw_registry"
36
+ if [ -d "$AW_REGISTRY" ] && [ ! -e ".aw_registry" ] && [ -d ".git" ]; then
37
+ ln -s "$AW_REGISTRY" ".aw_registry" 2>/dev/null
51
38
  fi
52
- # Also fire on shell startup for the initial directory
53
- _aw_chpwd
54
39
  `;
55
40
 
56
- function installOmnipresenceHook() {
57
- const profiles = [];
58
- if (platform() === 'darwin') {
59
- profiles.push(join(HOME, '.zshrc'));
60
- profiles.push(join(HOME, '.zprofile'));
61
- }
62
- profiles.push(join(HOME, '.bashrc'));
63
- profiles.push(join(HOME, '.bash_profile'));
64
-
65
- const target = profiles.find(p => existsSync(p)) || join(HOME, '.zshrc');
66
- const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
41
+ function installGitTemplate() {
42
+ const hooksDir = join(GIT_TEMPLATE_DIR, 'hooks');
43
+ mkdirSync(hooksDir, { recursive: true });
67
44
 
68
- if (current.includes(CHPWD_MARKER)) {
69
- fmt.logStep(`Omnipresence hook already in ${target.replace(HOME, '~')}`);
70
- return target;
71
- }
45
+ const hookPath = join(hooksDir, 'post-checkout');
46
+ writeFileSync(hookPath, POST_CHECKOUT_HOOK);
47
+ chmodSync(hookPath, '755');
72
48
 
73
- writeFileSync(target, current + CHPWD_HOOK);
74
- fmt.logStep(`Omnipresence hook added to ${target.replace(HOME, '~')}`);
75
- fmt.logStep(chalk.dim(' Every cd into a git repo auto-links .aw_registry'));
76
- return target;
49
+ // Set git global template dir (merges with existing hooks)
50
+ try {
51
+ const current = execSync('git config --global init.templateDir', {
52
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
53
+ }).trim();
54
+
55
+ if (current && current !== GIT_TEMPLATE_DIR) {
56
+ // Another template dir is set — don't override, just inform
57
+ fmt.logWarn(`Git template dir already set to ${current}`);
58
+ fmt.logStep(chalk.dim(' Skipping git template — run: git config --global init.templateDir ~/.git-templates/aw'));
59
+ return false;
60
+ }
61
+ } catch { /* not set yet — good */ }
62
+
63
+ execSync(`git config --global init.templateDir "${GIT_TEMPLATE_DIR}"`, { stdio: 'pipe' });
64
+ fmt.logStep('Git template hook installed (auto-links on clone)');
65
+ return true;
77
66
  }
78
67
 
79
- function installAutoSync() {
80
- const profiles = [];
81
- if (platform() === 'darwin') {
82
- profiles.push(join(HOME, '.zshrc'));
83
- }
84
- profiles.push(join(HOME, '.bashrc'));
85
-
86
- const marker = '# aw-auto-sync';
87
- const snippet = `\n${marker}\n[ -f "$HOME/.aw_registry/.sync-config.json" ] && command -v aw >/dev/null 2>&1 && (cd "$HOME" && aw pull --silent) &\n`;
88
-
89
- const target = profiles.find(p => existsSync(p)) || join(HOME, '.zshrc');
90
- const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
91
-
92
- if (current.includes(marker)) return;
68
+ // ── IDE tasks for auto-pull ─────────────────────────────────────────────
69
+
70
+ function installIdeTasks() {
71
+ // VS Code / Cursor task — runs aw pull on folder open
72
+ const vscodeTask = {
73
+ version: '2.0.0',
74
+ tasks: [
75
+ {
76
+ label: 'aw: pull registry',
77
+ type: 'shell',
78
+ command: 'aw pull',
79
+ presentation: { reveal: 'silent', panel: 'shared', close: true },
80
+ runOptions: { runOn: 'folderOpen' },
81
+ problemMatcher: [],
82
+ },
83
+ ],
84
+ };
93
85
 
94
- writeFileSync(target, current + snippet);
95
- fmt.logStep('Auto-sync on new terminal (background, zero delay)');
86
+ // Install globally for VS Code and Cursor
87
+ for (const ide of ['Code', 'Cursor']) {
88
+ const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
89
+ if (!existsSync(userDir)) continue;
90
+
91
+ const tasksPath = join(userDir, 'tasks.json');
92
+ if (existsSync(tasksPath)) {
93
+ // Don't override existing tasks — check if aw task already there
94
+ try {
95
+ const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
96
+ if (existing.tasks?.some(t => t.label === 'aw: pull registry')) continue;
97
+ // Add our task to existing
98
+ existing.tasks = existing.tasks || [];
99
+ existing.tasks.push(vscodeTask.tasks[0]);
100
+ writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
101
+ fmt.logStep(`Added auto-pull task to ${ide}`);
102
+ } catch { /* corrupted tasks.json, skip */ }
103
+ } else {
104
+ writeFileSync(tasksPath, JSON.stringify(vscodeTask, null, 2) + '\n');
105
+ fmt.logStep(`Added auto-pull task to ${ide}`);
106
+ }
107
+ }
96
108
  }
97
109
 
98
110
  function saveManifest(data) {
@@ -125,8 +137,8 @@ export async function initCommand(args) {
125
137
 
126
138
  if (!user) {
127
139
  try {
128
- user = execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
129
- } catch { /* gh not available */ }
140
+ user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
141
+ } catch { /* git not configured */ }
130
142
  }
131
143
 
132
144
  // ── Step 1: Create global source of truth ─────────────────────────────
@@ -135,13 +147,13 @@ export async function initCommand(args) {
135
147
  mkdirSync(GLOBAL_AW_DIR, { recursive: true });
136
148
  }
137
149
 
138
- const cfg = config.create(GLOBAL_AW_DIR, { namespace, user, scope: 'omnipresent' });
150
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
139
151
 
140
152
  fmt.note([
141
153
  `${chalk.dim('source:')} ~/.aw_registry/`,
142
154
  namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
143
155
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
144
- `${chalk.dim('mode:')} omnipresent — works in every project`,
156
+ `${chalk.dim('version:')} v${VERSION}`,
145
157
  ].filter(Boolean).join('\n'), 'Config created');
146
158
 
147
159
  // ── Step 2: Pull registry content ─────────────────────────────────────
@@ -160,33 +172,25 @@ export async function initCommand(args) {
160
172
  initAwDocs(HOME);
161
173
  const mcpFiles = setupMcp(HOME, namespace) || [];
162
174
 
163
- // ── Step 4: Install omnipresence hook (chpwd) ─────────────────────────
164
-
165
- const hookTarget = installOmnipresenceHook();
175
+ // ── Step 4: Git template hook (omnipresence) ──────────────────────────
166
176
 
167
- // ── Step 5: Install auto-sync on new terminal ─────────────────────────
177
+ const gitTemplateInstalled = installGitTemplate();
168
178
 
169
- installAutoSync();
179
+ // ── Step 5: IDE auto-pull tasks ───────────────────────────────────────
170
180
 
171
- // ── Step 6: Start background daemon ───────────────────────────────────
172
-
173
- let daemonInstalled = false;
174
- try {
175
- daemonCommand({ _positional: ['install'], '--interval': '1h' });
176
- daemonInstalled = true;
177
- } catch { /* non-fatal */ }
181
+ installIdeTasks();
178
182
 
179
- // ── Step 7: Symlink in current directory if it's a git repo ───────────
183
+ // ── Step 6: Symlink in current directory if it's a git repo ───────────
180
184
 
181
185
  const cwd = process.cwd();
182
186
  if (cwd !== HOME && existsSync(join(cwd, '.git')) && !existsSync(join(cwd, '.aw_registry'))) {
183
187
  try {
184
188
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
185
- fmt.logStep(`Linked .aw_registry in current project`);
189
+ fmt.logStep('Linked .aw_registry in current project');
186
190
  } catch { /* best effort */ }
187
191
  }
188
192
 
189
- // ── Step 8: Write manifest for perfect nuke cleanup ───────────────────
193
+ // ── Step 7: Write manifest for nuke cleanup ───────────────────────────
190
194
 
191
195
  const manifest = {
192
196
  version: 1,
@@ -196,23 +200,24 @@ export async function initCommand(args) {
196
200
  ...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
197
201
  ...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
198
202
  ],
199
- shellProfile: hookTarget,
200
- shellMarkers: [CHPWD_MARKER, '# aw-auto-sync'],
201
- daemon: daemonInstalled ? 'launchd' : null,
203
+ gitTemplate: gitTemplateInstalled ? GIT_TEMPLATE_DIR : null,
202
204
  };
203
205
  saveManifest(manifest);
204
206
 
205
207
  // ── Done ──────────────────────────────────────────────────────────────
206
208
 
207
209
  fmt.outro([
208
- `Omnipresent install complete`,
209
- ``,
210
+ 'Install complete',
211
+ '',
210
212
  ` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
211
213
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
212
- ` ${chalk.green('✓')} Auto-symlink: every git repo you cd into gets .aw_registry`,
213
- ` ${chalk.green('✓')} Auto-sync: background daemon + new terminal pull`,
214
- ``,
215
- ` ${chalk.dim('Open any project in any IDE — AW is already there.')}`,
216
- ` ${chalk.dim('To undo everything:')} ${chalk.bold('aw nuke')}`,
217
- ].join('\n'));
214
+ gitTemplateInstalled ? ` ${chalk.green('✓')} Git hook: new clones auto-link .aw_registry` : null,
215
+ ` ${chalk.green('✓')} IDE task: auto-pull on workspace open`,
216
+ cwd !== HOME && existsSync(join(cwd, '.aw_registry')) ? ` ${chalk.green('✓')} Linked in current project` : null,
217
+ '',
218
+ ` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
219
+ ` ${chalk.dim('New clones:')} auto-linked via git hook`,
220
+ ` ${chalk.dim('Update:')} ${chalk.bold('aw pull')} ${chalk.dim('(or auto on IDE open)')}`,
221
+ ` ${chalk.dim('Uninstall:')} ${chalk.bold('aw nuke')}`,
222
+ ].filter(Boolean).join('\n'));
218
223
  }
@@ -0,0 +1,26 @@
1
+ // commands/link-project.mjs — Symlink ~/.aw_registry into current project
2
+
3
+ import { existsSync, symlinkSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { homedir } from 'node:os';
6
+ import * as fmt from '../fmt.mjs';
7
+ import { chalk } from '../fmt.mjs';
8
+
9
+ const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
10
+
11
+ export function linkProjectCommand(args) {
12
+ const cwd = process.cwd();
13
+ const target = join(cwd, '.aw_registry');
14
+
15
+ if (!existsSync(GLOBAL_AW_DIR)) {
16
+ fmt.cancel('No global install found. Run: aw init');
17
+ }
18
+
19
+ if (existsSync(target)) {
20
+ fmt.logSuccess('.aw_registry already linked in this project');
21
+ return;
22
+ }
23
+
24
+ symlinkSync(GLOBAL_AW_DIR, target);
25
+ fmt.logSuccess(`Linked .aw_registry → ~/.aw_registry/`);
26
+ }
package/commands/nuke.mjs CHANGED
@@ -1,20 +1,17 @@
1
- // commands/nuke.mjs — Safe omnipresent cleanup: reads manifest, removes only what AW created
1
+ // commands/nuke.mjs — Safe cleanup: reads manifest, removes only what AW created
2
2
  //
3
3
  // Safety guarantee: NEVER deletes files that AW didn't create.
4
- // Uses .aw-manifest.json (written by init) to know exactly what to remove.
5
4
 
6
5
  import { join } from 'node:path';
7
- import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync, writeFileSync, readlinkSync } from 'node:fs';
6
+ import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync, readlinkSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
8
  import { execSync } from 'node:child_process';
9
- import { homedir, platform } from 'node:os';
10
9
  import * as fmt from '../fmt.mjs';
11
10
  import { chalk } from '../fmt.mjs';
12
11
 
13
12
  const HOME = homedir();
14
13
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
15
14
  const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
16
- const DAEMON_LABEL = 'ai.ghl.aw.pull';
17
- const PLIST_PATH = join(HOME, 'Library', 'LaunchAgents', `${DAEMON_LABEL}.plist`);
18
15
 
19
16
  // IDE dirs where AW creates symlinks
20
17
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
@@ -27,242 +24,82 @@ function loadManifest() {
27
24
  } catch { return null; }
28
25
  }
29
26
 
30
- // ── Step 1: Stop and remove daemon ──────────────────────────────────────────
31
-
32
- function removeDaemon(manifest) {
33
- const isMac = platform() === 'darwin';
34
-
35
- if (isMac) {
36
- if (existsSync(PLIST_PATH)) {
37
- try { execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch { /* not loaded */ }
38
- unlinkSync(PLIST_PATH);
39
- fmt.logStep('Removed launchd daemon');
40
- }
41
- } else {
42
- // Linux: remove cron line with # aw-daemon marker
43
- try {
44
- const current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
45
- if (current.includes('# aw-daemon')) {
46
- const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
47
- execSync(`echo ${JSON.stringify(cleaned)} | crontab -`);
48
- fmt.logStep('Removed cron job');
49
- }
50
- } catch { /* no crontab */ }
51
- }
52
- }
53
-
54
- // ── Step 2: Remove shell hooks (by marker, surgically) ──────────────────────
55
-
56
- function removeShellHooks(manifest) {
57
- const markers = manifest?.shellMarkers || ['# aw-omnipresence', '# aw-auto-sync'];
58
- const profilePath = manifest?.shellProfile;
59
-
60
- // Find all shell profiles that might have our hooks
61
- const profiles = new Set();
62
- if (profilePath && existsSync(profilePath)) profiles.add(profilePath);
63
- for (const p of ['.zshrc', '.zprofile', '.bashrc', '.bash_profile']) {
64
- const full = join(HOME, p);
65
- if (existsSync(full)) profiles.add(full);
66
- }
67
-
68
- for (const profile of profiles) {
69
- let content;
70
- try { content = readFileSync(profile, 'utf8'); } catch { continue; }
71
-
72
- let modified = false;
73
- for (const marker of markers) {
74
- if (!content.includes(marker)) continue;
75
-
76
- // Remove the block: from marker line to next blank line or end
77
- const lines = content.split('\n');
78
- const result = [];
79
- let skipping = false;
80
-
81
- for (let i = 0; i < lines.length; i++) {
82
- if (lines[i].includes(marker)) {
83
- skipping = true;
84
- modified = true;
85
- continue;
86
- }
87
- if (skipping) {
88
- // Stop skipping at a blank line (end of block)
89
- if (lines[i].trim() === '') {
90
- skipping = false;
91
- // Don't add the trailing blank line of the block
92
- continue;
93
- }
94
- continue; // skip this line (part of the hook block)
95
- }
96
- result.push(lines[i]);
97
- }
98
- content = result.join('\n');
99
- }
100
-
101
- if (modified) {
102
- // Clean up trailing newlines (max 1)
103
- content = content.replace(/\n{3,}$/g, '\n');
104
- writeFileSync(profile, content);
105
- fmt.logStep(`Cleaned hooks from ${profile.replace(HOME, '~')}`);
106
- }
107
- }
108
- }
109
-
110
- // ── Step 3: Remove AW-created files (from manifest) ─────────────────────────
111
-
27
+ // Remove AW-created files listed in manifest
112
28
  function removeCreatedFiles(manifest) {
113
29
  if (!manifest?.createdFiles?.length) return;
114
-
115
30
  let removed = 0;
116
31
  for (const rel of manifest.createdFiles) {
117
32
  const p = join(HOME, rel);
118
33
  try {
119
- if (existsSync(p)) {
120
- rmSync(p);
121
- removed++;
122
- }
34
+ if (existsSync(p)) { rmSync(p); removed++; }
123
35
  } catch { /* best effort */ }
124
36
  }
125
- if (removed > 0) {
126
- fmt.logStep(`Removed ${removed} generated file${removed > 1 ? 's' : ''}`);
127
- }
37
+ if (removed > 0) fmt.logStep(`Removed ${removed} generated file${removed > 1 ? 's' : ''}`);
128
38
  }
129
39
 
130
- // ── Step 4: Remove AW symlinks from IDE dirs ────────────────────────────────
131
-
40
+ // Remove symlinks from IDE dirs that point into .aw_registry
132
41
  function removeIdeSymlinks() {
133
42
  let removed = 0;
134
43
 
135
44
  for (const ide of IDE_DIRS) {
136
- for (const type of CONTENT_TYPES) {
45
+ for (const type of [...CONTENT_TYPES, 'commands/aw', 'commands/ghl']) {
137
46
  const dir = join(HOME, ide, type);
138
47
  if (!existsSync(dir)) continue;
139
48
 
140
49
  for (const entry of readdirSync(dir)) {
141
50
  const p = join(dir, entry);
142
51
  try {
143
- const stat = lstatSync(p);
144
- if (stat.isSymbolicLink()) {
52
+ if (lstatSync(p).isSymbolicLink()) {
145
53
  const target = readlinkSync(p);
146
- // Only remove if it points into .aw_registry
147
54
  if (target.includes('.aw_registry') || target.includes('aw_registry')) {
148
- unlinkSync(p);
149
- removed++;
55
+ unlinkSync(p); removed++;
150
56
  }
151
57
  }
152
58
  } catch { /* best effort */ }
153
59
  }
154
- }
155
-
156
- // Remove commands/aw/ dir (generated commands) and commands/ghl/ (generated platform commands)
157
- for (const sub of ['commands/aw', 'commands/ghl']) {
158
- const dir = join(HOME, ide, sub);
159
- if (!existsSync(dir)) continue;
160
- try {
161
- // Only remove if contents are symlinks to .aw_registry or generated by AW
162
- const entries = readdirSync(dir);
163
- for (const entry of entries) {
164
- const p = join(dir, entry);
165
- const stat = lstatSync(p);
166
- if (stat.isSymbolicLink()) {
167
- unlinkSync(p);
168
- removed++;
169
- }
170
- }
171
- // Remove dir if now empty
172
- if (readdirSync(dir).length === 0) rmSync(dir);
173
- } catch { /* best effort */ }
60
+ // Remove dir if now empty
61
+ try { if (readdirSync(dir).length === 0) rmSync(dir); } catch {}
174
62
  }
175
63
  }
176
64
 
177
- // Remove .agents/skills/ symlinks
178
- const agentsSkills = join(HOME, '.agents', 'skills');
179
- if (existsSync(agentsSkills)) {
180
- try {
181
- for (const entry of readdirSync(agentsSkills)) {
182
- const p = join(agentsSkills, entry);
183
- if (lstatSync(p).isSymbolicLink()) {
184
- const target = readlinkSync(p);
185
- if (target.includes('.aw_registry') || target.includes('aw_registry')) {
186
- unlinkSync(p);
187
- removed++;
188
- }
189
- }
190
- }
191
- if (readdirSync(agentsSkills).length === 0) rmSync(agentsSkills);
192
- const agentsDir = join(HOME, '.agents');
193
- if (existsSync(agentsDir) && readdirSync(agentsDir).length === 0) rmSync(agentsDir);
194
- } catch { /* best effort */ }
195
- }
196
-
197
- if (removed > 0) {
198
- fmt.logStep(`Removed ${removed} IDE symlink${removed > 1 ? 's' : ''}`);
199
- }
65
+ if (removed > 0) fmt.logStep(`Removed ${removed} IDE symlink${removed > 1 ? 's' : ''}`);
200
66
  }
201
67
 
202
- // ── Step 5: Find and remove .aw_registry symlinks from project dirs ─────────
203
-
68
+ // Find and remove .aw_registry symlinks from project directories
204
69
  function removeProjectSymlinks() {
205
- // Check common project locations for .aw_registry symlinks
206
- // We look in the current directory + scan known parent dirs
207
70
  let removed = 0;
71
+ const dirsToCheck = new Set([process.cwd()]);
208
72
 
209
- const dirsToCheck = new Set();
210
- dirsToCheck.add(process.cwd());
211
-
212
- // Also check the home directory's immediate children (common project roots)
213
- try {
214
- for (const entry of readdirSync(HOME)) {
215
- if (entry.startsWith('.')) continue; // skip dotfiles
216
- const p = join(HOME, entry);
217
- try {
218
- if (lstatSync(p).isDirectory()) {
219
- // Check this dir and its immediate children
220
- dirsToCheck.add(p);
221
- try {
222
- for (const sub of readdirSync(p)) {
223
- const sp = join(p, sub);
224
- try { if (lstatSync(sp).isDirectory()) dirsToCheck.add(sp); } catch {}
225
- }
226
- } catch {}
227
- }
228
- } catch {}
229
- }
230
- } catch {}
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));
80
+ }
81
+ } catch {}
82
+ }
231
83
 
232
84
  for (const dir of dirsToCheck) {
233
85
  const link = join(dir, '.aw_registry');
234
86
  try {
235
- if (lstatSync(link).isSymbolicLink()) {
236
- unlinkSync(link);
237
- removed++;
238
- }
87
+ if (lstatSync(link).isSymbolicLink()) { unlinkSync(link); removed++; }
239
88
  } catch { /* doesn't exist or not a symlink */ }
240
89
  }
241
90
 
242
- if (removed > 0) {
243
- fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
244
- }
91
+ if (removed > 0) fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
245
92
  }
246
93
 
247
- // ── Main ─────────────────────────────────────────────────────────────────────
248
-
249
94
  export function nukeCommand(args) {
250
95
  fmt.intro('aw nuke');
251
96
 
252
- // Check if omnipresent install exists
253
97
  if (!existsSync(GLOBAL_AW_DIR)) {
254
- // Fallback: check cwd for project-scope install
255
- const localAw = join(process.cwd(), '.aw_registry');
256
- if (existsSync(localAw)) {
257
- try {
258
- if (lstatSync(localAw).isSymbolicLink()) {
259
- unlinkSync(localAw);
260
- fmt.logSuccess('Removed local .aw_registry symlink');
261
- } else {
262
- rmSync(localAw, { recursive: true, force: true });
263
- fmt.logSuccess('Removed local .aw_registry/');
264
- }
265
- } catch {}
98
+ // Check cwd for local symlink
99
+ const local = join(process.cwd(), '.aw_registry');
100
+ if (existsSync(local) && lstatSync(local).isSymbolicLink()) {
101
+ unlinkSync(local);
102
+ fmt.logSuccess('Removed local .aw_registry symlink');
266
103
  fmt.outro('Done');
267
104
  return;
268
105
  }
@@ -275,42 +112,70 @@ export function nukeCommand(args) {
275
112
  fmt.note([
276
113
  `${chalk.dim('source:')} ~/.aw_registry/`,
277
114
  manifest ? `${chalk.dim('installed:')} ${manifest.installedAt}` : null,
278
- manifest?.daemon ? `${chalk.dim('daemon:')} ${manifest.daemon}` : null,
279
- ].filter(Boolean).join('\n'), 'Cleaning up omnipresent install');
280
-
281
- // 1. Stop daemon
282
- removeDaemon(manifest);
115
+ ].filter(Boolean).join('\n'), 'Cleaning up');
283
116
 
284
- // 2. Remove shell hooks
285
- removeShellHooks(manifest);
286
-
287
- // 3. Remove AW-created files (instruction files, MCP configs, etc.)
117
+ // 1. Remove AW-created files (instruction files, MCP configs)
288
118
  removeCreatedFiles(manifest);
289
119
 
290
- // 4. Remove IDE symlinks (only those pointing to .aw_registry)
120
+ // 2. Remove IDE symlinks (only those pointing to .aw_registry)
291
121
  removeIdeSymlinks();
292
122
 
293
- // 5. Remove .aw_registry symlinks from project directories
123
+ // 3. Remove .aw_registry symlinks from project directories
294
124
  removeProjectSymlinks();
295
125
 
296
- // 6. Remove .aw_docs/ from home
126
+ // 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
+ }
141
+
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: 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
+ }
160
+
161
+ // 7. Remove ~/.aw_docs/
297
162
  const awDocs = join(HOME, '.aw_docs');
298
163
  if (existsSync(awDocs)) {
299
164
  rmSync(awDocs, { recursive: true, force: true });
300
165
  fmt.logStep('Removed ~/.aw_docs/');
301
166
  }
302
167
 
303
- // 7. Remove ~/.aw_registry/ itself (the source of truth)
168
+ // 8. Remove ~/.aw_registry/ itself
304
169
  rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
305
170
  fmt.logStep('Removed ~/.aw_registry/');
306
171
 
307
172
  fmt.outro([
308
- 'Omnipresent install fully removed',
173
+ 'Fully removed',
309
174
  '',
310
- ` ${chalk.green('✓')} Daemon stopped`,
311
- ` ${chalk.green('✓')} Shell hooks cleaned`,
312
- ` ${chalk.green('✓')} IDE symlinks removed`,
313
- ` ${chalk.green('✓')} Project symlinks removed`,
175
+ ` ${chalk.green('✓')} IDE symlinks cleaned`,
176
+ ` ${chalk.green('✓')} Project symlinks cleaned`,
177
+ ` ${chalk.green('✓')} Git template hook removed`,
178
+ ` ${chalk.green('✓')} IDE auto-pull tasks removed`,
314
179
  ` ${chalk.green('✓')} Source of truth deleted`,
315
180
  '',
316
181
  ` ${chalk.dim('No existing files were touched.')}`,
package/commands/pull.mjs CHANGED
@@ -25,8 +25,23 @@ export function pullCommand(args) {
25
25
  const workspaceDir = args._workspaceDir || (existsSync(join(localDir, '.sync-config.json')) ? localDir : GLOBAL_AW_DIR);
26
26
  const dryRun = args['--dry-run'] === true;
27
27
  const verbose = args['-v'] === true || args['--verbose'] === true;
28
+ const silent = args['--silent'] === true || args._silent === true;
28
29
  const renameNamespace = args._renameNamespace || null;
29
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
+ }
44
+
30
45
  // No args = re-pull everything in sync config
31
46
  if (!input) {
32
47
  const cfg = config.load(workspaceDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {