@ghl-ai/aw 0.1.42-beta.26 → 0.1.42-beta.28

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 CHANGED
@@ -339,12 +339,16 @@ export async function initCommand(args) {
339
339
  if (oldManifest) pruneStaleHooks(oldManifest);
340
340
 
341
341
  await installAwEcc(cwd, { silent });
342
+
343
+ const setupSpinner = silent ? null : fmt.spinner();
344
+ if (setupSpinner) setupSpinner.start('Configuring hooks and IDE integration...');
342
345
  ensureAwRuntimeHook(HOME);
343
346
  syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
344
347
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
345
348
  applyStoredStartupPreferences(HOME);
346
349
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
347
350
  installGlobalHooks();
351
+ if (setupSpinner) setupSpinner.stop('Hooks and IDE integration configured');
348
352
 
349
353
  // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
350
354
  if (cwd !== HOME) {
@@ -482,6 +486,9 @@ export async function initCommand(args) {
482
486
  if (oldManifestFresh) pruneStaleHooks(oldManifestFresh);
483
487
 
484
488
  await installAwEcc(cwd, { silent });
489
+
490
+ const setupSpinnerFresh = silent ? null : fmt.spinner();
491
+ if (setupSpinnerFresh) setupSpinnerFresh.start('Configuring hooks and IDE integration...');
485
492
  ensureAwRuntimeHook(HOME);
486
493
 
487
494
  // Parallel batch B: post-ECC setup (instructions and MCP are independent)
@@ -494,6 +501,7 @@ export async function initCommand(args) {
494
501
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
495
502
  const hooksInstalled = installGlobalHooks();
496
503
  installIdeTasks();
504
+ if (setupSpinnerFresh) setupSpinnerFresh.stop('Hooks and IDE integration configured');
497
505
 
498
506
  // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
499
507
  if (cwd !== HOME) {
package/ecc.mjs CHANGED
@@ -259,13 +259,16 @@ export async function installAwEcc(
259
259
  { targets = ["cursor", "claude", "codex"], silent = false } = {},
260
260
  ) {
261
261
  if (process.env.AW_NO_ECC === '1') return;
262
- if (!silent) fmt.logStep("Installing aw-ecc engine...");
263
262
 
264
263
  const repoDir = eccDir();
265
264
  const home = homedir();
266
265
 
266
+ const eccSpinner = silent ? null : fmt.spinner();
267
+
267
268
  try {
269
+ if (eccSpinner) eccSpinner.start('Cloning aw-ecc engine...');
268
270
  cloneOrUpdate(AW_ECC_TAG, repoDir);
271
+ if (eccSpinner) eccSpinner.message('Installing aw-ecc dependencies...');
269
272
 
270
273
  // Claude Code: plugin install via marketplace CLI (proper agent dispatch)
271
274
  if (targets.includes("claude")) {
@@ -291,6 +294,7 @@ export async function installAwEcc(
291
294
  run(`node "${generateHooksScript}"`, { cwd: repoDir });
292
295
  } catch { /* best effort — older engine versions may not have this script */ }
293
296
  }
297
+ if (eccSpinner) eccSpinner.message('Applying aw-ecc to IDE targets...');
294
298
  // Each target writes to disjoint paths (~/.claude/, ~/.cursor/, ~/.codex/) — safe to parallelize.
295
299
  await Promise.all(fileCopyTargets.map(async (target) => {
296
300
  try {
@@ -319,9 +323,10 @@ export async function installAwEcc(
319
323
 
320
324
  applyStoredStartupPreferences();
321
325
 
322
- if (!silent) fmt.logSuccess("aw-ecc engine installed");
326
+ if (eccSpinner) eccSpinner.stop('aw-ecc engine installed');
323
327
  } catch (err) {
324
- if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
328
+ if (eccSpinner) eccSpinner.stop(chalk.yellow(`aw-ecc install failed: ${err.message}`));
329
+ else if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
325
330
  }
326
331
  }
327
332
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.42-beta.26",
3
+ "version": "0.1.42-beta.28",
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",