@ghl-ai/aw 0.1.25-beta.9 → 0.1.26-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/cli.mjs +2 -1
- package/commands/init.mjs +63 -23
- package/commands/pull.mjs +20 -6
- package/commands/push.mjs +6 -6
- package/commands/search.mjs +5 -5
- package/constants.mjs +5 -2
- package/git.mjs +4 -7
- package/integrate.mjs +39 -69
- package/mcp.mjs +40 -4
- 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)'),
|
package/commands/init.mjs
CHANGED
|
@@ -146,26 +146,47 @@ export async function initCommand(args) {
|
|
|
146
146
|
fmt.cancel([
|
|
147
147
|
`Missing required ${chalk.bold('--namespace')} flag`,
|
|
148
148
|
'',
|
|
149
|
-
` ${chalk.dim('Usage:')} aw init --namespace <team>`,
|
|
149
|
+
` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
|
|
150
150
|
` ${chalk.dim('Teams:')} ${list}`,
|
|
151
151
|
'',
|
|
152
|
-
` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
|
|
152
|
+
` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
|
|
153
153
|
].join('\n'));
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
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)) {
|
|
157
164
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
158
165
|
fmt.cancel([
|
|
159
|
-
`Unknown
|
|
166
|
+
`Unknown team ${chalk.red(team)}`,
|
|
160
167
|
'',
|
|
161
168
|
` ${chalk.dim('Allowed:')} ${list}`,
|
|
162
169
|
'',
|
|
163
|
-
` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce')}`,
|
|
170
|
+
` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
|
|
171
|
+
].join('\n'));
|
|
172
|
+
}
|
|
173
|
+
|
|
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`)}`,
|
|
164
181
|
].join('\n'));
|
|
165
182
|
}
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
|
|
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}`);
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
const hasConfig = config.exists(GLOBAL_AW_DIR);
|
|
@@ -176,21 +197,40 @@ export async function initCommand(args) {
|
|
|
176
197
|
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
177
198
|
|
|
178
199
|
if (isExisting) {
|
|
179
|
-
|
|
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
|
+
}
|
|
180
218
|
|
|
181
219
|
// Pull latest (parallel)
|
|
182
|
-
// cfg.include has the renamed namespace (e.g. 'revex'), but the repo
|
|
183
|
-
// only has '
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
const pullJobs =
|
|
220
|
+
// cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
|
|
221
|
+
// only has '.aw_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 => {
|
|
187
225
|
const isTeamNs = p !== 'platform';
|
|
226
|
+
const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
|
|
188
227
|
return pullAsync({
|
|
189
228
|
...args,
|
|
190
229
|
_positional: [isTeamNs ? '[template]' : p],
|
|
191
230
|
_workspaceDir: GLOBAL_AW_DIR,
|
|
192
231
|
_skipIntegrate: true,
|
|
193
232
|
_renameNamespace: isTeamNs ? p : undefined,
|
|
233
|
+
_teamNS: derivedTeamNS,
|
|
194
234
|
});
|
|
195
235
|
});
|
|
196
236
|
await Promise.all(pullJobs);
|
|
@@ -199,9 +239,9 @@ export async function initCommand(args) {
|
|
|
199
239
|
// Re-link IDE dirs (idempotent)
|
|
200
240
|
linkWorkspace(HOME);
|
|
201
241
|
generateCommands(HOME);
|
|
202
|
-
copyInstructions(HOME, null, namespace) || [];
|
|
242
|
+
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
203
243
|
initAwDocs(HOME);
|
|
204
|
-
setupMcp(HOME, namespace) || [];
|
|
244
|
+
setupMcp(HOME, freshCfg?.namespace || team) || [];
|
|
205
245
|
|
|
206
246
|
// Link current project if needed
|
|
207
247
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
@@ -213,7 +253,7 @@ export async function initCommand(args) {
|
|
|
213
253
|
|
|
214
254
|
if (!silent) {
|
|
215
255
|
fmt.outro([
|
|
216
|
-
'Sync complete',
|
|
256
|
+
isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
|
|
217
257
|
'',
|
|
218
258
|
` ${chalk.green('✓')} Registry updated`,
|
|
219
259
|
` ${chalk.green('✓')} IDE integration refreshed`,
|
|
@@ -235,26 +275,26 @@ export async function initCommand(args) {
|
|
|
235
275
|
// Step 1: Create global source of truth
|
|
236
276
|
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
237
277
|
|
|
238
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace, user });
|
|
278
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
239
279
|
|
|
240
280
|
fmt.note([
|
|
241
281
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
242
|
-
|
|
282
|
+
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
243
283
|
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
244
284
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
245
285
|
].filter(Boolean).join('\n'), 'Config created');
|
|
246
286
|
|
|
247
287
|
// Step 2: Pull registry content (parallel)
|
|
248
288
|
const s = fmt.spinner();
|
|
249
|
-
const pullTargets =
|
|
289
|
+
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
250
290
|
s.start(`Pulling ${pullTargets}...`);
|
|
251
291
|
|
|
252
292
|
const pullJobs = [
|
|
253
293
|
pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
254
294
|
];
|
|
255
|
-
if (
|
|
295
|
+
if (folderName) {
|
|
256
296
|
pullJobs.push(
|
|
257
|
-
pullAsync({ ...args, _positional: ['[template]'], _renameNamespace:
|
|
297
|
+
pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
258
298
|
);
|
|
259
299
|
}
|
|
260
300
|
|
|
@@ -275,9 +315,9 @@ export async function initCommand(args) {
|
|
|
275
315
|
fmt.logStep('Linking IDE symlinks...');
|
|
276
316
|
linkWorkspace(HOME);
|
|
277
317
|
generateCommands(HOME);
|
|
278
|
-
const instructionFiles = copyInstructions(HOME, null,
|
|
318
|
+
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
279
319
|
initAwDocs(HOME);
|
|
280
|
-
const mcpFiles = setupMcp(HOME,
|
|
320
|
+
const mcpFiles = setupMcp(HOME, team) || [];
|
|
281
321
|
const gitTemplateInstalled = installGitTemplate();
|
|
282
322
|
installIdeTasks();
|
|
283
323
|
|
package/commands/pull.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import * as config from '../config.mjs';
|
|
|
8
8
|
import * as fmt from '../fmt.mjs';
|
|
9
9
|
import { chalk } from '../fmt.mjs';
|
|
10
10
|
import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
|
|
11
|
+
import { REGISTRY_DIR } from '../constants.mjs';
|
|
11
12
|
import { walkRegistryTree } from '../registry.mjs';
|
|
12
13
|
import { matchesAny } from '../glob.mjs';
|
|
13
14
|
import { computePlan } from '../plan.mjs';
|
|
@@ -17,6 +18,13 @@ import { resolveInput } from '../paths.mjs';
|
|
|
17
18
|
import { linkWorkspace } from '../link.mjs';
|
|
18
19
|
import { generateCommands, copyInstructions } from '../integrate.mjs';
|
|
19
20
|
|
|
21
|
+
// Filter out top-level platform CLI meta-commands (drop, pull, push, etc.)
|
|
22
|
+
// but keep domain-specific commands (platform/design/commands/, platform/infra/commands/).
|
|
23
|
+
function filterActions(actions, pattern) {
|
|
24
|
+
if (pattern !== 'platform') return actions;
|
|
25
|
+
return actions.filter(a => !(a.type === 'commands' && a.namespacePath === 'platform'));
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export async function pullCommand(args) {
|
|
21
29
|
const input = args._positional?.[0] || '';
|
|
22
30
|
const cwd = process.cwd();
|
|
@@ -27,6 +35,7 @@ export async 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 = {
|
|
@@ -51,12 +60,14 @@ export async function pullCommand(args) {
|
|
|
51
60
|
log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
|
|
52
61
|
const pullJobs = cfg.include.map(p => {
|
|
53
62
|
const isTeamNs = p !== 'platform';
|
|
63
|
+
const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
|
|
54
64
|
return pullAsync({
|
|
55
65
|
...args,
|
|
56
66
|
_positional: [isTeamNs ? '[template]' : p],
|
|
57
67
|
_workspaceDir: workspaceDir,
|
|
58
68
|
_skipIntegrate: true,
|
|
59
69
|
_renameNamespace: isTeamNs ? p : undefined,
|
|
70
|
+
_teamNS: derivedTeamNS,
|
|
60
71
|
});
|
|
61
72
|
});
|
|
62
73
|
await Promise.all(pullJobs);
|
|
@@ -101,7 +112,7 @@ export async function pullCommand(args) {
|
|
|
101
112
|
|
|
102
113
|
try {
|
|
103
114
|
const registryDirs = [];
|
|
104
|
-
const regBase = join(tempDir,
|
|
115
|
+
const regBase = join(tempDir, REGISTRY_DIR);
|
|
105
116
|
|
|
106
117
|
if (existsSync(regBase)) {
|
|
107
118
|
for (const name of listDirs(regBase)) {
|
|
@@ -153,9 +164,10 @@ export async function pullCommand(args) {
|
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
// Compute plan
|
|
156
|
-
const { actions } = computePlan(registryDirs, workspaceDir, [pattern], {
|
|
167
|
+
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
|
|
157
168
|
skipOrphans: true,
|
|
158
169
|
});
|
|
170
|
+
const actions = filterActions(rawActions, pattern);
|
|
159
171
|
|
|
160
172
|
if (dryRun) {
|
|
161
173
|
printDryRun(actions, verbose);
|
|
@@ -165,7 +177,7 @@ export async function pullCommand(args) {
|
|
|
165
177
|
// Apply
|
|
166
178
|
const s2 = log.spinner();
|
|
167
179
|
s2.start('Applying changes...');
|
|
168
|
-
const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
|
|
180
|
+
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
169
181
|
updateManifest(workspaceDir, actions, cfg.namespace);
|
|
170
182
|
s2.stop('Changes applied');
|
|
171
183
|
|
|
@@ -208,6 +220,7 @@ export async function pullAsync(args) {
|
|
|
208
220
|
const input = args._positional?.[0] || '';
|
|
209
221
|
const workspaceDir = args._workspaceDir;
|
|
210
222
|
const renameNamespace = args._renameNamespace || null;
|
|
223
|
+
const teamNSOverride = args._teamNS || null;
|
|
211
224
|
|
|
212
225
|
const resolved = resolveInput(input, workspaceDir);
|
|
213
226
|
let pattern = resolved.registryPath;
|
|
@@ -223,7 +236,7 @@ export async function pullAsync(args) {
|
|
|
223
236
|
|
|
224
237
|
try {
|
|
225
238
|
const registryDirs = [];
|
|
226
|
-
const regBase = join(tempDir,
|
|
239
|
+
const regBase = join(tempDir, REGISTRY_DIR);
|
|
227
240
|
|
|
228
241
|
if (existsSync(regBase)) {
|
|
229
242
|
for (const name of listDirs(regBase)) {
|
|
@@ -259,8 +272,9 @@ export async function pullAsync(args) {
|
|
|
259
272
|
config.addPattern(workspaceDir, pattern);
|
|
260
273
|
}
|
|
261
274
|
|
|
262
|
-
const { actions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
|
|
263
|
-
const
|
|
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 });
|
|
264
278
|
updateManifest(workspaceDir, actions, cfg.namespace);
|
|
265
279
|
|
|
266
280
|
const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
|
package/commands/push.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { execSync, execFileSync } from 'node:child_process';
|
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import * as fmt from '../fmt.mjs';
|
|
8
8
|
import { chalk } from '../fmt.mjs';
|
|
9
|
-
import { REGISTRY_REPO, REGISTRY_BASE_BRANCH } from '../constants.mjs';
|
|
9
|
+
import { REGISTRY_REPO, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
10
10
|
import { resolveInput } from '../paths.mjs';
|
|
11
11
|
import { load as loadManifest } from '../manifest.mjs';
|
|
12
12
|
import { hashFile } from '../registry.mjs';
|
|
@@ -98,8 +98,8 @@ export function pushCommand(args) {
|
|
|
98
98
|
|
|
99
99
|
const isDir = statSync(absPath).isDirectory();
|
|
100
100
|
const registryTarget = isDir
|
|
101
|
-
?
|
|
102
|
-
:
|
|
101
|
+
? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
|
|
102
|
+
: `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
|
|
103
103
|
|
|
104
104
|
fmt.note(
|
|
105
105
|
[
|
|
@@ -148,13 +148,13 @@ export function pushCommand(args) {
|
|
|
148
148
|
|
|
149
149
|
// Check if this is a new namespace — auto-add CODEOWNERS entry
|
|
150
150
|
let newNamespace = false;
|
|
151
|
-
const nsDir = join(tempDir,
|
|
151
|
+
const nsDir = join(tempDir, REGISTRY_DIR, topNamespace);
|
|
152
152
|
const codeownersPath = join(tempDir, 'CODEOWNERS');
|
|
153
153
|
if (!existsSync(nsDir) || isNewNamespaceInCodeowners(codeownersPath, topNamespace)) {
|
|
154
154
|
newNamespace = true;
|
|
155
155
|
const ghUser = getGitHubUser();
|
|
156
156
|
if (ghUser && existsSync(codeownersPath)) {
|
|
157
|
-
const line =
|
|
157
|
+
const line = `/${REGISTRY_DIR}/${topNamespace}/ @${ghUser}\n`;
|
|
158
158
|
appendFileSync(codeownersPath, line);
|
|
159
159
|
execSync('git add CODEOWNERS', { cwd: tempDir, stdio: 'pipe' });
|
|
160
160
|
}
|
|
@@ -236,5 +236,5 @@ function getGitHubUser() {
|
|
|
236
236
|
function isNewNamespaceInCodeowners(codeownersPath, namespace) {
|
|
237
237
|
if (!existsSync(codeownersPath)) return true;
|
|
238
238
|
const content = readFileSync(codeownersPath, 'utf8');
|
|
239
|
-
return !content.includes(
|
|
239
|
+
return !content.includes(`/${REGISTRY_DIR}/${namespace}/`);
|
|
240
240
|
}
|
package/commands/search.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 { REGISTRY_BASE_BRANCH, REGISTRY_REPO } from '../constants.mjs';
|
|
10
|
+
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, REGISTRY_DIR } from '../constants.mjs';
|
|
11
11
|
|
|
12
12
|
export function searchCommand(args) {
|
|
13
13
|
const query = (args._positional || []).join(' ').toLowerCase();
|
|
@@ -150,14 +150,14 @@ function searchRemote(repo, query) {
|
|
|
150
150
|
try {
|
|
151
151
|
// git archive fetches a single file from remote — no clone needed
|
|
152
152
|
content = execSync(
|
|
153
|
-
`git archive --remote="${repoUrl}" ${REGISTRY_BASE_BRANCH}
|
|
153
|
+
`git archive --remote="${repoUrl}" ${REGISTRY_BASE_BRANCH} ${REGISTRY_DIR}/registry.json | tar -xO ${REGISTRY_DIR}/registry.json`,
|
|
154
154
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
155
155
|
);
|
|
156
156
|
} catch {
|
|
157
157
|
// GitHub doesn't support git archive — fallback to gh API
|
|
158
158
|
try {
|
|
159
159
|
const raw = execSync(
|
|
160
|
-
`gh api "repos/${repo}/contents/registry
|
|
160
|
+
`gh api "repos/${repo}/contents/${REGISTRY_DIR}/registry.json?ref=${REGISTRY_BASE_BRANCH}" --jq .content -H "Accept: application/vnd.github.v3+json"`,
|
|
161
161
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
162
162
|
);
|
|
163
163
|
content = Buffer.from(raw.trim(), 'base64').toString('utf8');
|
|
@@ -167,9 +167,9 @@ function searchRemote(repo, query) {
|
|
|
167
167
|
try {
|
|
168
168
|
execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' });
|
|
169
169
|
execSync('git sparse-checkout init --cone', { cwd: tempDir, stdio: 'pipe' });
|
|
170
|
-
execSync(
|
|
170
|
+
execSync(`git sparse-checkout set --skip-checks "${REGISTRY_DIR}/registry.json"`, { cwd: tempDir, stdio: 'pipe' });
|
|
171
171
|
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir, stdio: 'pipe' });
|
|
172
|
-
content = readFileSync(join(tempDir,
|
|
172
|
+
content = readFileSync(join(tempDir, REGISTRY_DIR, 'registry.json'), 'utf8');
|
|
173
173
|
} finally {
|
|
174
174
|
try { execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' }); } catch {}
|
|
175
175
|
}
|
package/constants.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// constants.mjs — Single source of truth for registry settings.
|
|
2
2
|
|
|
3
3
|
/** Base branch for PRs and sync checkout */
|
|
4
|
-
export const REGISTRY_BASE_BRANCH = '
|
|
4
|
+
export const REGISTRY_BASE_BRANCH = 'feat/aw-platform-docs';
|
|
5
5
|
|
|
6
6
|
/** Default registry repository */
|
|
7
|
-
export const REGISTRY_REPO = 'GoHighLevel/
|
|
7
|
+
export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
|
|
8
|
+
|
|
9
|
+
/** Directory inside the registry repo that holds platform/ and [template]/ */
|
|
10
|
+
export const REGISTRY_DIR = '.aw_registry';
|
package/git.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdtempSync, existsSync } from 'node:fs';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
-
import { REGISTRY_BASE_BRANCH } from './constants.mjs';
|
|
8
|
+
import { REGISTRY_BASE_BRANCH, REGISTRY_DIR } from './constants.mjs';
|
|
9
9
|
|
|
10
10
|
const exec = promisify(execCb);
|
|
11
11
|
|
|
@@ -76,16 +76,13 @@ export function cleanup(tempDir) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Compute sparse checkout paths from include paths.
|
|
79
|
-
* e.g., ["platform", "dev/agents/debugger"] -> ["
|
|
79
|
+
* e.g., ["platform", "dev/agents/debugger"] -> [".aw_registry/platform", ".aw_registry/dev/agents/debugger"]
|
|
80
80
|
*/
|
|
81
81
|
export function includeToSparsePaths(paths) {
|
|
82
82
|
const result = new Set();
|
|
83
83
|
for (const p of paths) {
|
|
84
|
-
result.add(
|
|
84
|
+
result.add(`${REGISTRY_DIR}/${p}`);
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
result.add('registry/CLAUDE.md');
|
|
88
|
-
result.add('registry/AGENTS.md');
|
|
89
|
-
result.add('registry/AW-PROTOCOL.md');
|
|
86
|
+
result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
|
|
90
87
|
return [...result];
|
|
91
88
|
}
|
package/integrate.mjs
CHANGED
|
@@ -3,24 +3,11 @@
|
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import * as fmt from './fmt.mjs';
|
|
6
|
-
|
|
7
|
-
// AW CLI commands to generate
|
|
8
|
-
const AW_COMMANDS = [
|
|
9
|
-
{ name: 'pull', description: 'Pull agents & skills from registry', hint: '<path>' },
|
|
10
|
-
{ name: 'push', description: 'Push local changes to registry', hint: '<path>' },
|
|
11
|
-
{ name: 'status', description: 'Show workspace sync status', hint: '' },
|
|
12
|
-
{ name: 'drop', description: 'Stop syncing a path', hint: '<path>' },
|
|
13
|
-
{ name: 'search', description: 'Search local and remote registry', hint: '<query>' },
|
|
14
|
-
{ name: 'nuke', description: 'Remove entire .aw_registry/', hint: '' },
|
|
15
|
-
];
|
|
6
|
+
import * as config from './config.mjs';
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* link.mjs symlinks everything from commands/ → .claude/commands/aw/ for discovery.
|
|
21
|
-
*
|
|
22
|
-
* A .generated-manifest.json tracks which files were generated so they can be
|
|
23
|
-
* cleaned on rebuild without needing filename prefixes.
|
|
9
|
+
* Count hand-written commands already present in the registry.
|
|
10
|
+
* No CLI stub generation — all commands come from the registry itself.
|
|
24
11
|
*/
|
|
25
12
|
export function generateCommands(cwd) {
|
|
26
13
|
const awDir = join(cwd, '.aw_registry');
|
|
@@ -29,56 +16,15 @@ export function generateCommands(cwd) {
|
|
|
29
16
|
const oldGenDir = join(awDir, '.generated-commands');
|
|
30
17
|
if (existsSync(oldGenDir)) rmSync(oldGenDir, { recursive: true, force: true });
|
|
31
18
|
|
|
19
|
+
// Count hand-written commands across all namespaces for reporting
|
|
32
20
|
let count = 0;
|
|
33
|
-
const namespaces =
|
|
34
|
-
|
|
21
|
+
const namespaces = getTeamNamespaces(awDir);
|
|
35
22
|
for (const ns of namespaces) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for (const cmd of AW_COMMANDS) {
|
|
41
|
-
const fileName = `${cmd.name}.md`;
|
|
42
|
-
// Skip if a hand-written command with same name exists
|
|
43
|
-
if (existsSync(join(commandsDir, fileName))) continue;
|
|
44
|
-
|
|
45
|
-
const content = [
|
|
46
|
-
'---',
|
|
47
|
-
`name: aw:${cmd.name}`,
|
|
48
|
-
`description: ${cmd.description}`,
|
|
49
|
-
cmd.hint ? `argument-hint: "${cmd.hint}"` : null,
|
|
50
|
-
'---',
|
|
51
|
-
'',
|
|
52
|
-
'Run the following command:',
|
|
53
|
-
'',
|
|
54
|
-
'```',
|
|
55
|
-
`node bin/aw ${cmd.name} $ARGUMENTS`,
|
|
56
|
-
'```',
|
|
57
|
-
'',
|
|
58
|
-
].filter(v => v !== null).join('\n');
|
|
59
|
-
|
|
60
|
-
writeFileSync(join(commandsDir, fileName), content);
|
|
61
|
-
count++;
|
|
23
|
+
const nsDir = join(awDir, ns);
|
|
24
|
+
if (existsSync(nsDir)) {
|
|
25
|
+
const cmdFiles = findFiles(nsDir, 'commands');
|
|
26
|
+
count += cmdFiles.length;
|
|
62
27
|
}
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Codex skills — .agents/skills/<name>/SKILL.md
|
|
67
|
-
const agentsSkillsDir = join(cwd, '.agents/skills');
|
|
68
|
-
mkdirSync(agentsSkillsDir, { recursive: true });
|
|
69
|
-
for (const cmd of AW_COMMANDS) {
|
|
70
|
-
const skillDir = join(agentsSkillsDir, cmd.name);
|
|
71
|
-
mkdirSync(skillDir, { recursive: true });
|
|
72
|
-
const content = [
|
|
73
|
-
'---',
|
|
74
|
-
`name: ${cmd.name}`,
|
|
75
|
-
`description: ${cmd.description}`,
|
|
76
|
-
'---',
|
|
77
|
-
'',
|
|
78
|
-
`Run: \`node bin/aw ${cmd.name}\` followed by the user's arguments.`,
|
|
79
|
-
'',
|
|
80
|
-
].join('\n');
|
|
81
|
-
writeFileSync(join(skillDir, 'SKILL.md'), content);
|
|
82
28
|
}
|
|
83
29
|
|
|
84
30
|
if (count > 0) {
|
|
@@ -88,6 +34,27 @@ export function generateCommands(cwd) {
|
|
|
88
34
|
return count;
|
|
89
35
|
}
|
|
90
36
|
|
|
37
|
+
function findFiles(dir, typeName) {
|
|
38
|
+
const results = [];
|
|
39
|
+
function walk(d) {
|
|
40
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
41
|
+
if (entry.name.startsWith('.')) continue;
|
|
42
|
+
const full = join(d, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (entry.name === typeName) {
|
|
45
|
+
for (const f of readdirSync(full)) {
|
|
46
|
+
if (f.endsWith('.md')) results.push(join(full, f));
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
walk(full);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(dir);
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
91
58
|
/**
|
|
92
59
|
* Copy CLAUDE.md and AGENTS.md to project root.
|
|
93
60
|
*/
|
|
@@ -98,7 +65,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
|
|
|
98
65
|
if (existsSync(dest)) continue;
|
|
99
66
|
|
|
100
67
|
if (tempDir) {
|
|
101
|
-
const src = join(tempDir, '
|
|
68
|
+
const src = join(tempDir, '.aw_registry', file);
|
|
102
69
|
if (existsSync(src)) {
|
|
103
70
|
let content = readFileSync(src, 'utf8');
|
|
104
71
|
if (namespace) {
|
|
@@ -460,10 +427,13 @@ No active tasks. Tasks are created during workflow execution.
|
|
|
460
427
|
fmt.logSuccess('Created .aw_docs/ (local orchestration state)');
|
|
461
428
|
}
|
|
462
429
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Return team namespace paths from config (excludes 'platform').
|
|
432
|
+
* E.g. ['revex/courses'] — generated CLI commands only go into team namespaces.
|
|
433
|
+
*/
|
|
434
|
+
function getTeamNamespaces(awDir) {
|
|
435
|
+
const cfg = config.load(awDir);
|
|
436
|
+
if (!cfg || !cfg.include) return [];
|
|
437
|
+
return cfg.include.filter(p => p !== 'platform');
|
|
468
438
|
}
|
|
469
439
|
|
package/mcp.mjs
CHANGED
|
@@ -35,12 +35,12 @@ export function setupMcp(cwd, namespace) {
|
|
|
35
35
|
|
|
36
36
|
const mcpUrl = paths.ghlMcpUrl;
|
|
37
37
|
|
|
38
|
-
const ghlAiServer = { type: '
|
|
38
|
+
const ghlAiServer = { type: 'http', url: mcpUrl };
|
|
39
39
|
const gitJenkinsServer = paths.gitJenkinsPath
|
|
40
40
|
? { command: 'node', args: [paths.gitJenkinsPath] }
|
|
41
41
|
: null;
|
|
42
42
|
|
|
43
|
-
// ── Claude Code: ~/.claude/settings.json ──
|
|
43
|
+
// ── Claude Code: ~/.claude/settings.json (global) ──
|
|
44
44
|
const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
|
|
45
45
|
if (mergeJsonMcpServer(claudeSettingsPath, 'ghl-ai', ghlAiServer)) {
|
|
46
46
|
updatedFiles.push(claudeSettingsPath);
|
|
@@ -49,7 +49,37 @@ export function setupMcp(cwd, namespace) {
|
|
|
49
49
|
updatedFiles.push(claudeSettingsPath);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// ──
|
|
52
|
+
// ── Claude Code: .mcp.json (project root — highest priority) ──
|
|
53
|
+
// Claude Code resolves project .mcp.json before global settings.json,
|
|
54
|
+
// so we must write here to ensure the HTTP URL takes precedence over
|
|
55
|
+
// any stale command-based configs in parent directories.
|
|
56
|
+
const projectMcpPath = join(cwd, '.mcp.json');
|
|
57
|
+
if (mergeJsonMcpServer(projectMcpPath, 'ghl-ai', ghlAiServer)) {
|
|
58
|
+
updatedFiles.push(projectMcpPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Claude Code: .claude/mcp.json (workspace-level) ──
|
|
62
|
+
// Claude Code also reads .claude/mcp.json for workspace-scoped MCP config.
|
|
63
|
+
// Write here so the MCP server is available regardless of which resolution
|
|
64
|
+
// path Claude Code uses (root .mcp.json or .claude/mcp.json).
|
|
65
|
+
const claudeWorkspaceMcpPath = join(cwd, '.claude', 'mcp.json');
|
|
66
|
+
if (mergeJsonMcpServer(claudeWorkspaceMcpPath, 'ghl-ai', ghlAiServer)) {
|
|
67
|
+
updatedFiles.push(claudeWorkspaceMcpPath);
|
|
68
|
+
}
|
|
69
|
+
if (gitJenkinsServer && mergeJsonMcpServer(claudeWorkspaceMcpPath, 'git-jenkins', gitJenkinsServer)) {
|
|
70
|
+
updatedFiles.push(claudeWorkspaceMcpPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Cursor: project .cursor/mcp.json (workspace-level) ──
|
|
74
|
+
const cursorProjectMcpPath = join(cwd, '.cursor', 'mcp.json');
|
|
75
|
+
if (mergeJsonMcpServer(cursorProjectMcpPath, 'ghl-ai', ghlAiServer)) {
|
|
76
|
+
updatedFiles.push(cursorProjectMcpPath);
|
|
77
|
+
}
|
|
78
|
+
if (gitJenkinsServer && mergeJsonMcpServer(cursorProjectMcpPath, 'git-jenkins', gitJenkinsServer)) {
|
|
79
|
+
updatedFiles.push(cursorProjectMcpPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Cursor: ~/.cursor/mcp.json (global) ──
|
|
53
83
|
const cursorMcpPath = join(HOME, '.cursor', 'mcp.json');
|
|
54
84
|
if (mergeJsonMcpServer(cursorMcpPath, 'ghl-ai', ghlAiServer)) {
|
|
55
85
|
updatedFiles.push(cursorMcpPath);
|
|
@@ -94,12 +124,18 @@ function mergeJsonMcpServer(filePath, serverName, serverConfig) {
|
|
|
94
124
|
config.mcpServers = {};
|
|
95
125
|
}
|
|
96
126
|
|
|
97
|
-
// Check if already configured with same
|
|
127
|
+
// Check if already configured with same config
|
|
98
128
|
const existing = config.mcpServers[serverName];
|
|
99
129
|
if (existing && JSON.stringify(existing) === JSON.stringify(serverConfig)) {
|
|
100
130
|
return false;
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
// If we're writing an HTTP URL server, remove any stale command-based config
|
|
134
|
+
// to prevent old local MCP servers from shadowing the remote one.
|
|
135
|
+
if (serverConfig.type === 'http' && existing && existing.command) {
|
|
136
|
+
fmt.logStep(`Replacing stale command-based '${serverName}' with HTTP URL`);
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
config.mcpServers[serverName] = serverConfig;
|
|
104
140
|
|
|
105
141
|
mkdirSync(join(filePath, '..'), { recursive: true });
|
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,
|