@ghl-ai/aw 0.1.36-beta.12 → 0.1.36-beta.121
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 +2 -2
- package/commands/init.mjs +101 -33
- package/commands/link-project.mjs +9 -10
- package/commands/nuke.mjs +153 -149
- package/commands/pull.mjs +149 -87
- package/commands/push.mjs +336 -145
- package/commands/search.mjs +16 -12
- package/commands/status.mjs +75 -32
- package/constants.mjs +8 -1
- package/ecc.mjs +113 -30
- package/fmt.mjs +36 -3
- package/git.mjs +329 -117
- package/hooks.mjs +15 -2
- package/integrate.mjs +16 -6
- package/link.mjs +9 -4
- package/mcp.mjs +170 -36
- package/package.json +7 -4
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
package/commands/init.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
|
|
5
5
|
// Uses IDE tasks for auto-pull on workspace open.
|
|
6
6
|
|
|
7
|
-
import { existsSync, writeFileSync, symlinkSync, lstatSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { existsSync, writeFileSync, symlinkSync, lstatSync, readdirSync, readFileSync, rmSync, realpathSync, appendFileSync } from 'node:fs';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
9
|
import { join, dirname, sep } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
@@ -28,16 +28,43 @@ import {
|
|
|
28
28
|
includeToSparsePaths,
|
|
29
29
|
sparseCheckoutAsync,
|
|
30
30
|
cleanup,
|
|
31
|
+
syncWorktreeSparseCheckout,
|
|
32
|
+
findNearestWorktree,
|
|
31
33
|
} from '../git.mjs';
|
|
32
|
-
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
34
|
+
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
|
|
33
35
|
|
|
34
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
37
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// Resolve HOME to the real path — on macOS /var is a symlink to /private/var,
|
|
40
|
+
// so homedir() returns /var/... while process.cwd() returns /private/var/...
|
|
41
|
+
// Without normalization, the `cwd !== HOME` guard would fail and HOME would be
|
|
42
|
+
// treated as a project directory, causing addProjectWorktree to delete ~/.aw.
|
|
43
|
+
const _rawHome = homedir();
|
|
44
|
+
const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _rawHome; } })();
|
|
38
45
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
39
46
|
const AW_HOME = join(HOME, '.aw');
|
|
40
47
|
|
|
48
|
+
// ── Ensure ~/.aw/.gitignore has personal/local entries ───────────────────
|
|
49
|
+
|
|
50
|
+
const AW_GITIGNORE_ENTRIES = [
|
|
51
|
+
'.aw_registry/.sync-config.json',
|
|
52
|
+
'hooks/',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function ensureAwGitignore(awHome) {
|
|
56
|
+
// Use .git/info/exclude so the tracked .gitignore stays clean
|
|
57
|
+
const excludePath = join(awHome, '.git', 'info', 'exclude');
|
|
58
|
+
let existing = '';
|
|
59
|
+
try { existing = readFileSync(excludePath, 'utf8'); } catch { /* doesn't exist yet */ }
|
|
60
|
+
const missing = AW_GITIGNORE_ENTRIES.filter(e => !existing.includes(e));
|
|
61
|
+
if (missing.length === 0) return;
|
|
62
|
+
const block = (existing.endsWith('\n') || existing === '' ? '' : '\n')
|
|
63
|
+
+ '# aw: personal/local — do not commit\n'
|
|
64
|
+
+ missing.join('\n') + '\n';
|
|
65
|
+
try { appendFileSync(excludePath, block); } catch { /* best effort */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
42
69
|
|
|
43
70
|
function installIdeTasks() {
|
|
@@ -83,14 +110,13 @@ export async function initCommand(args) {
|
|
|
83
110
|
let user = args['--user'] || '';
|
|
84
111
|
const silent = args['--silent'] === true;
|
|
85
112
|
|
|
86
|
-
fmt.intro(`aw init ${chalk.dim('v' + VERSION)}`);
|
|
113
|
+
if (!silent) fmt.intro(`aw init ${chalk.dim('v' + VERSION)}`);
|
|
87
114
|
|
|
88
115
|
// ── Validate ──────────────────────────────────────────────────────────
|
|
89
116
|
|
|
90
117
|
let nsParts = namespace ? namespace.split('/') : [];
|
|
91
118
|
let team = nsParts[0] || null;
|
|
92
119
|
let subTeam = nsParts[1] || null;
|
|
93
|
-
let teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
94
120
|
let folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
95
121
|
|
|
96
122
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
@@ -124,7 +150,7 @@ export async function initCommand(args) {
|
|
|
124
150
|
|
|
125
151
|
// ── Detect installation state ─────────────────────────────────────────
|
|
126
152
|
|
|
127
|
-
const repoUrl =
|
|
153
|
+
const repoUrl = REGISTRY_URL;
|
|
128
154
|
const isGitNative = isValidClone(AW_HOME, repoUrl);
|
|
129
155
|
const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
|
|
130
156
|
|
|
@@ -132,6 +158,8 @@ export async function initCommand(args) {
|
|
|
132
158
|
|
|
133
159
|
let namespaceExistsInRemote = false;
|
|
134
160
|
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
161
|
+
const probeSpinner = fmt.spinner();
|
|
162
|
+
probeSpinner.start(`Checking namespace ${chalk.cyan(folderName)} in registry...`);
|
|
135
163
|
try {
|
|
136
164
|
const probePaths = includeToSparsePaths([folderName]);
|
|
137
165
|
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
@@ -143,7 +171,11 @@ export async function initCommand(args) {
|
|
|
143
171
|
} finally {
|
|
144
172
|
cleanup(probeDir);
|
|
145
173
|
}
|
|
174
|
+
probeSpinner.stop(namespaceExistsInRemote
|
|
175
|
+
? `Namespace ${chalk.cyan(folderName)} found in registry`
|
|
176
|
+
: `Namespace ${chalk.cyan(folderName)} not yet in registry`);
|
|
146
177
|
} catch {
|
|
178
|
+
probeSpinner.stop(chalk.dim('Could not verify namespace (continuing)'));
|
|
147
179
|
namespaceExistsInRemote = true;
|
|
148
180
|
}
|
|
149
181
|
}
|
|
@@ -163,11 +195,12 @@ export async function initCommand(args) {
|
|
|
163
195
|
}
|
|
164
196
|
|
|
165
197
|
if (choice === 'platform-only') {
|
|
166
|
-
namespace = null; team = null; subTeam = null;
|
|
198
|
+
namespace = null; team = null; subTeam = null; folderName = null;
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
const
|
|
202
|
+
const _rawCwd = process.cwd();
|
|
203
|
+
const cwd = (() => { try { return realpathSync(_rawCwd); } catch { return _rawCwd; } })();
|
|
171
204
|
|
|
172
205
|
// ── Re-init path: already set up with native git clone ────────────────
|
|
173
206
|
|
|
@@ -184,22 +217,37 @@ export async function initCommand(args) {
|
|
|
184
217
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
185
218
|
}
|
|
186
219
|
|
|
187
|
-
const s = fmt.spinner
|
|
188
|
-
if (!silent) s.start('Fetching latest...');
|
|
220
|
+
const s = fmt.spinner();
|
|
221
|
+
if (!silent) s.start('Fetching latest from registry...');
|
|
189
222
|
try {
|
|
190
|
-
fetchAndMerge(AW_HOME);
|
|
191
|
-
if (!silent)
|
|
223
|
+
const { conflicts } = await fetchAndMerge(AW_HOME, { silent });
|
|
224
|
+
if (!silent) {
|
|
225
|
+
if (conflicts.length > 0) {
|
|
226
|
+
s.stop(chalk.yellow(`Conflicts in ${conflicts.length} file${conflicts.length > 1 ? 's' : ''} — resolve then run aw init again`));
|
|
227
|
+
fmt.logWarn(conflicts.map(f => ` • ${f}`).join('\n'));
|
|
228
|
+
} else {
|
|
229
|
+
s.stop('Registry up to date');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
192
232
|
} catch (e) {
|
|
193
|
-
if (!silent) s.stop(chalk.yellow('Fetch failed
|
|
233
|
+
if (!silent) s.stop(chalk.yellow('Fetch failed — continuing with local cache'));
|
|
194
234
|
}
|
|
195
235
|
|
|
236
|
+
ensureAwGitignore(AW_HOME);
|
|
196
237
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
197
238
|
|
|
239
|
+
// Ensure project worktree sparse checkout matches the global clone.
|
|
240
|
+
// Covers the case where a namespace was added from HOME (or another project)
|
|
241
|
+
// and this project's .aw/ hasn't been updated yet.
|
|
242
|
+
const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
|
|
243
|
+
if (localAw) {
|
|
244
|
+
try { syncWorktreeSparseCheckout(AW_HOME, localAw); } catch { /* best effort */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
198
247
|
await installAwEcc(cwd, { silent });
|
|
199
248
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
200
249
|
initAwDocs(HOME);
|
|
201
250
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
202
|
-
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
203
251
|
installGlobalHooks();
|
|
204
252
|
|
|
205
253
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
@@ -215,10 +263,13 @@ export async function initCommand(args) {
|
|
|
215
263
|
}
|
|
216
264
|
|
|
217
265
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
218
|
-
if (
|
|
266
|
+
// Only skip if already a valid symlink (new model). Old git worktrees must be migrated.
|
|
267
|
+
const awLink = join(cwd, '.aw');
|
|
268
|
+
const isAlreadySymlink = (() => { try { return lstatSync(awLink).isSymbolicLink() && existsSync(awLink); } catch { return false; } })();
|
|
269
|
+
if (cwd !== HOME && !isInsideAw && !isAlreadySymlink) {
|
|
219
270
|
try {
|
|
220
271
|
addProjectWorktree(AW_HOME, cwd);
|
|
221
|
-
if (!silent) fmt.logStep('Linked current project
|
|
272
|
+
if (!silent) fmt.logStep('Linked current project');
|
|
222
273
|
} catch { /* best effort */ }
|
|
223
274
|
}
|
|
224
275
|
|
|
@@ -226,18 +277,18 @@ export async function initCommand(args) {
|
|
|
226
277
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
227
278
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
228
279
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
229
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
230
|
-
generateCommands(HOME);
|
|
280
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
281
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
231
282
|
|
|
232
283
|
if (silent) {
|
|
233
284
|
autoUpdate(await args._updateCheck);
|
|
234
285
|
} else {
|
|
235
286
|
fmt.outro([
|
|
236
|
-
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : '
|
|
287
|
+
`⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
|
|
237
288
|
'',
|
|
238
|
-
` ${chalk.green('✓')} Registry
|
|
239
|
-
` ${chalk.green('✓')} IDE
|
|
240
|
-
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')}
|
|
289
|
+
` ${chalk.green('✓')} Registry synced`,
|
|
290
|
+
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
291
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
241
292
|
].filter(Boolean).join('\n'));
|
|
242
293
|
}
|
|
243
294
|
return;
|
|
@@ -282,7 +333,8 @@ export async function initCommand(args) {
|
|
|
282
333
|
s.start(`Cloning registry...`);
|
|
283
334
|
|
|
284
335
|
try {
|
|
285
|
-
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
336
|
+
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
337
|
+
ensureAwGitignore(AW_HOME);
|
|
286
338
|
s.stop('Registry cloned');
|
|
287
339
|
} catch (e) {
|
|
288
340
|
s.stop(chalk.red('Clone failed'));
|
|
@@ -290,13 +342,25 @@ export async function initCommand(args) {
|
|
|
290
342
|
}
|
|
291
343
|
|
|
292
344
|
// Create backward-compat symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/
|
|
293
|
-
|
|
345
|
+
// Use lstatSync (not existsSync) so we handle dangling symlinks correctly.
|
|
346
|
+
let awRegistryLstat = null;
|
|
347
|
+
try { awRegistryLstat = lstatSync(GLOBAL_AW_DIR); } catch { /* doesn't exist */ }
|
|
348
|
+
if (!awRegistryLstat) {
|
|
294
349
|
try {
|
|
295
350
|
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
296
351
|
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
297
352
|
} catch (e) {
|
|
298
353
|
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
299
354
|
}
|
|
355
|
+
} else if (awRegistryLstat.isSymbolicLink()) {
|
|
356
|
+
// Stale or dangling — re-point to the new clone
|
|
357
|
+
try {
|
|
358
|
+
rmSync(GLOBAL_AW_DIR);
|
|
359
|
+
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
360
|
+
fmt.logStep('Updated ~/.aw_registry/ symlink');
|
|
361
|
+
} catch (e) {
|
|
362
|
+
fmt.logWarn(`Could not update symlink ~/.aw_registry/: ${e.message}`);
|
|
363
|
+
}
|
|
300
364
|
}
|
|
301
365
|
|
|
302
366
|
// Create sync config
|
|
@@ -309,8 +373,7 @@ export async function initCommand(args) {
|
|
|
309
373
|
await installAwEcc(cwd, { silent });
|
|
310
374
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
311
375
|
initAwDocs(HOME);
|
|
312
|
-
const mcpFiles = await setupMcp(HOME, team) || [];
|
|
313
|
-
if (cwd !== HOME) await setupMcp(cwd, team);
|
|
376
|
+
const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
|
|
314
377
|
const hooksInstalled = installGlobalHooks();
|
|
315
378
|
installIdeTasks();
|
|
316
379
|
|
|
@@ -325,28 +388,33 @@ export async function initCommand(args) {
|
|
|
325
388
|
} catch { /* not there, fine */ }
|
|
326
389
|
}
|
|
327
390
|
|
|
328
|
-
// Step 4: Link current project as a
|
|
391
|
+
// Step 4: Link current project as a symlink to ~/.aw (gives IDE git panel, shared across all workspaces)
|
|
329
392
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
330
|
-
|
|
393
|
+
const awLinkFresh = join(cwd, '.aw');
|
|
394
|
+
const isAlreadySymlinkFresh = (() => { try { return lstatSync(awLinkFresh).isSymbolicLink() && existsSync(awLinkFresh); } catch { return false; } })();
|
|
395
|
+
if (cwd !== HOME && !isInsideAw && !isAlreadySymlinkFresh) {
|
|
331
396
|
try {
|
|
332
397
|
addProjectWorktree(AW_HOME, cwd);
|
|
333
|
-
fmt.logStep('Linked current project
|
|
398
|
+
fmt.logStep('Linked current project');
|
|
334
399
|
} catch { /* best effort */ }
|
|
335
400
|
}
|
|
336
401
|
|
|
337
402
|
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
338
403
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
339
|
-
fmt.
|
|
404
|
+
const ideSpinner = fmt.spinner();
|
|
405
|
+
ideSpinner.start('Wiring IDE symlinks...');
|
|
340
406
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
341
407
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
342
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
343
|
-
|
|
408
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
409
|
+
ideSpinner.message('Generating commands...');
|
|
410
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
411
|
+
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
344
412
|
|
|
345
413
|
// Offer to update if a newer version is available
|
|
346
414
|
await promptUpdate(await args._updateCheck);
|
|
347
415
|
|
|
348
416
|
fmt.outro([
|
|
349
|
-
'Install complete',
|
|
417
|
+
'⟁ Install complete',
|
|
350
418
|
'',
|
|
351
419
|
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
352
420
|
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
8
|
import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
|
|
9
|
-
import { REGISTRY_DIR,
|
|
9
|
+
import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
|
|
10
10
|
import { linkWorkspace } from '../link.mjs';
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
12
|
|
|
@@ -18,8 +18,7 @@ export function linkProjectCommand(args) {
|
|
|
18
18
|
|
|
19
19
|
fmt.intro('aw link');
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
if (!isValidClone(AW_HOME, repoUrl)) {
|
|
21
|
+
if (!isValidClone(AW_HOME, REGISTRY_URL)) {
|
|
23
22
|
fmt.cancel('Registry not initialized. Run: aw init');
|
|
24
23
|
return;
|
|
25
24
|
}
|
|
@@ -39,9 +38,9 @@ export function linkProjectCommand(args) {
|
|
|
39
38
|
// Worktree exists — refresh global IDE symlinks pointing to this project's registry
|
|
40
39
|
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
41
40
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
42
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
43
|
-
generateCommands(HOME);
|
|
44
|
-
fmt.logSuccess(`Already linked — refreshed IDE symlinks`);
|
|
41
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
42
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
43
|
+
fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`);
|
|
45
44
|
return;
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -49,10 +48,10 @@ export function linkProjectCommand(args) {
|
|
|
49
48
|
addProjectWorktree(AW_HOME, cwd);
|
|
50
49
|
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
51
50
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
52
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
53
|
-
generateCommands(HOME);
|
|
51
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
52
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
54
53
|
fmt.logSuccess([
|
|
55
|
-
`
|
|
54
|
+
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
|
56
55
|
'',
|
|
57
56
|
` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
|
|
58
57
|
` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
|
|
@@ -62,5 +61,5 @@ export function linkProjectCommand(args) {
|
|
|
62
61
|
fmt.cancel(`Failed to link project: ${e.message}`);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
fmt.outro('Done');
|
|
64
|
+
fmt.outro('⟁ Done');
|
|
66
65
|
}
|