@ghl-ai/aw 0.1.0 → 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 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
  '',
@@ -128,7 +133,7 @@ export async function run(argv) {
128
133
 
129
134
  if (command && COMMANDS[command]) {
130
135
  const handler = await COMMANDS[command]();
131
- handler(args);
136
+ await handler(args);
132
137
  return;
133
138
  }
134
139
 
@@ -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,25 +1,106 @@
1
- // commands/init.mjs — Initialize workspace config + pull ghl + template
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
2
10
 
3
- import { mkdirSync, existsSync } from 'node:fs';
11
+ import { mkdirSync, existsSync, writeFileSync, readFileSync, chmodSync, symlinkSync, lstatSync } from 'node:fs';
4
12
  import { execSync } from 'node:child_process';
5
- import { join } from 'node:path';
13
+ import { join, resolve, relative } from 'node:path';
14
+ import { homedir, platform } from 'node:os';
6
15
  import * as config from '../config.mjs';
7
16
  import * as fmt from '../fmt.mjs';
8
17
  import { chalk } from '../fmt.mjs';
9
18
  import { pullCommand } from './pull.mjs';
10
19
  import { linkWorkspace } from '../link.mjs';
20
+ import { daemonCommand } from './daemon.mjs';
11
21
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
12
22
  import { setupMcp } from '../mcp.mjs';
13
23
 
14
- export function initCommand(args) {
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'));
55
+ }
56
+ profiles.push(join(HOME, '.bashrc'));
57
+ profiles.push(join(HOME, '.bash_profile'));
58
+
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;
71
+ }
72
+
73
+ function installAutoSync() {
74
+ const profiles = [];
75
+ if (platform() === 'darwin') {
76
+ profiles.push(join(HOME, '.zshrc'));
77
+ }
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');
94
+ }
95
+
96
+ export async function initCommand(args) {
15
97
  const namespace = args['--namespace'] || null;
16
98
  let user = args['--user'] || '';
17
- const cwd = process.cwd();
18
- const workspaceDir = join(cwd, '.aw_registry');
19
99
 
20
100
  fmt.intro('aw init');
21
101
 
22
- // Validate namespace slug if provided
102
+ // ── Validate ──────────────────────────────────────────────────────────
103
+
23
104
  if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
24
105
  fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
25
106
  }
@@ -28,48 +109,104 @@ export function initCommand(args) {
28
109
  fmt.cancel("'ghl' is a reserved namespace — it is the shared platform layer");
29
110
  }
30
111
 
31
- // Auto-detect user from GitHub if not provided
112
+ // ── Check existing install ────────────────────────────────────────────
113
+
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 ──────────────────────────────────────────────────
119
+
32
120
  if (!user) {
33
121
  try {
34
122
  user = execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
35
- } catch {
36
- // gh not available or not logged in — skip
37
- }
123
+ } catch { /* gh not available */ }
38
124
  }
39
125
 
40
- // Create workspace dir
41
- if (!existsSync(workspaceDir)) {
42
- mkdirSync(workspaceDir, { recursive: true });
43
- fmt.logStep('Created .aw_registry/');
44
- }
126
+ // ── Step 1: Create global source of truth ─────────────────────────────
45
127
 
46
- // Check existing config
47
- if (config.exists(workspaceDir)) {
48
- 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 });
49
130
  }
50
131
 
51
- // Create config
52
- const cfg = config.create(workspaceDir, { namespace, user });
132
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace, user, scope: 'omnipresent' });
53
133
 
54
- const infoLines = [
134
+ fmt.note([
135
+ `${chalk.dim('source:')} ~/.aw_registry/`,
55
136
  namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
56
137
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
57
- ].filter(Boolean).join('\n');
138
+ `${chalk.dim('mode:')} omnipresent — works in every project`,
139
+ ].filter(Boolean).join('\n'), 'Config created');
58
140
 
59
- fmt.note(infoLines, 'Config created');
141
+ // ── Step 2: Pull registry content ─────────────────────────────────────
60
142
 
61
- // Always pull ghl/ (platform layer)
62
- pullCommand({ ...args, _positional: ['ghl'] });
143
+ pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: GLOBAL_AW_DIR });
63
144
 
64
- // Pull [template]/ as the team namespace
65
145
  if (namespace) {
66
- pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace });
146
+ pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
67
147
  }
68
148
 
69
- // Post-pull IDE integration
70
- linkWorkspace(cwd);
71
- generateCommands(cwd);
72
- copyInstructions(cwd, null, namespace);
73
- initAwDocs(cwd);
74
- setupMcp(cwd, namespace);
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 */ }
181
+ }
182
+
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'));
75
212
  }
