@ghl-ai/aw 0.1.49 → 0.1.50-beta.1

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.
@@ -12,9 +12,9 @@
12
12
  # - .codex/scripts/codex-web-bootstrap.sh (passes --harness codex-web)
13
13
  #
14
14
  # Override knobs (env):
15
- # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@0.1.49-beta.0.
16
- # Override with @ghl-ai/aw@latest for the stable channel or pin
17
- # to another @ghl-ai/aw@0.1.x for reproducible CI runs.
15
+ # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest.
16
+ # Override to pin a specific @ghl-ai/aw@0.1.x for reproducible
17
+ # CI runs.
18
18
  set -Eeuo pipefail
19
19
 
20
20
  # GitHub auth is resolved inside `aw c4` so GITHUB_PAT, GITHUB_TOKEN, and
@@ -106,7 +106,7 @@ if [ -f "$ENVOY_CA" ] && [ -z "${NODE_EXTRA_CA_CERTS:-}" ]; then
106
106
  echo "[aw-c4-bootstrap] enabled NODE_EXTRA_CA_CERTS=$ENVOY_CA"
107
107
  fi
108
108
 
109
- AW_PACKAGE="${AW_PACKAGE:-@ghl-ai/aw@0.1.49-beta.0}"
109
+ AW_PACKAGE="${AW_PACKAGE:-@ghl-ai/aw@latest}"
110
110
 
111
111
  echo "[aw-c4-bootstrap] installing ${AW_PACKAGE}"
112
112
  npm install -g "${AW_PACKAGE}"
