@vzn/vx 0.0.591 → 0.0.595
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/launcher.cjs +9 -2
- package/package.json +5 -5
- package/src/bin.ts +26 -11
- package/src/cache/archive.ts +18 -7
- package/src/cache/cache.ts +137 -101
- package/src/cache/config-evals.ts +19 -4
- package/src/cache/git-inputs.ts +33 -17
- package/src/cache/inputs.ts +157 -54
- package/src/cache/layered-cache.ts +25 -2
- package/src/cache/tar-stream.ts +4 -2
- package/src/cli/cache.ts +4 -16
- package/src/cli/completions.ts +42 -6
- package/src/cli/foreign-flags.ts +56 -6
- package/src/cli/help.ts +27 -8
- package/src/cli/info.ts +4 -6
- package/src/cli/init.ts +4 -4
- package/src/cli/last.ts +49 -20
- package/src/cli/lock.ts +3 -3
- package/src/cli/plan-format.ts +9 -0
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +80 -50
- package/src/cli/select.ts +44 -5
- package/src/cli/show.ts +47 -9
- package/src/cli/upgrade.ts +2 -3
- package/src/cli/watch-filter.ts +21 -7
- package/src/cli/watch-fs.ts +32 -8
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch.ts +40 -15
- package/src/cli/why.ts +4 -13
- package/src/cli/workspace-config.ts +1 -1
- package/src/config.ts +5 -5
- package/src/exec/env.ts +8 -5
- package/src/exec/index.ts +1 -0
- package/src/exec/proc-sample.ts +5 -1
- package/src/exec/runner.ts +126 -47
- package/src/exec/sandbox-deny-scan.ts +2 -2
- package/src/exec/sandbox-paths.ts +11 -5
- package/src/exec/sandbox-runtime.ts +89 -23
- package/src/exec/sandbox-violations.ts +23 -9
- package/src/graph/index.ts +1 -0
- package/src/graph/scheduler.ts +14 -4
- package/src/graph/task-graph.ts +3 -2
- package/src/index.ts +9 -0
- package/src/orchestrator/affected-tasks.ts +34 -10
- package/src/orchestrator/doctor.ts +2 -1
- package/src/orchestrator/execute-task.ts +43 -10
- package/src/orchestrator/hit-restore.ts +9 -5
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/local-shortcircuit.ts +4 -1
- package/src/orchestrator/lockfile-claim.ts +45 -49
- package/src/orchestrator/logger.ts +4 -1
- package/src/orchestrator/miss-save.ts +10 -3
- package/src/orchestrator/placement.ts +17 -3
- package/src/orchestrator/plan.ts +25 -1
- package/src/orchestrator/plugin-host.ts +41 -3
- package/src/orchestrator/plugin.ts +11 -14
- package/src/orchestrator/projects.ts +18 -9
- package/src/orchestrator/run-context.ts +2 -2
- package/src/orchestrator/run-lock.ts +14 -4
- package/src/orchestrator/run.ts +94 -16
- package/src/orchestrator/sandbox-request.ts +6 -10
- package/src/orchestrator/signals.ts +5 -2
- package/src/orchestrator/summary.ts +14 -1
- package/src/util/index.ts +7 -1
- package/src/util/secret-mask.ts +36 -0
- package/src/util/settle.ts +15 -0
- package/src/workspace/affected.ts +47 -19
- package/src/workspace/config-cache.ts +25 -4
- package/src/workspace/config-eval.ts +4 -0
- package/src/workspace/config-imports.ts +2 -2
- package/src/workspace/config-schema.ts +47 -10
- package/src/workspace/filter.ts +5 -4
- package/src/workspace/migrate-scripts.ts +23 -6
- package/src/workspace/package-graph.ts +15 -3
- package/src/workspace/project-loader.ts +89 -2
- package/src/workspace/workspace.ts +121 -14
|
@@ -20,8 +20,8 @@ export const PLUGIN_TEMPLATES: Readonly<Record<string, { plugin: string; test: s
|
|
|
20
20
|
},
|
|
21
21
|
executor: {
|
|
22
22
|
plugin:
|
|
23
|
-
"// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n // vx's own `sh`: a bare name is looked up on `req.env`'s PATH, which\n // leads with the project's node_modules/.bin, where a dependency's\n // `sh` would run every command.\n const sh = Bun.which('sh') ?? '/bin/sh'\n const child = Bun.spawn([sh, '-c',
|
|
24
|
-
test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n\nit(\"runs the command through this machine's sh, not a project's node_modules/.bin sh\", async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: { hello: { exec: { command: 'echo real' } } } }`,\n )\n await mkdir(path.join(dir, 'node_modules', '.bin'), { recursive: true })\n await writeFile(path.join(dir, 'node_modules', '.bin', 'sh'), '#!/bin/sh\\necho planted\\n', {\n mode: 0o755,\n })\n await workspace('shellExecutor()')\n const out: string[] = []\n const r = await run({\n cwd: root,\n tasks: ['hello'],\n log: { ...log(), taskStdout: (_n, chunk) => void out.push(chunk) },\n handleSignals: false,\n })\n expect([r.outcomes.map((o) => o.status), out.join('')]).toEqual([['success'], 'real\\n'])\n}, 20_000)\n",
|
|
23
|
+
"// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, withForwardArgs, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n // vx's own `sh`: a bare name is looked up on `req.env`'s PATH, which\n // leads with the project's node_modules/.bin, where a dependency's\n // `sh` would run every command.\n const sh = Bun.which('sh') ?? '/bin/sh'\n // The args after `--`, quoted and placed as the local executor places\n // them (before a trailing comment, never on a heredoc's terminator).\n const command = withForwardArgs(req.command, req.forwardArgs)\n const child = Bun.spawn([sh, '-c', command], {\n cwd: req.cwd,\n env: req.env,\n stdout: 'pipe',\n stderr: 'pipe',\n detached: true,\n })\n const stop = (): void => {\n try {\n process.kill(-child.pid, 'SIGTERM')\n } catch {\n // already gone\n }\n }\n req.signal?.addEventListener('abort', stop, { once: true })\n const drain = async (stream: ReadableStream<Uint8Array>, on: (s: string) => void) => {\n const decoder = new TextDecoder()\n let all = ''\n for await (const chunk of stream) {\n const text = decoder.decode(chunk, { stream: true })\n all += text\n on(text)\n }\n return all\n }\n const [stdout, stderr, exitCode] = await Promise.all([\n drain(child.stdout, req.onStdout),\n drain(child.stderr, req.onStderr),\n child.exited,\n ])\n req.signal?.removeEventListener('abort', stop)\n return {\n exitCode,\n durationMs: Date.now() - started,\n stdout,\n stderr,\n violations: [],\n where: 'shell',\n }\n },\n }\n return definePlugin(import.meta, { executor: () => executor })\n}\n",
|
|
24
|
+
test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n\nit(\"runs the command through this machine's sh, not a project's node_modules/.bin sh\", async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: { hello: { exec: { command: 'echo real' } } } }`,\n )\n await mkdir(path.join(dir, 'node_modules', '.bin'), { recursive: true })\n await writeFile(path.join(dir, 'node_modules', '.bin', 'sh'), '#!/bin/sh\\necho planted\\n', {\n mode: 0o755,\n })\n await workspace('shellExecutor()')\n const out: string[] = []\n const r = await run({\n cwd: root,\n tasks: ['hello'],\n log: { ...log(), taskStdout: (_n, chunk) => void out.push(chunk) },\n handleSignals: false,\n })\n expect([r.outcomes.map((o) => o.status), out.join('')]).toEqual([['success'], 'real\\n'])\n}, 20_000)\n\nit('puts the args after -- on the command as the local executor does', async () => {\n await pkg(\n 'a',\n `export default { tasks: { say: { exec: { command: \"printf '<%s>' # each arg\" } } } }`,\n )\n await workspace('shellExecutor()')\n const out: string[] = []\n const r = await run({\n cwd: root,\n tasks: ['say'],\n forwardArgs: [\"it's\", 'a b'],\n log: { ...log(), taskStdout: (_n, chunk) => void out.push(chunk) },\n handleSignals: false,\n })\n expect([r.outcomes.map((o) => o.status), out.join('')]).toEqual([['success'], \"<it's><a b>\"])\n}, 20_000)\n",
|
|
25
25
|
},
|
|
26
26
|
graph: {
|
|
27
27
|
plugin:
|
package/src/cli/run.ts
CHANGED
|
@@ -3,7 +3,6 @@ import path from 'node:path'
|
|
|
3
3
|
import { isatty } from 'node:tty'
|
|
4
4
|
import { translateForeign } from './foreign-flags.js'
|
|
5
5
|
import { flagHint, seeHelp } from './help.js'
|
|
6
|
-
import { defaultAffectedBase, findWorkspaceRoot } from '../workspace/index.js'
|
|
7
6
|
import {
|
|
8
7
|
planRun,
|
|
9
8
|
formatRunReportMarkdown,
|
|
@@ -15,10 +14,9 @@ import {
|
|
|
15
14
|
} from '../orchestrator/index.js'
|
|
16
15
|
import type { ContinueMode } from '../graph/index.js'
|
|
17
16
|
import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
|
|
18
|
-
import { findCwdSelection, pickTask, resolveFilters } from './select.js'
|
|
17
|
+
import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
|
|
19
18
|
import { nxTargetHint, taskNamesHere } from './task-verb.js'
|
|
20
|
-
import {
|
|
21
|
-
import { MAX_TIMEOUT_MS, isUserError, parseDecimalInt, machineParallelism } from '../util/index.js'
|
|
19
|
+
import { MAX_TIMEOUT_MS, parseDecimalInt, machineParallelism } from '../util/index.js'
|
|
22
20
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
23
21
|
|
|
24
22
|
export interface RunArgs {
|
|
@@ -113,7 +111,7 @@ export function parseConcurrency(v: string, cpus = machineParallelism()): number
|
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
/** `verb` is the one being parsed for: `vx watch` reads `vx run`'s flags, and
|
|
116
|
-
* its
|
|
114
|
+
* its flag hints named `vx run` (M-58). The caller appends the help pointer. */
|
|
117
115
|
export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' = 'run'): RunArgs {
|
|
118
116
|
const out: RunArgs = {
|
|
119
117
|
tasks: [],
|
|
@@ -185,7 +183,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
185
183
|
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
186
184
|
return {
|
|
187
185
|
...out,
|
|
188
|
-
error: `unknown flag: ${a} (the flag is --exclude-dependencies)
|
|
186
|
+
error: `unknown flag: ${a} (the flag is --exclude-dependencies)`,
|
|
189
187
|
}
|
|
190
188
|
} else if (a === '--exclude-dependencies') {
|
|
191
189
|
out.excludeDependencies = 'all'
|
|
@@ -215,7 +213,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
215
213
|
return { ...out, error: `--retry requires a value (a non-negative integer)` }
|
|
216
214
|
const n = parseDecimalInt(v)
|
|
217
215
|
if (n === null) {
|
|
218
|
-
return { ...out, error: `--retry must be a non-negative integer
|
|
216
|
+
return { ...out, error: `--retry must be a non-negative integer (got ${v})` }
|
|
219
217
|
}
|
|
220
218
|
out.retries = n
|
|
221
219
|
} else if (a === '--timeout' || a?.startsWith('--timeout=')) {
|
|
@@ -234,19 +232,31 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
234
232
|
}
|
|
235
233
|
}
|
|
236
234
|
if (n === null || n <= 0) {
|
|
237
|
-
return { ...out, error: `--timeout must be a positive integer
|
|
235
|
+
return { ...out, error: `--timeout must be a positive integer in ms (got ${v})` }
|
|
238
236
|
}
|
|
239
237
|
out.timeout = n
|
|
240
238
|
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
241
239
|
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
240
|
+
if (v === undefined || v === '') {
|
|
241
|
+
return {
|
|
242
|
+
...out,
|
|
243
|
+
error: `--output-logs requires a value (full, errors-only, hash-only, or none)`,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
242
246
|
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
243
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
...out,
|
|
249
|
+
error: `--output-logs must be full, errors-only, hash-only, or none (got ${v})`,
|
|
250
|
+
}
|
|
244
251
|
}
|
|
245
252
|
out.outputLogs = v
|
|
246
253
|
} else if (a === '--download' || a?.startsWith('--download=')) {
|
|
247
254
|
const v = a === '--download' ? before[++i] : a.slice('--download='.length)
|
|
255
|
+
if (v === undefined || v === '') {
|
|
256
|
+
return { ...out, error: `--download requires a value (all, toplevel, or none)` }
|
|
257
|
+
}
|
|
248
258
|
if (v !== 'all' && v !== 'toplevel' && v !== 'none') {
|
|
249
|
-
return { ...out, error: `--download must be all, toplevel, or none` }
|
|
259
|
+
return { ...out, error: `--download must be all, toplevel, or none (got ${v})` }
|
|
250
260
|
}
|
|
251
261
|
out.download = v
|
|
252
262
|
} else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
|
|
@@ -301,8 +311,11 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
301
311
|
out.continueMode = 'always'
|
|
302
312
|
} else if (a?.startsWith('--continue=')) {
|
|
303
313
|
const v = a.slice('--continue='.length)
|
|
314
|
+
if (v === '') {
|
|
315
|
+
return { ...out, error: `--continue= requires a mode (never, deps-ok, or always)` }
|
|
316
|
+
}
|
|
304
317
|
if (v !== 'never' && v !== 'deps-ok' && v !== 'always') {
|
|
305
|
-
return { ...out, error: `--continue must be never, deps-ok, or always` }
|
|
318
|
+
return { ...out, error: `--continue must be never, deps-ok, or always (got ${v})` }
|
|
306
319
|
}
|
|
307
320
|
out.continueMode = v
|
|
308
321
|
} else if (a === '--verbosity' || a?.startsWith('--verbosity=')) {
|
|
@@ -366,7 +379,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
366
379
|
}
|
|
367
380
|
out.report = fmt
|
|
368
381
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
369
|
-
return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}
|
|
382
|
+
return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}` }
|
|
370
383
|
} else if (a !== undefined) {
|
|
371
384
|
out.tasks.push(a)
|
|
372
385
|
}
|
|
@@ -428,6 +441,32 @@ function onCI(v: string | undefined): boolean {
|
|
|
428
441
|
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
429
442
|
}
|
|
430
443
|
|
|
444
|
+
/** The filters a run's scope comes from: `--filter`s, with `--affected` as its `...[<base>]`. */
|
|
445
|
+
async function scopeFilters(
|
|
446
|
+
parsed: RunArgs,
|
|
447
|
+
cwd: string,
|
|
448
|
+
): Promise<{ filterStrings: string[]; affectedFilter?: string } | { error: string }> {
|
|
449
|
+
// `--affected[=<base>]` is sugar for `--filter '...[<base>]'`: the
|
|
450
|
+
// changed projects AND their dependents — what a CI gate must run, what
|
|
451
|
+
// the flag's name says, what the guides promised while the sugar was the
|
|
452
|
+
// changed-only `[<base>]` (item 287). `--filter '[<base>]'` stays the
|
|
453
|
+
// "only what I touched" form. Merging it into the filter list means the
|
|
454
|
+
// same code path handles plain filter use, --affected alone, and the combo.
|
|
455
|
+
// Its place does not change the selection: `applyFilters` takes every
|
|
456
|
+
// include before any exclude (item 979), so `--affected --filter '!app'`
|
|
457
|
+
// drops app from either side (item 955 had put it first for that). First
|
|
458
|
+
// it stays, so a message naming the filters names it first.
|
|
459
|
+
const filterStrings = [...parsed.filters]
|
|
460
|
+
let affectedFilter: string | undefined
|
|
461
|
+
if (parsed.affected !== undefined) {
|
|
462
|
+
const f = await affectedFilterFor(cwd, parsed.affected)
|
|
463
|
+
if (typeof f === 'object') return f
|
|
464
|
+
affectedFilter = f
|
|
465
|
+
filterStrings.unshift(affectedFilter)
|
|
466
|
+
}
|
|
467
|
+
return { filterStrings, ...(affectedFilter !== undefined ? { affectedFilter } : {}) }
|
|
468
|
+
}
|
|
469
|
+
|
|
431
470
|
/**
|
|
432
471
|
* Resolve parsed `vx run` argv into the `RunOptions` the orchestrator
|
|
433
472
|
* consumes. Shared between `runCmd` and `watchCmd` so both subcommands
|
|
@@ -459,35 +498,9 @@ export async function resolveRunOptions(
|
|
|
459
498
|
}
|
|
460
499
|
}
|
|
461
500
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
// changed-only `[<base>]` (item 287). `--filter '[<base>]'` stays the
|
|
466
|
-
// "only what I touched" form. Merging it into the filter list means the
|
|
467
|
-
// same code path handles plain filter use, --affected alone, and the combo.
|
|
468
|
-
// Its place does not change the selection: `applyFilters` takes every
|
|
469
|
-
// include before any exclude (item 979), so `--affected --filter '!app'`
|
|
470
|
-
// drops app from either side (item 955 had put it first for that). First
|
|
471
|
-
// it stays, so a message naming the filters names it first.
|
|
472
|
-
const filterStrings = [...parsed.filters]
|
|
473
|
-
let affectedFilter: string | undefined
|
|
474
|
-
if (parsed.affected !== undefined) {
|
|
475
|
-
const root = await findWorkspaceRoot(cwd)
|
|
476
|
-
let base = parsed.affected
|
|
477
|
-
// The workspace's `affectedBase` — or a plugin's `config` stage, from
|
|
478
|
-
// nx.json's `defaultBase` or `TURBO_SCM_BASE` — comes before the guess.
|
|
479
|
-
if (base === '') base = (await loadCliWorkspace(root)).workspaceConfig?.affectedBase ?? ''
|
|
480
|
-
if (base === '') {
|
|
481
|
-
try {
|
|
482
|
-
base = await defaultAffectedBase(root)
|
|
483
|
-
} catch (err) {
|
|
484
|
-
if (!isUserError(err)) throw err
|
|
485
|
-
return { error: err.message }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
affectedFilter = `...[${base}]`
|
|
489
|
-
filterStrings.unshift(affectedFilter)
|
|
490
|
-
}
|
|
501
|
+
const scope = await scopeFilters(parsed, cwd)
|
|
502
|
+
if ('error' in scope) return scope
|
|
503
|
+
const { filterStrings, affectedFilter } = scope
|
|
491
504
|
|
|
492
505
|
// Project scope applies to bare task names only. Anchored entries
|
|
493
506
|
// (pkg#task) resolve directly to their own project regardless.
|
|
@@ -601,7 +614,7 @@ const TASKS_SHOWN = 12
|
|
|
601
614
|
export async function runCmd(args: readonly string[]): Promise<number> {
|
|
602
615
|
const parsed = parseRunArgs(args)
|
|
603
616
|
if (parsed.error) {
|
|
604
|
-
process.stderr.write(`vx run: ${parsed.error}\n`)
|
|
617
|
+
process.stderr.write(`vx run: ${parsed.error}${seeHelp('run')}\n`)
|
|
605
618
|
return 1
|
|
606
619
|
}
|
|
607
620
|
|
|
@@ -625,14 +638,31 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
625
638
|
)
|
|
626
639
|
return 1
|
|
627
640
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
{},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
641
|
+
const load = {
|
|
642
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
643
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
644
|
+
}
|
|
645
|
+
// `--filter` / `--affected` scope the menu, as they scope a bare task:
|
|
646
|
+
// the menu listed every project and the anchored pick ran outside it.
|
|
647
|
+
const scope = await scopeFilters(parsed, cwd)
|
|
648
|
+
if ('error' in scope) {
|
|
649
|
+
process.stderr.write(`vx run: ${scope.error}\n`)
|
|
650
|
+
return 1
|
|
651
|
+
}
|
|
652
|
+
let only: ReadonlySet<string> | undefined
|
|
653
|
+
if (scope.filterStrings.length > 0) {
|
|
654
|
+
const selected = await resolveFilters(cwd, scope.filterStrings, load, scope.affectedFilter)
|
|
655
|
+
if ('error' in selected) {
|
|
656
|
+
process.stderr.write(`vx run: ${selected.error}\n`)
|
|
657
|
+
return 1
|
|
658
|
+
}
|
|
659
|
+
if ('empty' in selected) {
|
|
660
|
+
process.stderr.write(`vx run: ${selected.empty}\n`)
|
|
661
|
+
return 0
|
|
662
|
+
}
|
|
663
|
+
only = new Set(selected.names)
|
|
664
|
+
}
|
|
665
|
+
const picked = await pickTask(cwd, {}, load, only)
|
|
636
666
|
if (picked === 'interrupted') return 130
|
|
637
667
|
if (!picked) return 1
|
|
638
668
|
tasks = [`${picked.project}#${picked.task}`]
|
package/src/cli/select.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
refIsHead,
|
|
13
13
|
applyFilters,
|
|
14
14
|
buildPackageGraph,
|
|
15
|
+
defaultAffectedBase,
|
|
15
16
|
findWorkspaceRoot,
|
|
16
17
|
type LoadReads,
|
|
17
18
|
type FingerprintClaims,
|
|
@@ -26,7 +27,7 @@ import type { ProjectConfig } from '../config.js'
|
|
|
26
27
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
27
28
|
import { parseDependencySpec } from '../graph/index.js'
|
|
28
29
|
import { declaresInput } from '../cache/index.js'
|
|
29
|
-
import { listed, maskedLine, nearest, UserError } from '../util/index.js'
|
|
30
|
+
import { isUserError, listed, maskedLine, nearest, UserError } from '../util/index.js'
|
|
30
31
|
import {
|
|
31
32
|
claimedAffected,
|
|
32
33
|
fingerprintClaims,
|
|
@@ -228,6 +229,30 @@ export function taskEdgesFrom(staged: ReadonlyMap<string, ProjectEntry>): Map<st
|
|
|
228
229
|
return out
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The filter `--affected[=<base>]` stands for: `...[<base>]`, the changed
|
|
234
|
+
* projects and their dependents. An empty base is the workspace's
|
|
235
|
+
* `affectedBase` (or a plugin's `config` stage, from nx.json's
|
|
236
|
+
* `defaultBase` or `TURBO_SCM_BASE`), then the guess.
|
|
237
|
+
*/
|
|
238
|
+
export async function affectedFilterFor(
|
|
239
|
+
cwd: string,
|
|
240
|
+
affected: string,
|
|
241
|
+
): Promise<string | { error: string }> {
|
|
242
|
+
const root = await findWorkspaceRoot(cwd)
|
|
243
|
+
let base = affected
|
|
244
|
+
if (base === '') base = (await loadCliWorkspace(root)).workspaceConfig?.affectedBase ?? ''
|
|
245
|
+
if (base === '') {
|
|
246
|
+
try {
|
|
247
|
+
base = await defaultAffectedBase(root)
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (!isUserError(err)) throw err
|
|
250
|
+
return { error: err.message }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return `...[${base}]`
|
|
254
|
+
}
|
|
255
|
+
|
|
231
256
|
export async function resolveFilters(
|
|
232
257
|
cwd: string,
|
|
233
258
|
raw: string[],
|
|
@@ -243,7 +268,10 @@ export async function resolveFilters(
|
|
|
243
268
|
} catch (err) {
|
|
244
269
|
return { error: err instanceof Error ? err.message : String(err) }
|
|
245
270
|
}
|
|
246
|
-
|
|
271
|
+
// `sinceViaDeps` (`e2e...[main]`) walks the changed projects' dependents.
|
|
272
|
+
const walksGraph = parsed.some(
|
|
273
|
+
(f) => f.withDeps || f.withDependents || f.onlyDeps || f.sinceViaDeps === true,
|
|
274
|
+
)
|
|
247
275
|
// A filter that diffs against git starts the run's whole-tree walk now:
|
|
248
276
|
// the diff reads its untracked files from it (one walk where it spawned
|
|
249
277
|
// its own), and the run reuses it with the discovery (I-26).
|
|
@@ -422,7 +450,7 @@ export async function resolveFilters(
|
|
|
422
450
|
}
|
|
423
451
|
}
|
|
424
452
|
|
|
425
|
-
|
|
453
|
+
interface PickedTask {
|
|
426
454
|
project: string
|
|
427
455
|
task: string
|
|
428
456
|
description?: string
|
|
@@ -436,6 +464,8 @@ export async function pickTask(
|
|
|
436
464
|
cwd: string,
|
|
437
465
|
io: { input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream } = {},
|
|
438
466
|
load: CliLoadOptions = {},
|
|
467
|
+
/** The projects `--filter` / `--affected` selected; every project when absent. */
|
|
468
|
+
only?: ReadonlySet<string>,
|
|
439
469
|
): Promise<PickedTask | null | 'interrupted'> {
|
|
440
470
|
const projects = await loadWorkspaceProjects(cwd)
|
|
441
471
|
// The staged load: a task a `project` plugin gave a config-less package
|
|
@@ -443,6 +473,7 @@ export async function pickTask(
|
|
|
443
473
|
const staged = await loadCliProjects(await findWorkspaceRoot(cwd), projects, 'all', load)
|
|
444
474
|
const entries: PickedTask[] = []
|
|
445
475
|
for (const meta of projects) {
|
|
476
|
+
if (only !== undefined && !only.has(meta.name)) continue
|
|
446
477
|
const config = staged.get(meta.name)?.config
|
|
447
478
|
if (config === undefined) continue
|
|
448
479
|
const taskNames = Object.keys(config.tasks ?? {})
|
|
@@ -453,13 +484,21 @@ export async function pickTask(
|
|
|
453
484
|
entries.push({ project: meta.name, task: t, ...(desc ? { description: desc } : {}) })
|
|
454
485
|
}
|
|
455
486
|
}
|
|
487
|
+
if (entries.length === 0 && only !== undefined) {
|
|
488
|
+
process.stderr.write(
|
|
489
|
+
`vx run: no tasks declared in the selected projects (${[...only].sort().join(', ')})\n`,
|
|
490
|
+
)
|
|
491
|
+
return null
|
|
492
|
+
}
|
|
456
493
|
if (entries.length === 0) {
|
|
457
494
|
process.stderr.write(
|
|
458
495
|
'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts\n',
|
|
459
496
|
)
|
|
460
497
|
return null
|
|
461
498
|
}
|
|
462
|
-
|
|
499
|
+
// The menu is a conversation with the terminal, not the run's output: on
|
|
500
|
+
// stdout, `vx run > out.txt` put it in the file and asked a blank screen.
|
|
501
|
+
const out = io.output ?? process.stderr
|
|
463
502
|
const numW = String(entries.length).length
|
|
464
503
|
const idW = Math.max(...entries.map((e) => `${e.project}#${e.task}`.length))
|
|
465
504
|
out.write('Tasks:\n')
|
|
@@ -474,7 +513,7 @@ export async function pickTask(
|
|
|
474
513
|
const readline = await import('node:readline/promises')
|
|
475
514
|
const rl = readline.createInterface({
|
|
476
515
|
input: io.input ?? process.stdin,
|
|
477
|
-
output:
|
|
516
|
+
output: out,
|
|
478
517
|
})
|
|
479
518
|
// On a terminal readline takes Ctrl-C and Ctrl-D itself, raw, and
|
|
480
519
|
// rejects the pending question with an AbortError — which reached the
|
package/src/cli/show.ts
CHANGED
|
@@ -10,8 +10,9 @@ 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 { isDefaultBuild } from '../orchestrator/index.js'
|
|
13
|
-
import { flagHint, seeHelp } from './help.js'
|
|
13
|
+
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
14
14
|
import { listed, nearMatches, relPosix, secretMask, UserError } from '../util/index.js'
|
|
15
|
+
import { affectedFilterFor, resolveFilters } from './select.js'
|
|
15
16
|
import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
|
|
16
17
|
import {
|
|
17
18
|
findWorkspaceRoot,
|
|
@@ -21,19 +22,30 @@ import {
|
|
|
21
22
|
type ProjectMeta,
|
|
22
23
|
} from '../workspace/index.js'
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
interface ShowArgs {
|
|
25
26
|
target?: string
|
|
26
27
|
format: 'pretty' | 'json'
|
|
28
|
+
/** `--filter`, as `vx run` reads it: which projects the list shows. */
|
|
29
|
+
filters: string[]
|
|
30
|
+
/** `--affected[=<base>]`: `''` is the default base. */
|
|
31
|
+
affected?: string
|
|
27
32
|
error?: string
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export function parseShowArgs(args: readonly string[]): ShowArgs {
|
|
31
|
-
const out: ShowArgs = { format: 'pretty' }
|
|
36
|
+
const out: ShowArgs = { format: 'pretty', filters: [] }
|
|
32
37
|
for (let i = 0; i < args.length; i++) {
|
|
33
38
|
const a = args[i]!
|
|
34
39
|
let format: string | undefined
|
|
35
40
|
if (a === '--format') format = args[++i] ?? ''
|
|
36
41
|
else if (a.startsWith('--format=')) format = a.slice('--format='.length)
|
|
42
|
+
else if (a === '--filter') {
|
|
43
|
+
const v = args[++i]
|
|
44
|
+
if (v === undefined) return { ...out, error: '--filter requires a value (a pattern)' }
|
|
45
|
+
out.filters.push(v)
|
|
46
|
+
} else if (a.startsWith('--filter=')) out.filters.push(a.slice('--filter='.length))
|
|
47
|
+
else if (a === '--affected') out.affected = ''
|
|
48
|
+
else if (a.startsWith('--affected=')) out.affected = a.slice('--affected='.length)
|
|
37
49
|
else if (a.startsWith('-'))
|
|
38
50
|
return { ...out, error: `unknown flag: ${a}${flagHint('show', a)}${seeHelp('show')}` }
|
|
39
51
|
else if (out.target === 'project')
|
|
@@ -47,10 +59,9 @@ export function parseShowArgs(args: readonly string[]): ShowArgs {
|
|
|
47
59
|
else out.target = a
|
|
48
60
|
|
|
49
61
|
if (format !== undefined) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
out.format = format
|
|
62
|
+
const fv = formatValue(format, 'show')
|
|
63
|
+
if (typeof fv === 'object') return { ...out, ...fv }
|
|
64
|
+
out.format = fv
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
return out
|
|
@@ -104,10 +115,35 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
104
115
|
const bareTask = projectName !== undefined && taskName === undefined && !byName.has(projectName)
|
|
105
116
|
const scope = projectName === undefined || bareTask ? 'all' : [projectName]
|
|
106
117
|
|
|
118
|
+
// `turbo ls --filter` / `nx show projects --affected`: the list, and a
|
|
119
|
+
// task's projects, narrowed by the selection `vx run` would make.
|
|
120
|
+
let selected: Set<string> | undefined
|
|
121
|
+
if (parsed.filters.length > 0 || parsed.affected !== undefined) {
|
|
122
|
+
if (scope !== 'all') {
|
|
123
|
+
throw new UserError(
|
|
124
|
+
'vx show: --filter and --affected narrow a list: `vx show` or `vx show <task>`',
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const raw = [...parsed.filters]
|
|
128
|
+
let affected: string | undefined
|
|
129
|
+
if (parsed.affected !== undefined) {
|
|
130
|
+
const f = await affectedFilterFor(process.cwd(), parsed.affected)
|
|
131
|
+
if (typeof f === 'object') throw new UserError(`vx show: ${f.error}`)
|
|
132
|
+
affected = f
|
|
133
|
+
raw.unshift(f)
|
|
134
|
+
}
|
|
135
|
+
const r = await resolveFilters(process.cwd(), raw, { noCreate: true }, affected)
|
|
136
|
+
if ('error' in r) throw new UserError(`vx show: ${r.error}`)
|
|
137
|
+
if ('empty' in r) process.stderr.write(`vx show: ${r.empty}\n`)
|
|
138
|
+
selected = new Set('empty' in r ? [] : r.names)
|
|
139
|
+
}
|
|
140
|
+
const inSelection = (name: string): boolean => selected === undefined || selected.has(name)
|
|
141
|
+
|
|
107
142
|
const projects = await loadCliProjects(root, metas, scope, { noCreate: true, closure: true })
|
|
108
143
|
|
|
109
144
|
if (parsed.target === undefined) {
|
|
110
|
-
|
|
145
|
+
const shown = metas.filter((m) => inSelection(m.name))
|
|
146
|
+
process.stdout.write(renderList(root, shown, projects, parsed.format))
|
|
111
147
|
return 0
|
|
112
148
|
}
|
|
113
149
|
|
|
@@ -125,7 +161,8 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
125
161
|
`vx show: unknown project or task: "${projectName}"${nx}${suggest(projectName!, [...byName.keys(), ...names], '', 'projects and tasks')}`,
|
|
126
162
|
)
|
|
127
163
|
}
|
|
128
|
-
|
|
164
|
+
const shown = declaring.filter((p) => inSelection(p.name))
|
|
165
|
+
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format))
|
|
129
166
|
return 0
|
|
130
167
|
}
|
|
131
168
|
|
|
@@ -240,6 +277,7 @@ function renderList(
|
|
|
240
277
|
2,
|
|
241
278
|
)}\n`
|
|
242
279
|
}
|
|
280
|
+
if (rows.length === 0) return ''
|
|
243
281
|
const nameW = Math.max(...rows.map((r) => r.name.length))
|
|
244
282
|
const dirW = Math.max(...rows.map((r) => r.dir.length))
|
|
245
283
|
const lines = rows.map((r) => {
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `vx upgrade [
|
|
1
|
+
// `vx upgrade [TAG]` — self-update the compiled binary in place.
|
|
2
2
|
// Asks the GitHub release API for the asset of this os/arch and the
|
|
3
3
|
// SHA-256 digest it publishes, downloads the asset, verifies the digest,
|
|
4
4
|
// writes next to the current executable, atomic rename over it. The
|
|
@@ -299,8 +299,7 @@ export async function upgradeCmd(args: readonly string[]): Promise<number> {
|
|
|
299
299
|
}
|
|
300
300
|
if (!isCompiledBinary()) {
|
|
301
301
|
throw new UserError(
|
|
302
|
-
'vx upgrade only
|
|
303
|
-
'You are running from source — use git pull instead. ' +
|
|
302
|
+
'vx upgrade: only the compiled binary upgrades itself; this vx runs from source — use git pull ' +
|
|
304
303
|
'(npm installs update with: npm install -g @vzn/vx@latest)',
|
|
305
304
|
)
|
|
306
305
|
}
|
package/src/cli/watch-filter.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isLiteralPattern,
|
|
9
9
|
normalizeGlob,
|
|
10
10
|
outputMatcher,
|
|
11
|
+
realPath,
|
|
11
12
|
staticPrefix,
|
|
12
13
|
taskGlob,
|
|
13
14
|
} from '../util/index.js'
|
|
@@ -54,7 +55,9 @@ export function makeWatchIgnore(
|
|
|
54
55
|
outputs: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
55
56
|
inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>> = new Map(),
|
|
56
57
|
): (base: string, filename: string) => boolean {
|
|
57
|
-
|
|
58
|
+
// Watchers report under the real root; a cache dir named through a link
|
|
59
|
+
// (`VX_CACHE_DIR`, macOS `/var` -> `/private/var`) is matched as its target.
|
|
60
|
+
const cacheAbs = realThrough(path.resolve(cacheDir))
|
|
58
61
|
// A task's own outputs are not edits: without this every cycle that
|
|
59
62
|
// writes `dist/` (or `out.txt`) re-runs once more, reporting
|
|
60
63
|
// "up-to-date" for the trouble. Matched under the directory the globs
|
|
@@ -132,6 +135,16 @@ export function makeWatchIgnore(
|
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
/** `p` with its longest existing prefix realpath'd: the cache dir may not exist yet. */
|
|
139
|
+
function realThrough(p: string): string {
|
|
140
|
+
try {
|
|
141
|
+
return realPath(p)
|
|
142
|
+
} catch {
|
|
143
|
+
const up = path.dirname(p)
|
|
144
|
+
return up === p ? p : path.join(realThrough(up), path.basename(p))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
/**
|
|
136
149
|
* The literal directory a glob's matches live under (`''` when the glob
|
|
137
150
|
* starts with a pattern, or negates). A literal entry is a file or its
|
|
@@ -185,7 +198,7 @@ export function gitIgnored(workspaceRoot: string, paths: readonly string[]): Set
|
|
|
185
198
|
}
|
|
186
199
|
// Each record: source, line, pattern, path. No source: no pattern
|
|
187
200
|
// matched; a `!` pattern: re-included. Either way not ignored.
|
|
188
|
-
const fields = new TextDecoder().decode(proc.stdout).split('\0')
|
|
201
|
+
const fields = new TextDecoder('utf-8', { ignoreBOM: true }).decode(proc.stdout).split('\0')
|
|
189
202
|
const records = Math.floor(fields.length / 4)
|
|
190
203
|
for (let i = 0; i < records; i++) {
|
|
191
204
|
const [source, , pattern, p] = fields.slice(i * 4, i * 4 + 4)
|
|
@@ -232,10 +245,11 @@ export function makeRootEventFilter(
|
|
|
232
245
|
fenced: (ownDir: string, abs: string) => boolean = () => false,
|
|
233
246
|
): (filename: string) => boolean {
|
|
234
247
|
const dirs = projectDirs.map((d) => path.resolve(d))
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
// A literal is its tree, as the key resolves it: `shared` matched only
|
|
249
|
+
// the directory's own event and no edit under it ran a cycle (WD-2).
|
|
250
|
+
const globs = asTrees(workspaceInputs.filter((g) => !normalizeGlob(g).startsWith('!'))).map((g) =>
|
|
251
|
+
taskGlob(g),
|
|
252
|
+
)
|
|
239
253
|
return (filename: string): boolean => {
|
|
240
254
|
const rel = filename.split(path.sep).join('/')
|
|
241
255
|
// The depth test is a READING AID, not a guard: both predicates below
|
|
@@ -359,7 +373,7 @@ export function gitFiles(workspaceRoot: string): Set<string> | undefined {
|
|
|
359
373
|
}
|
|
360
374
|
if (proc.exitCode !== 0) return undefined
|
|
361
375
|
const files = new Set<string>()
|
|
362
|
-
for (const p of new TextDecoder().decode(proc.stdout).split('\0')) {
|
|
376
|
+
for (const p of new TextDecoder('utf-8', { ignoreBOM: true }).decode(proc.stdout).split('\0')) {
|
|
363
377
|
if (p.length === 0) continue
|
|
364
378
|
// An untracked nested repository is listed as `dir/`.
|
|
365
379
|
let abs = path.join(workspaceRoot, p.endsWith('/') ? p.slice(0, -1) : p)
|