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.
- package/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +179 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +97 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +3 -2
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +177 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +235 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- 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
|
|
11
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|