@vzn/vx 0.0.356 → 0.0.358
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/package.json +5 -5
- package/src/cli/foreign-flags.ts +2 -1
- package/src/cli/init.ts +33 -2
- package/src/exec/runner.ts +17 -3
- package/src/orchestrator/execute-task.ts +17 -1
- package/src/util/secret-mask.ts +17 -3
- package/src/workspace/affected.ts +10 -1
- package/src/workspace/index.ts +1 -0
- package/src/workspace/migration.ts +4 -1
- package/src/workspace/workspace.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.358",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.358",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.358",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.358",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.358"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/foreign-flags.ts
CHANGED
|
@@ -572,7 +572,8 @@ export const FOREIGN_VERBS: Readonly<Record<string, string>> = {
|
|
|
572
572
|
ls: '`turbo ls` is `vx show` here: every project, its directory and its task count',
|
|
573
573
|
query:
|
|
574
574
|
'`turbo query` has no vx form: `vx run <task> --dry=json` prints the planned graph as JSON',
|
|
575
|
-
reset:
|
|
575
|
+
reset:
|
|
576
|
+
'`nx reset` is `vx cache prune --older-than <age>` here, or remove the cache directory `vx info` names to drop it all',
|
|
576
577
|
daemon: 'vx has no daemon: there is nothing to start or stop',
|
|
577
578
|
login: TURBO_LOGIN,
|
|
578
579
|
logout: TURBO_LOGIN,
|
package/src/cli/init.ts
CHANGED
|
@@ -14,9 +14,9 @@ import { PLUGIN_TEMPLATES } from './plugin-templates.js'
|
|
|
14
14
|
import { isUserError, UserError } from '../util/index.js'
|
|
15
15
|
import {
|
|
16
16
|
applyMigration,
|
|
17
|
+
discoverProjects,
|
|
17
18
|
findWorkspaceRoot,
|
|
18
19
|
type LoadReads,
|
|
19
|
-
listProjects,
|
|
20
20
|
loadWorkspace,
|
|
21
21
|
migrateScripts,
|
|
22
22
|
WORKSPACE_CONFIG_FILENAMES,
|
|
@@ -74,7 +74,8 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
74
74
|
}
|
|
75
75
|
throw err
|
|
76
76
|
})
|
|
77
|
-
const
|
|
77
|
+
const nameless: string[] = []
|
|
78
|
+
const metas = await discoverProjects(await loadWorkspace(root, reads), nameless)
|
|
78
79
|
// `init` reads scripts only; a runner's own config beside them is the
|
|
79
80
|
// richer source (dependsOn, inputs, outputs) and was ignored without a
|
|
80
81
|
// word — the walkthrough on a Turbo repo (2026-09-09) got the scripts'
|
|
@@ -106,6 +107,7 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
106
107
|
),
|
|
107
108
|
source: 'package.json scripts',
|
|
108
109
|
verb: 'vx init',
|
|
110
|
+
notes: namelessNotes(root, nameless),
|
|
109
111
|
dry: parsed.dry,
|
|
110
112
|
force: parsed.force,
|
|
111
113
|
init: true,
|
|
@@ -348,6 +350,35 @@ function firstTask(file: string, runner: 'turbo' | 'nx'): string {
|
|
|
348
350
|
return first ?? 'build'
|
|
349
351
|
}
|
|
350
352
|
|
|
353
|
+
/**
|
|
354
|
+
* A member whose package.json has no `name` is no project, so its scripts
|
|
355
|
+
* map to nothing, and init said nothing of them (D-106).
|
|
356
|
+
*/
|
|
357
|
+
function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
358
|
+
const withScripts = dirs
|
|
359
|
+
.filter((dir) => {
|
|
360
|
+
const scripts = (
|
|
361
|
+
JSON.parse(readText(path.join(dir, 'package.json')) || '{}') as {
|
|
362
|
+
scripts?: unknown
|
|
363
|
+
}
|
|
364
|
+
).scripts
|
|
365
|
+
return (
|
|
366
|
+
typeof scripts === 'object' &&
|
|
367
|
+
scripts !== null &&
|
|
368
|
+
Object.values(scripts).some((v) => typeof v === 'string' && v !== '')
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
|
+
.map((dir) => path.relative(root, dir).split(path.sep).join('/'))
|
|
372
|
+
.sort()
|
|
373
|
+
if (withScripts.length === 0) return []
|
|
374
|
+
const shown =
|
|
375
|
+
withScripts.slice(0, 3).join(', ') +
|
|
376
|
+
(withScripts.length > 3 ? `, and ${withScripts.length - 3} more` : '')
|
|
377
|
+
return [
|
|
378
|
+
`not mapped: ${shown} — ${withScripts.length === 1 ? 'its package.json has' : 'their package.json files have'} no "name", and vx names a project by it; give ${withScripts.length === 1 ? 'it one' : 'each one'} and run \`vx init\` again`,
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
|
|
351
382
|
/** The root package.json as an object, `{}` when unreadable or not one. */
|
|
352
383
|
async function readRootManifest(root: string): Promise<Record<string, unknown>> {
|
|
353
384
|
const pj: unknown = await Bun.file(path.join(root, 'package.json'))
|
package/src/exec/runner.ts
CHANGED
|
@@ -235,9 +235,23 @@ export function execWrap(command: string): string {
|
|
|
235
235
|
*/
|
|
236
236
|
export function execWord(command: string): string | undefined {
|
|
237
237
|
if (SHELL_CONTROL.test(command)) return undefined
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// The word as the shell reads it: quotes group and are removed, so
|
|
239
|
+
// `"./my tool.sh"` names one file (the verdict looked for `"./my`).
|
|
240
|
+
const raw = command.trimStart()
|
|
241
|
+
let first = ''
|
|
242
|
+
let quote = ''
|
|
243
|
+
let i = 0
|
|
244
|
+
for (; i < raw.length; i++) {
|
|
245
|
+
const c = raw[i]!
|
|
246
|
+
if (quote !== '') {
|
|
247
|
+
if (c === quote) quote = ''
|
|
248
|
+
else first += c
|
|
249
|
+
} else if (c === "'" || c === '"') quote = c
|
|
250
|
+
else if (/\s/.test(c)) break
|
|
251
|
+
else first += c
|
|
252
|
+
}
|
|
253
|
+
if (quote !== '' || first === '' || raw.slice(0, i).includes('=')) return undefined
|
|
254
|
+
return SHELL_BUILTINS.has(first) ? undefined : first
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
/**
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from '../exec/index.js'
|
|
38
38
|
import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
39
39
|
import { killGraceMs, maskedEmitter, printable, relPosix, secretMask, span } from '../util/index.js'
|
|
40
|
-
import { SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
40
|
+
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
41
41
|
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
42
42
|
import {
|
|
43
43
|
mayWriteFingerprint,
|
|
@@ -354,6 +354,22 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
354
354
|
persistentOpts.timeoutMs = effectiveTimeout
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
// A stop that landed during the awaits above (the key, the sandbox's
|
|
358
|
+
// arming, request and wrap) leaves nothing to kill yet: spawned now, the
|
|
359
|
+
// server came up after the teardown and held the run's exit 7 s.
|
|
360
|
+
if (args.stopSignal?.aborted === true) {
|
|
361
|
+
if (bridgeTag !== undefined) releaseBridges(bridgeTag)
|
|
362
|
+
await placeholderSweeper(placeholders)()
|
|
363
|
+
return {
|
|
364
|
+
node,
|
|
365
|
+
status: 'aborted',
|
|
366
|
+
// The signal a spawned server would have been sent (a hang-up forwards SIGTERM).
|
|
367
|
+
exitCode: signalExitCode(forwardedSignal(args.stopSignal.reason)),
|
|
368
|
+
durationMs: 0,
|
|
369
|
+
wallclockStartNs,
|
|
370
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
357
373
|
const spawn = runPersistent(persistentOpts)
|
|
358
374
|
// The host side of a port bridge lives exactly as long as the server:
|
|
359
375
|
// released on the child's exit, whether the run tore it down or it died.
|
package/src/util/secret-mask.ts
CHANGED
|
@@ -16,6 +16,22 @@ const SECRET_NAME = /TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL/i
|
|
|
16
16
|
*/
|
|
17
17
|
const NOT_SECRET = /_(FILE|PATH|DIR)$|^GIT_CONFIG_KEY_\d+$/i
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Each name's verdict, decided once: every task asks it of the whole
|
|
21
|
+
* process env (two regex tests a variable, ~10 µs a call at 150 names).
|
|
22
|
+
* The values are read fresh each time; only the name decides this.
|
|
23
|
+
*/
|
|
24
|
+
const secretNames = new Map<string, boolean>()
|
|
25
|
+
|
|
26
|
+
function secretNamed(name: string): boolean {
|
|
27
|
+
let secret = secretNames.get(name)
|
|
28
|
+
if (secret === undefined) {
|
|
29
|
+
secret = SECRET_NAME.test(name) && !NOT_SECRET.test(name)
|
|
30
|
+
secretNames.set(name, secret)
|
|
31
|
+
}
|
|
32
|
+
return secret
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
/**
|
|
20
36
|
* Values shorter than this are not masked: `KEY=1` would mask every `1`
|
|
21
37
|
* in every line, and a value that short is no secret worth the noise.
|
|
@@ -51,9 +67,7 @@ export function secretMask(
|
|
|
51
67
|
}
|
|
52
68
|
for (const source of sources) {
|
|
53
69
|
if (source === undefined) continue
|
|
54
|
-
for (const name in source)
|
|
55
|
-
if (SECRET_NAME.test(name) && !NOT_SECRET.test(name)) add(source[name])
|
|
56
|
-
}
|
|
70
|
+
for (const name in source) if (secretNamed(name)) add(source[name])
|
|
57
71
|
for (const name of named) add(source[name])
|
|
58
72
|
}
|
|
59
73
|
if (values.size === 0) return null
|
|
@@ -700,7 +700,16 @@ async function verifyRef(workspaceRoot: string, ref: string): Promise<void> {
|
|
|
700
700
|
(stderr.length > 0 ? `: ${stderr}` : ''),
|
|
701
701
|
)
|
|
702
702
|
}
|
|
703
|
-
|
|
703
|
+
// A shallow clone (CI's checkout fetches one commit by default) has no
|
|
704
|
+
// HEAD~1 and no base branch: the ref exists, the history does not.
|
|
705
|
+
const shallow = spawnGitSync(['rev-parse', '--is-shallow-repository'], workspaceRoot, 'pipe')
|
|
706
|
+
const isShallow = new TextDecoder().decode(shallow.stdout).trim() === 'true'
|
|
707
|
+
throw new UserError(
|
|
708
|
+
`git ref "${ref}" did not resolve. Pass a branch or commit you have locally.` +
|
|
709
|
+
(isShallow
|
|
710
|
+
? ' This clone is shallow: fetch the history the base needs (`git fetch --unshallow`, or `fetch-depth: 0` on actions/checkout).'
|
|
711
|
+
: ''),
|
|
712
|
+
)
|
|
704
713
|
}
|
|
705
714
|
|
|
706
715
|
function isDirectory(abs: string): boolean {
|
package/src/workspace/index.ts
CHANGED
|
@@ -365,6 +365,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
365
365
|
} else if (empty) {
|
|
366
366
|
report.push(
|
|
367
367
|
`${verb}: no package.json scripts to turn into tasks.`,
|
|
368
|
+
...(args.notes ?? []).map((n) => `note: ${n}`),
|
|
368
369
|
hasWorkspaceFile
|
|
369
370
|
? `${workspaceName} already exists.`
|
|
370
371
|
: dry
|
|
@@ -477,7 +478,9 @@ export function vxInvocation(userAgent: string | undefined, installed: boolean):
|
|
|
477
478
|
case 'pnpm':
|
|
478
479
|
return installed ? 'pnpm vx' : 'pnpm dlx @vzn/vx'
|
|
479
480
|
case 'yarn':
|
|
480
|
-
|
|
481
|
+
// `dlx` is Yarn 2+; Yarn 1 answers `Command "dlx" not found`.
|
|
482
|
+
if (installed) return 'yarn vx'
|
|
483
|
+
return userAgent!.startsWith('yarn/1.') ? 'npx @vzn/vx' : 'yarn dlx @vzn/vx'
|
|
481
484
|
default:
|
|
482
485
|
return 'vx'
|
|
483
486
|
}
|
|
@@ -522,6 +522,18 @@ function absent<T>(err: unknown, value: T): T {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
525
|
+
return discoverProjects(workspace)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* `listProjects`, and `nameless` collects each member directory whose
|
|
530
|
+
* manifest has no `name`: `vx init` names them, since their scripts map to
|
|
531
|
+
* nothing.
|
|
532
|
+
*/
|
|
533
|
+
export async function discoverProjects(
|
|
534
|
+
workspace: Workspace,
|
|
535
|
+
nameless?: string[],
|
|
536
|
+
): Promise<ProjectMeta[]> {
|
|
525
537
|
// Run all package globs concurrently. Disk-bound walks parallelize
|
|
526
538
|
// well; serializing them just stretches the discovery phase by N×.
|
|
527
539
|
const { positive, negative } = splitPackageGlobs(workspace.packageGlobs)
|
|
@@ -567,6 +579,7 @@ export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]>
|
|
|
567
579
|
if (entry === null) continue
|
|
568
580
|
const { dir, pkg, configPath } = entry
|
|
569
581
|
if (!pkg.name) {
|
|
582
|
+
nameless?.push(dir)
|
|
570
583
|
// A nameless manifest can't be addressed, filtered, or made affected —
|
|
571
584
|
// and vx identifies projects by name, so it simply vanishes. Silent is
|
|
572
585
|
// fine for a dir that declares no tasks; a dir with a vx config was
|