@ghl-ai/aw 0.1.36-beta.8 → 0.1.36-beta.81
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 +116 -33
- package/commands/link-project.mjs +9 -10
- package/commands/nuke.mjs +153 -130
- package/commands/pull.mjs +120 -15
- package/commands/push.mjs +263 -151
- package/commands/search.mjs +16 -12
- package/commands/status.mjs +75 -32
- package/constants.mjs +7 -0
- package/ecc.mjs +113 -30
- package/fmt.mjs +36 -3
- package/git.mjs +267 -106
- package/hooks.mjs +4 -0
- package/integrate.mjs +16 -6
- package/link.mjs +9 -4
- package/mcp.mjs +164 -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,12 +4,11 @@
|
|
|
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 } 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';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
13
12
|
import * as config from '../config.mjs';
|
|
14
13
|
import * as fmt from '../fmt.mjs';
|
|
15
14
|
import { chalk } from '../fmt.mjs';
|
|
@@ -29,16 +28,43 @@ import {
|
|
|
29
28
|
includeToSparsePaths,
|
|
30
29
|
sparseCheckoutAsync,
|
|
31
30
|
cleanup,
|
|
31
|
+
syncWorktreeSparseCheckout,
|
|
32
|
+
findNearestWorktree,
|
|
32
33
|
} from '../git.mjs';
|
|
33
|
-
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
34
|
+
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
37
38
|
|
|
38
|
-
|
|
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; } })();
|
|
39
45
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
40
46
|
const AW_HOME = join(HOME, '.aw');
|
|
41
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
|
+
|
|
42
68
|
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
43
69
|
|
|
44
70
|
function installIdeTasks() {
|
|
@@ -91,7 +117,6 @@ export async function initCommand(args) {
|
|
|
91
117
|
let nsParts = namespace ? namespace.split('/') : [];
|
|
92
118
|
let team = nsParts[0] || null;
|
|
93
119
|
let subTeam = nsParts[1] || null;
|
|
94
|
-
let teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
95
120
|
let folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
96
121
|
|
|
97
122
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
@@ -125,7 +150,7 @@ export async function initCommand(args) {
|
|
|
125
150
|
|
|
126
151
|
// ── Detect installation state ─────────────────────────────────────────
|
|
127
152
|
|
|
128
|
-
const repoUrl =
|
|
153
|
+
const repoUrl = REGISTRY_URL;
|
|
129
154
|
const isGitNative = isValidClone(AW_HOME, repoUrl);
|
|
130
155
|
const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
|
|
131
156
|
|
|
@@ -133,6 +158,8 @@ export async function initCommand(args) {
|
|
|
133
158
|
|
|
134
159
|
let namespaceExistsInRemote = false;
|
|
135
160
|
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
161
|
+
const probeSpinner = fmt.spinner();
|
|
162
|
+
probeSpinner.start(`Checking namespace ${chalk.cyan(folderName)} in registry...`);
|
|
136
163
|
try {
|
|
137
164
|
const probePaths = includeToSparsePaths([folderName]);
|
|
138
165
|
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
@@ -144,7 +171,11 @@ export async function initCommand(args) {
|
|
|
144
171
|
} finally {
|
|
145
172
|
cleanup(probeDir);
|
|
146
173
|
}
|
|
174
|
+
probeSpinner.stop(namespaceExistsInRemote
|
|
175
|
+
? `Namespace ${chalk.cyan(folderName)} found in registry`
|
|
176
|
+
: `Namespace ${chalk.cyan(folderName)} not yet in registry`);
|
|
147
177
|
} catch {
|
|
178
|
+
probeSpinner.stop(chalk.dim('Could not verify namespace (continuing)'));
|
|
148
179
|
namespaceExistsInRemote = true;
|
|
149
180
|
}
|
|
150
181
|
}
|
|
@@ -164,11 +195,12 @@ export async function initCommand(args) {
|
|
|
164
195
|
}
|
|
165
196
|
|
|
166
197
|
if (choice === 'platform-only') {
|
|
167
|
-
namespace = null; team = null; subTeam = null;
|
|
198
|
+
namespace = null; team = null; subTeam = null; folderName = null;
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
201
|
|
|
171
|
-
const
|
|
202
|
+
const _rawCwd = process.cwd();
|
|
203
|
+
const cwd = (() => { try { return realpathSync(_rawCwd); } catch { return _rawCwd; } })();
|
|
172
204
|
|
|
173
205
|
// ── Re-init path: already set up with native git clone ────────────────
|
|
174
206
|
|
|
@@ -185,29 +217,52 @@ export async function initCommand(args) {
|
|
|
185
217
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
186
218
|
}
|
|
187
219
|
|
|
188
|
-
const s = fmt.spinner
|
|
189
|
-
if (!silent) s.start('Fetching latest...');
|
|
220
|
+
const s = fmt.spinner();
|
|
221
|
+
if (!silent) s.start('Fetching latest from registry...');
|
|
190
222
|
try {
|
|
191
|
-
fetchAndMerge(AW_HOME);
|
|
192
|
-
if (!silent) s.stop('Registry
|
|
223
|
+
await fetchAndMerge(AW_HOME);
|
|
224
|
+
if (!silent) s.stop('Registry up to date');
|
|
193
225
|
} catch (e) {
|
|
194
|
-
if (!silent) s.stop(chalk.yellow('Fetch failed
|
|
226
|
+
if (!silent) s.stop(chalk.yellow('Fetch failed — continuing with local cache'));
|
|
195
227
|
}
|
|
196
228
|
|
|
229
|
+
ensureAwGitignore(AW_HOME);
|
|
197
230
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
198
231
|
|
|
232
|
+
// Ensure project worktree sparse checkout matches the global clone.
|
|
233
|
+
// Covers the case where a namespace was added from HOME (or another project)
|
|
234
|
+
// and this project's .aw/ hasn't been updated yet.
|
|
235
|
+
const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
|
|
236
|
+
if (localAw) {
|
|
237
|
+
try { syncWorktreeSparseCheckout(AW_HOME, localAw); } catch { /* best effort */ }
|
|
238
|
+
}
|
|
239
|
+
|
|
199
240
|
await installAwEcc(cwd, { silent });
|
|
200
241
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
201
242
|
initAwDocs(HOME);
|
|
202
243
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
203
|
-
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
204
244
|
installGlobalHooks();
|
|
205
245
|
|
|
246
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
247
|
+
if (cwd !== HOME) {
|
|
248
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
249
|
+
try {
|
|
250
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
251
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
252
|
+
rmSync(oldLocalHook);
|
|
253
|
+
if (!silent) fmt.logStep('Removed legacy .git/hooks/post-checkout');
|
|
254
|
+
}
|
|
255
|
+
} catch { /* not there, fine */ }
|
|
256
|
+
}
|
|
257
|
+
|
|
206
258
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
207
|
-
if (
|
|
259
|
+
// Only skip if already a valid symlink (new model). Old git worktrees must be migrated.
|
|
260
|
+
const awLink = join(cwd, '.aw');
|
|
261
|
+
const isAlreadySymlink = (() => { try { return lstatSync(awLink).isSymbolicLink() && existsSync(awLink); } catch { return false; } })();
|
|
262
|
+
if (cwd !== HOME && !isInsideAw && !isAlreadySymlink) {
|
|
208
263
|
try {
|
|
209
264
|
addProjectWorktree(AW_HOME, cwd);
|
|
210
|
-
if (!silent) fmt.logStep('Linked current project
|
|
265
|
+
if (!silent) fmt.logStep('Linked current project');
|
|
211
266
|
} catch { /* best effort */ }
|
|
212
267
|
}
|
|
213
268
|
|
|
@@ -215,18 +270,18 @@ export async function initCommand(args) {
|
|
|
215
270
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
216
271
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
217
272
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
218
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
219
|
-
generateCommands(HOME);
|
|
273
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
274
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
220
275
|
|
|
221
276
|
if (silent) {
|
|
222
277
|
autoUpdate(await args._updateCheck);
|
|
223
278
|
} else {
|
|
224
279
|
fmt.outro([
|
|
225
|
-
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : '
|
|
280
|
+
`⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
|
|
226
281
|
'',
|
|
227
|
-
` ${chalk.green('✓')} Registry
|
|
228
|
-
` ${chalk.green('✓')} IDE
|
|
229
|
-
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')}
|
|
282
|
+
` ${chalk.green('✓')} Registry synced`,
|
|
283
|
+
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
284
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
230
285
|
].filter(Boolean).join('\n'));
|
|
231
286
|
}
|
|
232
287
|
return;
|
|
@@ -271,7 +326,8 @@ export async function initCommand(args) {
|
|
|
271
326
|
s.start(`Cloning registry...`);
|
|
272
327
|
|
|
273
328
|
try {
|
|
274
|
-
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
329
|
+
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
330
|
+
ensureAwGitignore(AW_HOME);
|
|
275
331
|
s.stop('Registry cloned');
|
|
276
332
|
} catch (e) {
|
|
277
333
|
s.stop(chalk.red('Clone failed'));
|
|
@@ -279,13 +335,25 @@ export async function initCommand(args) {
|
|
|
279
335
|
}
|
|
280
336
|
|
|
281
337
|
// Create backward-compat symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/
|
|
282
|
-
|
|
338
|
+
// Use lstatSync (not existsSync) so we handle dangling symlinks correctly.
|
|
339
|
+
let awRegistryLstat = null;
|
|
340
|
+
try { awRegistryLstat = lstatSync(GLOBAL_AW_DIR); } catch { /* doesn't exist */ }
|
|
341
|
+
if (!awRegistryLstat) {
|
|
283
342
|
try {
|
|
284
343
|
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
285
344
|
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
286
345
|
} catch (e) {
|
|
287
346
|
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
288
347
|
}
|
|
348
|
+
} else if (awRegistryLstat.isSymbolicLink()) {
|
|
349
|
+
// Stale or dangling — re-point to the new clone
|
|
350
|
+
try {
|
|
351
|
+
rmSync(GLOBAL_AW_DIR);
|
|
352
|
+
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
353
|
+
fmt.logStep('Updated ~/.aw_registry/ symlink');
|
|
354
|
+
} catch (e) {
|
|
355
|
+
fmt.logWarn(`Could not update symlink ~/.aw_registry/: ${e.message}`);
|
|
356
|
+
}
|
|
289
357
|
}
|
|
290
358
|
|
|
291
359
|
// Create sync config
|
|
@@ -298,33 +366,48 @@ export async function initCommand(args) {
|
|
|
298
366
|
await installAwEcc(cwd, { silent });
|
|
299
367
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
300
368
|
initAwDocs(HOME);
|
|
301
|
-
const mcpFiles = await setupMcp(HOME, team) || [];
|
|
302
|
-
if (cwd !== HOME) await setupMcp(cwd, team);
|
|
369
|
+
const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
|
|
303
370
|
const hooksInstalled = installGlobalHooks();
|
|
304
371
|
installIdeTasks();
|
|
305
372
|
|
|
306
|
-
//
|
|
373
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
374
|
+
if (cwd !== HOME) {
|
|
375
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
376
|
+
try {
|
|
377
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
378
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
379
|
+
rmSync(oldLocalHook);
|
|
380
|
+
}
|
|
381
|
+
} catch { /* not there, fine */ }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Step 4: Link current project as a symlink to ~/.aw (gives IDE git panel, shared across all workspaces)
|
|
307
385
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
308
|
-
|
|
386
|
+
const awLinkFresh = join(cwd, '.aw');
|
|
387
|
+
const isAlreadySymlinkFresh = (() => { try { return lstatSync(awLinkFresh).isSymbolicLink() && existsSync(awLinkFresh); } catch { return false; } })();
|
|
388
|
+
if (cwd !== HOME && !isInsideAw && !isAlreadySymlinkFresh) {
|
|
309
389
|
try {
|
|
310
390
|
addProjectWorktree(AW_HOME, cwd);
|
|
311
|
-
fmt.logStep('Linked current project
|
|
391
|
+
fmt.logStep('Linked current project');
|
|
312
392
|
} catch { /* best effort */ }
|
|
313
393
|
}
|
|
314
394
|
|
|
315
395
|
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
316
396
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
317
|
-
fmt.
|
|
397
|
+
const ideSpinner = fmt.spinner();
|
|
398
|
+
ideSpinner.start('Wiring IDE symlinks...');
|
|
318
399
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
319
400
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
320
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
321
|
-
|
|
401
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
402
|
+
ideSpinner.message('Generating commands...');
|
|
403
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
404
|
+
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
322
405
|
|
|
323
406
|
// Offer to update if a newer version is available
|
|
324
407
|
await promptUpdate(await args._updateCheck);
|
|
325
408
|
|
|
326
409
|
fmt.outro([
|
|
327
|
-
'Install complete',
|
|
410
|
+
'⟁ Install complete',
|
|
328
411
|
'',
|
|
329
412
|
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
330
413
|
` ${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
|
}
|