@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 CHANGED
@@ -65,7 +65,8 @@ function parseArgs(argv) {
65
65
 
66
66
  function printHelp() {
67
67
  fmt.banner('aw', {
68
- subtitle: ` ${chalk.dim('v' + VERSION)} ${chalk.hex('#FF6B35')('Agentic Workspace CLI')} ${chalk.dim('— pull, push & manage agents, skills and more from the registry')}`,
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)'),
@@ -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
- // Unload first if already loaded (to apply new interval)
67
- try { execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch { /* not loaded */ }
68
- execSync(`launchctl load "${PLIST_PATH}"`);
69
-
70
- fmt.logStep(`Daemon installed — runs every ${formatInterval(intervalSeconds)}`);
71
- fmt.logStep(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
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
- try { execSync(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
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
- fmt.logStep('Daemon removed.');
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
- execSync(`echo ${JSON.stringify(updated)} | crontab -`);
121
- fmt.logStep(`Cron job installed — runs every ${formatInterval(intervalSeconds)}`);
122
- fmt.logStep(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
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
- execSync(`echo ${JSON.stringify(cleaned)} | crontab -`);
130
- fmt.logStep('Cron job removed.');
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
@@ -80,7 +80,7 @@ export function dropCommand(args) {
80
80
  }
81
81
 
82
82
  // Re-link to remove dead symlinks
83
- linkWorkspace(HOME);
83
+ linkWorkspace(HOME, null, { silent: true });
84
84
 
85
- fmt.outro('Done');
85
+ fmt.outro(`⟁ Dropped ${chalk.cyan(regPath)}`);
86
86
  }
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 = `https://github.com/${REGISTRY_REPO}.git`;
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; teamNS = null; folderName = 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 ? fmt.spinner() : { start: () => {}, stop: () => {} };
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 updated');
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 (continuing with local)'));
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` : 'Sync complete',
240
+ `⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
226
241
  '',
227
- ` ${chalk.green('✓')} Registry updated`,
228
- ` ${chalk.green('✓')} IDE integration refreshed`,
229
- cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Current project linked` : null,
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
- if (!existsSync(GLOBAL_AW_DIR)) {
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.logStep('Linking IDE symlinks...');
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
- generateCommands(HOME);
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, REGISTRY_REPO } from '../constants.mjs';
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
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
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
- `Linked project as git worktree`,
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
  }