@vzn/vx 0.0.636 → 0.0.637
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 +39 -11
- package/docs/caching.md +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -63
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +103 -149
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +24 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +158 -63
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +113 -72
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +121 -95
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/init.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
// `vx init [--dry [--format json]] [--force] [--mjs] [--native|--keep]` — a workspace from nowhere
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `turbo()` or `nx()`, which read the runner's own config live (`adopt`).
|
|
1
|
+
// `vx init [--dry [--format json]] [--force] [--mjs] [--native|--keep]` — a workspace from nowhere.
|
|
2
|
+
// Core reads no package.json (M, 2026-10-14): a repo with one (package.json
|
|
3
|
+
// scripts, Turbo, Nx) is `@vzn/vx-migrate`'s, which maps it and installs
|
|
4
|
+
// what it writes (`migrate`); without one, the workspace file and an empty
|
|
5
|
+
// root project are written here, then the lock (`starter`).
|
|
7
6
|
// `vx init --plugin <seam>` writes a runnable plugin for one seam and its
|
|
8
7
|
// test instead (plugin-templates.ts, the examples the gate runs).
|
|
9
8
|
|
|
10
|
-
import { existsSync
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
11
10
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
12
11
|
import path from 'node:path'
|
|
12
|
+
import { gitTop, workspaceFileWritten } from '../handoff.js'
|
|
13
13
|
import { flagHint, formatValue, refuse, seeHelp } from './help.js'
|
|
14
|
+
import { lockAfterMigration } from './lock.js'
|
|
14
15
|
import { PLUGIN_TEMPLATES } from './plugin-templates.js'
|
|
15
16
|
import { isCompiledBinary } from './upgrade.js'
|
|
16
17
|
import { VERSION } from '../version.js'
|
|
17
|
-
import {
|
|
18
|
+
import { DOT_VX_GITATTRIBUTES, DOT_VX_GITIGNORE, UserError } from '../util/index.js'
|
|
18
19
|
import {
|
|
19
|
-
applyMigration,
|
|
20
|
-
discoverProjects,
|
|
21
20
|
findWorkspaceRoot,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
migrateScripts,
|
|
21
|
+
PROJECT_CONFIG_FILENAMES,
|
|
22
|
+
vxInvocation,
|
|
25
23
|
WORKSPACE_CONFIG_FILENAMES,
|
|
26
24
|
} from '../workspace/index.js'
|
|
27
25
|
|
|
@@ -97,94 +95,28 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
97
95
|
'VX_E_USAGE',
|
|
98
96
|
)
|
|
99
97
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
112
|
-
const nameless: string[] = []
|
|
113
|
-
const workspace = await loadWorkspace(root, reads)
|
|
114
|
-
const metas = await discoverProjects(workspace, nameless)
|
|
115
|
-
// `init` reads scripts only; a runner's own config beside them is the
|
|
116
|
-
// richer source (dependsOn, inputs, outputs) and was ignored without a
|
|
117
|
-
// word — the walkthrough on a Turbo repo (2026-09-09) got the scripts'
|
|
118
|
-
// TODOs and none of the edges turbo.json already declared.
|
|
119
|
-
// Turbo 2.5+ reads `turbo.jsonc` as well (item 938).
|
|
120
|
-
let turbo: string | undefined
|
|
121
|
-
for (const name of ['turbo.json', 'turbo.jsonc']) {
|
|
122
|
-
if (await Bun.file(path.join(root, name)).exists()) {
|
|
123
|
-
turbo = name
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// A Turbo or Nx repo already says its tasks: writing a config per package
|
|
128
|
-
// from the scripts dropped every edge turbo.json declares (the
|
|
129
|
-
// first-five-minutes walk, 2026-09-28). vx-migrate maps the runner's own
|
|
130
|
-
// config, so `vx init` is the one command (M, 2026-10-08); its `--keep`
|
|
131
|
-
// comes back here for the workspace file alone.
|
|
132
|
-
const runner: ['turbo' | 'nx', string] | undefined =
|
|
133
|
-
turbo !== undefined
|
|
134
|
-
? ['turbo', turbo]
|
|
135
|
-
: (await Bun.file(path.join(root, 'nx.json')).exists())
|
|
136
|
-
? ['nx', 'nx.json']
|
|
137
|
-
: undefined
|
|
138
|
-
if (runner !== undefined) {
|
|
139
|
-
if (parsed.mode === 'keep') return adopt(root, ...runner, parsed)
|
|
140
|
-
// A second `vx init` after the native migration refused to overwrite
|
|
141
|
-
// the configs it wrote, exit 1, after a package fetch: the first-run
|
|
142
|
-
// walk (2026-10-09). Set up is an answer, with the next step.
|
|
143
|
-
const existing = WORKSPACE_CONFIG_FILENAMES.find((f) => existsSync(path.join(root, f)))
|
|
144
|
-
if (
|
|
145
|
-
existing !== undefined &&
|
|
146
|
-
!parsed.force &&
|
|
147
|
-
!parsed.dry &&
|
|
148
|
-
parsed.mode === undefined &&
|
|
149
|
-
metas.some((m) => m.configPath !== null) &&
|
|
150
|
-
!readFileSync(path.join(root, existing), 'utf8').includes(`${runner[0]}(`)
|
|
151
|
-
) {
|
|
152
|
-
process.stdout.write(
|
|
153
|
-
`vx is already set up here (${existing} and the projects' vx.config files); --force regenerates them from ${runner[1]}.\n\nnext: ${adoptionNext(root, ...runner, ['@vzn/vx'])}\n`,
|
|
154
|
-
)
|
|
155
|
-
return 0
|
|
156
|
-
}
|
|
157
|
-
return migrate(root, parsed)
|
|
98
|
+
const cwd = process.cwd()
|
|
99
|
+
const marked = await findWorkspaceRoot(cwd).catch(() => undefined)
|
|
100
|
+
if (parsed.plugin !== undefined) return scaffoldPlugin(parsed.plugin, marked ?? cwd, parsed)
|
|
101
|
+
// The directories from here up to the workspace root, else the git
|
|
102
|
+
// top-level, else `/`: where a package manager's root or an installed
|
|
103
|
+
// vx-migrate may be.
|
|
104
|
+
const top = marked ?? (await gitTop(cwd))
|
|
105
|
+
const dirs: string[] = []
|
|
106
|
+
for (let d = cwd; ; d = path.dirname(d)) {
|
|
107
|
+
dirs.push(d)
|
|
108
|
+
if (d === top || path.dirname(d) === d) break
|
|
158
109
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
const unmapped = namelessNotes(root, nameless)
|
|
165
|
-
return applyMigration({
|
|
166
|
-
root,
|
|
167
|
-
metas,
|
|
168
|
-
plan: migrateScripts(
|
|
169
|
-
metas,
|
|
170
|
-
metas.some((m) => m.dir === root) ? undefined : await readRootManifest(root),
|
|
171
|
-
root,
|
|
172
|
-
),
|
|
173
|
-
source: 'package.json scripts',
|
|
174
|
-
verb: 'vx init',
|
|
175
|
-
notes: [...(await unreadWorkspaces(root, workspace.packageGlobs)), ...unmapped],
|
|
176
|
-
unmapped: unmapped.length > 0,
|
|
177
|
-
dry: parsed.dry,
|
|
178
|
-
json: parsed.json === true,
|
|
179
|
-
force: parsed.force,
|
|
180
|
-
init: true,
|
|
181
|
-
format: parsed.mjs ? 'mjs' : 'ts',
|
|
182
|
-
})
|
|
110
|
+
const manifest = dirs.some((d) => MANIFESTS.some((f) => existsSync(path.join(d, f))))
|
|
111
|
+
if (manifest) return migrate(dirs, parsed)
|
|
112
|
+
return starter(marked ?? cwd, top === null, parsed)
|
|
183
113
|
}
|
|
184
114
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
115
|
+
/** What makes a repo vx-migrate's to map: a package manager's root, a Turbo or Nx config. */
|
|
116
|
+
const MANIFESTS = ['package.json', 'pnpm-workspace.yaml', 'turbo.json', 'turbo.jsonc', 'nx.json']
|
|
117
|
+
|
|
118
|
+
/** Write `plugins/<seam>.ts` and its test under `root`: the workspace root, else the cwd. */
|
|
119
|
+
async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promise<number> {
|
|
188
120
|
const template = PLUGIN_TEMPLATES[seam]!
|
|
189
121
|
const files = [
|
|
190
122
|
[path.join('plugins', `${seam}.ts`), template.plugin],
|
|
@@ -204,338 +136,115 @@ async function scaffoldPlugin(seam: string, args: InitArgs): Promise<number> {
|
|
|
204
136
|
}
|
|
205
137
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
206
138
|
const workspaceFile =
|
|
207
|
-
WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? 'vx
|
|
139
|
+
WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? '.vx/workspace.ts'
|
|
208
140
|
process.stdout.write(
|
|
209
141
|
`${args.dry ? 'would write' : 'wrote'} ${files.map(([rel]) => rel).join(' and ')}\n` +
|
|
210
|
-
`Declare it in ${workspaceFile}: import { ${factory} } from '
|
|
142
|
+
`Declare it in ${workspaceFile}: import { ${factory} } from '../plugins/${seam}.ts', then plugins: [${factory}(…)]\n` +
|
|
211
143
|
`Test it: bun test plugins/${seam}.test.ts (needs @vzn/vx installed)\n`,
|
|
212
144
|
)
|
|
213
145
|
return 0
|
|
214
146
|
}
|
|
215
147
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*/
|
|
221
|
-
async function migrate(root: string, args: InitArgs): Promise<number> {
|
|
222
|
-
const flags = [
|
|
223
|
-
args.dry && '--dry',
|
|
224
|
-
args.force && '--force',
|
|
225
|
-
args.mjs && '--mjs',
|
|
226
|
-
args.mode === 'native' && '--native',
|
|
227
|
-
args.json === true && '--format=json',
|
|
228
|
-
].filter((f): f is string => typeof f === 'string')
|
|
229
|
-
return Bun.spawn(migrateCommand(root, flags), {
|
|
230
|
-
cwd: root,
|
|
231
|
-
stdio: ['inherit', 'inherit', 'inherit'],
|
|
232
|
-
// A compiled vx is a Bun runtime only when told so.
|
|
233
|
-
env: isCompiledBinary() ? { ...process.env, BUN_BE_BUN: '1' } : process.env,
|
|
234
|
-
}).exited
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function migrateCommand(root: string, flags: readonly string[]): string[] {
|
|
238
|
-
const installed = path.join(root, 'node_modules', '@vzn', 'vx-migrate', 'src', 'bin.ts')
|
|
239
|
-
if (existsSync(installed)) return [process.execPath, '--no-install', installed, ...flags]
|
|
240
|
-
// A source checkout is 0.0.0, which npm never had.
|
|
241
|
-
const version = VERSION === '0.0.0' ? 'latest' : VERSION
|
|
242
|
-
return [process.execPath, 'x', `@vzn/vx-migrate@${version}`, ...flags]
|
|
243
|
-
}
|
|
148
|
+
const WORKSPACE =
|
|
149
|
+
"import { defineWorkspace } from '@vzn/vx'\n\nexport default defineWorkspace({ plugins: [] })\n"
|
|
150
|
+
const PROJECT =
|
|
151
|
+
"import { defineProject } from '@vzn/vx'\n\nexport default defineProject({ tasks: {} })\n"
|
|
244
152
|
|
|
245
153
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
154
|
+
* No package.json: `.vx/workspace.ts` declaring no plugin and a root
|
|
155
|
+
* `vx.config.ts` with no task, then the lock. `@vzn/vx` resolves to the
|
|
156
|
+
* running vx (core-alias.ts), so the two load with nothing installed.
|
|
157
|
+
* A file already here is kept; `--force` replaces it.
|
|
249
158
|
*/
|
|
250
|
-
async function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`vx init: ${existing} exists; add ${runner}() from @vzn/vx-migrate to its plugins, or --force replaces it`,
|
|
271
|
-
)
|
|
159
|
+
async function starter(root: string, noGit: boolean, args: InitArgs): Promise<number> {
|
|
160
|
+
if (args.mode !== undefined) {
|
|
161
|
+
throw new UserError(
|
|
162
|
+
`vx init: --${args.mode} answers a Turbo or Nx repo's question, and there is no package.json here`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
const ext = args.mjs ? 'mjs' : 'ts'
|
|
166
|
+
const config = `vx.config.${ext}`
|
|
167
|
+
const files: { path: string; contents: string }[] = []
|
|
168
|
+
const kept: string[] = []
|
|
169
|
+
const replaced: string[] = []
|
|
170
|
+
for (const [names, name, contents] of [
|
|
171
|
+
[WORKSPACE_CONFIG_FILENAMES, `.vx/workspace.${ext}`, WORKSPACE],
|
|
172
|
+
[PROJECT_CONFIG_FILENAMES, config, PROJECT],
|
|
173
|
+
] as const) {
|
|
174
|
+
const existing = names.find((n) => existsSync(path.join(root, n)))
|
|
175
|
+
if (existing !== undefined && !args.force) kept.push(existing)
|
|
176
|
+
else {
|
|
177
|
+
files.push({ path: name, contents })
|
|
178
|
+
if (existing !== undefined && existing !== name) replaced.push(existing)
|
|
272
179
|
}
|
|
273
|
-
} else if (args.dry) {
|
|
274
|
-
if (args.json !== true) process.stdout.write(`── ${name} ──\n${text}\n`)
|
|
275
|
-
} else {
|
|
276
|
-
if (existing !== undefined && existing !== name) await unlink(path.join(root, existing))
|
|
277
|
-
await Bun.write(path.join(root, name), text)
|
|
278
180
|
}
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
? 'nx.json connects Nx Cloud, whose cache vx cannot share: runs cache on this machine (nxCache() serves a self-hosted Nx cache).\n'
|
|
289
|
-
: ''
|
|
290
|
-
: kept && !readFileSync(path.join(root, existing), 'utf8').includes(`${cache}(`)
|
|
291
|
-
? `${remote}: add ${cache}() from @vzn/vx-migrate to its plugins and vx shares that remote cache.\n`
|
|
292
|
-
: kept
|
|
293
|
-
? ''
|
|
294
|
-
: `${cache}(): ${remote}, so vx shares that remote cache (inert where the variable is unset).\n`
|
|
181
|
+
// vx's own housekeeping, as a run's first cache open and `vx lock` write them.
|
|
182
|
+
for (const [name, contents] of [
|
|
183
|
+
['.vx/.gitignore', DOT_VX_GITIGNORE],
|
|
184
|
+
['.vx/.gitattributes', DOT_VX_GITATTRIBUTES],
|
|
185
|
+
] as const) {
|
|
186
|
+
if (!existsSync(path.join(root, name))) files.push({ path: name, contents })
|
|
187
|
+
}
|
|
188
|
+
const vx = vxInvocation(process.env['npm_config_user_agent'], false)
|
|
189
|
+
const next = `${noGit ? 'git init, ' : ''}declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then ${vx} lock && ${vx} run <task>`
|
|
295
190
|
if (args.json === true) {
|
|
296
191
|
process.stdout.write(
|
|
297
|
-
`${JSON.stringify({
|
|
298
|
-
dry: true,
|
|
299
|
-
source,
|
|
300
|
-
files: kept ? [] : [{ path: name, contents: text }],
|
|
301
|
-
kept: kept ? [existing] : [],
|
|
302
|
-
replaced: !kept && existing !== undefined && existing !== name ? [existing] : [],
|
|
303
|
-
todos: [],
|
|
304
|
-
notes: cacheLine === '' ? [] : [cacheLine.trimEnd()],
|
|
305
|
-
next: adoptionNext(root, runner, source),
|
|
306
|
-
})}\n`,
|
|
192
|
+
`${JSON.stringify({ dry: true, source: 'no package.json', files, kept, replaced, todos: [], notes: [], next })}\n`,
|
|
307
193
|
)
|
|
308
194
|
return 0
|
|
309
195
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
196
|
+
const report = [
|
|
197
|
+
'vx init: no package.json, so no scripts to map: a workspace with one project, the root.',
|
|
198
|
+
]
|
|
199
|
+
const named = files.map((f) => f.path).join(', ')
|
|
200
|
+
if (files.length > 0)
|
|
201
|
+
report.push(args.dry ? `would write ${named} (dry run, nothing written).` : `wrote ${named}.`)
|
|
202
|
+
if (kept.length > 0) report.push(`kept ${kept.join(', ')}.`)
|
|
203
|
+
if (replaced.length > 0)
|
|
204
|
+
report.push(`${args.dry ? 'would replace' : 'replaced'} ${replaced.join(', ')}.`)
|
|
205
|
+
if (args.dry) {
|
|
206
|
+
for (const f of files) process.stdout.write(`── ${f.path} ──\n${f.contents}\n`)
|
|
207
|
+
process.stdout.write(`${report.join('\n')}\n\nnext: ${next}\n`)
|
|
208
|
+
return 0
|
|
209
|
+
}
|
|
210
|
+
for (const f of files) await Bun.write(path.join(root, f.path), f.contents)
|
|
211
|
+
for (const f of replaced) await unlink(path.join(root, f))
|
|
212
|
+
// The lock that follows loads the workspace file just written.
|
|
213
|
+
workspaceFileWritten()
|
|
214
|
+
process.stdout.write(`${report.join('\n')}\n\n`)
|
|
215
|
+
const locked = (await lockAfterMigration(root)) === 0
|
|
216
|
+
process.stdout.write(`\nnext: ${locked ? next : `fix the above and run vx lock, then ${next}`}\n`)
|
|
314
217
|
return 0
|
|
315
218
|
}
|
|
316
219
|
|
|
317
|
-
const REMOTE_CACHE = { turbo: 'turboCache', nx: 'nxCache' } as const
|
|
318
|
-
|
|
319
220
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
221
|
+
* A repo with a package.json is `@vzn/vx-migrate`'s: the installed one,
|
|
222
|
+
* else this version's through `bun x`. It finds the root itself, asks a
|
|
223
|
+
* terminal native or keep in a Turbo or Nx repo, and installs what it writes.
|
|
323
224
|
*/
|
|
324
|
-
function
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
set(json?.nxCloudId) ||
|
|
339
|
-
set(json?.nxCloudAccessToken) ||
|
|
340
|
-
(typeof runner === 'string' && /(^|\/)nx-cloud$/.test(runner))
|
|
341
|
-
)
|
|
225
|
+
async function migrate(dirs: readonly string[], args: InitArgs): Promise<number> {
|
|
226
|
+
const flags = [
|
|
227
|
+
args.dry && '--dry',
|
|
228
|
+
args.force && '--force',
|
|
229
|
+
args.mjs && '--mjs',
|
|
230
|
+
args.mode !== undefined && `--${args.mode}`,
|
|
231
|
+
args.json === true && '--format=json',
|
|
232
|
+
].filter((f): f is string => typeof f === 'string')
|
|
233
|
+
return Bun.spawn(migrateCommand(dirs, flags), {
|
|
234
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
235
|
+
// A compiled vx is a Bun runtime only when told so.
|
|
236
|
+
env: isCompiledBinary() ? { ...process.env, BUN_BE_BUN: '1' } : process.env,
|
|
237
|
+
}).exited
|
|
342
238
|
}
|
|
343
239
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Where the repo shows a remote cache its runner uses, or undefined: turbo.json's
|
|
351
|
-
* enabled `remoteCache`, a `turbo link`ed `.turbo/config.json`, or a CI file that sets the runner's cache variable (a
|
|
352
|
-
* local repo holds no token; CI does).
|
|
353
|
-
*/
|
|
354
|
-
function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undefined {
|
|
355
|
-
if (runner === 'turbo') {
|
|
356
|
-
for (const f of ['turbo.json', 'turbo.jsonc']) {
|
|
357
|
-
let rc: unknown
|
|
358
|
-
try {
|
|
359
|
-
rc = (
|
|
360
|
-
Bun.JSONC.parse(readFileSync(path.join(root, f), 'utf8')) as { remoteCache?: unknown }
|
|
361
|
-
)?.remoteCache
|
|
362
|
-
} catch {
|
|
363
|
-
continue
|
|
364
|
-
}
|
|
365
|
-
if (typeof rc === 'object' && rc !== null) {
|
|
366
|
-
// Turned off in turbo.json is off, whatever CI sets.
|
|
367
|
-
if ((rc as { enabled?: unknown }).enabled === false) return undefined
|
|
368
|
-
return `${f} names a remoteCache`
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// `turbo link` writes the team here (gitignored, on the machine that
|
|
372
|
-
// linked); turboCache() reads it, so a linked repo has a remote cache.
|
|
373
|
-
let linked: unknown
|
|
374
|
-
try {
|
|
375
|
-
linked = JSON.parse(readFileSync(path.join(root, '.turbo', 'config.json'), 'utf8'))
|
|
376
|
-
} catch {}
|
|
377
|
-
if (
|
|
378
|
-
typeof linked === 'object' &&
|
|
379
|
-
linked !== null &&
|
|
380
|
-
Object.entries(linked).some(
|
|
381
|
-
([k, v]) => /^(teamid|teamslug|token)$/i.test(k) && typeof v === 'string' && v !== '',
|
|
382
|
-
)
|
|
383
|
-
) {
|
|
384
|
-
return '.turbo/config.json links a team (turbo link)'
|
|
385
|
-
}
|
|
240
|
+
export function migrateCommand(dirs: readonly string[], flags: readonly string[]): string[] {
|
|
241
|
+
for (const d of dirs) {
|
|
242
|
+
const installed = path.join(d, 'node_modules', '@vzn', 'vx-migrate', 'src', 'bin.ts')
|
|
243
|
+
if (existsSync(installed)) return [process.execPath, '--no-install', installed, ...flags]
|
|
386
244
|
}
|
|
387
|
-
const variable = REMOTE_CACHE_ENV[runner]
|
|
388
|
-
// A line that sets it (`TURBO_TOKEN: …`, `TURBO_TOKEN=…`), not one that
|
|
389
|
-
// names it: a comment or a doc line counted as "sets" (J's lead).
|
|
390
|
-
const sets = new RegExp(`^(?!\\s*#).*\\b${variable}\\s*[:=]`, 'm')
|
|
391
|
-
const workflows = new Bun.Glob('.github/workflows/*.{yml,yaml}')
|
|
392
|
-
const files = [...workflows.scanSync({ cwd: root, dot: true })].sort().concat(CI_FILES)
|
|
393
|
-
for (const f of files) {
|
|
394
|
-
let text: string
|
|
395
|
-
try {
|
|
396
|
-
text = readFileSync(path.join(root, f), 'utf8')
|
|
397
|
-
} catch {
|
|
398
|
-
continue
|
|
399
|
-
}
|
|
400
|
-
if (sets.test(text)) return `${f} sets ${variable}`
|
|
401
|
-
}
|
|
402
|
-
return undefined
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm'
|
|
406
|
-
|
|
407
|
-
const LOCKFILES: ReadonlyArray<[string, PackageManager]> = [
|
|
408
|
-
['pnpm-lock.yaml', 'pnpm'],
|
|
409
|
-
['yarn.lock', 'yarn'],
|
|
410
|
-
['bun.lock', 'bun'],
|
|
411
|
-
['bun.lockb', 'bun'],
|
|
412
|
-
['package-lock.json', 'npm'],
|
|
413
|
-
]
|
|
414
|
-
|
|
415
|
-
const INSTALL: Record<PackageManager, string> = {
|
|
416
|
-
pnpm: 'pnpm add -D -w',
|
|
417
|
-
yarn: 'yarn add -D',
|
|
418
|
-
bun: 'bun add -d',
|
|
419
|
-
npm: 'npm install -D',
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const EXEC: Record<PackageManager, string> = { pnpm: 'pnpm', yarn: 'yarn', bun: 'bunx', npm: 'npx' }
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Install what the workspace file imports, if missing, then run the repo's
|
|
426
|
-
* build. Pinned to this vx's version: every @vzn package releases in
|
|
427
|
-
* lockstep, and `latest` beside an older vx mismatched them (agent trial 2).
|
|
428
|
-
*/
|
|
429
|
-
export function adoptionNext(
|
|
430
|
-
root: string,
|
|
431
|
-
runner: 'turbo' | 'nx',
|
|
432
|
-
source: string,
|
|
433
|
-
needs: readonly string[] = ['@vzn/vx', '@vzn/vx-migrate'],
|
|
434
|
-
version: string = VERSION,
|
|
435
|
-
): string {
|
|
436
|
-
const pm = LOCKFILES.find(([f]) => existsSync(path.join(root, f)))?.[1] ?? 'npm'
|
|
437
|
-
const missing = needs.filter(
|
|
438
|
-
(p) => !existsSync(path.join(root, 'node_modules', p, 'package.json')),
|
|
439
|
-
)
|
|
440
|
-
// A global vx (no runner started this one) runs the workspace's plugins as they are.
|
|
441
|
-
const vx = process.env['npm_config_user_agent'] === undefined ? 'vx' : `${EXEC[pm]} vx`
|
|
442
|
-
const task = firstTask(path.join(root, source), runner)
|
|
443
|
-
const run = `${vx} run ${task} --all`
|
|
444
|
-
if (missing.length === 0) return run
|
|
445
|
-
// Yarn 1 refuses `add` at a workspace root without `-W`; Berry has no such
|
|
446
|
-
// flag and refuses it. A Berry lockfile carries `__metadata`.
|
|
447
|
-
const install =
|
|
448
|
-
pm === 'yarn' && !readText(path.join(root, 'yarn.lock')).includes('__metadata:')
|
|
449
|
-
? `${INSTALL.yarn} -W`
|
|
450
|
-
: INSTALL[pm]
|
|
451
245
|
// A source checkout is 0.0.0, which npm never had.
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
function readText(file: string): string {
|
|
457
|
-
try {
|
|
458
|
-
return readFileSync(file, 'utf8')
|
|
459
|
-
} catch {
|
|
460
|
-
return ''
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** `build` when the runner's config names it, else the first task it names. */
|
|
465
|
-
function firstTask(file: string, runner: 'turbo' | 'nx'): string {
|
|
466
|
-
let text: string
|
|
467
|
-
try {
|
|
468
|
-
text = readFileSync(file, 'utf8')
|
|
469
|
-
} catch {
|
|
470
|
-
return 'build'
|
|
471
|
-
}
|
|
472
|
-
if (/"build"\s*:/.test(text)) return 'build'
|
|
473
|
-
const field = runner === 'turbo' ? '(?:tasks|pipeline)' : 'targetDefaults'
|
|
474
|
-
const first = new RegExp(`"${field}"\\s*:\\s*\\{\\s*"([^"]+)"`).exec(text)?.[1]
|
|
475
|
-
return first ?? 'build'
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* A member whose package.json has no `name` is no project, so its scripts
|
|
480
|
-
* map to nothing, and init said nothing of them (D-106).
|
|
481
|
-
*/
|
|
482
|
-
function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
483
|
-
const withScripts = dirs
|
|
484
|
-
.filter((dir) => {
|
|
485
|
-
const scripts = (
|
|
486
|
-
JSON.parse(readText(path.join(dir, 'package.json')).replace(/^\uFEFF/, '') || '{}') as {
|
|
487
|
-
scripts?: unknown
|
|
488
|
-
}
|
|
489
|
-
).scripts
|
|
490
|
-
return (
|
|
491
|
-
typeof scripts === 'object' &&
|
|
492
|
-
scripts !== null &&
|
|
493
|
-
Object.values(scripts).some((v) => typeof v === 'string' && v !== '')
|
|
494
|
-
)
|
|
495
|
-
})
|
|
496
|
-
.map((dir) => relPosix(root, dir))
|
|
497
|
-
.sort()
|
|
498
|
-
// The root's own manifest is the empty relative path: a nameless
|
|
499
|
-
// single-package repo read `not mapped: — its package.json…` (M-54).
|
|
500
|
-
.map((rel) => (rel === '' ? 'the root' : rel))
|
|
501
|
-
if (withScripts.length === 0) return []
|
|
502
|
-
const shown =
|
|
503
|
-
withScripts.slice(0, 3).join(', ') +
|
|
504
|
-
(withScripts.length > 3 ? `, and ${withScripts.length - 3} more` : '')
|
|
505
|
-
return [
|
|
506
|
-
`not mapped: ${shown} — ${withScripts.length === 1 ? 'its package.json has' : 'their package.json files have'} no "name", and vx names a project by it; give ${withScripts.length === 1 ? 'it one' : 'each one'} and run \`vx init\` again`,
|
|
507
|
-
]
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* `pnpm-workspace.yaml`'s `packages` decides the members, as pnpm reads it,
|
|
512
|
-
* and a root `workspaces` that lists other globs was dropped without a word:
|
|
513
|
-
* a bun or npm repo with a stale yaml lost those members (M-51).
|
|
514
|
-
*/
|
|
515
|
-
async function unreadWorkspaces(root: string, globs: readonly string[]): Promise<string[]> {
|
|
516
|
-
if (!existsSync(path.join(root, 'pnpm-workspace.yaml'))) return []
|
|
517
|
-
const ws = (await readRootManifest(root))['workspaces']
|
|
518
|
-
const list = Array.isArray(ws)
|
|
519
|
-
? ws
|
|
520
|
-
: typeof ws === 'object' &&
|
|
521
|
-
ws !== null &&
|
|
522
|
-
Array.isArray((ws as { packages?: unknown }).packages)
|
|
523
|
-
? (ws as { packages: unknown[] }).packages
|
|
524
|
-
: undefined
|
|
525
|
-
if (list === undefined) return []
|
|
526
|
-
const pkg = list.filter((g): g is string => typeof g === 'string')
|
|
527
|
-
if ([...pkg].sort().join('\0') === [...globs].sort().join('\0')) return []
|
|
528
|
-
return [
|
|
529
|
-
`pnpm-workspace.yaml's \`packages\` lists the members, as pnpm reads them; package.json's \`workspaces\` (${pkg.map((g) => `"${g}"`).join(', ')}) is not read: if bun, npm or yarn installs this repo, copy those globs into pnpm-workspace.yaml's \`packages\`, or delete that key so package.json decides`,
|
|
530
|
-
]
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** The root package.json as an object, `{}` when unreadable or not one. */
|
|
534
|
-
async function readRootManifest(root: string): Promise<Record<string, unknown>> {
|
|
535
|
-
const pj: unknown = await Bun.file(path.join(root, 'package.json'))
|
|
536
|
-
.json()
|
|
537
|
-
.catch(() => ({}))
|
|
538
|
-
return typeof pj === 'object' && pj !== null && !Array.isArray(pj)
|
|
539
|
-
? (pj as Record<string, unknown>)
|
|
540
|
-
: {}
|
|
246
|
+
const version = VERSION === '0.0.0' ? 'latest' : VERSION
|
|
247
|
+
// Bun's own install output ("Slow filesystem detected…") in a pnpm or npm
|
|
248
|
+
// repo reads as a stranger speaking (Growth's first-run funnel F2).
|
|
249
|
+
return [process.execPath, 'x', '--silent', `@vzn/vx-migrate@${version}`, ...flags]
|
|
541
250
|
}
|