agentic-workflow-manager 3.1.0 → 3.2.1

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 (92) 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/hooks/claude.js +192 -0
  6. package/dist/src/commands/hooks/codex.js +191 -0
  7. package/dist/src/commands/hooks/index.js +23 -8
  8. package/dist/src/commands/hooks/install.js +11 -107
  9. package/dist/src/commands/hooks/resync.js +28 -18
  10. package/dist/src/commands/hooks/shared.js +94 -0
  11. package/dist/src/commands/hooks/status.js +13 -64
  12. package/dist/src/commands/hooks/uninstall.js +11 -47
  13. package/dist/src/commands/init.js +90 -28
  14. package/dist/src/commands/registry/index.js +1 -1
  15. package/dist/src/commands/sync.js +113 -0
  16. package/dist/src/commands/update.js +107 -0
  17. package/dist/src/core/agent-targets.js +50 -0
  18. package/dist/src/core/artifact-state.js +65 -0
  19. package/dist/src/core/atomic-file.js +72 -0
  20. package/dist/src/core/bundle-install.js +60 -38
  21. package/dist/src/core/bundles.js +21 -0
  22. package/dist/src/core/context/managed-block.js +190 -0
  23. package/dist/src/core/context/orchestrator.js +3 -1
  24. package/dist/src/core/context/project-constitution-inject.js +1 -1
  25. package/dist/src/core/context/regenerate.js +3 -3
  26. package/dist/src/core/context/strategies/codex-agents.js +100 -0
  27. package/dist/src/core/diagnostics/checks.js +16 -0
  28. package/dist/src/core/diagnostics/context.js +37 -8
  29. package/dist/src/core/diagnostics/provider-checks.js +219 -0
  30. package/dist/src/core/executor.js +30 -9
  31. package/dist/src/core/init/mutation-targets.js +139 -0
  32. package/dist/src/core/init/provider-facts.js +179 -0
  33. package/dist/src/core/init/steps.js +65 -9
  34. package/dist/src/core/install-planner.js +206 -0
  35. package/dist/src/core/install-transaction.js +405 -0
  36. package/dist/src/core/profile.js +1 -1
  37. package/dist/src/core/provider-artifacts.js +54 -0
  38. package/dist/src/core/provider-version.js +36 -0
  39. package/dist/src/core/reconciliation.js +61 -0
  40. package/dist/src/core/registries.js +10 -0
  41. package/dist/src/core/renderers/canonical-agent.js +27 -0
  42. package/dist/src/core/renderers/codex-agent.js +60 -0
  43. package/dist/src/core/skill-integrity.js +4 -3
  44. package/dist/src/index.js +97 -289
  45. package/dist/src/providers/index.js +143 -39
  46. package/dist/src/ui/provider-preflight.js +26 -0
  47. package/dist/src/utils/config.js +101 -9
  48. package/dist/tests/commands/agent.test.js +103 -0
  49. package/dist/tests/commands/backup.test.js +109 -0
  50. package/dist/tests/commands/doctor.test.js +65 -4
  51. package/dist/tests/commands/hooks/codex.test.js +230 -0
  52. package/dist/tests/commands/hooks/install.test.js +20 -1
  53. package/dist/tests/commands/hooks/resync.test.js +53 -6
  54. package/dist/tests/commands/hooks/status.test.js +10 -0
  55. package/dist/tests/commands/hooks/uninstall.test.js +12 -0
  56. package/dist/tests/commands/init.test.js +195 -8
  57. package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
  58. package/dist/tests/core/agent-targets.test.js +45 -0
  59. package/dist/tests/core/artifact-state.test.js +144 -0
  60. package/dist/tests/core/atomic-file.test.js +96 -0
  61. package/dist/tests/core/bundle-install.test.js +208 -17
  62. package/dist/tests/core/bundles.test.js +12 -0
  63. package/dist/tests/core/context/managed-block.test.js +90 -0
  64. package/dist/tests/core/context/orchestrator.test.js +48 -3
  65. package/dist/tests/core/context/regenerate.test.js +1 -1
  66. package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
  67. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  68. package/dist/tests/core/diagnostics/checks.test.js +19 -0
  69. package/dist/tests/core/diagnostics/context.test.js +74 -0
  70. package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
  71. package/dist/tests/core/executor.test.js +20 -0
  72. package/dist/tests/core/export/engine.test.js +3 -2
  73. package/dist/tests/core/init/mutation-targets.test.js +235 -0
  74. package/dist/tests/core/init/orchestrator.test.js +1 -1
  75. package/dist/tests/core/init/provider-facts.test.js +177 -0
  76. package/dist/tests/core/init/steps.test.js +106 -2
  77. package/dist/tests/core/install-planner.test.js +224 -0
  78. package/dist/tests/core/install-transaction.test.js +257 -0
  79. package/dist/tests/core/provider-artifacts.test.js +64 -0
  80. package/dist/tests/core/provider-version.test.js +53 -0
  81. package/dist/tests/core/reconciliation.test.js +165 -0
  82. package/dist/tests/core/registries-sync.test.js +4 -4
  83. package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
  84. package/dist/tests/core/renderers/codex-agent.test.js +128 -0
  85. package/dist/tests/core/versioning.test.js +1 -1
  86. package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
  87. package/dist/tests/providers/hooks-config.test.js +43 -25
  88. package/dist/tests/providers/index.test.js +144 -39
  89. package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
  90. package/dist/tests/ui/provider-preflight.test.js +25 -0
  91. package/dist/tests/utils/config.test.js +178 -18
  92. package/package.json +1 -1
