@ghl-ai/aw 0.1.25-beta.1 → 0.1.25-beta.10

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
@@ -71,8 +71,9 @@ function printHelp() {
71
71
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
72
72
  const help = [
73
73
  sec('Setup'),
74
- cmd('aw init --namespace <team>', 'Initialize workspace (required)'),
74
+ cmd('aw init --namespace <team/sub-team>', 'Initialize workspace (required)'),
75
75
  ` ${chalk.dim('Teams: revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
76
+ ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
76
77
 
77
78
  sec('Download'),
78
79
  cmd('aw pull', 'Re-pull all synced paths (like git pull)'),
@@ -99,7 +100,7 @@ function printHelp() {
99
100
  sec('Examples'),
100
101
  '',
101
102
  ` ${chalk.dim('# Pull content from registry using path')}`,
102
- cmd('aw pull ghl', 'All shared platform agents & skills'),
103
+ cmd('aw pull platform', 'All shared platform agents & skills'),
103
104
  cmd('aw pull <team>', 'Everything from a team namespace'),
104
105
  cmd('aw pull <team>/agents', 'All agents from a team'),
105
106
  cmd('aw pull <team>/agents/<name>', 'One specific agent'),
package/commands/init.mjs CHANGED
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs';
13
13
  import * as config from '../config.mjs';
14
14
  import * as fmt from '../fmt.mjs';
15
15
  import { chalk } from '../fmt.mjs';
16
- import { pullCommand } from './pull.mjs';
16
+ import { pullCommand, pullAsync } from './pull.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
19
19
  import { setupMcp } from '../mcp.mjs';
@@ -112,6 +112,24 @@ function saveManifest(data) {
112
112
  }
113
113
 
114
114
 
115
+ function printPullSummary(pattern, actions) {
116
+ for (const type of ['agents', 'skills', 'commands', 'evals']) {
117
+ const typeActions = actions.filter(a => a.type === type);
118
+ if (typeActions.length === 0) continue;
119
+
120
+ const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
121
+ for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
122
+
123
+ const parts = [];
124
+ if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
125
+ if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
126
+ if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
127
+ const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
128
+
129
+ fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
130
+ }
131
+ }
132
+
115
133
  const ALLOWED_NAMESPACES = ['revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
116
134
 
117
135
  export async function initCommand(args) {
@@ -128,45 +146,92 @@ export async function initCommand(args) {
128
146
  fmt.cancel([
129
147
  `Missing required ${chalk.bold('--namespace')} flag`,
130
148
  '',
131
- ` ${chalk.dim('Usage:')} aw init --namespace <team>`,
149
+ ` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
132
150
  ` ${chalk.dim('Teams:')} ${list}`,
133
151
  '',
134
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
152
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
135
153
  ].join('\n'));
136
154
  }
137
155
 
