@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 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
- 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 = `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,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 ? 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);
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
- if (cwd !== HOME && !isWorktree(join(cwd, '.aw'))) {
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
- if (cwd !== HOME) {
216
- linkWorkspace(cwd);
217
- generateCommands(cwd);
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` : 'Sync complete',
241
+ `⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
225
242
  '',
226
- ` ${chalk.green('✓')} Registry updated`,
227
- ` ${chalk.green('✓')} IDE integration refreshed`,
228
- cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Current project linked` : null,
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: Link IDE dirs + setup tasks
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
- if (cwd !== HOME && !isWorktree(join(cwd, '.aw'))) {
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
- if (cwd !== HOME) {
317
- linkWorkspace(cwd);
318
- generateCommands(cwd);
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, REGISTRY_REPO } from '../constants.mjs';
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
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
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 in case they're stale
35
- linkWorkspace(cwd);
36
- generateCommands(cwd);
37
- fmt.logSuccess(`Already linked refreshed IDE symlinks`);
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
- linkWorkspace(cwd);
44
- generateCommands(cwd);
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
- `Linked project as git worktree`,
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
  }