package/commands/nuke.mjs CHANGED
@@ -1,95 +1,319 @@
1
- // commands/nuke.mjs — Remove entire .aw_registry/ and all generated IDE files
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 } 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
- // IDE dirs to scan for per-file symlinks
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
- // Generated files from mcp.mjs + integrate.mjs
13
- const FILES_TO_CLEAN = [
14
- '.claude/settings.local.json',
15
- '.cursor/mcp.json',
16
- '.codex/config.toml',
17
- 'mcp.json',
18
- 'CLAUDE.md',
19
- 'AGENTS.md',
20
- ];
21
-
22
- // Dirs to remove if empty after cleanup (deepest first)
23
- const DIRS_TO_PRUNE = [
24
- // IDE content dirs
25
- ...IDE_DIRS.flatMap(ide => CONTENT_TYPES.map(t => `${ide}/${t}`)),
26
- // IDE root dirs
27
- ...IDE_DIRS,
28
- // Codex agents dir
29
- '.agents/skills',
30
- '.agents',
31
- ];
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
+ }
32
29
 
33
- export function nukeCommand(args) {
34
- const cwd = process.cwd();
35
- const workspaceDir = join(cwd, '.aw_registry');
30
+ // ── Step 1: Stop and remove daemon ──────────────────────────────────────────
36
31
 
37
- fmt.intro('aw nuke');
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
+ }
38
100
 
39
- if (!existsSync(workspaceDir)) {
40
- fmt.cancel('No .aw_registry/ found nothing to remove');
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
+ }
41
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;
42
134
 
43
- // 1. Remove symlinks + generated files from all IDE dirs
44
135
  for (const ide of IDE_DIRS) {
45
136
  for (const type of CONTENT_TYPES) {
46
- const dir = join(cwd, ide, type);
137
+ const dir = join(HOME, ide, type);
47
138
  if (!existsSync(dir)) continue;
139
+
48
140
  for (const entry of readdirSync(dir)) {
49
141
  const p = join(dir, entry);
50
142
  try {
51
143
  const stat = lstatSync(p);
52
144
  if (stat.isSymbolicLink()) {
53
- unlinkSync(p);
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
+ }
54
151
  }
55
152
  } catch { /* best effort */ }
56
153
  }
57
154
  }
58
- // Clean commands/aw/ (generated commands + symlinked registry commands)
59
- const awCmdDir = join(cwd, ide, 'commands/aw');
60
- if (existsSync(awCmdDir)) {
61
- rmSync(awCmdDir, { recursive: true, force: true });
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 */ }
62
174
  }
63
175
  }
64
- // 2. Clean .agents/skills/ (symlinks + generated Codex skill dirs)
65
- const agentsSkillsDir = join(cwd, '.agents/skills');
66
- if (existsSync(agentsSkillsDir)) {
67
- rmSync(agentsSkillsDir, { recursive: true, force: true });
68
- }
69
176
 
70
- // 3. Remove generated files
71
- for (const rel of FILES_TO_CLEAN) {
72
- const p = join(cwd, rel);
73
- try { if (existsSync(p)) rmSync(p); } catch { /* best effort */ }
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 */ }
74
195
  }
75
196
 
76
- // 4. Remove .aw_registry/ and .aw_docs/
77
- rmSync(workspaceDir, { recursive: true, force: true });
78
- const awDocsDir = join(cwd, '.aw_docs');
79
- if (existsSync(awDocsDir)) {
80
- rmSync(awDocsDir, { recursive: true, force: true });
197
+ if (removed > 0) {
198
+ fmt.logStep(`Removed ${removed} IDE symlink${removed > 1 ? 's' : ''}`);
81
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;
82
208
 
83
- // 5. Prune empty dirs
84
- for (const rel of DIRS_TO_PRUNE) {
85
- const p = join(cwd, rel);
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');
86
234
  try {
87
- if (existsSync(p) && readdirSync(p).length === 0) {
88
- rmSync(p, { recursive: true });
235
+ if (lstatSync(link).isSymbolicLink()) {
236
+ unlinkSync(link);
237
+ removed++;
89
238
  }
90
- } catch { /* best effort */ }
239
+ } catch { /* doesn't exist or not a symlink */ }
240
+ }
241
+
242
+ if (removed > 0) {
243
+ fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
91
244
  }
245
+ }
246
+
247
+ // ── Main ─────────────────────────────────────────────────────────────────────
248
+
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;
271
+ }
272
+
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/');
301
+ }
302
+
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/');
92
306
 
