@ghl-ai/aw 0.1.8 → 0.1.11
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 +1 -0
- package/commands/init.mjs +150 -122
- package/commands/link-project.mjs +26 -0
- package/commands/nuke.mjs +78 -213
- package/package.json +1 -1
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 —
|
|
1
|
+
// commands/init.mjs — Clean init: clone registry, link IDEs, git hooks for omnipresence.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
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,
|
|
14
|
-
import { homedir
|
|
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(
|
|
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
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
45
|
+
const hookPath = join(hooksDir, 'post-checkout');
|
|
46
|
+
writeFileSync(hookPath, POST_CHECKOUT_HOOK);
|
|
47
|
+
chmodSync(hookPath, '755');
|
|
72
48
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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: sync registry',
|
|
77
|
+
type: 'shell',
|
|
78
|
+
command: 'aw init --silent',
|
|
79
|
+
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
80
|
+
runOptions: { runOn: 'folderOpen' },
|
|
81
|
+
problemMatcher: [],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
|
|
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: sync registry' || 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) {
|
|
@@ -115,79 +127,95 @@ export async function initCommand(args) {
|
|
|
115
127
|
fmt.cancel("'ghl' is a reserved namespace — it is the shared platform layer");
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
130
|
+
const isExisting = config.exists(GLOBAL_AW_DIR);
|
|
131
|
+
const silent = args['--silent'] === true;
|
|
132
|
+
const cwd = process.cwd();
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
135
|
+
|
|
136
|
+
if (isExisting) {
|
|
137
|
+
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
138
|
+
|
|
139
|
+
// Pull latest
|
|
140
|
+
pullCommand({ ...args, _positional: [], _workspaceDir: GLOBAL_AW_DIR, '--silent': silent });
|
|
141
|
+
|
|
142
|
+
// Re-link IDE dirs (idempotent)
|
|
143
|
+
linkWorkspace(HOME);
|
|
144
|
+
generateCommands(HOME);
|
|
145
|
+
copyInstructions(HOME, null, namespace) || [];
|
|
146
|
+
initAwDocs(HOME);
|
|
147
|
+
setupMcp(HOME, namespace) || [];
|
|
148
|
+
|
|
149
|
+
// Link current project if needed
|
|
150
|
+
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
151
|
+
try {
|
|
152
|
+
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
153
|
+
if (!silent) fmt.logStep('Linked .aw_registry in current project');
|
|
154
|
+
} catch { /* best effort */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!silent) {
|
|
158
|
+
fmt.outro([
|
|
159
|
+
'Sync complete',
|
|
160
|
+
'',
|
|
161
|
+
` ${chalk.green('✓')} Registry updated`,
|
|
162
|
+
` ${chalk.green('✓')} IDE integration refreshed`,
|
|
163
|
+
cwd !== HOME && existsSync(join(cwd, '.aw_registry')) ? ` ${chalk.green('✓')} Current project linked` : null,
|
|
164
|
+
].filter(Boolean).join('\n'));
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
122
167
|
}
|
|
123
168
|
|
|
124
|
-
// ──
|
|
169
|
+
// ── Full init: first time setup ───────────────────────────────────────
|
|
125
170
|
|
|
171
|
+
// Auto-detect user
|
|
126
172
|
if (!user) {
|
|
127
173
|
try {
|
|
128
|
-
user = execSync('
|
|
129
|
-
} catch { /*
|
|
174
|
+
user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
175
|
+
} catch { /* git not configured */ }
|
|
130
176
|
}
|
|
131
177
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
if (!existsSync(GLOBAL_AW_DIR)) {
|
|
135
|
-
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
136
|
-
}
|
|
178
|
+
// Step 1: Create global source of truth
|
|
179
|
+
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
137
180
|
|
|
138
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace, user
|
|
181
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
|
|
139
182
|
|
|
140
183
|
fmt.note([
|
|
141
184
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
142
185
|
namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
143
186
|
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
144
|
-
`${chalk.dim('
|
|
187
|
+
`${chalk.dim('version:')} v${VERSION}`,
|
|
145
188
|
].filter(Boolean).join('\n'), 'Config created');
|
|
146
189
|
|
|
147
|
-
//
|
|
148
|
-
|
|
190
|
+
// Step 2: Pull registry content
|
|
149
191
|
pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: GLOBAL_AW_DIR });
|
|
150
192
|
|
|
151
193
|
if (namespace) {
|
|
152
194
|
pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
|
|
153
195
|
}
|
|
154
196
|
|
|
155
|
-
//
|
|
156
|
-
|
|
197
|
+
// Step 3: Link to global IDE dirs
|
|
157
198
|
linkWorkspace(HOME);
|
|
158
199
|
generateCommands(HOME);
|
|
159
200
|
const instructionFiles = copyInstructions(HOME, null, namespace) || [];
|
|
160
201
|
initAwDocs(HOME);
|
|
161
202
|
const mcpFiles = setupMcp(HOME, namespace) || [];
|
|
162
203
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
const hookTarget = installOmnipresenceHook();
|
|
166
|
-
|
|
167
|
-
// ── Step 5: Install auto-sync on new terminal ─────────────────────────
|
|
204
|
+
// Step 4: Git template hook (omnipresence)
|
|
205
|
+
const gitTemplateInstalled = installGitTemplate();
|
|
168
206
|
|
|
169
|
-
|
|
207
|
+
// Step 5: IDE auto-init tasks
|
|
208
|
+
installIdeTasks();
|
|
170
209
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
let daemonInstalled = false;
|
|
174
|
-
try {
|
|
175
|
-
daemonCommand({ _positional: ['install'], '--interval': '1h' });
|
|
176
|
-
daemonInstalled = true;
|
|
177
|
-
} catch { /* non-fatal */ }
|
|
178
|
-
|
|
179
|
-
// ── Step 7: Symlink in current directory if it's a git repo ───────────
|
|
180
|
-
|
|
181
|
-
const cwd = process.cwd();
|
|
182
|
-
if (cwd !== HOME && existsSync(join(cwd, '.git')) && !existsSync(join(cwd, '.aw_registry'))) {
|
|
210
|
+
// Step 6: Symlink in current directory if it's a git repo
|
|
211
|
+
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
183
212
|
try {
|
|
184
213
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
185
|
-
fmt.logStep(
|
|
214
|
+
fmt.logStep('Linked .aw_registry in current project');
|
|
186
215
|
} catch { /* best effort */ }
|
|
187
216
|
}
|
|
188
217
|
|
|
189
|
-
//
|
|
190
|
-
|
|
218
|
+
// Step 7: Write manifest for nuke cleanup
|
|
191
219
|
const manifest = {
|
|
192
220
|
version: 1,
|
|
193
221
|
installedAt: new Date().toISOString(),
|
|
@@ -196,23 +224,23 @@ export async function initCommand(args) {
|
|
|
196
224
|
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
197
225
|
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
198
226
|
],
|
|
199
|
-
|
|
200
|
-
shellMarkers: [CHPWD_MARKER, '# aw-auto-sync'],
|
|
201
|
-
daemon: daemonInstalled ? 'launchd' : null,
|
|
227
|
+
gitTemplate: gitTemplateInstalled ? GIT_TEMPLATE_DIR : null,
|
|
202
228
|
};
|
|
203
229
|
saveManifest(manifest);
|
|
204
230
|
|
|
205
|
-
//
|
|
206
|
-
|
|
231
|
+
// Done
|
|
207
232
|
fmt.outro([
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
'Install complete',
|
|
234
|
+
'',
|
|
210
235
|
` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
|
|
211
236
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
212
|
-
` ${chalk.green('✓')}
|
|
213
|
-
` ${chalk.green('✓')}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
` ${chalk.dim('
|
|
217
|
-
|
|
237
|
+
gitTemplateInstalled ? ` ${chalk.green('✓')} Git hook: new clones auto-link .aw_registry` : null,
|
|
238
|
+
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
|
239
|
+
cwd !== HOME && existsSync(join(cwd, '.aw_registry')) ? ` ${chalk.green('✓')} Linked in current project` : null,
|
|
240
|
+
'',
|
|
241
|
+
` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
|
|
242
|
+
` ${chalk.dim('New clones:')} auto-linked via git hook`,
|
|
243
|
+
` ${chalk.dim('Update:')} ${chalk.bold('aw init')} ${chalk.dim('(or auto on IDE open)')}`,
|
|
244
|
+
` ${chalk.dim('Uninstall:')} ${chalk.bold('aw nuke')}`,
|
|
245
|
+
].filter(Boolean).join('\n'));
|
|
218
246
|
}
|
|
@@ -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
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
255
|
-
const
|
|
256
|
-
if (existsSync(
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
120
|
+
// 2. Remove IDE symlinks (only those pointing to .aw_registry)
|
|
291
121
|
removeIdeSymlinks();
|
|
292
122
|
|
|
293
|
-
//
|
|
123
|
+
// 3. Remove .aw_registry symlinks from project directories
|
|
294
124
|
removeProjectSymlinks();
|
|
295
125
|
|
|
296
|
-
//
|
|
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: 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
|
+
}
|
|
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
|
-
//
|
|
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
|
-
'
|
|
173
|
+
'Fully removed',
|
|
309
174
|
'',
|
|
310
|
-
` ${chalk.green('✓')}
|
|
311
|
-
` ${chalk.green('✓')}
|
|
312
|
-
` ${chalk.green('✓')}
|
|
313
|
-
` ${chalk.green('✓')}
|
|
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.')}`,
|