@ghl-ai/aw 0.1.42-beta.25 → 0.1.42-beta.26
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 +30 -9
- package/commands/nuke.mjs +49 -60
- package/ecc.mjs +4 -17
- package/package.json +1 -1
- package/render-rules.mjs +1 -0
package/commands/init.mjs
CHANGED
|
@@ -29,8 +29,9 @@ import { installLocalCommitHook } from '../hooks.mjs';
|
|
|
29
29
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
30
30
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
31
31
|
import { loadConfig as ensureTelemetryConfig } from '../telemetry.mjs';
|
|
32
|
-
import { installAwEcc } from '../ecc.mjs';
|
|
32
|
+
import { installAwEcc, AW_ECC_TAG } from '../ecc.mjs';
|
|
33
33
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
34
|
+
import { readHookManifest, pruneStaleHooks, writeHookManifest } from '../hook-cleanup.mjs';
|
|
34
35
|
import {
|
|
35
36
|
initPersistentClone,
|
|
36
37
|
isValidClone,
|
|
@@ -333,6 +334,10 @@ export async function initCommand(args) {
|
|
|
333
334
|
try { syncWorktreeSparseCheckout(AW_HOME, localAw); } catch { /* best effort */ }
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// Prune stale hooks from prior version before installing new ones
|
|
338
|
+
const oldManifest = readHookManifest();
|
|
339
|
+
if (oldManifest) pruneStaleHooks(oldManifest);
|
|
340
|
+
|
|
336
341
|
await installAwEcc(cwd, { silent });
|
|
337
342
|
ensureAwRuntimeHook(HOME);
|
|
338
343
|
syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
|
|
@@ -372,6 +377,9 @@ export async function initCommand(args) {
|
|
|
372
377
|
const commands = generateCommands(HOME, { silent: true });
|
|
373
378
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
374
379
|
|
|
380
|
+
// Write hook manifest after all hook installation is complete
|
|
381
|
+
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
382
|
+
|
|
375
383
|
if (silent) {
|
|
376
384
|
autoUpdate(await args._updateCheck);
|
|
377
385
|
} else {
|
|
@@ -463,17 +471,25 @@ export async function initCommand(args) {
|
|
|
463
471
|
if (folderName) {
|
|
464
472
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
465
473
|
}
|
|
474
|
+
// Parallel batch A: rules sync (HOME + cwd are independent targets)
|
|
466
475
|
syncRulesTargets(HOME);
|
|
467
|
-
if (cwd !== HOME)
|
|
468
|
-
syncRulesTargets(cwd);
|
|
469
|
-
}
|
|
476
|
+
if (cwd !== HOME) syncRulesTargets(cwd);
|
|
470
477
|
removeLegacyRegistryRules();
|
|
471
478
|
|
|
472
479
|
// Step 3: Setup tasks, MCP, hooks
|
|
480
|
+
// Prune stale hooks from prior version before installing new ones
|
|
481
|
+
const oldManifestFresh = readHookManifest();
|
|
482
|
+
if (oldManifestFresh) pruneStaleHooks(oldManifestFresh);
|
|
483
|
+
|
|
473
484
|
await installAwEcc(cwd, { silent });
|
|
474
485
|
ensureAwRuntimeHook(HOME);
|
|
475
|
-
|
|
476
|
-
|
|
486
|
+
|
|
487
|
+
// Parallel batch B: post-ECC setup (instructions and MCP are independent)
|
|
488
|
+
const [, mcpFiles] = await Promise.all([
|
|
489
|
+
Promise.resolve(syncHomeAndProjectInstructions(cwd, team)),
|
|
490
|
+
setupMcp(HOME, team, { silent }),
|
|
491
|
+
]);
|
|
492
|
+
// applyStoredStartupPreferences reads settings written by ECC — keep after batch B
|
|
477
493
|
applyStoredStartupPreferences(HOME);
|
|
478
494
|
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
479
495
|
const hooksInstalled = installGlobalHooks();
|
|
@@ -507,12 +523,17 @@ export async function initCommand(args) {
|
|
|
507
523
|
ideSpinner.start('Wiring IDE symlinks...');
|
|
508
524
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
509
525
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
510
|
-
|
|
526
|
+
// Parallel batch C: symlinks + commands are independent
|
|
511
527
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
512
|
-
|
|
513
|
-
|
|
528
|
+
const [symlinks, commands] = [
|
|
529
|
+
linkWorkspace(HOME, awDirForLinks, { silent: true }),
|
|
530
|
+
generateCommands(HOME, { silent: true }),
|
|
531
|
+
];
|
|
514
532
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
515
533
|
|
|
534
|
+
// Write hook manifest after all hook installation is complete
|
|
535
|
+
try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
|
|
536
|
+
|
|
516
537
|
// Ensure telemetry config exists (generates machine_id on first run)
|
|
517
538
|
ensureTelemetryConfig();
|
|
518
539
|
|
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
|
@@ -10,7 +10,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
|
|
|
10
10
|
|
|
11
11
|
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
12
12
|
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
13
|
-
export const AW_ECC_TAG = "v1.4.
|
|
13
|
+
export const AW_ECC_TAG = "v1.4.41-beta.1";
|
|
14
14
|
|
|
15
15
|
const MARKETPLACE_NAME = "aw-marketplace";
|
|
16
16
|
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
@@ -291,15 +291,13 @@ export async function installAwEcc(
|
|
|
291
291
|
run(`node "${generateHooksScript}"`, { cwd: repoDir });
|
|
292
292
|
} catch { /* best effort — older engine versions may not have this script */ }
|
|
293
293
|
}
|
|
294
|
-
|
|
294
|
+
// Each target writes to disjoint paths (~/.claude/, ~/.cursor/, ~/.codex/) — safe to parallelize.
|
|
295
|
+
await Promise.all(fileCopyTargets.map(async (target) => {
|
|
295
296
|
try {
|
|
296
297
|
const snapshot = snapshotProtectedConfigs(home, target);
|
|
297
298
|
|
|
298
299
|
// Always use HOME as cwd so files land in ~/.<target>/ globally.
|
|
299
300
|
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
301
|
const installArgs = target === "claude"
|
|
304
302
|
? `--target ${target} --modules ${CLAUDE_FILE_COPY_MODULES.join(",")}`
|
|
305
303
|
: `--target ${target} --profile full`;
|
|
@@ -307,27 +305,16 @@ export async function installAwEcc(
|
|
|
307
305
|
`node ${join(repoDir, "scripts/install-apply.js")} ${installArgs}`,
|
|
308
306
|
{ cwd: runCwd },
|
|
309
307
|
);
|
|
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
308
|
if (target === "cursor") {
|
|
313
309
|
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
310
|
transformCursorAwRefs(home);
|
|
318
311
|
}
|
|
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
312
|
if (target === "codex") {
|
|
323
313
|
syncEccToCodex(repoDir);
|
|
324
314
|
}
|
|
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
315
|
restoreProtectedConfigs(snapshot);
|
|
329
316
|
} catch { /* target not supported — skip */ }
|
|
330
|
-
}
|
|
317
|
+
}));
|
|
331
318
|
}
|
|
332
319
|
|
|
333
320
|
applyStoredStartupPreferences();
|
package/package.json
CHANGED
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
|
|