@ghl-ai/aw 0.1.25-beta.1 → 0.1.25-beta.11
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 +3 -2
- package/commands/init.mjs +122 -30
- package/commands/pull.mjs +106 -8
- package/commands/push.mjs +2 -2
- package/git.mjs +30 -2
- package/link.mjs +23 -17
- package/package.json +1 -1
- package/plan.mjs +7 -5
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
|
|
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,102 @@ 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
|
-
|
|
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
|
|
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 (
|
|
150
|
-
fmt.cancel(
|
|
174
|
+
if (team && !subTeam) {
|
|
175
|
+
fmt.cancel([
|
|
176
|
+
`Missing sub-team in ${chalk.red(namespace)}`,
|
|
177
|
+
'',
|
|
178
|
+
` ${chalk.dim('Format:')} --namespace <team>/<sub-team>`,
|
|
179
|
+
'',
|
|
180
|
+
` ${chalk.dim('Example:')} ${chalk.bold(`aw init --namespace ${team}/courses`)}`,
|
|
181
|
+
].join('\n'));
|
|
151
182
|
}
|
|
152
183
|
|
|
153
|
-
const
|
|
184
|
+
const SLUG_RE = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
185
|
+
if (team && !SLUG_RE.test(team)) {
|
|
186
|
+
fmt.cancel(`Invalid team '${team}' — must match: ${SLUG_RE}`);
|
|
187
|
+
}
|
|
188
|
+
if (subTeam && !SLUG_RE.test(subTeam)) {
|
|
189
|
+
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const hasConfig = config.exists(GLOBAL_AW_DIR);
|
|
193
|
+
const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
|
|
194
|
+
const isExisting = hasConfig && hasPlatform;
|
|
154
195
|
const cwd = process.cwd();
|
|
155
196
|
|
|
156
197
|
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
157
198
|
|
|
158
199
|
if (isExisting) {
|
|
159
|
-
|
|
200
|
+
const cfg = config.load(GLOBAL_AW_DIR);
|
|
201
|
+
|
|
202
|
+
// Add new sub-team if not already tracked
|
|
203
|
+
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
204
|
+
if (isNewSubTeam) {
|
|
205
|
+
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
206
|
+
await pullAsync({
|
|
207
|
+
...args,
|
|
208
|
+
_positional: ['[template]'],
|
|
209
|
+
_renameNamespace: folderName,
|
|
210
|
+
_teamNS: teamNS,
|
|
211
|
+
_workspaceDir: GLOBAL_AW_DIR,
|
|
212
|
+
_skipIntegrate: true,
|
|
213
|
+
});
|
|
214
|
+
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
215
|
+
} else {
|
|
216
|
+
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
217
|
+
}
|
|
160
218
|
|
|
161
|
-
// Pull latest
|
|
162
|
-
|
|
219
|
+
// Pull latest (parallel)
|
|
220
|
+
// cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
|
|
221
|
+
// only has 'registry/[template]/' — remap non-platform entries back.
|
|
222
|
+
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
223
|
+
if (freshCfg && freshCfg.include.length > 0) {
|
|
224
|
+
const pullJobs = freshCfg.include.map(p => {
|
|
225
|
+
const isTeamNs = p !== 'platform';
|
|
226
|
+
const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
|
|
227
|
+
return pullAsync({
|
|
228
|
+
...args,
|
|
229
|
+
_positional: [isTeamNs ? '[template]' : p],
|
|
230
|
+
_workspaceDir: GLOBAL_AW_DIR,
|
|
231
|
+
_skipIntegrate: true,
|
|
232
|
+
_renameNamespace: isTeamNs ? p : undefined,
|
|
233
|
+
_teamNS: derivedTeamNS,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
await Promise.all(pullJobs);
|
|
237
|
+
}
|
|
163
238
|
|
|
164
239
|
// Re-link IDE dirs (idempotent)
|
|
165
240
|
linkWorkspace(HOME);
|
|
166
241
|
generateCommands(HOME);
|
|
167
|
-
copyInstructions(HOME, null, namespace) || [];
|
|
242
|
+
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
168
243
|
initAwDocs(HOME);
|
|
169
|
-
setupMcp(HOME, namespace) || [];
|
|
244
|
+
setupMcp(HOME, freshCfg?.namespace || team) || [];
|
|
170
245
|
|
|
171
246
|
// Link current project if needed
|
|
172
247
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
@@ -178,7 +253,7 @@ export async function initCommand(args) {
|
|
|
178
253
|
|
|
179
254
|
if (!silent) {
|
|
180
255
|
fmt.outro([
|
|
181
|
-
'Sync complete',
|
|
256
|
+
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
|
|
182
257
|
'',
|
|
183
258
|
` ${chalk.green('✓')} Registry updated`,
|
|
184
259
|
` ${chalk.green('✓')} IDE integration refreshed`,
|
|
@@ -200,36 +275,53 @@ export async function initCommand(args) {
|
|
|
200
275
|
// Step 1: Create global source of truth
|
|
201
276
|
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
202
277
|
|
|
203
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
|
|
278
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
204
279
|
|
|
205
280
|
fmt.note([
|
|
206
281
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
207
|
-
|
|
282
|
+
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
208
283
|
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
209
284
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
210
285
|
].filter(Boolean).join('\n'), 'Config created');
|
|
211
286
|
|
|
212
|
-
// Step 2: Pull registry content
|
|
213
|
-
|
|
287
|
+
// Step 2: Pull registry content (parallel)
|
|
288
|
+
const s = fmt.spinner();
|
|
289
|
+
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
290
|
+
s.start(`Pulling ${pullTargets}...`);
|
|
291
|
+
|
|
292
|
+
const pullJobs = [
|
|
293
|
+
pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
294
|
+
];
|
|
295
|
+
if (folderName) {
|
|
296
|
+
pullJobs.push(
|
|
297
|
+
pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
214
300
|
|
|
215
|
-
|
|
216
|
-
|
|
301
|
+
let pullResults;
|
|
302
|
+
try {
|
|
303
|
+
pullResults = await Promise.all(pullJobs);
|
|
304
|
+
s.stop(`Pulled ${pullTargets}`);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
s.stop(chalk.red('Pull failed'));
|
|
307
|
+
fmt.cancel(e.message);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const { pattern, actions } of pullResults) {
|
|
311
|
+
printPullSummary(pattern, actions);
|
|
217
312
|
}
|
|
218
313
|
|
|
219
|
-
// Step 3: Link
|
|
314
|
+
// Step 3: Link IDE dirs + setup tasks
|
|
315
|
+
fmt.logStep('Linking IDE symlinks...');
|
|
220
316
|
linkWorkspace(HOME);
|
|
221
317
|
generateCommands(HOME);
|
|
222
|
-
const instructionFiles = copyInstructions(HOME, null,
|
|
318
|
+
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
223
319
|
initAwDocs(HOME);
|
|
224
|
-
const mcpFiles = setupMcp(HOME,
|
|
225
|
-
|
|
226
|
-
// Step 4: Git template hook (omnipresence)
|
|
320
|
+
const mcpFiles = setupMcp(HOME, team) || [];
|
|
227
321
|
const gitTemplateInstalled = installGitTemplate();
|
|
228
|
-
|
|
229
|
-
// Step 5: IDE auto-init tasks
|
|
230
322
|
installIdeTasks();
|
|
231
323
|
|
|
232
|
-
// Step
|
|
324
|
+
// Step 4: Symlink in current directory if it's a git repo
|
|
233
325
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
234
326
|
try {
|
|
235
327
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -237,7 +329,7 @@ export async function initCommand(args) {
|
|
|
237
329
|
} catch { /* best effort */ }
|
|
238
330
|
}
|
|
239
331
|
|
|
240
|
-
// Step
|
|
332
|
+
// Step 5: Write manifest for nuke cleanup
|
|
241
333
|
const manifest = {
|
|
242
334
|
version: 1,
|
|
243
335
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 === '
|
|
96
|
-
fmt.cancel("Cannot push to '
|
|
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
|
|
71
|
-
if (
|
|
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:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for (const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
package/plan.mjs
CHANGED
|
@@ -24,11 +24,12 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
|
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// Key must include namespacePath to avoid collisions when the same
|
|
28
|
+
// slug exists under different domains (e.g. backend/agents/developer
|
|
29
|
+
// vs frontend/agents/developer).
|
|
29
30
|
const key = entry.skillRelPath
|
|
30
|
-
? `${entry.type}/${entry.slug}/${entry.skillRelPath}`
|
|
31
|
-
: `${entry.type}/${entry.slug}`;
|
|
31
|
+
? `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath}`
|
|
32
|
+
: `${entry.namespacePath}/${entry.type}/${entry.slug}`;
|
|
32
33
|
plan.set(key, { ...entry, source: entry.namespacePath || name });
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -108,9 +109,10 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
if (typeIdx === -1) continue;
|
|
112
|
+
const namespace = parts.slice(0, typeIdx).join('/');
|
|
111
113
|
const type = parts[typeIdx];
|
|
112
114
|
const slug = parts[typeIdx + 1]?.replace(/\.md$/, '');
|
|
113
|
-
const key = `${type}/${slug}`;
|
|
115
|
+
const key = `${namespace}/${type}/${slug}`;
|
|
114
116
|
if (!plan.has(key)) {
|
|
115
117
|
actions.push({
|
|
116
118
|
slug,
|