@vzn/vx 0.0.522 → 0.0.524

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.522",
3
+ "version": "0.0.524",
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.522",
25
- "@vzn/vx-linux-arm64": "0.0.522",
26
- "@vzn/vx-darwin-x64": "0.0.522",
27
- "@vzn/vx-darwin-arm64": "0.0.522"
24
+ "@vzn/vx-linux-x64": "0.0.524",
25
+ "@vzn/vx-linux-arm64": "0.0.524",
26
+ "@vzn/vx-darwin-x64": "0.0.524",
27
+ "@vzn/vx-darwin-arm64": "0.0.524"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -34,6 +34,7 @@
34
34
  import { Database } from 'bun:sqlite'
35
35
  import {
36
36
  accessSync,
37
+ chmodSync,
37
38
  closeSync,
38
39
  constants,
39
40
  existsSync,
@@ -169,38 +170,6 @@ function outOfFdsAtOpen(dbFile: string, err: unknown): UserError | undefined {
169
170
  }
170
171
  }
171
172
 
172
- /** Say once, on the channel the opener has, that an upgrade emptied the index. */
173
- export function noteSchemaReset(cache: Cache, warn: (message: string) => void): void {
174
- if (cache.storeFallback !== null) {
175
- warn(
176
- `[vx] shared cache store ${cache.storeFallback} is not usable; entries stay in ${cache.storeDir ?? 'this workspace'}, where no other workspace hits them`,
177
- )
178
- }
179
- if (cache.storeMoved !== null) {
180
- const { from, to } = cache.storeMoved
181
- warn(
182
- `[vx] cache entries now live in the shared store ${to}; the ones in ${from} miss once, and \`vx cache prune\` reclaims their artifacts`,
183
- )
184
- }
185
- if (cache.storeReset !== null) {
186
- const { from, to } = cache.storeReset
187
- warn(
188
- `[vx] shared cache store ${cache.storeDir} re-indexed: schema ${from} → ${to} (vx version changed); its artifacts stay and are indexed again as tasks hit them`,
189
- )
190
- }
191
- if (cache.formatChange !== null) {
192
- const { from, to } = cache.formatChange
193
- warn(
194
- `[vx] cache format changed: ${from} → ${to} (vx version changed); every cached task misses once and re-saves, and the old entries, never read again, age out under \`vx cache prune --older-than\` or \`cacheRetention\``,
195
- )
196
- return
197
- }
198
- if (cache.schemaReset === null) return
199
- const { from, to } = cache.schemaReset
200
- warn(
201
- `[vx] cache index reset: schema ${from} → ${to} (vx version changed); run history starts over, and each artifact is indexed again when its task next hits`,
202
- )
203
- }
204
173
  // SCHEMA history (drop+recreate on mismatch; pre-alpha, no migrations):
205
174
  // v20: file_hashes.content_hash (git blob OIDs).
206
175
  // v21: dropped the unused outputs_hash column (pure-input hashing).
@@ -459,7 +428,16 @@ function ensurePrivateDir(dir: string): string | null {
459
428
  if (!st.isDirectory()) return `${dir} is not a directory`
460
429
  const uid = process.getuid?.()
461
430
  if (uid !== undefined && st.uid !== uid) return `${dir} belongs to another user`
462
- if ((st.mode & 0o077) !== 0) return `${dir} is open to other users (chmod 700 it)`
431
+ // The user's own directory open to others (a `~/.vx` made at the
432
+ // default umask, 0755) is vx's to close: refusing it left the store
433
+ // unused on a stock macOS home (owner, 2026-10-06: "you should own it").
434
+ if ((st.mode & 0o077) !== 0) {
435
+ try {
436
+ chmodSync(dir, 0o700)
437
+ } catch (err) {
438
+ return `${dir} is open to other users and chmod 700 failed (${errorText(err)})`
439
+ }
440
+ }
463
441
  return null
464
442
  }
465
443
 
