@ghl-ai/aw 0.1.49 → 0.1.50-beta.0
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/c4/templates/scripts/aw-c4-bootstrap.sh +4 -4
- package/cli.mjs +3 -0
- package/commands/c4.mjs +3 -3
- package/commands/init.mjs +97 -23
- package/link.mjs +119 -3
- package/package.json +1 -1
|
@@ -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@
|
|
16
|
-
# Override
|
|
17
|
-
#
|
|
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@
|
|
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@
|
|
354
|
-
const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@
|
|
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@
|
|
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';
|
|
@@ -108,6 +108,35 @@ function scaffoldNamespace(awHome, folderName) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function getSkillInitSpecs(args) {
|
|
112
|
+
const positional = args._positional || [];
|
|
113
|
+
const mode = positional[0];
|
|
114
|
+
const rawTargets = [];
|
|
115
|
+
|
|
116
|
+
if (mode === 'skill' || mode === 'skills') {
|
|
117
|
+
rawTargets.push(...positional.slice(1));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args['--skill']) {
|
|
121
|
+
rawTargets.push(args['--skill']);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ((mode === 'skill' || mode === 'skills' || args['--skill']) && rawTargets.length === 0) {
|
|
125
|
+
fmt.cancel(`Missing skill path.\n\n ${chalk.dim('Example:')} ${chalk.bold('aw init skills platform/core/skills/pr-review')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (rawTargets.length === 0) return [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
return [...new Map(rawTargets.map(input => {
|
|
132
|
+
const spec = normalizeSkillTarget(input);
|
|
133
|
+
return [spec.registryPath, spec];
|
|
134
|
+
})).values()];
|
|
135
|
+
} catch (e) {
|
|
136
|
+
fmt.cancel(e.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
// ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
|
|
112
141
|
//
|
|
113
142
|
// Strategy: only .aw_registry/, .aw_rules/, content/, and .aw_docs/ are
|
|
@@ -215,6 +244,14 @@ export async function initCommand(args) {
|
|
|
215
244
|
let user = args['--user'] || '';
|
|
216
245
|
const silent = args['--silent'] === true;
|
|
217
246
|
const skipIntegrations = args['--no-integrations'] === true;
|
|
247
|
+
const skillSpecs = getSkillInitSpecs(args);
|
|
248
|
+
const skillTargets = skillSpecs.map(spec => spec.registryPath);
|
|
249
|
+
const isSkillInit = skillTargets.length > 0;
|
|
250
|
+
const skillNamespace = skillSpecs[0]?.namespace || null;
|
|
251
|
+
|
|
252
|
+
if (isSkillInit && namespace) {
|
|
253
|
+
fmt.cancel('Use either `aw init skills <path...>` or `aw init --namespace <team/sub-team>`, not both.');
|
|
254
|
+
}
|
|
218
255
|
|
|
219
256
|
// In silent mode, suppress ALL fmt output and show a single spinner.
|
|
220
257
|
// setSilent(true) makes every fmt.* call a no-op — internal functions
|
|
@@ -323,8 +360,26 @@ export async function initCommand(args) {
|
|
|
323
360
|
if (isGitNative) {
|
|
324
361
|
const cfg = config.load(GLOBAL_AW_DIR);
|
|
325
362
|
|
|
326
|
-
const
|
|
327
|
-
|
|
363
|
+
const newSkillTargets = isSkillInit && cfg
|
|
364
|
+
? skillTargets.filter(target => !cfg.include.some(pattern => target === pattern || target.startsWith(pattern + '/')))
|
|
365
|
+
: [];
|
|
366
|
+
const isNewSubTeam = !isSkillInit && folderName && cfg && !cfg.include.includes(folderName);
|
|
367
|
+
|
|
368
|
+
if (isSkillInit) {
|
|
369
|
+
if (newSkillTargets.length > 0) {
|
|
370
|
+
if (!silent) fmt.logStep(`Adding ${chalk.cyan(newSkillTargets.length)} skill${newSkillTargets.length > 1 ? 's' : ''}...`);
|
|
371
|
+
addToSparseCheckout(AW_HOME, [
|
|
372
|
+
...newSkillTargets.map(target => `.aw_registry/${target}`),
|
|
373
|
+
DOCS_SOURCE_DIR,
|
|
374
|
+
AW_DOCS_DIR,
|
|
375
|
+
RULES_SOURCE_DIR,
|
|
376
|
+
`.aw_registry/AW-PROTOCOL.md`,
|
|
377
|
+
]);
|
|
378
|
+
for (const target of newSkillTargets) config.addPattern(GLOBAL_AW_DIR, target);
|
|
379
|
+
} else if (!silent) {
|
|
380
|
+
fmt.logStep('Already initialized — syncing selected skills...');
|
|
381
|
+
}
|
|
382
|
+
} else if (isNewSubTeam) {
|
|
328
383
|
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
329
384
|
const newSparsePaths = [`.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
|
|
330
385
|
addToSparseCheckout(AW_HOME, newSparsePaths);
|
|
@@ -352,6 +407,7 @@ export async function initCommand(args) {
|
|
|
352
407
|
|
|
353
408
|
ensureAwGitignore(AW_HOME);
|
|
354
409
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
410
|
+
const activeNamespace = skillNamespace || freshCfg?.namespace || team;
|
|
355
411
|
syncRulesTargets(HOME);
|
|
356
412
|
if (cwd !== HOME) {
|
|
357
413
|
syncRulesTargets(cwd);
|
|
@@ -374,8 +430,8 @@ export async function initCommand(args) {
|
|
|
374
430
|
await installAwEcc(cwd, { silent });
|
|
375
431
|
|
|
376
432
|
ensureAwRuntimeHook(HOME);
|
|
377
|
-
syncHomeAndProjectInstructions(cwd,
|
|
378
|
-
await setupMcp(HOME,
|
|
433
|
+
syncHomeAndProjectInstructions(cwd, activeNamespace);
|
|
434
|
+
await setupMcp(HOME, activeNamespace, { silent });
|
|
379
435
|
applyStoredStartupPreferences(HOME);
|
|
380
436
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
381
437
|
installGlobalHooks();
|
|
@@ -409,10 +465,12 @@ export async function initCommand(args) {
|
|
|
409
465
|
if (!silent) fmt.logStep('Wiring IDE symlinks...');
|
|
410
466
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
411
467
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
412
|
-
const symlinks =
|
|
413
|
-
|
|
468
|
+
const symlinks = isSkillInit
|
|
469
|
+
? linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true })
|
|
470
|
+
: linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
471
|
+
const commands = isSkillInit ? 0 : generateCommands(HOME, { silent: true });
|
|
414
472
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
415
|
-
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
473
|
+
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
|
|
416
474
|
|
|
417
475
|
// Write hook manifest after all hook installation is complete
|
|
418
476
|
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
@@ -420,7 +478,7 @@ export async function initCommand(args) {
|
|
|
420
478
|
// Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
|
|
421
479
|
let installedIntegrations = [];
|
|
422
480
|
if (!silent && !skipIntegrations && !isNewSubTeam) {
|
|
423
|
-
installedIntegrations = await autoInstallIntegrations(
|
|
481
|
+
installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
|
|
424
482
|
}
|
|
425
483
|
|
|
426
484
|
if (silent) {
|
|
@@ -431,7 +489,7 @@ export async function initCommand(args) {
|
|
|
431
489
|
`⟁ ${isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Up to date'}`,
|
|
432
490
|
'',
|
|
433
491
|
` ${chalk.green('✓')} Registry synced`,
|
|
434
|
-
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
492
|
+
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`,
|
|
435
493
|
removedLegacyStartupFiles.length > 0
|
|
436
494
|
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
437
495
|
: null,
|
|
@@ -465,13 +523,24 @@ export async function initCommand(args) {
|
|
|
465
523
|
}
|
|
466
524
|
|
|
467
525
|
// Determine sparse paths
|
|
468
|
-
const sparsePaths =
|
|
469
|
-
|
|
526
|
+
const sparsePaths = isSkillInit
|
|
527
|
+
? [
|
|
528
|
+
...skillTargets.map(target => `.aw_registry/${target}`),
|
|
529
|
+
DOCS_SOURCE_DIR,
|
|
530
|
+
AW_DOCS_DIR,
|
|
531
|
+
RULES_SOURCE_DIR,
|
|
532
|
+
`.aw_registry/AW-PROTOCOL.md`,
|
|
533
|
+
`CODEOWNERS`,
|
|
534
|
+
]
|
|
535
|
+
: [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
|
|
536
|
+
if (!isSkillInit && folderName) {
|
|
470
537
|
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
471
538
|
}
|
|
472
539
|
|
|
473
540
|
fmt.note([
|
|
474
|
-
|
|
541
|
+
isSkillInit
|
|
542
|
+
? `${chalk.dim('skills:')} ${skillTargets.join(', ')}`
|
|
543
|
+
: (folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`),
|
|
475
544
|
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
476
545
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
477
546
|
].filter(Boolean).join('\n'), 'Config');
|
|
@@ -511,8 +580,11 @@ export async function initCommand(args) {
|
|
|
511
580
|
}
|
|
512
581
|
|
|
513
582
|
// Create sync config — default to 'platform' when no namespace specified
|
|
514
|
-
const
|
|
515
|
-
|
|
583
|
+
const activeNamespace = skillNamespace || team || 'platform';
|
|
584
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: activeNamespace, user });
|
|
585
|
+
if (isSkillInit) {
|
|
586
|
+
for (const target of skillTargets) config.addPattern(GLOBAL_AW_DIR, target);
|
|
587
|
+
} else if (folderName) {
|
|
516
588
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
517
589
|
scaffoldNamespace(AW_HOME, folderName);
|
|
518
590
|
}
|
|
@@ -533,8 +605,8 @@ export async function initCommand(args) {
|
|
|
533
605
|
|
|
534
606
|
// Parallel batch B: post-ECC setup (instructions and MCP are independent)
|
|
535
607
|
const [, mcpFiles] = await Promise.all([
|
|
536
|
-
Promise.resolve(syncHomeAndProjectInstructions(cwd,
|
|
537
|
-
setupMcp(HOME,
|
|
608
|
+
Promise.resolve(syncHomeAndProjectInstructions(cwd, activeNamespace)),
|
|
609
|
+
setupMcp(HOME, activeNamespace, { silent }),
|
|
538
610
|
]);
|
|
539
611
|
// applyStoredStartupPreferences reads settings written by ECC — keep after batch B
|
|
540
612
|
applyStoredStartupPreferences(HOME);
|
|
@@ -572,11 +644,13 @@ export async function initCommand(args) {
|
|
|
572
644
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
573
645
|
// Parallel batch C: symlinks + commands are independent
|
|
574
646
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
575
|
-
const [symlinks, commands] =
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
647
|
+
const [symlinks, commands] = isSkillInit
|
|
648
|
+
? [linkSkills(HOME, skillTargets, awDirForLinks, { silent: true, exclusive: true }), 0]
|
|
649
|
+
: [
|
|
650
|
+
linkWorkspace(HOME, awDirForLinks, { silent: true }),
|
|
651
|
+
generateCommands(HOME, { silent: true }),
|
|
652
|
+
];
|
|
653
|
+
if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} ${isSkillInit ? 'skill symlinks' : 'symlinks'} · ${chalk.bold(commands)} commands`);
|
|
580
654
|
|
|
581
655
|
// Write hook manifest after all hook installation is complete
|
|
582
656
|
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
@@ -587,7 +661,7 @@ export async function initCommand(args) {
|
|
|
587
661
|
// Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
|
|
588
662
|
let installedIntegrations = [];
|
|
589
663
|
if (!silent && !skipIntegrations) {
|
|
590
|
-
installedIntegrations = await autoInstallIntegrations(
|
|
664
|
+
installedIntegrations = await autoInstallIntegrations(activeNamespace, { silent });
|
|
591
665
|
}
|
|
592
666
|
|
|
593
667
|
// Offer to update if a newer version is available
|
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 =
|
|
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,117 @@ 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
|
+
}
|
|
164
|
+
|
|
165
|
+
if (cwd === realHomeDir()) {
|
|
166
|
+
const agentsSkillsDir = join(cwd, '.agents', 'skills');
|
|
167
|
+
if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function linkOneSkill(cwd, awDir, target) {
|
|
172
|
+
const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
|
|
173
|
+
if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
|
|
174
|
+
throw new Error(`Skill not found in registry: ${target.registryPath}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let created = 0;
|
|
178
|
+
for (const ide of IDE_DIRS) {
|
|
179
|
+
const linkDir = join(cwd, ide, 'skills');
|
|
180
|
+
mkdirSync(linkDir, { recursive: true });
|
|
181
|
+
const linkPath = join(linkDir, target.flat);
|
|
182
|
+
const relTarget = relative(linkDir, skillDir);
|
|
183
|
+
forceSymlink(relTarget, linkPath);
|
|
184
|
+
created++;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (cwd === realHomeDir()) {
|
|
188
|
+
const agentsSkillsDir = join(cwd, '.agents', 'skills');
|
|
189
|
+
mkdirSync(agentsSkillsDir, { recursive: true });
|
|
190
|
+
const linkPath = join(agentsSkillsDir, target.flat);
|
|
191
|
+
const relTarget = relative(agentsSkillsDir, skillDir);
|
|
192
|
+
forceSymlink(relTarget, linkPath);
|
|
193
|
+
created++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return created;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create/refresh symlinks for specific skills only.
|
|
201
|
+
*
|
|
202
|
+
* Unlike linkWorkspace(), this does not walk every namespace. It can be used
|
|
203
|
+
* by lean init flows to avoid exposing every registry skill to IDE runtimes.
|
|
204
|
+
*/
|
|
205
|
+
export function linkSkills(cwd, skillTargets, awDirOverride = null, { silent = false, exclusive = false } = {}) {
|
|
206
|
+
try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
|
|
207
|
+
|
|
208
|
+
const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
|
|
209
|
+
let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
210
|
+
try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
|
|
211
|
+
if (!existsSync(awDir)) return 0;
|
|
212
|
+
|
|
213
|
+
const targets = [...new Map(skillTargets.map(input => {
|
|
214
|
+
const target = normalizeSkillTarget(input);
|
|
215
|
+
return [target.registryPath, target];
|
|
216
|
+
})).values()];
|
|
217
|
+
|
|
218
|
+
for (const target of targets) {
|
|
219
|
+
const skillDir = join(awDir, target.namespace, ...target.segments, 'skills', target.skill);
|
|
220
|
+
if (!existsSync(skillDir) || !lstatSync(skillDir).isDirectory()) {
|
|
221
|
+
throw new Error(`Skill not found in registry: ${target.registryPath}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (exclusive) cleanSkillSymlinks(cwd);
|
|
226
|
+
|
|
227
|
+
let created = 0;
|
|
228
|
+
for (const target of targets) {
|
|
229
|
+
created += linkOneSkill(cwd, awDir, target);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (created > 0 && !silent) {
|
|
233
|
+
fmt.logSuccess(`Linked ${created} skill symlink${created > 1 ? 's' : ''}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return created;
|
|
237
|
+
}
|
|
238
|
+
|
|
123
239
|
/**
|
|
124
240
|
* Create/refresh all IDE symlinks.
|
|
125
241
|
*
|
|
@@ -140,7 +256,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
140
256
|
// where $HOME may be /var/... but process.cwd() resolves to /private/var/...
|
|
141
257
|
try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
|
|
142
258
|
|
|
143
|
-
const GLOBAL_AW_DIR = join(
|
|
259
|
+
const GLOBAL_AW_DIR = join(realHomeDir(), '.aw_registry');
|
|
144
260
|
let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
145
261
|
try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
|
|
146
262
|
if (!existsSync(awDir)) return 0;
|
|
@@ -246,7 +362,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
246
362
|
}
|
|
247
363
|
|
|
248
364
|
// Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
|
|
249
|
-
if (cwd ===
|
|
365
|
+
if (cwd === realHomeDir()) {
|
|
250
366
|
const agentsSkillsDir = join(cwd, '.agents/skills');
|
|
251
367
|
for (const ns of namespaces) {
|
|
252
368
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|