@vzn/vx 0.0.645 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cli/init.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// `vx init [--dry [--format json]] [--force] [--mjs] [--native|--keep]` — a workspace from nowhere.
|
|
2
2
|
// It installs vx globally and, where a package.json is, as its devDependency
|
|
3
|
-
// (
|
|
3
|
+
// (2026-10-20). A Turbo or Nx repo is `@vzn/vx-migrate`'s, which maps it
|
|
4
4
|
// and installs what it writes (`migrate`); any other gets the workspace file
|
|
5
5
|
// and an empty root project, then the lock (`starter`).
|
|
6
|
-
// `vx init --plugin <
|
|
6
|
+
// `vx init --plugin <hook>` writes a runnable plugin for one hook and its
|
|
7
7
|
// test instead (plugin-templates.ts, the examples the gate runs).
|
|
8
8
|
|
|
9
|
-
import { existsSync } from 'node:fs'
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
10
10
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { gitTop, workspaceFileWritten } from '../handoff.js'
|
|
@@ -22,11 +22,23 @@ import {
|
|
|
22
22
|
type ManagedInstall,
|
|
23
23
|
} from './upgrade.js'
|
|
24
24
|
import { VERSION } from '../version.js'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
detectColors,
|
|
27
|
+
DOT_VX_GITATTRIBUTES,
|
|
28
|
+
DOT_VX_GITIGNORE,
|
|
29
|
+
paint,
|
|
30
|
+
refuseSymlink,
|
|
31
|
+
sayNotice,
|
|
32
|
+
SUCCESS,
|
|
33
|
+
ui,
|
|
34
|
+
UserError,
|
|
35
|
+
writeNoFollow,
|
|
36
|
+
} from '../util/index.js'
|
|
26
37
|
import {
|
|
27
38
|
findWorkspaceRoot,
|
|
28
39
|
PROJECT_CONFIG_FILENAMES,
|
|
29
|
-
|
|
40
|
+
renderWorkspace,
|
|
41
|
+
starterProject,
|
|
30
42
|
WORKSPACE_CONFIG_FILENAMES,
|
|
31
43
|
} from '../workspace/index.js'
|
|
32
44
|
|
|
@@ -35,7 +47,7 @@ interface InitArgs {
|
|
|
35
47
|
force: boolean
|
|
36
48
|
/** `vx.config.mjs` instead of `.ts` — see `ApplyMigrationArgs.format`. */
|
|
37
49
|
mjs: boolean
|
|
38
|
-
/** `--plugin <
|
|
50
|
+
/** `--plugin <hook>`: scaffold that hook's plugin instead of the workspace. */
|
|
39
51
|
plugin?: string
|
|
40
52
|
/** `--native` / `--keep` in a Turbo or Nx repo; unset, vx-migrate asks a terminal. */
|
|
41
53
|
mode?: 'native' | 'keep'
|
|
@@ -54,7 +66,16 @@ export function parseInitArgs(args: readonly string[]): InitArgs {
|
|
|
54
66
|
if (seam === undefined || !Object.hasOwn(PLUGIN_TEMPLATES, seam)) {
|
|
55
67
|
return {
|
|
56
68
|
...out,
|
|
57
|
-
error:
|
|
69
|
+
error:
|
|
70
|
+
seam === undefined || seam === ''
|
|
71
|
+
? `--plugin requires a value: one of ${seams}${seeHelp('init')}`
|
|
72
|
+
: `--plugin must be one of ${seams} (got ${seam})${seeHelp('init')}`,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (out.plugin !== undefined && out.plugin !== seam) {
|
|
76
|
+
return {
|
|
77
|
+
...out,
|
|
78
|
+
error: `--plugin takes one hook; run vx init --plugin once per hook${seeHelp('init')}`,
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
out.plugin = seam
|
|
@@ -117,16 +138,47 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
117
138
|
if (dirs.some((d) => TOOL_CONFIGS.some((f) => existsSync(path.join(d, f))))) {
|
|
118
139
|
return migrate(dirs, parsed)
|
|
119
140
|
}
|
|
120
|
-
|
|
121
|
-
const pkgRoot = dirs.findLast((d) => existsSync(path.join(d, 'package.json'))) ?? null
|
|
141
|
+
const pkgRoot = packageRoot(dirs, top !== null)
|
|
122
142
|
return starter(marked ?? pkgRoot ?? cwd, pkgRoot, top === null, parsed)
|
|
123
143
|
}
|
|
124
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Inside git, the topmost package.json: a monorepo's root, wherever below
|
|
147
|
+
* it init ran. Outside git `dirs` reach `/`, and the topmost took a stray
|
|
148
|
+
* package.json in the home directory (2026-10-28); there it is the nearest
|
|
149
|
+
* one, or the outermost above it that declares a workspace.
|
|
150
|
+
*/
|
|
151
|
+
function packageRoot(dirs: readonly string[], inGit: boolean): string | null {
|
|
152
|
+
const has = (d: string, f: string): boolean => existsSync(path.join(d, f))
|
|
153
|
+
if (inGit) return dirs.findLast((d) => has(d, 'package.json')) ?? null
|
|
154
|
+
const at = dirs.findIndex((d) => has(d, 'package.json'))
|
|
155
|
+
if (at === -1) return null
|
|
156
|
+
const outer = dirs
|
|
157
|
+
.slice(at + 1)
|
|
158
|
+
.findLast(
|
|
159
|
+
(d) => has(d, 'package.json') && (has(d, 'pnpm-workspace.yaml') || declaresWorkspaces(d)),
|
|
160
|
+
)
|
|
161
|
+
return outer ?? dirs[at]!
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function declaresWorkspaces(dir: string): boolean {
|
|
165
|
+
try {
|
|
166
|
+
const pj = JSON.parse(
|
|
167
|
+
readFileSync(path.join(dir, 'package.json'), 'utf8').replace(/^\uFEFF/, ''),
|
|
168
|
+
) as {
|
|
169
|
+
workspaces?: unknown
|
|
170
|
+
}
|
|
171
|
+
return pj.workspaces !== undefined
|
|
172
|
+
} catch {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
/** What makes a repo vx-migrate's to map: a Turbo or Nx config. */
|
|
126
178
|
const TOOL_CONFIGS = ['turbo.json', 'turbo.jsonc', 'nx.json']
|
|
127
179
|
|
|
128
180
|
/**
|
|
129
|
-
* The installs `vx init` runs (
|
|
181
|
+
* The installs `vx init` runs (2026-10-20): vx at `version` globally,
|
|
130
182
|
* with the manager that started init, so a bare `vx` is on PATH, unless
|
|
131
183
|
* the global vx already answers `version`; and, where `pkgRoot` has a
|
|
132
184
|
* package.json, as its devDependency with the repo's own manager.
|
|
@@ -137,10 +189,13 @@ export function initInstalls(
|
|
|
137
189
|
pkgRoot: string | null,
|
|
138
190
|
globalAt: string | undefined,
|
|
139
191
|
execPath = process.execPath,
|
|
192
|
+
plugins: readonly string[] = [],
|
|
140
193
|
): { argv: string[]; cwd: string | null }[] {
|
|
141
194
|
const out: { argv: string[]; cwd: string | null }[] = []
|
|
142
195
|
if (globalAt !== version) {
|
|
143
|
-
|
|
196
|
+
// Yarn 2+ has no `yarn global`: Bun, which vx needs anyway, as with no runner.
|
|
197
|
+
const ua = /^(npm|pnpm|yarn|bun)\/(\d+)/.exec(userAgent ?? '')
|
|
198
|
+
const runner = ua === null || (ua[1] === 'yarn' && Number(ua[2]) >= 2) ? 'bun' : ua[1]!
|
|
144
199
|
const manager = runner as ManagedInstall['manager']
|
|
145
200
|
out.push({ argv: upgradeArgv({ manager, scope: 'global', cwd: '' }, version), cwd: null })
|
|
146
201
|
}
|
|
@@ -150,6 +205,7 @@ export function initInstalls(
|
|
|
150
205
|
scope: 'project',
|
|
151
206
|
cwd: pkgRoot,
|
|
152
207
|
field: 'devDependencies',
|
|
208
|
+
plugins: [...plugins],
|
|
153
209
|
}
|
|
154
210
|
out.push({ argv: upgradeArgv(install, version), cwd: pkgRoot })
|
|
155
211
|
}
|
|
@@ -168,11 +224,30 @@ function globalVersion(): string | undefined {
|
|
|
168
224
|
return undefined
|
|
169
225
|
}
|
|
170
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The package a manager's output says the registry answered 404 for, as
|
|
229
|
+
* `name@version`: a release's platform tarball stays unserved for minutes
|
|
230
|
+
* after publish (v0.0.646, 2026-10-23). bun: `GET <url> - 404`; npm:
|
|
231
|
+
* `404 Not Found - GET <url>`; pnpm: `GET <url>: Not Found - 404`.
|
|
232
|
+
*/
|
|
233
|
+
export function notServed(output: string): string | undefined {
|
|
234
|
+
for (const line of output.split('\n')) {
|
|
235
|
+
if (!line.includes('404')) continue
|
|
236
|
+
const m = /https?:\/\/\S+?\/((?:@[^/\s]+\/)?[^/\s]+)\/-\/[^/\s]+?-(\d[^/\s]*?)\.tgz/.exec(line)
|
|
237
|
+
if (m !== null) return `${decodeURIComponent(m[1]!)}@${m[2]}`
|
|
238
|
+
}
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
171
242
|
/**
|
|
172
243
|
* Run `initInstalls`, in order. A source checkout (0.0.0, which npm never
|
|
173
244
|
* had) installs nothing: the release would replace the code under test.
|
|
174
245
|
*/
|
|
175
|
-
async function install(
|
|
246
|
+
async function install(
|
|
247
|
+
pkgRoot: string | null,
|
|
248
|
+
dry: boolean,
|
|
249
|
+
plugins: readonly string[] = [],
|
|
250
|
+
): Promise<string[]> {
|
|
176
251
|
if (VERSION === '0.0.0') return []
|
|
177
252
|
const version = await targetVersion(undefined, 'vx init')
|
|
178
253
|
const steps = initInstalls(
|
|
@@ -180,6 +255,8 @@ async function install(pkgRoot: string | null, dry: boolean): Promise<string[]>
|
|
|
180
255
|
process.env['npm_config_user_agent'],
|
|
181
256
|
pkgRoot,
|
|
182
257
|
globalVersion(),
|
|
258
|
+
process.execPath,
|
|
259
|
+
plugins,
|
|
183
260
|
)
|
|
184
261
|
if (dry) return steps.map((s) => s.argv.join(' '))
|
|
185
262
|
for (const s of steps) {
|
|
@@ -193,22 +270,62 @@ async function install(pkgRoot: string | null, dry: boolean): Promise<string[]>
|
|
|
193
270
|
'out',
|
|
194
271
|
ui.text([ui.cmd(s.argv.join(' ')), ui.dim(s.cwd === null ? '' : ` in ${s.cwd}`)]),
|
|
195
272
|
)
|
|
196
|
-
const proc = Bun.
|
|
273
|
+
const proc = Bun.spawn({
|
|
197
274
|
cmd: [bin, ...s.argv.slice(1)],
|
|
198
275
|
cwd: s.cwd ?? process.cwd(),
|
|
199
|
-
stdio: ['inherit', 'inherit', '
|
|
276
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
200
277
|
})
|
|
201
|
-
|
|
202
|
-
|
|
278
|
+
// Shown as it comes, and kept: a registry 404 is the cause to name.
|
|
279
|
+
let said = ''
|
|
280
|
+
const decoder = new TextDecoder()
|
|
281
|
+
for await (const chunk of proc.stderr) {
|
|
282
|
+
process.stderr.write(chunk)
|
|
283
|
+
said += decoder.decode(chunk, { stream: true })
|
|
284
|
+
}
|
|
285
|
+
const code = await proc.exited
|
|
286
|
+
if (code !== 0) {
|
|
287
|
+
const missing = notServed(said)
|
|
288
|
+
throw new UserError(
|
|
289
|
+
missing === undefined
|
|
290
|
+
? `vx init: ${s.argv.join(' ')} failed (exit ${code})`
|
|
291
|
+
: `vx init: the npm registry does not serve ${missing} yet (404), as a release can take minutes to reach it; run bunx @vzn/vx init again`,
|
|
292
|
+
)
|
|
203
293
|
}
|
|
204
294
|
}
|
|
205
295
|
return steps.map((s) => s.argv.join(' '))
|
|
206
296
|
}
|
|
207
297
|
|
|
298
|
+
const PLUGINS_MANIFEST = `${JSON.stringify({ name: 'workspace-plugins', private: true }, null, 2)}\n`
|
|
299
|
+
|
|
300
|
+
/** Whether the nearest package.json above `dir` has a name, as `definePlugin` needs. */
|
|
301
|
+
function ownerNamed(dir: string): boolean {
|
|
302
|
+
for (let d = dir; ; d = path.dirname(d)) {
|
|
303
|
+
let text: string
|
|
304
|
+
try {
|
|
305
|
+
text = readFileSync(path.join(d, 'package.json'), 'utf8').replace(/^/, '')
|
|
306
|
+
} catch {
|
|
307
|
+
if (path.dirname(d) === d) return false
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const name = (JSON.parse(text) as { name?: unknown } | null)?.name
|
|
312
|
+
return typeof name === 'string' && name !== ''
|
|
313
|
+
} catch {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
208
319
|
/** Write `plugins/<seam>.ts` and its test under `root`: the workspace root, else the cwd. */
|
|
209
320
|
async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promise<number> {
|
|
210
321
|
const template = PLUGIN_TEMPLATES[seam]!
|
|
322
|
+
// `definePlugin` names the plugin by the nearest package.json, and a
|
|
323
|
+
// monorepo root often has no name: the plugin refused to load.
|
|
324
|
+
const manifest: [string, string][] = ownerNamed(path.join(root, 'plugins'))
|
|
325
|
+
? []
|
|
326
|
+
: [[path.join('plugins', 'package.json'), PLUGINS_MANIFEST]]
|
|
211
327
|
const files = [
|
|
328
|
+
...manifest,
|
|
212
329
|
[path.join('plugins', `${seam}.ts`), template.plugin],
|
|
213
330
|
[path.join('plugins', `${seam}.test.ts`), template.test],
|
|
214
331
|
] as const
|
|
@@ -221,24 +338,69 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
221
338
|
}
|
|
222
339
|
}
|
|
223
340
|
if (!args.dry) {
|
|
224
|
-
await mkdir(path.join(root, 'plugins'), { recursive: true })
|
|
341
|
+
await mkdir(path.join(root, 'plugins'), { recursive: true }).catch(
|
|
342
|
+
(err: NodeJS.ErrnoException) => {
|
|
343
|
+
if (err.code !== 'EEXIST' && err.code !== 'ENOTDIR') throw err
|
|
344
|
+
throw new UserError(
|
|
345
|
+
`vx init: cannot write ${files[0][0]}: plugins is a file, not a directory; move it and re-run`,
|
|
346
|
+
)
|
|
347
|
+
},
|
|
348
|
+
)
|
|
225
349
|
for (const [rel, text] of files) await Bun.write(path.join(root, rel), text)
|
|
226
350
|
}
|
|
227
351
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
228
352
|
const workspaceFile =
|
|
229
353
|
WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? '.vx/workspace.ts'
|
|
354
|
+
const c = detectColors(process.stdout)
|
|
355
|
+
const dim = (t: string): string => paint('', t, c, { dim: true })
|
|
356
|
+
const bold = (t: string): string => paint('', t, c, { bold: true })
|
|
230
357
|
process.stdout.write(
|
|
231
|
-
`${args.dry ? 'would write' : 'wrote'} ${files.map(([rel]) => rel).join(' and ')}\n` +
|
|
232
|
-
|
|
233
|
-
|
|
358
|
+
`${paint(SUCCESS, args.dry ? 'would write' : 'wrote', c, { bold: true })} ${files.map(([rel]) => bold(rel)).join(' and ')}\n` +
|
|
359
|
+
`${dim('Declare it in')} ${workspaceFile}${dim(':')} ${bold(`import { ${factory} } from '../plugins/${seam}.ts'`)}${dim(', then')} ${bold(`plugins: [${factory}(…)]`)}\n` +
|
|
360
|
+
`${dim('Test it:')} ${bold(`bun test plugins/${seam}.test.ts`)} ${dim('(needs @vzn/vx installed)')}\n`,
|
|
234
361
|
)
|
|
235
362
|
return 0
|
|
236
363
|
}
|
|
237
364
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
365
|
+
/**
|
|
366
|
+
* The `@vzn/vx-lockfile` plugin the lockfile at `pkgRoot` calls for: without
|
|
367
|
+
* it a JS workspace has no project edges, and the first `^build` a user
|
|
368
|
+
* writes is refused at `vx lock`.
|
|
369
|
+
*/
|
|
370
|
+
function lockfilePlugin(pkgRoot: string | null): 'pnpm' | 'bun' | 'yarn' | 'npm' | null {
|
|
371
|
+
if (pkgRoot === null) return null
|
|
372
|
+
const has = (f: string): boolean => existsSync(path.join(pkgRoot, f))
|
|
373
|
+
if (has('pnpm-lock.yaml')) return 'pnpm'
|
|
374
|
+
if (has('bun.lock') || has('bun.lockb')) return 'bun'
|
|
375
|
+
if (has('yarn.lock')) return 'yarn'
|
|
376
|
+
if (has('package-lock.json') || has('npm-shrinkwrap.json')) return 'npm'
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Whether `pkgRoot` has package.json scripts to map: its own, or a
|
|
382
|
+
* workspace's members (`workspaces`, `pnpm-workspace.yaml`). Core maps none
|
|
383
|
+
* (2026-10-20), so `next` names the tool that does.
|
|
384
|
+
*/
|
|
385
|
+
function hasScripts(pkgRoot: string | null): boolean {
|
|
386
|
+
if (pkgRoot === null) return false
|
|
387
|
+
if (existsSync(path.join(pkgRoot, 'pnpm-workspace.yaml'))) return true
|
|
388
|
+
try {
|
|
389
|
+
// A byte-order mark is stripped, as npm strips it.
|
|
390
|
+
const pj = JSON.parse(
|
|
391
|
+
readFileSync(path.join(pkgRoot, 'package.json'), 'utf8').replace(/^/, ''),
|
|
392
|
+
) as {
|
|
393
|
+
scripts?: unknown
|
|
394
|
+
workspaces?: unknown
|
|
395
|
+
}
|
|
396
|
+
return (
|
|
397
|
+
pj.workspaces !== undefined ||
|
|
398
|
+
(typeof pj.scripts === 'object' && pj.scripts !== null && Object.keys(pj.scripts).length > 0)
|
|
399
|
+
)
|
|
400
|
+
} catch {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
}
|
|
242
404
|
|
|
243
405
|
/**
|
|
244
406
|
* No Turbo or Nx config: the installs, then `.vx/workspace.ts` declaring
|
|
@@ -255,17 +417,31 @@ async function starter(
|
|
|
255
417
|
if (args.mode !== undefined) {
|
|
256
418
|
throw new UserError(
|
|
257
419
|
`vx init: --${args.mode} answers a Turbo or Nx repo's question, and there is no turbo.json or nx.json here`,
|
|
420
|
+
'VX_E_USAGE',
|
|
258
421
|
)
|
|
259
422
|
}
|
|
260
|
-
|
|
423
|
+
// A source checkout installs nothing, so it declares the plugin only where one is.
|
|
424
|
+
const found = lockfilePlugin(pkgRoot)
|
|
425
|
+
const plugin =
|
|
426
|
+
found !== null &&
|
|
427
|
+
(VERSION !== '0.0.0' ||
|
|
428
|
+
existsSync(path.join(pkgRoot!, 'node_modules', '@vzn', 'vx-lockfile', 'package.json')))
|
|
429
|
+
? found
|
|
430
|
+
: null
|
|
431
|
+
const plugins = plugin === null ? [] : ['@vzn/vx-lockfile']
|
|
432
|
+
const installs = args.json === true || args.dry ? await install(pkgRoot, true, plugins) : []
|
|
261
433
|
const ext = args.mjs ? 'mjs' : 'ts'
|
|
262
434
|
const config = `vx.config.${ext}`
|
|
263
435
|
const files: { path: string; contents: string }[] = []
|
|
264
436
|
const kept: string[] = []
|
|
265
437
|
const replaced: string[] = []
|
|
266
438
|
for (const [names, name, contents] of [
|
|
267
|
-
[
|
|
268
|
-
|
|
439
|
+
[
|
|
440
|
+
WORKSPACE_CONFIG_FILENAMES,
|
|
441
|
+
`.vx/workspace.${ext}`,
|
|
442
|
+
renderWorkspace(ext, plugin === null ? [] : [{ pkg: '@vzn/vx-lockfile', factory: plugin }]),
|
|
443
|
+
],
|
|
444
|
+
[PROJECT_CONFIG_FILENAMES, config, starterProject(ext)],
|
|
269
445
|
] as const) {
|
|
270
446
|
const existing = names.find((n) => existsSync(path.join(root, n)))
|
|
271
447
|
if (existing !== undefined && !args.force) kept.push(existing)
|
|
@@ -281,8 +457,8 @@ async function starter(
|
|
|
281
457
|
] as const) {
|
|
282
458
|
if (!existsSync(path.join(root, name))) files.push({ path: name, contents })
|
|
283
459
|
}
|
|
284
|
-
const
|
|
285
|
-
const next = `${noGit ? 'git init, ' : ''}
|
|
460
|
+
const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
|
|
461
|
+
const next = `${noGit ? 'git init, ' : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
|
|
286
462
|
if (args.json === true) {
|
|
287
463
|
process.stdout.write(
|
|
288
464
|
`${JSON.stringify({ dry: true, source: 'no turbo.json or nx.json', files, kept, replaced, todos: [], notes: [], next, installs })}\n`,
|
|
@@ -290,19 +466,30 @@ async function starter(
|
|
|
290
466
|
return 0
|
|
291
467
|
}
|
|
292
468
|
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json: one project, the root']]
|
|
469
|
+
// The files are root-relative: a root above the cwd is named, or they read as written here.
|
|
470
|
+
if (root !== process.cwd()) rows.push(['root', root])
|
|
293
471
|
if (args.dry) rows.push(['dry run', 'nothing written'])
|
|
294
472
|
if (installs.length > 0) rows.push(['install', ...installs])
|
|
295
473
|
if (files.length > 0) rows.push(['files', ...files.map((f) => f.path)])
|
|
296
474
|
if (kept.length > 0) rows.push(['kept', ...kept])
|
|
297
475
|
if (replaced.length > 0) rows.push(['replaced', ...replaced])
|
|
298
476
|
if (args.dry) {
|
|
299
|
-
for (const f of files)
|
|
477
|
+
for (const f of files) ui.print('out', ui.excerpt(f.path, f.contents))
|
|
300
478
|
ui.print('out', ui.report(`vx init ${VERSION}`, [...rows, null, ['next', next]]))
|
|
301
479
|
return 0
|
|
302
480
|
}
|
|
303
|
-
const
|
|
481
|
+
for (const f of files) refuseSymlink(path.join(root, f.path))
|
|
482
|
+
const ran = await install(pkgRoot, false, plugins)
|
|
304
483
|
if (ran.length > 0) rows.push(['installed', ...ran])
|
|
305
|
-
for (const f of files)
|
|
484
|
+
for (const f of files) {
|
|
485
|
+
await writeNoFollow(path.join(root, f.path), f.contents).catch((err: NodeJS.ErrnoException) => {
|
|
486
|
+
// writeNoFollow's mkdir of a parent that is a file says EEXIST.
|
|
487
|
+
if (err.code !== 'ENOTDIR' && err.code !== 'EEXIST') throw err
|
|
488
|
+
throw new UserError(
|
|
489
|
+
`vx init: cannot write ${f.path}: ${path.dirname(f.path)} is a file, not a directory; move it and re-run`,
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
306
493
|
for (const f of replaced) await unlink(path.join(root, f))
|
|
307
494
|
// The lock that follows loads the workspace file just written.
|
|
308
495
|
workspaceFileWritten()
|
|
@@ -312,14 +499,15 @@ async function starter(
|
|
|
312
499
|
lock.code === 0 ? next : `fix the above, then vx lock and ${next}`,
|
|
313
500
|
])
|
|
314
501
|
ui.print('out', ui.report(`vx init ${VERSION}`, rows))
|
|
315
|
-
|
|
502
|
+
// A run reads only the lock: init without one is not done, as vx-migrate exits.
|
|
503
|
+
return lock.code === 0 ? 0 : 1
|
|
316
504
|
}
|
|
317
505
|
|
|
318
506
|
/**
|
|
319
507
|
* A Turbo or Nx repo is `@vzn/vx-migrate`'s, after the global install, at the latest
|
|
320
508
|
* release: it installs @vzn/vx and every @vzn/vx-* plugin the root lists
|
|
321
509
|
* at its own version, so init always lands on latest with all of them
|
|
322
|
-
* together, as `vx upgrade` moves them (
|
|
510
|
+
* together, as `vx upgrade` moves them (2026-10-16: an installed 0.0.637
|
|
323
511
|
* pinned core back from 0.0.639). It finds the root itself, asks a
|
|
324
512
|
* terminal native or keep in a Turbo or Nx repo, and installs what it writes.
|
|
325
513
|
*/
|