@@ -1,59 +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.uninstallHook = uninstallHook;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
4
  const providers_1 = require("../../providers");
10
- const paths_1 = require("../../core/paths");
11
- function backupSettings(settingsPath) {
12
- if (!fs_1.default.existsSync(settingsPath))
13
- return null;
14
- const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
15
- fs_1.default.mkdirSync(backupDir, { recursive: true });
16
- const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
17
- const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.bak`);
18
- fs_1.default.copyFileSync(settingsPath, backupPath);
19
- return backupPath;
20
- }
21
- function isAwmEntry(entry, scriptsDir, matcher) {
22
- return (entry?.matcher === matcher &&
23
- Array.isArray(entry?.hooks) &&
24
- entry.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(scriptsDir)));
25
- }
5
+ const claude_1 = require("./claude");
6
+ const codex_1 = require("./codex");
26
7
  function uninstallHook(options) {
27
8
  const config = (0, providers_1.getHookConfig)(options.agent);
28
9
  if (!config) {
29
10
  throw new Error(`hooks not supported for agent target: ${options.agent}`);
30
11
  }
31
- if (!fs_1.default.existsSync(config.settingsPath)) {
32
- return { status: 'not-installed', backupPath: null };
33
- }
34
- let settings;
35
- try {
36
- settings = JSON.parse(fs_1.default.readFileSync(config.settingsPath, 'utf-8'));
37
- }
38
- catch {
39
- throw new Error(`${config.settingsPath} is not valid JSON. Manual cleanup required.`);
40
- }
41
- const entries = settings?.hooks?.[config.eventName] ?? [];
42
- const beforeLength = entries.length;
43
- const filtered = entries.filter((e) => !isAwmEntry(e, config.scriptsDir, config.matcher));
44
- if (filtered.length === beforeLength) {
45
- return { status: 'not-installed', backupPath: null };
46
- }
47
- const backupPath = backupSettings(config.settingsPath);
48
- if (filtered.length === 0) {
49
- delete settings.hooks[config.eventName];
50
- if (Object.keys(settings.hooks).length === 0) {
51
- delete settings.hooks;
12
+ switch (config.type) {
13
+ case 'cc-settings-merge':
14
+ return (0, claude_1.uninstallClaudeHook)(options.agent);
15
+ case 'codex-hooks-json':
16
+ return (0, codex_1.uninstallCodexHook)(options.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)}`);
52
21
  }
53
22
  }
54
- else {
55
- settings.hooks[config.eventName] = filtered;
56
- }
57
- fs_1.default.writeFileSync(config.settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
58
- return { status: 'uninstalled', backupPath };
59
23
  }
@@ -49,6 +49,11 @@ const bundles_1 = require("../core/bundles");
49
49
  const registries_1 = require("../core/registries");
50
50
  const orchestrator_1 = require("../core/init/orchestrator");
51
51
  const steps_1 = require("../core/init/steps");
52
+ const mutation_targets_1 = require("../core/init/mutation-targets");
53
+ const provider_facts_1 = require("../core/init/provider-facts");
54
+ const install_transaction_1 = require("../core/install-transaction");
55
+ const provider_version_1 = require("../core/provider-version");
56
+ const providers_1 = require("../providers");
52
57
  const paths_1 = require("../core/paths");
53
58
  const config_1 = require("../utils/config");
54
59
  // ---------------------------------------------------------------------------
@@ -91,44 +96,101 @@ function renderInitOutcome(o) {
91
96
  }
