@vzn/vx 0.0.591 → 0.0.595
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/launcher.cjs +9 -2
- package/package.json +5 -5
- package/src/bin.ts +26 -11
- package/src/cache/archive.ts +18 -7
- package/src/cache/cache.ts +137 -101
- package/src/cache/config-evals.ts +19 -4
- package/src/cache/git-inputs.ts +33 -17
- package/src/cache/inputs.ts +157 -54
- package/src/cache/layered-cache.ts +25 -2
- package/src/cache/tar-stream.ts +4 -2
- package/src/cli/cache.ts +4 -16
- package/src/cli/completions.ts +42 -6
- package/src/cli/foreign-flags.ts +56 -6
- package/src/cli/help.ts +27 -8
- package/src/cli/info.ts +4 -6
- package/src/cli/init.ts +4 -4
- package/src/cli/last.ts +49 -20
- package/src/cli/lock.ts +3 -3
- package/src/cli/plan-format.ts +9 -0
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +80 -50
- package/src/cli/select.ts +44 -5
- package/src/cli/show.ts +47 -9
- package/src/cli/upgrade.ts +2 -3
- package/src/cli/watch-filter.ts +21 -7
- package/src/cli/watch-fs.ts +32 -8
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch.ts +40 -15
- package/src/cli/why.ts +4 -13
- package/src/cli/workspace-config.ts +1 -1
- package/src/config.ts +5 -5
- package/src/exec/env.ts +8 -5
- package/src/exec/index.ts +1 -0
- package/src/exec/proc-sample.ts +5 -1
- package/src/exec/runner.ts +126 -47
- package/src/exec/sandbox-deny-scan.ts +2 -2
- package/src/exec/sandbox-paths.ts +11 -5
- package/src/exec/sandbox-runtime.ts +89 -23
- package/src/exec/sandbox-violations.ts +23 -9
- package/src/graph/index.ts +1 -0
- package/src/graph/scheduler.ts +14 -4
- package/src/graph/task-graph.ts +3 -2
- package/src/index.ts +9 -0
- package/src/orchestrator/affected-tasks.ts +34 -10
- package/src/orchestrator/doctor.ts +2 -1
- package/src/orchestrator/execute-task.ts +43 -10
- package/src/orchestrator/hit-restore.ts +9 -5
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/local-shortcircuit.ts +4 -1
- package/src/orchestrator/lockfile-claim.ts +45 -49
- package/src/orchestrator/logger.ts +4 -1
- package/src/orchestrator/miss-save.ts +10 -3
- package/src/orchestrator/placement.ts +17 -3
- package/src/orchestrator/plan.ts +25 -1
- package/src/orchestrator/plugin-host.ts +41 -3
- package/src/orchestrator/plugin.ts +11 -14
- package/src/orchestrator/projects.ts +18 -9
- package/src/orchestrator/run-context.ts +2 -2
- package/src/orchestrator/run-lock.ts +14 -4
- package/src/orchestrator/run.ts +94 -16
- package/src/orchestrator/sandbox-request.ts +6 -10
- package/src/orchestrator/signals.ts +5 -2
- package/src/orchestrator/summary.ts +14 -1
- package/src/util/index.ts +7 -1
- package/src/util/secret-mask.ts +36 -0
- package/src/util/settle.ts +15 -0
- package/src/workspace/affected.ts +47 -19
- package/src/workspace/config-cache.ts +25 -4
- package/src/workspace/config-eval.ts +4 -0
- package/src/workspace/config-imports.ts +2 -2
- package/src/workspace/config-schema.ts +47 -10
- package/src/workspace/filter.ts +5 -4
- package/src/workspace/migrate-scripts.ts +23 -6
- package/src/workspace/package-graph.ts +15 -3
- package/src/workspace/project-loader.ts +89 -2
- package/src/workspace/workspace.ts +121 -14
package/src/cli/watch-fs.ts
CHANGED
|
@@ -77,10 +77,10 @@ export function pollWatcher(
|
|
|
77
77
|
*/
|
|
78
78
|
skipDir: (rel: string) => boolean = (rel) => POLL_SKIP.has(path.basename(rel)),
|
|
79
79
|
): WatchHandle {
|
|
80
|
-
let previous = new Map<string,
|
|
80
|
+
let previous = new Map<string, string>()
|
|
81
81
|
let first = true
|
|
82
82
|
const scan = (): void => {
|
|
83
|
-
const current = new Map<string,
|
|
83
|
+
const current = new Map<string, string>()
|
|
84
84
|
const walk = (abs: string, rel: string): void => {
|
|
85
85
|
let entries: fs.Dirent[]
|
|
86
86
|
try {
|
|
@@ -95,15 +95,23 @@ export function pollWatcher(
|
|
|
95
95
|
if (recursive && !skipDir(childRel)) walk(path.join(abs, e.name), childRel)
|
|
96
96
|
continue
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
// A link is an input as its target string (watch-judge.ts), so that
|
|
99
|
+
// string is sampled, never the target's times: a retarget inside
|
|
100
|
+
// one clock tick carries the same times.
|
|
101
|
+
const link = e.isSymbolicLink()
|
|
102
|
+
if (!e.isFile() && !link) continue
|
|
99
103
|
try {
|
|
100
104
|
// The later of the two clocks, as `modifiedBefore` reads them: a
|
|
101
105
|
// replacement that carries the old file's mtime (`cp -p`, `rsync
|
|
102
106
|
// -a`, `mv` of a file stamped the same) moved nothing under mtime
|
|
103
107
|
// alone, and the poller never ran it where the native watcher did.
|
|
104
108
|
// A rename or a write moves ctime, and no process can set it.
|
|
105
|
-
const
|
|
106
|
-
current.set(childRel,
|
|
109
|
+
const p = path.join(abs, e.name)
|
|
110
|
+
if (link) current.set(childRel, `link:${fs.readlinkSync(p)}`)
|
|
111
|
+
else {
|
|
112
|
+
const st = fs.statSync(p)
|
|
113
|
+
current.set(childRel, String(Math.max(st.mtimeMs, st.ctimeMs)))
|
|
114
|
+
}
|
|
107
115
|
} catch {
|
|
108
116
|
// raced with a delete; the next scan settles it
|
|
109
117
|
}
|
|
@@ -111,8 +119,8 @@ export function pollWatcher(
|
|
|
111
119
|
}
|
|
112
120
|
walk(dir, '')
|
|
113
121
|
if (!first) {
|
|
114
|
-
for (const [rel,
|
|
115
|
-
if (previous.get(rel) !==
|
|
122
|
+
for (const [rel, stamp] of current) {
|
|
123
|
+
if (previous.get(rel) !== stamp) onEvent(rel)
|
|
116
124
|
}
|
|
117
125
|
for (const rel of previous.keys()) {
|
|
118
126
|
if (!current.has(rel)) onEvent(rel)
|
|
@@ -237,6 +245,12 @@ export function armWatcher(
|
|
|
237
245
|
*/
|
|
238
246
|
function treeWatcher(root: string, listener: (filename: string) => void): WatchHandle {
|
|
239
247
|
const watchers = new Map<string, fs.FSWatcher>()
|
|
248
|
+
// Which directory each watch holds: inotify follows the inode, so a name
|
|
249
|
+
// removed and made again before its event is handled stats as a
|
|
250
|
+
// directory still, and the watch on the deleted one heard nothing more.
|
|
251
|
+
// The birth time too: a freed inode number goes to the next directory.
|
|
252
|
+
const held = new Map<string, string>()
|
|
253
|
+
const idOf = (st: fs.Stats): string => `${st.dev}:${st.ino}:${st.birthtimeMs}`
|
|
240
254
|
let closed = false
|
|
241
255
|
let armed = false
|
|
242
256
|
let warned = false
|
|
@@ -249,6 +263,7 @@ function treeWatcher(root: string, listener: (filename: string) => void): WatchH
|
|
|
249
263
|
if (key !== rel && !key.startsWith(rel + '/')) continue
|
|
250
264
|
w.close()
|
|
251
265
|
watchers.delete(key)
|
|
266
|
+
held.delete(key)
|
|
252
267
|
}
|
|
253
268
|
}
|
|
254
269
|
const watchDir = (rel: string, report: boolean): void => {
|
|
@@ -269,7 +284,10 @@ function treeWatcher(root: string, listener: (filename: string) => void): WatchH
|
|
|
269
284
|
return
|
|
270
285
|
}
|
|
271
286
|
if (!st.isDirectory()) return
|
|
272
|
-
if (watchers.has(child))
|
|
287
|
+
if (watchers.has(child)) {
|
|
288
|
+
if (held.get(child) === idOf(st)) return
|
|
289
|
+
drop(child)
|
|
290
|
+
}
|
|
273
291
|
watchDir(child, true)
|
|
274
292
|
})
|
|
275
293
|
} catch (err) {
|
|
@@ -289,6 +307,11 @@ function treeWatcher(root: string, listener: (filename: string) => void): WatchH
|
|
|
289
307
|
}
|
|
290
308
|
w.on('error', () => drop(rel))
|
|
291
309
|
watchers.set(rel, w)
|
|
310
|
+
try {
|
|
311
|
+
held.set(rel, idOf(fs.statSync(abs)))
|
|
312
|
+
} catch {
|
|
313
|
+
held.set(rel, '')
|
|
314
|
+
}
|
|
292
315
|
let entries: fs.Dirent[]
|
|
293
316
|
try {
|
|
294
317
|
entries = fs.readdirSync(abs, { withFileTypes: true })
|
|
@@ -314,6 +337,7 @@ function treeWatcher(root: string, listener: (filename: string) => void): WatchH
|
|
|
314
337
|
closed = true
|
|
315
338
|
for (const w of watchers.values()) w.close()
|
|
316
339
|
watchers.clear()
|
|
340
|
+
held.clear()
|
|
317
341
|
},
|
|
318
342
|
}
|
|
319
343
|
}
|
package/src/cli/watch-judge.ts
CHANGED
|
@@ -77,7 +77,7 @@ export class ChangeJudge {
|
|
|
77
77
|
// are judged one window after the run ends, all together — an edit made
|
|
78
78
|
// meanwhile still differs from what the loop last saw and re-runs.
|
|
79
79
|
readonly pending = new Map<string, string>()
|
|
80
|
-
/** The last cycle's
|
|
80
|
+
/** The last cycle's window, its start on the mtime clock (`fsClockNow`); a write inside it is the run's own. */
|
|
81
81
|
lastCycle: { start: number; end: number } | undefined
|
|
82
82
|
// Declared outputs are ignored by PATH above. A task with no `cache`
|
|
83
83
|
// block declares none and still writes into its project, and the
|
package/src/cli/watch.ts
CHANGED
|
@@ -55,6 +55,12 @@ function sayCannot(what: string, err: unknown): void {
|
|
|
55
55
|
|
|
56
56
|
/** Wait this long after the last filesystem event before re-running. */
|
|
57
57
|
const DEBOUNCE_MS = 150
|
|
58
|
+
/**
|
|
59
|
+
* ...but no longer than this after the first: a writer that never pauses
|
|
60
|
+
* for a window (a dev server logging into its project every 50 ms) reset
|
|
61
|
+
* the timer forever, and an edit made meanwhile never ran.
|
|
62
|
+
*/
|
|
63
|
+
const DEBOUNCE_MAX_MS = 1_000
|
|
58
64
|
|
|
59
65
|
/**
|
|
60
66
|
* The run flags a watch loop cannot honour, as the refusal line it prints; null
|
|
@@ -80,7 +86,7 @@ export function watchRefusal(parsed: RunArgs): string | null {
|
|
|
80
86
|
export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
81
87
|
const parsed = parseRunArgs(args, 'watch')
|
|
82
88
|
if (parsed.error) {
|
|
83
|
-
process.stderr.write(`vx watch: ${parsed.error}\n`)
|
|
89
|
+
process.stderr.write(`vx watch: ${parsed.error}${seeHelp('watch')}\n`)
|
|
84
90
|
return 1
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -131,10 +137,13 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
131
137
|
// worth; every cycle after an edit must load and list live.
|
|
132
138
|
delete opts.staged
|
|
133
139
|
delete opts.discovered
|
|
134
|
-
process.once('SIGINT', () =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
process.once('SIGINT', () => stop.abort('SIGINT'))
|
|
141
|
+
// Said once the loop is down, the last line: written as the signal
|
|
142
|
+
// landed, the aborted cycle's summary printed below it.
|
|
143
|
+
const stopped = (): number => {
|
|
144
|
+
if (stop.signal.reason === 'SIGINT') process.stdout.write('\nvx watch: stopped\n')
|
|
145
|
+
return 0
|
|
146
|
+
}
|
|
138
147
|
process.once('SIGTERM', () => stop.abort('SIGTERM'))
|
|
139
148
|
// A task runs in its own session (exec/kill-tree.ts): the terminal
|
|
140
149
|
// closing reaches the loop alone, and the loop passes it on.
|
|
@@ -200,7 +209,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
200
209
|
}
|
|
201
210
|
if (stop.signal.aborted) {
|
|
202
211
|
await held?.stop(forwardedSignal(stop.signal.reason))
|
|
203
|
-
return
|
|
212
|
+
return stopped()
|
|
204
213
|
}
|
|
205
214
|
if (refusedToStart) return 1
|
|
206
215
|
// After the initial run, so a `pkg#task` naming no project gets the
|
|
@@ -228,7 +237,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
228
237
|
...fingerprintClaims(plugins).keys(),
|
|
229
238
|
...(opts.frozen === true ? [LOCKFILE_NAME] : []),
|
|
230
239
|
])
|
|
231
|
-
|
|
240
|
+
await runWatchLoop({
|
|
232
241
|
opts,
|
|
233
242
|
held,
|
|
234
243
|
stop: stop.signal,
|
|
@@ -279,6 +288,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
279
288
|
// The RESOLVED cache dir, not the `.vx` literal — see `makeWatchIgnore`.
|
|
280
289
|
cacheDir: opts.cacheDir ?? ws.cacheDir,
|
|
281
290
|
})
|
|
291
|
+
return stopped()
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
interface WatchLoopArgs {
|
|
@@ -339,7 +349,7 @@ interface Rediscovered {
|
|
|
339
349
|
fenceDirs: readonly string[]
|
|
340
350
|
}
|
|
341
351
|
|
|
342
|
-
async function runWatchLoop(args: WatchLoopArgs): Promise<
|
|
352
|
+
async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
343
353
|
const { opts, stop, workspaceRoot, projects, cacheDir } = args
|
|
344
354
|
// The watched set as of the last cycle: `rearm` replaces these after an
|
|
345
355
|
// event that can change it, and every filter below reads the current one.
|
|
@@ -365,11 +375,15 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
365
375
|
/** The cycle in flight, so the stop path can wait for its teardown before resolving. */
|
|
366
376
|
let inFlight: Promise<void> = Promise.resolve()
|
|
367
377
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
378
|
+
let windowOpened = 0
|
|
368
379
|
|
|
369
380
|
const trigger = (label: string, abs: string): void => {
|
|
370
381
|
if (!changes.pending.has(abs)) changes.pending.set(abs, label)
|
|
371
382
|
if (running) return
|
|
372
|
-
if (debounceTimer)
|
|
383
|
+
if (debounceTimer) {
|
|
384
|
+
if (Date.now() - windowOpened >= DEBOUNCE_MAX_MS) return
|
|
385
|
+
clearTimeout(debounceTimer)
|
|
386
|
+
} else windowOpened = Date.now()
|
|
373
387
|
debounceTimer = setTimeout(() => {
|
|
374
388
|
debounceTimer = null
|
|
375
389
|
if (running) return
|
|
@@ -393,7 +407,10 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
393
407
|
await held?.stop()
|
|
394
408
|
held = undefined
|
|
395
409
|
restartTimings()
|
|
396
|
-
|
|
410
|
+
// On the mtime clock, as the arm is: from `Date.now()` a write the
|
|
411
|
+
// run made within a tick of it carried an earlier mtime and read as
|
|
412
|
+
// an edit (WD-20). The end needs no stamp: an mtime never leads it.
|
|
413
|
+
const start = fsClockNow(cacheDir)
|
|
397
414
|
const cycle = await runOrchestrator(opts)
|
|
398
415
|
held = cycle.persistent
|
|
399
416
|
if (cycle.refused !== undefined) process.stderr.write(`vx watch: ${cycle.refused}\n`)
|
|
@@ -491,7 +508,9 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
491
508
|
const handle = arm(proj.dir, true, (filename) => {
|
|
492
509
|
const abs = path.join(proj.dir, filename)
|
|
493
510
|
if (isIgnoredPath(proj.dir, filename) || fenced(proj.dir, abs)) return
|
|
494
|
-
|
|
511
|
+
// A module the config imports from inside the project is the config
|
|
512
|
+
// too: its edit can add an output or a `workspaceFiles` input.
|
|
513
|
+
if (shapesWatchedSet(filename) || configImportFiles.includes(abs)) reread = true
|
|
495
514
|
trigger(`${proj.name} ${filename}`, abs)
|
|
496
515
|
})
|
|
497
516
|
perProject.set(proj.dir, handle)
|
|
@@ -512,8 +531,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
512
531
|
// / .git / .vx and declared outputs out of what remains.
|
|
513
532
|
arm(workspaceRoot, true, (filename) => {
|
|
514
533
|
if (!matters(filename) || isIgnoredPath(workspaceRoot, filename)) return
|
|
515
|
-
|
|
516
|
-
|
|
534
|
+
const abs = path.join(workspaceRoot, filename)
|
|
535
|
+
if (
|
|
536
|
+
shapesWatchedSet(filename) ||
|
|
537
|
+
filename === LOCKFILE_NAME ||
|
|
538
|
+
configImportFiles.includes(abs)
|
|
539
|
+
)
|
|
540
|
+
reread = true
|
|
541
|
+
trigger(`root ${filename}`, abs)
|
|
517
542
|
})
|
|
518
543
|
: // The root itself, non-recursive, beside the per-project arms, so
|
|
519
544
|
// lockfile + pnpm-workspace.yaml edits trigger re-runs even when
|
|
@@ -820,7 +845,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
820
845
|
|
|
821
846
|
process.stdout.write(`\n${watchingLine(projects.length)}; press Ctrl+C to stop\n`)
|
|
822
847
|
|
|
823
|
-
return await new Promise<
|
|
848
|
+
return await new Promise<void>((resolve) => {
|
|
824
849
|
const cleanup = async (): Promise<void> => {
|
|
825
850
|
pool.closeAll()
|
|
826
851
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
@@ -828,7 +853,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
828
853
|
// it has returned, so the process never exits over a live child.
|
|
829
854
|
await inFlight
|
|
830
855
|
await held?.stop(forwardedSignal(stop.reason))
|
|
831
|
-
resolve(
|
|
856
|
+
resolve()
|
|
832
857
|
}
|
|
833
858
|
if (stop.aborted) {
|
|
834
859
|
void cleanup()
|
package/src/cli/why.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { Cache } from '../cache/index.js'
|
|
10
|
-
import { flagHint, seeHelp } from './help.js'
|
|
10
|
+
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
11
|
import { splitTaskId } from '../graph/index.js'
|
|
12
12
|
import {
|
|
13
13
|
cacheKeyDiff,
|
|
@@ -70,17 +70,8 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
|
|
|
70
70
|
continue
|
|
71
71
|
}
|
|
72
72
|
if (a === '--format' || a.startsWith('--format=')) {
|
|
73
|
-
const fv = a === '--format' ? args[++i] : a.slice(9)
|
|
74
|
-
if (fv
|
|
75
|
-
// An omitted value reads as empty, not as the literal word "undefined".
|
|
76
|
-
return {
|
|
77
|
-
...out,
|
|
78
|
-
error:
|
|
79
|
-
fv === undefined || fv === ''
|
|
80
|
-
? '--format requires a value (pretty | json)'
|
|
81
|
-
: `invalid --format: ${fv} (expected pretty | json)`,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
73
|
+
const fv = formatValue(a === '--format' ? args[++i] : a.slice(9), 'why')
|
|
74
|
+
if (typeof fv === 'object') return { ...out, ...fv }
|
|
84
75
|
out.format = fv
|
|
85
76
|
continue
|
|
86
77
|
}
|
|
@@ -235,7 +226,7 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
235
226
|
const why = whyDidThisRerun(db, runId, taskId)
|
|
236
227
|
if (!why.found) {
|
|
237
228
|
throw new UserError(
|
|
238
|
-
`vx why: run ${runId} has no row for ${taskId}; \`vx last --list\` shows the recorded runs, \`vx last
|
|
229
|
+
`vx why: run ${runId} has no row for ${taskId}; \`vx last --list\` shows the recorded runs, \`vx last RUNID\` what one ran`,
|
|
239
230
|
)
|
|
240
231
|
}
|
|
241
232
|
const diff = cacheKeyDiff(db, runId, taskId)
|
package/src/config.ts
CHANGED
|
@@ -510,9 +510,9 @@ export interface CacheInputs {
|
|
|
510
510
|
* project files" (gitignore-aware), or narrow as needed. An empty
|
|
511
511
|
* array means no file inputs at all.
|
|
512
512
|
*
|
|
513
|
-
* Declared outputs and any nested-project
|
|
514
|
-
* automatically — a task cannot invalidate itself,
|
|
515
|
-
* across project boundaries.
|
|
513
|
+
* Declared outputs (both `outputs` fields) and any nested-project
|
|
514
|
+
* files are excluded automatically — a task cannot invalidate itself,
|
|
515
|
+
* and cannot read across project boundaries.
|
|
516
516
|
*/
|
|
517
517
|
files: string[]
|
|
518
518
|
/**
|
|
@@ -525,8 +525,8 @@ export interface CacheInputs {
|
|
|
525
525
|
* NO project-boundary rule applies: these globs may match files
|
|
526
526
|
* inside other projects' directories. Deliberate escape hatch —
|
|
527
527
|
* prefer project-relative `files` declarations where possible.
|
|
528
|
-
* Declared `outputs
|
|
529
|
-
* task cannot invalidate itself).
|
|
528
|
+
* Declared outputs (both `outputs` fields) are excluded
|
|
529
|
+
* automatically (a task cannot invalidate itself).
|
|
530
530
|
*/
|
|
531
531
|
workspaceFiles?: string[]
|
|
532
532
|
/**
|
package/src/exec/env.ts
CHANGED
|
@@ -33,6 +33,11 @@ export const ESSENTIAL_ENV: readonly string[] = [
|
|
|
33
33
|
'NO_COLOR',
|
|
34
34
|
'CI',
|
|
35
35
|
'NODE_OPTIONS',
|
|
36
|
+
// Where corepack and pnpm keep their own binaries, as HOME is: a machine
|
|
37
|
+
// path, never task output. Without them a corepack shim re-downloads its
|
|
38
|
+
// manager (offline or sandboxed: fails) and pnpm loses its global dir.
|
|
39
|
+
'COREPACK_HOME',
|
|
40
|
+
'PNPM_HOME',
|
|
36
41
|
]
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -125,11 +130,9 @@ export function packageManagerPath(root: string): string | null {
|
|
|
125
130
|
if (known !== undefined) return known
|
|
126
131
|
let name: string | undefined
|
|
127
132
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
).packageManager
|
|
133
|
+
// A byte-order mark is stripped, as npm and discovery strip it.
|
|
134
|
+
const manifest = readFileSync(path.join(root, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
|
|
135
|
+
const field = (JSON.parse(manifest) as { packageManager?: unknown }).packageManager
|
|
133
136
|
if (typeof field === 'string') name = MANAGERS.find((m) => field.startsWith(`${m}@`))
|
|
134
137
|
} catch {}
|
|
135
138
|
name ??= LOCKFILES.find(([f]) => existsSync(path.join(root, f)))?.[1]
|
package/src/exec/index.ts
CHANGED
package/src/exec/proc-sample.ts
CHANGED
|
@@ -134,12 +134,16 @@ async function psTable(): Promise<Map<number, Proc> | undefined> {
|
|
|
134
134
|
return table
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* `ps`'s `time`: `[[dd-]hh:]mm:ss[.cc]`, macOS printing hundredths after
|
|
139
|
+
* the locale's decimal point — `0:01,50` under a German or French locale.
|
|
140
|
+
*/
|
|
138
141
|
export function psTimeMs(s: string): number | undefined {
|
|
139
142
|
const dash = s.indexOf('-')
|
|
140
143
|
const days = dash === -1 ? 0 : Number(s.slice(0, dash))
|
|
141
144
|
const parts = s
|
|
142
145
|
.slice(dash + 1)
|
|
146
|
+
.replace(',', '.')
|
|
143
147
|
.split(':')
|
|
144
148
|
.map(Number)
|
|
145
149
|
if (parts.length < 2 || parts.some((n) => !Number.isFinite(n)) || !Number.isFinite(days)) {
|