package/cli.mjs CHANGED
@@ -89,9 +89,11 @@ function printHelp() {
89
89
  sec('Setup'),
90
90
  cmd('aw init', 'Initialize workspace (auto-installs suggested integrations)'),
91
91
  cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
92
+ cmd('aw init skills <path...>', 'Initialize/link only specific skill folders'),
92
93
  cmd('aw init --no-integrations', 'Skip integration setup (Codex, Caveman, Graphify, etc)'),
93
94
  ` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
94
95
  ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
96
+ ` ${chalk.dim('Example: aw init skills platform/core/skills/pr-review')}`,
95
97
  cmd('aw init-repo', 'Scaffold cloud-bootstrap files (idempotent, --dry-run/--force/--diff)'),
96
98
 
97
99
  sec('Download'),
@@ -152,6 +154,7 @@ function printHelp() {
152
154
  cmd('aw pull <team>/agents', 'All agents from a team'),
153
155
  cmd('aw pull <team>/agents/<name>', 'One specific agent'),
154
156
  cmd('aw pull <team>/skills/<name>', 'One specific skill folder'),
157
+ cmd('aw init skills <team>/<path>/skills/<name>', 'Install and link only selected skill symlinks'),
155
158
  '',
156
159
  ` ${chalk.dim('# Push your local changes to registry')}`,
157
160
  cmd('aw push', 'Push all modified files (one PR)'),
package/commands/c4.mjs CHANGED
@@ -350,10 +350,10 @@ export async function c4Command(rawArgs, overrides = {}) {
350
350
  return exit(0);
351
351
  }
352
352
 
353
- // Step 6 — npm install -g @ghl-ai/aw@0.1.49-beta.0.
354
- const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@0.1.49-beta.0'], { stdio: 'pipe' });
353
+ // Step 6 — npm install -g @ghl-ai/aw@latest.
354
+ const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@latest'], { stdio: 'pipe' });
355
355
  if (npmRes && npmRes.status !== 0) {
356
- writer.stderr('[aw-c4] npm install -g @ghl-ai/aw@0.1.49-beta.0 failed (non-fatal); using existing aw if present\n');
356
+ writer.stderr('[aw-c4] npm install -g @ghl-ai/aw@latest failed (non-fatal); using existing aw if present\n');
357
357
  }
358
358
 
359
359
  // Step 7 — aw init --silent.
package/commands/init.mjs CHANGED
@@ -23,7 +23,7 @@ import * as p from '@clack/prompts';
23
23
  import * as config from '../config.mjs';
24
24
  import * as fmt from '../fmt.mjs';
25
25
  import { chalk, setSilent } from '../fmt.mjs';
26
- import { linkWorkspace } from '../link.mjs';
26
+ import { linkWorkspace, linkSkills, normalizeSkillTarget } from '../link.mjs';
27
27
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
28
28
  import { setupMcp } from '../mcp.mjs';
29
29
  import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
@@ -40,6 +40,7 @@ import {
40
40
  isValidClone,
41
41
  fetchAndMerge,
42
42
  addToSparseCheckout,
43
+ setSparseCheckout,
43
44
  addProjectWorktree,
44
45
  isWorktree,
45
46
  includeToSparsePaths,
@@ -108,6 +109,56 @@ function scaffoldNamespace(awHome, folderName) {
108
109
  }
109
110
  }
110
111
 
112
+ function getSkillInitSpecs(args) {
113
+ const positional = args._positional || [];
114
+ const mode = positional[0];
115
+ const rawTargets = [];
116
+
117
+ if (mode === 'skill' || mode === 'skills') {
118
+ rawTargets.push(...positional.slice(1));
119
+ }
120
+
121
+ if (args['--skill']) {
122
+ rawTargets.push(args['--skill']);
123
+ }
124
+
125
+ if ((mode === 'skill' || mode === 'skills' || args['--skill']) && rawTargets.length === 0) {
126
+ fmt.cancel(`Missing skill path.\n\n ${chalk.dim('Example:')} ${chalk.bold('aw init skills platform/core/skills/pr-review')}`);
127
+ }
128
+
129
+ if (rawTargets.length === 0) return [];
130
+
131
+ try {
132
+ return [...new Map(rawTargets.map(input => {
133
+ const spec = normalizeSkillTarget(input);
134
+ return [spec.registryPath, spec];
135
+ })).values()];
136
+ } catch (e) {
137
+ fmt.cancel(e.message);
138
+ }
139
+ }
140
+
141
+ function skillInitSparsePaths(skillSpecs) {
142
+ const paths = new Set([
143
+ DOCS_SOURCE_DIR,
144
+ AW_DOCS_DIR,
145
+ RULES_SOURCE_DIR,
146
+ `${REGISTRY_DIR}/AW-PROTOCOL.md`,
147
+ `CODEOWNERS`,
148
+ ]);
149
+
150
+ for (const spec of skillSpecs) {
151
+ paths.add(`${REGISTRY_DIR}/${spec.registryPath}`);
152
+
153
+ for (let i = 0; i <= spec.segments.length; i++) {
154
+ const scope = [spec.namespace, ...spec.segments.slice(0, i)].join('/');
155
+ paths.add(`${REGISTRY_DIR}/${scope}/references`);
156
+ }
157
+ }
158
+
159
+ return [...paths];
160
+ }
161
+
111
162
  // ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
112
163
  //
113
164
  // Strategy: only .aw_registry/, .aw_rules/, content/, and .aw_docs/ are
@@ -215,6 +266,14 @@ export async function initCommand(args) {
215
266
  let user = args['--user'] || '';
216
267
  const silent = args['--silent'] === true;
217
268
  const skipIntegrations = args['--no-integrations'] === true;
269
+ const skillSpecs = getSkillInitSpecs(args);
270
+ const skillTargets = skillSpecs.map(spec => spec.registryPath);
271
+ const isSkillInit = skillTargets.length > 0;
272
+ const skillNamespace = skillSpecs[0]?.namespace || null;
273
+
274
+ if (isSkillInit && namespace) {
275
+ fmt.cancel('Use either `aw init skills <path...>` or `aw init --namespace <team/sub-team>`, not both.');
276
+ }
218
277
 
219
278
  // In silent mode, suppress ALL fmt output and show a single spinner.
220
279
  // setSilent(true) makes every fmt.* call a no-op — internal functions
@@ -323,8 +382,19 @@ export async function initCommand(args) {
323
382
  if (isGitNative) {
324
383
  const cfg = config.load(GLOBAL_AW_DIR);
325
384
 
326
- const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
327
- if (isNewSubTeam) {
385
+ const isNewSubTeam = !isSkillInit && folderName && cfg && !cfg.include.includes(folderName);
386
+
387
+ if (isSkillInit) {
388
+ if (!silent) fmt.logStep('Syncing selected skills...');
389
+ setSparseCheckout(AW_HOME, skillInitSparsePaths(skillSpecs));
390
+ if (cfg) {
391
+ config.save(GLOBAL_AW_DIR, {
392
+ ...cfg,
393
+ namespace: skillNamespace || cfg.namespace || 'platform',
394
+ include: skillTargets,
395
+ });
396
+ }
397
+ } else if (isNewSubTeam) {
328
398
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
329
399
  const newSparsePaths = [`.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
330
400
  addToSparseCheckout(AW_HOME, newSparsePaths);
@@ -352,6 +422,7 @@ export async function initCommand(args) {
352
422
 
353
423
  ensureAwGitignore(AW_HOME);
354
424
  const freshCfg = config.load(GLOBAL_AW_DIR);
425
+ const activeNamespace = skillNamespace || freshCfg?.namespace || team;
355
426
  syncRulesTargets(HOME);
356
427
  if (cwd !== HOME) {
357
428
  syncRulesTargets(cwd);
@@ -374,8 +445,8 @@ export async function initCommand(args) {
374
445
  await installAwEcc(cwd, { silent });
375
446
 
376
447
  ensureAwRuntimeHook(HOME);
377
- syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
378
- await setupMcp(HOME, freshCfg?.namespace || team, { silent });
448
+ syncHomeAndProjectInstructions(cwd, activeNamespace);
449
+ await setupMcp(HOME, activeNamespace, { silent });
379
450
  applyStoredStartupPreferences(HOME);
380
451
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
381
452
  installGlobalHooks();
@@ -409,10 +480,12 @@ export async function initCommand(args) {
409
480
  if (!silent) fmt.logStep('Wiring IDE symlinks...');
410
481
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
411
482
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
412
- const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
413
- const commands = generateCommands(HOME, { silent: true });
483
+ const symlinks = isSkillInit
484
+ ? linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true })
485
+ : linkWorkspace(HOME, awDirForLinks, { silent: true });
486
+ const commands = isSkillInit ? 0 : generateCommands(HOME, { silent: true });
414
487
  if (cwd !== HOME) installLocalCommitHook(cwd);
415
- if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
488
+ if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'targeted symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
416
489
 
417
490
  // Write hook manifest after all hook installation is complete
418
491
  try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
@@ -420,7 +493,7 @@ export async function initCommand(args) {
420
493
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
421
494
  let installedIntegrations = [];
422
495
  if (!silent && !skipIntegrations && !isNewSubTeam) {
423
- installedIntegrations = await autoInstallIntegrations(freshCfg?.namespace || team, { silent });
496
+ installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
424
497
  }
425
498
 
426
499
  if (silent) {
@@ -431,7 +504,7 @@ export async function initCommand(args) {
431
504
  `⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
432
505
  '',
433
506
  ` ${chalk.green('✓')} Registry synced`,
434
- ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
507
+ ` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} ${isSkillInit ? 'targeted symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`,
435
508
  removedLegacyStartupFiles.length > 0
436
509
  ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
437
510
  : null,
@@ -465,13 +538,17 @@ export async function initCommand(args) {
465
538
  }
466
539
 
467
540
  // Determine sparse paths
468
- const sparsePaths = [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
469
- if (folderName) {
541
+ const sparsePaths = isSkillInit
542
+ ? skillInitSparsePaths(skillSpecs)
543
+ : [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
544
+ if (!isSkillInit && folderName) {
470
545
  sparsePaths.push(`.aw_registry/${folderName}`);
471
546
  }
472
547
 
473
548
  fmt.note([
474
- folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
549
+ isSkillInit
550
+ ? `${chalk.dim('skills:')} ${skillTargets.join(', ')}`
551
+ : (folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`),
475
552
  user ? `${chalk.dim('user:')} ${user}` : null,
476
553
  `${chalk.dim('version:')} v${VERSION}`,
477
554
  ].filter(Boolean).join('\n'), 'Config');
@@ -511,8 +588,11 @@ export async function initCommand(args) {
511
588
  }
512
589
 
513
590
  // Create sync config — default to 'platform' when no namespace specified
514
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
515
- if (folderName) {
591
+ const activeNamespace = skillNamespace || team || 'platform';
592
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: activeNamespace, user });
593
+ if (isSkillInit) {
594
+ for (const target of skillTargets) config.addPattern(GLOBAL_AW_DIR, target);
595
+ } else if (folderName) {
516
596
  config.addPattern(GLOBAL_AW_DIR, folderName);
517
597
  scaffoldNamespace(AW_HOME, folderName);
518
598
  }
@@ -533,8 +613,8 @@ export async function initCommand(args) {
533
613
 
534
614
  // Parallel batch B: post-ECC setup (instructions and MCP are independent)
535
615
  const [, mcpFiles] = await Promise.all([
536
- Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
537
- setupMcp(HOME, team, { silent }),
616
+ Promise.resolve(syncHomeAndProjectInstructions(cwd, activeNamespace)),
617
+ setupMcp(HOME, activeNamespace, { silent }),
538
618
  ]);
539
619
  // applyStoredStartupPreferences reads settings written by ECC — keep after batch B
540
620
  applyStoredStartupPreferences(HOME);
@@ -572,11 +652,13 @@ export async function initCommand(args) {
572
652
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
573
653
  // Parallel batch C: symlinks + commands are independent
574
654
  if (cwd !== HOME) installLocalCommitHook(cwd);
575
- const [symlinks, commands] = [
576
- linkWorkspace(HOME, awDirForLinks, { silent: true }),
577
- generateCommands(HOME, { silent: true }),
578
- ];
579
- if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
655
+ const [symlinks, commands] = isSkillInit
656
+ ? [linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true }), 0]
657
+ : [
658
+ linkWorkspace(HOME, awDirForLinks, { silent: true }),
659
+ generateCommands(HOME, { silent: true }),
660
+ ];
661
+ if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'targeted symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
580
662
 
581
663
  // Write hook manifest after all hook installation is complete
582
664
  try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
@@ -587,7 +669,7 @@ export async function initCommand(args) {
587
669
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
588
670
  let installedIntegrations = [];
589
671
  if (!silent && !skipIntegrations) {
590
- installedIntegrations = await autoInstallIntegrations(team, { silent });
672
+ installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
591
673
  }
592
674
 
593
675
  // Offer to update if a newer version is available
package/ecc.mjs CHANGED
@@ -12,7 +12,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
12
12
 
13
13
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
14
14
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
15
- export const AW_ECC_TAG = "v1.4.55";
15
+ export const AW_ECC_TAG = "v1.4.61";
16
16
 
17
17
  const MARKETPLACE_NAME = "aw-marketplace";
18
18
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
package/git.mjs CHANGED
@@ -251,6 +251,19 @@ export function addToSparseCheckout(awHome, newPaths) {
251
251
  }
252
252
  }
253
253
 
254
+ /**
255
+ * Replace the current sparse checkout with an exact path set.
256
+ */
257
+ export function setSparseCheckout(awHome, sparsePaths) {
258
+ try {
259
+ execSync('git sparse-checkout init --no-cone', { cwd: awHome, stdio: 'pipe' });
260
+ execSync(`git sparse-checkout set ${sparsePaths.map(p => `"${p}"`).join(' ')}`, { cwd: awHome, stdio: 'pipe' });
261
+ execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: awHome, stdio: 'pipe' });
262
+ } catch (e) {
263
+ throw new Error(`Failed to set sparse checkout: ${e.message}`);
264
+ }
265
+ }
266
+
254
267
  /**
255
268
  * Mirror the main clone's sparse-checkout paths into a project worktree.
256
269
  * The worktree stays on its own branch — only the checked-out file set is updated.
package/link.mjs CHANGED
@@ -16,6 +16,11 @@ const IDE_DIRS = ['.claude', '.cursor', '.codex'];
16
16
  const FILE_TYPES = ['agents'];
17
17
  const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', 'references', 'docs']);
18
18
 
19
+ function realHomeDir() {
20
+ const rawHome = homedir();
21
+ try { return realpathSync(rawHome); } catch { return rawHome; }
22
+ }
23
+
19
24
  /**
20
25
  * List namespace directories inside .aw_registry/ (skip dotfiles).
21
26
  */
@@ -77,7 +82,7 @@ function cleanIdeSymlinks(cwd) {
77
82
  cleanSymlinksRecursive(ideDir);
78
83
  }
79
84
  // Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
80
- const HOME = homedir();
85
+ const HOME = realHomeDir();
81
86
  if (cwd === HOME) {
82
87
  const agentsSkillsDir = join(cwd, '.agents', 'skills');
83
88
  if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
@@ -120,6 +125,156 @@ function flatName(ns, name) {
120
125
  return `${ns}-${name}`;
121
126
  }
122
127
 
128
+ function stripRegistryPrefix(input) {
129
+ return String(input || '')
130
+ .trim()
131
+ .replace(/\\/g, '/')
132
+ .replace(/\/SKILL\.md$/i, '')
133
+ .replace(/^.*?\.aw_registry\//, '')
134
+ .replace(/^\.\/+/, '')
135
+ .replace(/\/+$/, '');
136
+ }
137
+
138
+ export function normalizeSkillTarget(input) {
139
+ const normalized = stripRegistryPrefix(input);
140
+ const parts = normalized.split('/').filter(Boolean);
141
+ const skillIndex = parts.indexOf('skills');
142
+
143
+ if (skillIndex < 1 || skillIndex === parts.length - 1) {
144
+ throw new Error(`Skill target must look like <team>/<path>/skills/<name>: ${input}`);
145
+ }
146
+
147
+ if (parts.length !== skillIndex + 2) {
148
+ throw new Error(`Skill target must point at a skill folder, not a nested file: ${input}`);
149
+ }
150
+
151
+ const namespace = parts[0];
152
+ const segments = parts.slice(1, skillIndex);
153
+ const skill = parts[skillIndex + 1];
154
+ const registryPath = [namespace, ...segments, 'skills', skill].join('/');
155
+ const flat = [namespace, ...segments, skill].join('-');
156
+ return { namespace, segments, skill, registryPath, flat };
157
+ }
158
+
159
+ function cleanSkillSymlinks(cwd) {
160
+ for (const ide of IDE_DIRS) {
161
+ const skillsDir = join(cwd, ide, 'skills');
162
+ if (existsSync(skillsDir)) cleanSymlinksRecursive(skillsDir);
163
+ const referencesDir = join(cwd, ide, 'references');
164
+ if (existsSync(referencesDir)) cleanSymlinksRecursive(referencesDir);
165
+ }
166
+
167
+ if (cwd === realHomeDir()) {
168
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
169
+ if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
170
+ const agentsReferencesDir = join(cwd, '.agents', 'references');
171
+ if (existsSync(agentsReferencesDir)) cleanSymlinksRecursive(agentsReferencesDir);
172
+ }
173
+ }
174
+
175
+ function linkOneSkill(cwd, awDir, target) {
176
+ const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
177
+ if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
178
+ throw new Error(`Skill not found in registry: ${target.registryPath}`);
179
+ }
180
+
181
+ let created = 0;
182
+ for (const ide of IDE_DIRS) {
183
+ const linkDir = join(cwd, ide, 'skills');
184
+ mkdirSync(linkDir, { recursive: true });
185
+ const linkPath = join(linkDir, target.flat);
186
+ const relTarget = relative(linkDir, skillDir);
187
+ forceSymlink(relTarget, linkPath);
188
+ created++;
189
+ }
190
+
191
+ if (cwd === realHomeDir()) {
192
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
193
+ mkdirSync(agentsSkillsDir, { recursive: true });
194
+ const linkPath = join(agentsSkillsDir, target.flat);
195
+ const relTarget = relative(agentsSkillsDir, skillDir);
196
+ forceSymlink(relTarget, linkPath);
197
+ created++;
198
+ }
199
+
200
+ return created;
201
+ }
202
+
203
+ function linkReferenceFiles(cwd, awDir, namespaces = null) {
204
+ const allowedNamespaces = namespaces ? new Set(namespaces) : null;
205
+ const namespacesToLink = listNamespaceDirs(awDir)
206
+ .filter(ns => !allowedNamespaces || allowedNamespaces.has(ns));
207
+ let created = 0;
208
+
209
+ for (const ns of namespacesToLink) {
210
+ for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
211
+ for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
212
+ const targetPath = join(referencesDir, file);
213
+ if (lstatSync(targetPath).isDirectory()) continue;
214
+
215
+ for (const ide of IDE_DIRS) {
216
+ const linkDir = join(cwd, ide, 'references');
217
+ mkdirSync(linkDir, { recursive: true });
218
+ const linkPath = join(linkDir, file);
219
+ const relTarget = relative(linkDir, targetPath);
220
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
221
+ }
222
+
223
+ if (cwd === realHomeDir()) {
224
+ const agentsReferencesDir = join(cwd, '.agents', 'references');
225
+ mkdirSync(agentsReferencesDir, { recursive: true });
226
+ const linkPath = join(agentsReferencesDir, file);
227
+ const relTarget = relative(agentsReferencesDir, targetPath);
228
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ return created;
235
+ }
236
+
237
+ /**
238
+ * Create/refresh symlinks for specific skills only.
239
+ *
240
+ * Unlike linkWorkspace(), this does not walk every namespace. It can be used
241
+ * by lean init flows to avoid exposing every registry skill to IDE runtimes.
242
+ */
243
+ export function linkSkills(cwd, skillTargets, awDirOverride = null, { silent = false, exclusive = false } = {}) {
244
+ try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
245
+
246
+ const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
247
+ let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
248
+ try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
249
+ if (!existsSync(awDir)) return 0;
250
+
251
+ const targets = [...new Map(skillTargets.map(input => {
252
+ const target = normalizeSkillTarget(input);
253
+ return [target.registryPath, target];
254
+ })).values()];
255
+
256
+ for (const target of targets) {
257
+ const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
258
+ if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
259
+ throw new Error(`Skill not found in registry: ${target.registryPath}`);
260
+ }
261
+ }
262
+
263
+ if (exclusive) cleanSkillSymlinks(cwd);
264
+
265
+ let created = 0;
266
+ for (const target of targets) {
267
+ created += linkOneSkill(cwd, awDir, target);
268
+ }
269
+ created += linkReferenceFiles(cwd, awDir, targets.map(target => target.namespace));
270
+
271
+ if (created > 0 && !silent) {
272
+ fmt.logSuccess(`Linked ${created} targeted symlink${created > 1 ? 's' : ''}`);
273
+ }
274
+
275
+ return created;
276
+ }
277
+
123
278
  /**
124
279
  * Create/refresh all IDE symlinks.
125
280
  *
@@ -140,7 +295,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
140
295
  // where $HOME may be /var/... but process.cwd() resolves to /private/var/...
141
296
  try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
142
297
 
143
- const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
298
+ const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
144
299
  let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
145
300
  try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
146
301
  if (!existsSync(awDir)) return 0;
@@ -228,25 +383,10 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
228
383
 
229
384
  // Shared references: flatten namespace references into each IDE's references/
230
385
  // so links like ../../references/foo.md continue to work from flattened skill dirs.
231
- for (const ns of namespaces) {
232
- for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
233
- for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
234
- const targetPath = join(referencesDir, file);
235
- if (lstatSync(targetPath).isDirectory()) continue;
236
-
237
- for (const ide of IDE_DIRS) {
238
- const linkDir = join(cwd, ide, 'references');
239
- mkdirSync(linkDir, { recursive: true });
240
- const linkPath = join(linkDir, file);
241
- const relTarget = relative(linkDir, targetPath);
242
- try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
243
- }
244
- }
245
- }
246
- }
386
+ created += linkReferenceFiles(cwd, awDir);
247
387
 
248
388
  // Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
249
- if (cwd === homedir()) {
389
+ if (cwd === realHomeDir()) {
250
390
  const agentsSkillsDir = join(cwd, '.agents/skills');
251
391
  for (const ns of namespaces) {
252
392
  for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
@@ -260,20 +400,6 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
260
400
  }
261
401
  }
262
402
  }
263
-
264
- const agentsReferencesDir = join(cwd, '.agents', 'references');
265
- for (const ns of namespaces) {
266
- for (const { typeDirPath: referencesDir } of findNestedTypeDirs(join(awDir, ns), 'references')) {
267
- mkdirSync(agentsReferencesDir, { recursive: true });
268
- for (const file of readdirSync(referencesDir).filter(f => !f.startsWith('.'))) {
269
- const targetPath = join(referencesDir, file);
270
- if (lstatSync(targetPath).isDirectory()) continue;
271
- const linkPath = join(agentsReferencesDir, file);
272
- const relTarget = relative(agentsReferencesDir, targetPath);
273
- try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
274
- }
275
- }
276
- }
277
403
  }
278
404
 
279
405
  // Commands: per-file symlinks (recursive for nested domain dirs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.49",
3
+ "version": "0.1.50-beta.1",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {