@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 +3 -1
- package/commands/daemon.mjs +32 -23
- package/commands/drop.mjs +46 -48
- package/commands/init.mjs +210 -147
- package/commands/link-project.mjs +49 -10
- package/commands/nuke.mjs +159 -101
- package/commands/pull.mjs +159 -367
- package/commands/push.mjs +517 -395
- package/commands/search.mjs +19 -14
- package/commands/status.mjs +101 -83
- package/constants.mjs +14 -1
- package/ecc.mjs +113 -30
- package/fmt.mjs +36 -3
- package/git.mjs +663 -6
- package/hooks.mjs +53 -5
- package/integrate.mjs +19 -7
- package/link.mjs +14 -4
- package/mcp.mjs +164 -36
- package/package.json +7 -4
- package/apply.mjs +0 -79
- package/manifest.mjs +0 -64
- package/plan.mjs +0 -147
package/cli.mjs
CHANGED
|
@@ -65,7 +65,8 @@ function parseArgs(argv) {
|
|
|
65
65
|
|
|
66
66
|
function printHelp() {
|
|
67
67
|
fmt.banner('aw', {
|
|
68
|
-
|
|
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)'),
|
package/commands/daemon.mjs
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fmt.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
|
24
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
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
|
|
52
|
-
fmt.
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
}
|