@vzn/vx 0.0.81 → 0.0.82
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.mjs +51 -23
- package/package.json +5 -5
- package/src/cache/git-inputs.ts +21 -3
- package/src/workspace/workspace.ts +13 -5
package/launcher.mjs
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// with no prebuilt binary if the user happens to have Bun).
|
|
16
16
|
// Anything else is a clear, actionable error.
|
|
17
17
|
|
|
18
|
-
import { spawnSync } from 'node:child_process'
|
|
19
|
-
import { existsSync } from 'node:fs'
|
|
18
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process'
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
20
|
import { createRequire } from 'node:module'
|
|
21
21
|
import { constants as osConstants } from 'node:os'
|
|
22
22
|
import { dirname, join } from 'node:path'
|
|
@@ -57,35 +57,63 @@ function hasBun() {
|
|
|
57
57
|
return probe.status === 0
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Is this launcher in its terminal's foreground process group? Then a
|
|
61
|
+
// keyboard Ctrl-C reached the binary too, and a forward would be its second
|
|
62
|
+
// signal, which vx reads as "stop now".
|
|
63
|
+
function inForeground() {
|
|
64
|
+
try {
|
|
65
|
+
const stat = readFileSync('/proc/self/stat', 'utf8')
|
|
66
|
+
const f = stat.slice(stat.lastIndexOf(')') + 2).split(' ')
|
|
67
|
+
return f[2] === f[5] // pgrp === tpgid
|
|
68
|
+
} catch {
|
|
69
|
+
try {
|
|
70
|
+
const out = execFileSync('ps', ['-o', 'pgid=,tpgid=', '-p', String(process.pid)], {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
})
|
|
73
|
+
const [pgid, tpgid] = out.trim().split(/\s+/)
|
|
74
|
+
return pgid === tpgid
|
|
75
|
+
} catch {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function run(cmd, cmdArgs) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
const child = spawn(cmd, cmdArgs, { stdio: 'inherit' })
|
|
83
|
+
// A signal sent to this launcher alone (`kill`, a process manager, a CI
|
|
84
|
+
// cancel) reaches vx only through here; unhandled, it killed the launcher
|
|
85
|
+
// and left vx running under init. Either way the launcher waits for vx.
|
|
86
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
87
|
+
process.on(sig, () => {
|
|
88
|
+
if (!inForeground()) child.kill(sig)
|
|
89
|
+
})
|
|
65
90
|
}
|
|
91
|
+
child.on('error', (err) => {
|
|
92
|
+
process.stderr.write(`${base}: failed to launch (${err.message})\n`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
})
|
|
66
95
|
// Mirror the child's exit; a signal death maps to the POSIX 128+signo code.
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
child.on('exit', (code, signal) => {
|
|
97
|
+
process.exit(signal ? 128 + (osConstants.signals[signal] ?? 1) : (code ?? 0))
|
|
98
|
+
})
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
const bin = platformBinary()
|
|
72
|
-
if (bin !== undefined) {
|
|
73
|
-
run(bin, args)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
102
|
// No prebuilt binary for this platform — fall back to the shipped source if Bun
|
|
77
103
|
// is available (a source checkout, or an unsupported platform + Bun installed).
|
|
78
104
|
const source = join(here, sourceEntry)
|
|
79
|
-
if (
|
|
105
|
+
if (bin !== undefined) {
|
|
106
|
+
run(bin, args)
|
|
107
|
+
} else if (existsSync(source) && hasBun()) {
|
|
80
108
|
run('bun', [source, ...args])
|
|
109
|
+
} else {
|
|
110
|
+
const supported = SUPPORTED.join(', ')
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`${base}: no prebuilt binary for ${key}.\n` +
|
|
113
|
+
` Supported platforms: ${supported}.\n` +
|
|
114
|
+
` If your platform should be supported, reinstall so npm fetches the\n` +
|
|
115
|
+
` matching ${name}-${key} optionalDependency, or install Bun (>=1.4) to\n` +
|
|
116
|
+
` run ${base} from source.\n`,
|
|
117
|
+
)
|
|
118
|
+
process.exit(1)
|
|
81
119
|
}
|
|
82
|
-
|
|
83
|
-
const supported = SUPPORTED.join(', ')
|
|
84
|
-
process.stderr.write(
|
|
85
|
-
`${base}: no prebuilt binary for ${key}.\n` +
|
|
86
|
-
` Supported platforms: ${supported}.\n` +
|
|
87
|
-
` If your platform should be supported, reinstall so npm fetches the\n` +
|
|
88
|
-
` matching ${name}-${key} optionalDependency, or install Bun (>=1.4) to\n` +
|
|
89
|
-
` run ${base} from source.\n`,
|
|
90
|
-
)
|
|
91
|
-
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.82",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.82",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.82",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.82",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.82"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/git-inputs.ts
CHANGED
|
@@ -483,6 +483,8 @@ async function dropFilteredOids(
|
|
|
483
483
|
args: {
|
|
484
484
|
/** Every path the enumeration listed: tracked (dirty ones too) and untracked. */
|
|
485
485
|
listed: readonly string[]
|
|
486
|
+
/** Whether `git status` named an ignored `.gitattributes`, which git applies too. */
|
|
487
|
+
ignoredAttributes: boolean
|
|
486
488
|
workspaceRoot: string
|
|
487
489
|
gitDir: string
|
|
488
490
|
gitPrefix: string
|
|
@@ -501,6 +503,7 @@ async function dropFilteredOids(
|
|
|
501
503
|
// a line scan of `git var -l`, three stats, and a key walk that exits on the
|
|
502
504
|
// first `.gitattributes` it does not find.
|
|
503
505
|
let attributesPossible =
|
|
506
|
+
args.ignoredAttributes ||
|
|
504
507
|
attributeFilesOutsideTree(args.gitVars, process.env).some((f) =>
|
|
505
508
|
existsSync(path.resolve(args.workspaceRoot, f)),
|
|
506
509
|
) ||
|
|
@@ -715,14 +718,25 @@ export function attributeFilesOutsideTree(
|
|
|
715
718
|
function parseStatusOutput(
|
|
716
719
|
out: string,
|
|
717
720
|
undecodableRecords: ReadonlySet<string>,
|
|
718
|
-
): {
|
|
721
|
+
): {
|
|
722
|
+
dirty: Set<string>
|
|
723
|
+
untracked: string[]
|
|
724
|
+
undecodable: Set<string>
|
|
725
|
+
ignoredAttributes: boolean
|
|
726
|
+
} {
|
|
719
727
|
const tokens = out.split('\0')
|
|
720
728
|
const dirty = new Set<string>()
|
|
721
729
|
const untracked: string[] = []
|
|
722
730
|
const undecodable = new Set<string>()
|
|
731
|
+
let ignoredAttributes = false
|
|
723
732
|
for (let i = 0; i < tokens.length; i++) {
|
|
724
733
|
const token = tokens[i]!
|
|
725
734
|
if (token.length < 4) continue
|
|
735
|
+
if (token[0] === '!') {
|
|
736
|
+
if (token === '!! .gitattributes' || token.endsWith('/.gitattributes'))
|
|
737
|
+
ignoredAttributes = true
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
726
740
|
if (undecodableRecords.size > 0 && undecodableRecords.has(token))
|
|
727
741
|
undecodable.add(token.slice(3))
|
|
728
742
|
if (token[0] === '?') {
|
|
@@ -743,7 +757,7 @@ function parseStatusOutput(
|
|
|
743
757
|
dirty.add(tokens[i]!)
|
|
744
758
|
}
|
|
745
759
|
}
|
|
746
|
-
return { dirty, untracked, undecodable }
|
|
760
|
+
return { dirty, untracked, undecodable, ignoredAttributes }
|
|
747
761
|
}
|
|
748
762
|
|
|
749
763
|
/**
|
|
@@ -885,7 +899,10 @@ export async function startGitEnumeration(
|
|
|
885
899
|
// time (~50 ms of CPU, concurrent with status but contending with it).
|
|
886
900
|
const running = Promise.all([
|
|
887
901
|
spawnGit(['ls-files', '-s', '-v', '-z', '--', ...pathspecs]),
|
|
888
|
-
|
|
902
|
+
// `--ignored=matching` names an ignored path without descending into an
|
|
903
|
+
// ignored directory: git applies an ignored `.gitattributes` as it does
|
|
904
|
+
// any other, and the filter gate below must see it (A-19).
|
|
905
|
+
spawnGit(['status', '--porcelain', '-z', '-uall', '--ignored=matching', '--', ...pathspecs]),
|
|
889
906
|
// The gate for whether a clean filter can rewrite bytes between the
|
|
890
907
|
// index and the worktree: git's merged config (`core.autocrlf`) and,
|
|
891
908
|
// from git 2.42, the attributes files it reads outside the tree
|
|
@@ -983,6 +1000,7 @@ export async function startGitEnumeration(
|
|
|
983
1000
|
// ONLY where a filter can actually apply.
|
|
984
1001
|
await dropFilteredOids(trusted, {
|
|
985
1002
|
listed: all,
|
|
1003
|
+
ignoredAttributes: parsedStatus?.ignoredAttributes ?? false,
|
|
986
1004
|
workspaceRoot,
|
|
987
1005
|
gitDir,
|
|
988
1006
|
gitPrefix,
|
|
@@ -470,11 +470,17 @@ async function findConfigFile(dir: string): Promise<string | null> {
|
|
|
470
470
|
} catch {
|
|
471
471
|
return null
|
|
472
472
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
// By precedence slot, not a Map per directory, and no `path.join`: at
|
|
474
|
+
// 5,000 projects that cut `listProjects` from 53.5 to 47.7 ms (min of
|
|
475
|
+
// 15, six interleaved rounds, A/A 54.7).
|
|
476
|
+
const found: Dirent[] = []
|
|
477
|
+
for (const e of entries) {
|
|
478
|
+
const rank = CONFIG_RANK.get(e.name)
|
|
479
|
+
if (rank !== undefined) found[rank] = e
|
|
480
|
+
}
|
|
481
|
+
for (const entry of found) {
|
|
476
482
|
if (entry === undefined) continue
|
|
477
|
-
const candidate = path.
|
|
483
|
+
const candidate = dir + path.sep + entry.name
|
|
478
484
|
if (entry.isFile()) return candidate
|
|
479
485
|
if (entry.isSymbolicLink() && (await isFile(candidate))) return candidate
|
|
480
486
|
}
|
|
@@ -487,6 +493,8 @@ async function findConfigFile(dir: string): Promise<string | null> {
|
|
|
487
493
|
return null
|
|
488
494
|
}
|
|
489
495
|
|
|
496
|
+
const CONFIG_RANK = new Map(PROJECT_CONFIG_FILENAMES.map((n, i) => [n, i]))
|
|
497
|
+
|
|
490
498
|
async function isFile(p: string): Promise<boolean> {
|
|
491
499
|
try {
|
|
492
500
|
return (await stat(p)).isFile()
|
|
@@ -518,7 +526,7 @@ export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]>
|
|
|
518
526
|
// a 1000-member workspace.
|
|
519
527
|
const loaded = await Promise.all(
|
|
520
528
|
[...matches].map(async (dir) => {
|
|
521
|
-
const pkgJsonPath = path.
|
|
529
|
+
const pkgJsonPath = dir + path.sep + 'package.json'
|
|
522
530
|
const [configPath, text] = await Promise.all([
|
|
523
531
|
findConfigFile(dir),
|
|
524
532
|
Bun.file(pkgJsonPath)
|