92
97
  async function runInit(opts = {}) {
93
98
  const cwd = opts.cwd ?? process.cwd();
94
- const agent = opts.agent ?? 'claude-code';
95
- // #7: make init the source of truth for the default agent. Persist the resolved
96
- // agent so later `awm add`/`awm sync` (which read preferences.defaultAgent) target
97
- // the right agent instead of stamping the static default. Do NOT clobber an existing
98
- // explicit preference on a bare re-init: only write when an agent was passed via -a,
99
- // or when no preferences file exists yet.
100
- if (opts.agent != null || !(0, config_1.preferencesExist)()) {
101
- (0, config_1.savePreferences)({ ...(0, config_1.getPreferences)(), defaultAgent: agent });
99
+ const agent = opts.agent === undefined ? 'claude-code' : (0, providers_1.requireAgentTarget)(opts.agent);
100
+ // R2: gate BEFORE anything is read or written an unsupported provider
101
+ // version must never touch preferences.json or any provider file.
102
+ const gate = opts.assertProviderSupported ?? provider_version_1.assertProviderSupported;
103
+ try {
104
+ gate(agent);
105
+ }
106
+ catch (error) {
107
+ process.stderr.write(`awm init: ${error.message}\n`);
108
+ return 2;
102
109
  }
110
+ // #7 / R11: make init the source of truth for the default agent, but never
111
+ // clobber an existing explicit default on a bare re-init (`opts.agent`
112
+ // undefined) — only ENABLE the resolved agent into enabledAgents. On a
113
+ // machine with no preferences yet, `loadPreferences(agent)` proposes
114
+ // `{ defaultAgent: agent, enabledAgents: [agent], ... }` as the seed.
115
+ const loaded = (0, config_1.loadPreferences)(agent);
116
+ const nextPreferences = opts.agent === undefined
117
+ ? loaded.prefs
118
+ : (0, config_1.enableAgent)(loaded.prefs, agent);
119
+ // R19: Claude's baseline is read-only from every OTHER agent's init run —
120
+ // snapshotted before any write, compared after every step ran. Only meaningful
121
+ // when THIS run targets a different agent: a claude-code init run is expected
122
+ // to change claude-code's own managed paths (that's the point of running it),
123
+ // so the guard would otherwise misfire on the CLI's own default/most common
124
+ // path — every fresh `awm init` (no --agent) genuinely mutates its baseline
125
+ // and would incorrectly be flagged as a violation.
126
+ const beforeClaudeFacts = agent === 'claude-code' ? null : (0, provider_facts_1.gatherProviderFacts)('claude-code');
103
127
  let outcome;
104
128
  try {
105
129
  const mergedActions = {
106
130
  ...steps_1.defaultActions,
107
131
  ...(opts.actions ?? {}),
108
132
  };
109
- (0, registries_1.seedBaselineRegistry)();
110
- if ((0, registries_1.listRegistries)().some((r) => !fs_1.default.existsSync(r.contentRoot))) {
111
- await mergedActions.syncCache();
112
- }
113
- const bundles = (0, bundles_1.discoverAllBundles)();
114
- const ctx = (0, context_1.gatherContext)({ cwd, bundles, agent });
115
- // In machineOnly mode, null out the project context so project steps are skipped
116
- const effectiveCtx = opts.machineOnly
117
- ? { ...ctx, project: null }
118
- : ctx;
119
- const confirmExtensions = makeConfirmExtensions(!!opts.yes);
120
- outcome = await (0, orchestrator_1.runInitSteps)({
133
+ // Enumerate every path this run may write to and open one backup
134
+ // session covering the whole init (preferences + every machine/project
135
+ // step) BEFORE calling anything else — including `seedBaselineRegistry`,
136
+ // whose `resolveBaseRemote()` incidentally reads preferences.json and
137
+ // (on a machine with none yet) would otherwise auto-vivify it with
138
+ // stock defaults ahead of our own write, corrupting the "before"
139
+ // snapshot rollback restores to. Uses whatever bundle content already
140
+ // exists on disk right now (before this run's own registry sync).
141
+ // Known narrow gap: on a truly first-ever init (no prior registry
142
+ // clone at all), the bundle artifacts this SAME run freshly clones and
143
+ // installs can't be named yet at this point, so a failure later in
144
+ // that exact run won't roll those specific artifacts back — every
145
+ // other target (preferences, hook, injection, previously-installed
146
+ // bundle content) is covered.
147
+ const preSyncBundles = (0, bundles_1.discoverAllBundles)();
148
+ const mutationTargets = (0, mutation_targets_1.planInitMutationTargets)({
121
149
  cwd,
122
- ctx: effectiveCtx,
123
- bundles,
124
150
  agent,
125
- installMethod: 'symlink',
126
- registryRoot: (0, registries_1.capabilityRoot)('hooks') ?? '',
127
- contentDir: (0, registries_1.contentRoots)()[0] ?? '',
128
- sensorPacksRoot: (0, registries_1.capabilityRoot)('sensor-packs') ?? '',
129
- confirmExtensions,
130
- actions: mergedActions,
151
+ bundles: preSyncBundles,
131
152
  });
153
+ const backup = (0, install_transaction_1.beginBackupSession)(mutationTargets);
154
+ try {
155
+ (0, config_1.savePreferences)(nextPreferences);
156
+ (0, registries_1.seedBaselineRegistry)();
157
+ if ((0, registries_1.listRegistries)().some((r) => !fs_1.default.existsSync(r.contentRoot))) {
158
+ await mergedActions.syncCache();
159
+ }
160
+ const bundles = (0, bundles_1.discoverAllBundles)();
161
+ const ctx = (0, context_1.gatherContext)({ cwd, bundles, agent });
162
+ // In machineOnly mode, null out the project context so project steps are skipped
163
+ const effectiveCtx = opts.machineOnly
164
+ ? { ...ctx, project: null }
165
+ : ctx;
166
+ const confirmExtensions = makeConfirmExtensions(!!opts.yes);
167
+ outcome = await (0, orchestrator_1.runInitSteps)({
168
+ cwd,
169
+ ctx: effectiveCtx,
170
+ bundles,
171
+ agent,
172
+ enabledAgents: nextPreferences.enabledAgents,
173
+ installMethod: 'symlink',
174
+ registryRoot: (0, registries_1.capabilityRoot)('hooks') ?? '',
175
+ contentDir: (0, registries_1.contentRoots)()[0] ?? '',
176
+ sensorPacksRoot: (0, registries_1.capabilityRoot)('sensor-packs') ?? '',
177
+ confirmExtensions,
178
+ actions: mergedActions,
179
+ });
180
+ if (outcome.failed > 0) {
181
+ throw new Error('one or more init steps failed');
182
+ }
183
+ if (beforeClaudeFacts) {
184
+ (0, provider_facts_1.assertClaudeBaselinePreserved)(beforeClaudeFacts, (0, provider_facts_1.gatherProviderFacts)('claude-code'));
185
+ }
186
+ backup.commit();
187
+ outcome.transactionId = backup.transactionId;
188
+ outcome.modifiedFiles = backup.targetPaths;
189
+ }
190
+ catch (error) {
191
+ backup.rollback();
192
+ throw error;
193
+ }
132
194
  }
133
195
  catch (err) {
134
196
  process.stderr.write(`awm init: internal error: ${err.message}\n`);
@@ -70,7 +70,7 @@ function registerRegistryCommand(program) {
70
70
  const agentPick = await (0, prompts_1.select)({
71
71
  message: 'Target agent',
72
72
  initialValue: prefs.defaultAgent,
73
- options: Object.keys(providers_1.PROVIDERS).map((a) => ({ value: a, label: a })),
73
+ options: providers_1.AGENT_TARGETS.map((a) => ({ value: a, label: a })),
74
74
  });
75
75
  if (!(0, prompts_1.isCancel)(agentPick))
76
76
  agents = [agentPick];
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSyncCore = runSyncCore;
7
+ // src/commands/sync.ts
8
+ //
9
+ // `awm sync` — rebuilds local skill/workflow/agent symlinks from
10
+ // `.awm/profile.json` for the resolved agent targets (R12/R13): defaults to
11
+ // every enabled agent when `--agent` is absent, otherwise an explicit
12
+ // comma-separated subset (validated against `enabledAgents`).
13
+ // NOTE: deliberately no top-level `@clack/prompts` import here — this module
14
+ // is `require()`d directly by tests exercising `runSyncCore`, and `@clack/prompts`
15
+ // ships ESM-only, which the CommonJS ts-jest transform can't load. The
16
+ // intro/outro chrome lives in `index.ts`'s Commander registration instead
17
+ // (which already imports clack at the top and is never `require()`d by tests).
18
+ const picocolors_1 = __importDefault(require("picocolors"));
19
+ const profile_1 = require("../core/profile");
20
+ const registries_1 = require("../core/registries");
21
+ const bundles_1 = require("../core/bundles");
22
+ const bundle_install_1 = require("../core/bundle-install");
23
+ const profile_pins_1 = require("../core/profile-pins");
24
+ const config_1 = require("../utils/config");
25
+ const agent_targets_1 = require("../core/agent-targets");
26
+ const defaultDeps = {
27
+ syncRegistries: registries_1.syncRegistries,
28
+ verifyMinCliVersions: registries_1.verifyMinCliVersions,
29
+ verifyProjectPins: profile_pins_1.verifyProjectPins,
30
+ syncProfile: bundle_install_1.syncProfile,
31
+ };
32
+ /** Core, UI-free `awm sync` logic — see `runSync` for the Commander-facing wrapper. */
33
+ async function runSyncCore(options, deps = {}) {
34
+ const d = { ...defaultDeps, ...deps };
35
+ const cwd = options.cwd ?? process.cwd();
36
+ const projectRoot = (0, profile_1.findProjectRoot)(cwd);
37
+ if (!projectRoot) {
38
+ console.error(picocolors_1.default.red('No project root found (need a .git/, package.json, or .awm/profile.json here).'));
39
+ return { code: 1, selectedAgents: [] };
40
+ }
41
+ let profile;
42
+ try {
43
+ profile = (0, profile_1.readProfile)(projectRoot);
44
+ }
45
+ catch (e) {
46
+ console.error(picocolors_1.default.red(e.message));
47
+ return { code: 1, selectedAgents: [] };
48
+ }
49
+ const prefs = (0, config_1.getPreferences)();
50
+ const resolved = (0, agent_targets_1.resolveAgentTargetsOrError)({ prefs, explicit: options.agent });
51
+ if (!resolved.ok) {
52
+ console.error(picocolors_1.default.red(resolved.error));
53
+ return { code: 1, selectedAgents: [] };
54
+ }
55
+ const selectedAgents = resolved.targets;
56
+ const syncResults = await d.syncRegistries();
57
+ for (const r of syncResults) {
58
+ if (r.action === 'error')
59
+ console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
60
+ }
61
+ // Gate minCliVersion (WS-4) before pins (contract gates first — CONSTITUTION).
62
+ try {
63
+ (0, registries_1.assertRegistryGates)(d.verifyMinCliVersions());
64
+ }
65
+ catch (e) {
66
+ console.error(picocolors_1.default.red(e.message));
67
+ return { code: 1, selectedAgents };
68
+ }
69
+ const pins = profile.registries ?? {};
70
+ if (Object.keys(pins).length > 0) {
71
+ const failures = await d.verifyProjectPins(pins);
72
+ if (failures.length > 0) {
73
+ for (const f of failures) {
74
+ if (f.reason === 'missing-registry') {
75
+ const registriesConfig = (0, registries_1.readRegistriesConfig)();
76
+ const isConfigured = registriesConfig.some((r) => r.name === f.name);
77
+ if (isConfigured) {
78
+ console.error(picocolors_1.default.red(`The registry "${f.name}" is configured but not yet synced on this machine. Run: awm update`));
79
+ }
80
+ else {
81
+ console.error(picocolors_1.default.red(`The registry "${f.name}" is not configured on this machine. Run: awm registry add <remote>`));
82
+ }
83
+ }
84
+ else {
85
+ console.error(picocolors_1.default.red(`This machine has ${f.name} @ ${f.actual ? `v${f.actual}` : 'HEAD (no tag)'} but the project requires v${f.required}.`));
86
+ console.error(picocolors_1.default.red(` Run: awm pin ${f.name} ${f.required} && awm update`));
87
+ }
88
+ }
89
+ return { code: 1, selectedAgents };
90
+ }
91
+ }
92
+ if (profile.extensions.length === 0) {
93
+ console.log(picocolors_1.default.yellow('No extensions in .awm/profile.json — nothing to sync. Use `awm add <bundle>` first.'));
94
+ return { code: 0, selectedAgents };
95
+ }
96
+ const method = options.method === 'copy' ? 'copy' : 'symlink';
97
+ let result;
98
+ try {
99
+ result = d.syncProfile({ projectRoot, bundles: (0, bundles_1.discoverAllBundles)(), agents: selectedAgents, method });
100
+ }
101
+ catch (e) {
102
+ console.error(picocolors_1.default.red(e.message));
103
+ return { code: 1, selectedAgents };
104
+ }
105
+ if (result.skipped.length > 0) {
106
+ for (const sk of result.skipped)
107
+ console.log(picocolors_1.default.yellow(` ⚠ Skipped: ${sk}`));
108
+ }
109
+ const lines = result.installed.map((n) => picocolors_1.default.green(n)).join('\n ');
110
+ const installedNote = lines ? `\n ${lines}` : picocolors_1.default.dim(' (all up to date)');
111
+ console.log(`✅ Synced extensions [${result.extensions.join(', ')}]:${installedNote}`);
112
+ return { code: 0, selectedAgents, result };
113
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runUpdateCore = runUpdateCore;
7
+ // src/commands/update.ts
8
+ //
9
+ // `awm update` — syncs configured registries, regenerates global context,
10
+ // reconciles machine-scope artifacts (baseline/ambient bundles) against the
11
+ // freshly synced content, and re-syncs the SessionStart hook's managed files
12
+ // — all scoped to the resolved agent targets (R12/R13). Every stage after
13
+ // the registry sync is a real gate: a failure in context regeneration,
14
+ // artifact reconciliation, or hook resync reports the failing provider and
15
+ // returns a non-zero exit code (no silent `catch {}` swallowing failures).
16
+ // NOTE: deliberately no top-level `@clack/prompts` import here — this module
17
+ // is `require()`d directly by tests exercising `runUpdateCore`, and
18
+ // `@clack/prompts` ships ESM-only, which the CommonJS ts-jest transform can't
19
+ // load. The intro/outro chrome lives in `index.ts`'s Commander registration
20
+ // instead (which already imports clack at the top and is never `require()`d
21
+ // by tests).
22
+ const picocolors_1 = __importDefault(require("picocolors"));
23
+ const config_1 = require("../utils/config");
24
+ const agent_targets_1 = require("../core/agent-targets");
25
+ const registries_1 = require("../core/registries");
26
+ const regenerate_1 = require("../core/context/regenerate");
27
+ const reconciliation_1 = require("../core/reconciliation");
28
+ const install_transaction_1 = require("../core/install-transaction");
29
+ const resync_1 = require("../commands/hooks/resync");
30
+ const defaultDeps = {
31
+ syncRegistries: registries_1.syncRegistries,
32
+ verifyMinCliVersions: registries_1.verifyMinCliVersions,
33
+ regenerateGlobalContext: regenerate_1.regenerateGlobalContext,
34
+ planReconciliation: reconciliation_1.planReconciliation,
35
+ applyInstallPlan: install_transaction_1.applyInstallPlan,
36
+ resyncInstalledHooks: resync_1.resyncInstalledHooks,
37
+ offerSelfUpdate: async () => {
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
+ const { offerSelfUpdate: real } = require('../core/update-check');
40
+ await real();
41
+ },
42
+ };
43
+ /**
44
+ * Core, UI-free `awm update` logic: resolves agent targets, runs every stage
45
+ * in order (registry sync → CLI-version gate → context regen → artifact
46
+ * reconciliation → hook resync → self-update offer), and returns the
47
+ * selected targets alongside an exit code so callers/tests don't need to
48
+ * scrape console output. `deps` is fully injectable for tests (mirrors
49
+ * `runInit`'s `actions` seam).
50
+ */
51
+ async function runUpdateCore(options = {}, deps = {}) {
52
+ const d = { ...defaultDeps, ...deps };
53
+ const prefs = (0, config_1.getPreferences)();
54
+ const resolved = (0, agent_targets_1.resolveAgentTargetsOrError)({ prefs, explicit: options.agent });
55
+ if (!resolved.ok) {
56
+ console.error(picocolors_1.default.red(resolved.error));
57
+ return { code: 1, selectedAgents: [] };
58
+ }
59
+ const selectedAgents = resolved.targets;
60
+ const registryResults = await d.syncRegistries();
61
+ for (const r of registryResults) {
62
+ if (r.action === 'error')
63
+ console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
64
+ else
65
+ console.log(picocolors_1.default.green(` ✓ Registry ${r.name} ${r.action === 'pulled' ? 'updated' : 're-cloned'} @ ${r.version}`));
66
+ }
67
+ try {
68
+ (0, registries_1.assertRegistryGates)(d.verifyMinCliVersions());
69
+ }
70
+ catch (e) {
71
+ console.error(picocolors_1.default.red(e.message));
72
+ return { code: 1, selectedAgents };
73
+ }
74
+ const regen = d.regenerateGlobalContext(selectedAgents);
75
+ const refreshed = regen.filter((r) => r.action === 'refreshed').map((r) => r.agent);
76
+ if (refreshed.length > 0)
77
+ console.log(picocolors_1.default.green(` ✓ Regenerated AWM context for: ${refreshed.join(', ')}`));
78
+ let artifactResult;
79
+ try {
80
+ const artifactPlan = d.planReconciliation({ targets: selectedAgents, roots: (0, registries_1.contentRoots)() });
81
+ artifactResult = d.applyInstallPlan(artifactPlan);
82
+ }
83
+ catch (e) {
84
+ console.error(picocolors_1.default.red(`Artifact reconciliation failed: ${e.message}`));
85
+ return { code: 1, selectedAgents };
86
+ }
87
+ if (artifactResult.installed.length > 0) {
88
+ console.log(picocolors_1.default.green(` ✓ Reconciled artifacts: ${artifactResult.installed.join(', ')}`));
89
+ }
90
+ const hooksRoot = (0, registries_1.capabilityRoot)('hooks');
91
+ if (hooksRoot) {
92
+ try {
93
+ for (const r of d.resyncInstalledHooks(hooksRoot, selectedAgents)) {
94
+ if (r.action === 'resynced')
95
+ console.log(picocolors_1.default.green(` ✓ Re-synced ${r.agent} hook scripts`));
96
+ else if (r.action === 'registry-missing')
97
+ console.warn(picocolors_1.default.yellow(` ⚠ ${r.agent} hook installed but registry hooks missing — run 'awm hooks install'`));
98
+ }
99
+ }
100
+ catch (e) {
101
+ console.error(picocolors_1.default.red(`Hook resync failed: ${e.message}`));
102
+ return { code: 1, selectedAgents };
103
+ }
104
+ }
105
+ await d.offerSelfUpdate();
106
+ return { code: 0, selectedAgents };
107
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAgentTargets = resolveAgentTargets;
4
+ exports.resolveAgentTargetsOrError = resolveAgentTargetsOrError;
5
+ const providers_1 = require("../providers");
6
+ function resolveAgentTargets(input) {
7
+ if (!input || typeof input !== 'object' ||
8
+ !input.prefs || !Array.isArray(input.prefs.enabledAgents) ||
9
+ !input.prefs.enabledAgents.every(providers_1.isAgentTarget)) {
10
+ throw new Error('resolveAgentTargets requires valid enabledAgents preferences');
11
+ }
12
+ if (input.explicit !== undefined && typeof input.explicit !== 'string') {
13
+ throw new Error('--agent must be a comma-separated provider list');
14
+ }
15
+ if (input.explicit === undefined)
16
+ return [...input.prefs.enabledAgents];
17
+ const raw = input.explicit
18
+ .split(',')
19
+ .map((value) => value.trim())
20
+ .filter(Boolean);
21
+ if (raw.length === 0)
22
+ throw new Error('--agent requires at least one provider');
23
+ const agents = Array.from(new Set(raw));
24
+ for (const agent of agents) {
25
+ if (!(0, providers_1.isAgentTarget)(agent))
26
+ throw new Error(`Invalid agent "${agent}".`);
27
+ if (!input.prefs.enabledAgents.includes(agent)) {
28
+ throw new Error(`${agent} is not enabled; run awm init --agent ${agent}`);
29
+ }
30
+ }
31
+ return agents;
32
+ }
33
+ /**
34
+ * Non-throwing wrapper over `resolveAgentTargets` — every one of `add`,
35
+ * `remove`, `sync`, `update` and `doctor` needs the exact same
36
+ * try/resolve/catch shape, but each has a different failure-handling style
37
+ * (`process.exit` in `index.ts`'s interactive Commander actions vs. returning
38
+ * `{ code: 1, ... }` from the testable `run*Core` functions). Keeping this
39
+ * wrapper pure (no logging, no process.exit) lets every call site reuse the
40
+ * SAME resolution logic while choosing its own way to report/exit on
41
+ * failure, rather than duplicating the try/catch at each of the 6+ sites.
42
+ */
43
+ function resolveAgentTargetsOrError(input) {
44
+ try {
45
+ return { ok: true, targets: resolveAgentTargets(input) };
46
+ }
47
+ catch (e) {
48
+ return { ok: false, error: e.message };
49
+ }
50
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.artifactStateFile = artifactStateFile;
7
+ exports.readArtifactState = readArtifactState;
8
+ exports.writeArtifactState = writeArtifactState;
9
+ exports.mergeArtifactRecords = mergeArtifactRecords;
10
+ // src/core/artifact-state.ts
11
+ //
12
+ // Persistent ownership ledger for artifacts materialized by the planner
13
+ // (install-planner.ts). One record per physical target path; `owners` tracks
14
+ // which agent targets currently depend on that target so a later removal
15
+ // (planRemoval) knows whether it's safe to delete (R16).
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const paths_1 = require("./paths");
19
+ const atomic_file_1 = require("./atomic-file");
20
+ function artifactStateFile() {
21
+ return path_1.default.join((0, paths_1.awmHome)(), 'state', 'artifacts.json');
22
+ }
23
+ function readArtifactState(file = artifactStateFile()) {
24
+ if (!fs_1.default.existsSync(file))
25
+ return [];
26
+ let value;
27
+ try {
28
+ value = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
29
+ }
30
+ catch {
31
+ throw new Error(`${file} is not valid JSON. Fix it manually, then re-run.`);
32
+ }
33
+ if (!Array.isArray(value))
34
+ throw new Error(`${file} must contain an array`);
35
+ return value;
36
+ }
37
+ function writeArtifactState(records, file = artifactStateFile()) {
38
+ const ordered = [...records].sort((a, b) => a.targetPath.localeCompare(b.targetPath));
39
+ (0, atomic_file_1.writeFileAtomic)(file, JSON.stringify(ordered, null, 2) + '\n', 0o600);
40
+ }
41
+ /**
42
+ * Upserts `incoming` records into `existing`, keyed by `targetPath` — the
43
+ * natural unique key for a ManagedArtifactRecord (see module docstring: "one
44
+ * record per physical target path"). For each `targetPath` present in
45
+ * `incoming`, the incoming record REPLACES any existing record for that same
46
+ * path (it reflects the current true ownership state for that target after
47
+ * the apply that produced it). Every existing record whose `targetPath` is
48
+ * NOT touched by `incoming` is preserved unchanged.
49
+ *
50
+ * This is what makes a sequence of `applyInstallPlan` calls (e.g. the several
51
+ * separate calls a real `awm init` run makes — one per bundle) additive
52
+ * instead of each one clobbering every earlier call's ownership records.
53
+ * `writeArtifactState` itself stays a low-level "write exactly this array"
54
+ * primitive; callers that already have the final desired record set (e.g. a
55
+ * future `planRemoval`-driven deletion) should keep calling it directly
56
+ * rather than through this merge.
57
+ */
58
+ function mergeArtifactRecords(existing, incoming) {
59
+ const byTargetPath = new Map();
60
+ for (const record of existing)
61
+ byTargetPath.set(record.targetPath, record);
62
+ for (const record of incoming)
63
+ byTargetPath.set(record.targetPath, record);
64
+ return Array.from(byTargetPath.values());
65
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeFileAtomic = writeFileAtomic;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ function targetMode(file) {
11
+ try {
12
+ const stat = fs_1.default.lstatSync(file);
13
+ if (stat.isSymbolicLink()) {
14
+ throw new Error(`refusing to replace symlink target: ${file}`);
15
+ }
16
+ return stat.isFile() ? stat.mode & 0o777 : undefined;
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return undefined;
21
+ throw error;
22
+ }
23
+ }
24
+ function writeFileAtomic(file, content, mode = 0o644) {
25
+ if (typeof file !== 'string' || file.length === 0) {
26
+ throw new Error('file must be a non-empty string');
27
+ }
28
+ if (typeof content !== 'string') {
29
+ throw new Error('content must be a string');
30
+ }
31
+ if (!Number.isInteger(mode) || mode < 0 || mode > 0o777) {
32
+ throw new Error('mode must be an integer between 0 and 0777');
33
+ }
34
+ const existingMode = targetMode(file);
35
+ const effectiveMode = existingMode ?? mode;
36
+ const directory = path_1.default.dirname(file);
37
+ const nonce = crypto_1.default.randomBytes(16).toString('hex');
38
+ const temporary = path_1.default.join(directory, `.${path_1.default.basename(file)}.${process.pid}.${nonce}.tmp`);
39
+ fs_1.default.mkdirSync(directory, { recursive: true });
40
+ let descriptor;
41
+ let ownsTemporary = false;
42
+ try {
43
+ descriptor = fs_1.default.openSync(temporary, 'wx', effectiveMode);
44
+ ownsTemporary = true;
45
+ fs_1.default.writeFileSync(descriptor, content, { encoding: 'utf8' });
46
+ fs_1.default.fchmodSync(descriptor, effectiveMode);
47
+ fs_1.default.fsyncSync(descriptor);
48
+ fs_1.default.closeSync(descriptor);
49
+ descriptor = undefined;
50
+ targetMode(file);
51
+ fs_1.default.renameSync(temporary, file);
52
+ }
53
+ catch (error) {
54
+ if (descriptor !== undefined) {
55
+ try {
56
+ fs_1.default.closeSync(descriptor);
57
+ }
58
+ catch {
59
+ // best-effort: preserve the original write error; the descriptor closes on process exit.
60
+ }
61
+ }
62
+ if (ownsTemporary) {
63
+ try {
64
+ fs_1.default.rmSync(temporary, { force: true });
65
+ }
66
+ catch {
67
+ // best-effort: preserve the original write error; a failed temp cleanup may leave one sibling file.
68
+ }
69
+ }
70
+ throw error;
71
+ }
72
+ }