@ghl-ai/aw 0.1.53 → 0.1.54-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/eccRegistryBridge.mjs +59 -7
- package/commands/c4.mjs +29 -8
- package/commands/integrations.mjs +0 -14
- package/git.mjs +15 -3
- package/package.json +1 -1
package/c4/eccRegistryBridge.mjs
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* On Codex Web, neither Claude's plugin marketplace nor Cursor's direct
|
|
14
14
|
* `~/.cursor/skills/...` resolution applies. The only way Codex can see
|
|
15
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
|
|
17
|
-
*
|
|
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.
|
|
18
19
|
*
|
|
19
20
|
* Also installs a fallback symlink at `<homeOf(awHome)>/.aw_registry` →
|
|
20
21
|
* `<awHome>/.aw_registry` because some legacy hooks read from that path.
|
|
@@ -46,6 +47,26 @@ function isBrokenSymlink(path) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
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
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Install the fallback symlink `~/.aw_registry → ~/.aw/.aw_registry` if not
|
|
51
72
|
* present and not blocked by an existing real directory. Best-effort.
|
|
@@ -82,7 +103,8 @@ function installRegistryFallbackLink(awHome) {
|
|
|
82
103
|
*
|
|
83
104
|
* Result shape:
|
|
84
105
|
* - linked — skill names freshly bridged this call
|
|
85
|
-
* - skipped — skill names whose registry target already existed
|
|
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
|
|
86
108
|
* - broken — skill directories that exist in eccHome/skills/ but have no SKILL.md
|
|
87
109
|
* (broken ECC layout — distinct from already-bridged so the orchestrator
|
|
88
110
|
* can surface a real diagnostic rather than treating it as success)
|
|
@@ -94,14 +116,18 @@ function installRegistryFallbackLink(awHome) {
|
|
|
94
116
|
* @param {object} opts
|
|
95
117
|
* @param {string} opts.awHome Path of the AW home (e.g. ~/.aw).
|
|
96
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.
|
|
97
122
|
* @returns {{
|
|
98
123
|
* linked: string[],
|
|
99
124
|
* skipped: string[],
|
|
125
|
+
* replaced: string[],
|
|
100
126
|
* broken: string[],
|
|
101
127
|
* reason?: 'already-bridged' | 'ecc-missing'
|
|
102
128
|
* }}
|
|
103
129
|
*/
|
|
104
|
-
export function applyEccRegistryBridge({ awHome, eccHome } = {}) {
|
|
130
|
+
export function applyEccRegistryBridge({ awHome, eccHome, preferEccSkills = false } = {}) {
|
|
105
131
|
if (!awHome || !eccHome) {
|
|
106
132
|
throw new Error('applyEccRegistryBridge: awHome and eccHome are required');
|
|
107
133
|
}
|
|
@@ -113,6 +139,7 @@ export function applyEccRegistryBridge({ awHome, eccHome } = {}) {
|
|
|
113
139
|
|
|
114
140
|
const linked = [];
|
|
115
141
|
const skipped = [];
|
|
142
|
+
const replaced = [];
|
|
116
143
|
const broken = [];
|
|
117
144
|
|
|
118
145
|
let entries;
|
|
@@ -138,7 +165,9 @@ export function applyEccRegistryBridge({ awHome, eccHome } = {}) {
|
|
|
138
165
|
const targetDir = join(awHome, '.aw_registry/platform/core/skills', name);
|
|
139
166
|
const target = join(targetDir, 'SKILL.md');
|
|
140
167
|
|
|
141
|
-
// If target already exists and is
|
|
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.
|
|
142
171
|
let targetExists = false;
|
|
143
172
|
try {
|
|
144
173
|
lstatSync(target);
|
|
@@ -148,7 +177,29 @@ export function applyEccRegistryBridge({ awHome, eccHome } = {}) {
|
|
|
148
177
|
}
|
|
149
178
|
|
|
150
179
|
if (targetExists && !isBrokenSymlink(target)) {
|
|
151
|
-
|
|
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
|
+
}
|
|
152
203
|
continue;
|
|
153
204
|
}
|
|
154
205
|
|
|
@@ -169,12 +220,13 @@ export function applyEccRegistryBridge({ awHome, eccHome } = {}) {
|
|
|
169
220
|
|
|
170
221
|
installRegistryFallbackLink(awHome);
|
|
171
222
|
|
|
172
|
-
const result = { linked, skipped, broken };
|
|
223
|
+
const result = { linked, skipped, replaced, broken };
|
|
173
224
|
// Only flag 'already-bridged' when every well-formed skill was already linked
|
|
174
225
|
// AND no broken skills exist — i.e. this run was a true no-op.
|
|
175
226
|
if (
|
|
176
227
|
skillDirCount > 0 &&
|
|
177
228
|
linked.length === 0 &&
|
|
229
|
+
replaced.length === 0 &&
|
|
178
230
|
broken.length === 0 &&
|
|
179
231
|
skipped.length === skillDirCount
|
|
180
232
|
) {
|
package/commands/c4.mjs
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* --dry-run stops before step 6 (no npm install, no aw init, no harness writes).
|
|
11
11
|
* --diagnose runs only steps 1, 16, 17, 18 — and ALWAYS exits 0
|
|
12
12
|
* (G7: report mode, not gate mode).
|
|
13
|
+
* --skip-self-install or AW_C4_SKIP_SELF_INSTALL=1 skips only the
|
|
14
|
+
* `npm install -g @ghl-ai/aw@latest` self-update step and uses
|
|
15
|
+
* the aw binary already provided by the runtime image.
|
|
13
16
|
*
|
|
14
17
|
* Every barrel symbol used here is dependency-injected via the second
|
|
15
18
|
* argument so the test in tests/c4/c4.command.test.mjs can exercise the
|
|
@@ -45,7 +48,14 @@ function normalizeArgs(input) {
|
|
|
45
48
|
const out = { _positional: [] };
|
|
46
49
|
for (let i = 0; i < input.length; i++) {
|
|
47
50
|
const a = input[i];
|
|
48
|
-
if (
|
|
51
|
+
if (
|
|
52
|
+
a === '--dry-run' ||
|
|
53
|
+
a === '-v' ||
|
|
54
|
+
a === '--verbose' ||
|
|
55
|
+
a === '--diagnose' ||
|
|
56
|
+
a === '--skip-pull' ||
|
|
57
|
+
a === '--skip-self-install'
|
|
58
|
+
) {
|
|
49
59
|
out[a] = true;
|
|
50
60
|
} else if (a === '--harness') {
|
|
51
61
|
out['--harness'] = input[i + 1];
|
|
@@ -109,6 +119,12 @@ function setupGitAuth({ token, c4, home, cwd }) {
|
|
|
109
119
|
c4.ensureOriginRemote({ cwd });
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
function resolveCodexRouterSkillPath({ awHome, eccHome, existsSync = fsExistsSync }) {
|
|
123
|
+
const eccRouterSkillPath = join(eccHome, 'skills/using-aw-skills/SKILL.md');
|
|
124
|
+
if (existsSync(eccRouterSkillPath)) return eccRouterSkillPath;
|
|
125
|
+
return join(awHome, '.aw_registry/platform/core/skills/using-aw-skills/SKILL.md');
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
113
129
|
* Per-harness branch (step 10).
|
|
114
130
|
*
|
|
@@ -117,7 +133,7 @@ function setupGitAuth({ token, c4, home, cwd }) {
|
|
|
117
133
|
* marketplace / slim card) bubble up as thrown Errors.
|
|
118
134
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
119
135
|
|
|
120
|
-
function runHarnessBranch({ harness, c4, home, eccHome, cwd, writer }) {
|
|
136
|
+
function runHarnessBranch({ harness, c4, home, eccHome, cwd, writer, existsSync = fsExistsSync }) {
|
|
121
137
|
if (harness === 'claude-web') {
|
|
122
138
|
// Marketplace registration is non-fatal — the slim card is the routing
|
|
123
139
|
// mechanism (state.json::resolved_decisions exit-code item 3 lists slim
|
|
@@ -138,7 +154,7 @@ function runHarnessBranch({ harness, c4, home, eccHome, cwd, writer }) {
|
|
|
138
154
|
const awHome = join(home, '.aw');
|
|
139
155
|
c4.applyEccRegistryBridge({ awHome, eccHome });
|
|
140
156
|
c4.ensureCodexHooksFlag(home);
|
|
141
|
-
const routerSkillPath =
|
|
157
|
+
const routerSkillPath = resolveCodexRouterSkillPath({ awHome, eccHome, existsSync });
|
|
142
158
|
// Pass hookPath explicitly: defaultHookPath() reads os.homedir() at module
|
|
143
159
|
// load time, which ignores the orchestrator's `home` (env.HOME). Without
|
|
144
160
|
// this, installs land in the real ~/.codex even when HOME is overridden
|
|
@@ -350,10 +366,15 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
350
366
|
return exit(0);
|
|
351
367
|
}
|
|
352
368
|
|
|
353
|
-
// Step 6 — npm install -g @ghl-ai/aw@latest.
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
writer.
|
|
369
|
+
// Step 6 — npm install -g @ghl-ai/aw@latest unless the image owns AW.
|
|
370
|
+
const skipSelfInstall = args['--skip-self-install'] === true || env.AW_C4_SKIP_SELF_INSTALL === '1';
|
|
371
|
+
if (skipSelfInstall) {
|
|
372
|
+
writer.stdout('[aw-c4] self-install skipped; using runtime aw\n');
|
|
373
|
+
} else {
|
|
374
|
+
const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@latest'], { stdio: 'pipe' });
|
|
375
|
+
if (npmRes && npmRes.status !== 0) {
|
|
376
|
+
writer.stderr('[aw-c4] npm install -g @ghl-ai/aw@latest failed (non-fatal); using existing aw if present\n');
|
|
377
|
+
}
|
|
357
378
|
}
|
|
358
379
|
|
|
359
380
|
// Step 7 — aw init --silent.
|
|
@@ -374,7 +395,7 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
374
395
|
// Step 10 — per-harness branch.
|
|
375
396
|
let branch;
|
|
376
397
|
try {
|
|
377
|
-
branch = runHarnessBranch({ harness, c4, home, eccHome, cwd, writer });
|
|
398
|
+
branch = runHarnessBranch({ harness, c4, home, eccHome, cwd, writer, existsSync: fs.existsSync });
|
|
378
399
|
} catch (err) {
|
|
379
400
|
writer.stderr(`[aw-c4] FATAL: harness branch (${harness}) failed: ${err?.message ?? err}\n`);
|
|
380
401
|
return exit(1);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// commands/integrations.mjs — CLI command: aw integrations add/remove/list/bundle
|
|
2
2
|
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
4
|
import * as fmt from '../fmt.mjs';
|
|
6
5
|
import { chalk } from '../fmt.mjs';
|
|
7
|
-
import { listIntegrations as listLocalIntegrations } from '../integrations/index.mjs';
|
|
8
6
|
import {
|
|
9
7
|
INTEGRATIONS,
|
|
10
8
|
BUNDLES,
|
|
@@ -42,7 +40,6 @@ async function cmdList() {
|
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
const installed = getInstalledList();
|
|
45
|
-
const localIntegrations = listLocalIntegrations(homedir(), { env: process.env });
|
|
46
43
|
|
|
47
44
|
// Group by type
|
|
48
45
|
const plugins = Object.entries(INTEGRATIONS).filter(
|
|
@@ -71,16 +68,6 @@ async function cmdList() {
|
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
// Local Integrations
|
|
75
|
-
if (localIntegrations.length > 0) {
|
|
76
|
-
fmt.logMessage(`\n${chalk.bold.underline('Local Integrations')}`);
|
|
77
|
-
for (const integration of localIntegrations) {
|
|
78
|
-
fmt.logMessage(
|
|
79
|
-
` ⚙️ ${integration.name.padEnd(25)} — ${integration.summary}`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
// Available Plugins
|
|
85
72
|
fmt.logMessage(`\n${chalk.bold.underline('Available Plugins')}`);
|
|
86
73
|
for (const [key, integration] of plugins) {
|
|
@@ -140,7 +127,6 @@ async function cmdList() {
|
|
|
140
127
|
fmt.logMessage(` aw integrations add <key> Install a specific tool`);
|
|
141
128
|
fmt.logMessage(` aw integrations remove <key> Remove a tool`);
|
|
142
129
|
fmt.logMessage(` aw integrations bundle <name> Install a preset bundle`);
|
|
143
|
-
fmt.logMessage(` aw integration add <name> Configure a local integration`);
|
|
144
130
|
}
|
|
145
131
|
|
|
146
132
|
// ────────────────────────────────────────────────────────────────────────────────
|
package/git.mjs
CHANGED
|
@@ -388,8 +388,17 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
|
|
|
388
388
|
// We avoid `merge --ff-only` and `merge --no-edit` entirely: both trigger a
|
|
389
389
|
// git 2.46+ bug on blob:none + no-cone sparse-checkout repos that silently
|
|
390
390
|
// drops bare-name patterns (e.g. "content", "CODEOWNERS") when HEAD advances.
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
//
|
|
392
|
+
// --autostash: AW writes into the registry working tree from external
|
|
393
|
+
// sources (ensureAwRuntimeHook copies ~/.aw-ecc/.../session-start.sh into a
|
|
394
|
+
// tracked path; transformCursorAwRefs rewrites /aw: → /aw- through Cursor
|
|
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
|
+
try {
|
|
401
|
+
await exec(`git -C "${awHome}" rebase --autostash origin/${REGISTRY_BASE_BRANCH}`);
|
|
393
402
|
updated = true;
|
|
394
403
|
// Push branch rebase rewrites commit SHAs — force-push so origin/upload/...
|
|
395
404
|
// stays in sync with the rebased local branch. Without this, VS Code and
|
|
@@ -683,7 +692,10 @@ export function checkoutMain(awHome) {
|
|
|
683
692
|
*/
|
|
684
693
|
export async function rebaseOntoOriginMain(awHome) {
|
|
685
694
|
try {
|
|
686
|
-
|
|
695
|
+
// --autostash: see fetchAndMerge() above. Project-local worktrees see the
|
|
696
|
+
// same external-write patterns (runtime hook copy + Cursor symlink writes)
|
|
697
|
+
// and need the same dirty-tree tolerance here.
|
|
698
|
+
await exec(`git -C "${awHome}" rebase --autostash origin/${REGISTRY_BASE_BRANCH}`);
|
|
687
699
|
} catch (e) {
|
|
688
700
|
// Surface stderr so callers can show a meaningful message
|
|
689
701
|
const stderr = e.stderr?.toString().trim() || e.stdout?.toString().trim() || e.message;
|