@@ -550,8 +528,7 @@ export class Cache implements CacheLayer {
550
528
 
551
529
  /**
552
530
  * The store this open was asked for and could not use, with why: the
553
- * entries went to a store inside `cacheDir` instead. Said once by
554
- * `noteSchemaReset`.
531
+ * entries went to a store inside `cacheDir` instead.
555
532
  */
556
533
  readonly storeFallback: string | null = null
557
534
 
@@ -561,24 +538,22 @@ export class Cache implements CacheLayer {
561
538
  /**
562
539
  * Set when THIS open found an index written by another `SCHEMA_VERSION`
563
540
  * and dropped every table. The next open sees the current version and
564
- * reports null, so whoever opened first is the one that can say so —
565
- * `noteSchemaReset` is that one line, at every opener that has a
566
- * channel. Without it an upgrade empties the cache and the history in
567
- * silence, and the all-miss run that follows looks like a bug.
541
+ * reports null. vx prints nothing for it: the cache is vx's to keep
542
+ * (owner, 2026-10-06).
568
543
  */
569
544
  readonly schemaReset: SchemaReset | null = null
570
545
 
571
546
  /**
572
547
  * Set when THIS open found entries written under another
573
548
  * `CACHE_VERSION`: the index survives, but no old key is derived again,
574
- * so every cached task misses once. Reported by `noteSchemaReset`.
549
+ * so every cached task misses once. Not printed, like `schemaReset`.
575
550
  */
576
551
  readonly formatChange: SchemaReset | null = null
577
552
 
578
553
  /**
579
554
  * Set when THIS open found the shared store written under another
580
555
  * `SCHEMA_VERSION` and dropped its inventory; the artifacts stay and are
581
- * indexed again as they hit. Reported by `noteSchemaReset`.
556
+ * indexed again as they hit. Never printed: the cache is vx's to keep.
582
557
  */
583
558
  readonly storeReset: SchemaReset | null = null
584
559
 
@@ -25,7 +25,6 @@ export {
25
25
  FILE_HASH_RACY_MS,
26
26
  OUTPUT_DIRS_RACY_MS,
27
27
  racyWindowMs,
28
- noteSchemaReset,
29
28
  type SchemaReset,
30
29
  SCHEMA_VERSION,
31
30
  } from './cache.js'
package/src/cli/cache.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { Cache, noteSchemaReset } from '../cache/index.js'
2
+ import { Cache } from '../cache/index.js'
3
3
  import { flagHint, refusedWord, seeHelp } from './help.js'
4
4
  import { nearest, parseDuration, parseSize } from '../util/index.js'
5
5
  import { acquireRunLock } from '../orchestrator/index.js'
@@ -189,7 +189,6 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
189
189
  // stack (an unprivileged user on a root-owned `.vx`, 2026-09-16). A dry
190
190
  // run only reads, and reads a read-only cache fine.
191
191
  if (!parsed.dryRun) cache.assertWritable()
192
- noteSchemaReset(cache, warnToStderr)
193
192
  // A prune beside a run on this workspace evicts what the run has just
194
193
  // probed as hits, whose `accessed_at` bumps it has not flushed yet; the
195
194
  // run then re-runs those tasks (upstream survey, nx#36688). So a prune
package/src/cli/last.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // Read-only — no config evaluation, no re-hash, no cache probe.
6
6
 
7
7
  import type { Database } from 'bun:sqlite'
8
- import { Cache, noteSchemaReset } from '../cache/index.js'
8
+ import { Cache } from '../cache/index.js'
9
9
  import { formatBytes } from './format.js'
10
10
  import { flagHint, seeHelp } from './help.js'
11
11
  import {
@@ -21,7 +21,7 @@ import {
21
21
  } from '../orchestrator/index.js'
22
22
  import { formatElapsed, UserError } from '../util/index.js'
23
23
  import { findWorkspaceRoot } from '../workspace/index.js'
24
- import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
24
+ import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
25
25
 
26
26
  interface LastArgs {
27
27
  runId?: string
@@ -232,7 +232,6 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
232
232
 
233
233
  const root = await findWorkspaceRoot(process.cwd())
234
234
  const cache = Cache.inspect(await cliCacheDir(root, parsed.cacheDir))
235
- noteSchemaReset(cache, warnToStderr)
236
235
  try {
237
236
  const db = cache.dbHandle()
238
237
  // Nothing recorded at all is its own answer: `--failed` said "no
package/src/cli/why.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // no config evaluation, no re-hash.
7
7
 
8
8
  import path from 'node:path'
9
- import { Cache, noteSchemaReset } from '../cache/index.js'
9
+ import { Cache } from '../cache/index.js'
10
10
  import { flagHint, seeHelp } from './help.js'
11
11
  import { splitTaskId } from '../graph/index.js'
12
12
  import {
@@ -18,7 +18,7 @@ import {
18
18
  } from '../orchestrator/index.js'
19
19
  import { MASKED, nearMatches, printable, UserError } from '../util/index.js'
20
20
  import { findWorkspaceRoot } from '../workspace/index.js'
21
- import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
21
+ import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
22
22
 
23
23
  interface WhyArgs {
24
24
  target?: string
@@ -185,7 +185,6 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
185
185
 
186
186
  const root = await findWorkspaceRoot(process.cwd())
187
187
  const cache = Cache.inspect(await cliCacheDir(root, parsed.cacheDir))
188
- noteSchemaReset(cache, warnToStderr)
189
188
  try {
190
189
  const db = cache.dbHandle()
191
190
  const taskId = resolveTarget(cache, await rootSpelled(root, parsed.target))
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import type { WorkspaceConfig } from '../config.js'
10
10
  import { UserError } from '../util/index.js'
11
- import { Cache, noteSchemaReset } from '../cache/index.js'
11
+ import { Cache } from '../cache/index.js'
12
12
  import { discoverProjects, loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
13
13
  import type { VxPlugin } from '../orchestrator/index.js'
14
14
  import {
@@ -147,7 +147,6 @@ export async function loadCliProjects(
147
147
  'open',
148
148
  opts.cacheDir === undefined ? ws.storeRoot : null,
149
149
  )
150
- if (cache !== null) noteSchemaReset(cache, warnToStderr)
151
150
  try {
152
151
  const loaded = await loadProjects({
153
152
  workspaceRoot,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import os from 'node:os'
9
9
  import path from 'node:path'
10
- import { Cache, CACHE_VERSION, noteSchemaReset, SCHEMA_VERSION } from '../cache/index.js'
10
+ import { Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
11
11
  import { PLUGIN_HOOKS } from '../config.js'
12
12
  import {
13
13
  cgroupCpuQuota,
@@ -154,7 +154,6 @@ async function collectWorkspaceInfo(
154
154
  const metas = await discoverProjects(await loadWorkspace(root, reads), plugins, cacheDir, warn)
155
155
  const storeRoot = opts.cacheDir === undefined ? resolveStoreRoot(root, workspaceConfig) : null
156
156
  const cache = Cache.inspect(cacheDir)
157
- noteSchemaReset(cache, warn)
158
157
  let stats
159
158
  let orphans
160
159
  let flaky: FlakyTask[]
@@ -12,7 +12,6 @@ import type { WorkspaceConfig } from '../config.js'
12
12
  import { beginRun, mark, nearest, UserError } from '../util/index.js'
13
13
  import {
14
14
  Cache,
15
- noteSchemaReset,
16
15
  type CacheLayer,
17
16
  type CachePolicy,
18
17
  FULL_CACHE_POLICY,
@@ -287,7 +286,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
287
286
  storeRoot,
288
287
  )
289
288
  localCache.assertWritable()
290
- noteSchemaReset(localCache, (m) => log.status(m))
291
289
  // Two digests from one read: the config-evaluation cache keys on every
292
290
  // file (a config may import a dependency), the task keys on the files no
293
291
  // plugin claims (`VxPlugin.fingerprint`).
@@ -329,6 +329,23 @@ export interface WorkspaceIdentity {
329
329
  id: string
330
330
  /** Human name for switchers/badges: the repo (or root dir) basename. */
331
331
  name: string
332
+ /** The origin remote, normalized (`github.com/org/repo`); absent without one. */
333
+ repository?: string
334
+ /** The root inside its git work tree, POSIX, `.` at the top; absent outside git. */
335
+ path?: string
336
+ }
337
+
338
+ /** Where `root` sits in the work tree whose `.git` is nearest above it. */
339
+ function workTreePath(root: string): string | undefined {
340
+ const abs = path.resolve(root)
341
+ for (let dir = abs; ;) {
342
+ if (fs.existsSync(path.join(dir, '.git'))) {
343
+ return path.relative(dir, abs).split(path.sep).join('/') || '.'
344
+ }
345
+ const up = path.dirname(dir)
346
+ if (up === dir) return undefined
347
+ dir = up
348
+ }
332
349
  }
333
350
 
334
351
  /**
@@ -375,6 +392,8 @@ export function captureWorkspaceIdentity(
375
392
  originUrl?: string | null,
376
393
  ): WorkspaceIdentity {
377
394
  const base = workspaceRoot.replace(/\/+$/, '').split('/').pop() || 'workspace'
395
+ const at = workTreePath(workspaceRoot)
396
+ const where = at === undefined ? {} : { path: at }
378
397
  try {
379
398
  let url = originUrl ?? ''
380
399
  if (originUrl === undefined) {
@@ -393,7 +412,7 @@ export function captureWorkspaceIdentity(
393
412
  if (url.length > 0) {
394
413
  const normalized = normalizeRemoteUrl(url)
395
414
  const name = normalized.split('/').pop() || base
396
- return { id: xxh3hex(normalized), name }
415
+ return { id: xxh3hex(normalized), name, repository: normalized, ...where }
397
416
  }
398
417
  } catch {
399
418
  // git unavailable — fall through to the salt.
@@ -409,8 +428,8 @@ export function captureWorkspaceIdentity(
409
428
  fs.mkdirSync(path.dirname(saltPath), { recursive: true })
410
429
  fs.writeFileSync(saltPath, salt + '\n')
411
430
  }
412
- return { id: xxh3hex(salt), name: base }
431
+ return { id: xxh3hex(salt), name: base, ...where }
413
432
  } catch {
414
- return { id: xxh3hex(workspaceRoot), name: base }
433
+ return { id: xxh3hex(workspaceRoot), name: base, ...where }
415
434
  }
416
435
  }
@@ -752,6 +752,8 @@ async function runOnBus(
752
752
  arch: hostContext.arch,
753
753
  workspaceId: wsIdentity.id,
754
754
  workspaceName: wsIdentity.name,
755
+ ...(wsIdentity.repository !== undefined ? { repository: wsIdentity.repository } : {}),
756
+ ...(wsIdentity.path !== undefined ? { workspacePath: wsIdentity.path } : {}),
755
757
  tags: maskTags(options.tags ?? {}),
756
758
  }
757
759
  telemetry = await subscribeTelemetry(
@@ -123,6 +123,10 @@ export interface RunContextRecord {
123
123
  */
124
124
  workspaceId: string
125
125
  workspaceName: string
126
+ /** The origin remote, normalized (`github.com/org/repo`); absent without one. Additive. */
127
+ repository?: string
128
+ /** The workspace root relative to its git work tree, `.` at the top; absent outside git. Additive. */
129
+ workspacePath?: string
126
130
  // git / CI / host — straight from run-context.ts.
127
131
  commitSha: string | null
128
132
  branch: string | null
@@ -289,7 +289,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
289
289
  files.push({
290
290
  relPath: relPosix(root, abs),
291
291
  abs,
292
- contents: renderConfigFile(source, p, verb, format),
292
+ contents: renderConfigFile(p, format),
293
293
  })
294
294
  }
295
295
  for (const f of plan.extraFiles) {
@@ -302,7 +302,8 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
302
302
  const hasWorkspaceFile = (
303
303
  await Promise.all(WORKSPACE_CONFIG_FILENAMES.map((n) => Bun.file(path.join(root, n)).exists()))
304
304
  ).some(Boolean)
305
- if (!hasWorkspaceFile) {
305
+ // A plan may bring its own (vx-migrate declares the plugins it detects).
306
+ if (!hasWorkspaceFile && !plan.extraFiles.some((f) => f.relPath === workspaceName)) {
306
307
  const abs = path.join(root, workspaceName)
307
308
  files.push({ relPath: relPosix(root, abs), abs, contents: workspaceFile(format) })
308
309
  }
@@ -550,11 +551,8 @@ function exampleConfig(format: MigrationFormat): string {
550
551
  // `import { defineWorkspace } from '@vzn/vx'` loads a SECOND copy of core
551
552
  // into every run — measured 2026-09-09 at ~17 ms on a two-package
552
553
  // workspace, a fifth of the whole run — for an identity function.
553
- const WORKSPACE_BODY = `// Plugins are consulted in this order; running here and caching in
554
- // .vx/cache are the floor under all of them, so an empty list is a
555
- // complete workspace. Add a remote cache, a remote executor or telemetry
556
- // as one entry each — those imports are the runtime ones.
557
- export default { plugins: [] }`
554
+ // No explanatory comment: the file is the user's (owner, 2026-10-06).
555
+ const WORKSPACE_BODY = `export default { plugins: [] }`
558
556
 
559
557
  function workspaceFile(format: MigrationFormat): string {
560
558
  return format === 'ts'
@@ -586,22 +584,18 @@ function renderValue(v: unknown, indent: string): string {
586
584
  return `{\n${body.join('\n')}\n${indent}}`
587
585
  }
588
586
 
589
- function renderConfigFile(
590
- source: string,
591
- p: GeneratedProject,
592
- verb: string,
593
- format: MigrationFormat,
594
- ): string {
587
+ function renderConfigFile(p: GeneratedProject, format: MigrationFormat): string {
595
588
  // A type-only import: the editor type-checks against the installed
596
589
  // package, and Bun erases it, so the config loads in a workspace that
597
590
  // runs the vx binary without the package installed. `mjs` is the same
598
591
  // object with nothing to erase.
599
- const lines: string[] = [
600
- `// Generated by \`${verb}\` from ${source}.${p.tasks.some((t) => t.todos.length > 0) ? ' Review the TODO(vx-migrate) comments.' : ''}`,
601
- ]
592
+ // No "generated by" header: the file is the user's from here (owner,
593
+ // 2026-10-06), and each TODO(vx-migrate) sits on its own task.
594
+ const lines: string[] = []
602
595
  if (format === 'ts') lines.push("import type { ProjectConfig } from '@vzn/vx/config'")
603
596
  if (p.importLines.length > 0) lines.push(...p.importLines)
604
- lines.push('', 'export default {')
597
+ if (lines.length > 0) lines.push('')
598
+ lines.push('export default {')
605
599
  if (p.tags !== undefined && p.tags.length > 0) lines.push(` tags: ${renderValue(p.tags, ' ')},`)
606
600
  lines.push(' tasks: {')
607
601
  for (const t of p.tasks) {