@ghl-ai/aw 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +5 -0
- package/commands/daemon.mjs +192 -0
- package/commands/init.mjs +160 -115
- package/commands/nuke.mjs +280 -52
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const COMMANDS = {
|
|
|
13
13
|
status: () => import('./commands/status.mjs').then(m => m.statusCommand),
|
|
14
14
|
search: () => import('./commands/search.mjs').then(m => m.searchCommand),
|
|
15
15
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
16
|
+
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
function parseArgs(argv) {
|
|
@@ -85,6 +86,10 @@ function printHelp() {
|
|
|
85
86
|
cmd('aw status', 'Show synced paths, modified files & conflicts'),
|
|
86
87
|
cmd('aw drop <path>', 'Stop syncing or delete local content'),
|
|
87
88
|
cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
|
|
89
|
+
cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
|
|
90
|
+
cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
|
|
91
|
+
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
92
|
+
cmd('aw daemon status', 'Check if daemon is running'),
|
|
88
93
|
|
|
89
94
|
sec('Examples'),
|
|
90
95
|
'',
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// commands/daemon.mjs — Install/uninstall a launchd (macOS) or cron (Linux) job
|
|
2
|
+
// that silently runs `aw pull` on a schedule without any user interaction.
|
|
3
|
+
|
|
4
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { homedir, platform } from 'node:os';
|
|
8
|
+
import * as fmt from '../fmt.mjs';
|
|
9
|
+
import { chalk } from '../fmt.mjs';
|
|
10
|
+
|
|
11
|
+
const LABEL = 'ai.ghl.aw.pull';
|
|
12
|
+
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
13
|
+
const DEFAULT_INTERVAL = 3600; // 1 hour in seconds
|
|
14
|
+
|
|
15
|
+
function getAwBin() {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('which aw', { encoding: 'utf8' }).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return 'aw';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── macOS: launchd ──────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function installLaunchd(intervalSeconds) {
|
|
26
|
+
const awBin = getAwBin();
|
|
27
|
+
const logDir = join(homedir(), '.aw_registry', 'logs');
|
|
28
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
32
|
+
<plist version="1.0">
|
|
33
|
+
<dict>
|
|
34
|
+
<key>Label</key>
|
|
35
|
+
<string>${LABEL}</string>
|
|
36
|
+
|
|
37
|
+
<key>ProgramArguments</key>
|
|
38
|
+
<array>
|
|
39
|
+
<string>${awBin}</string>
|
|
40
|
+
<string>pull</string>
|
|
41
|
+
<string>--silent</string>
|
|
42
|
+
</array>
|
|
43
|
+
|
|
44
|
+
<key>StartInterval</key>
|
|
45
|
+
<integer>${intervalSeconds}</integer>
|
|
46
|
+
|
|
47
|
+
<key>RunAtLoad</key>
|
|
48
|
+
<true/>
|
|
49
|
+
|
|
50
|
+
<key>StandardOutPath</key>
|
|
51
|
+
<string>${logDir}/pull.log</string>
|
|
52
|
+
|
|
53
|
+
<key>StandardErrorPath</key>
|
|
54
|
+
<string>${logDir}/pull-error.log</string>
|
|
55
|
+
|
|
56
|
+
<key>EnvironmentVariables</key>
|
|
57
|
+
<dict>
|
|
58
|
+
<key>PATH</key>
|
|
59
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
60
|
+
</dict>
|
|
61
|
+
</dict>
|
|
62
|
+
</plist>`;
|
|
63
|
+
|
|
64
|
+
writeFileSync(PLIST_PATH, plist);
|
|
65
|
+
|
|
66
|
+
// Unload first if already loaded (to apply new interval)
|
|
67
|
+
try { execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch { /* not loaded */ }
|
|
68
|
+
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
69
|
+
|
|
70
|
+
fmt.logStep(`Daemon installed — runs every ${formatInterval(intervalSeconds)}`);
|
|
71
|
+
fmt.logStep(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function uninstallLaunchd() {
|
|
75
|
+
if (!existsSync(PLIST_PATH)) {
|
|
76
|
+
fmt.logStep('No daemon installed.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try { execSync(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
|
|
80
|
+
unlinkSync(PLIST_PATH);
|
|
81
|
+
fmt.logStep('Daemon removed.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function statusLaunchd() {
|
|
85
|
+
if (!existsSync(PLIST_PATH)) return null;
|
|
86
|
+
try {
|
|
87
|
+
const out = execSync(`launchctl list ${LABEL} 2>/dev/null`, { encoding: 'utf8' });
|
|
88
|
+
const pid = out.match(/"PID"\s*=\s*(\d+)/)?.[1];
|
|
89
|
+
const lastExit = out.match(/"LastExitStatus"\s*=\s*(\d+)/)?.[1];
|
|
90
|
+
return { running: !!pid, pid, lastExit };
|
|
91
|
+
} catch {
|
|
92
|
+
return { running: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Linux: crontab ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function toCronExpression(intervalSeconds) {
|
|
99
|
+
const minutes = Math.round(intervalSeconds / 60);
|
|
100
|
+
if (minutes < 60) return `*/${minutes} * * * *`;
|
|
101
|
+
const hours = Math.round(minutes / 60);
|
|
102
|
+
return `0 */${hours} * * *`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function installCron(intervalSeconds) {
|
|
106
|
+
const awBin = getAwBin();
|
|
107
|
+
const logDir = join(homedir(), '.aw_registry', 'logs');
|
|
108
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const cronExpr = toCronExpression(intervalSeconds);
|
|
111
|
+
const cronLine = `${cronExpr} ${awBin} pull --silent >> ${logDir}/pull.log 2>&1 # aw-daemon`;
|
|
112
|
+
|
|
113
|
+
let current = '';
|
|
114
|
+
try { current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { /* empty */ }
|
|
115
|
+
|
|
116
|
+
// Remove old aw-daemon line if present
|
|
117
|
+
const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
|
|
118
|
+
const updated = cleaned.trimEnd() + '\n' + cronLine + '\n';
|
|
119
|
+
|
|
120
|
+
execSync(`echo ${JSON.stringify(updated)} | crontab -`);
|
|
121
|
+
fmt.logStep(`Cron job installed — runs every ${formatInterval(intervalSeconds)}`);
|
|
122
|
+
fmt.logStep(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function uninstallCron() {
|
|
126
|
+
let current = '';
|
|
127
|
+
try { current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { return; }
|
|
128
|
+
const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
|
|
129
|
+
execSync(`echo ${JSON.stringify(cleaned)} | crontab -`);
|
|
130
|
+
fmt.logStep('Cron job removed.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function formatInterval(seconds) {
|
|
136
|
+
if (seconds < 3600) return `${seconds / 60} minutes`;
|
|
137
|
+
if (seconds === 3600) return '1 hour';
|
|
138
|
+
return `${seconds / 3600} hours`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseInterval(str) {
|
|
142
|
+
if (!str) return DEFAULT_INTERVAL;
|
|
143
|
+
const n = parseInt(str);
|
|
144
|
+
if (str.endsWith('m')) return n * 60;
|
|
145
|
+
if (str.endsWith('h')) return n * 3600;
|
|
146
|
+
return n; // assume seconds
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export function daemonCommand(args) {
|
|
152
|
+
const subcommand = args._positional[0] || 'install';
|
|
153
|
+
const interval = parseInterval(args['--interval'] || args._positional[1]);
|
|
154
|
+
const isMac = platform() === 'darwin';
|
|
155
|
+
|
|
156
|
+
fmt.intro(`aw daemon ${subcommand}`);
|
|
157
|
+
|
|
158
|
+
if (subcommand === 'install') {
|
|
159
|
+
fmt.logStep(`Platform: ${isMac ? 'macOS (launchd)' : 'Linux (cron)'}`);
|
|
160
|
+
fmt.logStep(`Interval: every ${formatInterval(interval)}`);
|
|
161
|
+
if (isMac) installLaunchd(interval);
|
|
162
|
+
else installCron(interval);
|
|
163
|
+
fmt.outro(`aw pull will run silently every ${formatInterval(interval)}`);
|
|
164
|
+
|
|
165
|
+
} else if (subcommand === 'uninstall' || subcommand === 'stop') {
|
|
166
|
+
if (isMac) uninstallLaunchd();
|
|
167
|
+
else uninstallCron();
|
|
168
|
+
fmt.outro('Daemon stopped.');
|
|
169
|
+
|
|
170
|
+
} else if (subcommand === 'status') {
|
|
171
|
+
if (isMac) {
|
|
172
|
+
const s = statusLaunchd();
|
|
173
|
+
if (!s) {
|
|
174
|
+
console.log(chalk.dim(' Not installed'));
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` ${s.running ? chalk.green('● running') : chalk.dim('○ not running')} PID: ${s.pid || '—'} Last exit: ${s.lastExit || '—'}`);
|
|
177
|
+
console.log(` Plist: ${chalk.dim(PLIST_PATH)}`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
try {
|
|
181
|
+
const cron = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
182
|
+
const line = cron.split('\n').find(l => l.includes('# aw-daemon'));
|
|
183
|
+
console.log(line ? chalk.green(' ● installed: ') + chalk.dim(line) : chalk.dim(' Not installed'));
|
|
184
|
+
} catch {
|
|
185
|
+
console.log(chalk.dim(' Not installed'));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} else {
|
|
190
|
+
fmt.cancel(`Unknown subcommand: ${subcommand}. Use install / uninstall / status`);
|
|
191
|
+
}
|
|
192
|
+
}
|
package/commands/init.mjs
CHANGED
|
@@ -1,88 +1,106 @@
|
|
|
1
|
-
// commands/init.mjs —
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// commands/init.mjs — True omnipresent init: one install, works everywhere
|
|
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';
|
|
4
12
|
import { execSync } from 'node:child_process';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
7
|
-
import { select, isCancel } from '@clack/prompts';
|
|
13
|
+
import { join, resolve, relative } from 'node:path';
|
|
14
|
+
import { homedir, platform } from 'node:os';
|
|
8
15
|
import * as config from '../config.mjs';
|
|
9
16
|
import * as fmt from '../fmt.mjs';
|
|
10
17
|
import { chalk } from '../fmt.mjs';
|
|
11
18
|
import { pullCommand } from './pull.mjs';
|
|
12
19
|
import { linkWorkspace } from '../link.mjs';
|
|
20
|
+
import { daemonCommand } from './daemon.mjs';
|
|
13
21
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
14
22
|
import { setupMcp } from '../mcp.mjs';
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})),
|
|
48
|
-
initialValue: 'project',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (isCancel(result)) {
|
|
52
|
-
fmt.cancel('Installation cancelled.');
|
|
53
|
-
process.exit(0);
|
|
24
|
+
const HOME = homedir();
|
|
25
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
26
|
+
const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
|
|
27
|
+
|
|
28
|
+
// Shell hook that auto-symlinks .aw_registry into every directory you cd into
|
|
29
|
+
const CHPWD_MARKER = '# aw-omnipresence';
|
|
30
|
+
const CHPWD_HOOK = `
|
|
31
|
+
${CHPWD_MARKER}
|
|
32
|
+
_aw_chpwd() {
|
|
33
|
+
# Only act inside git repos (has .git/) and skip home dir
|
|
34
|
+
[ "$PWD" = "$HOME" ] && return
|
|
35
|
+
[ ! -d ".git" ] && return
|
|
36
|
+
# Skip if .aw_registry already exists (real dir or symlink)
|
|
37
|
+
[ -e ".aw_registry" ] && return
|
|
38
|
+
# Create symlink to global source of truth
|
|
39
|
+
ln -s "$HOME/.aw_registry" ".aw_registry" 2>/dev/null
|
|
40
|
+
}
|
|
41
|
+
# Fire on every directory change (zsh)
|
|
42
|
+
if [ -n "$ZSH_VERSION" ]; then
|
|
43
|
+
autoload -U add-zsh-hook
|
|
44
|
+
add-zsh-hook chpwd _aw_chpwd
|
|
45
|
+
fi
|
|
46
|
+
# Also fire on shell startup for the initial directory
|
|
47
|
+
_aw_chpwd
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
function installOmnipresenceHook() {
|
|
51
|
+
const profiles = [];
|
|
52
|
+
if (platform() === 'darwin') {
|
|
53
|
+
profiles.push(join(HOME, '.zshrc'));
|
|
54
|
+
profiles.push(join(HOME, '.zprofile'));
|
|
54
55
|
}
|
|
56
|
+
profiles.push(join(HOME, '.bashrc'));
|
|
57
|
+
profiles.push(join(HOME, '.bash_profile'));
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
const target = profiles.find(p => existsSync(p)) || join(HOME, '.zshrc');
|
|
60
|
+
const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
|
61
|
+
|
|
62
|
+
if (current.includes(CHPWD_MARKER)) {
|
|
63
|
+
fmt.logStep(`Omnipresence hook already in ${target.replace(HOME, '~')}`);
|
|
64
|
+
return target;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
writeFileSync(target, current + CHPWD_HOOK);
|
|
68
|
+
fmt.logStep(`Omnipresence hook added to ${target.replace(HOME, '~')}`);
|
|
69
|
+
fmt.logStep(chalk.dim(' Every cd into a git repo auto-links .aw_registry'));
|
|
70
|
+
return target;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const content = readFileSync(gitignorePath, 'utf8');
|
|
64
|
-
return !content.split('\n').some(line => line.trim() === p);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (toAdd.length === 0) return;
|
|
68
|
-
|
|
69
|
-
const block = `\n# Agentic Workspace (local scope)\n${toAdd.join('\n')}\n`;
|
|
70
|
-
if (existsSync(gitignorePath)) {
|
|
71
|
-
writeFileSync(gitignorePath, readFileSync(gitignorePath, 'utf8') + block);
|
|
72
|
-
} else {
|
|
73
|
-
writeFileSync(gitignorePath, block.trimStart());
|
|
73
|
+
function installAutoSync() {
|
|
74
|
+
const profiles = [];
|
|
75
|
+
if (platform() === 'darwin') {
|
|
76
|
+
profiles.push(join(HOME, '.zshrc'));
|
|
74
77
|
}
|
|
75
|
-
|
|
78
|
+
profiles.push(join(HOME, '.bashrc'));
|
|
79
|
+
|
|
80
|
+
const marker = '# aw-auto-sync';
|
|
81
|
+
const snippet = `\n${marker}\n[ -f "$HOME/.aw_registry/.sync-config.json" ] && command -v aw >/dev/null 2>&1 && aw pull --silent &\n`;
|
|
82
|
+
|
|
83
|
+
const target = profiles.find(p => existsSync(p)) || join(HOME, '.zshrc');
|
|
84
|
+
const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
|
85
|
+
|
|
86
|
+
if (current.includes(marker)) return;
|
|
87
|
+
|
|
88
|
+
writeFileSync(target, current + snippet);
|
|
89
|
+
fmt.logStep('Auto-sync on new terminal (background, zero delay)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function saveManifest(data) {
|
|
93
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
export async function initCommand(args) {
|
|
79
97
|
const namespace = args['--namespace'] || null;
|
|
80
98
|
let user = args['--user'] || '';
|
|
81
|
-
const cwd = process.cwd();
|
|
82
99
|
|
|
83
100
|
fmt.intro('aw init');
|
|
84
101
|
|
|
85
|
-
// Validate
|
|
102
|
+
// ── Validate ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
86
104
|
if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
|
|
87
105
|
fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
|
|
88
106
|
}
|
|
@@ -91,77 +109,104 @@ export async function initCommand(args) {
|
|
|
91
109
|
fmt.cancel("'ghl' is a reserved namespace — it is the shared platform layer");
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
//
|
|
95
|
-
const scopeKey = await pickScope(args['--scope']);
|
|
96
|
-
const scope = SCOPES[scopeKey];
|
|
97
|
-
const rootDir = scope.rootDir();
|
|
98
|
-
const workspaceDir = join(rootDir, '.aw_registry');
|
|
112
|
+
// ── Check existing install ────────────────────────────────────────────
|
|
99
113
|
|
|
100
|
-
|
|
114
|
+
if (config.exists(GLOBAL_AW_DIR)) {
|
|
115
|
+
fmt.cancel(`.aw_registry already initialized at ~/\nRun ${chalk.bold('aw nuke')} first to start fresh.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Auto-detect user ──────────────────────────────────────────────────
|
|
101
119
|
|
|
102
|
-
// Auto-detect user from GitHub if not provided
|
|
103
120
|
if (!user) {
|
|
104
121
|
try {
|
|
105
122
|
user = execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
106
|
-
} catch {
|
|
107
|
-
// gh not available or not logged in — skip
|
|
108
|
-
}
|
|
123
|
+
} catch { /* gh not available */ }
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
// Create
|
|
112
|
-
if (!existsSync(workspaceDir)) {
|
|
113
|
-
mkdirSync(workspaceDir, { recursive: true });
|
|
114
|
-
fmt.logStep(`Created ${scopeKey === 'user' ? '~/' : ''}.aw_registry/`);
|
|
115
|
-
}
|
|
126
|
+
// ── Step 1: Create global source of truth ─────────────────────────────
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
fmt.cancel('.sync-config.json already exists. Delete it to re-init.');
|
|
128
|
+
if (!existsSync(GLOBAL_AW_DIR)) {
|
|
129
|
+
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
const cfg = config.create(workspaceDir, { namespace, user, scope: scopeKey });
|
|
132
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace, user, scope: 'omnipresent' });
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
fmt.note([
|
|
135
|
+
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
126
136
|
namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
127
137
|
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
128
|
-
`${chalk.dim('
|
|
129
|
-
].filter(Boolean).join('\n');
|
|
138
|
+
`${chalk.dim('mode:')} omnipresent — works in every project`,
|
|
139
|
+
].filter(Boolean).join('\n'), 'Config created');
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
// ── Step 2: Pull registry content ─────────────────────────────────────
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: workspaceDir });
|
|
143
|
+
pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: GLOBAL_AW_DIR });
|
|
135
144
|
|
|
136
|
-
// Pull [template]/ as the team namespace
|
|
137
145
|
if (namespace) {
|
|
138
|
-
pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir:
|
|
146
|
+
pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
// ── Step 3: Link to global IDE dirs ───────────────────────────────────
|
|
150
|
+
|
|
151
|
+
linkWorkspace(HOME);
|
|
152
|
+
generateCommands(HOME);
|
|
153
|
+
const instructionFiles = copyInstructions(HOME, null, namespace) || [];
|
|
154
|
+
initAwDocs(HOME);
|
|
155
|
+
const mcpFiles = setupMcp(HOME, namespace) || [];
|
|
156
|
+
|
|
157
|
+
// ── Step 4: Install omnipresence hook (chpwd) ─────────────────────────
|
|
158
|
+
|
|
159
|
+
const hookTarget = installOmnipresenceHook();
|
|
160
|
+
|
|
161
|
+
// ── Step 5: Install auto-sync on new terminal ─────────────────────────
|
|
162
|
+
|
|
163
|
+
installAutoSync();
|
|
164
|
+
|
|
165
|
+
// ── Step 6: Start background daemon ───────────────────────────────────
|
|
166
|
+
|
|
167
|
+
let daemonInstalled = false;
|
|
168
|
+
try {
|
|
169
|
+
daemonCommand({ _positional: ['install'], '--interval': '1h' });
|
|
170
|
+
daemonInstalled = true;
|
|
171
|
+
} catch { /* non-fatal */ }
|
|
172
|
+
|
|
173
|
+
// ── Step 7: Symlink in current directory if it's a git repo ───────────
|
|
174
|
+
|
|
175
|
+
const cwd = process.cwd();
|
|
176
|
+
if (cwd !== HOME && existsSync(join(cwd, '.git')) && !existsSync(join(cwd, '.aw_registry'))) {
|
|
177
|
+
try {
|
|
178
|
+
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
179
|
+
fmt.logStep(`Linked .aw_registry in current project`);
|
|
180
|
+
} catch { /* best effort */ }
|
|
158
181
|
}
|
|
159
182
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
183
|
+
// ── Step 8: Write manifest for perfect nuke cleanup ───────────────────
|
|
184
|
+
|
|
185
|
+
const manifest = {
|
|
186
|
+
version: 1,
|
|
187
|
+
installedAt: new Date().toISOString(),
|
|
188
|
+
globalDir: GLOBAL_AW_DIR,
|
|
189
|
+
createdFiles: [
|
|
190
|
+
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
191
|
+
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
192
|
+
],
|
|
193
|
+
shellProfile: hookTarget,
|
|
194
|
+
shellMarkers: [CHPWD_MARKER, '# aw-auto-sync'],
|
|
195
|
+
daemon: daemonInstalled ? 'launchd' : null,
|
|
196
|
+
};
|
|
197
|
+
saveManifest(manifest);
|
|
198
|
+
|
|
199
|
+
// ── Done ──────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
fmt.outro([
|
|
202
|
+
`Omnipresent install complete`,
|
|
203
|
+
``,
|
|
204
|
+
` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
|
|
205
|
+
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
206
|
+
` ${chalk.green('✓')} Auto-symlink: every git repo you cd into gets .aw_registry`,
|
|
207
|
+
` ${chalk.green('✓')} Auto-sync: background daemon + new terminal pull`,
|
|
208
|
+
``,
|
|
209
|
+
` ${chalk.dim('Open any project in any IDE — AW is already there.')}`,
|
|
210
|
+
` ${chalk.dim('To undo everything:')} ${chalk.bold('aw nuke')}`,
|
|
211
|
+
].join('\n'));
|
|
167
212
|
}
|
package/commands/nuke.mjs
CHANGED
|
@@ -1,91 +1,319 @@
|
|
|
1
|
-
// commands/nuke.mjs —
|
|
1
|
+
// commands/nuke.mjs — Safe omnipresent cleanup: reads manifest, removes only what AW created
|
|
2
|
+
//
|
|
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.
|
|
2
5
|
|
|
3
6
|
import { join } from 'node:path';
|
|
4
|
-
import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync, writeFileSync, readlinkSync } from 'node:fs';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { homedir, platform } from 'node:os';
|
|
5
10
|
import * as fmt from '../fmt.mjs';
|
|
6
11
|
import { chalk } from '../fmt.mjs';
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
const HOME = homedir();
|
|
14
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
15
|
+
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
|
+
|
|
19
|
+
// IDE dirs where AW creates symlinks
|
|
9
20
|
const IDE_DIRS = ['.claude', '.cursor', '.codex'];
|
|
10
21
|
const CONTENT_TYPES = ['agents', 'skills', 'commands', 'blueprints', 'evals'];
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'.agents',
|
|
19
|
-
];
|
|
23
|
+
function loadManifest() {
|
|
24
|
+
if (!existsSync(MANIFEST_PATH)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
const cwd = process.cwd();
|
|
23
|
-
const workspaceDir = join(cwd, '.aw_registry');
|
|
30
|
+
// ── Step 1: Stop and remove daemon ──────────────────────────────────────────
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
function removeDaemon(manifest) {
|
|
33
|
+
const isMac = platform() === 'darwin';
|
|
26
34
|
|
|
27
|
-
if (
|
|
28
|
-
|
|
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 */ }
|
|
29
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
|
+
|
|
112
|
+
function removeCreatedFiles(manifest) {
|
|
113
|
+
if (!manifest?.createdFiles?.length) return;
|
|
114
|
+
|
|
115
|
+
let removed = 0;
|
|
116
|
+
for (const rel of manifest.createdFiles) {
|
|
117
|
+
const p = join(HOME, rel);
|
|
118
|
+
try {
|
|
119
|
+
if (existsSync(p)) {
|
|
120
|
+
rmSync(p);
|
|
121
|
+
removed++;
|
|
122
|
+
}
|
|
123
|
+
} catch { /* best effort */ }
|
|
124
|
+
}
|
|
125
|
+
if (removed > 0) {
|
|
126
|
+
fmt.logStep(`Removed ${removed} generated file${removed > 1 ? 's' : ''}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Step 4: Remove AW symlinks from IDE dirs ────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function removeIdeSymlinks() {
|
|
133
|
+
let removed = 0;
|
|
30
134
|
|
|
31
|
-
// 1. Remove symlinks from all IDE dirs
|
|
32
135
|
for (const ide of IDE_DIRS) {
|
|
33
136
|
for (const type of CONTENT_TYPES) {
|
|
34
|
-
const dir = join(
|
|
137
|
+
const dir = join(HOME, ide, type);
|
|
35
138
|
if (!existsSync(dir)) continue;
|
|
139
|
+
|
|
36
140
|
for (const entry of readdirSync(dir)) {
|
|
37
141
|
const p = join(dir, entry);
|
|
38
142
|
try {
|
|
39
143
|
const stat = lstatSync(p);
|
|
40
144
|
if (stat.isSymbolicLink()) {
|
|
41
|
-
|
|
145
|
+
const target = readlinkSync(p);
|
|
146
|
+
// Only remove if it points into .aw_registry
|
|
147
|
+
if (target.includes('.aw_registry') || target.includes('aw_registry')) {
|
|
148
|
+
unlinkSync(p);
|
|
149
|
+
removed++;
|
|
150
|
+
}
|
|
42
151
|
}
|
|
43
152
|
} catch { /* best effort */ }
|
|
44
153
|
}
|
|
45
154
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 */ }
|
|
50
174
|
}
|
|
51
175
|
}
|
|
52
176
|
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
if (existsSync(
|
|
56
|
-
|
|
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 */ }
|
|
57
195
|
}
|
|
58
196
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
197
|
+
if (removed > 0) {
|
|
198
|
+
fmt.logStep(`Removed ${removed} IDE symlink${removed > 1 ? 's' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Step 5: Find and remove .aw_registry symlinks from project dirs ─────────
|
|
203
|
+
|
|
204
|
+
function removeProjectSymlinks() {
|
|
205
|
+
// Check common project locations for .aw_registry symlinks
|
|
206
|
+
// We look in the current directory + scan known parent dirs
|
|
207
|
+
let removed = 0;
|
|
208
|
+
|
|
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 {}
|
|
231
|
+
|
|
232
|
+
for (const dir of dirsToCheck) {
|
|
233
|
+
const link = join(dir, '.aw_registry');
|
|
63
234
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
235
|
+
if (lstatSync(link).isSymbolicLink()) {
|
|
236
|
+
unlinkSync(link);
|
|
237
|
+
removed++;
|
|
238
|
+
}
|
|
239
|
+
} catch { /* doesn't exist or not a symlink */ }
|
|
66
240
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
241
|
+
|
|
242
|
+
if (removed > 0) {
|
|
243
|
+
fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
|
|
70
244
|
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
71
248
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
|
|
249
|
+
export function nukeCommand(args) {
|
|
250
|
+
fmt.intro('aw nuke');
|
|
251
|
+
|
|
252
|
+
// Check if omnipresent install exists
|
|
253
|
+
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 {}
|
|
266
|
+
fmt.outro('Done');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
fmt.cancel('No .aw_registry found — nothing to remove');
|
|
270
|
+
return;
|
|
77
271
|
}
|
|
78
272
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
273
|
+
const manifest = loadManifest();
|
|
274
|
+
|
|
275
|
+
fmt.note([
|
|
276
|
+
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
277
|
+
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);
|
|
283
|
+
|
|
284
|
+
// 2. Remove shell hooks
|
|
285
|
+
removeShellHooks(manifest);
|
|
286
|
+
|
|
287
|
+
// 3. Remove AW-created files (instruction files, MCP configs, etc.)
|
|
288
|
+
removeCreatedFiles(manifest);
|
|
289
|
+
|
|
290
|
+
// 4. Remove IDE symlinks (only those pointing to .aw_registry)
|
|
291
|
+
removeIdeSymlinks();
|
|
292
|
+
|
|
293
|
+
// 5. Remove .aw_registry symlinks from project directories
|
|
294
|
+
removeProjectSymlinks();
|
|
295
|
+
|
|
296
|
+
// 6. Remove .aw_docs/ from home
|
|
297
|
+
const awDocs = join(HOME, '.aw_docs');
|
|
298
|
+
if (existsSync(awDocs)) {
|
|
299
|
+
rmSync(awDocs, { recursive: true, force: true });
|
|
300
|
+
fmt.logStep('Removed ~/.aw_docs/');
|
|
87
301
|
}
|
|
88
302
|
|
|
89
|
-
|
|
90
|
-
|
|
303
|
+
// 7. Remove ~/.aw_registry/ itself (the source of truth)
|
|
304
|
+
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
|
|
305
|
+
fmt.logStep('Removed ~/.aw_registry/');
|
|
306
|
+
|
|
307
|
+
fmt.outro([
|
|
308
|
+
'Omnipresent install fully removed',
|
|
309
|
+
'',
|
|
310
|
+
` ${chalk.green('✓')} Daemon stopped`,
|
|
311
|
+
` ${chalk.green('✓')} Shell hooks cleaned`,
|
|
312
|
+
` ${chalk.green('✓')} IDE symlinks removed`,
|
|
313
|
+
` ${chalk.green('✓')} Project symlinks removed`,
|
|
314
|
+
` ${chalk.green('✓')} Source of truth deleted`,
|
|
315
|
+
'',
|
|
316
|
+
` ${chalk.dim('No existing files were touched.')}`,
|
|
317
|
+
` ${chalk.dim('Run')} ${chalk.bold('aw init')} ${chalk.dim('to reinstall.')}`,
|
|
318
|
+
].join('\n'));
|
|
91
319
|
}
|