toolcraft 0.0.23 → 0.0.25
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 +2 -2
- package/dist/cli.compile-check.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +50 -13
- package/dist/error-report.js +32 -3
- package/dist/human-in-loop/approval-tasks.d.ts +1 -0
- package/dist/human-in-loop/approval-tasks.js +7 -5
- package/dist/human-in-loop/approvals-commands.js +51 -8
- package/dist/human-in-loop/runner.js +24 -19
- package/dist/human-in-loop/state-machine.d.ts +3 -3
- package/dist/human-in-loop/state-machine.js +13 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -1
- package/dist/mcp-proxy.js +85 -19
- package/dist/mcp.compile-check.js +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +50 -8
- package/dist/renderer.js +119 -13
- package/dist/sdk.compile-check.js +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +56 -11
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
- package/node_modules/@poe-code/agent-defs/package.json +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
- package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
- package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
- package/node_modules/@poe-code/config-mutations/package.json +1 -1
- package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
- package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
- package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
- package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
- package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
- package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
- package/node_modules/@poe-code/design-system/package.json +2 -1
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
- package/node_modules/@poe-code/process-runner/package.json +1 -1
- package/node_modules/@poe-code/task-list/README.md +0 -2
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +2 -0
- package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/move.js +215 -0
- package/node_modules/@poe-code/task-list/dist/open.js +3 -4
- package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
- package/node_modules/@poe-code/task-list/dist/state.js +9 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
- package/node_modules/@poe-code/task-list/package.json +1 -2
- package/node_modules/auth-store/dist/create-secret-store.js +4 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
- package/node_modules/auth-store/dist/index.d.ts +1 -1
- package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
- package/node_modules/auth-store/dist/keychain-store.js +18 -16
- package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
- package/node_modules/auth-store/dist/provider-store.js +55 -7
- package/node_modules/auth-store/dist/types.d.ts +3 -1
- package/node_modules/auth-store/package.json +2 -1
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
- package/node_modules/mcp-oauth/package.json +1 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
- package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
- package/node_modules/tiny-mcp-client/package.json +2 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
- package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
- package/package.json +10 -12
- package/node_modules/@poe-code/file-lock/README.md +0 -52
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
- package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
- package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
- package/node_modules/@poe-code/file-lock/package.json +0 -23
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { renderTemplate } from "@poe-code/design-system";
|
|
2
3
|
import { getConfigFormat, detectFormat } from "../formats/index.js";
|
|
3
4
|
import { resolvePath } from "./path-utils.js";
|
|
@@ -15,9 +16,78 @@ function createInvalidDocumentBackupPath(targetPath) {
|
|
|
15
16
|
const ext = targetPath.includes(".") ? targetPath.split(".").pop() : "bak";
|
|
16
17
|
return `${targetPath}.invalid-${createTimestamp()}.${ext}`;
|
|
17
18
|
}
|
|
18
|
-
async function backupInvalidDocument(
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
async function backupInvalidDocument(context, targetPath, content) {
|
|
20
|
+
const baseBackupPath = createInvalidDocumentBackupPath(targetPath);
|
|
21
|
+
let attempt = 0;
|
|
22
|
+
while (true) {
|
|
23
|
+
const backupPath = attempt === 0 ? baseBackupPath : `${baseBackupPath}-${attempt}`;
|
|
24
|
+
await assertRegularWriteTarget(context, backupPath);
|
|
25
|
+
try {
|
|
26
|
+
await context.fs.writeFile(backupPath, content, { encoding: "utf8", flag: "wx" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!isAlreadyExists(error)) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
attempt += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isAlreadyExists(error) {
|
|
38
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
39
|
+
}
|
|
40
|
+
async function assertRegularWriteTarget(context, targetPath) {
|
|
41
|
+
// Symlinks inside the managed home directory are untrusted: an attacker could
|
|
42
|
+
// plant one to redirect a credential/config write outside it. Symlinks at or
|
|
43
|
+
// above home are legitimate system links (e.g. /tmp -> /private/tmp on macOS,
|
|
44
|
+
// /var -> /private/var) and must not block writes, so bound the walk at home.
|
|
45
|
+
const boundary = path.dirname(path.resolve(context.homeDir));
|
|
46
|
+
let currentPath = path.resolve(targetPath);
|
|
47
|
+
while (currentPath !== boundary) {
|
|
48
|
+
try {
|
|
49
|
+
if ((await context.fs.lstat(currentPath)).isSymbolicLink()) {
|
|
50
|
+
throw new Error(`Refusing mutation write through symbolic link: ${currentPath}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!isNotFound(error)) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const parentPath = path.dirname(currentPath);
|
|
59
|
+
if (parentPath === currentPath) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
currentPath = parentPath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function writeAtomically(context, targetPath, content) {
|
|
66
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
67
|
+
let attempt = 0;
|
|
68
|
+
while (true) {
|
|
69
|
+
const tempPath = `${targetPath}.mutation-tmp-${attempt}`;
|
|
70
|
+
try {
|
|
71
|
+
await context.fs.writeFile(tempPath, content, { encoding: "utf8", flag: "wx" });
|
|
72
|
+
await context.fs.rename(tempPath, targetPath);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (isAlreadyExists(error)) {
|
|
77
|
+
attempt += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await context.fs.unlink(tempPath);
|
|
82
|
+
}
|
|
83
|
+
catch (cleanupError) {
|
|
84
|
+
if (!isNotFound(cleanupError)) {
|
|
85
|
+
void cleanupError;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
21
91
|
}
|
|
22
92
|
function describeMutation(kind, targetPath) {
|
|
23
93
|
const displayPath = targetPath ?? "target";
|
|
@@ -28,6 +98,8 @@ function describeMutation(kind, targetPath) {
|
|
|
28
98
|
return `Remove directory ${displayPath}`;
|
|
29
99
|
case "backup":
|
|
30
100
|
return `Backup ${displayPath}`;
|
|
101
|
+
case "restoreBackup":
|
|
102
|
+
return `Restore ${displayPath}`;
|
|
31
103
|
case "templateWrite":
|
|
32
104
|
return `Write ${displayPath}`;
|
|
33
105
|
case "chmod":
|
|
@@ -91,6 +163,8 @@ export async function applyMutation(mutation, context, options) {
|
|
|
91
163
|
return applyChmod(mutation, context, options);
|
|
92
164
|
case "backup":
|
|
93
165
|
return applyBackup(mutation, context, options);
|
|
166
|
+
case "restoreBackup":
|
|
167
|
+
return applyRestoreBackup(mutation, context, options);
|
|
94
168
|
case "configMerge":
|
|
95
169
|
return applyConfigMerge(mutation, context, options);
|
|
96
170
|
case "configPrune":
|
|
@@ -190,6 +264,9 @@ async function applyRemoveFile(mutation, context, options) {
|
|
|
190
264
|
const content = await context.fs.readFile(targetPath, "utf8");
|
|
191
265
|
const trimmed = content.trim();
|
|
192
266
|
// Check whenContentMatches guard
|
|
267
|
+
if (mutation.whenContentMatches) {
|
|
268
|
+
mutation.whenContentMatches.lastIndex = 0;
|
|
269
|
+
}
|
|
193
270
|
if (mutation.whenContentMatches && !mutation.whenContentMatches.test(trimmed)) {
|
|
194
271
|
return {
|
|
195
272
|
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
@@ -270,22 +347,122 @@ async function applyBackup(mutation, context, options) {
|
|
|
270
347
|
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
271
348
|
targetPath
|
|
272
349
|
};
|
|
350
|
+
if (mutation.once && (await findLatestGeneratedBackup(context.fs, targetPath)) !== null) {
|
|
351
|
+
return {
|
|
352
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
353
|
+
details
|
|
354
|
+
};
|
|
355
|
+
}
|
|
273
356
|
const content = await readFileIfExists(context.fs, targetPath);
|
|
274
|
-
if (content === null) {
|
|
357
|
+
if (content === null && !mutation.once) {
|
|
275
358
|
return {
|
|
276
359
|
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
277
360
|
details
|
|
278
361
|
};
|
|
279
362
|
}
|
|
280
363
|
if (!context.dryRun) {
|
|
281
|
-
const
|
|
282
|
-
|
|
364
|
+
const baseBackupPath = `${targetPath}.backup-${createTimestamp()}${content === null ? ".missing" : ""}`;
|
|
365
|
+
let attempt = 0;
|
|
366
|
+
while (true) {
|
|
367
|
+
const backupPath = attempt === 0 ? baseBackupPath : `${baseBackupPath}-${attempt}`;
|
|
368
|
+
try {
|
|
369
|
+
await assertRegularWriteTarget(context, backupPath);
|
|
370
|
+
await context.fs.writeFile(backupPath, content ?? "", { encoding: "utf8", flag: "wx" });
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (!isAlreadyExists(error)) {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
attempt += 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
283
380
|
}
|
|
284
381
|
return {
|
|
285
382
|
outcome: { changed: true, effect: "copy", detail: "backup" },
|
|
286
383
|
details
|
|
287
384
|
};
|
|
288
385
|
}
|
|
386
|
+
async function applyRestoreBackup(mutation, context, options) {
|
|
387
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
388
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
389
|
+
const details = {
|
|
390
|
+
kind: mutation.kind,
|
|
391
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
392
|
+
targetPath
|
|
393
|
+
};
|
|
394
|
+
const backup = await findLatestGeneratedBackup(context.fs, targetPath);
|
|
395
|
+
if (backup === null) {
|
|
396
|
+
return { outcome: { changed: false, effect: "none", detail: "noop" }, details };
|
|
397
|
+
}
|
|
398
|
+
if (!context.dryRun) {
|
|
399
|
+
await assertRegularWriteTarget(context, backup.path);
|
|
400
|
+
if (backup.originallyMissing) {
|
|
401
|
+
try {
|
|
402
|
+
await context.fs.unlink(targetPath);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
if (!isNotFound(error)) {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
const content = await context.fs.readFile(backup.path, "utf8");
|
|
412
|
+
await writeAtomically(context, targetPath, content);
|
|
413
|
+
}
|
|
414
|
+
await context.fs.unlink(backup.path);
|
|
415
|
+
}
|
|
416
|
+
return { outcome: { changed: true, effect: "copy", detail: "restore" }, details };
|
|
417
|
+
}
|
|
418
|
+
async function findLatestGeneratedBackup(fs, targetPath) {
|
|
419
|
+
const separatorIndex = targetPath.lastIndexOf("/");
|
|
420
|
+
const directoryPath = separatorIndex <= 0 ? "/" : targetPath.slice(0, separatorIndex);
|
|
421
|
+
const targetName = targetPath.slice(separatorIndex + 1);
|
|
422
|
+
let entries;
|
|
423
|
+
try {
|
|
424
|
+
entries = await fs.readdir(directoryPath);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (isNotFound(error)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const backupName = entries
|
|
433
|
+
.filter((entry) => isGeneratedBackupName(entry, targetName))
|
|
434
|
+
.sort()
|
|
435
|
+
.at(-1);
|
|
436
|
+
return backupName === undefined
|
|
437
|
+
? null
|
|
438
|
+
: {
|
|
439
|
+
path: `${directoryPath}/${backupName}`,
|
|
440
|
+
originallyMissing: backupName.endsWith(".missing")
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function isGeneratedBackupName(entry, targetName) {
|
|
444
|
+
const prefix = `${targetName}.backup-`;
|
|
445
|
+
if (!entry.startsWith(prefix)) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const suffix = entry.slice(prefix.length);
|
|
449
|
+
const timestamp = suffix.slice(0, 24);
|
|
450
|
+
if (timestamp.length !== 24 || timestamp[4] !== "-" || timestamp[7] !== "-" || timestamp[10] !== "T" || timestamp[13] !== "-" || timestamp[16] !== "-" || timestamp[19] !== "-" || timestamp[23] !== "Z") {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const parsedTimestamp = `${timestamp.slice(0, 13)}:${timestamp.slice(14, 16)}:${timestamp.slice(17, 19)}.${timestamp.slice(20)}`;
|
|
454
|
+
if (Number.isNaN(Date.parse(parsedTimestamp))) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
const collisionSuffix = suffix.slice(24);
|
|
458
|
+
if (collisionSuffix.length === 0) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
if (collisionSuffix === ".missing") {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
return collisionSuffix[0] === "-" && collisionSuffix.slice(1).length > 0 && [...collisionSuffix.slice(1)].every((character) => character >= "0" && character <= "9");
|
|
465
|
+
}
|
|
289
466
|
// ============================================================================
|
|
290
467
|
// Config Mutation Handlers
|
|
291
468
|
// ============================================================================
|
|
@@ -309,8 +486,8 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
309
486
|
}
|
|
310
487
|
catch {
|
|
311
488
|
// Invalid file - backup and start fresh
|
|
312
|
-
if (rawContent !== null) {
|
|
313
|
-
await backupInvalidDocument(context
|
|
489
|
+
if (rawContent !== null && !context.dryRun) {
|
|
490
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
314
491
|
}
|
|
315
492
|
current = {};
|
|
316
493
|
}
|
|
@@ -326,7 +503,7 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
326
503
|
const serialized = format.serialize(merged);
|
|
327
504
|
const changed = serialized !== rawContent;
|
|
328
505
|
if (changed && !context.dryRun) {
|
|
329
|
-
await context
|
|
506
|
+
await writeAtomically(context, targetPath, serialized);
|
|
330
507
|
}
|
|
331
508
|
return {
|
|
332
509
|
outcome: {
|
|
@@ -395,7 +572,7 @@ async function applyConfigPrune(mutation, context, options) {
|
|
|
395
572
|
}
|
|
396
573
|
const serialized = format.serialize(result);
|
|
397
574
|
if (!context.dryRun) {
|
|
398
|
-
await context
|
|
575
|
+
await writeAtomically(context, targetPath, serialized);
|
|
399
576
|
}
|
|
400
577
|
return {
|
|
401
578
|
outcome: { changed: true, effect: "write", detail: "update" },
|
|
@@ -421,8 +598,8 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
421
598
|
current = rawContent === null ? {} : format.parse(rawContent);
|
|
422
599
|
}
|
|
423
600
|
catch {
|
|
424
|
-
if (rawContent !== null) {
|
|
425
|
-
await backupInvalidDocument(context
|
|
601
|
+
if (rawContent !== null && !context.dryRun) {
|
|
602
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
426
603
|
}
|
|
427
604
|
current = {};
|
|
428
605
|
}
|
|
@@ -451,7 +628,7 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
451
628
|
}
|
|
452
629
|
const serialized = format.serialize(transformed);
|
|
453
630
|
if (!context.dryRun) {
|
|
454
|
-
await context
|
|
631
|
+
await writeAtomically(context, targetPath, serialized);
|
|
455
632
|
}
|
|
456
633
|
return {
|
|
457
634
|
outcome: {
|
|
@@ -482,15 +659,16 @@ async function applyTemplateWrite(mutation, context, options) {
|
|
|
482
659
|
? resolveValue(mutation.context, options)
|
|
483
660
|
: {};
|
|
484
661
|
const rendered = renderTemplate(template, templateContext);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
662
|
+
const current = await readFileIfExists(context.fs, targetPath);
|
|
663
|
+
const changed = current !== rendered;
|
|
664
|
+
if (changed && !context.dryRun) {
|
|
665
|
+
await writeAtomically(context, targetPath, rendered);
|
|
488
666
|
}
|
|
489
667
|
return {
|
|
490
668
|
outcome: {
|
|
491
|
-
changed
|
|
492
|
-
effect: "write",
|
|
493
|
-
detail:
|
|
669
|
+
changed,
|
|
670
|
+
effect: changed ? "write" : "none",
|
|
671
|
+
detail: changed ? (current === null ? "create" : "update") : "noop"
|
|
494
672
|
},
|
|
495
673
|
details
|
|
496
674
|
};
|
|
@@ -529,8 +707,8 @@ async function applyTemplateMerge(mutation, context, options, formatName) {
|
|
|
529
707
|
current = rawContent === null ? {} : format.parse(rawContent);
|
|
530
708
|
}
|
|
531
709
|
catch {
|
|
532
|
-
if (rawContent !== null) {
|
|
533
|
-
await backupInvalidDocument(context
|
|
710
|
+
if (rawContent !== null && !context.dryRun) {
|
|
711
|
+
await backupInvalidDocument(context, targetPath, rawContent);
|
|
534
712
|
}
|
|
535
713
|
current = {};
|
|
536
714
|
}
|
|
@@ -539,7 +717,7 @@ async function applyTemplateMerge(mutation, context, options, formatName) {
|
|
|
539
717
|
const serialized = format.serialize(merged);
|
|
540
718
|
const changed = serialized !== rawContent;
|
|
541
719
|
if (changed && !context.dryRun) {
|
|
542
|
-
await context
|
|
720
|
+
await writeAtomically(context, targetPath, serialized);
|
|
543
721
|
}
|
|
544
722
|
return {
|
|
545
723
|
outcome: {
|
|
@@ -45,8 +45,14 @@ export function validateHomePath(targetPath) {
|
|
|
45
45
|
export function resolvePath(rawPath, homeDir, pathMapper) {
|
|
46
46
|
validateHomePath(rawPath);
|
|
47
47
|
const expanded = expandHome(rawPath, homeDir);
|
|
48
|
+
const canonicalHome = path.resolve(homeDir);
|
|
49
|
+
const canonicalExpanded = path.resolve(expanded);
|
|
50
|
+
const relative = path.relative(canonicalHome, canonicalExpanded);
|
|
51
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
52
|
+
throw new Error(`Target path resolves outside home directory: "${rawPath}"`);
|
|
53
|
+
}
|
|
48
54
|
if (!pathMapper) {
|
|
49
|
-
return
|
|
55
|
+
return canonicalExpanded;
|
|
50
56
|
}
|
|
51
57
|
// Map the directory portion
|
|
52
58
|
const rawDirectory = path.dirname(expanded);
|
|
@@ -17,7 +17,7 @@ const extensionMap = {
|
|
|
17
17
|
*/
|
|
18
18
|
export function getConfigFormat(pathOrFormat) {
|
|
19
19
|
// Check if it's an explicit format name
|
|
20
|
-
if (pathOrFormat
|
|
20
|
+
if (Object.prototype.hasOwnProperty.call(formatRegistry, pathOrFormat)) {
|
|
21
21
|
return formatRegistry[pathOrFormat];
|
|
22
22
|
}
|
|
23
23
|
// Try to detect from extension
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as jsonc from "jsonc-parser";
|
|
2
|
+
import { cloneConfigObject, hasConfigEntry, setConfigEntry } from "./object.js";
|
|
2
3
|
function isConfigObject(value) {
|
|
3
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -27,31 +28,31 @@ function parse(content) {
|
|
|
27
28
|
if (!isConfigObject(parsed)) {
|
|
28
29
|
throw new Error("Expected JSON object.");
|
|
29
30
|
}
|
|
30
|
-
return parsed;
|
|
31
|
+
return cloneConfigObject(parsed);
|
|
31
32
|
}
|
|
32
33
|
function serialize(obj) {
|
|
33
34
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
34
35
|
}
|
|
35
36
|
function merge(base, patch) {
|
|
36
|
-
const result =
|
|
37
|
+
const result = cloneConfigObject(base);
|
|
37
38
|
for (const [key, value] of Object.entries(patch)) {
|
|
38
39
|
if (value === undefined) {
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
42
|
const existing = result[key];
|
|
42
43
|
if (isConfigObject(existing) && isConfigObject(value)) {
|
|
43
|
-
result
|
|
44
|
+
setConfigEntry(result, key, merge(existing, value));
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
46
|
-
result
|
|
47
|
+
setConfigEntry(result, key, value);
|
|
47
48
|
}
|
|
48
49
|
return result;
|
|
49
50
|
}
|
|
50
51
|
function prune(obj, shape) {
|
|
51
52
|
let changed = false;
|
|
52
|
-
const result =
|
|
53
|
+
const result = cloneConfigObject(obj);
|
|
53
54
|
for (const [key, pattern] of Object.entries(shape)) {
|
|
54
|
-
if (!(key
|
|
55
|
+
if (!hasConfigEntry(result, key)) {
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
58
|
const current = result[key];
|
|
@@ -71,10 +72,13 @@ function prune(obj, shape) {
|
|
|
71
72
|
delete result[key];
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
74
|
-
result
|
|
75
|
+
setConfigEntry(result, key, childResult);
|
|
75
76
|
}
|
|
76
77
|
continue;
|
|
77
78
|
}
|
|
79
|
+
if (isConfigObject(pattern) && Object.keys(pattern).length > 0) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
78
82
|
delete result[key];
|
|
79
83
|
changed = true;
|
|
80
84
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConfigObject, ConfigValue } from "../types.js";
|
|
2
|
+
export declare function cloneConfigObject(value: ConfigObject): ConfigObject;
|
|
3
|
+
export declare function setConfigEntry(target: ConfigObject, key: string, value: ConfigValue): void;
|
|
4
|
+
export declare function hasConfigEntry(target: ConfigObject, key: string): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function cloneConfigObject(value) {
|
|
2
|
+
const result = {};
|
|
3
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4
|
+
setConfigEntry(result, key, cloneConfigValue(entry));
|
|
5
|
+
}
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
export function setConfigEntry(target, key, value) {
|
|
9
|
+
Object.defineProperty(target, key, {
|
|
10
|
+
configurable: true,
|
|
11
|
+
enumerable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function hasConfigEntry(target, key) {
|
|
17
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
18
|
+
}
|
|
19
|
+
function cloneConfigValue(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map((entry) => cloneConfigValue(entry));
|
|
22
|
+
}
|
|
23
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
24
|
+
return cloneConfigObject(value);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
2
|
+
import { cloneConfigObject, hasConfigEntry, setConfigEntry } from "./object.js";
|
|
2
3
|
function isConfigObject(value) {
|
|
3
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -10,32 +11,32 @@ function parse(content) {
|
|
|
10
11
|
if (!isConfigObject(parsed)) {
|
|
11
12
|
throw new Error("Expected TOML document to be a table.");
|
|
12
13
|
}
|
|
13
|
-
return parsed;
|
|
14
|
+
return cloneConfigObject(parsed);
|
|
14
15
|
}
|
|
15
16
|
function serialize(obj) {
|
|
16
17
|
const serialized = stringifyToml(obj);
|
|
17
18
|
return serialized.endsWith("\n") ? serialized : `${serialized}\n`;
|
|
18
19
|
}
|
|
19
20
|
function merge(base, patch) {
|
|
20
|
-
const result =
|
|
21
|
+
const result = cloneConfigObject(base);
|
|
21
22
|
for (const [key, value] of Object.entries(patch)) {
|
|
22
23
|
if (value === undefined) {
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
25
26
|
const existing = result[key];
|
|
26
27
|
if (isConfigObject(existing) && isConfigObject(value)) {
|
|
27
|
-
result
|
|
28
|
+
setConfigEntry(result, key, merge(existing, value));
|
|
28
29
|
continue;
|
|
29
30
|
}
|
|
30
|
-
result
|
|
31
|
+
setConfigEntry(result, key, value);
|
|
31
32
|
}
|
|
32
33
|
return result;
|
|
33
34
|
}
|
|
34
35
|
function prune(obj, shape) {
|
|
35
36
|
let changed = false;
|
|
36
|
-
const result =
|
|
37
|
+
const result = cloneConfigObject(obj);
|
|
37
38
|
for (const [key, pattern] of Object.entries(shape)) {
|
|
38
|
-
if (!(key
|
|
39
|
+
if (!hasConfigEntry(result, key)) {
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
42
|
const current = result[key];
|
|
@@ -55,12 +56,14 @@ function prune(obj, shape) {
|
|
|
55
56
|
delete result[key];
|
|
56
57
|
}
|
|
57
58
|
else {
|
|
58
|
-
result
|
|
59
|
+
setConfigEntry(result, key, childResult);
|
|
59
60
|
}
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
if (!isConfigObject(pattern) || Object.keys(pattern).length === 0) {
|
|
64
|
+
delete result[key];
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
return { changed, result };
|
|
66
69
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2
|
+
import { cloneConfigObject, hasConfigEntry, setConfigEntry } from "./object.js";
|
|
2
3
|
function isConfigObject(value) {
|
|
3
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -13,32 +14,32 @@ function parse(content) {
|
|
|
13
14
|
if (!isConfigObject(parsed)) {
|
|
14
15
|
throw new Error("Expected YAML object.");
|
|
15
16
|
}
|
|
16
|
-
return parsed;
|
|
17
|
+
return cloneConfigObject(parsed);
|
|
17
18
|
}
|
|
18
19
|
function serialize(obj) {
|
|
19
20
|
const serialized = stringifyYaml(obj);
|
|
20
21
|
return serialized.endsWith("\n") ? serialized : `${serialized}\n`;
|
|
21
22
|
}
|
|
22
23
|
function merge(base, patch) {
|
|
23
|
-
const result =
|
|
24
|
+
const result = cloneConfigObject(base);
|
|
24
25
|
for (const [key, value] of Object.entries(patch)) {
|
|
25
26
|
if (value === undefined) {
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
28
29
|
const existing = result[key];
|
|
29
30
|
if (isConfigObject(existing) && isConfigObject(value)) {
|
|
30
|
-
result
|
|
31
|
+
setConfigEntry(result, key, merge(existing, value));
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
|
-
result
|
|
34
|
+
setConfigEntry(result, key, value);
|
|
34
35
|
}
|
|
35
36
|
return result;
|
|
36
37
|
}
|
|
37
38
|
function prune(obj, shape) {
|
|
38
39
|
let changed = false;
|
|
39
|
-
const result =
|
|
40
|
+
const result = cloneConfigObject(obj);
|
|
40
41
|
for (const [key, pattern] of Object.entries(shape)) {
|
|
41
|
-
if (!(key
|
|
42
|
+
if (!hasConfigEntry(result, key)) {
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
44
45
|
const current = result[key];
|
|
@@ -56,12 +57,14 @@ function prune(obj, shape) {
|
|
|
56
57
|
delete result[key];
|
|
57
58
|
}
|
|
58
59
|
else {
|
|
59
|
-
result
|
|
60
|
+
setConfigEntry(result, key, childResult);
|
|
60
61
|
}
|
|
61
62
|
continue;
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (!isConfigObject(pattern) || Object.keys(pattern).length === 0) {
|
|
65
|
+
delete result[key];
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
return { changed, result };
|
|
67
70
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EnsureDirectoryMutation, RemoveDirectoryMutation, RemoveFileMutation, ChmodMutation, BackupMutation, ValueResolver } from "../types.js";
|
|
1
|
+
import type { EnsureDirectoryMutation, RemoveDirectoryMutation, RemoveFileMutation, ChmodMutation, BackupMutation, RestoreBackupMutation, ValueResolver } from "../types.js";
|
|
2
2
|
export interface EnsureDirectoryOptions {
|
|
3
3
|
/** Directory path (must start with ~) */
|
|
4
4
|
path: ValueResolver<string>;
|
|
@@ -34,6 +34,14 @@ export interface ChmodOptions {
|
|
|
34
34
|
export interface BackupOptions {
|
|
35
35
|
/** Target file path to backup (must start with ~) */
|
|
36
36
|
target: ValueResolver<string>;
|
|
37
|
+
/** Keep the first baseline only, including an originally missing target. */
|
|
38
|
+
once?: boolean;
|
|
39
|
+
/** Optional human-readable label for logging */
|
|
40
|
+
label?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RestoreBackupOptions {
|
|
43
|
+
/** Target file path whose latest generated backup should be restored. */
|
|
44
|
+
target: ValueResolver<string>;
|
|
37
45
|
/** Optional human-readable label for logging */
|
|
38
46
|
label?: string;
|
|
39
47
|
}
|
|
@@ -42,11 +50,13 @@ declare function remove(options: RemoveOptions): RemoveFileMutation;
|
|
|
42
50
|
declare function removeDirectory(options: RemoveDirectoryOptions): RemoveDirectoryMutation;
|
|
43
51
|
declare function chmod(options: ChmodOptions): ChmodMutation;
|
|
44
52
|
declare function backup(options: BackupOptions): BackupMutation;
|
|
53
|
+
declare function restoreBackup(options: RestoreBackupOptions): RestoreBackupMutation;
|
|
45
54
|
export declare const fileMutation: {
|
|
46
55
|
ensureDirectory: typeof ensureDirectory;
|
|
47
56
|
remove: typeof remove;
|
|
48
57
|
removeDirectory: typeof removeDirectory;
|
|
49
58
|
chmod: typeof chmod;
|
|
50
59
|
backup: typeof backup;
|
|
60
|
+
restoreBackup: typeof restoreBackup;
|
|
51
61
|
};
|
|
52
62
|
export {};
|
|
@@ -34,6 +34,14 @@ function backup(options) {
|
|
|
34
34
|
return {
|
|
35
35
|
kind: "backup",
|
|
36
36
|
target: options.target,
|
|
37
|
+
once: options.once,
|
|
38
|
+
label: options.label
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function restoreBackup(options) {
|
|
42
|
+
return {
|
|
43
|
+
kind: "restoreBackup",
|
|
44
|
+
target: options.target,
|
|
37
45
|
label: options.label
|
|
38
46
|
};
|
|
39
47
|
}
|
|
@@ -42,5 +50,6 @@ export const fileMutation = {
|
|
|
42
50
|
remove,
|
|
43
51
|
removeDirectory,
|
|
44
52
|
chmod,
|
|
45
|
-
backup
|
|
53
|
+
backup,
|
|
54
|
+
restoreBackup
|
|
46
55
|
};
|
|
@@ -46,8 +46,12 @@ export function createMockFs(initialFiles, homeDir = DEFAULT_HOME_DIR) {
|
|
|
46
46
|
return Buffer.from(content, "utf8");
|
|
47
47
|
},
|
|
48
48
|
async writeFile(filePath, content, options) {
|
|
49
|
-
void options; // TypeScript satisfaction
|
|
50
49
|
const absolutePath = expandPath(filePath, homeDir);
|
|
50
|
+
if (options?.flag === "wx" && absolutePath in files) {
|
|
51
|
+
const error = new Error(`EEXIST: file already exists, open '${absolutePath}'`);
|
|
52
|
+
error.code = "EEXIST";
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
51
55
|
// Ensure parent directory exists
|
|
52
56
|
const parentDir = path.dirname(absolutePath);
|
|
53
57
|
if (!directories.has(parentDir)) {
|
|
@@ -90,6 +94,17 @@ export function createMockFs(initialFiles, homeDir = DEFAULT_HOME_DIR) {
|
|
|
90
94
|
}
|
|
91
95
|
delete files[absolutePath];
|
|
92
96
|
},
|
|
97
|
+
async rename(oldPath, newPath) {
|
|
98
|
+
const absoluteOldPath = expandPath(oldPath, homeDir);
|
|
99
|
+
const absoluteNewPath = expandPath(newPath, homeDir);
|
|
100
|
+
if (!(absoluteOldPath in files)) {
|
|
101
|
+
const error = new Error(`ENOENT: no such file or directory, rename '${absoluteOldPath}'`);
|
|
102
|
+
error.code = "ENOENT";
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
files[absoluteNewPath] = files[absoluteOldPath];
|
|
106
|
+
delete files[absoluteOldPath];
|
|
107
|
+
},
|
|
93
108
|
async stat(filePath) {
|
|
94
109
|
const absolutePath = expandPath(filePath, homeDir);
|
|
95
110
|
if (absolutePath in files) {
|
|
@@ -102,6 +117,15 @@ export function createMockFs(initialFiles, homeDir = DEFAULT_HOME_DIR) {
|
|
|
102
117
|
error.code = "ENOENT";
|
|
103
118
|
throw error;
|
|
104
119
|
},
|
|
120
|
+
async lstat(filePath) {
|
|
121
|
+
const absolutePath = expandPath(filePath, homeDir);
|
|
122
|
+
if (absolutePath in files || directories.has(absolutePath)) {
|
|
123
|
+
return { isSymbolicLink: () => false };
|
|
124
|
+
}
|
|
125
|
+
const error = new Error(`ENOENT: no such file or directory, lstat '${absolutePath}'`);
|
|
126
|
+
error.code = "ENOENT";
|
|
127
|
+
throw error;
|
|
128
|
+
},
|
|
105
129
|
async readdir(dirPath) {
|
|
106
130
|
const absolutePath = expandPath(dirPath, homeDir);
|
|
107
131
|
if (absolutePath in files) {
|