@vzn/vx 0.0.646 → 0.0.648
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/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What a project is (
|
|
1
|
+
// What a project is (2026-10-14): a directory holding a
|
|
2
2
|
// `vx.config.*`. `vx lock` finds them (`findProjectConfigs`) and records
|
|
3
3
|
// each by directory; a run takes them from the lock (`lockedProjects`) and
|
|
4
4
|
// never walks. A project is named by `name` in its config, else by a
|
|
@@ -13,13 +13,17 @@ import {
|
|
|
13
13
|
executablePath,
|
|
14
14
|
gitEnv,
|
|
15
15
|
gitSpawnRefusal,
|
|
16
|
+
hasControl,
|
|
16
17
|
isExecutableMissing,
|
|
17
18
|
isOutOfFds,
|
|
19
|
+
printable,
|
|
18
20
|
refusal,
|
|
19
21
|
relPosix,
|
|
20
22
|
UserError,
|
|
21
23
|
} from '../util/index.js'
|
|
22
|
-
import { gitTop, workspaceFile, workspaceRootOf } from '../handoff.js'
|
|
24
|
+
import { gitTop, WORKSPACE_CONFIG_FILENAMES, workspaceFile, workspaceRootOf } from '../handoff.js'
|
|
25
|
+
import { canonicalDir } from './canonical-dir.js'
|
|
26
|
+
import { parseFilter } from './filter.js'
|
|
23
27
|
import { repoIdOf } from './repo-id.js'
|
|
24
28
|
|
|
25
29
|
export interface ProjectMeta {
|
|
@@ -57,7 +61,7 @@ export const PROJECT_CONFIG_FILENAMES = [
|
|
|
57
61
|
* The workspace root for `start`: the directory holding the first
|
|
58
62
|
* `.vx/workspace.*` from `start` up to the git top-level, or up to `/`
|
|
59
63
|
* outside git ({@link workspaceFile}, shared with the hand-off). Nothing
|
|
60
|
-
* else decides it (
|
|
64
|
+
* else decides it (2026-10-13: "Workspace file is the ultimate"):
|
|
61
65
|
* not a `package.json`, not `pnpm-workspace.yaml`, not a lockfile. Throws
|
|
62
66
|
* `VX_E_NO_WORKSPACE` when there is none.
|
|
63
67
|
*
|
|
@@ -138,7 +142,7 @@ export function resolveCacheDir(root: string, config: WorkspaceConfig | null): s
|
|
|
138
142
|
|
|
139
143
|
/**
|
|
140
144
|
* Where this repository's shared store lives: `~/.vx/<repo id>/cache`, as
|
|
141
|
-
* Nx 23 keeps `~/.nx/<id>/cache` (
|
|
145
|
+
* Nx 23 keeps `~/.nx/<id>/cache` (2026-10-06), so every checkout of
|
|
142
146
|
* the repository hits what another saved and no other repository shares
|
|
143
147
|
* it (`repoIdOf`). Null when the workspace names its cache (`cache.dir`,
|
|
144
148
|
* `VX_CACHE_DIR`): that directory then holds everything, as one
|
|
@@ -192,7 +196,11 @@ export function linkDiscoveredEdges(
|
|
|
192
196
|
// one that does not), so a target naming one needs no other check:
|
|
193
197
|
// a plugin hands project dirs as they are, absolute, and resolving
|
|
194
198
|
// and judging each of 5,000 edges was ~15 ms at 1,000 projects.
|
|
195
|
-
|
|
199
|
+
let name = byDir.get(target) ?? byDir.get(path.resolve(root, target))
|
|
200
|
+
if (name === undefined) {
|
|
201
|
+
const real = canonicalDir(root, path.resolve(root, target))
|
|
202
|
+
if (real !== null) name = byDir.get(real)
|
|
203
|
+
}
|
|
196
204
|
if (name === undefined) {
|
|
197
205
|
const rel = relPosix(root, path.resolve(root, target))
|
|
198
206
|
const where = `plugin '${edge.by}' gave project "${edge.meta.name}" an edge to ${target}`
|
|
@@ -220,6 +228,7 @@ export function pathName(root: string, dir: string): string {
|
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
const CONFIG_RANK = new Map(PROJECT_CONFIG_FILENAMES.map((n, i) => [n, i]))
|
|
231
|
+
const WORKSPACE_RANK = new Map(WORKSPACE_CONFIG_FILENAMES.map((n, i) => [n, i]))
|
|
223
232
|
|
|
224
233
|
/** A directory holding a vx config, and the config file precedence picked there. */
|
|
225
234
|
export interface ConfigDir {
|
|
@@ -228,51 +237,132 @@ export interface ConfigDir {
|
|
|
228
237
|
readonly configPath: string
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
/** `vx lock`'s review copy of a project's resolved config, beside its `vx.config`. */
|
|
241
|
+
export const REVIEW_FILE = 'vx.lock.json'
|
|
242
|
+
|
|
243
|
+
export interface ProjectConfigs {
|
|
244
|
+
readonly configs: ConfigDir[]
|
|
245
|
+
/** Absolute: review copies in a directory with no config, which no `vx lock` rewrites. */
|
|
246
|
+
readonly strayReviews: string[]
|
|
247
|
+
/** Absolute: config files a higher-precedence one beside them hides, the root's workspace config among them, by the one read. */
|
|
248
|
+
readonly shadowed: { file: string; read: string }[]
|
|
249
|
+
}
|
|
250
|
+
|
|
231
251
|
/**
|
|
232
252
|
* Every project directory under `root`, for `vx lock` (a run reads the
|
|
233
253
|
* lock and never walks). In git: `git ls-files` over the config names,
|
|
234
|
-
* tracked and untracked, git's ignore rules applied,
|
|
235
|
-
* repository's own listing. Outside git: a walk
|
|
236
|
-
* that skips
|
|
254
|
+
* tracked and untracked, git's ignore rules applied, none under
|
|
255
|
+
* `node_modules`, and each nested repository's own listing. Outside git: a walk
|
|
256
|
+
* that skips `node_modules` and vx's own `.vx`, and enters every other
|
|
257
|
+
* dot-directory, as git lists `.github/actions/x`. A config at or under a
|
|
237
258
|
* nested `.vx/workspace.*` belongs to that workspace and is left out.
|
|
238
259
|
* Where a directory holds several, `PROJECT_CONFIG_FILENAMES` order picks
|
|
239
260
|
* the first that is a file.
|
|
240
261
|
*/
|
|
241
|
-
export async function findProjectConfigs(root: string): Promise<
|
|
242
|
-
const
|
|
262
|
+
export async function findProjectConfigs(root: string): Promise<ProjectConfigs> {
|
|
263
|
+
const inGit = (await gitTop(root)) !== null
|
|
264
|
+
const rels = inGit ? await gitConfigs(root) : await scanConfigs(root)
|
|
243
265
|
const nested: string[] = []
|
|
244
266
|
for (const rel of rels) {
|
|
245
|
-
const at = rel.lastIndexOf('
|
|
246
|
-
if (at > 0 && rel
|
|
247
|
-
nested.push(rel.slice(0, at - 1))
|
|
267
|
+
const at = rel.lastIndexOf('/.vx/')
|
|
268
|
+
if (at > 0 && WORKSPACE_RANK.has(rel.slice(at + 1))) nested.push(rel.slice(0, at))
|
|
248
269
|
}
|
|
270
|
+
if (inGit) nested.push(...(await unlistedNested(root, rels, nested)))
|
|
249
271
|
const byDir = new Map<string, string[]>()
|
|
272
|
+
const reviewed: string[] = []
|
|
250
273
|
for (const rel of rels) {
|
|
251
274
|
const slash = rel.lastIndexOf('/')
|
|
252
275
|
const base = rel.slice(slash + 1)
|
|
253
|
-
if (!CONFIG_RANK.has(base)) continue
|
|
254
276
|
const dir = slash === -1 ? '' : rel.slice(0, slash)
|
|
255
277
|
if (nested.some((n) => dir === n || dir.startsWith(`${n}/`))) continue
|
|
278
|
+
if (base === REVIEW_FILE) reviewed.push(dir)
|
|
279
|
+
if (!CONFIG_RANK.has(base)) continue
|
|
256
280
|
const names = byDir.get(dir)
|
|
257
281
|
if (names === undefined) byDir.set(dir, [base])
|
|
258
282
|
else names.push(base)
|
|
259
283
|
}
|
|
284
|
+
const shadowed: { file: string; read: string }[] = []
|
|
285
|
+
// The files among `names` (ranked), the first of them the one read: git
|
|
286
|
+
// lists a tracked config deleted from the worktree, and a directory may
|
|
287
|
+
// carry a config's name. Every other file is shadowed, said by `vx lock`.
|
|
288
|
+
const firstFile = async (
|
|
289
|
+
dir: string,
|
|
290
|
+
names: string[],
|
|
291
|
+
rank: ReadonlyMap<string, number>,
|
|
292
|
+
): Promise<string | null> => {
|
|
293
|
+
names.sort((a, b) => rank.get(a)! - rank.get(b)!)
|
|
294
|
+
const files = (
|
|
295
|
+
await Promise.all(names.map(async (n) => ((await isFile(path.join(dir, n))) ? n : null)))
|
|
296
|
+
).filter((n) => n !== null)
|
|
297
|
+
for (const n of files.slice(1))
|
|
298
|
+
shadowed.push({ file: path.join(dir, n), read: path.join(dir, files[0]!) })
|
|
299
|
+
return files[0] === undefined ? null : path.join(dir, files[0])
|
|
300
|
+
}
|
|
260
301
|
const found = await Promise.all(
|
|
261
302
|
[...byDir].map(async ([rel, names]) => {
|
|
262
303
|
const dir = rel === '' ? root : path.join(root, rel)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// and a directory may carry a config's name.
|
|
266
|
-
for (const name of names) {
|
|
267
|
-
const configPath = path.join(dir, name)
|
|
268
|
-
if (await isFile(configPath)) return { dir, configPath }
|
|
269
|
-
}
|
|
270
|
-
return null
|
|
304
|
+
const configPath = await firstFile(dir, names, CONFIG_RANK)
|
|
305
|
+
return configPath === null ? null : { dir, configPath }
|
|
271
306
|
}),
|
|
272
307
|
)
|
|
273
|
-
|
|
308
|
+
// The root's own, asked by name: a walk outside git lists no dot-directory.
|
|
309
|
+
await firstFile(root, [...WORKSPACE_CONFIG_FILENAMES], WORKSPACE_RANK)
|
|
310
|
+
shadowed.sort((a, b) => (a.file < b.file ? -1 : 1))
|
|
311
|
+
const configs = found
|
|
274
312
|
.filter((f): f is ConfigDir => f !== null)
|
|
275
313
|
.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0))
|
|
314
|
+
const configured = new Set(configs.map((c) => c.dir))
|
|
315
|
+
// On disk only: git lists a tracked copy deleted with its project.
|
|
316
|
+
const strayReviews = (
|
|
317
|
+
await Promise.all(
|
|
318
|
+
reviewed
|
|
319
|
+
.map((rel) => (rel === '' ? root : path.join(root, rel)))
|
|
320
|
+
.filter((dir) => !configured.has(dir))
|
|
321
|
+
.map(async (dir) => {
|
|
322
|
+
const file = path.join(dir, REVIEW_FILE)
|
|
323
|
+
return (await isFile(file)) ? file : null
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
.filter((f) => f !== null)
|
|
328
|
+
.sort()
|
|
329
|
+
return { configs, strayReviews, shadowed }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The nested workspace roots git did not list (an ignored `.vx/`) among the
|
|
334
|
+
* directories holding `rels` and their ancestors under `root`: a run from
|
|
335
|
+
* under one takes it as its root, so the outer lock must not claim it.
|
|
336
|
+
*/
|
|
337
|
+
async function unlistedNested(
|
|
338
|
+
root: string,
|
|
339
|
+
rels: readonly string[],
|
|
340
|
+
listed: readonly string[],
|
|
341
|
+
): Promise<string[]> {
|
|
342
|
+
const dirs = new Set<string>()
|
|
343
|
+
for (const rel of rels) {
|
|
344
|
+
for (let d = rel.slice(0, Math.max(rel.lastIndexOf('/'), 0)); d !== '';) {
|
|
345
|
+
if (dirs.has(d)) break
|
|
346
|
+
dirs.add(d)
|
|
347
|
+
d = d.slice(0, Math.max(d.lastIndexOf('/'), 0))
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const n of listed) dirs.delete(n)
|
|
351
|
+
// A stat, no descriptor: most directories have no `.vx`.
|
|
352
|
+
const found = await Promise.all(
|
|
353
|
+
[...dirs].map(async (d) =>
|
|
354
|
+
(await stat(path.join(root, d, '.vx')).then(
|
|
355
|
+
(st) => st.isDirectory(),
|
|
356
|
+
(err: unknown) => {
|
|
357
|
+
if (isOutOfFds(err)) throw err
|
|
358
|
+
return false
|
|
359
|
+
},
|
|
360
|
+
)) && (await workspaceFileIn(root, d))
|
|
361
|
+
? d
|
|
362
|
+
: null,
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
return found.filter((d) => d !== null)
|
|
276
366
|
}
|
|
277
367
|
|
|
278
368
|
async function isFile(p: string): Promise<boolean> {
|
|
@@ -302,10 +392,12 @@ async function gitConfigs(root: string): Promise<string[]> {
|
|
|
302
392
|
'--',
|
|
303
393
|
':(glob)**/vx.config.*',
|
|
304
394
|
':(glob)**/.vx/workspace.*',
|
|
395
|
+
`:(glob)**/${REVIEW_FILE}`,
|
|
396
|
+
NOT_INSTALLS,
|
|
305
397
|
]),
|
|
306
|
-
gitZ(root, ['ls-files', '-z', '--stage']),
|
|
398
|
+
gitZ(root, ['ls-files', '-z', '--stage', '--', NOT_INSTALLS]),
|
|
307
399
|
// An embedded repository is the one untracked entry git lists as `dir/`.
|
|
308
|
-
gitZ(root, ['ls-files', '-z', '--others', '--exclude-standard']),
|
|
400
|
+
gitZ(root, ['ls-files', '-z', '--others', '--exclude-standard', '--', NOT_INSTALLS]),
|
|
309
401
|
])
|
|
310
402
|
// A submodule's `.git` is a file, an embedded repository's a directory.
|
|
311
403
|
const isRepo = (rel: string): Promise<boolean> =>
|
|
@@ -329,6 +421,13 @@ async function gitConfigs(root: string): Promise<string[]> {
|
|
|
329
421
|
return [...configs, ...prefixed]
|
|
330
422
|
}
|
|
331
423
|
|
|
424
|
+
/**
|
|
425
|
+
* An install is no project, tracked or not, as the walk outside git skips
|
|
426
|
+
* it: a repo whose .gitignore does not name `node_modules` listed each
|
|
427
|
+
* installed package's vx.config as one.
|
|
428
|
+
*/
|
|
429
|
+
const NOT_INSTALLS = ':(exclude,glob)**/node_modules/**'
|
|
430
|
+
|
|
332
431
|
/** `git <args>`'s NUL-separated output at `cwd`. */
|
|
333
432
|
async function gitZ(cwd: string, args: string[]): Promise<string[]> {
|
|
334
433
|
let proc: Bun.Subprocess<'ignore', 'pipe', 'pipe'>
|
|
@@ -358,7 +457,7 @@ async function gitZ(cwd: string, args: string[]): Promise<string[]> {
|
|
|
358
457
|
.filter((p) => p !== '')
|
|
359
458
|
}
|
|
360
459
|
|
|
361
|
-
/** The config paths under `root` outside git, root-relative:
|
|
460
|
+
/** The config paths under `root` outside git, root-relative: none under `node_modules`, `.git` or `.vx`. */
|
|
362
461
|
async function scanConfigs(root: string): Promise<string[]> {
|
|
363
462
|
const out: string[] = []
|
|
364
463
|
const walk = async (rel: string): Promise<void> => {
|
|
@@ -375,9 +474,8 @@ async function scanConfigs(root: string): Promise<string[]> {
|
|
|
375
474
|
const below: string[] = []
|
|
376
475
|
for (const e of entries) {
|
|
377
476
|
const child = rel === '' ? e.name : `${rel}/${e.name}`
|
|
378
|
-
if (CONFIG_RANK.has(e.name)) out.push(child)
|
|
379
|
-
else if (e.isDirectory() && !e.name
|
|
380
|
-
below.push(child)
|
|
477
|
+
if (CONFIG_RANK.has(e.name) || e.name === REVIEW_FILE) out.push(child)
|
|
478
|
+
else if (e.isDirectory() && !SCAN_SKIPS.has(e.name)) below.push(child)
|
|
381
479
|
}
|
|
382
480
|
await Promise.all(below.map(walk))
|
|
383
481
|
}
|
|
@@ -385,9 +483,12 @@ async function scanConfigs(root: string): Promise<string[]> {
|
|
|
385
483
|
return out
|
|
386
484
|
}
|
|
387
485
|
|
|
486
|
+
// `.vx` holds vx's state (the cache), which a checkout ignores.
|
|
487
|
+
const SCAN_SKIPS = new Set(['node_modules', '.git', '.vx'])
|
|
488
|
+
|
|
388
489
|
async function workspaceFileIn(root: string, rel: string): Promise<boolean> {
|
|
389
490
|
const names = await readdir(path.join(root, rel, '.vx')).catch(() => [] as string[])
|
|
390
|
-
return names.some((n) =>
|
|
491
|
+
return names.some((n) => WORKSPACE_RANK.has(`.vx/${n}`))
|
|
391
492
|
}
|
|
392
493
|
|
|
393
494
|
/** How each project got its name, when not by its path: `config`, or the plugin's name. */
|
|
@@ -455,14 +556,24 @@ export async function discoveredProject(
|
|
|
455
556
|
const bad = nameFault(name)
|
|
456
557
|
if (bad !== null) throw new UserError(`${where} ${JSON.stringify(name)}: ${bad}`)
|
|
457
558
|
}
|
|
458
|
-
|
|
559
|
+
let dir = path.resolve(root, entry.dir)
|
|
459
560
|
const rel = relPosix(root, dir)
|
|
460
561
|
const named = name === undefined ? '' : ` "${name}"`
|
|
461
562
|
if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) {
|
|
462
563
|
throw new UserError(`${where}${named} at ${dir}: outside the workspace root`)
|
|
463
564
|
}
|
|
464
565
|
const shown = rel === '' ? '.' : rel
|
|
465
|
-
|
|
566
|
+
// Refused once the stage ended too, but naming no plugin and saying to set a `name`.
|
|
567
|
+
if (name === ROOT_NAME && rel !== '') {
|
|
568
|
+
throw new UserError(`${where} "//" at ${shown}: "//" names the workspace root`)
|
|
569
|
+
}
|
|
570
|
+
let meta = known.get(dir)
|
|
571
|
+
if (meta === undefined) {
|
|
572
|
+
// Only a directory no project holds is resolved: a locked one is
|
|
573
|
+
// already real, and a realpath per entry is a syscall per project.
|
|
574
|
+
dir = canonicalDir(root, dir) ?? dir
|
|
575
|
+
meta = known.get(dir)
|
|
576
|
+
}
|
|
466
577
|
if (meta !== undefined) {
|
|
467
578
|
const from = namedBy.get(meta)
|
|
468
579
|
if (name === undefined || name === meta.name || from === 'config') return { meta, added: false }
|
|
@@ -493,6 +604,38 @@ function nameFault(name: string): string | null {
|
|
|
493
604
|
if (name.trim() !== name) return 'the name has surrounding whitespace'
|
|
494
605
|
// A task is `<project>#<task>` split at the first `#`.
|
|
495
606
|
if (name.includes('#')) return 'the name holds "#" — vx addresses a task as <name>#<task>'
|
|
607
|
+
// Printed raw by the status line and summary: `ESC ]0;` retitled the terminal.
|
|
608
|
+
if (hasControl(name)) return 'the name holds a control character'
|
|
609
|
+
const why = projectNameProblem(name)
|
|
610
|
+
return why === null ? null : `the name ${why}`
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Why `dependsOn` or `--filter` cannot name a project called `name`, or
|
|
615
|
+
* null; surrounding whitespace and `#` are each caller's own words. Each
|
|
616
|
+
* shape loaded and could not be addressed: `^ui#build` was refused as `^`
|
|
617
|
+
* with `pkg#task`, `--filter '!ui'` excluded it, `tag:ui` and `ui...` read
|
|
618
|
+
* as a tag and a walk. Asked of the filter parser itself, as `tagProblem`.
|
|
619
|
+
*/
|
|
620
|
+
export function projectNameProblem(name: string): string | null {
|
|
621
|
+
if (name.includes('*')) return "holds '*', which makes it a pattern"
|
|
622
|
+
if (name.startsWith('^')) return "starts with '^', which names dependencies' tasks"
|
|
623
|
+
if (name.startsWith('!')) return "starts with '!', which a filter reads as an exclusion"
|
|
624
|
+
// The root's own name, which the filter reads as the root's directory.
|
|
625
|
+
if (name === ROOT_NAME) return null
|
|
626
|
+
const walk = "starts or ends with '...', which a filter reads as a dependency walk"
|
|
627
|
+
let f: ReturnType<typeof parseFilter>
|
|
628
|
+
try {
|
|
629
|
+
f = parseFilter(name, '/')
|
|
630
|
+
} catch {
|
|
631
|
+
// `...` alone: the walk with no project between.
|
|
632
|
+
return walk
|
|
633
|
+
}
|
|
634
|
+
if (f.withDeps || f.withDependents || f.onlyDeps || f.onlyDependents) return walk
|
|
635
|
+
if (f.gitSince !== undefined)
|
|
636
|
+
return `ends in "[${f.gitSince}]", which a filter reads as a git range`
|
|
637
|
+
if (f.tag === true) return "starts with 'tag:', which a filter reads as a tag selector"
|
|
638
|
+
if (f.isPath) return 'reads as a directory to a filter'
|
|
496
639
|
return null
|
|
497
640
|
}
|
|
498
641
|
|
|
@@ -513,7 +656,7 @@ export function checkProjectNames(root: string, metas: ProjectMeta[]): ProjectMe
|
|
|
513
656
|
const fault = namedBy.has(meta) ? null : nameFault(meta.name)
|
|
514
657
|
if (fault !== null) {
|
|
515
658
|
throw new UserError(
|
|
516
|
-
`${rel}: a project is named by its path unless named, and ${fault.replace('the name', 'this one')} — set \`name\` in its vx.config`,
|
|
659
|
+
`${printable(rel)}: a project is named by its path unless named, and ${fault.replace('the name', 'this one')} — set \`name\` in its vx.config`,
|
|
517
660
|
)
|
|
518
661
|
}
|
|
519
662
|
const next = metas[i + 1]
|