@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.
- 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 +105 -23
- package/ecc.mjs +1 -1
- package/git.mjs +13 -0
- package/link.mjs +159 -33
- 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';
|
|
@@ -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
|
-
|
|
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,
|
|
378
|
-
await setupMcp(HOME,
|
|
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 =
|
|
413
|
-
|
|
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(
|
|
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 =
|
|
469
|
-
|
|
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
|
-
|
|
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
|
|
515
|
-
|
|
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,
|
|
537
|
-
setupMcp(HOME,
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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(
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 ===
|
|
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)
|