deepseek-web-retrieval 0.4.0 → 0.5.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/CHANGELOG.md +99 -244
- package/README.md +16 -11
- package/dist/cli.js +1366 -615
- package/docs/agents/codex.md +28 -7
- package/docs/agents/opencode-v2.md +1 -1
- package/docs/agents/pi.md +88 -86
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +24 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +20 -25
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -112
package/src/installer/apply.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { codecFor, writeConfig } from "./formats";
|
|
4
|
+
import { deepEqual, valueAt } from "./formats/value";
|
|
5
|
+
import { withOperationLock } from "./operation-lock";
|
|
6
|
+
import { acquireFileOwnership, assertFileOwnership, releaseFileOwnership } from "./ownership";
|
|
7
|
+
import { pruneEmptyDir, revertRecordedFile } from "./revert";
|
|
8
|
+
import { deleteRecord, readRecord, statePathFor, writeRecord } from "./state";
|
|
9
|
+
import type { ConfigOp, FileEdit, InstallerInput, InstallPlan, InstallRecord, RecordedFile } from "./types";
|
|
7
10
|
|
|
8
11
|
export interface ApplyResult {
|
|
9
|
-
readonly lines: string[]
|
|
10
|
-
readonly ok: boolean
|
|
12
|
+
readonly lines: string[];
|
|
13
|
+
readonly ok: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A failed apply tells callers whether retrying is safe or recovery is still needed. */
|
|
17
|
+
export class ApplyError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly status: "rolled_back" | "recovery_required",
|
|
20
|
+
message: string,
|
|
21
|
+
cause: unknown,
|
|
22
|
+
) {
|
|
23
|
+
super(message, { cause });
|
|
24
|
+
this.name = "ApplyError";
|
|
25
|
+
}
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
interface PreparedEdit {
|
|
14
|
-
readonly edit: FileEdit
|
|
15
|
-
readonly recorded: RecordedFile
|
|
16
|
-
readonly write: () => void
|
|
29
|
+
readonly edit: FileEdit;
|
|
30
|
+
readonly recorded: RecordedFile;
|
|
31
|
+
readonly write: () => void;
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
/**
|
|
@@ -21,37 +36,110 @@ interface PreparedEdit {
|
|
|
21
36
|
* rendered (the plan and the record must describe one plan-time snapshot —
|
|
22
37
|
* ADR-0001), prepares every edit in memory (a malformed existing config fails
|
|
23
38
|
* before any file is written), writes the files, reverts artifacts orphaned by
|
|
24
|
-
* a tier change, records surgical-undo state
|
|
39
|
+
* a tier change, verifies the result, then records surgical-undo state. Write
|
|
40
|
+
* and verification failures restore touched files and the previous record.
|
|
41
|
+
* Incomplete recovery is reported explicitly and keeps new ownership claims
|
|
42
|
+
* so another install cannot adopt partially applied configuration.
|
|
25
43
|
*/
|
|
26
44
|
export function applyPlan(plan: InstallPlan, input: InstallerInput): ApplyResult {
|
|
27
45
|
if (plan.errors.length > 0) {
|
|
28
|
-
throw new Error(`refusing to apply a plan with errors: ${plan.errors.map((error) => error.message).join("; ")}`)
|
|
46
|
+
throw new Error(`refusing to apply a plan with errors: ${plan.errors.map((error) => error.message).join("; ")}`);
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
return withOperationLock([statePathFor(input)], () =>
|
|
49
|
+
withOperationLock(
|
|
50
|
+
[...plan.edits, ...(plan.reverts ?? [])].map((file) => file.file),
|
|
51
|
+
() => applyLocked(plan, input),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyLocked(plan: InstallPlan, input: InstallerInput): ApplyResult {
|
|
57
|
+
const touched = [...plan.edits, ...(plan.reverts ?? [])];
|
|
58
|
+
assertFileOwnership(input, touched);
|
|
59
|
+
assertNoDrift(plan);
|
|
60
|
+
const previous = readRecord(input);
|
|
61
|
+
if (Object.hasOwn(plan, "previousRecord") && !deepEqual(previous, plan.previousRecord)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"The install record changed since the plan was rendered. Re-run init to plan against the current install.",
|
|
64
|
+
);
|
|
39
65
|
}
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
|
|
67
|
+
const prepared = plan.edits.map((edit) => prepareEdit(edit, plan.host));
|
|
68
|
+
const snapshots = snapshotTouchedFiles(plan);
|
|
69
|
+
const files: RecordedFile[] = [];
|
|
70
|
+
const lines: string[] = [];
|
|
71
|
+
const releaseNewClaims = acquireFileOwnership(input, touched);
|
|
72
|
+
try {
|
|
73
|
+
for (const item of prepared) {
|
|
74
|
+
item.write();
|
|
75
|
+
const desired = item.edit.format === "rules" ? [] : item.edit.ops;
|
|
76
|
+
files.push(
|
|
77
|
+
mergeRecorded(
|
|
78
|
+
item.recorded,
|
|
79
|
+
previous?.files.find((file) => file.file === item.edit.file),
|
|
80
|
+
desired,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
lines.push(`${item.recorded.created ? "created" : "updated"} ${item.edit.file}`);
|
|
84
|
+
}
|
|
85
|
+
if ((plan.reverts ?? []).length > 0) {
|
|
86
|
+
lines.push(...applyReverts(plan));
|
|
87
|
+
}
|
|
88
|
+
const verification = verifyEdits(plan.edits, plan.host);
|
|
89
|
+
if (!verification.ok) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`verification failed: ${verification.lines.filter((line) => line.startsWith("FAIL")).join("; ")}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
lines.push(...verification.lines);
|
|
95
|
+
const record: InstallRecord = {
|
|
96
|
+
version: 1,
|
|
97
|
+
host: plan.host,
|
|
98
|
+
scope: plan.scope,
|
|
99
|
+
tiers: plan.tiers,
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
files,
|
|
102
|
+
};
|
|
103
|
+
writeRecord(input, record);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const rollback = rollbackApply(input, previous, snapshots);
|
|
106
|
+
if (rollback.complete) {
|
|
107
|
+
try {
|
|
108
|
+
releaseNewClaims();
|
|
109
|
+
} catch (releaseError) {
|
|
110
|
+
rollback.complete = false;
|
|
111
|
+
rollback.report.push(`could not release new config ownership: ${errorMessage(releaseError)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const status = rollback.complete ? "rolled_back" : "recovery_required";
|
|
115
|
+
const outcome = rollback.complete ? "was rolled back" : "recovery is required";
|
|
116
|
+
const guidance = rollback.complete
|
|
117
|
+
? "Fix the cause and re-run init."
|
|
118
|
+
: "Restore the reported files and ownership metadata before re-running init.";
|
|
119
|
+
throw new ApplyError(
|
|
120
|
+
status,
|
|
121
|
+
`apply failed and ${outcome} (${errorMessage(error)}). Rollback report: ${rollback.report.join("; ")}. ${guidance}`,
|
|
122
|
+
error,
|
|
123
|
+
);
|
|
42
124
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
125
|
+
try {
|
|
126
|
+
releaseFileOwnership(
|
|
127
|
+
input,
|
|
128
|
+
(previous?.files ?? []).filter((file) => !files.some((current) => current.file === file.file)),
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new ApplyError(
|
|
132
|
+
"recovery_required",
|
|
133
|
+
`apply was written and verified, but old config ownership could not be released (${errorMessage(error)}). ` +
|
|
134
|
+
"The new install record is retained; repair the reported ownership metadata before re-running init.",
|
|
135
|
+
error,
|
|
136
|
+
);
|
|
50
137
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
138
|
+
return { lines, ok: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function errorMessage(error: unknown): string {
|
|
142
|
+
return error instanceof Error ? error.message : String(error);
|
|
55
143
|
}
|
|
56
144
|
|
|
57
145
|
/**
|
|
@@ -61,19 +149,19 @@ export function applyPlan(plan: InstallPlan, input: InstallerInput): ApplyResult
|
|
|
61
149
|
* record's snapshots stay provably equal to what the user approved.
|
|
62
150
|
*/
|
|
63
151
|
function assertNoDrift(plan: InstallPlan): void {
|
|
64
|
-
const drifts: string[] = []
|
|
152
|
+
const drifts: string[] = [];
|
|
65
153
|
for (const edit of plan.edits) {
|
|
66
|
-
if (edit.format === "rules") continue
|
|
67
|
-
const loaded = codecFor(edit.format, plan.host).load(edit.file)
|
|
154
|
+
if (edit.format === "rules") continue;
|
|
155
|
+
const loaded = codecFor(edit.format, plan.host).load(edit.file);
|
|
68
156
|
if (loaded.parseError) {
|
|
69
|
-
drifts.push(`${edit.file} no longer parses (${loaded.parseError})`)
|
|
70
|
-
continue
|
|
157
|
+
drifts.push(`${edit.file} no longer parses (${loaded.parseError})`);
|
|
158
|
+
continue;
|
|
71
159
|
}
|
|
72
160
|
for (const op of edit.ops) {
|
|
73
|
-
if (op.op !== "set") continue
|
|
74
|
-
const current = valueAt(loaded.value, op.path)
|
|
161
|
+
if (op.op !== "set") continue;
|
|
162
|
+
const current = valueAt(loaded.value, op.path);
|
|
75
163
|
if (!deepEqual(current, op.previous)) {
|
|
76
|
-
drifts.push(`${edit.file}: ${op.path.join(".")} changed since the plan was rendered`)
|
|
164
|
+
drifts.push(`${edit.file}: ${op.path.join(".")} changed since the plan was rendered`);
|
|
77
165
|
}
|
|
78
166
|
}
|
|
79
167
|
}
|
|
@@ -81,26 +169,99 @@ function assertNoDrift(plan: InstallPlan): void {
|
|
|
81
169
|
throw new Error(
|
|
82
170
|
`refusing to apply — the config changed since the plan was rendered: ${drifts.join("; ")}. ` +
|
|
83
171
|
"Re-run init to re-plan against the current files.",
|
|
84
|
-
)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The on-disk state of one file before the apply mutated it. */
|
|
177
|
+
interface FileSnapshot {
|
|
178
|
+
readonly exists: boolean;
|
|
179
|
+
readonly text: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Captures the exact bytes of every file the apply may touch, edits and reverts. */
|
|
183
|
+
function snapshotTouchedFiles(plan: InstallPlan): Map<string, FileSnapshot> {
|
|
184
|
+
const snapshots = new Map<string, FileSnapshot>();
|
|
185
|
+
const touched = [...plan.edits.map((edit) => edit.file), ...(plan.reverts ?? []).map((revert) => revert.file)];
|
|
186
|
+
for (const file of new Set(touched)) {
|
|
187
|
+
snapshots.set(
|
|
188
|
+
file,
|
|
189
|
+
existsSync(file) ? { exists: true, text: readFileSync(file, "utf8") } : { exists: false, text: "" },
|
|
190
|
+
);
|
|
85
191
|
}
|
|
192
|
+
return snapshots;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Undoes a partially applied plan: every touched file returns to its pre-apply
|
|
197
|
+
* bytes (a file the apply created is deleted and its directory pruned) and the
|
|
198
|
+
* previous record is restored, so the disk and the install state again
|
|
199
|
+
* describe the world the user approved — the original snapshots a later re-init
|
|
200
|
+
* would inherit are what rollback writes back, not the failed run's values.
|
|
201
|
+
* Restore failures are reported in the thrown error, never thrown over the
|
|
202
|
+
* original cause, which stays the headline.
|
|
203
|
+
*/
|
|
204
|
+
function rollbackApply(
|
|
205
|
+
input: InstallerInput,
|
|
206
|
+
previous: InstallRecord | undefined,
|
|
207
|
+
snapshots: ReadonlyMap<string, FileSnapshot>,
|
|
208
|
+
): { complete: boolean; report: string[] } {
|
|
209
|
+
const report: string[] = [];
|
|
210
|
+
let complete = true;
|
|
211
|
+
for (const [file, snapshot] of [...snapshots.entries()].reverse()) {
|
|
212
|
+
try {
|
|
213
|
+
if (snapshot.exists) {
|
|
214
|
+
if (existsSync(file) && readFileSync(file, "utf8") === snapshot.text) continue;
|
|
215
|
+
writeConfig(file, snapshot.text);
|
|
216
|
+
if (readFileSync(file, "utf8") !== snapshot.text) throw new Error("restored contents differ from the snapshot");
|
|
217
|
+
} else {
|
|
218
|
+
if (!existsSync(file)) continue;
|
|
219
|
+
rmSync(file);
|
|
220
|
+
if (existsSync(file)) throw new Error("created file still exists after removal");
|
|
221
|
+
pruneEmptyDir(dirname(file));
|
|
222
|
+
}
|
|
223
|
+
report.push(`restored ${file}`);
|
|
224
|
+
} catch (restoreError) {
|
|
225
|
+
complete = false;
|
|
226
|
+
report.push(`could not restore ${file}: ${errorMessage(restoreError)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
if (previous !== undefined) writeRecord(input, previous);
|
|
231
|
+
else deleteRecord(input);
|
|
232
|
+
if (previous === undefined ? existsSync(statePathFor(input)) : !deepEqual(readRecord(input), previous)) {
|
|
233
|
+
throw new Error("restored install record differs from its previous state");
|
|
234
|
+
}
|
|
235
|
+
} catch (restoreError) {
|
|
236
|
+
complete = false;
|
|
237
|
+
report.push(`could not restore the install record: ${errorMessage(restoreError)}`);
|
|
238
|
+
}
|
|
239
|
+
return { complete, report };
|
|
86
240
|
}
|
|
87
241
|
|
|
88
242
|
function prepareEdit(edit: FileEdit, host: InstallPlan["host"]): PreparedEdit {
|
|
89
|
-
const codec = codecFor(edit.format, host)
|
|
90
|
-
const loaded = codec.load(edit.file)
|
|
243
|
+
const codec = codecFor(edit.format, host);
|
|
244
|
+
const loaded = codec.load(edit.file);
|
|
91
245
|
if (loaded.parseError) {
|
|
92
|
-
throw new Error(`${edit.file} does not parse: ${loaded.parseError}`)
|
|
246
|
+
throw new Error(`${edit.file} does not parse: ${loaded.parseError}`);
|
|
93
247
|
}
|
|
94
|
-
const applied = codec.apply(loaded.text, edit)
|
|
248
|
+
const applied = codec.apply(loaded.text, edit);
|
|
95
249
|
const recorded: RecordedFile =
|
|
96
250
|
edit.format === "rules"
|
|
97
251
|
? { file: edit.file, format: "rules", created: !loaded.exists, ops: [], blockApplied: edit.block }
|
|
98
|
-
: {
|
|
252
|
+
: {
|
|
253
|
+
file: edit.file,
|
|
254
|
+
format: edit.format,
|
|
255
|
+
created: !loaded.exists,
|
|
256
|
+
...(edit.shared ? { shared: true as const } : {}),
|
|
257
|
+
ops: applied.recorded,
|
|
258
|
+
createdParents: applied.createdParents,
|
|
259
|
+
};
|
|
99
260
|
return {
|
|
100
261
|
edit,
|
|
101
262
|
recorded,
|
|
102
263
|
write: () => writeConfig(edit.file, applied.text),
|
|
103
|
-
}
|
|
264
|
+
};
|
|
104
265
|
}
|
|
105
266
|
|
|
106
267
|
/**
|
|
@@ -110,67 +271,81 @@ function prepareEdit(edit: FileEdit, host: InstallPlan["host"]): PreparedEdit {
|
|
|
110
271
|
* installer created them and they are left empty.
|
|
111
272
|
*/
|
|
112
273
|
function applyReverts(plan: InstallPlan): string[] {
|
|
113
|
-
const lines: string[] = []
|
|
274
|
+
const lines: string[] = [];
|
|
114
275
|
for (const revert of plan.reverts ?? []) {
|
|
115
|
-
const whole = !plan.edits.some((edit) => edit.file === revert.file)
|
|
116
|
-
lines.push(...revertRecordedFile(revert, plan.host, { label: " (previous init)", whole }))
|
|
276
|
+
const whole = !plan.edits.some((edit) => edit.file === revert.file);
|
|
277
|
+
lines.push(...revertRecordedFile(revert, plan.host, { label: " (previous init)", whole }));
|
|
117
278
|
}
|
|
118
|
-
return lines
|
|
279
|
+
return lines;
|
|
119
280
|
}
|
|
120
281
|
|
|
121
282
|
/**
|
|
122
283
|
* Merges a re-apply with the record it supersedes: a set op whose previous value is
|
|
123
284
|
* what an earlier run applied inherits that run's snapshot, appended arrays
|
|
124
|
-
* accumulate
|
|
285
|
+
* accumulate only the entries the new plan still wants (entries it dropped were
|
|
286
|
+
* reverted as orphans during apply, so claiming them would orphan the record),
|
|
287
|
+
* and the created flag keeps the first install's truth.
|
|
125
288
|
*/
|
|
126
|
-
function mergeRecorded(
|
|
127
|
-
|
|
289
|
+
function mergeRecorded(
|
|
290
|
+
current: RecordedFile,
|
|
291
|
+
before: RecordedFile | undefined,
|
|
292
|
+
desired: readonly ConfigOp[],
|
|
293
|
+
): RecordedFile {
|
|
294
|
+
if (before === undefined) return current;
|
|
128
295
|
const ops = current.ops.map((op) => {
|
|
129
|
-
const old = before.ops.find(
|
|
130
|
-
|
|
296
|
+
const old = before.ops.find(
|
|
297
|
+
(candidate) => candidate.op === op.op && candidate.path.join("\u0000") === op.path.join("\u0000"),
|
|
298
|
+
);
|
|
299
|
+
if (old === undefined) return op;
|
|
131
300
|
if (op.op === "set" && old.op === "set" && deepEqual(op.previous, old.applied)) {
|
|
132
|
-
return { ...op, previous: old.previous }
|
|
301
|
+
return { ...op, previous: old.previous };
|
|
133
302
|
}
|
|
134
303
|
if (op.op === "append-array" && old.op === "append-array") {
|
|
135
|
-
|
|
304
|
+
const wanted = desired.find(
|
|
305
|
+
(candidate) => candidate.op === "append-array" && candidate.path.join("\u0000") === op.path.join("\u0000"),
|
|
306
|
+
);
|
|
307
|
+
const kept = (Array.isArray(old.applied) ? old.applied : []).filter((entry) =>
|
|
308
|
+
(wanted?.entries ?? []).some((candidate) => deepEqual(candidate, entry)),
|
|
309
|
+
);
|
|
310
|
+
return { ...op, applied: [...kept, ...(Array.isArray(op.applied) ? op.applied : [])] };
|
|
136
311
|
}
|
|
137
|
-
return op
|
|
138
|
-
})
|
|
312
|
+
return op;
|
|
313
|
+
});
|
|
139
314
|
return {
|
|
140
315
|
...current,
|
|
141
316
|
created: before.created,
|
|
142
317
|
ops,
|
|
143
318
|
createdParents: [...(before.createdParents ?? []), ...(current.createdParents ?? [])],
|
|
144
|
-
}
|
|
319
|
+
};
|
|
145
320
|
}
|
|
146
321
|
|
|
147
322
|
function verifyEdits(edits: readonly FileEdit[], host: InstallPlan["host"]): { lines: string[]; ok: boolean } {
|
|
148
|
-
const lines: string[] = []
|
|
149
|
-
let ok = true
|
|
323
|
+
const lines: string[] = [];
|
|
324
|
+
let ok = true;
|
|
150
325
|
for (const edit of edits) {
|
|
151
326
|
if (!existsSync(edit.file)) {
|
|
152
|
-
lines.push(`FAIL ${edit.file} does not exist after apply`)
|
|
153
|
-
ok = false
|
|
154
|
-
continue
|
|
327
|
+
lines.push(`FAIL ${edit.file} does not exist after apply`);
|
|
328
|
+
ok = false;
|
|
329
|
+
continue;
|
|
155
330
|
}
|
|
156
|
-
const codec = codecFor(edit.format, host)
|
|
157
|
-
const text = readFileSync(edit.file, "utf8")
|
|
331
|
+
const codec = codecFor(edit.format, host);
|
|
332
|
+
const text = readFileSync(edit.file, "utf8");
|
|
158
333
|
if (edit.format === "rules") {
|
|
159
|
-
const good = codec.verify(text, edit)
|
|
160
|
-
lines.push(`${good ? "pass" : "FAIL"} ${edit.file}: rules block ${good ? "present" : "missing"}`)
|
|
161
|
-
ok = ok && good
|
|
162
|
-
continue
|
|
334
|
+
const good = codec.verify(text, edit);
|
|
335
|
+
lines.push(`${good ? "pass" : "FAIL"} ${edit.file}: rules block ${good ? "present" : "missing"}`);
|
|
336
|
+
ok = ok && good;
|
|
337
|
+
continue;
|
|
163
338
|
}
|
|
164
339
|
for (const op of edit.ops) {
|
|
165
|
-
const good = codec.verify(text, edit, op)
|
|
166
|
-
lines.push(`${good ? "pass" : "FAIL"} ${edit.file}: ${describeOp(op)} ${good ? "holds" : "missing"}`)
|
|
167
|
-
ok = ok && good
|
|
340
|
+
const good = codec.verify(text, edit, op);
|
|
341
|
+
lines.push(`${good ? "pass" : "FAIL"} ${edit.file}: ${describeOp(op)} ${good ? "holds" : "missing"}`);
|
|
342
|
+
ok = ok && good;
|
|
168
343
|
}
|
|
169
344
|
}
|
|
170
|
-
return { lines, ok }
|
|
345
|
+
return { lines, ok };
|
|
171
346
|
}
|
|
172
347
|
|
|
173
348
|
function describeOp(op: ConfigOp): string {
|
|
174
|
-
if (op.op === "set") return `set ${op.path.join(".")}
|
|
175
|
-
return `append ${op.path.join(".")}
|
|
349
|
+
if (op.op === "set") return `set ${op.path.join(".")}`;
|
|
350
|
+
return `append ${op.path.join(".")}`;
|
|
176
351
|
}
|