93
- fmt.logSuccess('Removed .aw_registry/ and all generated IDE files');
94
- fmt.outro(`Run ${chalk.dim('aw init')} to start fresh`);
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'));
95
319
  }
package/integrate.mjs CHANGED
@@ -92,6 +92,7 @@ export function generateCommands(cwd) {
92
92
  * Copy CLAUDE.md and AGENTS.md to project root.
93
93
  */
94
94
  export function copyInstructions(cwd, tempDir, namespace) {
95
+ const createdFiles = [];
95
96
  for (const file of ['CLAUDE.md', 'AGENTS.md']) {
96
97
  const dest = join(cwd, file);
97
98
  if (existsSync(dest)) continue;
@@ -105,6 +106,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
105
106
  }
106
107
  writeFileSync(dest, content);
107
108
  fmt.logSuccess(`Created ${file}`);
109
+ createdFiles.push(dest);
108
110
  continue;
109
111
  }
110
112
  }
@@ -115,8 +117,10 @@ export function copyInstructions(cwd, tempDir, namespace) {
115
117
  if (content) {
116
118
  writeFileSync(dest, content);
117
119
  fmt.logSuccess(`Created ${file}`);
120
+ createdFiles.push(dest);
118
121
  }
119
122
  }
123
+ return createdFiles;
120
124
  }
121
125
 
