@ghl-ai/aw 0.1.36-beta.1 → 0.1.36-beta.100

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
@@ -65,7 +65,8 @@ function parseArgs(argv) {
65
65
 
66
66
  function printHelp() {
67
67
  fmt.banner('aw', {
68
- subtitle: ` ${chalk.dim('v' + VERSION)} ${chalk.hex('#FF6B35')('Agentic Workspace CLI')} ${chalk.dim('— pull, push & manage agents, skills and more from the registry')}`,
68
+ icon: '',
69
+ subtitle: ` ${chalk.hex('#FF6B35')('⟁')} ${chalk.dim('v' + VERSION)} ${chalk.hex('#FF6B35')('Agentic Workspace CLI')} ${chalk.dim('— pull, push & manage agents, skills and more from the registry')}`,
69
70
  });
70
71
 
71
72
  const cmd = (c, d) => ` ${chalk.hex('#FF6B35')(c.padEnd(38))} ${chalk.dim(d)}`;
@@ -92,6 +93,7 @@ function printHelp() {
92
93
 
93
94
  sec('Manage'),
94
95
  cmd('aw status', 'Show synced paths, modified files & conflicts'),
96
+ cmd('aw link', 'Link current project as a git worktree (wires IDE symlinks)'),
95
97
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
96
98
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
97
99
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
@@ -2,12 +2,15 @@
2
2
  // that silently runs `aw pull` on a schedule without any user interaction.
3
3
 
4
4
  import { existsSync, writeFileSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
5
- import { execSync } from 'node:child_process';
5
+ import { execSync, exec as execCb } from 'node:child_process';
6
+ import { promisify } from 'node:util';
6
7
  import { join } from 'node:path';
7
8
  import { homedir, platform } from 'node:os';
8
9
  import * as fmt from '../fmt.mjs';
9
10
  import { chalk } from '../fmt.mjs';
10
11
 
12
+ const exec = promisify(execCb);
13
+
11
14
  const LABEL = 'ai.ghl.aw.pull';
12
15
  const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
13
16
  const DEFAULT_INTERVAL = 3600; // 1 hour in seconds
@@ -22,7 +25,7 @@ function getAwBin() {
22
25
 
23
26
  // ── macOS: launchd ──────────────────────────────────────────────────────────
24
27
 
25
- function installLaunchd(intervalSeconds) {
28
+ async function installLaunchd(intervalSeconds) {
26
29
  const awBin = getAwBin();
27
30
  const logDir = join(homedir(), '.aw_registry', 'logs');
28
31
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
@@ -63,22 +66,24 @@ function installLaunchd(intervalSeconds) {
63
66
 
64
67
  writeFileSync(PLIST_PATH, plist);
65
68
 
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')}`);
69
+ const s = fmt.spinner();
70
+ s.start('Installing daemon...');
71
+ try { await exec(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch { /* not loaded */ }
72
+ await exec(`launchctl load "${PLIST_PATH}"`);
73
+ s.stop(`Daemon installed — runs every ${formatInterval(intervalSeconds)}`);
74
+ fmt.logInfo(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
72
75
  }
73
76
 
74
- function uninstallLaunchd() {
77
+ async function uninstallLaunchd() {
75
78
  if (!existsSync(PLIST_PATH)) {
76
79
  fmt.logStep('No daemon installed.');
77
80
  return;
78
81
  }
79
- try { execSync(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
82
+ const s = fmt.spinner();
83
+ s.start('Removing daemon...');
84
+ try { await exec(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
80
85
  unlinkSync(PLIST_PATH);
81
- fmt.logStep('Daemon removed.');
86
+ s.stop('Daemon removed.');
82
87
  }
83
88
 
84
89
  function statusLaunchd() {
@@ -102,7 +107,7 @@ function toCronExpression(intervalSeconds) {
102
107
  return `0 */${hours} * * *`;
103
108
  }
104
109
 
105
- function installCron(intervalSeconds) {
110
+ async function installCron(intervalSeconds) {
106
111
  const awBin = getAwBin();
107
112
  const logDir = join(homedir(), '.aw_registry', 'logs');
108
113
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
@@ -117,17 +122,21 @@ function installCron(intervalSeconds) {
117
122
  const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
118
123
  const updated = cleaned.trimEnd() + '\n' + cronLine + '\n';
119
124
 
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')}`);
125
+ const s = fmt.spinner();
126
+ s.start('Installing cron job...');
127
+ await exec(`echo ${JSON.stringify(updated)} | crontab -`);
128
+ s.stop(`Cron job installed — runs every ${formatInterval(intervalSeconds)}`);
129
+ fmt.logInfo(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
123
130
  }
124
131
 
125
- function uninstallCron() {
132
+ async function uninstallCron() {
126
133
  let current = '';
127
134
  try { current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { return; }
128
135
  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.');
136
+ const s = fmt.spinner();
137
+ s.start('Removing cron job...');
138
+ await exec(`echo ${JSON.stringify(cleaned)} | crontab -`);
139
+ s.stop('Cron job removed.');
131
140
  }
132
141
 
133
142
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -148,7 +157,7 @@ function parseInterval(str) {
148
157
 
149
158
  // ── Main ─────────────────────────────────────────────────────────────────────
150
159
 
151
- export function daemonCommand(args) {
160
+ export async function daemonCommand(args) {
152
161
  const subcommand = args._positional[0] || 'install';
153
162
  const interval = parseInterval(args['--interval'] || args._positional[1]);
154
163
  const isMac = platform() === 'darwin';
@@ -158,13 +167,13 @@ export function daemonCommand(args) {
158
167
  if (subcommand === 'install') {
159
168
  fmt.logStep(`Platform: ${isMac ? 'macOS (launchd)' : 'Linux (cron)'}`);
160
169
  fmt.logStep(`Interval: every ${formatInterval(interval)}`);
161
- if (isMac) installLaunchd(interval);
162
- else installCron(interval);
170
+ if (isMac) await installLaunchd(interval);
171
+ else await installCron(interval);
163
172
  fmt.outro(`aw pull will run silently every ${formatInterval(interval)}`);
164
173
 
165
174
  } else if (subcommand === 'uninstall' || subcommand === 'stop') {
166
- if (isMac) uninstallLaunchd();
167
- else uninstallCron();
175
+ if (isMac) await uninstallLaunchd();
176
+ else await uninstallCron();
168
177
  fmt.outro('Daemon stopped.');
169
178
 
170
179
  } else if (subcommand === 'status') {
package/commands/drop.mjs CHANGED
@@ -2,87 +2,85 @@
2
2
 
3
3
  import { join, resolve } from 'node:path';
4
4
  import { rmSync, existsSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
5
6
  import * as config from '../config.mjs';
6
7
  import * as fmt from '../fmt.mjs';
7
8
  import { chalk } from '../fmt.mjs';
8
- import { matchesAny } from '../glob.mjs';
9
9
  import { resolveInput } from '../paths.mjs';
10
- import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
10
+ import { removeFromSparseCheckout, isValidClone, getLocalRegistryDir } from '../git.mjs';
11
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
12
+ import { linkWorkspace } from '../link.mjs';
11
13
 
12
14
  export function dropCommand(args) {
13
15
  const input = args._positional?.[0];
14
16
  const cwd = process.cwd();
15
- const workspaceDir = join(cwd, '.aw_registry');
17
+
18
+ const HOME = homedir();
19
+ const AW_HOME = join(HOME, '.aw');
20
+ const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
21
+ const workspaceDir = getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
16
22
 
17
23
  fmt.intro('aw drop');
18
24
 
19
25
  if (!input) {
20
- fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)\n aw drop example-team/skills/example-skill/SKILL (delete single file)');
26
+ fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)');
27
+ return;
21
28
  }
22
29
 
23
- const cfg = config.load(workspaceDir);
24
- if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
30
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
31
+ if (!isValidClone(AW_HOME, repoUrl)) {
32
+ fmt.cancel('Registry not initialized. Run: aw init');
33
+ return;
34
+ }
25
35
 
26
- // Resolve to registry path (accepts both local and registry paths)
36
+ const cfg = config.load(GLOBAL_AW_DIR);
37
+ if (!cfg) {
38
+ fmt.cancel('No .sync-config.json found. Run: aw init');
39
+ return;
40
+ }
41
+
42
+ // Resolve to registry path
27
43
  const resolved = resolveInput(input, workspaceDir);
28
44
  const regPath = resolved.registryPath;
29
45
 
30
46
  if (!regPath) {
31
47
  fmt.cancel(`Could not resolve "${input}" to a registry path`);
48
+ return;
32
49
  }
33
50
 
34
- // Check if this path (or a parent) is in config → remove from config + delete files
51
+ // Check if this path (or a parent) is in config
35
52
  const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
36
53
 
37
54
  if (isConfigPath) {
38
- config.removePattern(workspaceDir, regPath);
55
+ // Remove from sparse checkout
56
+ try {
57
+ removeFromSparseCheckout(AW_HOME, [`${REGISTRY_DIR}/${regPath}`]);
58
+ } catch (e) {
59
+ fmt.logWarn(`Could not update sparse checkout: ${e.message}`);
60
+ }
61
+
62
+ config.removePattern(GLOBAL_AW_DIR, regPath);
39
63
  fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
40
64
  }
41
65
 
42
- // Delete matching local files
43
- const removed = deleteMatchingFiles(workspaceDir, regPath);
44
-
45
- if (removed > 0) {
46
- fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
47
- } else if (!isConfigPath) {
48
- fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
66
+ // Count removed files (they disappear from working tree via sparse checkout)
67
+ const registryAbsPath = join(AW_HOME, REGISTRY_DIR, regPath);
68
+ let removed = 0;
69
+ if (!existsSync(registryAbsPath)) {
70
+ removed = 1; // sparse checkout removed it
49
71
  }
50
72
 
51
- if (!isConfigPath && removed > 0) {
52
- fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
73
+ if (!isConfigPath && removed === 0) {
74
+ fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
75
+ return;
53
76
  }
54
77
 
55
- fmt.outro('Done');
56
- }
57
-
58
- /**
59
- * Find and delete local files whose registry path matches the given path.
60
- */
61
- function deleteMatchingFiles(workspaceDir, path) {
62
- const manifest = loadManifest(workspaceDir);
63
- let removed = 0;
64
-
65
- for (const [manifestKey] of Object.entries(manifest.files)) {
66
- const registryPath = manifestKeyToRegistryPath(manifestKey);
67
-
68
- if (matchesAny(registryPath, [path])) {
69
- const filePath = join(workspaceDir, manifestKey);
70
- if (existsSync(filePath)) {
71
- rmSync(filePath, { recursive: true, force: true });
72
- removed++;
73
- }
74
- delete manifest.files[manifestKey];
75
- }
78
+ if (!isConfigPath) {
79
+ fmt.logWarn(`Path was not in sync config — no sparse checkout change made`);
76
80
  }
77
81
 
78
- saveManifest(workspaceDir, manifest);
79
- return removed;
80
- }
82
+ // Re-link to remove dead symlinks
83
+ linkWorkspace(HOME, null, { silent: true });
81
84
 
82
- /**
83
- * Convert manifest key to registry path.
84
- * Manifest key now mirrors registry: "platform/agents/architecture-reviewer.md" → "platform/agents/architecture-reviewer"
85
- */
86
- function manifestKeyToRegistryPath(manifestKey) {
87
- return manifestKey.replace(/\.md$/, '');
85
+ fmt.outro(`⟁ Dropped ${chalk.cyan(regPath)}`);
88
86
  }