@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cli/migrate-turbo.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
// Turbo → vx mapping. Reads turbo.json (tasks in turbo 2, pipeline in
|
|
2
|
-
// turbo 1), per-package turbo.json `extends` overlays, and each
|
|
3
|
-
// package's scripts. A task is emitted for a package only when the
|
|
4
|
-
// package declares the script (turbo semantics). Global fields become
|
|
5
|
-
// a root vx-preset.ts that configs import and spread — TypeScript
|
|
6
|
-
// composition replaces turbo's global config.
|
|
7
|
-
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
10
|
-
import type { ProjectMeta } from '../workspace/index.js'
|
|
11
|
-
import type { GeneratedProject, GeneratedTask, MigrationPlan, RawExpr } from './migrate.js'
|
|
12
|
-
|
|
13
|
-
interface TurboTask {
|
|
14
|
-
dependsOn?: string[]
|
|
15
|
-
inputs?: string[]
|
|
16
|
-
outputs?: string[]
|
|
17
|
-
env?: string[]
|
|
18
|
-
passThroughEnv?: string[]
|
|
19
|
-
cache?: boolean
|
|
20
|
-
persistent?: boolean
|
|
21
|
-
[key: string]: unknown
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface TurboJson {
|
|
25
|
-
tasks?: Record<string, TurboTask>
|
|
26
|
-
pipeline?: Record<string, TurboTask>
|
|
27
|
-
globalDependencies?: string[]
|
|
28
|
-
globalEnv?: string[]
|
|
29
|
-
globalPassThroughEnv?: string[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const KNOWN_TASK_KEYS = new Set([
|
|
33
|
-
'dependsOn',
|
|
34
|
-
'inputs',
|
|
35
|
-
'outputs',
|
|
36
|
-
'env',
|
|
37
|
-
'passThroughEnv',
|
|
38
|
-
'cache',
|
|
39
|
-
'persistent',
|
|
40
|
-
'extends',
|
|
41
|
-
])
|
|
42
|
-
|
|
43
|
-
const PRESET_FILE = 'vx-preset.ts'
|
|
44
|
-
|
|
45
|
-
interface Globals {
|
|
46
|
-
inputs: boolean
|
|
47
|
-
env: boolean
|
|
48
|
-
pass: boolean
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
52
|
-
const text = await Bun.file(file).text()
|
|
53
|
-
try {
|
|
54
|
-
// turbo.json allows comments + trailing commas.
|
|
55
|
-
return (Bun.JSONC.parse(text) ?? {}) as TurboJson
|
|
56
|
-
} catch (err) {
|
|
57
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
58
|
-
throw new UserError(`failed to parse ${relPosix(root, file)}: ${msg}`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function tasksOf(cfg: TurboJson): Record<string, TurboTask> {
|
|
63
|
-
return cfg.tasks ?? cfg.pipeline ?? {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function scriptsOf(meta: ProjectMeta): Record<string, string> {
|
|
67
|
-
return (meta.packageJson as unknown as { scripts?: Record<string, string> }).scripts ?? {}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Declared task names for a package: plain root keys, `pkg#name` keys
|
|
71
|
-
* for this package, and per-package turbo.json keys — in that order. */
|
|
72
|
-
function taskNamesFor(
|
|
73
|
-
pkgName: string,
|
|
74
|
-
rootTasks: Record<string, TurboTask>,
|
|
75
|
-
pkgTasks: Record<string, TurboTask> | undefined,
|
|
76
|
-
): string[] {
|
|
77
|
-
const names: string[] = []
|
|
78
|
-
const push = (n: string): void => {
|
|
79
|
-
if (!names.includes(n)) names.push(n)
|
|
80
|
-
}
|
|
81
|
-
for (const key of Object.keys(rootTasks)) {
|
|
82
|
-
if (!key.includes('#')) push(key)
|
|
83
|
-
else if (key.startsWith(`${pkgName}#`)) push(key.slice(pkgName.length + 1))
|
|
84
|
-
}
|
|
85
|
-
for (const key of Object.keys(pkgTasks ?? {})) {
|
|
86
|
-
if (!key.includes('#')) push(key)
|
|
87
|
-
}
|
|
88
|
-
return names
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function migrateTurbo(
|
|
92
|
-
root: string,
|
|
93
|
-
metas: readonly ProjectMeta[],
|
|
94
|
-
): Promise<MigrationPlan> {
|
|
95
|
-
const rootCfg = await readTurboJson(path.join(root, 'turbo.json'), root)
|
|
96
|
-
const rootTasks = tasksOf(rootCfg)
|
|
97
|
-
|
|
98
|
-
const globalInputs = rootCfg.globalDependencies ?? []
|
|
99
|
-
const globalEnv = rootCfg.globalEnv ?? []
|
|
100
|
-
const globalPass = rootCfg.globalPassThroughEnv ?? []
|
|
101
|
-
const globals: Globals = {
|
|
102
|
-
inputs: globalInputs.length > 0,
|
|
103
|
-
env: globalEnv.length > 0,
|
|
104
|
-
pass: globalPass.length > 0,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const pkgTasksByName = new Map<string, Record<string, TurboTask>>()
|
|
108
|
-
for (const meta of metas) {
|
|
109
|
-
const file = path.join(meta.dir, 'turbo.json')
|
|
110
|
-
if (await Bun.file(file).exists()) {
|
|
111
|
-
pkgTasksByName.set(meta.name, tasksOf(await readTurboJson(file, root)))
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const notes: string[] = []
|
|
116
|
-
for (const key of Object.keys(rootTasks)) {
|
|
117
|
-
if (key.startsWith('//#')) {
|
|
118
|
-
notes.push(`note: root task ${key} not migrated — vx has no workspace-root tasks`)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// First pass: which tasks does each package emit? Needed so dependsOn
|
|
123
|
-
// edges can be validated/dropped against the real emitted set.
|
|
124
|
-
const emitted = new Map<string, Set<string>>()
|
|
125
|
-
for (const meta of metas) {
|
|
126
|
-
const scripts = scriptsOf(meta)
|
|
127
|
-
const set = new Set<string>()
|
|
128
|
-
for (const name of taskNamesFor(meta.name, rootTasks, pkgTasksByName.get(meta.name))) {
|
|
129
|
-
if (scripts[name] !== undefined) set.add(name)
|
|
130
|
-
}
|
|
131
|
-
emitted.set(meta.name, set)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const projects: GeneratedProject[] = []
|
|
135
|
-
for (const meta of metas) {
|
|
136
|
-
const scripts = scriptsOf(meta)
|
|
137
|
-
const pkgTasks = pkgTasksByName.get(meta.name)
|
|
138
|
-
const own = emitted.get(meta.name)!
|
|
139
|
-
const used = new Set<string>()
|
|
140
|
-
const tasks: GeneratedTask[] = []
|
|
141
|
-
for (const name of taskNamesFor(meta.name, rootTasks, pkgTasks)) {
|
|
142
|
-
if (!own.has(name)) continue
|
|
143
|
-
const def: TurboTask = {
|
|
144
|
-
...rootTasks[name],
|
|
145
|
-
...rootTasks[`${meta.name}#${name}`],
|
|
146
|
-
...pkgTasks?.[name],
|
|
147
|
-
}
|
|
148
|
-
tasks.push(buildTask(name, def, scripts[name]!, own, emitted, globals, used))
|
|
149
|
-
}
|
|
150
|
-
projects.push({
|
|
151
|
-
name: meta.name,
|
|
152
|
-
dir: meta.dir,
|
|
153
|
-
importLines: presetImportLines(used, root, meta.dir),
|
|
154
|
-
tasks,
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const extraFiles: MigrationPlan['extraFiles'] = []
|
|
159
|
-
if (globals.inputs || globals.env || globals.pass) {
|
|
160
|
-
extraFiles.push({
|
|
161
|
-
relPath: PRESET_FILE,
|
|
162
|
-
contents: renderPreset(globalInputs, globalEnv, globalPass),
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { headerNotes: [], projects, extraFiles, notes }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function buildTask(
|
|
170
|
-
name: string,
|
|
171
|
-
def: TurboTask,
|
|
172
|
-
command: string,
|
|
173
|
-
own: ReadonlySet<string>,
|
|
174
|
-
emitted: ReadonlyMap<string, ReadonlySet<string>>,
|
|
175
|
-
globals: Globals,
|
|
176
|
-
used: Set<string>,
|
|
177
|
-
): GeneratedTask {
|
|
178
|
-
const todos: string[] = []
|
|
179
|
-
const persistent = def.persistent === true
|
|
180
|
-
const cacheEnabled = def.cache !== false && !persistent
|
|
181
|
-
|
|
182
|
-
for (const [key, value] of Object.entries(def)) {
|
|
183
|
-
if (KNOWN_TASK_KEYS.has(key)) continue
|
|
184
|
-
todos.push(
|
|
185
|
-
`turbo key ${JSON.stringify(key)} (${JSON.stringify(value)}) has no vx equivalent — ` +
|
|
186
|
-
'map it manually',
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const deps: string[] = []
|
|
191
|
-
for (const d of def.dependsOn ?? []) {
|
|
192
|
-
if (d.includes('$TURBO_ROOT$')) {
|
|
193
|
-
todos.push(
|
|
194
|
-
`dependsOn ${JSON.stringify(d)} uses $TURBO_ROOT$ — vx has no workspace-root tasks; ` +
|
|
195
|
-
'restructure manually',
|
|
196
|
-
)
|
|
197
|
-
continue
|
|
198
|
-
}
|
|
199
|
-
if (d.startsWith('^')) {
|
|
200
|
-
deps.push(d)
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
const hashAt = d.indexOf('#')
|
|
204
|
-
if (hashAt !== -1) {
|
|
205
|
-
const pkg = d.slice(0, hashAt)
|
|
206
|
-
const task = d.slice(hashAt + 1)
|
|
207
|
-
if (emitted.get(pkg)?.has(task)) deps.push(d)
|
|
208
|
-
else
|
|
209
|
-
todos.push(
|
|
210
|
-
`dependsOn ${JSON.stringify(d)}: ${pkg} declares no ${task} script — edge dropped`,
|
|
211
|
-
)
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
// Same-project dep on a script this package lacks: turbo silently
|
|
215
|
-
// skips the task there, so the edge simply doesn't exist.
|
|
216
|
-
if (own.has(d)) deps.push(d)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const envNames: string[] = []
|
|
220
|
-
for (const e of def.env ?? []) {
|
|
221
|
-
if (/[*?[\]!]/.test(e)) {
|
|
222
|
-
todos.push(
|
|
223
|
-
`env ${JSON.stringify(e)}: wildcards are not supported in vx env names — ` +
|
|
224
|
-
'list explicit names in cache.inputs.env + exec.env.passThrough',
|
|
225
|
-
)
|
|
226
|
-
} else envNames.push(e)
|
|
227
|
-
}
|
|
228
|
-
const passNames: string[] = []
|
|
229
|
-
for (const e of def.passThroughEnv ?? []) {
|
|
230
|
-
if (/[*?[\]!]/.test(e)) {
|
|
231
|
-
todos.push(
|
|
232
|
-
`passThroughEnv ${JSON.stringify(e)}: wildcards are not supported — list explicit names`,
|
|
233
|
-
)
|
|
234
|
-
} else passNames.push(e)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const passThrough: (string | RawExpr)[] = []
|
|
238
|
-
if (globals.env) {
|
|
239
|
-
passThrough.push({ raw: '...globalEnvInputs' })
|
|
240
|
-
used.add('globalEnvInputs')
|
|
241
|
-
}
|
|
242
|
-
if (globals.pass) {
|
|
243
|
-
passThrough.push({ raw: '...globalPassThroughEnv' })
|
|
244
|
-
used.add('globalPassThroughEnv')
|
|
245
|
-
}
|
|
246
|
-
passThrough.push(...envNames, ...passNames)
|
|
247
|
-
|
|
248
|
-
const exec: Record<string, unknown> = { command }
|
|
249
|
-
if (passThrough.length > 0) exec.env = { passThrough }
|
|
250
|
-
if (persistent) {
|
|
251
|
-
exec.persistent = {}
|
|
252
|
-
todos.push(
|
|
253
|
-
'persistent task — set persistent.readyWhen (regex matched against output) so ' +
|
|
254
|
-
'dependents unblock on readiness, and consider exec.timeout to bound the wait',
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const task: Record<string, unknown> = { exec }
|
|
259
|
-
if (deps.length > 0) task.dependsOn = deps
|
|
260
|
-
|
|
261
|
-
if (cacheEnabled) {
|
|
262
|
-
const files: (string | RawExpr)[] = []
|
|
263
|
-
const wsFiles: (string | RawExpr)[] = []
|
|
264
|
-
// globalDependencies are workspace-root-relative by definition —
|
|
265
|
-
// they map to inputs.workspaceFiles, not project-relative files.
|
|
266
|
-
if (globals.inputs) {
|
|
267
|
-
wsFiles.push({ raw: '...globalInputs' })
|
|
268
|
-
used.add('globalInputs')
|
|
269
|
-
}
|
|
270
|
-
if (def.inputs === undefined) {
|
|
271
|
-
// Turbo's default input set is every package file.
|
|
272
|
-
files.push('**/*')
|
|
273
|
-
} else {
|
|
274
|
-
for (const i of def.inputs) {
|
|
275
|
-
if (i === '$TURBO_DEFAULT$') {
|
|
276
|
-
files.push('**/*')
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
const neg = i.startsWith('!')
|
|
280
|
-
const body = neg ? i.slice(1) : i
|
|
281
|
-
if (body.startsWith('$TURBO_ROOT$/')) {
|
|
282
|
-
wsFiles.push((neg ? '!' : '') + body.slice('$TURBO_ROOT$/'.length))
|
|
283
|
-
} else if (i.includes('$TURBO_ROOT$')) {
|
|
284
|
-
todos.push(
|
|
285
|
-
`input ${JSON.stringify(i)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
286
|
-
'prefix (→ cache.inputs.workspaceFiles) — map manually',
|
|
287
|
-
)
|
|
288
|
-
} else files.push(i)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const outFiles: string[] = []
|
|
293
|
-
const wsOutFiles: string[] = []
|
|
294
|
-
for (const o of def.outputs ?? []) {
|
|
295
|
-
if (o.startsWith('!')) {
|
|
296
|
-
todos.push(
|
|
297
|
-
`output ${JSON.stringify(o)}: vx outputs have no negation — narrow the positive ` +
|
|
298
|
-
'globs instead',
|
|
299
|
-
)
|
|
300
|
-
} else if (o.startsWith('$TURBO_ROOT$/')) {
|
|
301
|
-
wsOutFiles.push(o.slice('$TURBO_ROOT$/'.length))
|
|
302
|
-
} else if (o.includes('$TURBO_ROOT$')) {
|
|
303
|
-
todos.push(
|
|
304
|
-
`output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
305
|
-
'prefix (→ cache.outputs.workspaceFiles) — map manually',
|
|
306
|
-
)
|
|
307
|
-
} else outFiles.push(o)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const cacheEnv: (string | RawExpr)[] = []
|
|
311
|
-
if (globals.env) cacheEnv.push({ raw: '...globalEnvInputs' })
|
|
312
|
-
cacheEnv.push(...envNames)
|
|
313
|
-
|
|
314
|
-
const inputs: Record<string, unknown> = { files }
|
|
315
|
-
if (wsFiles.length > 0) inputs.workspaceFiles = wsFiles
|
|
316
|
-
if (cacheEnv.length > 0) inputs.env = cacheEnv
|
|
317
|
-
const outputs: Record<string, unknown> = { files: outFiles }
|
|
318
|
-
if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
|
|
319
|
-
task.cache = { inputs, outputs }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return { name, todos, task }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function presetImportLines(used: ReadonlySet<string>, root: string, dir: string): string[] {
|
|
326
|
-
if (used.size === 0) return []
|
|
327
|
-
const rel = relPosix(dir, path.join(root, PRESET_FILE))
|
|
328
|
-
const spec = rel.startsWith('.') ? rel : `./${rel}`
|
|
329
|
-
return [`import { ${[...used].sort().join(', ')} } from '${spec}'`]
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function renderPreset(inputs: string[], env: string[], pass: string[]): string {
|
|
333
|
-
const arr = (xs: string[]): string => `[${xs.map((x) => `'${x}'`).join(', ')}]`
|
|
334
|
-
const lines = [
|
|
335
|
-
'// Generated by `vx migrate` from turbo.json. TypeScript composition',
|
|
336
|
-
"// replaces turbo's global fields: each vx.config.ts imports these",
|
|
337
|
-
'// arrays and spreads them into the matching task fields.',
|
|
338
|
-
]
|
|
339
|
-
if (inputs.length > 0) {
|
|
340
|
-
lines.push(
|
|
341
|
-
'',
|
|
342
|
-
'// From globalDependencies — workspace-root-relative, spread into each',
|
|
343
|
-
'// task’s cache.inputs.workspaceFiles (the $TURBO_ROOT$ equivalent).',
|
|
344
|
-
`export const globalInputs = ${arr(inputs)}`,
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
if (env.length > 0) {
|
|
348
|
-
lines.push(
|
|
349
|
-
'',
|
|
350
|
-
'// From globalEnv: cache inputs AND passed through to every task',
|
|
351
|
-
'// (vx child environments are isolated; see docs/schema.md).',
|
|
352
|
-
`export const globalEnvInputs = ${arr(env)}`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
if (pass.length > 0) {
|
|
356
|
-
lines.push(
|
|
357
|
-
'',
|
|
358
|
-
'// From globalPassThroughEnv: forwarded to every task, never hashed.',
|
|
359
|
-
`export const globalPassThroughEnv = ${arr(pass)}`,
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
lines.push('')
|
|
363
|
-
return lines.join('\n')
|
|
364
|
-
}
|
package/src/cli/migrate.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// `vx migrate [--dry] [--force]` — generate per-package vx.config.ts
|
|
2
|
-
// from an existing Turbo or Nx setup. Source auto-detect: turbo.json →
|
|
3
|
-
// Turbo; .nx/workspace-data/project-graph.json → Nx (the resolved
|
|
4
|
-
// snapshot). Mappers live in migrate-turbo.ts / migrate-nx.ts and
|
|
5
|
-
// return an IR; this file owns detection, TS emission, the overwrite
|
|
6
|
-
// guard, and the final report.
|
|
7
|
-
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
10
|
-
import { findWorkspaceRoot, listProjects, loadWorkspace } from '../workspace/index.js'
|
|
11
|
-
import { migrateNx } from './migrate-nx.js'
|
|
12
|
-
import { migrateTurbo } from './migrate-turbo.js'
|
|
13
|
-
|
|
14
|
-
export interface MigrateArgs {
|
|
15
|
-
dry: boolean
|
|
16
|
-
force: boolean
|
|
17
|
-
from?: 'turbo' | 'nx'
|
|
18
|
-
error?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
22
|
-
const out: MigrateArgs = { dry: false, force: false }
|
|
23
|
-
for (let i = 0; i < args.length; i++) {
|
|
24
|
-
const a = args[i]
|
|
25
|
-
if (a === '--dry') out.dry = true
|
|
26
|
-
else if (a === '--force') out.force = true
|
|
27
|
-
else if (a === '--from' || a?.startsWith('--from=')) {
|
|
28
|
-
const v = a === '--from' ? args[++i] : a.slice('--from='.length)
|
|
29
|
-
if (v !== 'turbo' && v !== 'nx') return { ...out, error: `--from must be turbo or nx` }
|
|
30
|
-
out.from = v
|
|
31
|
-
} else if (a?.startsWith('-')) return { ...out, error: `unknown flag: ${a}` }
|
|
32
|
-
else return { ...out, error: `unexpected argument: ${a}` }
|
|
33
|
-
}
|
|
34
|
-
return out
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Verbatim TS expression spliced into a generated array (preset spreads). */
|
|
38
|
-
export interface RawExpr {
|
|
39
|
-
readonly raw: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface GeneratedTask {
|
|
43
|
-
name: string
|
|
44
|
-
/** Rendered as `// TODO(vx-migrate): …` above the task + listed in the report. */
|
|
45
|
-
todos: string[]
|
|
46
|
-
/** TaskConfig-shaped object; arrays may contain RawExpr splices.
|
|
47
|
-
* null = target has no vx representation (skipped; todos explain). */
|
|
48
|
-
task: Record<string, unknown> | null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface GeneratedProject {
|
|
52
|
-
/** package.json name */
|
|
53
|
-
name: string
|
|
54
|
-
/** absolute project dir */
|
|
55
|
-
dir: string
|
|
56
|
-
importLines: string[]
|
|
57
|
-
tasks: GeneratedTask[]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface MigrationPlan {
|
|
61
|
-
/** report lines printed right under the source line */
|
|
62
|
-
headerNotes: string[]
|
|
63
|
-
projects: GeneratedProject[]
|
|
64
|
-
/** extra root-relative files (e.g. the preset) */
|
|
65
|
-
extraFiles: { relPath: string; contents: string }[]
|
|
66
|
-
/** trailing report lines (e.g. implicit Nx deps) */
|
|
67
|
-
notes: string[]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
71
|
-
const parsed = parseMigrateArgs(args)
|
|
72
|
-
if (parsed.error) {
|
|
73
|
-
process.stderr.write(`vx migrate: ${parsed.error}\n`)
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
const root = await findWorkspaceRoot(process.cwd())
|
|
77
|
-
const metas = await listProjects(await loadWorkspace(root))
|
|
78
|
-
|
|
79
|
-
const hasTurbo = await Bun.file(path.join(root, 'turbo.json')).exists()
|
|
80
|
-
const graphRel = path.join('.nx', 'workspace-data', 'project-graph.json')
|
|
81
|
-
const hasGraph = await Bun.file(path.join(root, graphRel)).exists()
|
|
82
|
-
const hasNxJson = await Bun.file(path.join(root, 'nx.json')).exists()
|
|
83
|
-
|
|
84
|
-
// Evaluating teams routinely have both runners checked in — never
|
|
85
|
-
// ask anyone to delete anything; --from disambiguates.
|
|
86
|
-
if (parsed.from === undefined && hasTurbo && (hasGraph || hasNxJson)) {
|
|
87
|
-
throw new UserError(
|
|
88
|
-
'both turbo.json and an nx workspace are present — pass --from turbo or --from nx',
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
if (parsed.from === 'turbo' && !hasTurbo) {
|
|
92
|
-
throw new UserError('--from turbo, but no turbo.json at the workspace root')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let source: string
|
|
96
|
-
let plan: MigrationPlan
|
|
97
|
-
if (parsed.from === 'nx' || (parsed.from === undefined && !hasTurbo)) {
|
|
98
|
-
if (hasGraph) {
|
|
99
|
-
source = '.nx/workspace-data/project-graph.json'
|
|
100
|
-
plan = await migrateNx(root, metas)
|
|
101
|
-
} else if (hasNxJson || parsed.from === 'nx') {
|
|
102
|
-
// Modern Nx stores the graph in SQLite — the JSON snapshot only
|
|
103
|
-
// exists when exported explicitly.
|
|
104
|
-
throw new UserError(
|
|
105
|
-
'no resolved Nx graph found — export one with ' +
|
|
106
|
-
'`nx graph --file=.nx/workspace-data/project-graph.json`, then re-run vx migrate',
|
|
107
|
-
)
|
|
108
|
-
} else {
|
|
109
|
-
throw new UserError('nothing to migrate: no turbo.json or nx.json at the workspace root')
|
|
110
|
-
}
|
|
111
|
-
} else if (hasTurbo) {
|
|
112
|
-
source = 'turbo.json'
|
|
113
|
-
plan = await migrateTurbo(root, metas)
|
|
114
|
-
} else {
|
|
115
|
-
throw new UserError('nothing to migrate: no turbo.json or nx.json at the workspace root')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const files: { relPath: string; abs: string; contents: string }[] = []
|
|
119
|
-
for (const p of plan.projects) {
|
|
120
|
-
if (p.tasks.length === 0) continue
|
|
121
|
-
const abs = path.join(p.dir, 'vx.config.ts')
|
|
122
|
-
files.push({ relPath: relPosix(root, abs), abs, contents: renderConfigFile(source, p) })
|
|
123
|
-
}
|
|
124
|
-
for (const f of plan.extraFiles) {
|
|
125
|
-
files.push({ relPath: f.relPath, abs: path.join(root, f.relPath), contents: f.contents })
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!parsed.dry && !parsed.force) {
|
|
129
|
-
const conflicts: string[] = []
|
|
130
|
-
for (const p of plan.projects) {
|
|
131
|
-
if (p.tasks.length === 0) continue
|
|
132
|
-
const meta = metas.find((m) => m.dir === p.dir)
|
|
133
|
-
if (meta?.configPath) conflicts.push(relPosix(root, meta.configPath))
|
|
134
|
-
}
|
|
135
|
-
for (const f of plan.extraFiles) {
|
|
136
|
-
if (await Bun.file(path.join(root, f.relPath)).exists()) conflicts.push(f.relPath)
|
|
137
|
-
}
|
|
138
|
-
if (conflicts.length > 0) {
|
|
139
|
-
throw new UserError(
|
|
140
|
-
'refusing to overwrite existing files (pass --force to overwrite):\n' +
|
|
141
|
-
` ${conflicts.join('\n ')}`,
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (parsed.dry) {
|
|
147
|
-
for (const f of files) {
|
|
148
|
-
process.stdout.write(`── ${f.relPath} ──\n${f.contents}\n`)
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
for (const f of files) await Bun.write(f.abs, f.contents)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const todoList: string[] = []
|
|
155
|
-
let clean = 0
|
|
156
|
-
for (const p of plan.projects) {
|
|
157
|
-
for (const t of p.tasks) {
|
|
158
|
-
if (t.todos.length === 0 && t.task !== null) clean++
|
|
159
|
-
for (const reason of t.todos) todoList.push(`${p.name}#${t.name}: ${reason}`)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const report: string[] = [`vx migrate: ${source} → vx.config.ts`]
|
|
163
|
-
for (const n of plan.headerNotes) report.push(`note: ${n}`)
|
|
164
|
-
report.push(
|
|
165
|
-
'',
|
|
166
|
-
`${clean} task${clean === 1 ? '' : 's'} migrated clean, ` +
|
|
167
|
-
`${todoList.length} TODO${todoList.length === 1 ? '' : 's'}${todoList.length > 0 ? ':' : ''}`,
|
|
168
|
-
)
|
|
169
|
-
for (const line of todoList) report.push(` ${line}`)
|
|
170
|
-
report.push(...plan.notes)
|
|
171
|
-
report.push(parsed.dry ? 'files (dry run, nothing written):' : 'files written:')
|
|
172
|
-
for (const f of files) report.push(` ${f.relPath}`)
|
|
173
|
-
process.stdout.write(`${report.join('\n')}\n`)
|
|
174
|
-
return 0
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─── TS emission ──────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
const IDENT = /^[A-Za-z_$][\w$]*$/
|
|
180
|
-
|
|
181
|
-
function quote(s: string): string {
|
|
182
|
-
return `'${s.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function isRawExpr(v: unknown): v is RawExpr {
|
|
186
|
-
return typeof v === 'object' && v !== null && typeof (v as RawExpr).raw === 'string'
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function renderValue(v: unknown, indent: string): string {
|
|
190
|
-
if (isRawExpr(v)) return v.raw
|
|
191
|
-
if (typeof v === 'string') return quote(v)
|
|
192
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
193
|
-
if (Array.isArray(v)) return `[${v.map((x) => renderValue(x, indent)).join(', ')}]`
|
|
194
|
-
const entries = Object.entries(v as Record<string, unknown>).filter(([, x]) => x !== undefined)
|
|
195
|
-
if (entries.length === 0) return '{}'
|
|
196
|
-
const inner = `${indent} `
|
|
197
|
-
const body = entries.map(
|
|
198
|
-
([k, x]) => `${inner}${IDENT.test(k) ? k : quote(k)}: ${renderValue(x, inner)},`,
|
|
199
|
-
)
|
|
200
|
-
return `{\n${body.join('\n')}\n${indent}}`
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function renderConfigFile(source: string, p: GeneratedProject): string {
|
|
204
|
-
const lines: string[] = [
|
|
205
|
-
`// Generated by \`vx migrate\` from ${source}. Review the TODO(vx-migrate)`,
|
|
206
|
-
"// comments, then wrap the object in defineProject() from '@vzn/vx' for",
|
|
207
|
-
'// editor type-checking.',
|
|
208
|
-
]
|
|
209
|
-
if (p.importLines.length > 0) lines.push(...p.importLines)
|
|
210
|
-
lines.push('', 'export default {', ' tasks: {')
|
|
211
|
-
for (const t of p.tasks) {
|
|
212
|
-
for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
|
|
213
|
-
if (t.task === null) continue // skipped target — the TODO above explains
|
|
214
|
-
const key = IDENT.test(t.name) ? t.name : quote(t.name)
|
|
215
|
-
lines.push(` ${key}: ${renderValue(t.task, ' ')},`)
|
|
216
|
-
}
|
|
217
|
-
lines.push(' },', '}', '')
|
|
218
|
-
return lines.join('\n')
|
|
219
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// The devframe surface for a vx run: one definition that exposes the run
|
|
2
|
-
// as a live streaming channel (`vx:events`, the raw WireEvent feed) plus a
|
|
3
|
-
// patch-synced shared state (`vx:run`, the reduced RunState aggregate).
|
|
4
|
-
// A host adapter (dev server / MCP — see cli wiring) mounts this same
|
|
5
|
-
// definition; this module owns only the translation from our in-process
|
|
6
|
-
// event bus into devframe's streaming + shared-state primitives.
|
|
7
|
-
//
|
|
8
|
-
// devframe is touched ONLY through type-only imports here, so core `vx`
|
|
9
|
-
// never gains a runtime dependency on it — the host that actually boots a
|
|
10
|
-
// devframe server imports the runtime adapter dynamically and is the
|
|
11
|
-
// optional, opt-in path. See docs/design/event-stream-2026-06.md.
|
|
12
|
-
|
|
13
|
-
import type { DevframeDefinition, DevframeNodeContext, RpcStreamingChannel } from 'devframe'
|
|
14
|
-
import { VERSION } from '../version.js'
|
|
15
|
-
import { toWireEvent, type EventBus, type WireEvent } from './events.js'
|
|
16
|
-
import { initRunState, reduce, type RunState } from './run-state.js'
|
|
17
|
-
|
|
18
|
-
// Type the `vx:run` shared-state key so `ctx.rpc.sharedState.get` is
|
|
19
|
-
// checked against our aggregate shape.
|
|
20
|
-
declare module 'devframe' {
|
|
21
|
-
interface DevframeRpcSharedStates {
|
|
22
|
-
'vx:run': RunState
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface VxSurfaceOptions {
|
|
27
|
-
/** Stream id for the run's event feed. One run → one stream. */
|
|
28
|
-
streamId?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build a devframe definition that mirrors a single run's bus onto
|
|
33
|
-
* devframe's wire. `setup` runs at host boot (before the run emits), so
|
|
34
|
-
* it subscribes to the bus, forwards every event onto the `vx:events`
|
|
35
|
-
* stream, and folds it into the `vx:run` shared state. The host syncs
|
|
36
|
-
* both to any connected client (web SPA, MCP resource).
|
|
37
|
-
*/
|
|
38
|
-
export function createVxSurface(bus: EventBus, options: VxSurfaceOptions = {}): DevframeDefinition {
|
|
39
|
-
const streamId = options.streamId ?? 'run'
|
|
40
|
-
return {
|
|
41
|
-
id: 'vx',
|
|
42
|
-
name: 'vx',
|
|
43
|
-
version: VERSION,
|
|
44
|
-
async setup(ctx: DevframeNodeContext) {
|
|
45
|
-
const channel: RpcStreamingChannel<WireEvent> = ctx.rpc.streaming.create('vx:events')
|
|
46
|
-
const stream = channel.start({ id: streamId })
|
|
47
|
-
const state = await ctx.rpc.sharedState.get('vx:run', { initialValue: initRunState() })
|
|
48
|
-
|
|
49
|
-
// Local snapshot fed to `reduce`; the shared state is updated by
|
|
50
|
-
// assigning the new snapshot onto its draft (pure reduce → diffable
|
|
51
|
-
// patch). Kept beside the shared state rather than read back from it
|
|
52
|
-
// so reduction never depends on devframe's immutable accessors.
|
|
53
|
-
let snapshot = initRunState()
|
|
54
|
-
// `run()` emits `run:end` more than once (the normal end + a `finally`
|
|
55
|
-
// safety call) plus summary `run:status` lines AFTER the first
|
|
56
|
-
// `run:end`. Once the stream is closed, ignore everything — writing
|
|
57
|
-
// to a closed stream throws (StreamClosedError), and the run is over.
|
|
58
|
-
let closed = false
|
|
59
|
-
|
|
60
|
-
bus.subscribe((event) => {
|
|
61
|
-
if (closed) return
|
|
62
|
-
stream.write(toWireEvent(event))
|
|
63
|
-
snapshot = reduce(snapshot, event)
|
|
64
|
-
state.mutate((draft) => {
|
|
65
|
-
Object.assign(draft, snapshot)
|
|
66
|
-
})
|
|
67
|
-
if (event.kind === 'run:end') {
|
|
68
|
-
stream.close()
|
|
69
|
-
closed = true
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|