@ghl-ai/aw 0.1.36-beta.6 → 0.1.36-beta.61
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 +66 -26
- package/commands/link-project.mjs +14 -11
- package/commands/nuke.mjs +115 -131
- package/commands/pull.mjs +67 -11
- 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 +30 -12
- package/fmt.mjs +36 -3
- package/git.mjs +164 -39
- package/hooks.mjs +49 -0
- package/integrate.mjs +2 -2
- package/link.mjs +9 -4
- package/mcp.mjs +57 -32
- 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
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,13 +189,13 @@ 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);
|
|
@@ -200,9 +204,20 @@ export async function initCommand(args) {
|
|
|
200
204
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
201
205
|
initAwDocs(HOME);
|
|
202
206
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
203
|
-
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
204
207
|
installGlobalHooks();
|
|
205
208
|
|
|
209
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
210
|
+
if (cwd !== HOME) {
|
|
211
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
212
|
+
try {
|
|
213
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
214
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
215
|
+
rmSync(oldLocalHook);
|
|
216
|
+
if (!silent) fmt.logStep('Removed legacy .git/hooks/post-checkout');
|
|
217
|
+
}
|
|
218
|
+
} catch { /* not there, fine */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
206
221
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
207
222
|
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
208
223
|
try {
|
|
@@ -215,18 +230,18 @@ export async function initCommand(args) {
|
|
|
215
230
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
216
231
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
217
232
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
218
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
219
|
-
generateCommands(HOME);
|
|
233
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
234
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
220
235
|
|
|
221
236
|
if (silent) {
|
|
222
237
|
autoUpdate(await args._updateCheck);
|
|
223
238
|
} else {
|
|
224
239
|
fmt.outro([
|
|
225
|
-
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : '
|
|
240
|
+
`⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
|
|
226
241
|
'',
|
|
227
|
-
` ${chalk.green('✓')} Registry
|
|
228
|
-
` ${chalk.green('✓')} IDE
|
|
229
|
-
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')}
|
|
242
|
+
` ${chalk.green('✓')} Registry synced`,
|
|
243
|
+
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
244
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
230
245
|
].filter(Boolean).join('\n'));
|
|
231
246
|
}
|
|
232
247
|
return;
|
|
@@ -271,7 +286,7 @@ export async function initCommand(args) {
|
|
|
271
286
|
s.start(`Cloning registry...`);
|
|
272
287
|
|
|
273
288
|
try {
|
|
274
|
-
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
289
|
+
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
275
290
|
s.stop('Registry cloned');
|
|
276
291
|
} catch (e) {
|
|
277
292
|
s.stop(chalk.red('Clone failed'));
|
|
@@ -279,13 +294,25 @@ export async function initCommand(args) {
|
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
// Create backward-compat symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/
|
|
282
|
-
|
|
297
|
+
// Use lstatSync (not existsSync) so we handle dangling symlinks correctly.
|
|
298
|
+
let awRegistryLstat = null;
|
|
299
|
+
try { awRegistryLstat = lstatSync(GLOBAL_AW_DIR); } catch { /* doesn't exist */ }
|
|
300
|
+
if (!awRegistryLstat) {
|
|
283
301
|
try {
|
|
284
302
|
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
285
303
|
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
286
304
|
} catch (e) {
|
|
287
305
|
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
288
306
|
}
|
|
307
|
+
} else if (awRegistryLstat.isSymbolicLink()) {
|
|
308
|
+
// Stale or dangling — re-point to the new clone
|
|
309
|
+
try {
|
|
310
|
+
rmSync(GLOBAL_AW_DIR);
|
|
311
|
+
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
312
|
+
fmt.logStep('Updated ~/.aw_registry/ symlink');
|
|
313
|
+
} catch (e) {
|
|
314
|
+
fmt.logWarn(`Could not update symlink ~/.aw_registry/: ${e.message}`);
|
|
315
|
+
}
|
|
289
316
|
}
|
|
290
317
|
|
|
291
318
|
// Create sync config
|
|
@@ -298,11 +325,21 @@ export async function initCommand(args) {
|
|
|
298
325
|
await installAwEcc(cwd, { silent });
|
|
299
326
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
300
327
|
initAwDocs(HOME);
|
|
301
|
-
const mcpFiles = await setupMcp(HOME, team) || [];
|
|
302
|
-
if (cwd !== HOME) await setupMcp(cwd, team);
|
|
328
|
+
const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
|
|
303
329
|
const hooksInstalled = installGlobalHooks();
|
|
304
330
|
installIdeTasks();
|
|
305
331
|
|
|
332
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
333
|
+
if (cwd !== HOME) {
|
|
334
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
335
|
+
try {
|
|
336
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
337
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
338
|
+
rmSync(oldLocalHook);
|
|
339
|
+
}
|
|
340
|
+
} catch { /* not there, fine */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
306
343
|
// Step 4: Link current project as a git worktree (gives IDE git panel)
|
|
307
344
|
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
308
345
|
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
@@ -314,17 +351,20 @@ export async function initCommand(args) {
|
|
|
314
351
|
|
|
315
352
|
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
316
353
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
317
|
-
fmt.
|
|
354
|
+
const ideSpinner = fmt.spinner();
|
|
355
|
+
ideSpinner.start('Wiring IDE symlinks...');
|
|
318
356
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
319
357
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
320
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
321
|
-
|
|
358
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
359
|
+
ideSpinner.message('Generating commands...');
|
|
360
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
361
|
+
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
322
362
|
|
|
323
363
|
// Offer to update if a newer version is available
|
|
324
364
|
await promptUpdate(await args._updateCheck);
|
|
325
365
|
|
|
326
366
|
fmt.outro([
|
|
327
|
-
'Install complete',
|
|
367
|
+
'⟁ Install complete',
|
|
328
368
|
'',
|
|
329
369
|
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
330
370
|
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// commands/link-project.mjs — Link current project to registry via git worktree
|
|
2
2
|
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, rmSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
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
|
}
|
|
@@ -31,13 +30,17 @@ export function linkProjectCommand(args) {
|
|
|
31
30
|
|
|
32
31
|
const worktreeDir = join(cwd, '.aw');
|
|
33
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
|
+
|
|
34
37
|
if (isWorktree(worktreeDir)) {
|
|
35
38
|
// Worktree exists — refresh global IDE symlinks pointing to this project's registry
|
|
36
39
|
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
37
40
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
38
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
39
|
-
generateCommands(HOME);
|
|
40
|
-
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`);
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -45,10 +48,10 @@ export function linkProjectCommand(args) {
|
|
|
45
48
|
addProjectWorktree(AW_HOME, cwd);
|
|
46
49
|
const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
|
|
47
50
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
48
|
-
linkWorkspace(HOME, awDirForLinks);
|
|
49
|
-
generateCommands(HOME);
|
|
51
|
+
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
52
|
+
const commands = generateCommands(HOME, { silent: true });
|
|
50
53
|
fmt.logSuccess([
|
|
51
|
-
`
|
|
54
|
+
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
|
52
55
|
'',
|
|
53
56
|
` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
|
|
54
57
|
` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
|
|
@@ -58,5 +61,5 @@ export function linkProjectCommand(args) {
|
|
|
58
61
|
fmt.cancel(`Failed to link project: ${e.message}`);
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
fmt.outro('Done');
|
|
64
|
+
fmt.outro('⟁ Done');
|
|
62
65
|
}
|