@ghl-ai/aw 0.1.36-beta.5 → 0.1.36-beta.50
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 +68 -36
- package/commands/link-project.mjs +19 -11
- package/commands/nuke.mjs +121 -129
- package/commands/pull.mjs +68 -12
- package/commands/push.mjs +239 -144
- package/commands/search.mjs +16 -12
- package/commands/status.mjs +9 -8
- package/constants.mjs +7 -0
- package/ecc.mjs +1 -0
- package/fmt.mjs +36 -3
- package/git.mjs +158 -39
- package/hooks.mjs +51 -2
- package/integrate.mjs +2 -2
- package/link.mjs +12 -4
- package/mcp.mjs +16 -1
- 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 } from 'node:fs';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
|
-
import { join, dirname } from 'node:path';
|
|
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';
|
|
@@ -30,7 +29,7 @@ import {
|
|
|
30
29
|
sparseCheckoutAsync,
|
|
31
30
|
cleanup,
|
|
32
31
|
} from '../git.mjs';
|
|
33
|
-
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
32
|
+
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
|
|
34
33
|
|
|
35
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
35
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
@@ -91,7 +90,6 @@ export async function initCommand(args) {
|
|
|
91
90
|
let nsParts = namespace ? namespace.split('/') : [];
|
|
92
91
|
let team = nsParts[0] || null;
|
|
93
92
|
let subTeam = nsParts[1] || null;
|
|
94
|
-
let teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
95
93
|
let folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
96
94
|
|
|
97
95
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
@@ -125,7 +123,7 @@ export async function initCommand(args) {
|
|
|
125
123
|
|
|
126
124
|
// ── Detect installation state ─────────────────────────────────────────
|
|
127
125
|
|
|
128
|
-
const repoUrl =
|
|
126
|
+
const repoUrl = REGISTRY_URL;
|
|
129
127
|
const isGitNative = isValidClone(AW_HOME, repoUrl);
|
|
130
128
|
const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
|
|
131
129
|
|
|
@@ -133,6 +131,8 @@ export async function initCommand(args) {
|
|
|
133
131
|
|
|
134
132
|
let namespaceExistsInRemote = false;
|
|
135
133
|
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
134
|
+
const probeSpinner = fmt.spinner();
|
|
135
|
+
probeSpinner.start(`Checking namespace ${chalk.cyan(folderName)} in registry...`);
|
|
136
136
|
try {
|
|
137
137
|
const probePaths = includeToSparsePaths([folderName]);
|
|
138
138
|
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
@@ -144,7 +144,11 @@ export async function initCommand(args) {
|
|
|
144
144
|
} finally {
|
|
145
145
|
cleanup(probeDir);
|
|
146
146
|
}
|
|
147
|
+
probeSpinner.stop(namespaceExistsInRemote
|
|
148
|
+
? `Namespace ${chalk.cyan(folderName)} found in registry`
|
|
149
|
+
: `Namespace ${chalk.cyan(folderName)} not yet in registry`);
|
|
147
150
|
} catch {
|
|
151
|
+
probeSpinner.stop(chalk.dim('Could not verify namespace (continuing)'));
|
|
148
152
|
namespaceExistsInRemote = true;
|
|
149
153
|
}
|
|
150
154
|
}
|
|
@@ -164,7 +168,7 @@ export async function initCommand(args) {
|
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
if (choice === 'platform-only') {
|
|
167
|
-
namespace = null; team = null; subTeam = null;
|
|
171
|
+
namespace = null; team = null; subTeam = null; folderName = null;
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -185,47 +189,60 @@ export async function initCommand(args) {
|
|
|
185
189
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
const s = fmt.spinner
|
|
189
|
-
if (!silent) s.start('Fetching latest...');
|
|
192
|
+
const s = fmt.spinner();
|
|
193
|
+
if (!silent) s.start('Fetching latest from registry...');
|
|
190
194
|
try {
|
|
191
|
-
fetchAndMerge(AW_HOME);
|
|
192
|
-
if (!silent) s.stop('Registry
|
|
195
|
+
await fetchAndMerge(AW_HOME);
|
|
196
|
+
if (!silent) s.stop('Registry up to date');
|
|
193
197
|
} catch (e) {
|
|
194
|
-
if (!silent) s.stop(chalk.yellow('Fetch failed
|
|
198
|
+
if (!silent) s.stop(chalk.yellow('Fetch failed — continuing with local cache'));
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
198
202
|
|
|
199
|
-
linkWorkspace(HOME);
|
|
200
203
|
await installAwEcc(cwd, { silent });
|
|
201
|
-
generateCommands(HOME);
|
|
202
204
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
203
205
|
initAwDocs(HOME);
|
|
204
206
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
205
207
|
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
206
208
|
installGlobalHooks();
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
211
|
+
if (cwd !== HOME) {
|
|
212
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
213
|
+
try {
|
|
214
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
215
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
216
|
+
rmSync(oldLocalHook);
|
|
217
|
+
if (!silent) fmt.logStep('Removed legacy .git/hooks/post-checkout');
|
|
218
|
+
}
|
|
219
|
+
} catch { /* not there, fine */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
223
|
+
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
209
224
|
try {
|
|
210
225
|
addProjectWorktree(AW_HOME, cwd);
|
|
211
226
|
if (!silent) fmt.logStep('Linked current project as git worktree');
|
|
212
227
|
} catch { /* best effort */ }
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
230
|
+
// Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
231
|
+
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
232
|
+
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
233
|
+
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
234
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
235
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
219
236
|
|
|
220
237
|
if (silent) {
|
|
221
238
|
autoUpdate(await args._updateCheck);
|
|
222
239
|
} else {
|
|
223
240
|
fmt.outro([
|
|
224
|
-
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : '
|
|
241
|
+
`⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
|
|
225
242
|
'',
|
|
226
|
-
` ${chalk.green('✓')} Registry
|
|
227
|
-
` ${chalk.green('✓')} IDE
|
|
228
|
-
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')}
|
|
243
|
+
` ${chalk.green('✓')} Registry synced`,
|
|
244
|
+
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
245
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
229
246
|
].filter(Boolean).join('\n'));
|
|
230
247
|
}
|
|
231
248
|
return;
|
|
@@ -270,7 +287,7 @@ export async function initCommand(args) {
|
|
|
270
287
|
s.start(`Cloning registry...`);
|
|
271
288
|
|
|
272
289
|
try {
|
|
273
|
-
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
290
|
+
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
274
291
|
s.stop('Registry cloned');
|
|
275
292
|
} catch (e) {
|
|
276
293
|
s.stop(chalk.red('Clone failed'));
|
|
@@ -293,36 +310,51 @@ export async function initCommand(args) {
|
|
|
293
310
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
294
311
|
}
|
|
295
312
|
|
|
296
|
-
// Step 3:
|
|
297
|
-
fmt.logStep('Linking IDE symlinks...');
|
|
298
|
-
linkWorkspace(HOME);
|
|
313
|
+
// Step 3: Setup tasks, MCP, hooks
|
|
299
314
|
await installAwEcc(cwd, { silent });
|
|
300
|
-
generateCommands(HOME);
|
|
301
315
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
302
316
|
initAwDocs(HOME);
|
|
303
|
-
const mcpFiles = await setupMcp(HOME, team) || [];
|
|
304
|
-
if (cwd !== HOME) await setupMcp(cwd, team);
|
|
317
|
+
const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
|
|
318
|
+
if (cwd !== HOME) await setupMcp(cwd, team, { silent });
|
|
305
319
|
const hooksInstalled = installGlobalHooks();
|
|
306
320
|
installIdeTasks();
|
|
307
321
|
|
|
322
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
323
|
+
if (cwd !== HOME) {
|
|
324
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
325
|
+
try {
|
|
326
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
327
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
328
|
+
rmSync(oldLocalHook);
|
|
329
|
+
}
|
|
330
|
+
} catch { /* not there, fine */ }
|
|
331
|
+
}
|
|
332
|
+
|
|
308
333
|
// Step 4: Link current project as a git worktree (gives IDE git panel)
|
|
309
|
-
|
|
334
|
+
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
335
|
+
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
310
336
|
try {
|
|
311
337
|
addProjectWorktree(AW_HOME, cwd);
|
|
312
338
|
fmt.logStep('Linked current project as git worktree');
|
|
313
339
|
} catch { /* best effort */ }
|
|
314
340
|
}
|
|
315
341
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
342
|
+
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
343
|
+
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
344
|
+
const ideSpinner = fmt.spinner();
|
|
345
|
+
ideSpinner.start('Wiring IDE symlinks...');
|
|
346
|
+
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
347
|
+
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
348
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
349
|
+
ideSpinner.message('Generating commands...');
|
|
350
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
351
|
+
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
320
352
|
|
|
321
353
|
// Offer to update if a newer version is available
|
|
322
354
|
await promptUpdate(await args._updateCheck);
|
|
323
355
|
|
|
324
356
|
fmt.outro([
|
|
325
|
-
'Install complete',
|
|
357
|
+
'⟁ Install complete',
|
|
326
358
|
'',
|
|
327
359
|
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
328
360
|
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// commands/link-project.mjs — Link current project to registry via git worktree
|
|
2
2
|
|
|
3
|
+
import { existsSync, lstatSync, rmSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import * as fmt from '../fmt.mjs';
|
|
6
7
|
import { chalk } from '../fmt.mjs';
|
|
7
8
|
import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
|
|
8
|
-
import { REGISTRY_DIR,
|
|
9
|
+
import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
|
|
9
10
|
import { linkWorkspace } from '../link.mjs';
|
|
10
11
|
import { generateCommands } from '../integrate.mjs';
|
|
11
12
|
|
|
@@ -17,8 +18,7 @@ export function linkProjectCommand(args) {
|
|
|
17
18
|
|
|
18
19
|
fmt.intro('aw link');
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
if (!isValidClone(AW_HOME, repoUrl)) {
|
|
21
|
+
if (!isValidClone(AW_HOME, REGISTRY_URL)) {
|
|
22
22
|
fmt.cancel('Registry not initialized. Run: aw init');
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
@@ -30,20 +30,28 @@ export function linkProjectCommand(args) {
|
|
|
30
30
|
|
|
31
31
|
const worktreeDir = join(cwd, '.aw');
|
|
32
32
|
|
|
33
|
+
// Remove stale project-root .aw_registry symlink from old installs
|
|
34
|
+
const staleSymlink = join(cwd, REGISTRY_DIR);
|
|
35
|
+
try { if (lstatSync(staleSymlink).isSymbolicLink()) rmSync(staleSymlink); } catch { /* fine */ }
|
|
36
|
+
|
|
33
37
|
if (isWorktree(worktreeDir)) {
|
|
34
|
-
// Worktree exists — refresh IDE symlinks
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// Worktree exists — refresh global IDE symlinks pointing to this project's registry
|
|
39
|
+
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
40
|
+
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
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`);
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
try {
|
|
42
48
|
addProjectWorktree(AW_HOME, cwd);
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
50
|
+
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
51
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
52
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
45
53
|
fmt.logSuccess([
|
|
46
|
-
`
|
|
54
|
+
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
|
47
55
|
'',
|
|
48
56
|
` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
|
|
49
57
|
` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
|
|
@@ -53,5 +61,5 @@ export function linkProjectCommand(args) {
|
|
|
53
61
|
fmt.cancel(`Failed to link project: ${e.message}`);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
fmt.outro('Done');
|
|
64
|
+
fmt.outro('⟁ Done');
|
|
57
65
|
}
|