@ghl-ai/aw 0.1.42-beta.3 → 0.1.42-beta.30
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/commands/init.mjs +40 -9
- package/commands/nuke.mjs +49 -60
- package/ecc.mjs +79 -23
- package/hook-cleanup.mjs +301 -0
- package/hooks/codex-home.mjs +4 -3
- package/hooks/shared-phase-scripts.mjs +47 -0
- package/hooks.mjs +43 -3
- package/package.json +2 -1
- package/render-rules.mjs +1 -0
- package/startup.mjs +16 -4
package/commands/init.mjs
CHANGED
|
@@ -28,8 +28,10 @@ import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.m
|
|
|
28
28
|
import { installLocalCommitHook } from '../hooks.mjs';
|
|
29
29
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
30
30
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
31
|
-
import {
|
|
31
|
+
import { loadConfig as ensureTelemetryConfig } from '../telemetry.mjs';
|
|
32
|
+
import { installAwEcc, AW_ECC_TAG } from '../ecc.mjs';
|
|
32
33
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
34
|
+
import { readHookManifest, pruneStaleHooks, writeHookManifest } from '../hook-cleanup.mjs';
|
|
33
35
|
import {
|
|
34
36
|
initPersistentClone,
|
|
35
37
|
isValidClone,
|
|
@@ -323,6 +325,7 @@ export async function initCommand(args) {
|
|
|
323
325
|
syncRulesTargets(cwd);
|
|
324
326
|
}
|
|
325
327
|
removeLegacyRegistryRules();
|
|
328
|
+
if (!silent) fmt.logStep('Rules synced');
|
|
326
329
|
|
|
327
330
|
// Ensure project worktree sparse checkout matches the global clone.
|
|
328
331
|
// Covers the case where a namespace was added from HOME (or another project)
|
|
@@ -332,13 +335,19 @@ export async function initCommand(args) {
|
|
|
332
335
|
try { syncWorktreeSparseCheckout(AW_HOME, localAw); } catch { /* best effort */ }
|
|
333
336
|
}
|
|
334
337
|
|
|
338
|
+
// Prune stale hooks from prior version before installing new ones
|
|
339
|
+
const oldManifest = readHookManifest();
|
|
340
|
+
if (oldManifest) pruneStaleHooks(oldManifest);
|
|
341
|
+
|
|
335
342
|
await installAwEcc(cwd, { silent });
|
|
343
|
+
|
|
336
344
|
ensureAwRuntimeHook(HOME);
|
|
337
345
|
syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
|
|
338
346
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
339
347
|
applyStoredStartupPreferences(HOME);
|
|
340
348
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
341
349
|
installGlobalHooks();
|
|
350
|
+
if (!silent) fmt.logStep('Hooks and IDE integration configured');
|
|
342
351
|
|
|
343
352
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
344
353
|
if (cwd !== HOME) {
|
|
@@ -371,6 +380,9 @@ export async function initCommand(args) {
|
|
|
371
380
|
const commands = generateCommands(HOME, { silent: true });
|
|
372
381
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
373
382
|
|
|
383
|
+
// Write hook manifest after all hook installation is complete
|
|
384
|
+
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
385
|
+
|
|
374
386
|
if (silent) {
|
|
375
387
|
autoUpdate(await args._updateCheck);
|
|
376
388
|
} else {
|
|
@@ -462,21 +474,32 @@ export async function initCommand(args) {
|
|
|
462
474
|
if (folderName) {
|
|
463
475
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
464
476
|
}
|
|
477
|
+
// Parallel batch A: rules sync (HOME + cwd are independent targets)
|
|
465
478
|
syncRulesTargets(HOME);
|
|
466
|
-
if (cwd !== HOME)
|
|
467
|
-
syncRulesTargets(cwd);
|
|
468
|
-
}
|
|
479
|
+
if (cwd !== HOME) syncRulesTargets(cwd);
|
|
469
480
|
removeLegacyRegistryRules();
|
|
481
|
+
if (!silent) fmt.logStep('Rules synced');
|
|
470
482
|
|
|
471
483
|
// Step 3: Setup tasks, MCP, hooks
|
|
484
|
+
// Prune stale hooks from prior version before installing new ones
|
|
485
|
+
const oldManifestFresh = readHookManifest();
|
|
486
|
+
if (oldManifestFresh) pruneStaleHooks(oldManifestFresh);
|
|
487
|
+
|
|
472
488
|
await installAwEcc(cwd, { silent });
|
|
489
|
+
|
|
473
490
|
ensureAwRuntimeHook(HOME);
|
|
474
|
-
|
|
475
|
-
|
|
491
|
+
|
|
492
|
+
// Parallel batch B: post-ECC setup (instructions and MCP are independent)
|
|
493
|
+
const [, mcpFiles] = await Promise.all([
|
|
494
|
+
Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
|
|
495
|
+
setupMcp(HOME, team, { silent }),
|
|
496
|
+
]);
|
|
497
|
+
// applyStoredStartupPreferences reads settings written by ECC — keep after batch B
|
|
476
498
|
applyStoredStartupPreferences(HOME);
|
|
477
499
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
478
500
|
const hooksInstalled = installGlobalHooks();
|
|
479
501
|
installIdeTasks();
|
|
502
|
+
if (!silent) fmt.logStep('Hooks and IDE integration configured');
|
|
480
503
|
|
|
481
504
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
482
505
|
if (cwd !== HOME) {
|
|
@@ -506,12 +529,20 @@ export async function initCommand(args) {
|
|
|
506
529
|
ideSpinner.start('Wiring IDE symlinks...');
|
|
507
530
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
508
531
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
509
|
-
|
|
532
|
+
// Parallel batch C: symlinks + commands are independent
|
|
510
533
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
511
|
-
|
|
512
|
-
|
|
534
|
+
const [symlinks, commands] = [
|
|
535
|
+
linkWorkspace(HOME, awDirForLinks, { silent: true }),
|
|
536
|
+
generateCommands(HOME, { silent: true }),
|
|
537
|
+
];
|
|
513
538
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
514
539
|
|
|
540
|
+
// Write hook manifest after all hook installation is complete
|
|
541
|
+
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
542
|
+
|
|
543
|
+
// Ensure telemetry config exists (generates machine_id on first run)
|
|
544
|
+
ensureTelemetryConfig();
|
|
545
|
+
|
|
515
546
|
// Offer to update if a newer version is available
|
|
516
547
|
await promptUpdate(await args._updateCheck);
|
|
517
548
|
|
package/commands/nuke.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { uninstallAwEcc } from '../ecc.mjs';
|
|
|
16
16
|
import { removeMcpConfig } from '../mcp.mjs';
|
|
17
17
|
import { listProjectWorktrees } from '../git.mjs';
|
|
18
18
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
19
|
+
import { readHookManifest, pruneStaleHooks, removeHookManifest } from '../hook-cleanup.mjs';
|
|
19
20
|
|
|
20
21
|
const HOME = homedir();
|
|
21
22
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
@@ -129,43 +130,34 @@ function removeIdeSymlinks() {
|
|
|
129
130
|
return removed;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
// 3. Find and remove ALL .aw_registry symlinks from project directories
|
|
133
|
+
// 3. Find and remove ALL .aw_registry symlinks + legacy hooks from project directories.
|
|
134
|
+
// Uses a single batched find instead of 3 sequential ones for performance.
|
|
133
135
|
async function removeProjectSymlinks() {
|
|
134
136
|
let removed = 0;
|
|
137
|
+
let hooksRemoved = 0;
|
|
135
138
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`find "${HOME}" -maxdepth 4 -name ".aw_registry" -type l 2>/dev/null || true`,
|
|
139
|
+
// Single find: .aw_registry symlinks + legacy git hooks
|
|
140
|
+
const { stdout } = await exec(
|
|
141
|
+
`find "${HOME}" -maxdepth 5 \\( -name ".aw_registry" -type l -o -path "*/.git/hooks/post-checkout" -type f -o -path "*/.git/hooks/prepare-commit-msg" -type f \\) 2>/dev/null || true`,
|
|
140
142
|
{ encoding: 'utf8', timeout: 30000 }
|
|
141
143
|
);
|
|
142
|
-
for (const linkPath of registryLinks.trim().split('\n').filter(Boolean)) {
|
|
143
|
-
try {
|
|
144
|
-
removeWorkspaceHookDefaults(dirname(linkPath));
|
|
145
|
-
unlinkSync(linkPath);
|
|
146
|
-
removed++;
|
|
147
|
-
} catch { /* best effort */ }
|
|
148
|
-
}
|
|
149
144
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const content = readFileSync(hookPath, 'utf8');
|
|
162
|
-
// Only remove hooks that AW installed — identified by our marker comment
|
|
145
|
+
for (const itemPath of stdout.trim().split('\n').filter(Boolean)) {
|
|
146
|
+
try {
|
|
147
|
+
if (itemPath.endsWith('.aw_registry')) {
|
|
148
|
+
// Registry symlink — clean workspace hooks then remove
|
|
149
|
+
removeWorkspaceHookDefaults(dirname(itemPath));
|
|
150
|
+
unlinkSync(itemPath);
|
|
151
|
+
removed++;
|
|
152
|
+
} else if (itemPath.includes('.git/hooks/')) {
|
|
153
|
+
// Legacy git hook — only remove if AW-installed
|
|
154
|
+
const content = readFileSync(itemPath, 'utf8');
|
|
163
155
|
if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
164
|
-
unlinkSync(
|
|
156
|
+
unlinkSync(itemPath);
|
|
165
157
|
hooksRemoved++;
|
|
166
158
|
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
159
|
+
}
|
|
160
|
+
} catch { /* best effort */ }
|
|
169
161
|
}
|
|
170
162
|
|
|
171
163
|
return { removed, hooksRemoved };
|
|
@@ -232,6 +224,9 @@ export async function nukeCommand(args) {
|
|
|
232
224
|
|
|
233
225
|
const manifest = loadManifest();
|
|
234
226
|
|
|
227
|
+
// Read hook manifest into memory before any deletion (manifest file lives in ~/.aw/hooks/)
|
|
228
|
+
const hookManifest = readHookManifest();
|
|
229
|
+
|
|
235
230
|
// Use lstatSync (not existsSync) so a dangling symlink still shows as "found"
|
|
236
231
|
let registryFound = false;
|
|
237
232
|
try { lstatSync(GLOBAL_AW_DIR); registryFound = true; } catch { /* not present */ }
|
|
@@ -250,6 +245,9 @@ export async function nukeCommand(args) {
|
|
|
250
245
|
const ideCount = removeIdeSymlinks();
|
|
251
246
|
uninstallAwEcc();
|
|
252
247
|
removeMcpConfig();
|
|
248
|
+
// Prune stale hook entries using manifest (catches entries not tracked by install-state)
|
|
249
|
+
if (hookManifest) pruneStaleHooks(hookManifest);
|
|
250
|
+
removeHookManifest();
|
|
253
251
|
const totalIde = createdCount + ideCount;
|
|
254
252
|
ideSpinner.stop(`IDE symlinks and commands removed${totalIde > 0 ? ` (${totalIde} files)` : ''}`);
|
|
255
253
|
|
|
@@ -294,45 +292,36 @@ export async function nukeCommand(args) {
|
|
|
294
292
|
} catch { /* best effort */ }
|
|
295
293
|
}
|
|
296
294
|
|
|
297
|
-
// 4b.
|
|
298
|
-
// These are created by the symlink model and are NOT tracked by git worktree list.
|
|
295
|
+
// 4b+4c. Batched find for .aw symlinks and directories (single traversal instead of two).
|
|
299
296
|
try {
|
|
300
|
-
const { stdout:
|
|
301
|
-
`find "${HOME}" -maxdepth 5 -name ".aw" -type l 2>/dev/null || true`,
|
|
297
|
+
const { stdout: awItems } = await exec(
|
|
298
|
+
`find "${HOME}" -maxdepth 5 -name ".aw" \\( -type l -o -type d \\) 2>/dev/null || true`,
|
|
302
299
|
{ encoding: 'utf8', timeout: 30000 }
|
|
303
300
|
);
|
|
304
|
-
for (const
|
|
301
|
+
for (const itemPath of awItems.trim().split('\n').filter(Boolean)) {
|
|
302
|
+
if (itemPath === AW_HOME) continue; // skip ~/.aw itself
|
|
305
303
|
try {
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
} catch { /* best effort */ }
|
|
314
|
-
|
|
315
|
-
// 4c. Remove old-style .aw directories (git worktrees) that git worktree list missed.
|
|
316
|
-
// After ~/.aw is deleted these become dangling — find and clean them up.
|
|
317
|
-
try {
|
|
318
|
-
const { stdout: awDirs } = await exec(
|
|
319
|
-
`find "${HOME}" -maxdepth 5 -name ".aw" -type d 2>/dev/null || true`,
|
|
320
|
-
{ encoding: 'utf8', timeout: 30000 }
|
|
321
|
-
);
|
|
322
|
-
for (const dirPath of awDirs.trim().split('\n').filter(Boolean)) {
|
|
323
|
-
if (dirPath === AW_HOME) continue; // skip ~/.aw itself
|
|
324
|
-
try {
|
|
325
|
-
const gitFile = join(dirPath, '.git');
|
|
326
|
-
const stat = lstatSync(gitFile);
|
|
327
|
-
if (stat.isFile()) {
|
|
328
|
-
const content = readFileSync(gitFile, 'utf8').trim();
|
|
329
|
-
// Old git worktree pointing into ~/.aw — safe to remove
|
|
330
|
-
if (content.includes(`${AW_HOME}/.git/`)) {
|
|
331
|
-
rmSync(dirPath, { recursive: true, force: true });
|
|
304
|
+
const stat = lstatSync(itemPath);
|
|
305
|
+
if (stat.isSymbolicLink()) {
|
|
306
|
+
// New-style symlink pointing to ~/.aw
|
|
307
|
+
const target = readlinkSync(itemPath);
|
|
308
|
+
if (target === AW_HOME || target.endsWith('/.aw')) {
|
|
309
|
+
unlinkSync(itemPath);
|
|
332
310
|
wtRemoved++;
|
|
333
311
|
}
|
|
312
|
+
} else if (stat.isDirectory()) {
|
|
313
|
+
// Old-style git worktree — check .git file
|
|
314
|
+
const gitFile = join(itemPath, '.git');
|
|
315
|
+
const gitStat = lstatSync(gitFile);
|
|
316
|
+
if (gitStat.isFile()) {
|
|
317
|
+
const content = readFileSync(gitFile, 'utf8').trim();
|
|
318
|
+
if (content.includes(`${AW_HOME}/.git/`)) {
|
|
319
|
+
rmSync(itemPath, { recursive: true, force: true });
|
|
320
|
+
wtRemoved++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
334
323
|
}
|
|
335
|
-
} catch { /* not a worktree or already gone */ }
|
|
324
|
+
} catch { /* not a worktree/symlink or already gone */ }
|
|
336
325
|
}
|
|
337
326
|
} catch { /* best effort */ }
|
|
338
327
|
|
package/ecc.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execSync, exec as execCb } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execAsync = promisify(execCb);
|
|
2
4
|
import {
|
|
3
5
|
existsSync, readFileSync, readdirSync,
|
|
4
6
|
mkdirSync, rmSync, writeFileSync, renameSync,
|
|
@@ -10,7 +12,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
|
|
|
10
12
|
|
|
11
13
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
12
14
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
13
|
-
export const AW_ECC_TAG = "v1.4.
|
|
15
|
+
export const AW_ECC_TAG = "v1.4.41-beta.1";
|
|
14
16
|
|
|
15
17
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
16
18
|
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
@@ -63,6 +65,68 @@ function cloneWithRef(url, ref, dest) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// Async variants — allow event loop to tick so spinners can render.
|
|
69
|
+
async function runA(cmd, opts = {}) {
|
|
70
|
+
const { stdout } = await execAsync(cmd, { ...opts, maxBuffer: 10 * 1024 * 1024 });
|
|
71
|
+
return stdout;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchOverrideRefAsync(dest, ref) {
|
|
75
|
+
await runA(`git -C ${dest} fetch --quiet --depth 1 origin ${ref}`);
|
|
76
|
+
await runA(`git -C ${dest} checkout --quiet FETCH_HEAD`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function cloneWithRefAsync(url, ref, dest) {
|
|
80
|
+
try {
|
|
81
|
+
await runA(`git clone --quiet --depth 1 --branch ${ref} "${url}" "${dest}"`);
|
|
82
|
+
} catch {
|
|
83
|
+
await runA(`git clone --quiet --depth 1 "${url}" "${dest}"`);
|
|
84
|
+
await fetchOverrideRefAsync(dest, ref);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function cloneOrUpdateAsync(tag, dest) {
|
|
89
|
+
const overrideUrl = process.env.AW_ECC_CLONE_URL;
|
|
90
|
+
const overrideRef = process.env.AW_ECC_CLONE_REF?.trim();
|
|
91
|
+
|
|
92
|
+
if (existsSync(join(dest, ".git"))) {
|
|
93
|
+
try {
|
|
94
|
+
if (!overrideUrl && !overrideRef) {
|
|
95
|
+
await runA(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
|
|
96
|
+
await runA(`git -C ${dest} checkout --quiet ${tag}`);
|
|
97
|
+
} else {
|
|
98
|
+
if (overrideUrl) {
|
|
99
|
+
await runA(`git -C ${dest} remote set-url origin "${overrideUrl}"`);
|
|
100
|
+
}
|
|
101
|
+
await fetchOverrideRefAsync(dest, overrideRef || tag);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
} catch { /* fall through to fresh clone */ }
|
|
105
|
+
}
|
|
106
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
107
|
+
if (overrideUrl) {
|
|
108
|
+
if (overrideRef) {
|
|
109
|
+
await cloneWithRefAsync(overrideUrl, overrideRef, dest);
|
|
110
|
+
} else {
|
|
111
|
+
await runA(`git clone --quiet --depth 1 "${overrideUrl}" "${dest}"`);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (overrideRef) {
|
|
115
|
+
try {
|
|
116
|
+
await cloneWithRefAsync(AW_ECC_REPO_SSH, overrideRef, dest);
|
|
117
|
+
} catch {
|
|
118
|
+
await cloneWithRefAsync(AW_ECC_REPO_HTTPS, overrideRef, dest);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await runA(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`);
|
|
124
|
+
} catch {
|
|
125
|
+
await runA(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_HTTPS} ${dest}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
66
130
|
function readIfExists(path) {
|
|
67
131
|
try { return existsSync(path) ? readFileSync(path, "utf8") : null; } catch { return null; }
|
|
68
132
|
}
|
|
@@ -259,13 +323,16 @@ export async function installAwEcc(
|
|
|
259
323
|
{ targets = ["cursor", "claude", "codex"], silent = false } = {},
|
|
260
324
|
) {
|
|
261
325
|
if (process.env.AW_NO_ECC === '1') return;
|
|
262
|
-
if (!silent) fmt.logStep("Installing aw-ecc engine...");
|
|
263
326
|
|
|
264
327
|
const repoDir = eccDir();
|
|
265
328
|
const home = homedir();
|
|
266
329
|
|
|
330
|
+
const eccSpinner = silent ? null : fmt.spinner();
|
|
331
|
+
|
|
267
332
|
try {
|
|
268
|
-
|
|
333
|
+
if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
|
|
334
|
+
await cloneOrUpdateAsync(AW_ECC_TAG, repoDir);
|
|
335
|
+
if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
|
|
269
336
|
|
|
270
337
|
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
271
338
|
if (targets.includes("claude")) {
|
|
@@ -279,7 +346,7 @@ export async function installAwEcc(
|
|
|
279
346
|
// Claude + Cursor + Codex: file-copy via install-apply.js
|
|
280
347
|
const fileCopyTargets = targets.filter((t) => FILE_COPY_TARGETS.includes(t));
|
|
281
348
|
if (fileCopyTargets.length > 0) {
|
|
282
|
-
|
|
349
|
+
await runA("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
|
|
283
350
|
cwd: repoDir,
|
|
284
351
|
});
|
|
285
352
|
// generate-aw-hooks.js produces hooks.json and hook script sources that
|
|
@@ -291,15 +358,14 @@ export async function installAwEcc(
|
|
|
291
358
|
run(`node "${generateHooksScript}"`, { cwd: repoDir });
|
|
292
359
|
} catch { /* best effort — older engine versions may not have this script */ }
|
|
293
360
|
}
|
|
294
|
-
|
|
361
|
+
if (eccSpinner) eccSpinner.message('Applying aw-ecc to IDE targets...');
|
|
362
|
+
// Each target writes to disjoint paths (~/.claude/, ~/.cursor/, ~/.codex/) — safe to parallelize.
|
|
363
|
+
await Promise.all(fileCopyTargets.map(async (target) => {
|
|
295
364
|
try {
|
|
296
365
|
const snapshot = snapshotProtectedConfigs(home, target);
|
|
297
366
|
|
|
298
367
|
// Always use HOME as cwd so files land in ~/.<target>/ globally.
|
|
299
368
|
const runCwd = homedir();
|
|
300
|
-
// For claude: install the safe no-commands module set. The plugin
|
|
301
|
-
// already owns /aw:* command registration, and broader profiles with
|
|
302
|
-
// `--without baseline:commands` can fail on commands-core deps.
|
|
303
369
|
const installArgs = target === "claude"
|
|
304
370
|
? `--target ${target} --modules ${CLAUDE_FILE_COPY_MODULES.join(",")}`
|
|
305
371
|
: `--target ${target} --profile full`;
|
|
@@ -307,34 +373,24 @@ export async function installAwEcc(
|
|
|
307
373
|
`node ${join(repoDir, "scripts/install-apply.js")} ${installArgs}`,
|
|
308
374
|
{ cwd: runCwd },
|
|
309
375
|
);
|
|
310
|
-
// Move cursor commands into aw/ subfolder for namespace consistency
|
|
311
|
-
// so they're accessible as /aw:tdd, /aw:plan — same as Claude Code plugin.
|
|
312
376
|
if (target === "cursor") {
|
|
313
377
|
namespaceCursorCommands(runCwd);
|
|
314
|
-
// Cursor commands use hyphens (commands/aw/plan.md -> /aw-plan)
|
|
315
|
-
// but source skill/rule files use canonical colons (/aw:plan).
|
|
316
|
-
// Transform /aw: -> /aw- in installed cursor skill/rule files.
|
|
317
378
|
transformCursorAwRefs(home);
|
|
318
379
|
}
|
|
319
|
-
// Run sync script for codex: generates ~/.codex/prompts/*.md and
|
|
320
|
-
// merges AGENTS.md — Codex has no slash commands, so prompts are the
|
|
321
|
-
// equivalent of commands.
|
|
322
380
|
if (target === "codex") {
|
|
323
381
|
syncEccToCodex(repoDir);
|
|
324
382
|
}
|
|
325
|
-
|
|
326
|
-
// Critical: preserve user-owned config files if they existed before
|
|
327
|
-
// running aw-ecc (aw should only add, never replace user settings).
|
|
328
383
|
restoreProtectedConfigs(snapshot);
|
|
329
384
|
} catch { /* target not supported — skip */ }
|
|
330
|
-
}
|
|
385
|
+
}));
|
|
331
386
|
}
|
|
332
387
|
|
|
333
388
|
applyStoredStartupPreferences();
|
|
334
389
|
|
|
335
|
-
if (
|
|
390
|
+
if (eccSpinner) eccSpinner.stop('aw-ecc engine installed');
|
|
336
391
|
} catch (err) {
|
|
337
|
-
if (
|
|
392
|
+
if (eccSpinner) eccSpinner.stop(chalk.yellow(`aw-ecc install failed: ${err.message}`));
|
|
393
|
+
else if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
|
|
338
394
|
}
|
|
339
395
|
}
|
|
340
396
|
|
package/hook-cleanup.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// hook-cleanup.mjs — Hook cleanup manifest for safe version transitions.
|
|
2
|
+
//
|
|
3
|
+
// writeHookManifest() → snapshots all AW hook touchpoints to ~/.aw/hooks/manifest.json
|
|
4
|
+
// readHookManifest() → reads and validates the manifest
|
|
5
|
+
// pruneStaleHooks() → removes hook entries and runtime deps listed in a manifest
|
|
6
|
+
// removeHookManifest() → deletes the manifest file
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
10
|
+
rmSync, writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = 'aw-hooks.v1';
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
const MANIFEST_PATH = join(HOME, '.aw', 'hooks', 'manifest.json');
|
|
18
|
+
|
|
19
|
+
// ── Patterns matching AW-managed hook entries ────────────────────────────
|
|
20
|
+
|
|
21
|
+
function isManagedClaudeEntry(entry) {
|
|
22
|
+
if (entry?.description === 'AW usage telemetry') return true;
|
|
23
|
+
const cmds = Array.isArray(entry?.hooks) ? entry.hooks.map(h => h?.command || '') : [];
|
|
24
|
+
return cmds.some(c => c.includes('aw-usage-'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isManagedCursorEntry(entry) {
|
|
28
|
+
const cmd = String(entry?.command || '');
|
|
29
|
+
return cmd.includes('.cursor/hooks/') || cmd.includes('aw-ecc');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isManagedCodexEntry(entry) {
|
|
33
|
+
if (Array.isArray(entry?.hooks)) {
|
|
34
|
+
return entry.hooks.some(h => {
|
|
35
|
+
const cmd = String(h?.command || '');
|
|
36
|
+
return cmd.includes('.codex/hooks/') || cmd.includes('aw-ecc') || cmd.includes('aw-session-start');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Read helpers ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function readJson(filePath) {
|
|
45
|
+
if (!existsSync(filePath)) return null;
|
|
46
|
+
try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeJson(filePath, data) {
|
|
50
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
51
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Collect current hook state ───────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function collectClaudeTouchpoints() {
|
|
57
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
58
|
+
const hooksJsonPath = join(HOME, '.claude', 'hooks', 'hooks.json');
|
|
59
|
+
const settings = readJson(settingsPath);
|
|
60
|
+
const phases = [];
|
|
61
|
+
if (settings?.hooks) {
|
|
62
|
+
for (const phase of Object.keys(settings.hooks)) {
|
|
63
|
+
const entries = settings.hooks[phase];
|
|
64
|
+
if (Array.isArray(entries) && entries.some(isManagedClaudeEntry)) {
|
|
65
|
+
phases.push(phase);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
settingsPath,
|
|
71
|
+
hooksJsonPath: existsSync(hooksJsonPath) ? hooksJsonPath : null,
|
|
72
|
+
managedPhases: phases,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectCursorTouchpoints() {
|
|
77
|
+
const hooksJsonPath = join(HOME, '.cursor', 'hooks.json');
|
|
78
|
+
const config = readJson(hooksJsonPath);
|
|
79
|
+
const phases = [];
|
|
80
|
+
if (config?.hooks) {
|
|
81
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
82
|
+
const entries = config.hooks[phase];
|
|
83
|
+
if (Array.isArray(entries) && entries.some(isManagedCursorEntry)) {
|
|
84
|
+
phases.push(phase);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { hooksJsonPath, managedPhases: phases };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectCodexTouchpoints() {
|
|
92
|
+
const hooksJsonPath = join(HOME, '.codex', 'hooks.json');
|
|
93
|
+
const configPath = join(HOME, '.codex', 'config.toml');
|
|
94
|
+
const config = readJson(hooksJsonPath);
|
|
95
|
+
const phases = [];
|
|
96
|
+
if (config?.hooks) {
|
|
97
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
98
|
+
const entries = config.hooks[phase];
|
|
99
|
+
if (Array.isArray(entries) && entries.some(isManagedCodexEntry)) {
|
|
100
|
+
phases.push(phase);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
configPath: existsSync(configPath) ? configPath : null,
|
|
106
|
+
hooksJsonPath,
|
|
107
|
+
managedPhases: phases,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectGitTouchpoints() {
|
|
112
|
+
const hooksDir = join(HOME, '.aw', 'hooks');
|
|
113
|
+
const scripts = [];
|
|
114
|
+
if (existsSync(hooksDir)) {
|
|
115
|
+
try {
|
|
116
|
+
for (const entry of readdirSync(hooksDir)) {
|
|
117
|
+
// Exclude the manifest itself and hidden files
|
|
118
|
+
if (entry === 'manifest.json' || entry.startsWith('.')) continue;
|
|
119
|
+
scripts.push(entry);
|
|
120
|
+
}
|
|
121
|
+
} catch { /* best effort */ }
|
|
122
|
+
}
|
|
123
|
+
return { hooksDir, scripts };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectRuntimeDeps() {
|
|
127
|
+
const deps = [];
|
|
128
|
+
const hooksDir = join(HOME, '.aw-ecc', 'scripts', 'hooks');
|
|
129
|
+
const libDir = join(HOME, '.aw-ecc', 'scripts', 'lib');
|
|
130
|
+
|
|
131
|
+
if (existsSync(hooksDir)) {
|
|
132
|
+
try {
|
|
133
|
+
for (const entry of readdirSync(hooksDir)) {
|
|
134
|
+
if (entry.startsWith('aw-usage-')) {
|
|
135
|
+
deps.push(join(hooksDir, entry));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch { /* best effort */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const telemetryLib = join(libDir, 'aw-usage-telemetry.js');
|
|
142
|
+
if (existsSync(telemetryLib)) {
|
|
143
|
+
deps.push(telemetryLib);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return deps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write a hook manifest capturing all current AW hook touchpoints.
|
|
153
|
+
* @param {{ eccVersion: string, awVersion: string }} opts
|
|
154
|
+
* @returns {object} the written manifest
|
|
155
|
+
*/
|
|
156
|
+
export function writeHookManifest({ eccVersion, awVersion }) {
|
|
157
|
+
const manifest = {
|
|
158
|
+
schemaVersion: SCHEMA_VERSION,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
eccVersion,
|
|
161
|
+
awVersion,
|
|
162
|
+
touchpoints: {
|
|
163
|
+
claude: collectClaudeTouchpoints(),
|
|
164
|
+
cursor: collectCursorTouchpoints(),
|
|
165
|
+
codex: collectCodexTouchpoints(),
|
|
166
|
+
git: collectGitTouchpoints(),
|
|
167
|
+
},
|
|
168
|
+
runtimeDeps: collectRuntimeDeps(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
writeJson(MANIFEST_PATH, manifest);
|
|
172
|
+
return manifest;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read and validate the hook manifest.
|
|
177
|
+
* @returns {object|null} the manifest, or null if missing/invalid
|
|
178
|
+
*/
|
|
179
|
+
export function readHookManifest() {
|
|
180
|
+
const data = readJson(MANIFEST_PATH);
|
|
181
|
+
if (!data || data.schemaVersion !== SCHEMA_VERSION) return null;
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove hook entries and runtime deps listed in a manifest.
|
|
187
|
+
* Does NOT touch git hooks (handled by removeGlobalHooks).
|
|
188
|
+
* @param {object} manifest — from readHookManifest()
|
|
189
|
+
* @returns {number} count of items removed
|
|
190
|
+
*/
|
|
191
|
+
export function pruneStaleHooks(manifest) {
|
|
192
|
+
if (!manifest?.touchpoints) return 0;
|
|
193
|
+
let removed = 0;
|
|
194
|
+
|
|
195
|
+
// Claude: remove managed telemetry entries from settings.json
|
|
196
|
+
const claudeSettings = manifest.touchpoints.claude;
|
|
197
|
+
if (claudeSettings?.settingsPath && claudeSettings.managedPhases?.length > 0) {
|
|
198
|
+
const config = readJson(claudeSettings.settingsPath);
|
|
199
|
+
if (config?.hooks) {
|
|
200
|
+
let changed = false;
|
|
201
|
+
for (const phase of claudeSettings.managedPhases) {
|
|
202
|
+
const entries = config.hooks[phase];
|
|
203
|
+
if (!Array.isArray(entries)) continue;
|
|
204
|
+
const filtered = entries.filter(e => !isManagedClaudeEntry(e));
|
|
205
|
+
if (filtered.length !== entries.length) {
|
|
206
|
+
changed = true;
|
|
207
|
+
removed += entries.length - filtered.length;
|
|
208
|
+
if (filtered.length > 0) {
|
|
209
|
+
config.hooks[phase] = filtered;
|
|
210
|
+
} else {
|
|
211
|
+
delete config.hooks[phase];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (changed) {
|
|
216
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
217
|
+
writeJson(claudeSettings.settingsPath, config);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cursor: remove managed entries from hooks.json
|
|
223
|
+
const cursorHooks = manifest.touchpoints.cursor;
|
|
224
|
+
if (cursorHooks?.hooksJsonPath && cursorHooks.managedPhases?.length > 0) {
|
|
225
|
+
const config = readJson(cursorHooks.hooksJsonPath);
|
|
226
|
+
if (config?.hooks) {
|
|
227
|
+
let changed = false;
|
|
228
|
+
for (const phase of cursorHooks.managedPhases) {
|
|
229
|
+
const entries = config.hooks[phase];
|
|
230
|
+
if (!Array.isArray(entries)) continue;
|
|
231
|
+
const filtered = entries.filter(e => !isManagedCursorEntry(e));
|
|
232
|
+
if (filtered.length !== entries.length) {
|
|
233
|
+
changed = true;
|
|
234
|
+
removed += entries.length - filtered.length;
|
|
235
|
+
if (filtered.length > 0) {
|
|
236
|
+
config.hooks[phase] = filtered;
|
|
237
|
+
} else {
|
|
238
|
+
delete config.hooks[phase];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (changed) {
|
|
243
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
244
|
+
writeJson(cursorHooks.hooksJsonPath, config);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Codex: remove managed entries from hooks.json
|
|
250
|
+
const codexHooks = manifest.touchpoints.codex;
|
|
251
|
+
if (codexHooks?.hooksJsonPath && codexHooks.managedPhases?.length > 0) {
|
|
252
|
+
const config = readJson(codexHooks.hooksJsonPath);
|
|
253
|
+
if (config?.hooks) {
|
|
254
|
+
let changed = false;
|
|
255
|
+
for (const phase of codexHooks.managedPhases) {
|
|
256
|
+
const entries = config.hooks[phase];
|
|
257
|
+
if (!Array.isArray(entries)) continue;
|
|
258
|
+
const filtered = entries.filter(e => !isManagedCodexEntry(e));
|
|
259
|
+
if (filtered.length !== entries.length) {
|
|
260
|
+
changed = true;
|
|
261
|
+
removed += entries.length - filtered.length;
|
|
262
|
+
if (filtered.length > 0) {
|
|
263
|
+
config.hooks[phase] = filtered;
|
|
264
|
+
} else {
|
|
265
|
+
delete config.hooks[phase];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed) {
|
|
270
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
271
|
+
writeJson(codexHooks.hooksJsonPath, config);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Runtime deps: remove aw-usage-* files
|
|
277
|
+
if (Array.isArray(manifest.runtimeDeps)) {
|
|
278
|
+
for (const dep of manifest.runtimeDeps) {
|
|
279
|
+
if (existsSync(dep)) {
|
|
280
|
+
try {
|
|
281
|
+
rmSync(dep, { force: true });
|
|
282
|
+
removed++;
|
|
283
|
+
} catch { /* best effort */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return removed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Delete the manifest file.
|
|
293
|
+
*/
|
|
294
|
+
export function removeHookManifest() {
|
|
295
|
+
if (existsSync(MANIFEST_PATH)) {
|
|
296
|
+
rmSync(MANIFEST_PATH, { force: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Export for testing
|
|
301
|
+
export { MANIFEST_PATH, SCHEMA_VERSION };
|
package/hooks/codex-home.mjs
CHANGED
|
@@ -2,8 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
|
|
3
3
|
import { getSupportedHarnessPhaseEntries } from '../hook-manifest.mjs';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
buildRegistryDelegatingPhaseScript,
|
|
5
|
+
buildRegistryDelegatingPhaseScriptWithTelemetry,
|
|
7
6
|
buildReservedPhaseScript,
|
|
8
7
|
} from './shared-phase-scripts.mjs';
|
|
9
8
|
|
|
@@ -19,7 +18,7 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
|
|
|
19
18
|
scriptName: 'aw-session-start.sh',
|
|
20
19
|
scriptMarker: '# aw-managed: codex-global-session-start',
|
|
21
20
|
buildScriptContent() {
|
|
22
|
-
return
|
|
21
|
+
return buildRegistryDelegatingPhaseScriptWithTelemetry({
|
|
23
22
|
marker: this.scriptMarker,
|
|
24
23
|
phase: 'SessionStart',
|
|
25
24
|
targetCandidates: [
|
|
@@ -27,6 +26,8 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
|
|
|
27
26
|
'$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
|
|
28
27
|
],
|
|
29
28
|
warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform.',
|
|
29
|
+
telemetryHookPath: '$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js',
|
|
30
|
+
harnessEnv: 'codex',
|
|
30
31
|
});
|
|
31
32
|
},
|
|
32
33
|
buildEntry(command) {
|
|
@@ -37,6 +37,53 @@ echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"${phase}\\",\\"additiona
|
|
|
37
37
|
`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function buildRegistryDelegatingPhaseScriptWithTelemetry({
|
|
41
|
+
marker,
|
|
42
|
+
phase,
|
|
43
|
+
targetCandidates,
|
|
44
|
+
warningMessage,
|
|
45
|
+
telemetryHookPath,
|
|
46
|
+
harnessEnv,
|
|
47
|
+
}) {
|
|
48
|
+
const targetsBlock = targetCandidates
|
|
49
|
+
.map(target => ` "${target}"`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
const escapedWarning = escapeDoubleQuotes(warningMessage);
|
|
52
|
+
|
|
53
|
+
return `#!/usr/bin/env bash
|
|
54
|
+
${marker}
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
|
|
57
|
+
# Capture stdin so we can feed it to both telemetry and the AW router delegate.
|
|
58
|
+
STDIN=$(cat)
|
|
59
|
+
|
|
60
|
+
# Fire telemetry (non-blocking, all output suppressed).
|
|
61
|
+
TELEMETRY_HOOK="${telemetryHookPath}"
|
|
62
|
+
if [[ -f "$TELEMETRY_HOOK" ]] && command -v node >/dev/null 2>&1; then
|
|
63
|
+
printf '%s' "$STDIN" | AW_HARNESS=${harnessEnv} node "$TELEMETRY_HOOK" >/dev/null 2>&1 || true
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
TARGETS=(
|
|
67
|
+
${targetsBlock}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for target in "\${TARGETS[@]}"; do
|
|
71
|
+
if [[ -f "\$target" ]]; then
|
|
72
|
+
printf '%s' "$STDIN" | bash "\$target"
|
|
73
|
+
exit $?
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
CONTEXT="# AW Session Context
|
|
78
|
+
|
|
79
|
+
${escapedWarning}"
|
|
80
|
+
|
|
81
|
+
JSON_CONTEXT=$(printf '%s' "\$CONTEXT" | python3 -c 'import json, sys; print(json.dumps(sys.stdin.read()))')
|
|
82
|
+
|
|
83
|
+
echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"${phase}\\",\\"additionalContext\\":\${JSON_CONTEXT}}}"
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
40
87
|
export function buildDelegatingPhaseScript({
|
|
41
88
|
marker,
|
|
42
89
|
targetPath,
|
package/hooks.mjs
CHANGED
|
@@ -95,6 +95,16 @@ if [ -f ".aw/.git" ] && command -v aw >/dev/null 2>&1; then
|
|
|
95
95
|
AW_TRIGGER=hook:post-commit aw link >/dev/null 2>&1 &
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
+
# Fire commit_created telemetry if commit has AW co-author trailer
|
|
99
|
+
if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; then
|
|
100
|
+
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
101
|
+
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
102
|
+
COMMIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
103
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
104
|
+
node "$TELEMETRY_HOOK" "$COMMIT_HASH" "$BRANCH" >/dev/null 2>&1
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
98
108
|
# Chain to previous hooksPath
|
|
99
109
|
PREV_PATH_FILE="$HOME/.aw/hooks/.previous-hooks-path"
|
|
100
110
|
if [ -f "$PREV_PATH_FILE" ]; then
|
|
@@ -258,10 +268,31 @@ fi
|
|
|
258
268
|
exit 0
|
|
259
269
|
`;
|
|
260
270
|
|
|
271
|
+
// Standalone post-commit for local .git/hooks/ installation.
|
|
272
|
+
// Fires commit_created telemetry when the commit has AW co-author trailer.
|
|
273
|
+
const LOCAL_POST_COMMIT = `#!/bin/sh
|
|
274
|
+
# aw: local post-commit hook (installed by aw init)
|
|
275
|
+
|
|
276
|
+
# Skip aw temp dirs
|
|
277
|
+
case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*|*/.aw|*/.aw/*) exit 0 ;; esac
|
|
278
|
+
|
|
279
|
+
# Fire commit_created telemetry if commit has AW co-author trailer
|
|
280
|
+
if git log -1 --format='%b' HEAD 2>/dev/null | grep -qF "Co-Authored-By: AW"; then
|
|
281
|
+
TELEMETRY_HOOK="$HOME/.aw-ecc/scripts/hooks/aw-usage-commit-created.js"
|
|
282
|
+
if command -v node >/dev/null 2>&1 && [ -f "$TELEMETRY_HOOK" ]; then
|
|
283
|
+
COMMIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
284
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
285
|
+
node "$TELEMETRY_HOOK" "$COMMIT_HASH" "$BRANCH" >/dev/null 2>&1
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
exit 0
|
|
290
|
+
`;
|
|
291
|
+
|
|
261
292
|
/**
|
|
262
|
-
* Install prepare-commit-msg
|
|
263
|
-
* This covers repos where another tool (e.g. Claude Code) sets
|
|
264
|
-
* core.hooksPath that overrides the global one.
|
|
293
|
+
* Install prepare-commit-msg and post-commit hooks into a project's local
|
|
294
|
+
* .git/hooks/. This covers repos where another tool (e.g. Claude Code) sets
|
|
295
|
+
* a local core.hooksPath that overrides the global one.
|
|
265
296
|
*
|
|
266
297
|
* @param {string} projectDir — root of the project (must contain .git/)
|
|
267
298
|
*/
|
|
@@ -297,6 +328,15 @@ export function installLocalCommitHook(projectDir) {
|
|
|
297
328
|
|
|
298
329
|
writeFileSync(hookPath, LOCAL_PREPARE_COMMIT_MSG);
|
|
299
330
|
chmodSync(hookPath, '755');
|
|
331
|
+
|
|
332
|
+
// Also install post-commit for commit_created telemetry
|
|
333
|
+
const postCommitPath = join(hooksDir, 'post-commit');
|
|
334
|
+
const shouldWritePostCommit = !existsSync(postCommitPath)
|
|
335
|
+
|| readFileSync(postCommitPath, 'utf8').includes('aw:');
|
|
336
|
+
if (shouldWritePostCommit) {
|
|
337
|
+
writeFileSync(postCommitPath, LOCAL_POST_COMMIT);
|
|
338
|
+
chmodSync(postCommitPath, '755');
|
|
339
|
+
}
|
|
300
340
|
} catch { /* best effort */ }
|
|
301
341
|
}
|
|
302
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.42-beta.
|
|
3
|
+
"version": "0.1.42-beta.30",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"slack-sim/",
|
|
27
27
|
"file-tree.mjs",
|
|
28
28
|
"apply.mjs",
|
|
29
|
+
"hook-cleanup.mjs",
|
|
29
30
|
"hook-manifest.mjs",
|
|
30
31
|
"update.mjs",
|
|
31
32
|
"hooks.mjs",
|
package/render-rules.mjs
CHANGED
|
@@ -351,6 +351,7 @@ For every non-trivial request, execute these steps in order before any substanti
|
|
|
351
351
|
- /aw-review → Read aw-review/SKILL.md
|
|
352
352
|
- /aw-deploy → Read aw-deploy/SKILL.md
|
|
353
353
|
- /aw-ship → Read aw-ship/SKILL.md
|
|
354
|
+
- /aw-feature → Read aw-feature/SKILL.md
|
|
354
355
|
|
|
355
356
|
4. **Follow the skill's behavior** — produce the artifacts the skill defines, not general-knowledge answers.
|
|
356
357
|
|
package/startup.mjs
CHANGED
|
@@ -16,6 +16,11 @@ const DISABLED_MODE = 'disabled';
|
|
|
16
16
|
const CLAUDE_DISABLE_DESCRIPTION = 'AW-managed override: disable automatic AW session routing';
|
|
17
17
|
const CLAUDE_TELEMETRY_DESCRIPTION = 'AW usage telemetry';
|
|
18
18
|
const CLAUDE_TELEMETRY_HOOKS = [
|
|
19
|
+
{
|
|
20
|
+
phase: 'SessionStart',
|
|
21
|
+
matcher: undefined,
|
|
22
|
+
command: 'node "$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js"',
|
|
23
|
+
},
|
|
19
24
|
{
|
|
20
25
|
phase: 'PostToolUse',
|
|
21
26
|
matcher: 'Skill|Agent|Shell|Bash',
|
|
@@ -168,7 +173,10 @@ function hasCommandHook(entry, command) {
|
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
function isManagedClaudeTelemetryEntry(entry) {
|
|
171
|
-
|
|
176
|
+
if (entry?.description === CLAUDE_TELEMETRY_DESCRIPTION) return true;
|
|
177
|
+
// Also match legacy entries (no description) by command pattern
|
|
178
|
+
const cmds = Array.isArray(entry?.hooks) ? entry.hooks.map(h => h?.command || '') : [];
|
|
179
|
+
return cmds.some(c => c.includes('aw-usage-'));
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
function buildClaudeTelemetryEntry(hookDef) {
|
|
@@ -196,10 +204,14 @@ function enableClaudeTelemetryHooks(homeDir = homedir()) {
|
|
|
196
204
|
|
|
197
205
|
for (const hookDef of CLAUDE_TELEMETRY_HOOKS) {
|
|
198
206
|
const current = Array.isArray(config.hooks[hookDef.phase]) ? config.hooks[hookDef.phase] : [];
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
const managed = current.filter(isManagedClaudeTelemetryEntry);
|
|
208
|
+
|
|
209
|
+
// If exactly one properly described entry exists, nothing to do
|
|
210
|
+
if (managed.length === 1 && managed[0].description === CLAUDE_TELEMETRY_DESCRIPTION) continue;
|
|
201
211
|
|
|
202
|
-
|
|
212
|
+
// Remove all legacy/duplicate telemetry entries, then add the canonical one
|
|
213
|
+
const cleaned = current.filter(e => !isManagedClaudeTelemetryEntry(e));
|
|
214
|
+
config.hooks[hookDef.phase] = [...cleaned, buildClaudeTelemetryEntry(hookDef)];
|
|
203
215
|
changed = true;
|
|
204
216
|
}
|
|
205
217
|
|