122
126
  function generateClaudeMd(cwd, namespace) {
package/mcp.mjs CHANGED
@@ -29,110 +29,98 @@ function detectPaths() {
29
29
 
30
30
  /**
31
31
  * Setup MCP configs for all IDEs. Only writes if file doesn't exist.
32
+ * Returns list of file paths that were actually created.
32
33
  */
33
34
  export function setupMcp(cwd, namespace) {
34
35
  const paths = detectPaths();
35
- let created = 0;
36
+ const createdFiles = [];
37
+
38
+ const attempt = (filePath, contentFn, append = false) => {
39
+ if (writeIfMissing(filePath, contentFn, append)) {
40
+ createdFiles.push(filePath);
41
+ }
42
+ };
36
43
 
37
44
  // .claude/settings.local.json
38
- created += writeIfMissing(
39
- join(cwd, '.claude', 'settings.local.json'),
40
- () => {
41
- const servers = {};
42
- if (paths.ghlAiBridge) {
43
- servers['ghl-ai'] = {
44
- command: 'node',
45
- args: [paths.ghlAiBridge],
46
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
47
- };
48
- }
49
- if (paths.gitJenkinsPath) {
50
- servers['git-jenkins'] = {
51
- command: 'node',
52
- args: [paths.gitJenkinsPath],
53
- };
54
- }
55
- if (Object.keys(servers).length === 0) return null;
56
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
45
+ attempt(join(cwd, '.claude', 'settings.local.json'), () => {
46
+ const servers = {};
47
+ if (paths.ghlAiBridge) {
48
+ servers['ghl-ai'] = {
49
+ command: 'node',
50
+ args: [paths.ghlAiBridge],
51
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
52
+ };
53
+ }
54
+ if (paths.gitJenkinsPath) {
55
+ servers['git-jenkins'] = { command: 'node', args: [paths.gitJenkinsPath] };
57
56
  }
58
- );
57
+ if (Object.keys(servers).length === 0) return null;
58
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
59
+ });
59
60
 
60
61
  // .cursor/mcp.json
61
- created += writeIfMissing(
62
- join(cwd, '.cursor', 'mcp.json'),
63
- () => {
64
- const servers = {};
65
- if (paths.ghlAiBridge) {
66
- servers['ghl-ai'] = {
67
- command: 'node',
68
- args: [paths.ghlAiBridge],
69
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
70
- };
71
- }
72
- if (paths.gitJenkinsPath) {
73
- servers['git-jenkins'] = {
74
- command: 'node',
75
- args: [paths.gitJenkinsPath],
76
- };
77
- }
78
- if (Object.keys(servers).length === 0) return null;
79
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
62
+ attempt(join(cwd, '.cursor', 'mcp.json'), () => {
63
+ const servers = {};
64
+ if (paths.ghlAiBridge) {
65
+ servers['ghl-ai'] = {
66
+ command: 'node',
67
+ args: [paths.ghlAiBridge],
68
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
69
+ };
70
+ }
71
+ if (paths.gitJenkinsPath) {
72
+ servers['git-jenkins'] = { command: 'node', args: [paths.gitJenkinsPath] };
80
73
  }
81
- );
74
+ if (Object.keys(servers).length === 0) return null;
75
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
76
+ });
82
77
 
83
78
  // .codex/config.toml — append MCP servers
84
79
  const codexConfig = join(cwd, '.codex', 'config.toml');
85
80
  if (!hasMcpSection(codexConfig)) {
86
- created += writeIfMissing(
87
- codexConfig,
88
- () => {
89
- const lines = [];
90
- if (paths.ghlAiBridge) {
91
- lines.push(
92
- '[mcp_servers.ghl-ai]',
93
- 'command = "node"',
94
- `args = ["${paths.ghlAiBridge}"]`,
95
- '',
96
- '[mcp_servers.ghl-ai.env]',
97
- `GHL_MCP_URL = "${paths.ghlMcpUrl}"`,
98
- `TEAM_NAME = "${namespace || ''}"`,
99
- '',
100
- );
101
- }
102
- if (paths.gitJenkinsPath) {
103
- lines.push(
104
- '[mcp_servers.git-jenkins]',
105
- 'command = "node"',
106
- `args = ["${paths.gitJenkinsPath}"]`,
107
- '',
108
- );
109
- }
110
- if (lines.length === 0) return null;
111
- return lines.join('\n');
112
- },
113
- true // append mode
114
- );
81
+ attempt(codexConfig, () => {
82
+ const lines = [];
83
+ if (paths.ghlAiBridge) {
84
+ lines.push(
85
+ '[mcp_servers.ghl-ai]',
86
+ 'command = "node"',
87
+ `args = ["${paths.ghlAiBridge}"]`,
88
+ '',
89
+ '[mcp_servers.ghl-ai.env]',
90
+ `GHL_MCP_URL = "${paths.ghlMcpUrl}"`,
91
+ `TEAM_NAME = "${namespace || ''}"`,
92
+ '',
93
+ );
94
+ }
95
+ if (paths.gitJenkinsPath) {
96
+ lines.push(
97
+ '[mcp_servers.git-jenkins]',
98
+ 'command = "node"',
99
+ `args = ["${paths.gitJenkinsPath}"]`,
100
+ '',
101
+ );
102
+ }
103
+ if (lines.length === 0) return null;
104
+ return lines.join('\n');
105
+ }, true /* append mode */);
115
106
  }
116
107
 
117
108
  // mcp.json at project root (Claude Code auto-discovery)
118
- created += writeIfMissing(
119
- join(cwd, 'mcp.json'),
120
- () => {
121
- const servers = {};
122
- if (paths.ghlAiBridge) {
123
- servers['ghl-ai'] = {
124
- command: 'node',
125
- args: [paths.ghlAiBridge],
126
- env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
127
- };
128
- }
129
- if (Object.keys(servers).length === 0) return null;
130
- return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
109
+ attempt(join(cwd, 'mcp.json'), () => {
110
+ const servers = {};
111
+ if (paths.ghlAiBridge) {
112
+ servers['ghl-ai'] = {
113
+ command: 'node',
114
+ args: [paths.ghlAiBridge],
115
+ env: { GHL_MCP_URL: paths.ghlMcpUrl, TEAM_NAME: namespace || '' },
116
+ };
131
117
  }
132
- );
118
+ if (Object.keys(servers).length === 0) return null;
119
+ return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
120
+ });
133
121
 
134
- if (created > 0) {
135
- fmt.logSuccess(`Created ${created} MCP config${created > 1 ? 's' : ''}`);
122
+ if (createdFiles.length > 0) {
123
+ fmt.logSuccess(`Created ${createdFiles.length} MCP config${createdFiles.length > 1 ? 's' : ''}`);
136
124
  }
137
125
 
138
126
  const warnings = [];
@@ -142,13 +130,13 @@ export function setupMcp(cwd, namespace) {
142
130
  fmt.logWarn(w);
143
131
  }
144
132
 
145
- return created;
133
+ return createdFiles;
146
134
  }
147
135
 
148
136
  function writeIfMissing(filePath, contentFn, append = false) {
149
- if (!append && existsSync(filePath)) return 0;
137
+ if (!append && existsSync(filePath)) return false;
150
138
  const content = contentFn();
151
- if (!content) return 0;
139
+ if (!content) return false;
152
140
  mkdirSync(join(filePath, '..'), { recursive: true });
153
141
  if (append && existsSync(filePath)) {
154
142
  const existing = readFileSync(filePath, 'utf8');
@@ -156,7 +144,7 @@ function writeIfMissing(filePath, contentFn, append = false) {
156
144
  } else {
157
145
  writeFileSync(filePath, content);
158
146
  }
159
- return 1;
147
+ return true;
160
148
  }
161
149
 
162
150
  function hasMcpSection(filePath) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
7
- "aw": "./bin.js"
7
+ "aw": "bin.js"
8
8
  },
9
9
  "files": [
10
10
  "bin.js",