@ghl-ai/aw 0.1.61 → 0.1.62-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c4/commandSurface.mjs +104 -1
- package/c4/index.mjs +1 -1
- package/c4/templates/scripts/aw-c4-bootstrap.sh +32 -1
- package/commands/c4.mjs +42 -2
- package/package.json +1 -1
package/c4/commandSurface.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
symlinkSync,
|
|
37
37
|
unlinkSync,
|
|
38
38
|
} from 'node:fs';
|
|
39
|
-
import { join, basename, extname } from 'node:path';
|
|
39
|
+
import { join, basename, extname, relative } from 'node:path';
|
|
40
40
|
|
|
41
41
|
// Canonical AW routing-stage commands. Anything in <eccHome>/commands/*.md
|
|
42
42
|
// not on this list is treated as "not a slash-command" (e.g. README.md).
|
|
@@ -230,6 +230,109 @@ export function ensureCommandSurface(opts) {
|
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Link ALL registry commands from ~/.aw/.aw_registry into the harness
|
|
235
|
+
* command directory — not just the 10 stage commands.
|
|
236
|
+
*
|
|
237
|
+
* `ensureCommandSurface` only links ECC stage commands (plan, build, etc.).
|
|
238
|
+
* The full registry (synced by `aw init`) contains 100+ domain-specific
|
|
239
|
+
* commands (e.g. platform-review-security-hardening) that were invisible
|
|
240
|
+
* in cloud harnesses because nothing linked them.
|
|
241
|
+
*
|
|
242
|
+
* This function mirrors the logic in link.mjs::linkWorkspace's command
|
|
243
|
+
* section but is callable from the c4 orchestrator without importing the
|
|
244
|
+
* full link surface.
|
|
245
|
+
*
|
|
246
|
+
* @param {object} opts
|
|
247
|
+
* @param {'claude-web'|'cursor-cloud'|'codex-web'|string} opts.harness
|
|
248
|
+
* @param {string} opts.home
|
|
249
|
+
* @param {string} opts.awRegistryDir e.g. ~/.aw/.aw_registry
|
|
250
|
+
* @returns {{ linked: number, skipped: number, harness: string }}
|
|
251
|
+
*/
|
|
252
|
+
export function ensureRegistryCommandSurface(opts) {
|
|
253
|
+
if (!opts || typeof opts !== 'object') {
|
|
254
|
+
throw new Error('ensureRegistryCommandSurface: opts object is required');
|
|
255
|
+
}
|
|
256
|
+
const { harness, home, awRegistryDir } = opts;
|
|
257
|
+
|
|
258
|
+
if (harness === 'claude-web') {
|
|
259
|
+
return { linked: 0, skipped: 0, harness, installedAction: 'noop' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const targetDir = harnessTargetDir(harness, home);
|
|
263
|
+
if (!targetDir || !existsSync(awRegistryDir)) {
|
|
264
|
+
return { linked: 0, skipped: 0, harness, installedAction: 'unsupported' };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
mkdirSync(targetDir, { recursive: true });
|
|
269
|
+
} catch {
|
|
270
|
+
return { linked: 0, skipped: 0, harness, installedAction: 'symlink' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let linked = 0;
|
|
274
|
+
let skipped = 0;
|
|
275
|
+
|
|
276
|
+
const namespaces = safeReaddir(awRegistryDir).filter(
|
|
277
|
+
(d) => !d.startsWith('.') && isDir(join(awRegistryDir, d)),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
for (const ns of namespaces) {
|
|
281
|
+
for (const { dir: commandsDir, segments } of findCommandDirs(join(awRegistryDir, ns))) {
|
|
282
|
+
for (const file of safeReaddir(commandsDir).filter((f) => f.endsWith('.md') && !f.startsWith('.'))) {
|
|
283
|
+
const cmdFileName = [ns, ...segments, file].join('-');
|
|
284
|
+
const sourcePath = join(commandsDir, file);
|
|
285
|
+
const linkPath = join(targetDir, cmdFileName);
|
|
286
|
+
|
|
287
|
+
if (isCorrectSymlink(linkPath, sourcePath)) {
|
|
288
|
+
skipped++;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
if (existsSync(linkPath) || lstatExists(linkPath)) {
|
|
294
|
+
try { unlinkSync(linkPath); } catch { /* stale entry */ }
|
|
295
|
+
}
|
|
296
|
+
symlinkSync(sourcePath, linkPath);
|
|
297
|
+
linked++;
|
|
298
|
+
} catch {
|
|
299
|
+
skipped++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { linked, skipped, harness, installedAction: 'symlink' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function safeReaddir(dir) {
|
|
309
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isDir(p) {
|
|
313
|
+
try { return lstatSync(p).isDirectory(); } catch { return false; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Recursively find `commands/` directories under a namespace dir.
|
|
318
|
+
* Supports nested domain dirs (e.g. platform/review/commands/).
|
|
319
|
+
*/
|
|
320
|
+
function findCommandDirs(nsDir, segments = []) {
|
|
321
|
+
const results = [];
|
|
322
|
+
const commandsDir = join(nsDir, 'commands');
|
|
323
|
+
if (existsSync(commandsDir) && isDir(commandsDir)) {
|
|
324
|
+
results.push({ dir: commandsDir, segments });
|
|
325
|
+
}
|
|
326
|
+
for (const entry of safeReaddir(nsDir)) {
|
|
327
|
+
if (entry === 'commands' || entry.startsWith('.')) continue;
|
|
328
|
+
const sub = join(nsDir, entry);
|
|
329
|
+
if (isDir(sub)) {
|
|
330
|
+
results.push(...findCommandDirs(sub, [...segments, entry]));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
|
|
233
336
|
/**
|
|
234
337
|
* Read-only diagnostic. Walks the harness command directory and reports
|
|
235
338
|
* which AW stage commands are resolvable. Does not fix.
|
package/c4/index.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export {
|
|
|
49
49
|
export { MCP_URL_DEFAULT, registerGhlAiMcp } from './mcpServer.mjs';
|
|
50
50
|
export { probeMcpServer } from './mcpSmokeProbe.mjs';
|
|
51
51
|
export { ensureClaudeMarketplace } from './claudePluginRegistry.mjs';
|
|
52
|
-
export { ensureCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
|
|
52
|
+
export { ensureCommandSurface, ensureRegistryCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
|
|
53
53
|
export { installCursorSlashShim, CURSOR_SLASH_SHIM_RULE } from './cursorRulesShim.mjs';
|
|
54
54
|
export { ensureRepoLocalClaudeSettings } from './repoLocalClaudeSettings.mjs';
|
|
55
55
|
export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
|
|
@@ -112,4 +112,35 @@ echo "[aw-c4-bootstrap] installing ${AW_PACKAGE}"
|
|
|
112
112
|
npm install -g "${AW_PACKAGE}"
|
|
113
113
|
|
|
114
114
|
echo "[aw-c4-bootstrap] running: aw c4 $*"
|
|
115
|
-
|
|
115
|
+
aw c4 "$@"
|
|
116
|
+
aw_c4_exit=$?
|
|
117
|
+
|
|
118
|
+
# Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
|
|
119
|
+
# which can fail to fetch in silent mode without surfacing an error. When
|
|
120
|
+
# that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
|
|
121
|
+
# and only the 10 hardcoded stage commands get linked. Re-run `aw pull`
|
|
122
|
+
# post-c4 if the command count looks low.
|
|
123
|
+
verify_registry_commands() {
|
|
124
|
+
local aw_cmd_dir="$HOME/.aw/.aw_registry"
|
|
125
|
+
if [ ! -d "$aw_cmd_dir" ]; then
|
|
126
|
+
echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
|
|
127
|
+
aw init --no-integrations --silent 2>&1 | tail -3 || true
|
|
128
|
+
aw pull 2>&1 | tail -5 || true
|
|
129
|
+
return
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
local cmd_count
|
|
133
|
+
cmd_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
134
|
+
if [ "${cmd_count:-0}" -lt 20 ]; then
|
|
135
|
+
echo "[aw-c4-bootstrap] only ${cmd_count} registry commands found — running aw pull"
|
|
136
|
+
aw pull 2>&1 | tail -5 || true
|
|
137
|
+
local new_count
|
|
138
|
+
new_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
139
|
+
echo "[aw-c4-bootstrap] registry commands: ${cmd_count} → ${new_count}"
|
|
140
|
+
else
|
|
141
|
+
echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
|
|
142
|
+
fi
|
|
143
|
+
}
|
|
144
|
+
verify_registry_commands || true
|
|
145
|
+
|
|
146
|
+
exit "$aw_c4_exit"
|
package/commands/c4.mjs
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
27
|
-
import { existsSync as fsExistsSync } from 'node:fs';
|
|
27
|
+
import { existsSync as fsExistsSync, readdirSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
import * as c4Default from '../c4/index.mjs';
|
|
30
30
|
import { isMcpEnabled } from '../mcp.mjs';
|
|
@@ -97,6 +97,10 @@ function safe(label, fn, writer) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function safeReaddirSync(dir) {
|
|
101
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
async function safeAsync(label, fn, writer) {
|
|
101
105
|
try {
|
|
102
106
|
return { ok: true, value: await fn() };
|
|
@@ -392,6 +396,24 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
392
396
|
return exit(1);
|
|
393
397
|
}
|
|
394
398
|
|
|
399
|
+
// Step 9a — verify registry has content. `aw init --silent` can succeed
|
|
400
|
+
// (exit 0) but fail to fetch from remote (fetchAndMerge swallows the error
|
|
401
|
+
// in silent mode). When the registry dir exists but contains no namespace
|
|
402
|
+
// directories, the fetch likely failed. Retry with `aw pull` which runs
|
|
403
|
+
// a targeted git pull on the existing clone.
|
|
404
|
+
{
|
|
405
|
+
const registryEntries = safeReaddirSync(awRegistry).filter(
|
|
406
|
+
(e) => !e.startsWith('.'),
|
|
407
|
+
);
|
|
408
|
+
if (registryEntries.length === 0) {
|
|
409
|
+
writer.stderr('[aw-c4] registry empty after init — retrying with aw pull\n');
|
|
410
|
+
const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
|
|
411
|
+
if (pullRes && pullRes.status !== 0) {
|
|
412
|
+
writer.stderr('[aw-c4] aw pull also failed; commands may be incomplete\n');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
395
417
|
// Step 10 — per-harness branch.
|
|
396
418
|
let branch;
|
|
397
419
|
try {
|
|
@@ -409,9 +431,27 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
409
431
|
writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
|
|
410
432
|
}
|
|
411
433
|
|
|
412
|
-
// Step 12 — slash command surface.
|
|
434
|
+
// Step 12 — slash command surface (10 stage commands from ECC).
|
|
413
435
|
safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
|
|
414
436
|
|
|
437
|
+
// Step 12a — full registry command surface.
|
|
438
|
+
// `ensureCommandSurface` only links the 10 AW routing-stage commands from
|
|
439
|
+
// ~/.aw-ecc/commands/. The full registry (~100+ domain commands like
|
|
440
|
+
// platform-review-security-hardening) lives in ~/.aw/.aw_registry/ and is
|
|
441
|
+
// populated by `aw init` (step 7). Link them all into the harness command
|
|
442
|
+
// dir so `/aw:*` resolution works for every registered command.
|
|
443
|
+
const registryCmdResult = safe(
|
|
444
|
+
'ensureRegistryCommandSurface',
|
|
445
|
+
() => c4.ensureRegistryCommandSurface({ harness, home, awRegistryDir: awRegistry }),
|
|
446
|
+
writer,
|
|
447
|
+
);
|
|
448
|
+
if (registryCmdResult.ok) {
|
|
449
|
+
const { linked, skipped } = registryCmdResult.value;
|
|
450
|
+
if (linked > 0) {
|
|
451
|
+
writer.stdout(`[aw-c4] registry commands: ${linked} linked, ${skipped} skipped\n`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
415
455
|
// Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
|
|
416
456
|
// The model-side workaround for Cursor Cloud's chat UI not pre-expanding
|
|
417
457
|
// `/aw:<NAME>` slash commands. The function self-skips on other
|