@vzn/vx 0.0.651 → 0.0.652
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 +2 -3
- package/package.json +5 -5
- package/schemas/show.json +14 -20
- package/src/bin.ts +12 -0
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +19 -5
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/inputs.ts +74 -14
- package/src/cache/layer.ts +6 -6
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/run-history.ts +46 -17
- package/src/cache/schema.ts +4 -1
- package/src/cli/cache.ts +2 -2
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +24 -0
- package/src/cli/info.ts +1 -1
- package/src/cli/last.ts +34 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/show.ts +7 -10
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +11 -8
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +40 -1
- package/src/exec/index.ts +1 -0
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +314 -29
- package/src/exec/task-proxy.ts +93 -0
- package/src/graph/definition.ts +16 -3
- package/src/graph/scheduler.ts +3 -1
- package/src/graph/task-graph.ts +24 -23
- package/src/history/failure-mode.ts +24 -1
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +5 -5
- package/src/index.ts +4 -2
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +51 -18
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/miss-save.ts +27 -8
- package/src/orchestrator/persistent-task.ts +2 -0
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +2 -2
- package/src/orchestrator/plugin-host.ts +6 -2
- package/src/orchestrator/plugin.ts +6 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-failures.ts +61 -12
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +86 -67
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +92 -19
- package/src/orchestrator/sandbox-request.ts +50 -7
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +4 -4
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +3 -3
- package/src/report/framed-output.ts +16 -7
- package/src/report/logger.ts +1 -1
- package/src/report/plain-output.ts +2 -0
- package/src/report/summary.ts +5 -5
- package/src/settings.ts +8 -8
- package/src/util/config-frame.ts +128 -6
- package/src/util/paths.ts +1 -1
- package/src/util/settle.ts +2 -1
- package/src/util/tail.ts +4 -1
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +66 -61
- package/src/workspace/filter.ts +8 -1
- package/src/workspace/migration.ts +12 -6
|
@@ -115,7 +115,7 @@ const oneOf = (values: readonly string[]): string =>
|
|
|
115
115
|
|
|
116
116
|
function validateInputStability(stability: unknown, at: string): void {
|
|
117
117
|
if (typeof stability !== 'object' || stability === null || Array.isArray(stability)) {
|
|
118
|
-
throw new UserError(`${at} must be an object — \`
|
|
118
|
+
throw new UserError(`${at} must be an object — \`stability: { strategy: 'mutable' }\``)
|
|
119
119
|
}
|
|
120
120
|
assertKnownFields(stability, INPUT_STABILITY_FIELDS, at)
|
|
121
121
|
const { strategy, severity } = stability as Record<string, unknown>
|
|
@@ -128,13 +128,13 @@ function validateInputStability(stability: unknown, at: string): void {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* `
|
|
132
|
-
* task's `
|
|
131
|
+
* `VX_INPUTS_STABILITY_STRATEGY` and `_SEVERITY`, which override every
|
|
132
|
+
* task's `inputs.stability` for the run: each undefined when unset or
|
|
133
133
|
* empty, refused when it names no value.
|
|
134
134
|
*/
|
|
135
135
|
export function inputStabilityEnv(): InputStability {
|
|
136
|
-
const strategy = settingEnv('
|
|
137
|
-
const severity = settingEnv('
|
|
136
|
+
const strategy = settingEnv('inputs.stability.strategy') as InputStability['strategy']
|
|
137
|
+
const severity = settingEnv('inputs.stability.severity') as InputStability['severity']
|
|
138
138
|
return {
|
|
139
139
|
...(strategy !== undefined ? { strategy } : {}),
|
|
140
140
|
...(severity !== undefined ? { severity } : {}),
|
|
@@ -231,6 +231,8 @@ function ruleSelectorProblem(s: string): string | null {
|
|
|
231
231
|
// The root's own name, which the filter reads as the root's directory.
|
|
232
232
|
if (s === ROOT_NAME) return null
|
|
233
233
|
if (s.startsWith('!')) return "starts with '!', which a filter reads as an exclusion"
|
|
234
|
+
// The filter parser refuses one, which the catch below would read as a walk.
|
|
235
|
+
if (hasControl(s)) return 'holds a control character'
|
|
234
236
|
const walk = "starts or ends with '...', which a filter reads as a dependency walk"
|
|
235
237
|
let f: ReturnType<typeof parseFilter>
|
|
236
238
|
try {
|
|
@@ -590,6 +592,9 @@ export function validateProjectConfig(
|
|
|
590
592
|
const exec = (task as { exec?: unknown }).exec
|
|
591
593
|
const dependsOn = (task as { dependsOn?: unknown }).dependsOn
|
|
592
594
|
const cache = (task as { cache?: unknown }).cache
|
|
595
|
+
if (cache !== undefined && typeof cache !== 'boolean') {
|
|
596
|
+
throw new UserError(`${where}.cache must be true or false`)
|
|
597
|
+
}
|
|
593
598
|
if (exec !== undefined) {
|
|
594
599
|
if (typeof exec !== 'object' || exec === null) {
|
|
595
600
|
throw new UserError(`${where}.exec must be an object with a \`command\` string`)
|
|
@@ -623,9 +628,9 @@ export function validateProjectConfig(
|
|
|
623
628
|
// Documented and never held: an uncached 'only' task loaded, and the
|
|
624
629
|
// REAPI executor refused it at run time for want of described inputs
|
|
625
630
|
// (item 1001).
|
|
626
|
-
if (remote === 'only' && cache
|
|
631
|
+
if (remote === 'only' && cache !== true) {
|
|
627
632
|
throw new UserError(
|
|
628
|
-
`${where}.exec.remote 'only' needs \`cache\`: its inputs are what a worker reproduces ` +
|
|
633
|
+
`${where}.exec.remote 'only' needs \`cache: true\`: its inputs are what a worker reproduces ` +
|
|
629
634
|
`and its key is the address of its remote record`,
|
|
630
635
|
)
|
|
631
636
|
}
|
|
@@ -712,7 +717,7 @@ export function validateProjectConfig(
|
|
|
712
717
|
throw new UserError(`${where}.exec.interactive must be a boolean (or omitted)`)
|
|
713
718
|
}
|
|
714
719
|
if (interactive === true) {
|
|
715
|
-
if (cache
|
|
720
|
+
if (cache === true) {
|
|
716
721
|
throw new UserError(
|
|
717
722
|
`${where}: \`cache\` is not allowed on an interactive task — what it does ` +
|
|
718
723
|
`depends on what is typed, which no key holds`,
|
|
@@ -765,7 +770,7 @@ export function validateProjectConfig(
|
|
|
765
770
|
)
|
|
766
771
|
}
|
|
767
772
|
}
|
|
768
|
-
if (cache
|
|
773
|
+
if (cache === true) {
|
|
769
774
|
throw new UserError(
|
|
770
775
|
`${where}: \`cache\` is not allowed on a persistent task — persistent tasks ` +
|
|
771
776
|
`don't terminate, so there's no exit to cache`,
|
|
@@ -943,16 +948,8 @@ export function validateProjectConfig(
|
|
|
943
948
|
validateWorkspaceGlobs(wsInputs, `${where}.inputs.workspaceFiles`, true)
|
|
944
949
|
}
|
|
945
950
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
throw new UserError(
|
|
949
|
-
`${where}.cache must be an object when present — \`inputs: { files: [...] }, cache: { outputs: { files: [...] } }\`, or no \`cache\` for a task that never caches`,
|
|
950
|
-
)
|
|
951
|
-
}
|
|
952
|
-
assertKnownFields(cache, CACHE_FIELDS, `${where}.cache`)
|
|
953
|
-
const stability = (cache as { inputStability?: unknown }).inputStability
|
|
954
|
-
if (stability !== undefined)
|
|
955
|
-
validateInputStability(stability, `${where}.cache.inputStability`)
|
|
951
|
+
const outputs = (task as { outputs?: unknown }).outputs
|
|
952
|
+
if (cache === true) {
|
|
956
953
|
// The key is the task's inputs, and a group runs nothing to cache.
|
|
957
954
|
if (exec === undefined) {
|
|
958
955
|
throw new UserError(
|
|
@@ -964,23 +961,38 @@ export function validateProjectConfig(
|
|
|
964
961
|
`${where}: \`cache\` needs \`inputs\` — the key is the task's inputs, \`inputs: { files: [...] }\``,
|
|
965
962
|
)
|
|
966
963
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
964
|
+
if (outputs === undefined) {
|
|
965
|
+
throw new UserError(
|
|
966
|
+
`${where}: \`cache\` needs \`outputs\` — what the task produces, \`outputs: { files: [...] }\` (\`[]\` for none)`,
|
|
967
|
+
)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const stability = (inputs as { stability?: unknown } | undefined)?.stability
|
|
971
|
+
if (stability !== undefined) {
|
|
972
|
+
validateInputStability(stability, `${where}.inputs.stability`)
|
|
973
|
+
if (exec === undefined) {
|
|
974
|
+
throw new UserError(
|
|
975
|
+
`${where}.inputs.stability needs \`exec\` — a task with no command runs nothing that could write its inputs`,
|
|
976
|
+
)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (outputs !== undefined) {
|
|
980
|
+
if (outputs === null || typeof outputs !== 'object') {
|
|
981
|
+
throw new UserError(`${where}.outputs must be an object — \`outputs: { files: [...] }\``)
|
|
970
982
|
}
|
|
971
|
-
|
|
983
|
+
assertKnownFields(outputs, OUTPUT_FIELDS, `${where}.outputs`)
|
|
984
|
+
if (exec === undefined) {
|
|
972
985
|
throw new UserError(
|
|
973
|
-
`${where}.
|
|
986
|
+
`${where}.outputs needs \`exec\` — a task with no command writes nothing`,
|
|
974
987
|
)
|
|
975
988
|
}
|
|
976
|
-
assertKnownFields(outputs, CACHE_OUTPUT_FIELDS, `${where}.cache.outputs`)
|
|
977
989
|
const clean = (outputs as { clean?: unknown }).clean
|
|
978
990
|
if (clean !== undefined && typeof clean !== 'boolean') {
|
|
979
|
-
throw new UserError(`${where}.
|
|
991
|
+
throw new UserError(`${where}.outputs.clean must be true or false`)
|
|
980
992
|
}
|
|
981
993
|
const outFiles = (outputs as { files?: unknown }).files
|
|
982
994
|
if (!Array.isArray(outFiles)) {
|
|
983
|
-
throw new UserError(`${where}.
|
|
995
|
+
throw new UserError(`${where}.outputs.files must be an array of glob strings`)
|
|
984
996
|
}
|
|
985
997
|
// Reject zero-length strings and absolute paths up front.
|
|
986
998
|
// Both reach `resolveOutputs` as undefined behavior; the error
|
|
@@ -988,26 +1000,26 @@ export function validateProjectConfig(
|
|
|
988
1000
|
// at the user's config. Fail loud at load time.
|
|
989
1001
|
for (const g of outFiles as unknown[]) {
|
|
990
1002
|
if (typeof g !== 'string' || g.length === 0) {
|
|
991
|
-
throw new UserError(`${where}.
|
|
1003
|
+
throw new UserError(`${where}.outputs.files must be an array of non-empty strings`)
|
|
992
1004
|
}
|
|
993
1005
|
if (isAbsoluteGlob(g)) {
|
|
994
1006
|
throw new UserError(
|
|
995
|
-
`${where}.
|
|
1007
|
+
`${where}.outputs.files: absolute paths are not allowed (got "${g}") — ` +
|
|
996
1008
|
`outputs must be project-relative globs`,
|
|
997
1009
|
)
|
|
998
1010
|
}
|
|
999
1011
|
if (hasParentSegment(g)) {
|
|
1000
1012
|
throw new UserError(
|
|
1001
|
-
`${where}.
|
|
1013
|
+
`${where}.outputs.files: '..' path segments are not allowed (got "${g}") — ` +
|
|
1002
1014
|
`outputs must stay within the project (vx clears a task's outputs before it runs, ` +
|
|
1003
1015
|
`so a glob that escapes the project would delete files outside it)`,
|
|
1004
1016
|
)
|
|
1005
1017
|
}
|
|
1006
|
-
assertPosixGlob(g, `${where}.
|
|
1007
|
-
assertNoForeignToken(g, `${where}.
|
|
1018
|
+
assertPosixGlob(g, `${where}.outputs.files`, 'project')
|
|
1019
|
+
assertNoForeignToken(g, `${where}.outputs.files`)
|
|
1008
1020
|
if (namesDirItself(g)) {
|
|
1009
1021
|
throw new UserError(
|
|
1010
|
-
`${where}.
|
|
1022
|
+
`${where}.outputs.files: "${g}" names the project directory itself and selects nothing — ` +
|
|
1011
1023
|
`name the directory the task writes, such as "dist/**"`,
|
|
1012
1024
|
)
|
|
1013
1025
|
}
|
|
@@ -1016,31 +1028,31 @@ export function validateProjectConfig(
|
|
|
1016
1028
|
// a "corrupt artifact", dropped and run again.
|
|
1017
1029
|
if (!g.startsWith('!') && normalizeGlob(g).split('/')[0] === 'workspace-outputs') {
|
|
1018
1030
|
throw new UserError(
|
|
1019
|
-
`${where}.
|
|
1031
|
+
`${where}.outputs.files: "${g}" is under workspace-outputs/, a name vx's ` +
|
|
1020
1032
|
`artifacts reserve for outputs.workspaceFiles — write the task's files to another directory`,
|
|
1021
1033
|
)
|
|
1022
1034
|
}
|
|
1023
1035
|
// A `!` entry takes a path back from the outputs (A-44): it is not
|
|
1024
1036
|
// cleaned, saved or restored, and it stays an input.
|
|
1025
|
-
assertNotDoubleNegated(g, `${where}.
|
|
1037
|
+
assertNotDoubleNegated(g, `${where}.outputs.files`)
|
|
1026
1038
|
// Taken back by a `!`, the file is never cleaned or restored.
|
|
1027
1039
|
const own = g.startsWith('!')
|
|
1028
1040
|
? undefined
|
|
1029
1041
|
: ownFilesCovered(g, ownConfig).find((f) => !takenBack(outFiles as unknown[], f))
|
|
1030
1042
|
if (own !== undefined) {
|
|
1031
1043
|
throw new UserError(
|
|
1032
|
-
`${where}.
|
|
1044
|
+
`${where}.outputs.files: "${g}" covers the project's own ${own} — vx deletes a ` +
|
|
1033
1045
|
`task's outputs before it runs and restores them on a hit, so the project would lose ` +
|
|
1034
1046
|
`its config. Name the directory the task writes, such as "dist/**", ` +
|
|
1035
1047
|
`or take the file back with "!${own}".`,
|
|
1036
1048
|
)
|
|
1037
1049
|
}
|
|
1038
1050
|
}
|
|
1039
|
-
assertOutputsNotNegationOnly(outFiles as string[], `${where}.
|
|
1040
|
-
assertNoLiteralTakenBack(outFiles as string[], `${where}.
|
|
1051
|
+
assertOutputsNotNegationOnly(outFiles as string[], `${where}.outputs.files`)
|
|
1052
|
+
assertNoLiteralTakenBack(outFiles as string[], `${where}.outputs.files`, OUTPUT_LOST)
|
|
1041
1053
|
const wsOutputs = (outputs as { workspaceFiles?: unknown }).workspaceFiles
|
|
1042
1054
|
if (wsOutputs !== undefined) {
|
|
1043
|
-
validateWorkspaceGlobs(wsOutputs, `${where}.
|
|
1055
|
+
validateWorkspaceGlobs(wsOutputs, `${where}.outputs.workspaceFiles`, false)
|
|
1044
1056
|
}
|
|
1045
1057
|
}
|
|
1046
1058
|
}
|
|
@@ -1055,7 +1067,7 @@ export function validateProjectConfig(
|
|
|
1055
1067
|
// `tasks` first: `nearest` breaks a tie by order, and `taks` is one edit
|
|
1056
1068
|
// from both.
|
|
1057
1069
|
const PROJECT_FIELDS = new Set(['name', 'tasks', 'tags'])
|
|
1058
|
-
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'inputs', 'cache'])
|
|
1070
|
+
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'inputs', 'outputs', 'cache'])
|
|
1059
1071
|
const EXEC_FIELDS = new Set([
|
|
1060
1072
|
'command',
|
|
1061
1073
|
'env',
|
|
@@ -1069,7 +1081,6 @@ const EXEC_FIELDS = new Set([
|
|
|
1069
1081
|
])
|
|
1070
1082
|
const PERSISTENT_FIELDS = new Set(['readyWhen'])
|
|
1071
1083
|
const ENV_FIELDS = new Set(['passThrough', 'define', 'secret'])
|
|
1072
|
-
const CACHE_FIELDS = new Set(['outputs', 'inputStability'])
|
|
1073
1084
|
const INPUT_FIELDS = new Set([
|
|
1074
1085
|
'files',
|
|
1075
1086
|
'workspaceFiles',
|
|
@@ -1077,8 +1088,9 @@ const INPUT_FIELDS = new Set([
|
|
|
1077
1088
|
'tasks',
|
|
1078
1089
|
'runtime',
|
|
1079
1090
|
'workspaceRuntime',
|
|
1091
|
+
'stability',
|
|
1080
1092
|
])
|
|
1081
|
-
const
|
|
1093
|
+
const OUTPUT_FIELDS = new Set(['files', 'workspaceFiles', 'clean'])
|
|
1082
1094
|
|
|
1083
1095
|
/**
|
|
1084
1096
|
* Another runner's spelling of a field vx has under another name, by the
|
|
@@ -1090,8 +1102,10 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1090
1102
|
[
|
|
1091
1103
|
TASK_FIELDS,
|
|
1092
1104
|
{
|
|
1093
|
-
outputs: '`cache.outputs.files`',
|
|
1094
1105
|
env: '`inputs.env` (to key the task on a variable) or `exec.env` (to pass one)',
|
|
1106
|
+
// wireit's script keys.
|
|
1107
|
+
output: '`outputs.files`',
|
|
1108
|
+
dependencies: "`dependsOn` (an upstream's key goes into this one)",
|
|
1095
1109
|
passThroughEnv: '`exec.env.passThrough`',
|
|
1096
1110
|
command: '`exec.command`',
|
|
1097
1111
|
cmd: '`exec.command`',
|
|
@@ -1128,18 +1142,6 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1128
1142
|
shell: 'no field: `command` always runs in a shell',
|
|
1129
1143
|
},
|
|
1130
1144
|
],
|
|
1131
|
-
// wireit's `files` / `output` and include-style spellings written into
|
|
1132
|
-
// a cache block, refused with no word on where they go (D-118).
|
|
1133
|
-
[
|
|
1134
|
-
CACHE_FIELDS,
|
|
1135
|
-
{
|
|
1136
|
-
files: "the task's `inputs.files`",
|
|
1137
|
-
output: '`outputs.files`',
|
|
1138
|
-
env: "the task's `inputs.env`",
|
|
1139
|
-
dependencies: "the task's `dependsOn` (an upstream's key goes into this one)",
|
|
1140
|
-
enabled: 'no field: a task with a `cache` block caches, one without runs every time',
|
|
1141
|
-
},
|
|
1142
|
-
],
|
|
1143
1145
|
[
|
|
1144
1146
|
INPUT_FIELDS,
|
|
1145
1147
|
{
|
|
@@ -1151,7 +1153,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1151
1153
|
},
|
|
1152
1154
|
],
|
|
1153
1155
|
[
|
|
1154
|
-
|
|
1156
|
+
OUTPUT_FIELDS,
|
|
1155
1157
|
{
|
|
1156
1158
|
globs: '`files`',
|
|
1157
1159
|
include: '`files`',
|
|
@@ -1176,12 +1178,11 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1176
1178
|
sourceRoot: "a task's `inputs.files`",
|
|
1177
1179
|
projectType: 'no field: vx runs every project alike',
|
|
1178
1180
|
...Object.fromEntries(
|
|
1179
|
-
['dependsOn', 'inputs', 'cache', 'exec', 'description'].map((k) => [
|
|
1181
|
+
['dependsOn', 'inputs', 'outputs', 'cache', 'exec', 'description'].map((k) => [
|
|
1180
1182
|
k,
|
|
1181
1183
|
`\`tasks.<name>.${k}\`: a project holds tasks, and each task its own \`${k}\``,
|
|
1182
1184
|
]),
|
|
1183
1185
|
),
|
|
1184
|
-
outputs: '`tasks.<name>.cache.outputs.files`',
|
|
1185
1186
|
env: '`tasks.<name>.inputs.env` (to key a task on it) or `tasks.<name>.exec.env`',
|
|
1186
1187
|
},
|
|
1187
1188
|
],
|
|
@@ -1252,7 +1253,7 @@ function nestedHome(allowed: ReadonlySet<string>, key: string): string | undefin
|
|
|
1252
1253
|
for (const [field, own] of [
|
|
1253
1254
|
['exec', EXEC_FIELDS],
|
|
1254
1255
|
['inputs', INPUT_FIELDS],
|
|
1255
|
-
['
|
|
1256
|
+
['outputs', OUTPUT_FIELDS],
|
|
1256
1257
|
] as const) {
|
|
1257
1258
|
if (own.has(key)) return `\`${field}.${key}\``
|
|
1258
1259
|
}
|
|
@@ -1538,7 +1539,7 @@ function assertInputGlob(glob: string, where: string): void {
|
|
|
1538
1539
|
if (glob.replace(/^!/, '').split('/').includes('.git')) {
|
|
1539
1540
|
throw new UserError(
|
|
1540
1541
|
`${where}: "${glob}" names a path under .git, which git never lists, so it keys nothing — ` +
|
|
1541
|
-
`inputs are the
|
|
1542
|
+
`inputs are the workspace's files`,
|
|
1542
1543
|
)
|
|
1543
1544
|
}
|
|
1544
1545
|
}
|
|
@@ -1699,6 +1700,9 @@ export function taskNameProblem(name: string): string | null {
|
|
|
1699
1700
|
function tagProblem(tag: string): string | null {
|
|
1700
1701
|
if (tag.trim() !== tag) return 'has surrounding whitespace'
|
|
1701
1702
|
if (tag.includes('*')) return "holds '*', which makes it a pattern"
|
|
1703
|
+
// Refused by the caller in its own words; the filter parser refuses one
|
|
1704
|
+
// too, which the catch below would read as a walk.
|
|
1705
|
+
if (hasControl(tag)) return null
|
|
1702
1706
|
const walk = "ends in '...', which a filter reads as a dependency walk"
|
|
1703
1707
|
let f: ReturnType<typeof parseFilter>
|
|
1704
1708
|
try {
|
|
@@ -1828,6 +1832,7 @@ const SANDBOX_FIELDS = new Set([
|
|
|
1828
1832
|
'ignore',
|
|
1829
1833
|
'weakerWhenNested',
|
|
1830
1834
|
'weakerNetworkIsolation',
|
|
1835
|
+
'hermetic',
|
|
1831
1836
|
])
|
|
1832
1837
|
const GRANT_PATH_FIELDS = ['read', 'write'] as const
|
|
1833
1838
|
const GRANT_NAME_FIELDS = ['systemInfo', 'machLookup'] as const
|
|
@@ -1871,7 +1876,7 @@ function validateSandbox(sandbox: unknown, where: string): void {
|
|
|
1871
1876
|
assertKnownFields(sandbox, SANDBOX_FIELDS, `${where}.sandbox`)
|
|
1872
1877
|
const obj = sandbox as Record<string, unknown>
|
|
1873
1878
|
|
|
1874
|
-
for (const flag of ['weakerWhenNested', 'weakerNetworkIsolation']) {
|
|
1879
|
+
for (const flag of ['weakerWhenNested', 'weakerNetworkIsolation', 'hermetic']) {
|
|
1875
1880
|
if (obj[flag] !== undefined && typeof obj[flag] !== 'boolean') {
|
|
1876
1881
|
throw new UserError(`${where}.sandbox.${flag} must be a boolean`)
|
|
1877
1882
|
}
|
package/src/workspace/filter.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// given, the base set is "all projects" and excluded packages are removed.
|
|
23
23
|
|
|
24
24
|
import path from 'node:path'
|
|
25
|
-
import { BUN_GLOB_WILDCARDS, relPosix, UserError } from '../util/index.js'
|
|
25
|
+
import { BUN_GLOB_WILDCARDS, hasControl, printable, relPosix, UserError } from '../util/index.js'
|
|
26
26
|
import type { PackageGraph } from './package-graph.js'
|
|
27
27
|
import { canonicalDir } from './canonical-dir.js'
|
|
28
28
|
import type { ProjectMeta } from './workspace.js'
|
|
@@ -76,6 +76,13 @@ export function parseFilter(
|
|
|
76
76
|
/** The filter as typed, when `raw` is the `./` reading of a name (`dirFallback`). */
|
|
77
77
|
typed = raw,
|
|
78
78
|
): ParsedFilter {
|
|
79
|
+
// No project name, path or git ref holds one; echoed raw, a newline split
|
|
80
|
+
// the refusal in two and an escape painted the terminal.
|
|
81
|
+
if (hasControl(raw)) {
|
|
82
|
+
throw new UserError(
|
|
83
|
+
`filter "${printable(raw)}" holds a control character: no project name, path or git ref does`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
79
86
|
let s = raw
|
|
80
87
|
const negate = s.startsWith('!')
|
|
81
88
|
if (negate) s = s.slice(1)
|
|
@@ -42,6 +42,10 @@ export function isStarterConfig(file: string): boolean {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** A C0 or C1 control, or DEL. */
|
|
46
|
+
// oxlint-disable-next-line no-control-regex
|
|
47
|
+
const CONTROL = /[\x00-\x1f\x7f-\x9f]/g
|
|
48
|
+
|
|
45
49
|
/**
|
|
46
50
|
* Escape an arbitrary string into a single-quoted TS literal. Escapes
|
|
47
51
|
* backslash + quote AND raw newlines/CR — a value with an embedded newline
|
|
@@ -55,7 +59,10 @@ export function quoteTsLiteral(s: string): string {
|
|
|
55
59
|
.replaceAll('\\', '\\\\')
|
|
56
60
|
.replaceAll("'", "\\'")
|
|
57
61
|
.replaceAll('\n', '\\n')
|
|
58
|
-
.replaceAll('\r', '\\r')
|
|
62
|
+
.replaceAll('\r', '\\r')
|
|
63
|
+
// Any other control as its escape: a script's raw ESC landed in the
|
|
64
|
+
// file as the byte, and `cat vx.config.ts` retitled the terminal.
|
|
65
|
+
.replace(CONTROL, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`)}'`
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -658,7 +665,7 @@ const EXAMPLE_BODY = `export default {
|
|
|
658
665
|
build: {
|
|
659
666
|
exec: { command: 'tsc -b' },
|
|
660
667
|
dependsOn: ['^build'],
|
|
661
|
-
inputs: { files: ['src/**'] },
|
|
668
|
+
inputs: { files: ['src/**'] }, outputs: { files: ['dist/**'] }, cache: true,
|
|
662
669
|
},
|
|
663
670
|
},
|
|
664
671
|
}`
|
|
@@ -747,7 +754,7 @@ function renderConfigFile(p: GeneratedProject, format: MigrationFormat): string
|
|
|
747
754
|
// The suggested block sits in the task, commented: deleting `// `
|
|
748
755
|
// after setting the real globs caches it, no copying from the TODO.
|
|
749
756
|
const block =
|
|
750
|
-
t.task['cache']
|
|
757
|
+
t.task['cache'] !== true
|
|
751
758
|
? t.todos.find(isCacheTodo)?.match(/^cache: add `(.+?)`/)?.[1]
|
|
752
759
|
: undefined
|
|
753
760
|
lines.push(
|
|
@@ -790,11 +797,10 @@ function ownConfigNamed(
|
|
|
790
797
|
return {
|
|
791
798
|
...p,
|
|
792
799
|
tasks: p.tasks.map((t) => {
|
|
793
|
-
const outputs =
|
|
800
|
+
const outputs = t.task?.['outputs'] as { files?: unknown } | undefined
|
|
794
801
|
if (!Array.isArray(outputs?.files) || !outputs.files.includes(planned)) return t
|
|
795
|
-
const cache = t.task!['cache'] as Record<string, unknown>
|
|
796
802
|
const files = outputs.files.map((f: unknown) => (f === planned ? `!${name}` : f))
|
|
797
|
-
return { ...t, task: { ...t.task,
|
|
803
|
+
return { ...t, task: { ...t.task, outputs: { ...outputs, files } } }
|
|
798
804
|
}),
|
|
799
805
|
}
|
|
800
806
|
}
|