@vzn/vx 0.0.109 → 0.0.111
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/cache/archive.ts +22 -7
- package/src/cache/cache.ts +2 -2
- package/src/cli/plugin-commands.ts +1 -3
- package/src/orchestrator/run-lock.ts +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.111",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
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.111",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.111",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.111",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.111"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/archive.ts
CHANGED
|
@@ -49,6 +49,10 @@ const META_ENTRY = '.vx-meta.json'
|
|
|
49
49
|
/** Archive entry name of the always-present stdout record. */
|
|
50
50
|
const STDOUT_ENTRY = 'stdout'
|
|
51
51
|
|
|
52
|
+
const WIN32 = process.platform === 'win32'
|
|
53
|
+
// A Windows link target may be spelled with either separator.
|
|
54
|
+
const LINK_SEP = WIN32 ? /[\\/]/ : '/'
|
|
55
|
+
|
|
52
56
|
/**
|
|
53
57
|
* Sidecar shape. `files` maps an entry name to `[mode, mtimeMs]` — a
|
|
54
58
|
* pair, not an object, because the sidecar is real entropy inside an
|
|
@@ -157,12 +161,15 @@ export async function planArtifact(args: PackArgs): Promise<ArtifactPlan> {
|
|
|
157
161
|
`output ${shown} is not a regular file (a symlink to a directory?): vx stores regular files only — emit a file there, or narrow cache.outputs.files to the files the task produces (output globs take no '!')`,
|
|
158
162
|
)
|
|
159
163
|
}
|
|
160
|
-
|
|
164
|
+
// Windows reports 0o666 (0o444 read-only) and never an exec bit: a
|
|
165
|
+
// Linux restore of that would be world-writable.
|
|
166
|
+
const mode = WIN32 ? 0o644 : st.mode & 0o777
|
|
167
|
+
meta.files[name] = [mode, Math.floor(st.mtimeMs)]
|
|
161
168
|
return {
|
|
162
169
|
name,
|
|
163
170
|
abs,
|
|
164
171
|
size: st.size,
|
|
165
|
-
mode
|
|
172
|
+
mode,
|
|
166
173
|
// ustar's octal field holds no sign: an mtime before 1970 made the
|
|
167
174
|
// header unreadable and every save of it a "corrupt artifact". The
|
|
168
175
|
// sidecar above carries the real value.
|
|
@@ -640,7 +647,9 @@ class Extractor {
|
|
|
640
647
|
let n = 0
|
|
641
648
|
for (const s of this.staged) {
|
|
642
649
|
const [mode, mtimeMs] = metaFor(s.name)
|
|
643
|
-
|
|
650
|
+
// Windows has no mode bits: chmod there only sets the read-only
|
|
651
|
+
// attribute, and a read-only restore refuses the next rename over it.
|
|
652
|
+
if ((mode & 0o777) !== createdMode && !WIN32) chmodSync(s.tmp, mode & 0o777)
|
|
644
653
|
if (mtimeMs !== undefined) {
|
|
645
654
|
// A Date, not seconds: Bun reads a negative number of seconds as
|
|
646
655
|
// "now", and a Date before 1970 as itself (Bun 1.4.2).
|
|
@@ -725,8 +734,10 @@ async function linkOutError(base: string, probe: string, realBase: string): Prom
|
|
|
725
734
|
* A cycle is the user's tree, not the artifact, so it is refused by name.
|
|
726
735
|
*/
|
|
727
736
|
async function resolveThrough(p: string): Promise<string> {
|
|
728
|
-
const
|
|
729
|
-
|
|
737
|
+
const abs = path.resolve(p)
|
|
738
|
+
// The root is `/` or a drive (`C:\`): joining a drive onto `\` loses it.
|
|
739
|
+
let at = path.parse(abs).root
|
|
740
|
+
const pending = abs.slice(at.length).split(path.sep).filter(Boolean).reverse()
|
|
730
741
|
let hops = 0
|
|
731
742
|
while (pending.length > 0) {
|
|
732
743
|
const next = path.join(at, pending.pop()!)
|
|
@@ -747,8 +758,12 @@ async function resolveThrough(p: string): Promise<string> {
|
|
|
747
758
|
'write through it. Remove the link and re-run.',
|
|
748
759
|
)
|
|
749
760
|
}
|
|
750
|
-
|
|
751
|
-
|
|
761
|
+
let rest = link
|
|
762
|
+
if (path.isAbsolute(link)) {
|
|
763
|
+
at = path.parse(link).root
|
|
764
|
+
rest = link.slice(at.length)
|
|
765
|
+
}
|
|
766
|
+
pending.push(...rest.split(LINK_SEP).filter(Boolean).reverse())
|
|
752
767
|
}
|
|
753
768
|
return at
|
|
754
769
|
}
|
package/src/cache/cache.ts
CHANGED
|
@@ -1211,12 +1211,12 @@ export class Cache implements CacheLayer {
|
|
|
1211
1211
|
}): Map<string, string> {
|
|
1212
1212
|
const outputs = new Map<string, string>()
|
|
1213
1213
|
for (const f of args.outputFiles) {
|
|
1214
|
-
outputs.set(`outputs/${
|
|
1214
|
+
outputs.set(`outputs/${relPosix(args.projectDir, f)}`, f)
|
|
1215
1215
|
}
|
|
1216
1216
|
// Caller passes workspaceRoot whenever workspaceOutputFiles is
|
|
1217
1217
|
// non-empty; the rels are root-anchored by construction.
|
|
1218
1218
|
for (const f of args.workspaceOutputFiles ?? []) {
|
|
1219
|
-
outputs.set(`${WORKSPACE_OUTPUT_PREFIX}${
|
|
1219
|
+
outputs.set(`${WORKSPACE_OUTPUT_PREFIX}${relPosix(args.workspaceRoot!, f)}`, f)
|
|
1220
1220
|
}
|
|
1221
1221
|
return outputs
|
|
1222
1222
|
}
|
|
@@ -113,12 +113,10 @@ export async function pluginVerbs(cwd = process.cwd()): Promise<string[]> {
|
|
|
113
113
|
export async function pluginCommandHelp(cwd = process.cwd()): Promise<string[]> {
|
|
114
114
|
const ws = await workspacePlugins(cwd).catch(() => null)
|
|
115
115
|
if (ws === null || 'loadError' in ws) return []
|
|
116
|
+
// A verb has one owner: the workspace load refuses two plugins declaring it.
|
|
116
117
|
const lines: string[] = []
|
|
117
|
-
const seen = new Set<string>()
|
|
118
118
|
for (const plugin of ws.plugins) {
|
|
119
119
|
for (const [verb, command] of Object.entries(plugin.commands ?? {})) {
|
|
120
|
-
if (seen.has(verb)) continue
|
|
121
|
-
seen.add(verb)
|
|
122
120
|
lines.push(` vx ${verb.padEnd(17)} ${command.description} (${plugin.name})`)
|
|
123
121
|
}
|
|
124
122
|
}
|
|
@@ -240,7 +240,7 @@ async function place(lockDir: string): Promise<string | null> {
|
|
|
240
240
|
await mkdir(staging)
|
|
241
241
|
try {
|
|
242
242
|
await writeFile(path.join(staging, entry), '')
|
|
243
|
-
await rename(staging, lockDir)
|
|
243
|
+
await (WIN32 ? renameWin32(staging, lockDir) : rename(staging, lockDir))
|
|
244
244
|
return entry
|
|
245
245
|
} catch (err) {
|
|
246
246
|
await rm(staging, { recursive: true, force: true })
|
|
@@ -250,6 +250,36 @@ async function place(lockDir: string): Promise<string | null> {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
const WIN32 = process.platform === 'win32'
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Windows renames no directory onto another, even an empty one: EPERM
|
|
257
|
+
* where POSIX gives ENOTEMPTY, which read as a refusal and ran every
|
|
258
|
+
* contender unlocked. A name that exists is taken; an empty one (a
|
|
259
|
+
* holder's leave that lost its rmdir) is removed and the rename tried
|
|
260
|
+
* once more, as POSIX's rename onto it would replace it.
|
|
261
|
+
*/
|
|
262
|
+
async function renameWin32(staging: string, lockDir: string): Promise<void> {
|
|
263
|
+
try {
|
|
264
|
+
await rename(staging, lockDir)
|
|
265
|
+
return
|
|
266
|
+
} catch (err) {
|
|
267
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
268
|
+
if (code !== 'EPERM' && code !== 'EACCES') throw err
|
|
269
|
+
const empty = await rmdir(lockDir).then(
|
|
270
|
+
() => true,
|
|
271
|
+
() => false,
|
|
272
|
+
)
|
|
273
|
+
if (!empty) throw Object.assign(new Error('run lock taken'), { code: 'EEXIST' })
|
|
274
|
+
}
|
|
275
|
+
await rename(staging, lockDir).catch((err: NodeJS.ErrnoException) => {
|
|
276
|
+
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
277
|
+
throw Object.assign(new Error('run lock taken'), { code: 'EEXIST' })
|
|
278
|
+
}
|
|
279
|
+
throw err
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
253
283
|
/**
|
|
254
284
|
* Unlink one taking's entry, then the directory if that left it empty.
|
|
255
285
|
* Gone already is not an error (another waiter reclaimed it first); a
|