@vzn/vx 0.0.465 → 0.0.467

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.465",
3
+ "version": "0.0.467",
4
4
  "description": "The fastest 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.465",
25
- "@vzn/vx-linux-arm64": "0.0.465",
26
- "@vzn/vx-darwin-x64": "0.0.465",
27
- "@vzn/vx-darwin-arm64": "0.0.465"
24
+ "@vzn/vx-linux-x64": "0.0.467",
25
+ "@vzn/vx-linux-arm64": "0.0.467",
26
+ "@vzn/vx-darwin-x64": "0.0.467",
27
+ "@vzn/vx-darwin-arm64": "0.0.467"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/last.ts CHANGED
@@ -208,6 +208,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
208
208
  noteSchemaReset(cache, warnToStderr)
209
209
  try {
210
210
  const db = cache.dbHandle()
211
+ // Nothing recorded at all is its own answer: `--failed` said "no
212
+ // recorded run failed" and a run id pointed at a `--list` that lists
213
+ // nothing, while `--list --failed` past green runs said there were none.
214
+ const noRuns = (): boolean => listInvocations(db, { limit: 1 }).length === 0
211
215
 
212
216
  if (parsed.list !== undefined) {
213
217
  const invocations =
@@ -219,7 +223,9 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
219
223
  return 0
220
224
  }
221
225
  if (invocations.length === 0) {
222
- process.stdout.write('no recorded runs\n')
226
+ process.stdout.write(
227
+ parsed.failed === true && !noRuns() ? 'no recorded run failed\n' : 'no recorded runs\n',
228
+ )
223
229
  return 0
224
230
  }
225
231
  for (const inv of invocations) {
@@ -241,11 +247,13 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
241
247
  : (listInvocations(db, { limit: 1 })[0] ?? null)
242
248
  if (inv === null || inv === undefined) {
243
249
  throw new UserError(
244
- parsed.runId !== undefined
245
- ? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
246
- : parsed.failed === true
247
- ? 'vx last: no recorded run failed'
248
- : 'vx last: no recorded runs yet — run something first',
250
+ noRuns()
251
+ ? 'vx last: no recorded runs yet — run something first'
252
+ : parsed.runId !== undefined
253
+ ? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
254
+ : parsed.failed === true
255
+ ? 'vx last: no recorded run failed'
256
+ : 'vx last: no recorded runs yet — run something first',
249
257
  )
250
258
  }
251
259
  const detail = getRun(db, inv.runId)
@@ -114,8 +114,34 @@ export interface CollectInfoOptions {
114
114
  export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): Promise<InfoFacts> {
115
115
  const warn = opts.warn ?? warnToStderr
116
116
  const reads: LoadReads = new Map()
117
+ // Both first: outside a workspace, or under a workspace config that does
118
+ // not load, the doctor refuses at once, with no probe (~180 ms) ahead.
117
119
  const root = await findWorkspaceRoot(cwd, reads)
118
- const { workspaceConfig, plugins } = await loadWorkspacePlugins(root, warn)
120
+ const workspace = await loadWorkspacePlugins(root, warn)
121
+ // The sandbox probe asks nothing of the workspace, so it runs under the
122
+ // loads below instead of after them. Awaited on every path: it resets
123
+ // the runtime it brought up, whose sockets would hold the process open.
124
+ const probing = sandboxProbe()
125
+ // Rejects only if the reset does; awaited below, never unhandled.
126
+ probing.catch(() => {})
127
+ try {
128
+ return await collectWorkspaceInfo(cwd, root, reads, workspace, opts, probing)
129
+ } catch (err) {
130
+ // The workspace's refusal is the error worth reporting.
131
+ await probing.catch(() => {})
132
+ throw err
133
+ }
134
+ }
135
+
136
+ async function collectWorkspaceInfo(
137
+ cwd: string,
138
+ root: string,
139
+ reads: LoadReads,
140
+ { workspaceConfig, plugins }: Awaited<ReturnType<typeof loadWorkspacePlugins>>,
141
+ opts: CollectInfoOptions,
142
+ probing: Promise<Omit<InfoFacts['sandbox'], 'declared'>>,
143
+ ): Promise<InfoFacts> {
144
+ const warn = opts.warn ?? warnToStderr
119
145
  const cacheDir =
120
146
  opts.cacheDir === undefined
121
147
  ? resolveCacheDir(root, workspaceConfig)
@@ -173,7 +199,10 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
173
199
  }
174
200
 
175
201
  const lockPresent = await Bun.file(lockfilePath(root)).exists()
176
- const sandbox = await sandboxFact(sandboxed)
202
+ // Asked while the probe runs: each is a synchronous spawn.
203
+ const git = gitVersion()
204
+ const statusCache = gitStatusCache(root)
205
+ const sandbox = { ...(await probing), declared: sandboxed }
177
206
  return {
178
207
  vx: VERSION,
179
208
  // An unsupported Bun does not stop a run, it makes the run's ANSWERS
@@ -183,12 +212,12 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
183
212
  // have, which is a property 19 tests hold on purpose.
184
213
  bun: Bun.version,
185
214
  bunSupported: !isUnsupportedBun(Bun.version),
186
- git: gitVersion(),
215
+ git,
187
216
  // The one `git status` walk per run is the warm path's critical path on
188
217
  // a large tree (~55 ms at 1000 projects, measured 2026-09-02). git's
189
218
  // own caches make it near-free after the first run, and they are OFF by
190
219
  // default — say so, since nothing else in a run would.
191
- gitStatusCache: gitStatusCache(root),
220
+ gitStatusCache: statusCache,
192
221
  workspaceRoot: root,
193
222
  projects: metas.length,
194
223
  tasks: taskCount,
@@ -224,24 +253,19 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
224
253
  }
225
254
 
226
255
  /**
227
- * The runtime probe's verdict — one sandboxed `true` (memoized) — and the
228
- * count of loaded tasks that would meet it. The probe initializes the
229
- * Linux runtime, whose proxy sockets would keep a standalone process
230
- * alive; reset after asking, since the doctor runs nothing.
256
+ * The runtime probe's verdict — one sandboxed `true` (memoized). The probe
257
+ * initializes the Linux runtime, whose proxy sockets would keep a
258
+ * standalone process alive; reset after asking, since the doctor runs
259
+ * nothing.
231
260
  */
232
- async function sandboxFact(declared: number): Promise<InfoFacts['sandbox']> {
261
+ async function sandboxProbe(): Promise<Omit<InfoFacts['sandbox'], 'declared'>> {
233
262
  try {
234
263
  const verdict = await probeSandbox()
235
264
  const untraced = verdict.available ? await untracedReason() : null
236
- return {
237
- available: verdict.available,
238
- reason: stableSandboxReason(verdict.reason),
239
- declared,
240
- untraced,
241
- }
265
+ return { available: verdict.available, reason: stableSandboxReason(verdict.reason), untraced }
242
266
  } catch (err) {
243
267
  const message = err instanceof Error ? err.message : String(err)
244
- return { available: false, reason: stableSandboxReason(message), declared, untraced: null }
268
+ return { available: false, reason: stableSandboxReason(message), untraced: null }
245
269
  } finally {
246
270
  await resetSandbox()
247
271
  }
@@ -151,7 +151,6 @@ export async function affectedProjects(args: AffectedArgs): Promise<Set<string>>
151
151
  `("${since.slice(0, range) || 'HEAD'}"); vx diffs it against the working tree.`,
152
152
  )
153
153
  }
154
- await verifyRef(args.workspaceRoot, since)
155
154
  // Diff from the MERGE BASE of `since` and HEAD, not from `since` itself:
156
155
  // on a branch whose base has moved on, `git diff <base>` reports every
157
156
  // file OTHER people changed on the base (over-selection that defeats a
@@ -160,7 +159,9 @@ export async function affectedProjects(args: AffectedArgs): Promise<Set<string>>
160
159
  // when there is none (unrelated histories, a detached probe) the ref
161
160
  // itself is the base, as before. Turbo's two-dot range `A..HEAD` is the
162
161
  // one form that diffs from A itself.
163
- const base = fromMergeBase ? await mergeBase(args.workspaceRoot, since) : since
162
+ const base = fromMergeBase
163
+ ? await mergeBase(args.workspaceRoot, since)
164
+ : (await verifyRef(args.workspaceRoot, since), since)
164
165
 
165
166
  const [diffed, untracked] = await Promise.all([
166
167
  // `--no-renames` is crucial for project-affected detection: with
@@ -693,7 +694,13 @@ const resolvedRefs = new Map<string, string>()
693
694
  /** A ref that names an ancestor of HEAD by its own spelling (`HEAD~1`, `HEAD^`). */
694
695
  const HEAD_ANCESTOR = /^HEAD(?:~\d*|\^\d*)+$/
695
696
 
696
- /** `git merge-base <ref> HEAD`, or `ref` itself when the two share no ancestor. */
697
+ /**
698
+ * `git merge-base <ref> HEAD`, or `ref` itself when the two share no
699
+ * ancestor. A merge base proves the ref resolves, so `verifyRef`'s own
700
+ * spawn (synchronous, ~3.5 ms) runs only when there is none: to refuse a
701
+ * ref that does not resolve, or to pass one that does (exit 1, unrelated
702
+ * histories; a tree).
703
+ */
697
704
  async function mergeBase(workspaceRoot: string, ref: string): Promise<string> {
698
705
  // HEAD's own ancestor is its merge base with HEAD: the commit it names,
699
706
  // which the default base's search already resolved (`HEAD~1`).
@@ -702,7 +709,9 @@ async function mergeBase(workspaceRoot: string, ref: string): Promise<string> {
702
709
  const proc = spawnGit(['merge-base', '--end-of-options', ref, 'HEAD'], workspaceRoot)
703
710
  const [out, exit] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
704
711
  const sha = out.trim()
705
- return exit === 0 && sha.length > 0 ? sha : ref
712
+ if (exit === 0 && sha.length > 0) return sha
713
+ await verifyRef(workspaceRoot, ref)
714
+ return ref
706
715
  }
707
716
 
708
717
  async function verifyRef(workspaceRoot: string, ref: string): Promise<void> {