@kuznai/inception-engine 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/src/core/deploy.js +65 -1
- package/dist/src/core/ownership.d.ts +1 -0
- package/dist/src/core/ownership.js +34 -4
- package/dist/src/core/revert.js +81 -17
- package/dist/test/helpers/path.d.ts +2 -2
- package/dist/test/helpers/path.js +3 -3
- package/dist/test/os/posix/deploy.test.js +395 -0
- package/dist/test/os/posix/revert.test.js +188 -0
- package/dist/test/os/windows/deploy.test.d.ts +1 -0
- package/dist/test/os/windows/deploy.test.js +238 -0
- package/dist/test/unit/adapters.test.js +139 -2
- package/dist/test/unit/capabilities.test.js +44 -1
- package/dist/test/unit/cli.test.js +162 -0
- package/dist/test/unit/deploy.test.js +62 -647
- package/dist/test/unit/formatters.test.d.ts +1 -0
- package/dist/test/unit/formatters.test.js +74 -0
- package/dist/test/unit/ownership.test.js +61 -4
- package/dist/test/unit/preflight.test.js +59 -0
- package/dist/test/unit/resolve.test.js +67 -1
- package/dist/test/unit/revert.test.js +16 -182
- package/package.json +1 -1
- package/dist/test/os/windows/agentRules-integration.test.js +0 -245
- package/dist/test/os/windows/revert-integration.test.js +0 -72
- /package/dist/test/os/{windows/agentRules-integration.test.d.ts → posix/deploy.test.d.ts} +0 -0
- /package/dist/test/os/{windows/revert-integration.test.d.ts → posix/revert.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -171,6 +171,8 @@ Create an `inception.json` file at the root of your skills directory:
|
|
|
171
171
|
}
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
`files` and `configs` are intentionally narrower than the raw target-template syntax might suggest. Arbitrary project-local targets under `{repo}` and `{workspace}` are allowed, but user-profile targets under `{home}`, `{appdata}`, and `{xdg_config}` must resolve to documented agent-owned global surfaces such as `~/.claude/settings.json` or `~/.codex/AGENTS.md`. Manifests cannot target inception-engine's own `~/.inception-engine/` state directory.
|
|
175
|
+
|
|
174
176
|
Each **skill** entry has:
|
|
175
177
|
|
|
176
178
|
- **name** - Unique identifier using letters, digits, dots, underscores, or hyphens; must not start with a dot
|
|
@@ -542,9 +544,11 @@ inception-engine maintains a centralized deployment registry at `~/.inception-en
|
|
|
542
544
|
|
|
543
545
|
- **Strong binding**: Each registry entry binds a specific target path to its skill, agent, and action kind. For `skill-dir` and `file-write`, ownership checks also require the recorded `source` to match before an existing target is treated as managed. For `config-patch` and `frontmatter-emit`, overwrite protection is keyed by target path, kind, skill, and agent; the stored `patch` and `undoPatch` are used for patch-level revert bookkeeping rather than deploy-time identity checks.
|
|
544
546
|
|
|
547
|
+
- **Reserved state**: Manifest-driven deploy and revert actions are not allowed to touch `~/.inception-engine/`, and the registry loader refuses symlinked registry files or directories.
|
|
548
|
+
|
|
545
549
|
- **Atomic redeploy**: When overwriting an existing managed `skill-dir` target, the engine renames the old target to a backup, creates the new deployment, and only removes the backup on success. If the new deployment fails, the backup is restored. `file-write` and `config-patch` deployments write directly to the target without this backup/rollback model.
|
|
546
550
|
|
|
547
|
-
- **Cross-platform**: The registry uses the same resolved home directory as the rest of the tool, including sudo scenarios on POSIX and elevated PowerShell on Windows.
|
|
551
|
+
- **Cross-platform**: The registry uses the same resolved home directory as the rest of the tool, including sudo scenarios on POSIX and elevated PowerShell on Windows. On POSIX, inception-engine keeps the state directory at `0700` and the registry file at `0600`.
|
|
548
552
|
|
|
549
553
|
## Running with Privilege Escalation
|
|
550
554
|
|
package/dist/src/core/deploy.js
CHANGED
|
@@ -9,7 +9,7 @@ import { compileAdapterActions } from "./adapters/index.js";
|
|
|
9
9
|
import { applyTomlMcpPatch } from "./adapters/toml.js";
|
|
10
10
|
import { planCapabilityForDeploy } from "./capabilities.js";
|
|
11
11
|
import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
|
|
12
|
-
import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
12
|
+
import { lookupDeployment, registryDirPath, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
13
13
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
14
14
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
15
15
|
import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
|
|
@@ -153,6 +153,62 @@ function detectAmbiguities(detectedAgents, manifest) {
|
|
|
153
153
|
}
|
|
154
154
|
return warnings;
|
|
155
155
|
}
|
|
156
|
+
function normalizeTemplatePath(template) {
|
|
157
|
+
return template.replaceAll("\\", "/");
|
|
158
|
+
}
|
|
159
|
+
function isRepoScopedTemplate(template) {
|
|
160
|
+
return template.startsWith("{repo}") || template.startsWith("{workspace}");
|
|
161
|
+
}
|
|
162
|
+
function normalizePathForComparison(candidate) {
|
|
163
|
+
const normalized = path.normalize(candidate);
|
|
164
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
165
|
+
}
|
|
166
|
+
function isSameOrDescendantPath(candidate, root) {
|
|
167
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
168
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
169
|
+
return (normalizedCandidate === normalizedRoot ||
|
|
170
|
+
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
171
|
+
}
|
|
172
|
+
function collectApprovedGlobalSurfaceTemplates(agentId) {
|
|
173
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
174
|
+
if (!agent)
|
|
175
|
+
return new Set();
|
|
176
|
+
const supports = [
|
|
177
|
+
agent.mcpSupport,
|
|
178
|
+
agent.agentRulesSupport,
|
|
179
|
+
agent.permissionsSupport,
|
|
180
|
+
agent.hooksSupport,
|
|
181
|
+
agent.executionConfigSupport,
|
|
182
|
+
];
|
|
183
|
+
const approved = new Set();
|
|
184
|
+
for (const support of supports) {
|
|
185
|
+
if (!support || support.status !== "supported")
|
|
186
|
+
continue;
|
|
187
|
+
approved.add(normalizeTemplatePath(support.path.posix.join("/")));
|
|
188
|
+
approved.add(normalizeTemplatePath(support.path.windows.join("/")));
|
|
189
|
+
}
|
|
190
|
+
return approved;
|
|
191
|
+
}
|
|
192
|
+
function assertApprovedManagedTargetTemplate(template, targetAgents, kind) {
|
|
193
|
+
if (isRepoScopedTemplate(template))
|
|
194
|
+
return;
|
|
195
|
+
const normalizedTemplate = normalizeTemplatePath(template);
|
|
196
|
+
for (const agentId of targetAgents) {
|
|
197
|
+
const approvedTemplates = collectApprovedGlobalSurfaceTemplates(agentId);
|
|
198
|
+
if (approvedTemplates.has(normalizedTemplate)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
throw new UserError("DEPLOY_FAILED", `${kind} target "${template}" is not an approved managed surface. ` +
|
|
203
|
+
`Use {repo}/... or {workspace}/... for arbitrary project-local paths, ` +
|
|
204
|
+
`or target a documented agent-owned global config surface.`);
|
|
205
|
+
}
|
|
206
|
+
function assertTargetOutsideReservedEngineState(targetPath, home) {
|
|
207
|
+
const reservedDir = registryDirPath(home);
|
|
208
|
+
if (isSameOrDescendantPath(targetPath, reservedDir)) {
|
|
209
|
+
throw new UserError("DEPLOY_FAILED", `Target "${targetPath}" is inside inception-engine state directory "${reservedDir}" and cannot be managed by manifests`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
156
212
|
function checkAntigravityPathCollisions(manifest) {
|
|
157
213
|
const warnings = [];
|
|
158
214
|
const defNames = new Set((manifest.agentDefinitions ?? [])
|
|
@@ -223,6 +279,7 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
|
|
|
223
279
|
const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
224
280
|
if (targetAgents.length === 0)
|
|
225
281
|
return;
|
|
282
|
+
assertApprovedManagedTargetTemplate(fileEntry.target, targetAgents, "files");
|
|
226
283
|
const source = path.resolve(sourceDir, fileEntry.path);
|
|
227
284
|
await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
|
|
228
285
|
await validateSourceFile(source, fileEntry.path);
|
|
@@ -245,6 +302,10 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
|
|
|
245
302
|
function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace) {
|
|
246
303
|
const actions = [];
|
|
247
304
|
for (const configEntry of manifest.configs ?? []) {
|
|
305
|
+
const targetAgents = configEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
306
|
+
if (targetAgents.length === 0)
|
|
307
|
+
continue;
|
|
308
|
+
assertApprovedManagedTargetTemplate(configEntry.target, targetAgents, "configs");
|
|
248
309
|
for (const agentId of configEntry.agents) {
|
|
249
310
|
if (!detectedAgents.includes(agentId))
|
|
250
311
|
continue;
|
|
@@ -289,6 +350,9 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
|
|
|
289
350
|
...detectCollisions(actions),
|
|
290
351
|
...adapterResult.warnings,
|
|
291
352
|
];
|
|
353
|
+
for (const action of actions) {
|
|
354
|
+
assertTargetOutsideReservedEngineState(action.target, home);
|
|
355
|
+
}
|
|
292
356
|
return { actions, warnings };
|
|
293
357
|
}
|
|
294
358
|
export async function executeDeploy(actions, dryRun, verbose, home, deps = {}) {
|
|
@@ -21,6 +21,7 @@ export type VerifyExpected = {
|
|
|
21
21
|
agent: AgentId;
|
|
22
22
|
};
|
|
23
23
|
export declare function registryPath(home: string): string;
|
|
24
|
+
export declare function registryDirPath(home: string): string;
|
|
24
25
|
export declare const defaultRegistryPersistence: RegistryPersistence;
|
|
25
26
|
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed"> | Omit<FrontmatterEmitRegistryEntry, "deployed">;
|
|
26
27
|
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RegistrySchema, } from "../schemas/registry.js";
|
|
4
4
|
const REGISTRY_DIR = ".inception-engine";
|
|
@@ -6,8 +6,26 @@ const REGISTRY_FILE = "registry.json";
|
|
|
6
6
|
export function registryPath(home) {
|
|
7
7
|
return path.join(home, REGISTRY_DIR, REGISTRY_FILE);
|
|
8
8
|
}
|
|
9
|
+
export function registryDirPath(home) {
|
|
10
|
+
return path.join(home, REGISTRY_DIR);
|
|
11
|
+
}
|
|
12
|
+
async function assertSafeRegistryStoragePath(targetPath, label) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = await lstat(targetPath);
|
|
15
|
+
if (stat.isSymbolicLink()) {
|
|
16
|
+
throw new Error(`Refusing to use ${label} symlink: ${targetPath}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err.code === "ENOENT")
|
|
21
|
+
return;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
async function loadRegistry(home) {
|
|
10
26
|
try {
|
|
27
|
+
await assertSafeRegistryStoragePath(registryDirPath(home), "registry directory");
|
|
28
|
+
await assertSafeRegistryStoragePath(registryPath(home), "registry file");
|
|
11
29
|
const content = await readFile(registryPath(home), "utf-8");
|
|
12
30
|
const parsed = JSON.parse(content);
|
|
13
31
|
const result = RegistrySchema.safeParse(parsed);
|
|
@@ -18,9 +36,12 @@ async function loadRegistry(home) {
|
|
|
18
36
|
}
|
|
19
37
|
}
|
|
20
38
|
async function saveRegistry(home, registry) {
|
|
21
|
-
const dir =
|
|
39
|
+
const dir = registryDirPath(home);
|
|
40
|
+
await assertSafeRegistryStoragePath(dir, "registry directory");
|
|
22
41
|
await mkdir(dir, { recursive: true });
|
|
42
|
+
await setDirectoryPermissions(dir);
|
|
23
43
|
const filePath = registryPath(home);
|
|
44
|
+
await assertSafeRegistryStoragePath(filePath, "registry file");
|
|
24
45
|
await writeFile(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
25
46
|
await setFilePermissions(filePath);
|
|
26
47
|
}
|
|
@@ -29,12 +50,21 @@ export const defaultRegistryPersistence = {
|
|
|
29
50
|
save: saveRegistry,
|
|
30
51
|
};
|
|
31
52
|
/**
|
|
32
|
-
*
|
|
53
|
+
* Restrict access to the state directory regardless of umask.
|
|
54
|
+
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
55
|
+
*/
|
|
56
|
+
async function setDirectoryPermissions(dirPath) {
|
|
57
|
+
if (process.platform !== "win32") {
|
|
58
|
+
await chmod(dirPath, 0o700);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Restrict the registry file to the current user regardless of umask.
|
|
33
63
|
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
34
64
|
*/
|
|
35
65
|
async function setFilePermissions(filePath) {
|
|
36
66
|
if (process.platform !== "win32") {
|
|
37
|
-
await chmod(filePath,
|
|
67
|
+
await chmod(filePath, 0o600);
|
|
38
68
|
}
|
|
39
69
|
}
|
|
40
70
|
function emptyRegistry() {
|
package/dist/src/core/revert.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
4
5
|
import * as frontmatterAdapter from "./adapters/frontmatter.js";
|
|
5
6
|
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileExecutionConfigReverts, compileHookReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
6
7
|
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
7
8
|
import { applyUndoPatch } from "./merge-patch.js";
|
|
8
|
-
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
9
|
+
import { lookupDeployment, registryDirPath, unregisterDeployment, } from "./ownership.js";
|
|
9
10
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
10
11
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
11
12
|
function buildSkillDirReverts(manifest, home, agentFilter) {
|
|
@@ -125,6 +126,54 @@ function lstatOutcome(err) {
|
|
|
125
126
|
const msg = err instanceof Error ? err.message : String(err);
|
|
126
127
|
return { outcome: "fail", error: msg };
|
|
127
128
|
}
|
|
129
|
+
function normalizePathForComparison(candidate) {
|
|
130
|
+
const normalized = path.normalize(candidate);
|
|
131
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
132
|
+
}
|
|
133
|
+
function isSameOrDescendantPath(candidate, root) {
|
|
134
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
135
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
136
|
+
return (normalizedCandidate === normalizedRoot ||
|
|
137
|
+
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
138
|
+
}
|
|
139
|
+
function failReservedStateTarget(action, home) {
|
|
140
|
+
const reservedDir = registryDirPath(home);
|
|
141
|
+
if (!isSameOrDescendantPath(action.target, reservedDir)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
outcome: "fail",
|
|
146
|
+
error: `Refusing to modify inception-engine state directory "${reservedDir}" ` +
|
|
147
|
+
`via manifest-managed revert target "${action.target}"`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function preflightManagedSkillDirRevert(action, label, home, deps) {
|
|
151
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
152
|
+
if (reservedTargetFailure) {
|
|
153
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
154
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
155
|
+
}
|
|
156
|
+
return reservedTargetFailure;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await lstat(action.target);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const result = lstatOutcome(err);
|
|
163
|
+
if (result.outcome === "skip") {
|
|
164
|
+
logger.skip(label, "(not found, skipping)");
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
logger.fail(label, result.error);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
171
|
+
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
172
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
173
|
+
return { outcome: "skip" };
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
128
177
|
export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
129
178
|
const failed = [];
|
|
130
179
|
const planned = [];
|
|
@@ -198,6 +247,13 @@ async function applyFrontmatterRevert(action, frontmatterEntry) {
|
|
|
198
247
|
}
|
|
199
248
|
async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
200
249
|
const label = `${action.skill} -> ${action.agent}`;
|
|
250
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
251
|
+
if (reservedTargetFailure) {
|
|
252
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
253
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
254
|
+
}
|
|
255
|
+
return reservedTargetFailure;
|
|
256
|
+
}
|
|
201
257
|
try {
|
|
202
258
|
await lstat(action.target);
|
|
203
259
|
}
|
|
@@ -245,22 +301,9 @@ async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, dep
|
|
|
245
301
|
}
|
|
246
302
|
async function executeRevertAction(action, dryRun, verbose, home, planned, deps) {
|
|
247
303
|
const label = `${action.skill} -> ${action.agent}`;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
catch (err) {
|
|
252
|
-
const result = lstatOutcome(err);
|
|
253
|
-
if (result.outcome === "skip") {
|
|
254
|
-
logger.skip(label, "(not found, skipping)");
|
|
255
|
-
return result;
|
|
256
|
-
}
|
|
257
|
-
logger.fail(label, result.error);
|
|
258
|
-
return result;
|
|
259
|
-
}
|
|
260
|
-
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
261
|
-
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
262
|
-
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
263
|
-
return { outcome: "skip" };
|
|
304
|
+
const preflight = await preflightManagedSkillDirRevert(action, label, home, deps);
|
|
305
|
+
if (preflight) {
|
|
306
|
+
return preflight;
|
|
264
307
|
}
|
|
265
308
|
if (dryRun) {
|
|
266
309
|
planned.push({
|
|
@@ -302,6 +345,13 @@ async function executeRevertAction(action, dryRun, verbose, home, planned, deps)
|
|
|
302
345
|
}
|
|
303
346
|
async function revertFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
304
347
|
const label = `${action.skill} -> ${action.agent}`;
|
|
348
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
349
|
+
if (reservedTargetFailure) {
|
|
350
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
351
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
352
|
+
}
|
|
353
|
+
return reservedTargetFailure;
|
|
354
|
+
}
|
|
305
355
|
try {
|
|
306
356
|
await lstat(action.target);
|
|
307
357
|
}
|
|
@@ -353,6 +403,13 @@ async function revertFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
|
353
403
|
}
|
|
354
404
|
async function revertConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
355
405
|
const label = `${action.skill} -> ${action.agent}`;
|
|
406
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
407
|
+
if (reservedTargetFailure) {
|
|
408
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
409
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
410
|
+
}
|
|
411
|
+
return reservedTargetFailure;
|
|
412
|
+
}
|
|
356
413
|
try {
|
|
357
414
|
await lstat(action.target);
|
|
358
415
|
}
|
|
@@ -404,6 +461,13 @@ async function revertConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
404
461
|
}
|
|
405
462
|
async function revertTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
406
463
|
const label = `${action.skill} -> ${action.agent}`;
|
|
464
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
465
|
+
if (reservedTargetFailure) {
|
|
466
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
467
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
468
|
+
}
|
|
469
|
+
return reservedTargetFailure;
|
|
470
|
+
}
|
|
407
471
|
try {
|
|
408
472
|
await lstat(action.target);
|
|
409
473
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function normalizeSlashes(value: string): string;
|
|
6
6
|
/**
|
|
7
|
-
* Assert that a path ends with the given
|
|
8
|
-
*
|
|
7
|
+
* Assert that a path ends with the given suffix, normalizing separators on both
|
|
8
|
+
* sides so callers can safely use either literals or path.join(...).
|
|
9
9
|
*/
|
|
10
10
|
export declare function assertPathEndsWith(actual: string, suffix: string, msg?: string): void;
|
|
@@ -7,9 +7,9 @@ export function normalizeSlashes(value) {
|
|
|
7
7
|
return value.replaceAll("\\", "/");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Assert that a path ends with the given
|
|
11
|
-
*
|
|
10
|
+
* Assert that a path ends with the given suffix, normalizing separators on both
|
|
11
|
+
* sides so callers can safely use either literals or path.join(...).
|
|
12
12
|
*/
|
|
13
13
|
export function assertPathEndsWith(actual, suffix, msg) {
|
|
14
|
-
assert.ok(normalizeSlashes(actual).endsWith(suffix), msg ?? `Expected path "${actual}" to end with "${suffix}"`);
|
|
14
|
+
assert.ok(normalizeSlashes(actual).endsWith(normalizeSlashes(suffix)), msg ?? `Expected path "${actual}" to end with "${suffix}"`);
|
|
15
15
|
}
|