@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/util/secret-mask.ts
CHANGED
|
@@ -36,8 +36,11 @@ export function secretNamed(name: string): boolean {
|
|
|
36
36
|
/**
|
|
37
37
|
* A URL's password (`postgres://app:<pw>@db/app`): a credential whatever
|
|
38
38
|
* the variable is named, so `DATABASE_URL` and `REDIS_URL` printed it.
|
|
39
|
+
* Split as WHATWG splits it: the userinfo ends at the LAST `@` before the
|
|
40
|
+
* path, query or fragment, and the user name at its first `:`, so either
|
|
41
|
+
* may hold an `@` (`app:p@ss@db` printed `p@ss` whole).
|
|
39
42
|
*/
|
|
40
|
-
const URL_PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s
|
|
43
|
+
const URL_PASSWORD = /[a-z][a-z0-9+.-]*:\/\/[^\s:/?#]*:([^\s/?#]+)@/gi
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Values shorter than this are not masked: `KEY=1` would mask every `1`
|
|
@@ -260,6 +263,59 @@ export interface TaskEnvSecrets {
|
|
|
260
263
|
readonly secret?: readonly string[]
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Each stream's run of adjacent non-empty chunks joined, with `mask`
|
|
268
|
+
* applied to each stream's text as a whole: a value is masked where it
|
|
269
|
+
* starts and its rest dropped from the runs it reaches. The live stream
|
|
270
|
+
* masks chunk by chunk and lets a held piece go after a pause, so a value
|
|
271
|
+
* written in two pieces reached the capture in two chunks, and a write to
|
|
272
|
+
* the other stream between them made them two runs. A hit replays each
|
|
273
|
+
* stream to its own descriptor, so `vx run > log` joined them again.
|
|
274
|
+
*/
|
|
275
|
+
export function maskStreams(
|
|
276
|
+
chunks: ReadonlyArray<{ text: string; err: boolean }>,
|
|
277
|
+
mask: SecretMask | undefined,
|
|
278
|
+
): Array<{ text: string; err: boolean }> {
|
|
279
|
+
if (mask === undefined) return chunks.filter((c) => c.text.length > 0)
|
|
280
|
+
const runs: Array<{ text: string; err: boolean }> = []
|
|
281
|
+
for (const c of chunks) {
|
|
282
|
+
if (c.text.length === 0) continue
|
|
283
|
+
const last = runs.at(-1)
|
|
284
|
+
if (last !== undefined && last.err === c.err) last.text += c.text
|
|
285
|
+
else runs.push({ text: c.text, err: c.err })
|
|
286
|
+
}
|
|
287
|
+
for (const err of [false, true]) {
|
|
288
|
+
const own = runs.filter((r) => r.err === err)
|
|
289
|
+
const spans = mask.spans(own.map((r) => r.text).join(''))
|
|
290
|
+
if (spans.length === 0) continue
|
|
291
|
+
let at = 0
|
|
292
|
+
let i = 0
|
|
293
|
+
for (const r of own) {
|
|
294
|
+
const end = at + r.text.length
|
|
295
|
+
let text = ''
|
|
296
|
+
let from = at
|
|
297
|
+
for (; i < spans.length && spans[i]![0] < end; i++) {
|
|
298
|
+
const [s, e] = spans[i]!
|
|
299
|
+
if (s >= from) text += r.text.slice(from - at, s - at) + MASKED
|
|
300
|
+
from = Math.max(from, Math.min(e, end))
|
|
301
|
+
// A value that runs past this run is cut from the next one too.
|
|
302
|
+
if (e > end) break
|
|
303
|
+
}
|
|
304
|
+
r.text = text + r.text.slice(from - at)
|
|
305
|
+
at = end
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// A run emptied whole leaves its neighbours, the other stream, adjacent.
|
|
309
|
+
const kept: typeof runs = []
|
|
310
|
+
for (const r of runs) {
|
|
311
|
+
if (r.text.length === 0) continue
|
|
312
|
+
const last = kept.at(-1)
|
|
313
|
+
if (last !== undefined && last.err === r.err) last.text += r.text
|
|
314
|
+
else kept.push(r)
|
|
315
|
+
}
|
|
316
|
+
return kept
|
|
317
|
+
}
|
|
318
|
+
|
|
263
319
|
/**
|
|
264
320
|
* `mask.stream()` driving `emit`: what it holds back (a tail that could
|
|
265
321
|
* begin a value) is emitted `idleMs` after the last chunk when none
|
package/src/util/settle.ts
CHANGED
|
@@ -27,6 +27,22 @@ export function teardownTimeoutMs(): number {
|
|
|
27
27
|
return testTimerMs('TEARDOWN_TIMEOUT', DEFAULT_TIMEOUT_MS, true)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Grace after the direct child exits before we abort the stdout/stderr readers.
|
|
32
|
+
* The pipe reaches EOF only when EVERY write-end fd is closed, so a task that
|
|
33
|
+
* backgrounds a process inheriting fd 1/2 (`server & echo up` — `execWrap`
|
|
34
|
+
* leaves compound commands as `sh -c`, so `sh` exits while the grandchild holds
|
|
35
|
+
* the pipe) never EOFs. On a clean exit with no lingering writer the pipe EOFs
|
|
36
|
+
* at once, so the readers win this race immediately and normal tasks pay
|
|
37
|
+
* nothing; only a stuck reader waits out the grace. Residual buffered output is
|
|
38
|
+
* bounded by the OS pipe buffer (streamToString drains continuously DURING the
|
|
39
|
+
* run), so this is far longer than any real drain needs. What the leftover
|
|
40
|
+
* process writes after the bound is lost, and `POST_EXIT_CUT_LINE`
|
|
41
|
+
* (exec/runner.ts) says so.
|
|
42
|
+
* A runtime probe's drain is bounded the same (cache/inputs.ts).
|
|
43
|
+
*/
|
|
44
|
+
export const POST_EXIT_DRAIN_MS = 250
|
|
45
|
+
|
|
30
46
|
/**
|
|
31
47
|
* The SIGTERM→SIGKILL grace a run grants a child that ignores SIGTERM:
|
|
32
48
|
* the one-shot timeout escalation (`exec/runner.ts`),
|
|
@@ -51,7 +67,8 @@ export function killGraceMs(defaultMs: number): number {
|
|
|
51
67
|
export async function settleWithin(p: Promise<unknown>, ms: number): Promise<boolean> {
|
|
52
68
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
53
69
|
const deadline = new Promise<false>((resolve) => {
|
|
54
|
-
|
|
70
|
+
// A delay past a 32-bit int fires at 1 ms: the longest the timer holds instead.
|
|
71
|
+
timer = setTimeout(() => resolve(false), Math.min(ms, MAX_TIMEOUT_MS))
|
|
55
72
|
})
|
|
56
73
|
try {
|
|
57
74
|
return (await Promise.race([p.then(() => true), deadline])) !== false
|
package/src/util/size.ts
CHANGED
|
@@ -70,10 +70,20 @@ export function formatBytes(n: number): string {
|
|
|
70
70
|
return `${v.toFixed(0)} PB`
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** Bytes under `p` on disk, links not followed; 0 when
|
|
73
|
+
/** Bytes under `p` on disk, links not followed; 0 when missing, and for a directory this user cannot list. */
|
|
74
74
|
export function treeBytes(p: string): number {
|
|
75
75
|
const st = lstatSync(p, { throwIfNoEntry: false })
|
|
76
76
|
if (st === undefined) return 0
|
|
77
77
|
if (!st.isDirectory()) return st.size
|
|
78
|
-
|
|
78
|
+
let names: string[]
|
|
79
|
+
try {
|
|
80
|
+
names = readdirSync(p)
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// A directory this user cannot list (another user's store, a task's
|
|
83
|
+
// 0700 output) is a size nobody here can read, not a failed report.
|
|
84
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
85
|
+
if (code === 'EACCES' || code === 'EPERM') return 0
|
|
86
|
+
throw err
|
|
87
|
+
}
|
|
88
|
+
return names.reduce((n, e) => n + treeBytes(path.join(p, e)), 0)
|
|
79
89
|
}
|
package/src/util/tail.ts
CHANGED
|
@@ -32,8 +32,11 @@ export function createTail(): Tail {
|
|
|
32
32
|
return { chunks: [], chars: 0, dropped: 0 }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function appendTail(t: Tail, chunk: string,
|
|
35
|
+
export function appendTail(t: Tail, chunk: string, cap = PERSISTENT_TAIL_CHARS): void {
|
|
36
36
|
if (chunk.length === 0) return
|
|
37
|
+
// Every bound below is `chars > limit`, which NaN never is: a NaN cap
|
|
38
|
+
// kept everything, the unbounded heap this exists to prevent.
|
|
39
|
+
const limit = Number.isNaN(cap) ? 0 : cap
|
|
37
40
|
t.chunks.push(chunk)
|
|
38
41
|
t.chars += chunk.length
|
|
39
42
|
// Evict WHOLE chunks from the head — no concatenation until the flush
|
package/src/util/write-file.ts
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
|
-
import { lstatSync, readlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { mkdir, open, rename, rm } from 'node:fs/promises'
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { lstat, mkdir, open, rename, rm } from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { UserError } from './errors.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Refuses a `file`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Refuses a `file` whose directory resolves outside `root`: a repo can
|
|
8
|
+
* carry `.vx -> ../anywhere` (or `plugins -> ...`), and checking only the
|
|
9
|
+
* file's own name wrote through it. The nearest directory that exists is
|
|
10
|
+
* asked; one not made yet is made under it.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
function refuseOutside(file: string, root: string): void {
|
|
13
|
+
const top = realpathSync(root)
|
|
14
|
+
for (let at = path.dirname(file); ; at = path.dirname(at)) {
|
|
15
|
+
let real: string
|
|
16
|
+
try {
|
|
17
|
+
real = realpathSync(at)
|
|
18
|
+
} catch {
|
|
19
|
+
// quiet: not made yet (or a dangling link mkdir cannot make through)
|
|
20
|
+
if (at === path.dirname(at)) return
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const rel = path.relative(top, real)
|
|
24
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
25
|
+
throw new UserError(
|
|
26
|
+
`refusing to write ${file}: ${at} resolves to ${real}, outside the workspace; remove the link and run again`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Refuses a `file` that is a symlink, or whose directory resolves outside
|
|
35
|
+
* `root`: a repo can carry `vx.config.ts -> /anywhere`, and a write
|
|
36
|
+
* through it lands outside the workspace. Called before the first of a
|
|
37
|
+
* set of writes, so a refusal leaves none half-made.
|
|
38
|
+
*/
|
|
39
|
+
export function refuseSymlink(file: string, root: string): void {
|
|
40
|
+
refuseOutside(file, root)
|
|
12
41
|
let link: string
|
|
13
42
|
try {
|
|
14
43
|
if (!lstatSync(file).isSymbolicLink()) return
|
|
@@ -22,13 +51,61 @@ export function refuseSymlink(file: string): void {
|
|
|
22
51
|
)
|
|
23
52
|
}
|
|
24
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A rename over a directory (EISDIR) as a refusal naming `shown`, not a
|
|
56
|
+
* stack trace; any other error as it was.
|
|
57
|
+
*/
|
|
58
|
+
function directoryInTheWay(err: unknown, shown: string): unknown {
|
|
59
|
+
return (err as NodeJS.ErrnoException | null)?.code === 'EISDIR'
|
|
60
|
+
? new UserError(`cannot write ${shown}: a directory stands there; remove it and run again`)
|
|
61
|
+
: err
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A file to write: its path, its bytes, and the name a refusal shows. */
|
|
65
|
+
export interface PendingWrite {
|
|
66
|
+
file: string
|
|
67
|
+
text: string
|
|
68
|
+
shown: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Writes each file to a temp beside it, then renames them all over their
|
|
73
|
+
* names: a reader never sees half a file, and a refusal (one outside
|
|
74
|
+
* `root`, as `refuseSymlink`'s) leaves every file
|
|
75
|
+
* as it was, not one new beside one old (`vx lock` said it wrote nothing
|
|
76
|
+
* while the config lock held the edit). A refusal names the file the user
|
|
77
|
+
* knows, never its temp.
|
|
78
|
+
*/
|
|
79
|
+
export async function writeAllOrNone(files: readonly PendingWrite[], root: string): Promise<void> {
|
|
80
|
+
for (const f of files) refuseOutside(f.file, root)
|
|
81
|
+
const tag = `.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`
|
|
82
|
+
let at = 0
|
|
83
|
+
try {
|
|
84
|
+
for (; at < files.length; at++) await Bun.write(files[at]!.file + tag, files[at]!.text)
|
|
85
|
+
// A rename over a directory fails only after the ones before it landed.
|
|
86
|
+
for (at = 0; at < files.length; at++)
|
|
87
|
+
if ((await lstat(files[at]!.file).catch(() => null))?.isDirectory())
|
|
88
|
+
throw Object.assign(new Error('directory'), { code: 'EISDIR' })
|
|
89
|
+
for (at = 0; at < files.length; at++) await rename(files[at]!.file + tag, files[at]!.file)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Best effort: where the write was refused the temp cannot be named
|
|
92
|
+
// either (ENOTDIR), and that must not replace the refusal itself.
|
|
93
|
+
await Promise.all(files.map((f) => rm(f.file + tag, { force: true }).catch(() => {})))
|
|
94
|
+
const f = files[Math.min(at, files.length - 1)]!
|
|
95
|
+
const e = err as NodeJS.ErrnoException
|
|
96
|
+
if (typeof e.message === 'string') e.message = e.message.split(f.file + tag).join(f.file)
|
|
97
|
+
if (e.path === f.file + tag) e.path = f.file
|
|
98
|
+
throw directoryInTheWay(err, f.shown)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
25
102
|
/**
|
|
26
103
|
* Writes `text` to `file`, never through a link: a temp beside it, made
|
|
27
104
|
* with O_EXCL, renamed over it. A rename replaces a link put there after
|
|
28
|
-
* the check, never its target.
|
|
105
|
+
* the check, never its target. Refused outside `root` (`refuseSymlink`).
|
|
29
106
|
*/
|
|
30
|
-
export async function writeNoFollow(file: string, text: string): Promise<void> {
|
|
31
|
-
refuseSymlink(file)
|
|
107
|
+
export async function writeNoFollow(file: string, text: string, root: string): Promise<void> {
|
|
108
|
+
refuseSymlink(file, root)
|
|
32
109
|
await mkdir(path.dirname(file), { recursive: true })
|
|
33
110
|
const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`
|
|
34
111
|
try {
|