@ghl-ai/aw 0.1.69 → 0.1.70-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c4/claudePluginRegistry.mjs +3 -3
- package/c4/commandSurface.mjs +122 -224
- package/c4/cursorRulesShim.mjs +19 -41
- package/c4/diagnostics.mjs +23 -42
- package/c4/index.mjs +1 -2
- package/c4/preflight.mjs +12 -18
- package/c4/templates/scripts/aw-c4-bootstrap.sh +14 -14
- package/codex.mjs +13 -4
- package/commands/c4.mjs +31 -34
- package/commands/doctor.mjs +24 -61
- package/commands/init.mjs +12 -3
- package/commands/pull.mjs +2 -1
- package/constants.mjs +3 -0
- package/ecc.mjs +41 -74
- package/git.mjs +12 -9
- package/hooks/codex-home.mjs +3 -3
- package/link.mjs +13 -11
- package/package.json +1 -1
- package/startup.mjs +7 -22
- package/c4/eccRegistryBridge.mjs +0 -236
package/ecc.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
|
|
|
3
3
|
const execAsync = promisify(execCb);
|
|
4
4
|
import {
|
|
5
5
|
existsSync, readFileSync, readdirSync,
|
|
6
|
-
mkdirSync, rmSync, writeFileSync,
|
|
6
|
+
mkdirSync, rmSync, writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
@@ -16,7 +16,6 @@ export const AW_ECC_TAG = "v1.4.66";
|
|
|
16
16
|
const REQUIRED_ECC_FILES = [
|
|
17
17
|
"package.json",
|
|
18
18
|
"scripts/install-apply.js",
|
|
19
|
-
"scripts/sync-ecc-to-codex.sh",
|
|
20
19
|
];
|
|
21
20
|
|
|
22
21
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
@@ -24,19 +23,31 @@ const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
|
24
23
|
|
|
25
24
|
function eccDir() { return join(homedir(), ".aw-ecc"); }
|
|
26
25
|
|
|
27
|
-
//
|
|
28
|
-
// and install-state
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// some optional modules depend on commands-core and cause install-apply to fail.
|
|
26
|
+
// File-copy targets use explicit non-skill, non-command module sets so hooks,
|
|
27
|
+
// rules, shared references, and install-state can remain available while AW
|
|
28
|
+
// skills come from platform-docs/.aw_registry/aw. Slash access is backed by
|
|
29
|
+
// those skills directly, so aw-ecc command artifacts are intentionally skipped.
|
|
32
30
|
const FILE_COPY_TARGETS = ["claude", "cursor", "codex"];
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
]
|
|
31
|
+
const FILE_COPY_MODULES_BY_TARGET = {
|
|
32
|
+
claude: [
|
|
33
|
+
"rules-core",
|
|
34
|
+
"agents-core",
|
|
35
|
+
"hooks-runtime",
|
|
36
|
+
"platform-configs",
|
|
37
|
+
],
|
|
38
|
+
cursor: [
|
|
39
|
+
"rules-core",
|
|
40
|
+
"agents-core",
|
|
41
|
+
"hooks-runtime",
|
|
42
|
+
"platform-configs",
|
|
43
|
+
],
|
|
44
|
+
codex: [
|
|
45
|
+
"rules-core",
|
|
46
|
+
"agents-core",
|
|
47
|
+
"hooks-runtime",
|
|
48
|
+
"platform-configs",
|
|
49
|
+
],
|
|
50
|
+
};
|
|
40
51
|
|
|
41
52
|
const TARGET_STATE = {
|
|
42
53
|
claude: { state: ".claude/ecc/install-state.json" },
|
|
@@ -241,12 +252,11 @@ function cloneOrUpdate(tag, dest) {
|
|
|
241
252
|
|
|
242
253
|
/**
|
|
243
254
|
* Transform canonical /aw: references to Cursor-compatible /aw- in installed
|
|
244
|
-
*
|
|
245
|
-
*
|
|
255
|
+
* rule files. Cursor's model-side rule still sees hyphenated route mentions in
|
|
256
|
+
* some generated home instructions.
|
|
246
257
|
*/
|
|
247
258
|
function transformCursorAwRefs(home) {
|
|
248
259
|
const dirs = [
|
|
249
|
-
join(home, ".cursor", "skills"),
|
|
250
260
|
join(home, ".cursor", "rules"),
|
|
251
261
|
];
|
|
252
262
|
for (const dir of dirs) {
|
|
@@ -280,57 +290,16 @@ function uninstallClaudePlugin() {
|
|
|
280
290
|
try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
|
|
281
291
|
}
|
|
282
292
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const commandsDir = join(home, ".cursor", "commands");
|
|
290
|
-
const awDir = join(commandsDir, "aw");
|
|
291
|
-
if (!existsSync(commandsDir)) return;
|
|
292
|
-
|
|
293
|
-
// Move any flat .md files (not already in a subdirectory) into aw/
|
|
294
|
-
const moved = [];
|
|
295
|
-
for (const file of readdirSync(commandsDir)) {
|
|
296
|
-
if (!file.endsWith(".md") || file.startsWith(".")) continue;
|
|
297
|
-
const src = join(commandsDir, file);
|
|
298
|
-
mkdirSync(awDir, { recursive: true });
|
|
299
|
-
const dest = join(awDir, file);
|
|
293
|
+
function pruneEccAuthoredSkillsAndCommands(repoDir) {
|
|
294
|
+
// The local aw-ecc clone is now only a compatibility runtime for hooks,
|
|
295
|
+
// rules, agents, and configs. Active AW skills live in platform-docs under
|
|
296
|
+
// ~/.aw/.aw_registry/aw/skills, and slash adapters point at those SKILL.md
|
|
297
|
+
// files directly.
|
|
298
|
+
for (const relPath of ["commands", "skills"]) {
|
|
300
299
|
try {
|
|
301
|
-
|
|
302
|
-
moved.push({ from: src, to: dest });
|
|
300
|
+
rmSync(join(repoDir, relPath), { recursive: true, force: true });
|
|
303
301
|
} catch { /* best effort */ }
|
|
304
302
|
}
|
|
305
|
-
|
|
306
|
-
if (moved.length === 0) return;
|
|
307
|
-
|
|
308
|
-
// Update install-state so nuke removes files from the new aw/ location
|
|
309
|
-
const statePath = join(home, ".cursor", "ecc-install-state.json");
|
|
310
|
-
if (!existsSync(statePath)) return;
|
|
311
|
-
try {
|
|
312
|
-
const state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
313
|
-
const pathMap = new Map(moved.map(({ from, to }) => [from, to]));
|
|
314
|
-
state.operations = (state.operations || []).map((op) => {
|
|
315
|
-
const newPath = op.destinationPath && pathMap.get(op.destinationPath);
|
|
316
|
-
return newPath ? { ...op, destinationPath: newPath } : op;
|
|
317
|
-
});
|
|
318
|
-
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
319
|
-
} catch { /* best effort */ }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Run scripts/sync-ecc-to-codex.sh from the cloned ecc repo.
|
|
324
|
-
* Generates ~/.codex/prompts/*.md (Codex equivalent of slash commands),
|
|
325
|
-
* using aw-*.md for AW-namespaced commands and ecc-*.md for the rest,
|
|
326
|
-
* and merges ~/.codex/AGENTS.md. Best-effort — failure doesn't block init.
|
|
327
|
-
*/
|
|
328
|
-
function syncEccToCodex(repoDir) {
|
|
329
|
-
const syncScript = join(repoDir, "scripts", "sync-ecc-to-codex.sh");
|
|
330
|
-
if (!existsSync(syncScript)) return;
|
|
331
|
-
try {
|
|
332
|
-
run(`bash "${syncScript}"`, { cwd: homedir() });
|
|
333
|
-
} catch { /* best effort — codex sync failure is non-blocking */ }
|
|
334
303
|
}
|
|
335
304
|
|
|
336
305
|
export async function installAwEcc(
|
|
@@ -347,6 +316,7 @@ export async function installAwEcc(
|
|
|
347
316
|
try {
|
|
348
317
|
if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
|
|
349
318
|
await cloneOrUpdateAsync(AW_ECC_TAG, repoDir);
|
|
319
|
+
pruneEccAuthoredSkillsAndCommands(repoDir);
|
|
350
320
|
if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
|
|
351
321
|
|
|
352
322
|
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
@@ -381,20 +351,17 @@ export async function installAwEcc(
|
|
|
381
351
|
|
|
382
352
|
// Always use HOME as cwd so files land in ~/.<target>/ globally.
|
|
383
353
|
const runCwd = homedir();
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
354
|
+
const installModules = FILE_COPY_MODULES_BY_TARGET[target] || [];
|
|
355
|
+
const installArgs = installModules.length > 0
|
|
356
|
+
? `--target ${target} --modules ${installModules.join(",")}`
|
|
357
|
+
: `--target ${target}`;
|
|
387
358
|
run(
|
|
388
359
|
`node ${join(repoDir, "scripts/install-apply.js")} ${installArgs}`,
|
|
389
360
|
{ cwd: runCwd },
|
|
390
361
|
);
|
|
391
362
|
if (target === "cursor") {
|
|
392
|
-
namespaceCursorCommands(runCwd);
|
|
393
363
|
transformCursorAwRefs(home);
|
|
394
364
|
}
|
|
395
|
-
if (target === "codex") {
|
|
396
|
-
syncEccToCodex(repoDir);
|
|
397
|
-
}
|
|
398
365
|
restoreProtectedConfigs(snapshot);
|
|
399
366
|
} catch { /* target not supported — skip */ }
|
|
400
367
|
}));
|
|
@@ -446,8 +413,8 @@ export function uninstallAwEcc({ silent = false } = {}) {
|
|
|
446
413
|
} catch { /* corrupted state — skip */ }
|
|
447
414
|
}
|
|
448
415
|
|
|
449
|
-
// Codex: remove generated prompt files from sync
|
|
450
|
-
// (not tracked in install-state — cleaned via manifests, with prefix fallback)
|
|
416
|
+
// Codex: remove legacy generated prompt files from older ECC sync flows
|
|
417
|
+
// (not tracked in install-state — cleaned via manifests, with prefix fallback).
|
|
451
418
|
const codexPromptsDir = join(HOME, ".codex", "prompts");
|
|
452
419
|
if (existsSync(codexPromptsDir)) {
|
|
453
420
|
try {
|
package/git.mjs
CHANGED
|
@@ -5,7 +5,14 @@ import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync, symlinkSync,
|
|
|
5
5
|
import { join, basename, dirname } from 'node:path';
|
|
6
6
|
import { homedir, tmpdir } from 'node:os';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
REGISTRY_BASE_BRANCH,
|
|
10
|
+
REGISTRY_DIR,
|
|
11
|
+
AW_REGISTRY_NAMESPACE_DIR,
|
|
12
|
+
DOCS_SOURCE_DIR,
|
|
13
|
+
AW_DOCS_DIR,
|
|
14
|
+
RULES_SOURCE_DIR,
|
|
15
|
+
} from './constants.mjs';
|
|
9
16
|
|
|
10
17
|
const exec = promisify(execCb);
|
|
11
18
|
|
|
@@ -155,6 +162,7 @@ export function includeToSparsePaths(paths) {
|
|
|
155
162
|
}
|
|
156
163
|
result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
|
|
157
164
|
if (paths.includes('platform')) {
|
|
165
|
+
result.add(AW_REGISTRY_NAMESPACE_DIR);
|
|
158
166
|
result.add(DOCS_SOURCE_DIR);
|
|
159
167
|
result.add(AW_DOCS_DIR);
|
|
160
168
|
result.add(RULES_SOURCE_DIR);
|
|
@@ -389,14 +397,9 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
389
397
|
// git 2.46+ bug on blob:none + no-cone sparse-checkout repos that silently
|
|
390
398
|
// drops bare-name patterns (e.g. "content", "CODEOWNERS") when HEAD advances.
|
|
391
399
|
//
|
|
392
|
-
// --autostash: AW
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// skill directory symlinks that resolve into .aw_registry/). When those
|
|
396
|
-
// versions drift, the working tree is dirty at rebase time and rebase
|
|
397
|
-
// refuses to run, silently aborting the entire pull. Autostash stashes the
|
|
398
|
-
// dirty bytes, rebases cleanly, and reapplies them — so the registry stays
|
|
399
|
-
// in sync even when external sources have leaked into the tracked tree.
|
|
400
|
+
// --autostash: AW can have local registry edits pending when sync runs.
|
|
401
|
+
// Without autostash, git refuses to rebase and the pull silently aborts.
|
|
402
|
+
// Autostash stashes dirty bytes, rebases cleanly, and reapplies them.
|
|
400
403
|
try {
|
|
401
404
|
await exec(`git -C "${awHome}" rebase --autostash origin/${REGISTRY_BASE_BRANCH}`);
|
|
402
405
|
updated = true;
|
package/hooks/codex-home.mjs
CHANGED
|
@@ -26,10 +26,10 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
|
|
|
26
26
|
marker: this.scriptMarker,
|
|
27
27
|
phase: 'SessionStart',
|
|
28
28
|
targetCandidates: [
|
|
29
|
-
'$HOME/.aw_registry/
|
|
30
|
-
'$HOME/.aw/.aw_registry/
|
|
29
|
+
'$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
|
|
30
|
+
'$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
|
|
31
31
|
],
|
|
32
|
-
warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform.',
|
|
32
|
+
warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry/aw. Run aw init or aw pull platform.',
|
|
33
33
|
telemetryHookPath: '$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js',
|
|
34
34
|
harnessEnv: 'codex',
|
|
35
35
|
});
|
package/link.mjs
CHANGED
|
@@ -158,10 +158,12 @@ function cleanSymlinksRecursive(dir) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
* Compute flat IDE
|
|
161
|
+
* Compute flat IDE names. The top-level `aw` namespace is already encoded in
|
|
162
|
+
* its skill and command names, so keep those names stable instead of producing
|
|
163
|
+
* aw-aw-plan or aw-plan.md variants.
|
|
162
164
|
*/
|
|
163
|
-
function
|
|
164
|
-
return
|
|
165
|
+
function flatRegistryName(ns, ...parts) {
|
|
166
|
+
return (ns === 'aw' ? parts : [ns, ...parts]).join('-');
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
/**
|
|
@@ -202,7 +204,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
202
204
|
for (const type of FILE_TYPES) {
|
|
203
205
|
for (const { typeDirPath, segments } of findNestedTypeDirs(join(awDir, ns), type)) {
|
|
204
206
|
for (const file of readdirSync(typeDirPath).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
|
|
205
|
-
const flat =
|
|
207
|
+
const flat = flatRegistryName(ns, ...segments, file);
|
|
206
208
|
|
|
207
209
|
for (const ide of IDE_DIRS) {
|
|
208
210
|
const linkDir = join(cwd, ide, type);
|
|
@@ -219,7 +221,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
219
221
|
// Skills: per-skill directory symlinks (recursive for nested domain dirs)
|
|
220
222
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|
|
221
223
|
for (const skill of listDirs(skillsDir)) {
|
|
222
|
-
const flat =
|
|
224
|
+
const flat = flatRegistryName(ns, ...segments, skill);
|
|
223
225
|
|
|
224
226
|
for (const ide of IDE_DIRS) {
|
|
225
227
|
const linkDir = join(cwd, ide, 'skills');
|
|
@@ -233,7 +235,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
233
235
|
|
|
234
236
|
for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
|
|
235
237
|
if (skillSegments.length <= 1) continue;
|
|
236
|
-
const flat =
|
|
238
|
+
const flat = flatRegistryName(ns, ...segments, ...skillSegments);
|
|
237
239
|
|
|
238
240
|
for (const ide of IDE_DIRS) {
|
|
239
241
|
const linkDir = join(cwd, ide, 'skills');
|
|
@@ -254,7 +256,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
254
256
|
if (isColocated) {
|
|
255
257
|
// Colocated evals: agents/evals/<slug>/ — one level of slug dirs
|
|
256
258
|
for (const evalSlug of listDirs(evalsDir)) {
|
|
257
|
-
const flat =
|
|
259
|
+
const flat = flatRegistryName(ns, ...segments, evalSlug);
|
|
258
260
|
for (const ide of IDE_DIRS) {
|
|
259
261
|
const linkDir = join(cwd, ide, 'evals');
|
|
260
262
|
mkdirSync(linkDir, { recursive: true });
|
|
@@ -269,7 +271,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
269
271
|
for (const subType of listDirs(evalsDir)) {
|
|
270
272
|
const subDir = join(evalsDir, subType);
|
|
271
273
|
for (const evalName of listDirs(subDir)) {
|
|
272
|
-
const flat =
|
|
274
|
+
const flat = flatRegistryName(ns, ...segments, subType, evalName);
|
|
273
275
|
for (const ide of IDE_DIRS) {
|
|
274
276
|
const linkDir = join(cwd, ide, 'evals');
|
|
275
277
|
mkdirSync(linkDir, { recursive: true });
|
|
@@ -310,7 +312,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
310
312
|
for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
|
|
311
313
|
mkdirSync(agentsSkillsDir, { recursive: true });
|
|
312
314
|
for (const skill of listDirs(skillsDir)) {
|
|
313
|
-
const flat =
|
|
315
|
+
const flat = flatRegistryName(ns, ...segments, skill);
|
|
314
316
|
const linkPath = join(agentsSkillsDir, flat);
|
|
315
317
|
const targetPath = join(skillsDir, skill);
|
|
316
318
|
const relTarget = relative(agentsSkillsDir, targetPath);
|
|
@@ -319,7 +321,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
319
321
|
|
|
320
322
|
for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
|
|
321
323
|
if (skillSegments.length <= 1) continue;
|
|
322
|
-
const flat =
|
|
324
|
+
const flat = flatRegistryName(ns, ...segments, ...skillSegments);
|
|
323
325
|
const linkPath = join(agentsSkillsDir, flat);
|
|
324
326
|
const relTarget = relative(agentsSkillsDir, skillDirPath);
|
|
325
327
|
if (linkNestedSkillRoot(relTarget, linkPath, { silent })) created++;
|
|
@@ -346,7 +348,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
|
|
|
346
348
|
for (const ns of namespaces) {
|
|
347
349
|
for (const { typeDirPath: commandsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'commands')) {
|
|
348
350
|
for (const file of readdirSync(commandsDir).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
|
|
349
|
-
const cmdFileName =
|
|
351
|
+
const cmdFileName = flatRegistryName(ns, ...segments, file);
|
|
350
352
|
|
|
351
353
|
for (const ide of IDE_DIRS) {
|
|
352
354
|
const linkDir = join(cwd, ide, 'commands', 'aw');
|
package/package.json
CHANGED
package/startup.mjs
CHANGED
|
@@ -83,8 +83,10 @@ function resolveRegistryRoot(homeDir = homedir()) {
|
|
|
83
83
|
].find(existsSync) || null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function
|
|
87
|
-
|
|
86
|
+
function awRuntimeHookDestinationPath(homeDir = homedir()) {
|
|
87
|
+
const registryRoot = resolveRegistryRoot(homeDir);
|
|
88
|
+
if (!registryRoot) return null;
|
|
89
|
+
return join(registryRoot, 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh');
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
function readJson(filePath, fallback = {}) {
|
|
@@ -393,27 +395,10 @@ function hasCodexSessionStartScript(homeDir = homedir()) {
|
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
export function ensureAwRuntimeHook(homeDir = homedir()) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
if (!existsSync(sourcePath) || !registryRoot) return [];
|
|
399
|
-
|
|
400
|
-
const destinationPath = join(
|
|
401
|
-
registryRoot,
|
|
402
|
-
'platform',
|
|
403
|
-
'core',
|
|
404
|
-
'skills',
|
|
405
|
-
'using-aw-skills',
|
|
406
|
-
'hooks',
|
|
407
|
-
'session-start.sh',
|
|
408
|
-
);
|
|
409
|
-
const sourceContent = readFileSync(sourcePath, 'utf8');
|
|
410
|
-
const existingContent = existsSync(destinationPath) ? readFileSync(destinationPath, 'utf8') : null;
|
|
411
|
-
if (existingContent === sourceContent) return [];
|
|
412
|
-
|
|
413
|
-
mkdirSync(dirname(destinationPath), { recursive: true });
|
|
414
|
-
writeFileSync(destinationPath, sourceContent);
|
|
398
|
+
const destinationPath = awRuntimeHookDestinationPath(homeDir);
|
|
399
|
+
if (!destinationPath || !existsSync(destinationPath)) return [];
|
|
415
400
|
try { chmodSync(destinationPath, 0o755); } catch { /* best effort */ }
|
|
416
|
-
return [
|
|
401
|
+
return [];
|
|
417
402
|
}
|
|
418
403
|
|
|
419
404
|
function hasCodexHooksEnabled(homeDir = homedir()) {
|
package/c4/eccRegistryBridge.mjs
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* c4/eccRegistryBridge.mjs — Codex-only Bug A workaround.
|
|
3
|
-
*
|
|
4
|
-
* Codex Web's upstream `aw-session-start.sh` enumerator scans a fixed
|
|
5
|
-
* registry path:
|
|
6
|
-
*
|
|
7
|
-
* ~/.aw/.aw_registry/platform/core/skills/<name>/SKILL.md
|
|
8
|
-
*
|
|
9
|
-
* ECC ships skills under a different path:
|
|
10
|
-
*
|
|
11
|
-
* ~/.aw-ecc/skills/<name>/SKILL.md
|
|
12
|
-
*
|
|
13
|
-
* On Codex Web, neither Claude's plugin marketplace nor Cursor's direct
|
|
14
|
-
* `~/.cursor/skills/...` resolution applies. The only way Codex can see
|
|
15
|
-
* ECC skills is if the registry path is populated. We bridge by symlinking
|
|
16
|
-
* each ECC skill's SKILL.md into the registry layout. The default mode keeps
|
|
17
|
-
* the original non-clobbering behavior. `preferEccSkills` only retargets
|
|
18
|
-
* existing symlinks; regular files are treated as user-owned and preserved.
|
|
19
|
-
*
|
|
20
|
-
* Also installs a fallback symlink at `<homeOf(awHome)>/.aw_registry` →
|
|
21
|
-
* `<awHome>/.aw_registry` because some legacy hooks read from that path.
|
|
22
|
-
*
|
|
23
|
-
* **Scope**: Codex Web only. The orchestrator is responsible for *not*
|
|
24
|
-
* invoking this on claude-web or cursor-cloud; the module itself is
|
|
25
|
-
* harness-agnostic and just performs the bridge.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
existsSync,
|
|
30
|
-
lstatSync,
|
|
31
|
-
readlinkSync,
|
|
32
|
-
readdirSync,
|
|
33
|
-
mkdirSync,
|
|
34
|
-
symlinkSync,
|
|
35
|
-
unlinkSync,
|
|
36
|
-
} from 'node:fs';
|
|
37
|
-
import { dirname, join } from 'node:path';
|
|
38
|
-
|
|
39
|
-
/** True iff `path` is a symlink that points to a nonexistent target. */
|
|
40
|
-
function isBrokenSymlink(path) {
|
|
41
|
-
try {
|
|
42
|
-
const st = lstatSync(path);
|
|
43
|
-
if (!st.isSymbolicLink()) return false;
|
|
44
|
-
return !existsSync(path);
|
|
45
|
-
} catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** True iff `path` is a symlink that already points at `target`. */
|
|
51
|
-
function isSymlinkTo(path, target) {
|
|
52
|
-
try {
|
|
53
|
-
const st = lstatSync(path);
|
|
54
|
-
if (!st.isSymbolicLink()) return false;
|
|
55
|
-
return readlinkSync(path) === target;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** True iff `path` is any symlink, regardless of where it points. */
|
|
62
|
-
function isSymlink(path) {
|
|
63
|
-
try {
|
|
64
|
-
return lstatSync(path).isSymbolicLink();
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Install the fallback symlink `~/.aw_registry → ~/.aw/.aw_registry` if not
|
|
72
|
-
* present and not blocked by an existing real directory. Best-effort.
|
|
73
|
-
*/
|
|
74
|
-
function installRegistryFallbackLink(awHome) {
|
|
75
|
-
const home = dirname(awHome);
|
|
76
|
-
const fallback = join(home, '.aw_registry');
|
|
77
|
-
const target = join(awHome, '.aw_registry');
|
|
78
|
-
|
|
79
|
-
if (!existsSync(target)) {
|
|
80
|
-
// Nothing to point at yet — skip silently.
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let existsAtFallback = false;
|
|
85
|
-
try {
|
|
86
|
-
lstatSync(fallback);
|
|
87
|
-
existsAtFallback = true;
|
|
88
|
-
} catch {
|
|
89
|
-
existsAtFallback = false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (existsAtFallback) return; // do not clobber existing dir/symlink
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
symlinkSync(target, fallback);
|
|
96
|
-
} catch {
|
|
97
|
-
// best-effort
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Symlink every ECC skill into the registry layout. Idempotent.
|
|
103
|
-
*
|
|
104
|
-
* Result shape:
|
|
105
|
-
* - linked — skill names freshly bridged this call
|
|
106
|
-
* - skipped — skill names whose registry target already existed and was preserved
|
|
107
|
-
* - replaced — skill names whose registry target was replaced with the ECC symlink
|
|
108
|
-
* - broken — skill directories that exist in eccHome/skills/ but have no SKILL.md
|
|
109
|
-
* (broken ECC layout — distinct from already-bridged so the orchestrator
|
|
110
|
-
* can surface a real diagnostic rather than treating it as success)
|
|
111
|
-
* - reason — only set when the result is fully diagnostic:
|
|
112
|
-
* 'ecc-missing' — eccHome/skills/ does not exist
|
|
113
|
-
* 'already-bridged' — every well-formed skill was already linked
|
|
114
|
-
* and there were no broken skills
|
|
115
|
-
*
|
|
116
|
-
* @param {object} opts
|
|
117
|
-
* @param {string} opts.awHome Path of the AW home (e.g. ~/.aw).
|
|
118
|
-
* @param {string} opts.eccHome Path of the ECC home (e.g. ~/.aw-ecc).
|
|
119
|
-
* @param {boolean} [opts.preferEccSkills=false]
|
|
120
|
-
* Retarget existing registry SKILL.md symlinks to ECC. Regular files are
|
|
121
|
-
* never replaced because they may be user-owned overrides.
|
|
122
|
-
* @returns {{
|
|
123
|
-
* linked: string[],
|
|
124
|
-
* skipped: string[],
|
|
125
|
-
* replaced: string[],
|
|
126
|
-
* broken: string[],
|
|
127
|
-
* reason?: 'already-bridged' | 'ecc-missing'
|
|
128
|
-
* }}
|
|
129
|
-
*/
|
|
130
|
-
export function applyEccRegistryBridge({ awHome, eccHome, preferEccSkills = false } = {}) {
|
|
131
|
-
if (!awHome || !eccHome) {
|
|
132
|
-
throw new Error('applyEccRegistryBridge: awHome and eccHome are required');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const eccSkillsDir = join(eccHome, 'skills');
|
|
136
|
-
if (!existsSync(eccSkillsDir)) {
|
|
137
|
-
return { linked: [], skipped: [], broken: [], reason: 'ecc-missing' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const linked = [];
|
|
141
|
-
const skipped = [];
|
|
142
|
-
const replaced = [];
|
|
143
|
-
const broken = [];
|
|
144
|
-
|
|
145
|
-
let entries;
|
|
146
|
-
try {
|
|
147
|
-
entries = readdirSync(eccSkillsDir, { withFileTypes: true });
|
|
148
|
-
} catch {
|
|
149
|
-
return { linked: [], skipped: [], broken: [], reason: 'ecc-missing' };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let skillDirCount = 0;
|
|
153
|
-
for (const entry of entries) {
|
|
154
|
-
if (!entry.isDirectory()) continue;
|
|
155
|
-
skillDirCount += 1;
|
|
156
|
-
const name = entry.name;
|
|
157
|
-
const source = join(eccSkillsDir, name, 'SKILL.md');
|
|
158
|
-
|
|
159
|
-
// ECC skill directory exists but lacks SKILL.md → broken layout.
|
|
160
|
-
if (!existsSync(source)) {
|
|
161
|
-
broken.push(name);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const targetDir = join(awHome, '.aw_registry/platform/core/skills', name);
|
|
166
|
-
const target = join(targetDir, 'SKILL.md');
|
|
167
|
-
|
|
168
|
-
// If target already exists and is healthy, preserve regular files. The
|
|
169
|
-
// optional preference mode may only retarget generated symlinks; it must
|
|
170
|
-
// not delete user-owned registry files.
|
|
171
|
-
let targetExists = false;
|
|
172
|
-
try {
|
|
173
|
-
lstatSync(target);
|
|
174
|
-
targetExists = true;
|
|
175
|
-
} catch {
|
|
176
|
-
targetExists = false;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (targetExists && !isBrokenSymlink(target)) {
|
|
180
|
-
if (isSymlinkTo(target, source)) {
|
|
181
|
-
skipped.push(name);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!preferEccSkills || !isSymlink(target)) {
|
|
186
|
-
skipped.push(name);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
unlinkSync(target);
|
|
192
|
-
} catch {
|
|
193
|
-
broken.push(name);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
symlinkSync(source, target);
|
|
199
|
-
replaced.push(name);
|
|
200
|
-
} catch {
|
|
201
|
-
broken.push(name);
|
|
202
|
-
}
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Either absent or a broken symlink — replace.
|
|
207
|
-
mkdirSync(targetDir, { recursive: true });
|
|
208
|
-
if (targetExists) {
|
|
209
|
-
try { unlinkSync(target); } catch { /* best-effort */ }
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
symlinkSync(source, target);
|
|
213
|
-
linked.push(name);
|
|
214
|
-
} catch {
|
|
215
|
-
// Filesystem may forbid symlinks (rare on Codex Web). Treat as broken
|
|
216
|
-
// so the orchestrator can surface the real diagnostic.
|
|
217
|
-
broken.push(name);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
installRegistryFallbackLink(awHome);
|
|
222
|
-
|
|
223
|
-
const result = { linked, skipped, replaced, broken };
|
|
224
|
-
// Only flag 'already-bridged' when every well-formed skill was already linked
|
|
225
|
-
// AND no broken skills exist — i.e. this run was a true no-op.
|
|
226
|
-
if (
|
|
227
|
-
skillDirCount > 0 &&
|
|
228
|
-
linked.length === 0 &&
|
|
229
|
-
replaced.length === 0 &&
|
|
230
|
-
broken.length === 0 &&
|
|
231
|
-
skipped.length === skillDirCount
|
|
232
|
-
) {
|
|
233
|
-
result.reason = 'already-bridged';
|
|
234
|
-
}
|
|
235
|
-
return result;
|
|
236
|
-
}
|