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
|
@@ -0,0 +1,405 @@
|
|
|
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.applyInstallPlan = applyInstallPlan;
|
|
7
|
+
exports.defaultTransactionDeps = defaultTransactionDeps;
|
|
8
|
+
exports.beginBackupSession = beginBackupSession;
|
|
9
|
+
exports.restoreBackup = restoreBackup;
|
|
10
|
+
exports.listBackups = listBackups;
|
|
11
|
+
// src/core/install-transaction.ts
|
|
12
|
+
//
|
|
13
|
+
// Transactional filesystem apply for InstallPlan (install-planner.ts):
|
|
14
|
+
// validates every target, backs up every existing target BEFORE any
|
|
15
|
+
// replacement happens (R24.1), replaces targets, verifies, and — on any
|
|
16
|
+
// failure — rolls back every target already replaced, in reverse order
|
|
17
|
+
// (R25). Never removes a live target before its staged replacement exists
|
|
18
|
+
// (R17, enforced by executor.ts's stageArtifact/replaceArtifact split).
|
|
19
|
+
//
|
|
20
|
+
// Also exports a general-purpose `beginBackupSession`, for callers outside
|
|
21
|
+
// the planner (e.g. preferences/provider-config mutations during `init`)
|
|
22
|
+
// that want the same backup/restore guarantees without going through a full
|
|
23
|
+
// InstallPlan. `applyInstallPlan` and `beginBackupSession` write independent
|
|
24
|
+
// backup directories under ~/.awm/backups/<transactionId>/, each with a
|
|
25
|
+
// manifest.json (mode 0600) inside a 0700 directory — paths and existence
|
|
26
|
+
// flags only, never file contents or environment variables.
|
|
27
|
+
const fs_1 = __importDefault(require("fs"));
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
30
|
+
const artifact_state_1 = require("./artifact-state");
|
|
31
|
+
const paths_1 = require("./paths");
|
|
32
|
+
const atomic_file_1 = require("./atomic-file");
|
|
33
|
+
const codex_agent_1 = require("./renderers/codex-agent");
|
|
34
|
+
const executor_1 = require("./executor");
|
|
35
|
+
/**
|
|
36
|
+
* The single timestamp-sanitization rule for transaction IDs, shared by
|
|
37
|
+
* `applyInstallPlan` and `beginBackupSession` so both always produce IDs
|
|
38
|
+
* `restoreBackup`'s validation regex (`^\d{4}-\d{2}-\d{2}T[0-9A-Za-z.-]+$`)
|
|
39
|
+
* accepts. Strips both `:` and `.` (an ISO timestamp has both) so the result
|
|
40
|
+
* is filesystem- and regex-safe either way.
|
|
41
|
+
*/
|
|
42
|
+
function sanitizeTransactionTimestamp() {
|
|
43
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
44
|
+
}
|
|
45
|
+
function ensureBackupDir(backupDir) {
|
|
46
|
+
fs_1.default.mkdirSync(backupDir, { recursive: true });
|
|
47
|
+
fs_1.default.chmodSync(backupDir, 0o700);
|
|
48
|
+
}
|
|
49
|
+
function writeBackupManifest(backupDir, manifest) {
|
|
50
|
+
ensureBackupDir(backupDir);
|
|
51
|
+
(0, atomic_file_1.writeFileAtomic)(path_1.default.join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 0o600);
|
|
52
|
+
}
|
|
53
|
+
function readBackupManifest(manifestPath) {
|
|
54
|
+
if (!fs_1.default.existsSync(manifestPath)) {
|
|
55
|
+
throw new Error(`backup manifest not found: ${manifestPath}`);
|
|
56
|
+
}
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error(`${manifestPath} is not valid JSON`);
|
|
63
|
+
}
|
|
64
|
+
const manifest = value;
|
|
65
|
+
if (!manifest || typeof manifest.id !== 'string' || !Array.isArray(manifest.entries)) {
|
|
66
|
+
throw new Error(`${manifestPath} is not a valid backup manifest`);
|
|
67
|
+
}
|
|
68
|
+
return manifest;
|
|
69
|
+
}
|
|
70
|
+
function targetExists(targetPath) {
|
|
71
|
+
try {
|
|
72
|
+
fs_1.default.lstatSync(targetPath);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* sha256 of the target's content, for the manifest's integrity record.
|
|
81
|
+
* Files hash their bytes directly; directories and symlinks hash a stable
|
|
82
|
+
* summary (entry name + kind + size + mtime, or the symlink's own target)
|
|
83
|
+
* rather than walking and hashing every file individually — enough to detect
|
|
84
|
+
* "the backup no longer matches what's on disk" without the cost of a full
|
|
85
|
+
* recursive content hash.
|
|
86
|
+
*/
|
|
87
|
+
function computeContentHash(targetPath) {
|
|
88
|
+
const hash = crypto_1.default.createHash('sha256');
|
|
89
|
+
const stat = fs_1.default.lstatSync(targetPath);
|
|
90
|
+
if (stat.isSymbolicLink()) {
|
|
91
|
+
hash.update('symlink:').update(fs_1.default.readlinkSync(targetPath));
|
|
92
|
+
}
|
|
93
|
+
else if (stat.isDirectory()) {
|
|
94
|
+
hash.update('dir:');
|
|
95
|
+
for (const name of fs_1.default.readdirSync(targetPath).sort()) {
|
|
96
|
+
const entryStat = fs_1.default.lstatSync(path_1.default.join(targetPath, name));
|
|
97
|
+
const kind = entryStat.isDirectory() ? 'd' : entryStat.isSymbolicLink() ? 'l' : 'f';
|
|
98
|
+
hash.update(`${name}:${kind}:${entryStat.size}:${entryStat.mtimeMs}\n`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
hash.update(fs_1.default.readFileSync(targetPath));
|
|
103
|
+
}
|
|
104
|
+
return hash.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
/** Copies `targetPath` (if it exists) into `backupDir/relName` and returns the manifest entry. */
|
|
107
|
+
function backupEntryFor(targetPath, backupDir, relName) {
|
|
108
|
+
if (!targetExists(targetPath)) {
|
|
109
|
+
return { targetPath, existed: false, backupRelPath: null };
|
|
110
|
+
}
|
|
111
|
+
ensureBackupDir(backupDir);
|
|
112
|
+
// dereference:false (fs.cpSync default) — a symlinked target is backed up
|
|
113
|
+
// as a symlink, not by copying whatever it points at.
|
|
114
|
+
fs_1.default.cpSync(targetPath, path_1.default.join(backupDir, relName), { recursive: true });
|
|
115
|
+
return { targetPath, existed: true, backupRelPath: relName, contentHash: computeContentHash(targetPath) };
|
|
116
|
+
}
|
|
117
|
+
/** Restores `targetPath` from its backup, or removes it if it didn't exist before. */
|
|
118
|
+
function restoreTargetFrom(targetPath, backupPath) {
|
|
119
|
+
fs_1.default.rmSync(targetPath, { recursive: true, force: true });
|
|
120
|
+
if (backupPath !== null) {
|
|
121
|
+
fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
|
|
122
|
+
fs_1.default.cpSync(backupPath, targetPath, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function restoreManifestEntry(backupDir, entry) {
|
|
126
|
+
// Only ever operates on targetPath/backupRelPath literally enumerated in
|
|
127
|
+
// the validated manifest — never on a path derived from user input.
|
|
128
|
+
const backupPath = entry.existed && entry.backupRelPath ? path_1.default.join(backupDir, entry.backupRelPath) : null;
|
|
129
|
+
restoreTargetFrom(entry.targetPath, backupPath);
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// applyInstallPlan
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Real transactional apply for an InstallPlan. Order (per target, across the
|
|
136
|
+
* whole plan):
|
|
137
|
+
* 1. validate every op (no writes)
|
|
138
|
+
* 2. backup every op's existing target (before ANY replace — R24.1)
|
|
139
|
+
* 3. stage every op's replacement (source untouched, target untouched — R17)
|
|
140
|
+
* 4. replace every op's target with its staged replacement
|
|
141
|
+
* 5. verify every op
|
|
142
|
+
* 6. persist artifact-state records — merged into the existing ledger
|
|
143
|
+
* (upsert by targetPath, see mergeArtifactRecords), not a wholesale
|
|
144
|
+
* overwrite, so earlier applyInstallPlan calls' records survive
|
|
145
|
+
* Any failure from step 4 onward rolls back every already-replaced target, in
|
|
146
|
+
* reverse order, using the backups captured in step 2 (R25). Rollback is
|
|
147
|
+
* best-effort: a rollback failure for one target does not stop the others
|
|
148
|
+
* from being attempted, and never masks the original failure.
|
|
149
|
+
*/
|
|
150
|
+
function applyInstallPlan(plan, deps = defaultTransactionDeps()) {
|
|
151
|
+
for (const op of plan.operations)
|
|
152
|
+
deps.validate(op);
|
|
153
|
+
const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups', sanitizeTransactionTimestamp());
|
|
154
|
+
const transactionId = path_1.default.basename(backupDir);
|
|
155
|
+
const backups = new Map();
|
|
156
|
+
for (const op of plan.operations)
|
|
157
|
+
backups.set(op, deps.backup(op, backupDir));
|
|
158
|
+
const staged = new Map();
|
|
159
|
+
try {
|
|
160
|
+
for (const op of plan.operations)
|
|
161
|
+
staged.set(op, deps.stage(op));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Best-effort: clean up any temp files earlier iterations already
|
|
165
|
+
// staged before this one threw — none of them have been swapped in
|
|
166
|
+
// yet (replace() hasn't run), so there's nothing to roll back, just
|
|
167
|
+
// orphaned staging artifacts to remove.
|
|
168
|
+
for (const stagedPath of staged.values()) {
|
|
169
|
+
try {
|
|
170
|
+
fs_1.default.rmSync(stagedPath, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// best-effort: retain the original failure
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const replaced = [];
|
|
179
|
+
try {
|
|
180
|
+
for (const op of plan.operations) {
|
|
181
|
+
deps.replace(op, staged.get(op));
|
|
182
|
+
replaced.push(op);
|
|
183
|
+
}
|
|
184
|
+
for (const op of plan.operations)
|
|
185
|
+
deps.verify(op);
|
|
186
|
+
// Upsert this plan's records into the EXISTING persisted ledger,
|
|
187
|
+
// keyed by targetPath — a real `awm init` run makes several separate
|
|
188
|
+
// applyInstallPlan calls (one per bundle), and each one only knows
|
|
189
|
+
// about the artifacts IT touched. Writing plan.records wholesale
|
|
190
|
+
// would silently discard every earlier call's ownership records
|
|
191
|
+
// (BLOCKER found in post-implementation QA); merging preserves them.
|
|
192
|
+
(0, artifact_state_1.writeArtifactState)((0, artifact_state_1.mergeArtifactRecords)((0, artifact_state_1.readArtifactState)(), plan.records));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
for (const op of [...replaced].reverse()) {
|
|
196
|
+
try {
|
|
197
|
+
deps.rollback(op, backups.get(op) ?? null);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// best-effort: retain the original failure; the backup path remains available for manual recovery
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
// Every owner in plan.reports — 'install' (the owner whose selection
|
|
207
|
+
// caused the physical write) AND 'retain' (co-owners of the same
|
|
208
|
+
// shared target, e.g. OpenCode+Codex sharing one skill directory) —
|
|
209
|
+
// gained ownership of this target as a result of THIS run. The
|
|
210
|
+
// user-facing summary must list all of them, not just the first
|
|
211
|
+
// (R15/R15.1): a single physical write can still mean N providers now
|
|
212
|
+
// own the artifact.
|
|
213
|
+
installed: plan.reports
|
|
214
|
+
.map((report) => `${path_1.default.basename(report.targetPath)} → ${report.owner}`),
|
|
215
|
+
skipped: [],
|
|
216
|
+
transactionId,
|
|
217
|
+
modifiedFiles: plan.operations.map((op) => op.targetPath),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function stageRenderedFile(content, targetPath) {
|
|
221
|
+
const parent = path_1.default.dirname(targetPath);
|
|
222
|
+
fs_1.default.mkdirSync(parent, { recursive: true });
|
|
223
|
+
const staged = path_1.default.join(parent, `.${path_1.default.basename(targetPath)}.${process.pid}.staged`);
|
|
224
|
+
fs_1.default.rmSync(staged, { recursive: true, force: true });
|
|
225
|
+
fs_1.default.writeFileSync(staged, content, 'utf8');
|
|
226
|
+
return staged;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The real, filesystem-touching TransactionDeps used by applyInstallPlan by
|
|
230
|
+
* default. Renders `codex-agent-toml` targets from the canonical agent
|
|
231
|
+
* Markdown source at stage time; every other renderer uses the plain
|
|
232
|
+
* symlink/copy staging from executor.ts. Never logs target contents or
|
|
233
|
+
* environment variables.
|
|
234
|
+
*/
|
|
235
|
+
function defaultTransactionDeps() {
|
|
236
|
+
let index = 0;
|
|
237
|
+
let createdAt = null;
|
|
238
|
+
const manifestEntries = [];
|
|
239
|
+
return {
|
|
240
|
+
validate(op) {
|
|
241
|
+
if (!fs_1.default.existsSync(op.sourcePath)) {
|
|
242
|
+
throw new Error(`Source path does not exist: ${op.sourcePath}`);
|
|
243
|
+
}
|
|
244
|
+
if (op.renderer === 'codex-agent-toml') {
|
|
245
|
+
// Renders without writing anything, purely to surface parse errors
|
|
246
|
+
// before any backup/replace happens.
|
|
247
|
+
(0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
backup(op, backupDir) {
|
|
251
|
+
if (createdAt === null)
|
|
252
|
+
createdAt = new Date().toISOString();
|
|
253
|
+
const entry = backupEntryFor(op.targetPath, backupDir, String(index));
|
|
254
|
+
index += 1;
|
|
255
|
+
manifestEntries.push(entry);
|
|
256
|
+
writeBackupManifest(backupDir, {
|
|
257
|
+
id: path_1.default.basename(backupDir),
|
|
258
|
+
createdAt,
|
|
259
|
+
committed: false,
|
|
260
|
+
entries: manifestEntries,
|
|
261
|
+
});
|
|
262
|
+
return entry.existed ? path_1.default.join(backupDir, entry.backupRelPath) : null;
|
|
263
|
+
},
|
|
264
|
+
stage(op) {
|
|
265
|
+
if (op.renderer === 'codex-agent-toml') {
|
|
266
|
+
const rendered = (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
|
|
267
|
+
return stageRenderedFile(rendered, op.targetPath);
|
|
268
|
+
}
|
|
269
|
+
return (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method);
|
|
270
|
+
},
|
|
271
|
+
replace(op, staged) {
|
|
272
|
+
(0, executor_1.replaceArtifact)(staged, op.targetPath);
|
|
273
|
+
},
|
|
274
|
+
verify(op) {
|
|
275
|
+
let stat;
|
|
276
|
+
try {
|
|
277
|
+
stat = fs_1.default.lstatSync(op.targetPath);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
throw new Error(`verification failed: ${op.targetPath} is missing after install`);
|
|
281
|
+
}
|
|
282
|
+
if (op.renderer === 'codex-agent-toml') {
|
|
283
|
+
if (!stat.isFile()) {
|
|
284
|
+
throw new Error(`verification failed: ${op.targetPath} is not a regular file`);
|
|
285
|
+
}
|
|
286
|
+
const content = fs_1.default.readFileSync(op.targetPath, 'utf8');
|
|
287
|
+
if (!content.startsWith('name = ')) {
|
|
288
|
+
throw new Error(`verification failed: ${op.targetPath} does not look like rendered TOML`);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (op.method === 'symlink' && !stat.isSymbolicLink()) {
|
|
293
|
+
throw new Error(`verification failed: ${op.targetPath} is not a symlink`);
|
|
294
|
+
}
|
|
295
|
+
if (op.method === 'copy' && stat.isSymbolicLink()) {
|
|
296
|
+
throw new Error(`verification failed: ${op.targetPath} is unexpectedly a symlink`);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
rollback(op, backup) {
|
|
300
|
+
restoreTargetFrom(op.targetPath, backup);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// beginBackupSession — general-purpose backup wrapper for mutations outside
|
|
306
|
+
// the planner (preferences, provider configs, etc.)
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
function createBackupManifest(targets, backupDir) {
|
|
309
|
+
const entries = targets.map((target, i) => backupEntryFor(target, backupDir, String(i)));
|
|
310
|
+
return {
|
|
311
|
+
id: path_1.default.basename(backupDir),
|
|
312
|
+
createdAt: new Date().toISOString(),
|
|
313
|
+
committed: false,
|
|
314
|
+
entries,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function markBackupCommitted(backupDir) {
|
|
318
|
+
const manifest = readBackupManifest(path_1.default.join(backupDir, 'manifest.json'));
|
|
319
|
+
writeBackupManifest(backupDir, { ...manifest, committed: true });
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Backs up every (currently existing) path in `targetPaths` before the
|
|
323
|
+
* caller mutates them. `commit()` marks the backup as no-longer-needed
|
|
324
|
+
* (kept for manual recovery, never auto-deleted); `rollback()` restores
|
|
325
|
+
* every target to its pre-session state (or removes it, if it did not exist
|
|
326
|
+
* before the session began).
|
|
327
|
+
*/
|
|
328
|
+
function beginBackupSession(targetPaths) {
|
|
329
|
+
const unique = Array.from(new Set(targetPaths.map((target) => path_1.default.resolve(target))));
|
|
330
|
+
if (unique.some((target) => target === path_1.default.parse(target).root)) {
|
|
331
|
+
throw new Error('refusing to back up a filesystem root');
|
|
332
|
+
}
|
|
333
|
+
const transactionId = sanitizeTransactionTimestamp();
|
|
334
|
+
const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups', transactionId);
|
|
335
|
+
const manifest = createBackupManifest(unique, backupDir);
|
|
336
|
+
writeBackupManifest(backupDir, manifest);
|
|
337
|
+
return {
|
|
338
|
+
transactionId,
|
|
339
|
+
targetPaths: unique,
|
|
340
|
+
commit: () => markBackupCommitted(backupDir),
|
|
341
|
+
rollback: () => {
|
|
342
|
+
restoreBackup(transactionId);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// restoreBackup — explicit, ID-scoped restore (also used by `awm backup restore`)
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
/**
|
|
350
|
+
* Restores every target recorded in the given transaction's manifest.
|
|
351
|
+
* `transactionId` is untrusted input (it reaches here from CLI args) — it is
|
|
352
|
+
* validated against a strict format AND the resolved backup directory is
|
|
353
|
+
* checked to stay inside ~/.awm/backups before any read, to rule out path
|
|
354
|
+
* traversal. Only targets literally enumerated in the manifest are touched.
|
|
355
|
+
*/
|
|
356
|
+
function restoreBackup(transactionId) {
|
|
357
|
+
if (!/^\d{4}-\d{2}-\d{2}T[0-9A-Za-z.-]+$/.test(transactionId)) {
|
|
358
|
+
throw new Error('invalid backup transaction id');
|
|
359
|
+
}
|
|
360
|
+
const root = path_1.default.join((0, paths_1.awmHome)(), 'backups');
|
|
361
|
+
const dir = path_1.default.join(root, transactionId);
|
|
362
|
+
if (!dir.startsWith(`${root}${path_1.default.sep}`)) {
|
|
363
|
+
throw new Error('backup path escapes AWM backup root');
|
|
364
|
+
}
|
|
365
|
+
const manifest = readBackupManifest(path_1.default.join(dir, 'manifest.json'));
|
|
366
|
+
const restored = [];
|
|
367
|
+
for (const entry of manifest.entries) {
|
|
368
|
+
restoreManifestEntry(dir, entry);
|
|
369
|
+
restored.push(entry.targetPath);
|
|
370
|
+
}
|
|
371
|
+
return { restored };
|
|
372
|
+
}
|
|
373
|
+
/** Lists every recorded backup transaction under ~/.awm/backups, newest first. Corrupt manifests are skipped. */
|
|
374
|
+
function listBackups() {
|
|
375
|
+
const root = path_1.default.join((0, paths_1.awmHome)(), 'backups');
|
|
376
|
+
if (!fs_1.default.existsSync(root))
|
|
377
|
+
return [];
|
|
378
|
+
const ids = fs_1.default.readdirSync(root).filter((name) => {
|
|
379
|
+
try {
|
|
380
|
+
return fs_1.default.statSync(path_1.default.join(root, name)).isDirectory();
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
const summaries = [];
|
|
387
|
+
for (const id of ids) {
|
|
388
|
+
const manifestPath = path_1.default.join(root, id, 'manifest.json');
|
|
389
|
+
if (!fs_1.default.existsSync(manifestPath))
|
|
390
|
+
continue;
|
|
391
|
+
try {
|
|
392
|
+
const manifest = readBackupManifest(manifestPath);
|
|
393
|
+
summaries.push({
|
|
394
|
+
id: manifest.id,
|
|
395
|
+
createdAt: manifest.createdAt,
|
|
396
|
+
committed: manifest.committed,
|
|
397
|
+
targets: manifest.entries.map((entry) => entry.targetPath),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// corrupt manifest — skip rather than fail the whole listing
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return summaries.sort((a, b) => b.id.localeCompare(a.id));
|
|
405
|
+
}
|
package/dist/src/core/profile.js
CHANGED
|
@@ -120,7 +120,7 @@ function ensureSkillsGitignored(root, agents) {
|
|
|
120
120
|
const existingLines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
121
121
|
const toIgnore = [];
|
|
122
122
|
for (const agent of agents) {
|
|
123
|
-
const provider = providers_1.
|
|
123
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
124
124
|
for (const type of ['skill', 'workflow', 'agent']) {
|
|
125
125
|
const config = provider[type];
|
|
126
126
|
if (!config)
|
|
@@ -0,0 +1,54 @@
|
|
|
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.preflightLinkArtifactPairs = preflightLinkArtifactPairs;
|
|
7
|
+
exports.scanLegacyArtifacts = scanLegacyArtifacts;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const providers_1 = require("../providers");
|
|
11
|
+
const ARTIFACT_TYPES = ['skill', 'workflow', 'agent'];
|
|
12
|
+
function preflightLinkArtifactPairs(pairs) {
|
|
13
|
+
for (const { agent, artifact } of pairs) {
|
|
14
|
+
(0, providers_1.assertLinkRenderer)(artifact.type, agent);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function scanLegacyArtifacts(agents, scope) {
|
|
18
|
+
const artifacts = new Map();
|
|
19
|
+
for (const agent of agents) {
|
|
20
|
+
for (const type of ARTIFACT_TYPES) {
|
|
21
|
+
let config;
|
|
22
|
+
try {
|
|
23
|
+
config = (0, providers_1.assertLinkRenderer)(type, agent);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof providers_1.UnsupportedRendererError)
|
|
27
|
+
continue;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
if (!config)
|
|
31
|
+
continue;
|
|
32
|
+
const dir = config[scope];
|
|
33
|
+
if (!fs_1.default.existsSync(dir))
|
|
34
|
+
continue;
|
|
35
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
37
|
+
const existing = artifacts.get(entry.name);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.installedIn.push(agent);
|
|
40
|
+
existing.fullPaths.push(fullPath);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
artifacts.set(entry.name, {
|
|
44
|
+
name: entry.name,
|
|
45
|
+
type,
|
|
46
|
+
installedIn: [agent],
|
|
47
|
+
fullPaths: [fullPath],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Array.from(artifacts.values());
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertProviderSupported = assertProviderSupported;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const providers_1 = require("../providers");
|
|
6
|
+
const versioning_1 = require("./versioning");
|
|
7
|
+
function assertProviderSupported(agent, exec = child_process_1.execFileSync) {
|
|
8
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
9
|
+
if (!provider.versionCommand || !provider.minimumVersion) {
|
|
10
|
+
return { provider: agent, version: null };
|
|
11
|
+
}
|
|
12
|
+
let output;
|
|
13
|
+
try {
|
|
14
|
+
output = exec(provider.versionCommand.command, provider.versionCommand.args, {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
timeout: 5000,
|
|
18
|
+
}).toString();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const code = error.code;
|
|
22
|
+
if (code === 'ENOENT') {
|
|
23
|
+
throw new Error('Codex is not installed or not available on PATH. ' +
|
|
24
|
+
'Install the current stable @openai/codex release, then re-run.');
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Codex version probe failed: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
const match = output.trim().match(/^codex-cli (\d+\.\d+\.\d+)$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`could not parse Codex version from: ${output.trim()}`);
|
|
31
|
+
}
|
|
32
|
+
if ((0, versioning_1.compareSemver)(match[1], provider.minimumVersion) < 0) {
|
|
33
|
+
throw new Error(`requires Codex >= ${provider.minimumVersion}; found ${match[1]}`);
|
|
34
|
+
}
|
|
35
|
+
return { provider: agent, version: match[1] };
|
|
36
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planReconciliation = planReconciliation;
|
|
4
|
+
const bundles_1 = require("./bundles");
|
|
5
|
+
const install_planner_1 = require("./install-planner");
|
|
6
|
+
const bundle_install_1 = require("./bundle-install");
|
|
7
|
+
const config_1 = require("../utils/config");
|
|
8
|
+
function intentKey(intent) {
|
|
9
|
+
return `${intent.type}\0${intent.installName}\0${intent.sourcePath}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Computes the InstallPlan that reconciles every machine-scope (baseline +
|
|
13
|
+
* ambient) artifact for `targets` against the currently synced registries at
|
|
14
|
+
* `roots`. Pure — no filesystem writes; callers apply the result with
|
|
15
|
+
* `applyInstallPlan` (install-transaction.ts).
|
|
16
|
+
*/
|
|
17
|
+
function planReconciliation(params) {
|
|
18
|
+
const { targets, roots } = params;
|
|
19
|
+
const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots);
|
|
20
|
+
const contentDir = roots[0] ?? '';
|
|
21
|
+
const machineBundles = bundles.filter((b) => b.scope === 'baseline' || b.scope === 'ambient');
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const artifacts = [];
|
|
24
|
+
for (const b of machineBundles) {
|
|
25
|
+
let intents;
|
|
26
|
+
try {
|
|
27
|
+
intents = (0, bundle_install_1.expandBundleArtifacts)({
|
|
28
|
+
bundleName: b.name,
|
|
29
|
+
bundles,
|
|
30
|
+
agents: targets,
|
|
31
|
+
method: 'symlink',
|
|
32
|
+
projectRoot: '',
|
|
33
|
+
contentDir,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// A plain dangling `dependsOn` name doesn't reach here — resolveBundleClosure
|
|
38
|
+
// (bundles.ts) silently drops it before this point. This only fires on a
|
|
39
|
+
// genuinely malformed bundle definition (e.g. `dependsOn` not an array), which
|
|
40
|
+
// throws while walking the closure. Skip that one bundle rather than aborting
|
|
41
|
+
// reconciliation for every other bundle (see tests/core/reconciliation.test.ts).
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const intent of intents) {
|
|
45
|
+
const key = intentKey(intent);
|
|
46
|
+
if (seen.has(key))
|
|
47
|
+
continue;
|
|
48
|
+
seen.add(key);
|
|
49
|
+
artifacts.push(intent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const enabledAgents = params.enabledAgents ?? (0, config_1.getPreferences)().enabledAgents;
|
|
53
|
+
return (0, install_planner_1.planInstall)({
|
|
54
|
+
artifacts,
|
|
55
|
+
selectedAgents: targets,
|
|
56
|
+
enabledAgents,
|
|
57
|
+
scope: 'global',
|
|
58
|
+
projectRoot: '',
|
|
59
|
+
method: 'symlink',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -16,6 +16,7 @@ exports.syncRegistries = syncRegistries;
|
|
|
16
16
|
exports.readRegistryManifest = readRegistryManifest;
|
|
17
17
|
exports.registryNameForPath = registryNameForPath;
|
|
18
18
|
exports.verifyMinCliVersions = verifyMinCliVersions;
|
|
19
|
+
exports.assertRegistryGates = assertRegistryGates;
|
|
19
20
|
// src/core/registries.ts
|
|
20
21
|
const fs_1 = __importDefault(require("fs"));
|
|
21
22
|
const path_1 = __importDefault(require("path"));
|
|
@@ -203,3 +204,12 @@ function verifyMinCliVersions(current = (0, cli_version_1.cliVersion)()) {
|
|
|
203
204
|
}
|
|
204
205
|
return failures;
|
|
205
206
|
}
|
|
207
|
+
/** Throwing form of `verifyMinCliVersions`'s failure list — lets callers treat the
|
|
208
|
+
* min-CLI-version gate uniformly with other gates (version-gate, backup session, …)
|
|
209
|
+
* in a single try/catch rather than a bespoke console.warn loop. */
|
|
210
|
+
function assertRegistryGates(failures) {
|
|
211
|
+
if (failures.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
const detail = failures.map((f) => `${f.name} requires CLI >= ${f.min}`).join('; ');
|
|
214
|
+
throw new Error(`Registry version gate failed: ${detail}. Run: npm i -g agentic-workflow-manager`);
|
|
215
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCanonicalAgent = parseCanonicalAgent;
|
|
4
|
+
function parseCanonicalAgent(source) {
|
|
5
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
6
|
+
if (!match)
|
|
7
|
+
throw new Error('canonical agent requires YAML frontmatter');
|
|
8
|
+
const fields = new Map();
|
|
9
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
10
|
+
const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
11
|
+
if (!field)
|
|
12
|
+
throw new Error(`invalid canonical agent frontmatter line: ${line}`);
|
|
13
|
+
if (fields.has(field[1]))
|
|
14
|
+
throw new Error(`duplicate canonical agent frontmatter key: ${field[1]}`);
|
|
15
|
+
fields.set(field[1], field[2].replace(/^(['"])(.*)\1$/, '$2').trim());
|
|
16
|
+
}
|
|
17
|
+
const name = fields.get('name') ?? '';
|
|
18
|
+
const description = fields.get('description') ?? '';
|
|
19
|
+
const instructions = match[2].trim();
|
|
20
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
21
|
+
throw new Error('invalid agent name');
|
|
22
|
+
if (!description)
|
|
23
|
+
throw new Error('canonical agent requires a non-empty description');
|
|
24
|
+
if (!instructions)
|
|
25
|
+
throw new Error('canonical agent requires a non-empty instruction body');
|
|
26
|
+
return { name, description, instructions };
|
|
27
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderCodexAgent = renderCodexAgent;
|
|
4
|
+
const canonical_agent_1 = require("./canonical-agent");
|
|
5
|
+
// TOML requires control characters other than tab (U+0009) and newline
|
|
6
|
+
// (U+000A) to be escaped in both basic and multi-line basic strings,
|
|
7
|
+
// including U+007F (DEL) and the rest of the C0 range (U+0000-U+0008,
|
|
8
|
+
// U+000B-U+001F). JSON.stringify (used for single-line strings) already
|
|
9
|
+
// escapes U+0000-U+001F, but not U+007F. A plain backslash/quote replace
|
|
10
|
+
// (used for multi-line strings) escapes neither. escapeControlChars covers
|
|
11
|
+
// both: for tomlString it only ever finds a literal U+007F left over from
|
|
12
|
+
// JSON.stringify (the rest were already turned into escape sequences by
|
|
13
|
+
// JSON.stringify, so they are no longer literal control characters); for
|
|
14
|
+
// tomlMultiline it finds every raw control character in the value.
|
|
15
|
+
//
|
|
16
|
+
// The character class below is built from numeric code points rather than
|
|
17
|
+
// written as literal escape sequences in source, to avoid ambiguity between
|
|
18
|
+
// "the two characters backslash and u" and "the control character itself".
|
|
19
|
+
const CONTROL_CHAR_RANGES = [
|
|
20
|
+
[0x00, 0x08],
|
|
21
|
+
[0x0b, 0x1f],
|
|
22
|
+
[0x7f, 0x7f],
|
|
23
|
+
];
|
|
24
|
+
function buildControlCharsRegex() {
|
|
25
|
+
const classBody = CONTROL_CHAR_RANGES.map(([start, end]) => {
|
|
26
|
+
const from = String.fromCharCode(start);
|
|
27
|
+
const to = String.fromCharCode(end);
|
|
28
|
+
return start === end ? from : `${from}-${to}`;
|
|
29
|
+
}).join('');
|
|
30
|
+
return new RegExp(`[${classBody}]`, 'g');
|
|
31
|
+
}
|
|
32
|
+
const CONTROL_CHARS = buildControlCharsRegex();
|
|
33
|
+
function escapeControlChars(value) {
|
|
34
|
+
return value.replace(CONTROL_CHARS, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
35
|
+
}
|
|
36
|
+
function tomlString(value) {
|
|
37
|
+
// JSON.stringify escapes backslashes, double quotes, and control characters
|
|
38
|
+
// U+0000-U+001F, but TOML also requires U+007F (DEL) to be escaped.
|
|
39
|
+
return escapeControlChars(JSON.stringify(value));
|
|
40
|
+
}
|
|
41
|
+
function tomlMultiline(value) {
|
|
42
|
+
// Escape every backslash and every double quote individually (not just
|
|
43
|
+
// triple-quote runs) so no run of 3+ unescaped quotes can ever appear in
|
|
44
|
+
// the output, regardless of how many consecutive quotes are embedded.
|
|
45
|
+
// Order matters: backslashes first (so escaping a control character does
|
|
46
|
+
// not introduce a backslash that gets doubled), then control characters,
|
|
47
|
+
// then quotes. Tab and newline are left raw; they're legitimately
|
|
48
|
+
// allowed unescaped in a TOML multi-line basic string.
|
|
49
|
+
const escaped = escapeControlChars(value.replace(/\\/g, '\\\\')).replace(/"/g, '\\"');
|
|
50
|
+
return `"""\n${escaped}\n"""`;
|
|
51
|
+
}
|
|
52
|
+
function renderCodexAgent(source) {
|
|
53
|
+
const agent = (0, canonical_agent_1.parseCanonicalAgent)(source);
|
|
54
|
+
return [
|
|
55
|
+
`name = ${tomlString(agent.name)}`,
|
|
56
|
+
`description = ${tomlString(agent.description)}`,
|
|
57
|
+
`developer_instructions = ${tomlMultiline(agent.instructions)}`,
|
|
58
|
+
'',
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|