knodin 0.7.5 → 0.8.2
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 +18 -3
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +371 -66
- package/dist/bin/launcher.js +16 -1
- package/dist/src/agent-integration.js +82 -16
- package/dist/src/artifact-refresh.js +2 -1
- package/dist/src/cli-args.js +19 -1
- package/dist/src/cli-model.js +28 -2
- package/dist/src/codeflow-replay.js +2 -1
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-constraints.js +2 -1
- package/dist/src/competitive-runner.js +4 -4
- package/dist/src/context-export.js +3 -2
- package/dist/src/context.js +1 -1
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1160 -133
- package/dist/src/doctor.js +3 -1
- package/dist/src/engine/ann-hnsw.js +2 -12
- package/dist/src/engine/file-walker.js +8 -2
- package/dist/src/engine/git-history.js +12 -12
- package/dist/src/engine/index.js +1174 -313
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +28 -13
- package/dist/src/engine/source-policy.js +16 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/execution-profile.js +15 -10
- package/dist/src/failure-diagnosis.js +7 -1
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/index-activity.js +2 -1
- package/dist/src/init.js +86 -45
- package/dist/src/lifecycle-health.js +41 -9
- package/dist/src/mcp-graph-worker.js +69 -0
- package/dist/src/mcp-reliability.js +154 -0
- package/dist/src/mcp-worker-supervisor.js +350 -0
- package/dist/src/mirror.js +290 -0
- package/dist/src/node-runtime.js +157 -0
- package/dist/src/output-compression.js +2 -1
- package/dist/src/output-telemetry.js +16 -11
- package/dist/src/progressive-evidence.js +30 -26
- package/dist/src/pure-compression-cli.js +4 -3
- package/dist/src/relationship-adapters.js +15 -8
- package/dist/src/release-preflight.js +13 -10
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repository-init-process.js +13 -9
- package/dist/src/repository-management.js +34 -4
- package/dist/src/response-budget.js +8 -6
- package/dist/src/server.js +80 -35
- package/dist/src/structural-fast-path.js +16 -10
- package/dist/src/structural-snapshot.js +6 -2
- package/dist/src/system-config.js +25 -2
- package/dist/src/tools/knodin-tools.js +142 -31
- package/dist/src/update-ceremony.js +9 -5
- package/dist/src/update-trust.js +5 -4
- package/dist/src/visualization.js +372 -19
- package/dist/src/worktree-lifecycle.js +5 -2
- package/docs/BEHAVIORAL-CONTRACT.md +72 -0
- package/docs/CLI.md +20 -1
- package/docs/COMPARISON.md +403 -0
- package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
- package/docs/DIAGNOSTICS.md +46 -11
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -2
- package/docs/MCP.md +59 -8
- package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +5 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/releases/0.5.1.md +4 -4
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +17 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +1 -1
- package/schemas/support-bundle-v2.schema.json +212 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const VERSION = 1;
|
|
4
|
+
const [expectedDevRaw, expectedInoRaw, relativeTarget, mode, maxBytesRaw, bytesRaw, nonce, maxAgeRaw = "0", testFixtures = "", componentSwapPath = "", deleteSwapPath = "",] = process.argv.slice(2);
|
|
5
|
+
const expectedDev = Number(expectedDevRaw);
|
|
6
|
+
const expectedIno = Number(expectedInoRaw);
|
|
7
|
+
const maxBytes = Number(maxBytesRaw);
|
|
8
|
+
const bytes = Number(bytesRaw);
|
|
9
|
+
const maxAgeMs = Number(maxAgeRaw);
|
|
10
|
+
function emitPipe(value) {
|
|
11
|
+
const message = Buffer.from(`${JSON.stringify(value)}\n`);
|
|
12
|
+
let offset = 0;
|
|
13
|
+
while (offset < message.byteLength) {
|
|
14
|
+
try {
|
|
15
|
+
offset += fs.writeSync(1, message, offset, message.byteLength - offset);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code !== "EAGAIN")
|
|
19
|
+
throw error;
|
|
20
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function cwdIdentity(requiredDev = expectedDev, requiredIno = expectedIno) {
|
|
25
|
+
const descriptor = fs.openSync(".", fs.constants.O_RDONLY);
|
|
26
|
+
let observed;
|
|
27
|
+
try {
|
|
28
|
+
observed = fs.fstatSync(descriptor);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
fs.closeSync(descriptor);
|
|
32
|
+
}
|
|
33
|
+
if (observed.dev !== requiredDev || observed.ino !== requiredIno)
|
|
34
|
+
throw new Error("destination parent identity mismatch");
|
|
35
|
+
return observed;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
if (!Number.isSafeInteger(expectedDev) ||
|
|
39
|
+
!Number.isSafeInteger(expectedIno) ||
|
|
40
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
41
|
+
!Number.isSafeInteger(bytes) ||
|
|
42
|
+
!Number.isSafeInteger(maxAgeMs) ||
|
|
43
|
+
maxAgeMs < 0 ||
|
|
44
|
+
bytes < 0 ||
|
|
45
|
+
bytes > maxBytes ||
|
|
46
|
+
!(mode === "create" ||
|
|
47
|
+
mode === "replace" ||
|
|
48
|
+
mode === "append" ||
|
|
49
|
+
mode === "delete" ||
|
|
50
|
+
mode === "read") ||
|
|
51
|
+
!relativeTarget ||
|
|
52
|
+
path.isAbsolute(relativeTarget) ||
|
|
53
|
+
relativeTarget
|
|
54
|
+
.split(/[\\/]/)
|
|
55
|
+
.some((segment) => !segment || segment === "." || segment === "..") ||
|
|
56
|
+
!nonce ||
|
|
57
|
+
!/^[a-f0-9]{32}$/.test(nonce))
|
|
58
|
+
throw new Error("invalid helper arguments");
|
|
59
|
+
const allowFaultFixtures = testFixtures === "enabled";
|
|
60
|
+
if (allowFaultFixtures && process.env.KNODIN_DIAGNOSTICS_TEST_SWAP_TO) {
|
|
61
|
+
const boundPath = process.cwd();
|
|
62
|
+
const displaced = `${boundPath}-displaced`;
|
|
63
|
+
fs.renameSync(boundPath, displaced);
|
|
64
|
+
fs.symlinkSync(process.env.KNODIN_DIAGNOSTICS_TEST_SWAP_TO, boundPath);
|
|
65
|
+
}
|
|
66
|
+
if (allowFaultFixtures && process.env.KNODIN_DIAGNOSTICS_TEST_HELPER_FAULT === "crash")
|
|
67
|
+
process.exit(91);
|
|
68
|
+
const rootIdentity = cwdIdentity();
|
|
69
|
+
const rootPath = fs.realpathSync(".");
|
|
70
|
+
const segments = relativeTarget.split(/[\\/]/);
|
|
71
|
+
const basename = segments.pop();
|
|
72
|
+
if (!basename)
|
|
73
|
+
throw new Error("invalid helper target");
|
|
74
|
+
let boundIdentity = rootIdentity;
|
|
75
|
+
for (const segment of segments) {
|
|
76
|
+
cwdIdentity(boundIdentity.dev, boundIdentity.ino);
|
|
77
|
+
let created = false;
|
|
78
|
+
try {
|
|
79
|
+
fs.mkdirSync(segment, { mode: 0o700 });
|
|
80
|
+
created = true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== "EEXIST")
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (created && allowFaultFixtures && componentSwapPath) {
|
|
87
|
+
fs.renameSync(segment, `${segment}-displaced`);
|
|
88
|
+
fs.symlinkSync(componentSwapPath, segment);
|
|
89
|
+
}
|
|
90
|
+
const child = fs.lstatSync(segment);
|
|
91
|
+
if (child.isSymbolicLink() || !child.isDirectory())
|
|
92
|
+
throw new Error("destination component is not a private directory");
|
|
93
|
+
process.chdir(segment);
|
|
94
|
+
const bound = fs.statSync(".");
|
|
95
|
+
if (bound.dev !== child.dev || bound.ino !== child.ino)
|
|
96
|
+
throw new Error("destination component identity mismatch");
|
|
97
|
+
boundIdentity = bound;
|
|
98
|
+
const resolved = fs.realpathSync(".");
|
|
99
|
+
if (resolved !== rootPath && !resolved.startsWith(`${rootPath}${path.sep}`))
|
|
100
|
+
throw new Error("destination ancestry escaped repository root");
|
|
101
|
+
}
|
|
102
|
+
const identity = cwdIdentity(boundIdentity.dev, boundIdentity.ino);
|
|
103
|
+
const boundParentPath = fs.realpathSync(".");
|
|
104
|
+
if (allowFaultFixtures && process.env.KNODIN_DIAGNOSTICS_TEST_HELPER_FAULT === "protocol") {
|
|
105
|
+
emitPipe({ version: 999, type: "READY", nonce });
|
|
106
|
+
process.exit(92);
|
|
107
|
+
}
|
|
108
|
+
const readyMessage = {
|
|
109
|
+
version: VERSION,
|
|
110
|
+
type: "READY",
|
|
111
|
+
rootDev: rootIdentity.dev,
|
|
112
|
+
rootIno: rootIdentity.ino,
|
|
113
|
+
dev: identity.dev,
|
|
114
|
+
ino: identity.ino,
|
|
115
|
+
nonce,
|
|
116
|
+
};
|
|
117
|
+
emitPipe(readyMessage);
|
|
118
|
+
if (mode !== "append" &&
|
|
119
|
+
allowFaultFixtures &&
|
|
120
|
+
process.env.KNODIN_DIAGNOSTICS_TEST_POST_READY_ROOT_SWAP_TO) {
|
|
121
|
+
fs.renameSync(rootPath, `${rootPath}-displaced`);
|
|
122
|
+
fs.symlinkSync(process.env.KNODIN_DIAGNOSTICS_TEST_POST_READY_ROOT_SWAP_TO, rootPath);
|
|
123
|
+
const forgedParent = path.join(rootPath, ...relativeTarget.split(/[\\/]/).slice(0, -1));
|
|
124
|
+
fs.mkdirSync(forgedParent, { recursive: true });
|
|
125
|
+
fs.writeFileSync(path.join(forgedParent, `.knodin-write-${nonce}.receipt`), "forged\n");
|
|
126
|
+
}
|
|
127
|
+
if (mode !== "append" &&
|
|
128
|
+
allowFaultFixtures &&
|
|
129
|
+
process.env.KNODIN_DIAGNOSTICS_TEST_POST_READY_PARENT_SWAP_TO) {
|
|
130
|
+
const boundPath = process.cwd();
|
|
131
|
+
fs.renameSync(boundPath, `${boundPath}-displaced`);
|
|
132
|
+
fs.symlinkSync(process.env.KNODIN_DIAGNOSTICS_TEST_POST_READY_PARENT_SWAP_TO, boundPath);
|
|
133
|
+
fs.writeFileSync(path.join(boundPath, `.knodin-write-${nonce}.receipt`), `${JSON.stringify({ version: VERSION, type: "RECEIPT", dev: identity.dev, ino: identity.ino, nonce, bytes, targetDev: identity.dev, targetIno: identity.ino, data: "" })}\n`);
|
|
134
|
+
}
|
|
135
|
+
const commit = (payload, sequence) => {
|
|
136
|
+
if (mode === "append" &&
|
|
137
|
+
sequence === 2 &&
|
|
138
|
+
allowFaultFixtures &&
|
|
139
|
+
process.env.KNODIN_DIAGNOSTICS_TEST_APPEND_PARENT_SWAP_TO) {
|
|
140
|
+
const boundPath = process.cwd();
|
|
141
|
+
fs.renameSync(boundPath, `${boundPath}-displaced`);
|
|
142
|
+
fs.symlinkSync(process.env.KNODIN_DIAGNOSTICS_TEST_APPEND_PARENT_SWAP_TO, boundPath);
|
|
143
|
+
fs.writeFileSync(path.join(boundPath, `.knodin-write-${nonce}.${sequence}.receipt`), `${JSON.stringify({ version: VERSION, type: "RECEIPT", dev: identity.dev, ino: identity.ino, nonce, sequence, bytes: payload.byteLength })}\n`);
|
|
144
|
+
}
|
|
145
|
+
cwdIdentity(identity.dev, identity.ino);
|
|
146
|
+
const writeParent = fs.realpathSync(".");
|
|
147
|
+
if (writeParent !== boundParentPath)
|
|
148
|
+
throw new Error("destination parent path changed before write");
|
|
149
|
+
if (mode === "read") {
|
|
150
|
+
const descriptor = fs.openSync(basename, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
151
|
+
try {
|
|
152
|
+
const opened = fs.fstatSync(descriptor);
|
|
153
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.size > maxBytes) {
|
|
154
|
+
const limit = maxBytes >= 1024 * 1024 ? `${maxBytes / (1024 * 1024)} MiB` : `${maxBytes} byte`;
|
|
155
|
+
throw new Error(`read target exceeds its ${limit} safety limit`);
|
|
156
|
+
}
|
|
157
|
+
const content = fs.readFileSync(descriptor);
|
|
158
|
+
const final = fs.fstatSync(descriptor);
|
|
159
|
+
const current = fs.lstatSync(basename);
|
|
160
|
+
if (final.nlink !== 1 ||
|
|
161
|
+
current.isSymbolicLink() ||
|
|
162
|
+
current.nlink !== 1 ||
|
|
163
|
+
current.dev !== final.dev ||
|
|
164
|
+
current.ino !== final.ino)
|
|
165
|
+
throw new Error("read target identity changed during use");
|
|
166
|
+
cwdIdentity(identity.dev, identity.ino);
|
|
167
|
+
if (fs.realpathSync(".") !== boundParentPath)
|
|
168
|
+
throw new Error("destination parent path changed during read");
|
|
169
|
+
emitPipe({
|
|
170
|
+
version: VERSION,
|
|
171
|
+
type: "RECEIPT",
|
|
172
|
+
dev: identity.dev,
|
|
173
|
+
ino: identity.ino,
|
|
174
|
+
nonce,
|
|
175
|
+
bytes: content.byteLength,
|
|
176
|
+
targetDev: final.dev,
|
|
177
|
+
targetIno: final.ino,
|
|
178
|
+
data: content.toString("base64"),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
fs.closeSync(descriptor);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (mode === "delete") {
|
|
187
|
+
const expected = JSON.parse(payload.toString("utf8"));
|
|
188
|
+
if (!Number.isSafeInteger(expected.dev) || !Number.isSafeInteger(expected.ino))
|
|
189
|
+
throw new Error("invalid delete identity");
|
|
190
|
+
if (allowFaultFixtures && deleteSwapPath) {
|
|
191
|
+
const boundPath = process.cwd();
|
|
192
|
+
fs.renameSync(boundPath, `${boundPath}-displaced`);
|
|
193
|
+
fs.symlinkSync(deleteSwapPath, boundPath);
|
|
194
|
+
}
|
|
195
|
+
cwdIdentity(identity.dev, identity.ino);
|
|
196
|
+
const deleteParent = fs.realpathSync(".");
|
|
197
|
+
if (deleteParent !== rootPath && !deleteParent.startsWith(`${rootPath}${path.sep}`))
|
|
198
|
+
throw new Error("delete parent escaped repository root");
|
|
199
|
+
if (allowFaultFixtures &&
|
|
200
|
+
typeof expected.token === "string" &&
|
|
201
|
+
process.env.KNODIN_DIAGNOSTICS_TEST_LOCK_SUCCESSOR === "1") {
|
|
202
|
+
fs.renameSync(basename, `${basename}.fixture-original`);
|
|
203
|
+
const successor = `${JSON.stringify({ token: "f".repeat(32), acquiredAt: new Date().toISOString() })}\n`;
|
|
204
|
+
if (expected.kind === "directory") {
|
|
205
|
+
fs.mkdirSync(basename, { mode: 0o700 });
|
|
206
|
+
fs.writeFileSync(path.join(basename, "owner.json"), successor, {
|
|
207
|
+
flag: "wx",
|
|
208
|
+
mode: 0o600,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
fs.writeFileSync(basename, successor, { flag: "wx", mode: 0o600 });
|
|
213
|
+
}
|
|
214
|
+
const tombstone = `.${basename}.${process.pid}.${nonce}.delete`;
|
|
215
|
+
fs.renameSync(basename, tombstone);
|
|
216
|
+
const restoreWithoutClobber = () => {
|
|
217
|
+
if (expected.kind === "directory") {
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(basename, { mode: 0o700 });
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error.code !== "EEXIST")
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
fs.linkSync(tombstone, basename);
|
|
229
|
+
fs.unlinkSync(tombstone);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code !== "EEXIST")
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const directoryTarget = expected.kind === "directory";
|
|
237
|
+
const descriptor = fs.openSync(tombstone, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
238
|
+
let opened;
|
|
239
|
+
let content = "";
|
|
240
|
+
try {
|
|
241
|
+
opened = fs.fstatSync(descriptor);
|
|
242
|
+
if ((directoryTarget ? !opened.isDirectory() : !opened.isFile() || opened.nlink !== 1) ||
|
|
243
|
+
opened.dev !== expected.dev ||
|
|
244
|
+
opened.ino !== expected.ino) {
|
|
245
|
+
restoreWithoutClobber();
|
|
246
|
+
throw new Error("delete target identity mismatch");
|
|
247
|
+
}
|
|
248
|
+
if (typeof expected.token === "string" && !directoryTarget)
|
|
249
|
+
content = fs.readFileSync(descriptor, "utf8");
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
fs.closeSync(descriptor);
|
|
253
|
+
}
|
|
254
|
+
if (typeof expected.token === "string") {
|
|
255
|
+
if (directoryTarget) {
|
|
256
|
+
const ownerDescriptor = fs.openSync(path.join(tombstone, "owner.json"), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
257
|
+
try {
|
|
258
|
+
const ownerStats = fs.fstatSync(ownerDescriptor);
|
|
259
|
+
if (!ownerStats.isFile() || ownerStats.nlink !== 1)
|
|
260
|
+
throw new Error("legacy lock owner is unsafe");
|
|
261
|
+
content = fs.readFileSync(ownerDescriptor, "utf8");
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
fs.closeSync(ownerDescriptor);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const owner = JSON.parse(content);
|
|
268
|
+
if (owner.token !== expected.token) {
|
|
269
|
+
restoreWithoutClobber();
|
|
270
|
+
throw new Error("delete owner token mismatch");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const current = fs.lstatSync(tombstone);
|
|
274
|
+
if (current.dev !== opened.dev ||
|
|
275
|
+
current.ino !== opened.ino ||
|
|
276
|
+
(!directoryTarget && current.nlink !== 1))
|
|
277
|
+
throw new Error("delete target changed before unlink");
|
|
278
|
+
if (directoryTarget) {
|
|
279
|
+
fs.unlinkSync(path.join(tombstone, "owner.json"));
|
|
280
|
+
fs.rmdirSync(tombstone);
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
fs.unlinkSync(tombstone);
|
|
284
|
+
const directory = fs.openSync(".", fs.constants.O_RDONLY);
|
|
285
|
+
try {
|
|
286
|
+
fs.fsyncSync(directory);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
fs.closeSync(directory);
|
|
290
|
+
}
|
|
291
|
+
emitPipe({
|
|
292
|
+
version: VERSION,
|
|
293
|
+
type: "RECEIPT",
|
|
294
|
+
dev: identity.dev,
|
|
295
|
+
ino: identity.ino,
|
|
296
|
+
nonce,
|
|
297
|
+
bytes: payload.byteLength,
|
|
298
|
+
deletedBytes: opened.size,
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const temporary = `.${basename}.${process.pid}.${nonce}.tmp`;
|
|
303
|
+
let target = basename;
|
|
304
|
+
if (mode === "replace")
|
|
305
|
+
target = temporary;
|
|
306
|
+
let existing = Buffer.alloc(0);
|
|
307
|
+
let rotate = false;
|
|
308
|
+
if (mode === "append") {
|
|
309
|
+
try {
|
|
310
|
+
const current = fs.lstatSync(basename);
|
|
311
|
+
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1)
|
|
312
|
+
throw new Error("append target is not a regular file");
|
|
313
|
+
rotate =
|
|
314
|
+
current.size + payload.byteLength > maxBytes ||
|
|
315
|
+
(maxAgeMs > 0 && Date.now() - current.mtimeMs > maxAgeMs);
|
|
316
|
+
const source = fs.openSync(basename, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
317
|
+
try {
|
|
318
|
+
const opened = fs.fstatSync(source);
|
|
319
|
+
if (!opened.isFile() ||
|
|
320
|
+
opened.nlink !== 1 ||
|
|
321
|
+
opened.dev !== current.dev ||
|
|
322
|
+
opened.ino !== current.ino)
|
|
323
|
+
throw new Error("append target identity changed before read");
|
|
324
|
+
existing = fs.readFileSync(source);
|
|
325
|
+
if (fs.fstatSync(source).nlink !== 1)
|
|
326
|
+
throw new Error("append target acquired an external link");
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
fs.closeSync(source);
|
|
330
|
+
}
|
|
331
|
+
if (sequence === 2 &&
|
|
332
|
+
allowFaultFixtures &&
|
|
333
|
+
process.env.KNODIN_DIAGNOSTICS_TEST_APPEND_HARDLINK_TO)
|
|
334
|
+
fs.linkSync(basename, process.env.KNODIN_DIAGNOSTICS_TEST_APPEND_HARDLINK_TO);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error.code !== "ENOENT")
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (mode === "append")
|
|
342
|
+
target = temporary;
|
|
343
|
+
let written;
|
|
344
|
+
const descriptor = fs.openSync(target, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
|
|
345
|
+
try {
|
|
346
|
+
const opened = fs.fstatSync(descriptor);
|
|
347
|
+
written = opened;
|
|
348
|
+
if (!opened.isFile() || opened.nlink !== 1)
|
|
349
|
+
throw new Error("opened target is not a private regular file");
|
|
350
|
+
if (mode === "append" && !rotate)
|
|
351
|
+
fs.writeFileSync(descriptor, existing);
|
|
352
|
+
fs.writeFileSync(descriptor, payload);
|
|
353
|
+
fs.fsyncSync(descriptor);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
fs.closeSync(descriptor);
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
cwdIdentity(identity.dev, identity.ino);
|
|
360
|
+
if (fs.realpathSync(".") !== boundParentPath)
|
|
361
|
+
throw new Error("destination parent path changed before publish");
|
|
362
|
+
if (mode === "replace" || mode === "append") {
|
|
363
|
+
if (mode === "append" && rotate) {
|
|
364
|
+
const previousTemporary = `.${basename}.1.${process.pid}.${nonce}.tmp`;
|
|
365
|
+
fs.writeFileSync(previousTemporary, existing, { flag: "wx", mode: 0o600 });
|
|
366
|
+
fs.renameSync(previousTemporary, `${basename}.1`);
|
|
367
|
+
}
|
|
368
|
+
fs.renameSync(temporary, basename);
|
|
369
|
+
}
|
|
370
|
+
if (allowFaultFixtures && process.env.KNODIN_DIAGNOSTICS_TEST_POST_PUBLISH_TARGET_SWAP_TO) {
|
|
371
|
+
fs.renameSync(basename, `${basename}.fixture-published`);
|
|
372
|
+
fs.symlinkSync(process.env.KNODIN_DIAGNOSTICS_TEST_POST_PUBLISH_TARGET_SWAP_TO, basename);
|
|
373
|
+
}
|
|
374
|
+
const current = fs.lstatSync(basename);
|
|
375
|
+
if (!written ||
|
|
376
|
+
current.isSymbolicLink() ||
|
|
377
|
+
!current.isFile() ||
|
|
378
|
+
current.nlink !== 1 ||
|
|
379
|
+
current.dev !== written.dev ||
|
|
380
|
+
current.ino !== written.ino ||
|
|
381
|
+
(mode !== "append" && current.size !== payload.byteLength))
|
|
382
|
+
throw new Error("write receipt mismatch");
|
|
383
|
+
const directory = fs.openSync(".", fs.constants.O_RDONLY);
|
|
384
|
+
try {
|
|
385
|
+
fs.fsyncSync(directory);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
fs.closeSync(directory);
|
|
389
|
+
}
|
|
390
|
+
const message = {
|
|
391
|
+
version: VERSION,
|
|
392
|
+
type: "RECEIPT",
|
|
393
|
+
dev: identity.dev,
|
|
394
|
+
ino: identity.ino,
|
|
395
|
+
nonce,
|
|
396
|
+
bytes: payload.byteLength,
|
|
397
|
+
targetDev: current.dev,
|
|
398
|
+
targetIno: current.ino,
|
|
399
|
+
sequence,
|
|
400
|
+
};
|
|
401
|
+
emitPipe(message);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
try {
|
|
405
|
+
if (mode === "replace" || mode === "append")
|
|
406
|
+
fs.unlinkSync(temporary);
|
|
407
|
+
else if (mode === "create")
|
|
408
|
+
fs.unlinkSync(basename);
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
if (mode === "append") {
|
|
415
|
+
let pending = Buffer.alloc(0);
|
|
416
|
+
let idle;
|
|
417
|
+
await new Promise((resolve, reject) => {
|
|
418
|
+
const armIdle = () => {
|
|
419
|
+
if (idle)
|
|
420
|
+
clearTimeout(idle);
|
|
421
|
+
idle = setTimeout(resolve, 500);
|
|
422
|
+
};
|
|
423
|
+
process.stdin.on("data", (chunk) => {
|
|
424
|
+
try {
|
|
425
|
+
pending = Buffer.concat([pending, chunk]);
|
|
426
|
+
while (pending.byteLength >= 8) {
|
|
427
|
+
const sequence = pending.readUInt32BE(0);
|
|
428
|
+
const length = pending.readUInt32BE(4);
|
|
429
|
+
if (length > maxBytes)
|
|
430
|
+
throw new Error("payload exceeds declared bound");
|
|
431
|
+
if (pending.byteLength < 8 + length)
|
|
432
|
+
break;
|
|
433
|
+
commit(pending.subarray(8, 8 + length), sequence);
|
|
434
|
+
pending = pending.subarray(8 + length);
|
|
435
|
+
}
|
|
436
|
+
armIdle();
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
reject(error);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
process.stdin.once("end", resolve);
|
|
443
|
+
armIdle();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const chunks = [];
|
|
448
|
+
let total = 0;
|
|
449
|
+
while (total < bytes) {
|
|
450
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, bytes - total)));
|
|
451
|
+
const count = fs.readSync(0, chunk, 0, chunk.length, null);
|
|
452
|
+
if (count === 0)
|
|
453
|
+
throw new Error("payload ended early");
|
|
454
|
+
total += count;
|
|
455
|
+
if (total > bytes || total > maxBytes)
|
|
456
|
+
throw new Error("payload exceeds declared bound");
|
|
457
|
+
chunks.push(chunk.subarray(0, count));
|
|
458
|
+
}
|
|
459
|
+
commit(Buffer.concat(chunks, total));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
try {
|
|
464
|
+
fs.writeSync(1, `${JSON.stringify({
|
|
465
|
+
version: VERSION,
|
|
466
|
+
type: "ERROR",
|
|
467
|
+
nonce,
|
|
468
|
+
message: error instanceof Error ? error.message : "helper failed",
|
|
469
|
+
})}\n`);
|
|
470
|
+
}
|
|
471
|
+
catch { }
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
}
|