138
- if (namespace && !ALLOWED_NAMESPACES.includes(namespace)) {
156
+ // Parse team/sub-team
157
+ const nsParts = namespace ? namespace.split('/') : [];
158
+ const team = nsParts[0] || null;
159
+ const subTeam = nsParts[1] || null;
160
+ const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
161
+ const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
162
+
163
+ if (team && !ALLOWED_NAMESPACES.includes(team)) {
139
164
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
140
165
  fmt.cancel([
141
- `Unknown namespace ${chalk.red(namespace)}`,
166
+ `Unknown team ${chalk.red(team)}`,
142
167
  '',
143
168
  ` ${chalk.dim('Allowed:')} ${list}`,
144
169
  '',
145
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
170
+ ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
146
171
  ].join('\n'));
147
172
  }
148
173
 
149
- if (namespace && !/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/.test(namespace)) {
150
- fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
174
+ const SLUG_RE = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
175
+ if (team && !SLUG_RE.test(team)) {
176
+ fmt.cancel(`Invalid team '${team}' — must match: ${SLUG_RE}`);
177
+ }
178
+ if (subTeam && !SLUG_RE.test(subTeam)) {
179
+ fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
151
180
  }
152
181
 
153
- const isExisting = config.exists(GLOBAL_AW_DIR);
182
+ const hasConfig = config.exists(GLOBAL_AW_DIR);
183
+ const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
184
+ const isExisting = hasConfig && hasPlatform;
154
185
  const cwd = process.cwd();
155
186
 
156
187
  // ── Fast path: already initialized → just pull + link ─────────────────
157
188
 
158
189
  if (isExisting) {
159
- if (!silent) fmt.logStep('Already initialized — syncing...');
190
+ const cfg = config.load(GLOBAL_AW_DIR);
191
+
192
+ // Add new sub-team if not already tracked
193
+ const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
194
+ if (isNewSubTeam) {
195
+ if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
196
+ await pullAsync({
197
+ ...args,
198
+ _positional: ['[template]'],
199
+ _renameNamespace: folderName,
200
+ _teamNS: teamNS,
201
+ _workspaceDir: GLOBAL_AW_DIR,
202
+ _skipIntegrate: true,
203
+ });
204
+ config.addPattern(GLOBAL_AW_DIR, folderName);
205
+ } else {
206
+ if (!silent) fmt.logStep('Already initialized — syncing...');
207
+ }
160
208
 
161
- // Pull latest
162
- pullCommand({ ...args, _positional: [], _workspaceDir: GLOBAL_AW_DIR, '--silent': silent });
209
+ // Pull latest (parallel)
210
+ // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
211
+ // only has 'registry/[template]/' — remap non-platform entries back.
212
+ const freshCfg = config.load(GLOBAL_AW_DIR);
213
+ if (freshCfg && freshCfg.include.length > 0) {
214
+ const pullJobs = freshCfg.include.map(p => {
215
+ const isTeamNs = p !== 'platform';
216
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
217
+ return pullAsync({
218
+ ...args,
219
+ _positional: [isTeamNs ? '[template]' : p],
220
+ _workspaceDir: GLOBAL_AW_DIR,
221
+ _skipIntegrate: true,
222
+ _renameNamespace: isTeamNs ? p : undefined,
223
+ _teamNS: derivedTeamNS,
224
+ });
225
+ });
226
+ await Promise.all(pullJobs);
227
+ }
163
228
 
164
229
  // Re-link IDE dirs (idempotent)
165
230
  linkWorkspace(HOME);
166
231
  generateCommands(HOME);
167
- copyInstructions(HOME, null, namespace) || [];
232
+ copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
168
233
  initAwDocs(HOME);
169
- setupMcp(HOME, namespace) || [];
234
+ setupMcp(HOME, freshCfg?.namespace || team) || [];
170
235
 
171
236
  // Link current project if needed
172
237
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
@@ -178,7 +243,7 @@ export async function initCommand(args) {
178
243
 
179
244
  if (!silent) {
180
245
  fmt.outro([
181
- 'Sync complete',
246
+ isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
182
247
  '',
183
248
  ` ${chalk.green('✓')} Registry updated`,
184
249
  ` ${chalk.green('✓')} IDE integration refreshed`,
@@ -200,36 +265,53 @@ export async function initCommand(args) {
200
265
  // Step 1: Create global source of truth
201
266
  mkdirSync(GLOBAL_AW_DIR, { recursive: true });
202
267
 
203
- const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
268
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
204
269
 
205
270
  fmt.note([
206
271
  `${chalk.dim('source:')} ~/.aw_registry/`,
207
- namespace ? `${chalk.dim('namespace:')} ${cfg.namespace}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
272
+ folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
208
273
  user ? `${chalk.dim('user:')} ${cfg.user}` : null,
209
274
  `${chalk.dim('version:')} v${VERSION}`,
210
275
  ].filter(Boolean).join('\n'), 'Config created');
211
276
 
212
- // Step 2: Pull registry content
213
- pullCommand({ ...args, _positional: ['ghl'], _workspaceDir: GLOBAL_AW_DIR });
277
+ // Step 2: Pull registry content (parallel)
278
+ const s = fmt.spinner();
279
+ const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
280
+ s.start(`Pulling ${pullTargets}...`);
281
+
282
+ const pullJobs = [
283
+ pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
284
+ ];
285
+ if (folderName) {
286
+ pullJobs.push(
287
+ pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
288
+ );
289
+ }
290
+
291
+ let pullResults;
292
+ try {
293
+ pullResults = await Promise.all(pullJobs);
294
+ s.stop(`Pulled ${pullTargets}`);
295
+ } catch (e) {
296
+ s.stop(chalk.red('Pull failed'));
297
+ fmt.cancel(e.message);
298
+ }
214
299
 
215
- if (namespace) {
216
- pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
300
+ for (const { pattern, actions } of pullResults) {
301
+ printPullSummary(pattern, actions);
217
302
  }
218
303
 
219
- // Step 3: Link to global IDE dirs
304
+ // Step 3: Link IDE dirs + setup tasks
305
+ fmt.logStep('Linking IDE symlinks...');
220
306
  linkWorkspace(HOME);
221
307
  generateCommands(HOME);
222
- const instructionFiles = copyInstructions(HOME, null, namespace) || [];
308
+ const instructionFiles = copyInstructions(HOME, null, team) || [];
223
309
  initAwDocs(HOME);
224
- const mcpFiles = setupMcp(HOME, namespace) || [];
225
-
226
- // Step 4: Git template hook (omnipresence)
310
+ const mcpFiles = setupMcp(HOME, team) || [];
227
311
  const gitTemplateInstalled = installGitTemplate();
228
-
229
- // Step 5: IDE auto-init tasks
230
312
  installIdeTasks();
231
313
 
232
- // Step 6: Symlink in current directory if it's a git repo
314
+ // Step 4: Symlink in current directory if it's a git repo
233
315
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
234
316
  try {
235
317
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
@@ -237,7 +319,7 @@ export async function initCommand(args) {
237
319
  } catch { /* best effort */ }
238
320
  }
239
321
 
240
- // Step 7: Write manifest for nuke cleanup
322
+ // Step 5: Write manifest for nuke cleanup
241
323
  const manifest = {
242
324
  version: 1,
243
325
  installedAt: new Date().toISOString(),
package/commands/pull.mjs CHANGED
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
- import { sparseCheckout, cleanup, includeToSparsePaths } from '../git.mjs';
10
+ import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
11
11
  import { walkRegistryTree } from '../registry.mjs';
12
12
  import { matchesAny } from '../glob.mjs';
13
13
  import { computePlan } from '../plan.mjs';
@@ -17,7 +17,15 @@ import { resolveInput } from '../paths.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions } from '../integrate.mjs';
19
19
 
20
- export function pullCommand(args) {
20
+ // Platform-level types to skip (CLI meta-docs, not useful as agent content)
21
+ const SKIP_PLATFORM_TYPES = new Set(['commands']);
22
+
23
+ function filterActions(actions, pattern) {
24
+ if (pattern !== 'platform') return actions;
25
+ return actions.filter(a => !SKIP_PLATFORM_TYPES.has(a.type));
26
+ }
27
+
28
+ export async function pullCommand(args) {
21
29
  const input = args._positional?.[0] || '';
22
30
  const cwd = process.cwd();
23
31
  const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
@@ -27,6 +35,7 @@ export function pullCommand(args) {
27
35
  const verbose = args['-v'] === true || args['--verbose'] === true;
28
36
  const silent = args['--silent'] === true || args._silent === true;
29
37
  const renameNamespace = args._renameNamespace || null;
38
+ const teamNSOverride = args._teamNS || null;
30
39
 
31
40
  // Silent mode: wrap fmt to suppress all output and exit cleanly on errors
32
41
  const log = {
@@ -41,7 +50,7 @@ export function pullCommand(args) {
41
50
  spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
42
51
  };
43
52
 
44
- // No args = re-pull everything in sync config
53
+ // No args = re-pull everything in sync config (parallel)
45
54
  if (!input) {
46
55
  const cfg = config.load(workspaceDir);
47
56
  if (!cfg) log.cancel('No .sync-config.json found. Run: aw init');
@@ -49,9 +58,19 @@ export function pullCommand(args) {
49
58
  log.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
50
59
  }
51
60
  log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
52
- for (const p of cfg.include) {
53
- pullCommand({ ...args, _positional: [p], _skipIntegrate: true });
54
- }
61
+ const pullJobs = cfg.include.map(p => {
62
+ const isTeamNs = p !== 'platform';
63
+ const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
64
+ return pullAsync({
65
+ ...args,
66
+ _positional: [isTeamNs ? '[template]' : p],
67
+ _workspaceDir: workspaceDir,
68
+ _skipIntegrate: true,
69
+ _renameNamespace: isTeamNs ? p : undefined,
70
+ _teamNS: derivedTeamNS,
71
+ });
72
+ });
73
+ await Promise.all(pullJobs);
55
74
  // Post-pull IDE integration (once after all paths pulled)
56
75
  linkWorkspace(cwd);
57
76
  generateCommands(cwd);
@@ -145,9 +164,10 @@ export function pullCommand(args) {
145
164
  }
146
165
 
147
166
  // Compute plan
148
- const { actions } = computePlan(registryDirs, workspaceDir, [pattern], {
167
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
149
168
  skipOrphans: true,
150
169
  });
170
+ const actions = filterActions(rawActions, pattern);
151
171
 
152
172
  if (dryRun) {
153
173
  printDryRun(actions, verbose);
@@ -157,7 +177,7 @@ export function pullCommand(args) {
157
177
  // Apply
158
178
  const s2 = log.spinner();
159
179
  s2.start('Applying changes...');
160
- const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
180
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
161
181
  updateManifest(workspaceDir, actions, cfg.namespace);
162
182
  s2.stop('Changes applied');
163
183
 
@@ -191,6 +211,84 @@ export function pullCommand(args) {
191
211
  }
192
212
  }
193
213
 
214
+ /**
215
+ * Async pull for parallel use by init. Runs git fetch asynchronously,
216
+ * then applies changes synchronously. Returns a summary object instead
217
+ * of printing directly — the caller controls output.
218
+ */
219
+ export async function pullAsync(args) {
220
+ const input = args._positional?.[0] || '';
221
+ const workspaceDir = args._workspaceDir;
222
+ const renameNamespace = args._renameNamespace || null;
223
+ const teamNSOverride = args._teamNS || null;
224
+
225
+ const resolved = resolveInput(input, workspaceDir);
226
+ let pattern = resolved.registryPath;
227
+ if (!pattern) throw new Error(`Could not resolve "${input}" to a registry path`);
228
+
229
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
230
+
231
+ const cfg = config.load(workspaceDir);
232
+ if (!cfg) throw new Error('No .sync-config.json found');
233
+
234
+ const sparsePaths = includeToSparsePaths([pattern]);
235
+ const tempDir = await sparseCheckoutAsync(cfg.repo, sparsePaths);
236
+
237
+ try {
238
+ const registryDirs = [];
239
+ const regBase = join(tempDir, 'registry');
240
+
241
+ if (existsSync(regBase)) {
242
+ for (const name of listDirs(regBase)) {
243
+ registryDirs.push({ name, path: join(regBase, name) });
244
+ }
245
+ }
246
+
247
+ if (registryDirs.length === 0) {
248
+ throw new Error(`Nothing found in registry for ${pattern}`);
249
+ }
250
+
251
+ if (renameNamespace) {
252
+ for (const rd of registryDirs) {
253
+ if (rd.name === pattern) rd.name = renameNamespace;
254
+ }
255
+ pattern = renameNamespace;
256
+ }
257
+
258
+ let hasMatch = false;
259
+ for (const { name, path } of registryDirs) {
260
+ const entries = walkRegistryTree(path, name);
261
+ if (entries.some(e => matchesAny(e.registryPath, [pattern]))) {
262
+ hasMatch = true;
263
+ break;
264
+ }
265
+ }
266
+
267
+ if (!hasMatch) {
268
+ throw new Error(`Nothing found in registry for ${pattern}`);
269
+ }
270
+
271
+ if (!cfg.include.includes(pattern)) {
272
+ config.addPattern(workspaceDir, pattern);
273
+ }
274
+
275
+ const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
276
+ const actions = filterActions(rawActions, pattern);
277
+ const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
278
+ updateManifest(workspaceDir, actions, cfg.namespace);
279
+
280
+ const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
281
+ for (const fname of ROOT_REGISTRY_FILES) {
282
+ const src = join(regBase, fname);
283
+ if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
284
+ }
285
+
286
+ return { pattern, actions, conflictCount };
287
+ } finally {
288
+ cleanup(tempDir);
289
+ }
290
+ }
291
+
194
292
  function listDirs(dir) {
195
293
  return readdirSync(dir, { withFileTypes: true })
196
294
  .filter(d => d.isDirectory() && !d.name.startsWith('.'))
package/commands/push.mjs CHANGED
@@ -92,8 +92,8 @@ export function pushCommand(args) {
92
92
  const namespacePath = namespaceParts.join('/');
93
93
  const topNamespace = namespaceParts[0];
94
94
 
95
- if (topNamespace === 'ghl') {
96
- fmt.cancel("Cannot push to 'ghl' namespace — it is the shared platform layer");
95
+ if (topNamespace === 'platform') {
96
+ fmt.cancel("Cannot push to 'platform' namespace — it is the shared platform layer");
97
97
  }
98
98
 
99
99
  const isDir = statSync(absPath).isDirectory();
package/git.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  // git.mjs — Git sparse checkout helpers. Zero dependencies.
2
2
 
3
- import { execSync } from 'node:child_process';
3
+ import { execSync, exec as execCb } from 'node:child_process';
4
4
  import { mkdtempSync, existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
+ import { promisify } from 'node:util';
7
8
  import { REGISTRY_BASE_BRANCH } from './constants.mjs';
8
9
 
10
+ const exec = promisify(execCb);
11
+
9
12
  /**
10
- * Sparse-checkout registry paths from GitHub.
13
+ * Sparse-checkout registry paths from GitHub (sync).
11
14
  * Returns the temp directory path containing the checkout.
12
15
  */
13
16
  export function sparseCheckout(repo, paths) {
@@ -35,6 +38,31 @@ export function sparseCheckout(repo, paths) {
35
38
  return tempDir;
36
39
  }
37
40
 
41
+ /**
42
+ * Sparse-checkout registry paths from GitHub (async).
43
+ * Same as sparseCheckout but non-blocking — can run in parallel.
44
+ */
45
+ export async function sparseCheckoutAsync(repo, paths) {
46
+ const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
47
+
48
+ const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
49
+ try {
50
+ await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`);
51
+ } catch (e) {
52
+ throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
53
+ }
54
+
55
+ try {
56
+ await exec('git sparse-checkout init --cone', { cwd: tempDir });
57
+ await exec(`git sparse-checkout set --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, { cwd: tempDir });
58
+ await exec(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir });
59
+ } catch (e) {
60
+ throw new Error(`Failed sparse checkout: ${e.message}`);
61
+ }
62
+
63
+ return tempDir;
64
+ }
65
+
38
66
  /**
39
67
  * Clean up temp directory.
40
68
  */
package/link.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
4
4
  import { join, relative } from 'node:path';
5
5
  import { execSync } from 'node:child_process';
6
+ import { homedir } from 'node:os';
6
7
  import * as fmt from './fmt.mjs';
7
8
 
8
9
  const IDE_DIRS = ['.claude', '.cursor', '.codex'];
@@ -66,9 +67,12 @@ function cleanIdeSymlinks(cwd) {
66
67
  if (!existsSync(ideDir)) continue;
67
68
  cleanSymlinksRecursive(ideDir);
68
69
  }
69
- // Also clean .agents/skills/
70
- const agentsSkillsDir = join(cwd, '.agents', 'skills');
71
- if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
70
+ // Also clean .agents/skills/ (global only — Codex reads from ~/.agents/skills/)
71
+ const HOME = homedir();
72
+ if (cwd === HOME) {
73
+ const agentsSkillsDir = join(cwd, '.agents', 'skills');
74
+ if (existsSync(agentsSkillsDir)) cleanSymlinksRecursive(agentsSkillsDir);
75
+ }
72
76
  }
73
77
 
74
78
  /**
@@ -184,20 +188,22 @@ export function linkWorkspace(cwd) {
184
188
  }
185
189
  }
186
190
 
187
- // Codex per-skill symlinks: .agents/skills/<name>
188
- const agentsSkillsDir = join(cwd, '.agents/skills');
189
- for (const ns of namespaces) {
190
- for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
191
- mkdirSync(agentsSkillsDir, { recursive: true });
192
- for (const skill of listDirs(skillsDir)) {
193
- const flat = [ns, ...segments, skill].join('-');
194
- const linkPath = join(agentsSkillsDir, flat);
195
- const targetPath = join(skillsDir, skill);
196
- const relTarget = relative(agentsSkillsDir, targetPath);
197
- try {
198
- execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
199
- created++;
200
- } catch { /* best effort */ }
191
+ // Codex per-skill symlinks: ~/.agents/skills/<name> (global only)
192
+ if (cwd === homedir()) {
193
+ const agentsSkillsDir = join(cwd, '.agents/skills');
194
+ for (const ns of namespaces) {
195
+ for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
196
+ mkdirSync(agentsSkillsDir, { recursive: true });
197
+ for (const skill of listDirs(skillsDir)) {
198
+ const flat = [ns, ...segments, skill].join('-');
199
+ const linkPath = join(agentsSkillsDir, flat);
200
+ const targetPath = join(skillsDir, skill);
201
+ const relTarget = relative(agentsSkillsDir, targetPath);
202
+ try {
203
+ execSync(`ln -sfn "${relTarget}" "${linkPath}"`, { stdio: 'pipe' });
204
+ created++;
205
+ } catch { /* best effort */ }
206
+ }
201
207
  }
202
208
  }
203
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.25-beta.1",
3
+ "version": "0.1.25-beta.10",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {