agentic-workflow-manager 3.0.1 → 3.2.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.
Files changed (103) hide show
  1. package/dist/src/commands/add.js +84 -0
  2. package/dist/src/commands/agent.js +95 -0
  3. package/dist/src/commands/backup.js +46 -0
  4. package/dist/src/commands/doctor.js +80 -4
  5. package/dist/src/commands/export.js +48 -0
  6. package/dist/src/commands/hooks/claude.js +192 -0
  7. package/dist/src/commands/hooks/codex.js +191 -0
  8. package/dist/src/commands/hooks/index.js +23 -8
  9. package/dist/src/commands/hooks/install.js +11 -107
  10. package/dist/src/commands/hooks/resync.js +28 -18
  11. package/dist/src/commands/hooks/shared.js +94 -0
  12. package/dist/src/commands/hooks/status.js +13 -64
  13. package/dist/src/commands/hooks/uninstall.js +11 -47
  14. package/dist/src/commands/init.js +90 -28
  15. package/dist/src/commands/registry/index.js +1 -1
  16. package/dist/src/commands/sync.js +113 -0
  17. package/dist/src/commands/update.js +107 -0
  18. package/dist/src/core/agent-targets.js +50 -0
  19. package/dist/src/core/artifact-state.js +65 -0
  20. package/dist/src/core/atomic-file.js +72 -0
  21. package/dist/src/core/bundle-install.js +60 -38
  22. package/dist/src/core/bundles.js +21 -0
  23. package/dist/src/core/context/managed-block.js +190 -0
  24. package/dist/src/core/context/orchestrator.js +3 -1
  25. package/dist/src/core/context/project-constitution-inject.js +1 -1
  26. package/dist/src/core/context/regenerate.js +3 -3
  27. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  28. package/dist/src/core/diagnostics/checks.js +16 -0
  29. package/dist/src/core/diagnostics/context.js +37 -8
  30. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  31. package/dist/src/core/discovery.js +9 -3
  32. package/dist/src/core/executor.js +30 -9
  33. package/dist/src/core/export/index.js +51 -0
  34. package/dist/src/core/export/pack.js +58 -0
  35. package/dist/src/core/export/resolve.js +77 -0
  36. package/dist/src/core/export/transform.js +57 -0
  37. package/dist/src/core/export/types.js +2 -0
  38. package/dist/src/core/init/mutation-targets.js +139 -0
  39. package/dist/src/core/init/provider-facts.js +164 -0
  40. package/dist/src/core/init/steps.js +65 -9
  41. package/dist/src/core/install-planner.js +206 -0
  42. package/dist/src/core/install-transaction.js +405 -0
  43. package/dist/src/core/profile.js +1 -1
  44. package/dist/src/core/provider-artifacts.js +54 -0
  45. package/dist/src/core/provider-version.js +36 -0
  46. package/dist/src/core/reconciliation.js +61 -0
  47. package/dist/src/core/registries.js +10 -0
  48. package/dist/src/core/renderers/canonical-agent.js +27 -0
  49. package/dist/src/core/renderers/codex-agent.js +60 -0
  50. package/dist/src/core/skill-integrity.js +4 -3
  51. package/dist/src/index.js +99 -289
  52. package/dist/src/providers/index.js +143 -39
  53. package/dist/src/ui/provider-preflight.js +26 -0
  54. package/dist/src/utils/config.js +101 -9
  55. package/dist/tests/commands/agent.test.js +103 -0
  56. package/dist/tests/commands/backup.test.js +109 -0
  57. package/dist/tests/commands/doctor.test.js +65 -4
  58. package/dist/tests/commands/export.test.js +64 -0
  59. package/dist/tests/commands/hooks/codex.test.js +230 -0
  60. package/dist/tests/commands/hooks/install.test.js +20 -1
  61. package/dist/tests/commands/hooks/resync.test.js +53 -6
  62. package/dist/tests/commands/hooks/status.test.js +10 -0
  63. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  64. package/dist/tests/commands/init.test.js +195 -8
  65. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  66. package/dist/tests/core/agent-targets.test.js +45 -0
  67. package/dist/tests/core/artifact-state.test.js +144 -0
  68. package/dist/tests/core/atomic-file.test.js +96 -0
  69. package/dist/tests/core/bundle-install.test.js +208 -17
  70. package/dist/tests/core/bundles.test.js +12 -0
  71. package/dist/tests/core/context/managed-block.test.js +90 -0
  72. package/dist/tests/core/context/orchestrator.test.js +48 -3
  73. package/dist/tests/core/context/regenerate.test.js +1 -1
  74. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  75. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  76. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  77. package/dist/tests/core/diagnostics/context.test.js +74 -0
  78. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  79. package/dist/tests/core/executor.test.js +20 -0
  80. package/dist/tests/core/export/engine.test.js +104 -0
  81. package/dist/tests/core/export/pack.test.js +122 -0
  82. package/dist/tests/core/export/resolve.test.js +78 -0
  83. package/dist/tests/core/export/transform.test.js +74 -0
  84. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  85. package/dist/tests/core/init/orchestrator.test.js +1 -1
  86. package/dist/tests/core/init/provider-facts.test.js +129 -0
  87. package/dist/tests/core/init/steps.test.js +106 -2
  88. package/dist/tests/core/install-planner.test.js +224 -0
  89. package/dist/tests/core/install-transaction.test.js +257 -0
  90. package/dist/tests/core/provider-artifacts.test.js +64 -0
  91. package/dist/tests/core/provider-version.test.js +53 -0
  92. package/dist/tests/core/reconciliation.test.js +165 -0
  93. package/dist/tests/core/registries-sync.test.js +4 -4
  94. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  95. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  96. package/dist/tests/core/versioning.test.js +1 -1
  97. package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
  98. package/dist/tests/providers/hooks-config.test.js +43 -25
  99. package/dist/tests/providers/index.test.js +144 -39
  100. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  101. package/dist/tests/ui/provider-preflight.test.js +25 -0
  102. package/dist/tests/utils/config.test.js +178 -18
  103. package/package.json +1 -1
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ // cli/src/commands/hooks/codex.ts
3
+ //
4
+ // Codex hook adapter: merges a single AWM SessionStart entry into
5
+ // ~/.codex/hooks.json. Unlike Claude's wrapper-script model, the Codex
6
+ // SessionStart entry invokes the installed script directly (no run-hook.cmd
7
+ // indirection, no bootstrap-skill symlink).
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.awmCodexEntry = awmCodexEntry;
13
+ exports.installCodexHook = installCodexHook;
14
+ exports.computeCodexHookStatus = computeCodexHookStatus;
15
+ exports.uninstallCodexHook = uninstallCodexHook;
16
+ exports.resyncCodexHookFiles = resyncCodexHookFiles;
17
+ exports.codexResyncSourcesExist = codexResyncSourcesExist;
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const crypto_1 = __importDefault(require("crypto"));
21
+ const providers_1 = require("../../providers");
22
+ const shared_1 = require("./shared");
23
+ const atomic_file_1 = require("../../core/atomic-file");
24
+ function codexMatcher() {
25
+ const config = (0, providers_1.getHookConfig)('codex');
26
+ if (!config || config.type !== 'codex-hooks-json') {
27
+ throw new Error('Codex hook configuration is unavailable');
28
+ }
29
+ return config.matcher;
30
+ }
31
+ function awmCodexEntry(scriptsDir) {
32
+ return {
33
+ matcher: codexMatcher(),
34
+ hooks: [{
35
+ type: 'command',
36
+ command: path_1.default.join(scriptsDir, 'session-start'),
37
+ statusMessage: 'Loading AWM session state',
38
+ }],
39
+ };
40
+ }
41
+ function isAwmCodexEntry(entry, scriptsDir) {
42
+ return (entry?.matcher === codexMatcher() &&
43
+ Array.isArray(entry?.hooks) &&
44
+ entry.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(scriptsDir)));
45
+ }
46
+ function installCodexHook(options) {
47
+ const config = (0, providers_1.getHookConfig)('codex');
48
+ if (!config || config.type !== 'codex-hooks-json') {
49
+ throw new Error('Codex hook configuration is unavailable');
50
+ }
51
+ const source = path_1.default.join(options.registryRoot, 'hooks/codex-session-start');
52
+ if (!fs_1.default.existsSync(source)) {
53
+ throw new Error(`Codex hook source missing: ${source}. Run 'awm update' first.`);
54
+ }
55
+ const current = (0, shared_1.readStrictJson)(config.settingsPath);
56
+ const hooks = current.hooks && typeof current.hooks === 'object'
57
+ ? current.hooks
58
+ : {};
59
+ const entries = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
60
+ const matches = entries.filter((entry) => isAwmCodexEntry(entry, config.scriptsDir));
61
+ if (matches.length > 1) {
62
+ throw new Error('multiple AWM SessionStart entries in Codex hooks.json');
63
+ }
64
+ const newEntry = awmCodexEntry(config.scriptsDir);
65
+ if (matches.length === 1 && JSON.stringify(matches[0]) === JSON.stringify(newEntry)) {
66
+ return { status: 'already-up-to-date', scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath: null };
67
+ }
68
+ const nextEntries = matches.length === 1
69
+ ? entries.map((entry) => isAwmCodexEntry(entry, config.scriptsDir) ? newEntry : entry)
70
+ : [...entries, newEntry];
71
+ const merged = { ...current, hooks: { ...hooks, SessionStart: nextEntries } };
72
+ const backupPath = (0, shared_1.backupManagedFile)(config.settingsPath);
73
+ (0, shared_1.syncExecutable)(source, path_1.default.join(config.scriptsDir, 'session-start'), options.installMethod);
74
+ (0, atomic_file_1.writeFileAtomic)(config.settingsPath, JSON.stringify(merged, null, 2) + '\n');
75
+ return { status: 'installed', scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath };
76
+ }
77
+ function checkCodexSettingsEntry(settingsPath, scriptsDir, matcher, eventName) {
78
+ if (!fs_1.default.existsSync(settingsPath)) {
79
+ return { ok: false, detail: `hooks.json not found: ${settingsPath}` };
80
+ }
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
84
+ }
85
+ catch {
86
+ return { ok: false, detail: 'hooks.json is not valid JSON' };
87
+ }
88
+ const entries = parsed?.hooks?.[eventName] ?? [];
89
+ const awmEntry = entries.find((e) => e?.matcher === matcher &&
90
+ (e?.hooks ?? []).some((h) => typeof h?.command === 'string' && h.command.includes(scriptsDir)));
91
+ if (!awmEntry) {
92
+ return { ok: false, detail: `no AWM SessionStart entry in ${settingsPath}` };
93
+ }
94
+ return { ok: true, detail: settingsPath };
95
+ }
96
+ function hashFile(file) {
97
+ return crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(file)).digest('hex');
98
+ }
99
+ /**
100
+ * Trust derives from ~/.awm/hooks/codex/heartbeat.json, a small file the
101
+ * running session-start hook is expected to write (out of scope for this
102
+ * task — we only read it here). No heartbeat yet => pending-trust (hook
103
+ * installed but never confirmed running). Heartbeat present but its stored
104
+ * hash doesn't match the currently installed script => stale (resynced or
105
+ * tampered with since the last confirmed run). Hash matches => healthy.
106
+ */
107
+ function computeCodexTrust(scriptCheck, scriptPath, heartbeatPath) {
108
+ if (!scriptCheck.ok)
109
+ return undefined;
110
+ if (!fs_1.default.existsSync(heartbeatPath))
111
+ return 'pending-trust';
112
+ let heartbeat;
113
+ try {
114
+ heartbeat = JSON.parse(fs_1.default.readFileSync(heartbeatPath, 'utf-8'));
115
+ }
116
+ catch {
117
+ return 'stale';
118
+ }
119
+ return heartbeat?.hash === hashFile(scriptPath) ? 'healthy' : 'stale';
120
+ }
121
+ function computeCodexHookStatus(agent) {
122
+ const config = (0, providers_1.getHookConfig)(agent);
123
+ if (!config || config.type !== 'codex-hooks-json') {
124
+ throw new Error('Codex hook configuration is unavailable');
125
+ }
126
+ const scriptPath = path_1.default.join(config.scriptsDir, 'session-start');
127
+ const heartbeatPath = path_1.default.join(config.scriptsDir, 'heartbeat.json');
128
+ const sessionStartScript = (0, shared_1.checkExecutable)(scriptPath);
129
+ const settingsEntry = checkCodexSettingsEntry(config.settingsPath, config.scriptsDir, config.matcher, config.eventName);
130
+ const allOk = sessionStartScript.ok && settingsEntry.ok;
131
+ const settingsOnlyMissing = !settingsEntry.ok && sessionStartScript.ok;
132
+ let overall;
133
+ if (allOk)
134
+ overall = 'HEALTHY';
135
+ else if (settingsOnlyMissing)
136
+ overall = 'NOT_INSTALLED';
137
+ else
138
+ overall = 'DEGRADED';
139
+ // Trust only makes sense once the AWM entry is actually present in
140
+ // hooks.json — otherwise there's nothing installed to trust.
141
+ const trust = settingsEntry.ok
142
+ ? computeCodexTrust(sessionStartScript, scriptPath, heartbeatPath)
143
+ : undefined;
144
+ return {
145
+ overall,
146
+ trust,
147
+ checks: { sessionStartScript, settingsEntry },
148
+ };
149
+ }
150
+ function uninstallCodexHook(agent) {
151
+ const config = (0, providers_1.getHookConfig)(agent);
152
+ if (!config || config.type !== 'codex-hooks-json') {
153
+ throw new Error('Codex hook configuration is unavailable');
154
+ }
155
+ if (!fs_1.default.existsSync(config.settingsPath)) {
156
+ return { status: 'not-installed', backupPath: null };
157
+ }
158
+ const current = (0, shared_1.readStrictJson)(config.settingsPath);
159
+ const hooks = current.hooks && typeof current.hooks === 'object'
160
+ ? current.hooks
161
+ : {};
162
+ const entries = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
163
+ const beforeLength = entries.length;
164
+ const filtered = entries.filter((e) => !isAwmCodexEntry(e, config.scriptsDir));
165
+ if (filtered.length === beforeLength) {
166
+ return { status: 'not-installed', backupPath: null };
167
+ }
168
+ const backupPath = (0, shared_1.backupManagedFile)(config.settingsPath);
169
+ const nextHooks = { ...hooks };
170
+ if (filtered.length === 0) {
171
+ delete nextHooks.SessionStart;
172
+ }
173
+ else {
174
+ nextHooks.SessionStart = filtered;
175
+ }
176
+ const nextConfig = { ...current, hooks: nextHooks };
177
+ if (Object.keys(nextHooks).length === 0) {
178
+ delete nextConfig.hooks;
179
+ }
180
+ (0, atomic_file_1.writeFileAtomic)(config.settingsPath, JSON.stringify(nextConfig, null, 2) + '\n');
181
+ return { status: 'uninstalled', backupPath };
182
+ }
183
+ /** Refresh the Codex hook's script file in place (used by resync). Assumes the caller has verified the settings entry is already present. */
184
+ function resyncCodexHookFiles(config, registryRoot, method) {
185
+ const source = path_1.default.join(registryRoot, 'hooks/codex-session-start');
186
+ (0, shared_1.syncExecutable)(source, path_1.default.join(config.scriptsDir, 'session-start'), method);
187
+ }
188
+ /** True when the registry has everything needed to resync the Codex hook files. */
189
+ function codexResyncSourcesExist(registryRoot) {
190
+ return fs_1.default.existsSync(path_1.default.join(registryRoot, 'hooks/codex-session-start'));
191
+ }
@@ -10,12 +10,17 @@ const config_1 = require("../../utils/config");
10
10
  const install_1 = require("./install");
11
11
  const uninstall_1 = require("./uninstall");
12
12
  const status_1 = require("./status");
13
+ const providers_1 = require("../../providers");
13
14
  const registries_1 = require("../../core/registries");
15
+ const HOOK_TARGETS = providers_1.AGENT_TARGETS.filter((a) => (0, providers_1.getHookConfig)(a));
16
+ function targetOptionDescription() {
17
+ return `Target harness (${HOOK_TARGETS.join('|')})`;
18
+ }
14
19
  function registerHooksCommand(program) {
15
20
  const hooks = program.command('hooks').description('Manage SessionStart bootstrap hooks');
16
21
  hooks.command('install')
17
22
  .description('Install the AWM bootstrap hook into the target harness')
18
- .option('-t, --target <target>', 'Target harness (claude-code only in this version)', 'claude-code')
23
+ .option('-t, --target <target>', targetOptionDescription(), 'claude-code')
19
24
  .option('-y, --yes', 'Skip interactive confirmations', false)
20
25
  .action(async (options) => {
21
26
  const agent = (options.target ?? 'claude-code');
@@ -39,8 +44,10 @@ function registerHooksCommand(program) {
39
44
  console.log(picocolors_1.default.green('✓ AWM bootstrap hook installed.'));
40
45
  console.log('');
41
46
  console.log(` Scripts: ${result.scriptsDir}/session-start`);
42
- console.log(` ${result.scriptsDir}/run-hook.cmd`);
43
- console.log(` ${result.scriptsDir}/using-awm.md → ~/.awm/registries/baseline/skills/using-awm/SKILL.md`);
47
+ if (agent === 'claude-code') {
48
+ console.log(` ${result.scriptsDir}/run-hook.cmd`);
49
+ console.log(` ${result.scriptsDir}/using-awm.md → ~/.awm/registries/baseline/skills/using-awm/SKILL.md`);
50
+ }
44
51
  console.log('');
45
52
  console.log(` Settings file: ${result.settingsPath}`);
46
53
  if (result.backupPath) {
@@ -52,7 +59,7 @@ function registerHooksCommand(program) {
52
59
  console.log(` Verify: ${picocolors_1.default.cyan('awm hooks status')}`);
53
60
  console.log(` Remove: ${picocolors_1.default.cyan('awm hooks uninstall')}`);
54
61
  console.log('');
55
- console.log(picocolors_1.default.yellow(' ⚠ Restart Claude Code to activate the hook in existing sessions.'));
62
+ console.log(picocolors_1.default.yellow(` ⚠ Restart ${(0, providers_1.providerFor)(agent).label} to activate the hook in existing sessions.`));
56
63
  }
57
64
  catch (e) {
58
65
  console.error(picocolors_1.default.red(`✗ ${e.message}`));
@@ -61,7 +68,7 @@ function registerHooksCommand(program) {
61
68
  });
62
69
  hooks.command('uninstall')
63
70
  .description('Remove the AWM bootstrap hook')
64
- .option('-t, --target <target>', 'Target harness (claude-code only in this version)', 'claude-code')
71
+ .option('-t, --target <target>', targetOptionDescription(), 'claude-code')
65
72
  .option('-y, --yes', 'Skip interactive confirmations', false)
66
73
  .action(async (options) => {
67
74
  const agent = (options.target ?? 'claude-code');
@@ -90,17 +97,25 @@ function registerHooksCommand(program) {
90
97
  });
91
98
  hooks.command('status')
92
99
  .description('Check the bootstrap hook installation status')
93
- .option('-t, --target <target>', 'Target harness (claude-code only in this version)', 'claude-code')
100
+ .option('-t, --target <target>', targetOptionDescription(), 'claude-code')
94
101
  .action((options) => {
95
102
  const agent = (options.target ?? 'claude-code');
96
103
  try {
97
104
  const result = (0, status_1.computeHookStatus)(agent);
98
105
  const symbol = (ok) => ok ? picocolors_1.default.green('✓') : picocolors_1.default.red('✗');
99
106
  console.log('');
100
- console.log(` Bootstrap skill: ${symbol(result.checks.bootstrapSkill.ok)} ${result.checks.bootstrapSkill.detail}`);
107
+ if (result.checks.bootstrapSkill) {
108
+ console.log(` Bootstrap skill: ${symbol(result.checks.bootstrapSkill.ok)} ${result.checks.bootstrapSkill.detail}`);
109
+ }
101
110
  console.log(` Session-start: ${symbol(result.checks.sessionStartScript.ok)} ${result.checks.sessionStartScript.detail}`);
102
- console.log(` Run-hook wrapper: ${symbol(result.checks.runHookWrapper.ok)} ${result.checks.runHookWrapper.detail}`);
111
+ if (result.checks.runHookWrapper) {
112
+ console.log(` Run-hook wrapper: ${symbol(result.checks.runHookWrapper.ok)} ${result.checks.runHookWrapper.detail}`);
113
+ }
103
114
  console.log(` Settings entry: ${symbol(result.checks.settingsEntry.ok)} ${result.checks.settingsEntry.detail}`);
115
+ if (result.trust) {
116
+ const trustSymbol = result.trust === 'healthy' ? picocolors_1.default.green('✓') : result.trust === 'stale' ? picocolors_1.default.red('✗') : picocolors_1.default.yellow('…');
117
+ console.log(` Trust: ${trustSymbol} ${result.trust}`);
118
+ }
104
119
  console.log('');
105
120
  const overall = result.overall === 'HEALTHY' ? picocolors_1.default.green(result.overall) :
106
121
  result.overall === 'NOT_INSTALLED' ? picocolors_1.default.yellow(result.overall) :
@@ -1,119 +1,23 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.syncFile = syncFile;
7
3
  exports.installHook = installHook;
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
4
  const providers_1 = require("../../providers");
11
- const paths_1 = require("../../core/paths");
12
- function syncFile(source, dest, method) {
13
- try {
14
- fs_1.default.unlinkSync(dest);
15
- }
16
- catch { /* not exists, fine */ }
17
- fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
18
- if (method === 'symlink') {
19
- fs_1.default.symlinkSync(source, dest);
20
- }
21
- else {
22
- fs_1.default.copyFileSync(source, dest);
23
- const srcMode = fs_1.default.statSync(source).mode;
24
- fs_1.default.chmodSync(dest, srcMode);
25
- }
26
- }
27
- function backupSettings(settingsPath) {
28
- if (!fs_1.default.existsSync(settingsPath))
29
- return null;
30
- const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
31
- fs_1.default.mkdirSync(backupDir, { recursive: true });
32
- const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
33
- const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.bak`);
34
- fs_1.default.copyFileSync(settingsPath, backupPath);
35
- return backupPath;
36
- }
37
- function isAwmEntry(entry, scriptsDir, matcher) {
38
- return (entry?.matcher === matcher &&
39
- Array.isArray(entry?.hooks) &&
40
- entry.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(scriptsDir)));
41
- }
5
+ const claude_1 = require("./claude");
6
+ const codex_1 = require("./codex");
42
7
  function installHook(options) {
43
8
  const config = (0, providers_1.getHookConfig)(options.agent);
44
9
  if (!config) {
45
10
  throw new Error(`hooks not supported for agent target: ${options.agent}`);
46
11
  }
47
- // 1. Verify registry sources exist FIRST (before touching settings)
48
- const sourceHooks = path_1.default.join(options.registryRoot, 'hooks');
49
- const sourceSkill = path_1.default.join(options.registryRoot, 'skills/using-awm/SKILL.md');
50
- if (!fs_1.default.existsSync(path_1.default.join(sourceHooks, 'session-start'))) {
51
- throw new Error(`AWM registry not found at ${sourceHooks}. Run 'awm update' to refresh the registry.`);
52
- }
53
- if (!fs_1.default.existsSync(sourceSkill)) {
54
- throw new Error(`using-awm skill not found at ${sourceSkill}. Run 'awm update' first.`);
55
- }
56
- // 2. Sync scripts
57
- fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
58
- syncFile(path_1.default.join(sourceHooks, 'session-start'), path_1.default.join(config.scriptsDir, 'session-start'), options.installMethod);
59
- syncFile(path_1.default.join(sourceHooks, 'run-hook.cmd'), path_1.default.join(config.scriptsDir, 'run-hook.cmd'), options.installMethod);
60
- // 3. Link the skill (default: symlink so 'awm update' propagates; fall back to copy if symlink is unavailable, e.g. Windows without Developer Mode)
61
- const skillDest = path_1.default.join(config.scriptsDir, 'using-awm.md');
62
- try {
63
- fs_1.default.unlinkSync(skillDest);
64
- }
65
- catch { /* not exists */ }
66
- try {
67
- fs_1.default.symlinkSync(sourceSkill, skillDest);
68
- }
69
- catch {
70
- // best-effort: copy the single skill file; 'awm update' will not auto-propagate
71
- fs_1.default.copyFileSync(sourceSkill, skillDest);
72
- }
73
- // 4. Backup settings if it exists
74
- const backupPath = backupSettings(config.settingsPath);
75
- // 5. Read or initialize settings
76
- let settings = {};
77
- if (fs_1.default.existsSync(config.settingsPath)) {
78
- const raw = fs_1.default.readFileSync(config.settingsPath, 'utf-8');
79
- try {
80
- settings = JSON.parse(raw);
81
- }
82
- catch {
83
- throw new Error(`${config.settingsPath} is not valid JSON. Backup created at ${backupPath}. Fix the file manually, then re-run.`);
12
+ switch (config.type) {
13
+ case 'cc-settings-merge':
14
+ return (0, claude_1.installClaudeHook)(options);
15
+ case 'codex-hooks-json':
16
+ return (0, codex_1.installCodexHook)(options);
17
+ /* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
18
+ default: {
19
+ const exhaustive = config.type;
20
+ throw new Error(`Unknown hook config type: ${String(exhaustive)}`);
84
21
  }
85
22
  }
86
- else {
87
- fs_1.default.mkdirSync(path_1.default.dirname(config.settingsPath), { recursive: true });
88
- }
89
- // 6. Merge AWM entry
90
- if (!settings.hooks)
91
- settings.hooks = {};
92
- if (!settings.hooks[config.eventName])
93
- settings.hooks[config.eventName] = [];
94
- const entries = settings.hooks[config.eventName];
95
- const awmEntryIdx = entries.findIndex((e) => isAwmEntry(e, config.scriptsDir, config.matcher));
96
- const newEntry = {
97
- matcher: config.matcher,
98
- hooks: [{
99
- type: 'command',
100
- command: `${path_1.default.join(config.scriptsDir, 'run-hook.cmd')} session-start`,
101
- async: false
102
- }]
103
- };
104
- let status;
105
- if (awmEntryIdx >= 0) {
106
- if (JSON.stringify(entries[awmEntryIdx]) === JSON.stringify(newEntry)) {
107
- return { status: 'already-up-to-date', scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath: null };
108
- }
109
- entries[awmEntryIdx] = newEntry;
110
- status = 'installed';
111
- }
112
- else {
113
- entries.push(newEntry);
114
- status = 'installed';
115
- }
116
- // 7. Write settings
117
- fs_1.default.writeFileSync(config.settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
118
- return { status, scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath };
119
23
  }
@@ -8,7 +8,8 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const providers_1 = require("../../providers");
10
10
  const status_1 = require("./status");
11
- const install_1 = require("./install");
11
+ const claude_1 = require("./claude");
12
+ const codex_1 = require("./codex");
12
13
  function detectInstallMethod(scriptsDir) {
13
14
  try {
14
15
  return fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink() ? 'symlink' : 'copy';
@@ -17,9 +18,9 @@ function detectInstallMethod(scriptsDir) {
17
18
  return 'copy';
18
19
  }
19
20
  }
20
- function resyncInstalledHooks(registryRoot) {
21
+ function resyncInstalledHooks(registryRoot, targets = [...providers_1.AGENT_TARGETS]) {
21
22
  const results = [];
22
- for (const agent of Object.keys(providers_1.PROVIDERS)) {
23
+ for (const agent of targets) {
23
24
  const config = (0, providers_1.getHookConfig)(agent);
24
25
  if (!config)
25
26
  continue;
@@ -28,23 +29,32 @@ function resyncInstalledHooks(registryRoot) {
28
29
  results.push({ agent, action: 'not-installed' });
29
30
  continue;
30
31
  }
31
- const sourceHooks = path_1.default.join(registryRoot, 'hooks');
32
- const sourceSkill = path_1.default.join(registryRoot, 'skills/using-awm/SKILL.md');
33
- if (!fs_1.default.existsSync(path_1.default.join(sourceHooks, 'session-start')) || !fs_1.default.existsSync(path_1.default.join(sourceHooks, 'run-hook.cmd')) || !fs_1.default.existsSync(sourceSkill)) {
34
- results.push({ agent, action: 'registry-missing' });
35
- continue;
36
- }
37
32
  const method = detectInstallMethod(config.scriptsDir);
38
- fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
39
- (0, install_1.syncFile)(path_1.default.join(sourceHooks, 'session-start'), path_1.default.join(config.scriptsDir, 'session-start'), method);
40
- (0, install_1.syncFile)(path_1.default.join(sourceHooks, 'run-hook.cmd'), path_1.default.join(config.scriptsDir, 'run-hook.cmd'), method);
41
- const skillDest = path_1.default.join(config.scriptsDir, 'using-awm.md');
42
- try {
43
- fs_1.default.unlinkSync(skillDest);
33
+ switch (config.type) {
34
+ case 'cc-settings-merge': {
35
+ if (!(0, claude_1.claudeResyncSourcesExist)(registryRoot)) {
36
+ results.push({ agent, action: 'registry-missing' });
37
+ continue;
38
+ }
39
+ (0, claude_1.resyncClaudeHookFiles)(config, registryRoot, method);
40
+ results.push({ agent, action: 'resynced' });
41
+ break;
42
+ }
43
+ case 'codex-hooks-json': {
44
+ if (!(0, codex_1.codexResyncSourcesExist)(registryRoot)) {
45
+ results.push({ agent, action: 'registry-missing' });
46
+ continue;
47
+ }
48
+ (0, codex_1.resyncCodexHookFiles)(config, registryRoot, method);
49
+ results.push({ agent, action: 'resynced' });
50
+ break;
51
+ }
52
+ /* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
53
+ default: {
54
+ const exhaustive = config.type;
55
+ throw new Error(`Unknown hook config type: ${String(exhaustive)}`);
56
+ }
44
57
  }
45
- catch { /* not exists */ }
46
- fs_1.default.symlinkSync(sourceSkill, skillDest);
47
- results.push({ agent, action: 'resynced' });
48
58
  }
49
59
  return results;
50
60
  }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ // cli/src/commands/hooks/shared.ts
3
+ //
4
+ // Small helpers shared between the per-agent hook adapters (claude.ts, codex.ts).
5
+ // Each adapter owns its own entry shape / merge semantics; this module only
6
+ // generalizes the parts that are identical regardless of which JSON file or
7
+ // which single script is being managed.
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.syncExecutable = syncExecutable;
13
+ exports.backupManagedFile = backupManagedFile;
14
+ exports.readStrictJson = readStrictJson;
15
+ exports.checkExecutable = checkExecutable;
16
+ exports.checkFile = checkFile;
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const paths_1 = require("../../core/paths");
20
+ /**
21
+ * Sync a single managed file (script, wrapper, skill doc, ...) from `source`
22
+ * to `dest` via symlink (default; lets `awm update` propagate registry
23
+ * changes) or copy (fallback for platforms without symlink support).
24
+ */
25
+ function syncExecutable(source, dest, method) {
26
+ try {
27
+ fs_1.default.unlinkSync(dest);
28
+ }
29
+ catch { /* not exists, fine */ }
30
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
31
+ if (method === 'symlink') {
32
+ fs_1.default.symlinkSync(source, dest);
33
+ }
34
+ else {
35
+ fs_1.default.copyFileSync(source, dest);
36
+ const srcMode = fs_1.default.statSync(source).mode;
37
+ fs_1.default.chmodSync(dest, srcMode);
38
+ }
39
+ }
40
+ /**
41
+ * Backup any AWM-managed JSON config file (Claude's settings.json, Codex's
42
+ * hooks.json, ...) before mutating it. Returns null if the file doesn't
43
+ * exist yet (nothing to back up).
44
+ */
45
+ function backupManagedFile(filePath) {
46
+ if (!fs_1.default.existsSync(filePath))
47
+ return null;
48
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
49
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
50
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
51
+ const backupPath = path_1.default.join(backupDir, `${path_1.default.basename(filePath)}.${ts}.bak`);
52
+ fs_1.default.copyFileSync(filePath, backupPath);
53
+ return backupPath;
54
+ }
55
+ /**
56
+ * Parse a JSON config file. Missing file => {} (nothing configured yet).
57
+ * Malformed JSON => throws a clear, actionable error instead of silently
58
+ * clobbering whatever the user (or another tool) put there.
59
+ */
60
+ function readStrictJson(filePath) {
61
+ if (!fs_1.default.existsSync(filePath))
62
+ return {};
63
+ const raw = fs_1.default.readFileSync(filePath, 'utf-8');
64
+ try {
65
+ return JSON.parse(raw);
66
+ }
67
+ catch {
68
+ throw new Error(`${filePath} is not valid JSON. Fix the file manually, then re-run.`);
69
+ }
70
+ }
71
+ function checkExecutable(file) {
72
+ if (!fs_1.default.existsSync(file)) {
73
+ return { ok: false, detail: `missing: ${file}` };
74
+ }
75
+ try {
76
+ fs_1.default.accessSync(file, fs_1.default.constants.X_OK);
77
+ return { ok: true, detail: file };
78
+ }
79
+ catch {
80
+ return { ok: false, detail: `not executable: ${file}` };
81
+ }
82
+ }
83
+ function checkFile(file) {
84
+ if (!fs_1.default.existsSync(file)) {
85
+ return { ok: false, detail: `missing: ${file}` };
86
+ }
87
+ try {
88
+ fs_1.default.statSync(file);
89
+ return { ok: true, detail: file };
90
+ }
91
+ catch {
92
+ return { ok: false, detail: `broken link: ${file}` };
93
+ }
94
+ }
@@ -1,74 +1,23 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.computeHookStatus = computeHookStatus;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
4
  const providers_1 = require("../../providers");
10
- function checkExecutable(file) {
11
- if (!fs_1.default.existsSync(file)) {
12
- return { ok: false, detail: `missing: ${file}` };
13
- }
14
- try {
15
- fs_1.default.accessSync(file, fs_1.default.constants.X_OK);
16
- return { ok: true, detail: file };
17
- }
18
- catch {
19
- return { ok: false, detail: `not executable: ${file}` };
20
- }
21
- }
22
- function checkFile(file) {
23
- if (!fs_1.default.existsSync(file)) {
24
- return { ok: false, detail: `missing: ${file}` };
25
- }
26
- try {
27
- fs_1.default.statSync(file);
28
- return { ok: true, detail: file };
29
- }
30
- catch {
31
- return { ok: false, detail: `broken link: ${file}` };
32
- }
33
- }
34
- function checkSettingsEntry(settingsPath, scriptsDir, matcher, eventName) {
35
- if (!fs_1.default.existsSync(settingsPath)) {
36
- return { ok: false, detail: `settings.json not found: ${settingsPath}` };
37
- }
38
- let parsed;
39
- try {
40
- parsed = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
41
- }
42
- catch {
43
- return { ok: false, detail: 'settings.json is not valid JSON' };
44
- }
45
- const entries = parsed?.hooks?.[eventName] ?? [];
46
- const awmEntry = entries.find((e) => e?.matcher === matcher &&
47
- (e?.hooks ?? []).some((h) => typeof h?.command === 'string' && h.command.includes(scriptsDir)));
48
- if (!awmEntry) {
49
- return { ok: false, detail: `no AWM SessionStart entry in ${settingsPath}` };
50
- }
51
- return { ok: true, detail: settingsPath };
52
- }
5
+ const claude_1 = require("./claude");
6
+ const codex_1 = require("./codex");
53
7
  function computeHookStatus(agent) {
54
8
  const config = (0, providers_1.getHookConfig)(agent);
55
9
  if (!config) {
56
10
  throw new Error(`hooks not supported for agent target: ${agent}`);
57
11
  }
58
- const checks = {
59
- bootstrapSkill: checkFile(path_1.default.join(config.scriptsDir, 'using-awm.md')),
60
- sessionStartScript: checkExecutable(path_1.default.join(config.scriptsDir, 'session-start')),
61
- runHookWrapper: checkExecutable(path_1.default.join(config.scriptsDir, 'run-hook.cmd')),
62
- settingsEntry: checkSettingsEntry(config.settingsPath, config.scriptsDir, config.matcher, config.eventName)
63
- };
64
- const allOk = Object.values(checks).every((c) => c.ok);
65
- const settingsOnlyMissing = !checks.settingsEntry.ok && checks.bootstrapSkill.ok && checks.sessionStartScript.ok && checks.runHookWrapper.ok;
66
- let overall;
67
- if (allOk)
68
- overall = 'HEALTHY';
69
- else if (settingsOnlyMissing)
70
- overall = 'NOT_INSTALLED';
71
- else
72
- overall = 'DEGRADED';
73
- return { overall, checks };
12
+ switch (config.type) {
13
+ case 'cc-settings-merge':
14
+ return (0, claude_1.computeClaudeHookStatus)(agent);
15
+ case 'codex-hooks-json':
16
+ return (0, codex_1.computeCodexHookStatus)(agent);
17
+ /* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
18
+ default: {
19
+ const exhaustive = config.type;
20
+ throw new Error(`Unknown hook config type: ${String(exhaustive)}`);
21
+ }
22
+ }
74
23
  }