@vzn/vx 0.0.434 → 0.0.435

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.434",
3
+ "version": "0.0.435",
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.434",
25
- "@vzn/vx-linux-arm64": "0.0.434",
26
- "@vzn/vx-darwin-x64": "0.0.434",
27
- "@vzn/vx-darwin-arm64": "0.0.434"
24
+ "@vzn/vx-linux-x64": "0.0.435",
25
+ "@vzn/vx-linux-arm64": "0.0.435",
26
+ "@vzn/vx-darwin-x64": "0.0.435",
27
+ "@vzn/vx-darwin-arm64": "0.0.435"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/show.ts CHANGED
@@ -10,7 +10,7 @@ import path from 'node:path'
10
10
  import type { ProjectConfig, TaskConfig } from '../config.js'
11
11
  import { declaredTask } from '../graph/index.js'
12
12
  import { flagHint, seeHelp } from './help.js'
13
- import { nearMatches, relPosix, secretMask, UserError } from '../util/index.js'
13
+ import { listed, nearMatches, relPosix, secretMask, UserError } from '../util/index.js'
14
14
  import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
15
15
  import {
16
16
  findWorkspaceRoot,
@@ -94,7 +94,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
94
94
  if (projectName !== undefined && (taskName !== undefined || byName.has(projectName))) {
95
95
  if (!byName.has(projectName)) {
96
96
  throw new UserError(
97
- `vx show: unknown project: "${projectName}"${suggest(projectName, [...byName.keys()])}`,
97
+ `vx show: unknown project: "${projectName}"${suggest(projectName, [...byName.keys()], '', 'projects')}`,
98
98
  )
99
99
  }
100
100
  }
@@ -121,7 +121,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
121
121
  // `nx show projects` lists them; here a bare `vx show` does.
122
122
  const nx = projectName === 'projects' ? ' (`nx show projects` is `vx show` here)' : ''
123
123
  throw new UserError(
124
- `vx show: unknown project or task: "${projectName}"${nx}${suggest(projectName!, [...byName.keys(), ...names])}`,
124
+ `vx show: unknown project or task: "${projectName}"${nx}${suggest(projectName!, [...byName.keys(), ...names], '', 'projects and tasks')}`,
125
125
  )
126
126
  }
127
127
  process.stdout.write(renderTaskAcross(root, declaring, projectName!, parsed.format))
@@ -143,7 +143,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
143
143
  const task = declaredTask(config, taskName)
144
144
  if (task === undefined) {
145
145
  throw new UserError(
146
- `vx show: unknown task: "${meta.name}#${taskName}"${suggest(taskName, Object.keys(config?.tasks ?? {}), `${meta.name}#`)}`,
146
+ `vx show: unknown task: "${meta.name}#${taskName}"${suggest(taskName, Object.keys(config?.tasks ?? {}), `${meta.name}#`, 'its tasks')}`,
147
147
  )
148
148
  }
149
149
  process.stdout.write(renderTask(meta.name, dir, taskName, task, parsed.format))
@@ -154,14 +154,21 @@ export async function showCmd(args: readonly string[]): Promise<number> {
154
154
  * Near misses by edit distance, plus partial names in either direction.
155
155
  * `prefix` makes each a spec the user can paste (`app#build`, not `build`).
156
156
  */
157
- function suggest(query: string, candidates: readonly string[], prefix = ''): string {
157
+ function suggest(
158
+ query: string,
159
+ candidates: readonly string[],
160
+ prefix = '',
161
+ label = 'names',
162
+ ): string {
158
163
  const q = query.toLowerCase()
159
164
  const near = new Set(nearMatches(query, candidates))
160
165
  for (const c of candidates) {
161
166
  const n = c.toLowerCase()
162
167
  if (n.includes(q) || q.includes(n)) near.add(c)
163
168
  }
164
- return near.size > 0 ? ` — did you mean ${[...near].map((n) => prefix + n).join(', ')}?` : ''
169
+ if (near.size > 0) return ` — did you mean ${[...near].map((n) => prefix + n).join(', ')}?`
170
+ // Nothing near named nothing to pick from (M-57): say what exists.
171
+ return candidates.length === 0 ? '' : `; ${label}: ${listed(candidates.map((c) => prefix + c))}`
165
172
  }
166
173
 
167
174
  function projectDir(root: string, meta: ProjectMeta): string {
@@ -32,7 +32,6 @@ import {
32
32
  lstatSync,
33
33
  existsSync,
34
34
  mkdirSync,
35
- mkdtempSync,
36
35
  openSync,
37
36
  readFileSync,
38
37
  readlinkSync,
@@ -538,19 +537,42 @@ function scanCommandAgain(): string {
538
537
  }
539
538
  }
540
539
  /**
541
- * An empty executable only this process can write: SRT runs it outside the
542
- * sandbox on every wrap, so it lives in a fresh 0700 directory, never in the
543
- * shared `/tmp/claude`. Made once a process, again if a temp cleaner took it.
540
+ * An empty executable only this user can write: SRT runs it outside the
541
+ * sandbox on every wrap, so it lives in this user's own 0700 directory under
542
+ * the OS temp dir, never the shared `/tmp/claude`. One directory per user,
543
+ * not per process: a per-process one outlived every SIGKILLed run, and the
544
+ * exit hook that removed it fired under a test's emitted `exit` with the
545
+ * process going on (B-93). Anything else at the name (another owner, group
546
+ * or other bits, a link, a non-empty file) is refused, and the caller falls
547
+ * back to `true`.
544
548
  */
545
549
  function noScanCommand(): string {
546
550
  if (noScan !== undefined && existsSync(noScan)) return noScan
547
- const dir = mkdtempSync(path.join(os.tmpdir(), 'vx-noscan-'))
551
+ const uid = process.getuid!()
552
+ const dir = path.join(os.tmpdir(), `vx-noscan-${uid}`)
553
+ try {
554
+ mkdirSync(dir, { mode: 0o700 })
555
+ } catch (err) {
556
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err
557
+ }
558
+ const d = lstatSync(dir)
559
+ // A link reads mode 0777 here, and a file at the name fails the write
560
+ // below (ENOTDIR).
561
+ if (d.uid !== uid || (d.mode & 0o077) !== 0) {
562
+ throw new Error(`${dir} is not this user's own directory`)
563
+ }
548
564
  const file = path.join(dir, 'rg')
549
- writeFileSync(file, '', { mode: 0o700 })
550
- if (noScan === undefined) {
551
- process.on('exit', () => {
552
- if (noScan !== undefined) rmSync(path.dirname(noScan), { recursive: true, force: true })
553
- })
565
+ try {
566
+ writeFileSync(file, '', { mode: 0o700, flag: 'wx' })
567
+ } catch (err) {
568
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err
569
+ }
570
+ const f = lstatSync(file)
571
+ // SRT's dependency check (`Bun.which`) refuses what is not an executable
572
+ // file, which would make the sandbox unavailable rather than fall back. A
573
+ // directory's size is never 0.
574
+ if (f.size !== 0 || (f.mode & 0o100) === 0) {
575
+ throw new Error(`${file} is not an empty executable of this user's`)
554
576
  }
555
577
  noScan = file
556
578
  return file