@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/mcp.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// `vx mcp` — Model Context Protocol server adapter.
|
|
2
|
-
//
|
|
3
|
-
// Exposes vx as a typed tool surface to AI agents (Claude Code, Cursor,
|
|
4
|
-
// Continue.dev, etc.). The MCP envelope is JSON-RPC 2.0 — the same
|
|
5
|
-
// envelope wire-protocol-2026-06.md commits to — so this adapter is a
|
|
6
|
-
// thin mapping layer: MCP tool calls dispatch to internal RPC methods.
|
|
7
|
-
//
|
|
8
|
-
// stdio transport in v1: every relevant agent client runs servers over
|
|
9
|
-
// stdio. Streamable HTTP is a follow-up; the SDK supports both, dispatch
|
|
10
|
-
// is identical, only the transport object changes.
|
|
11
|
-
//
|
|
12
|
-
// Tools exposed:
|
|
13
|
-
// runTasks(tasks: string[], cwd?: string)
|
|
14
|
-
// getCacheStats(scope?: 'all' | { project: string })
|
|
15
|
-
// getRunHistory({ project?, task?, limit? })
|
|
16
|
-
// explainCacheKey(taskId)
|
|
17
|
-
// whyDidThisRerun({ runId, taskId })
|
|
18
|
-
//
|
|
19
|
-
// The implementations live in `src/cli/mcp-rpc.ts` so a future WS-side
|
|
20
|
-
// inspector can reuse them without duplicating logic.
|
|
21
|
-
|
|
22
|
-
import { UserError } from '../util/index.js'
|
|
23
|
-
import { VERSION } from '../version.js'
|
|
24
|
-
import { handleMcpRequest, listMcpTools, type McpToolDef } from './mcp-rpc.js'
|
|
25
|
-
|
|
26
|
-
export interface McpArgs {
|
|
27
|
-
/** stdio (default) | http (deferred to a follow-up) */
|
|
28
|
-
transport: 'stdio'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function parseMcpArgs(args: readonly string[]): McpArgs {
|
|
32
|
-
for (const a of args) {
|
|
33
|
-
if (a === '--http') {
|
|
34
|
-
throw new UserError('vx mcp --http is not yet implemented; use stdio (default)')
|
|
35
|
-
}
|
|
36
|
-
if (a !== '--stdio') {
|
|
37
|
-
throw new UserError(`vx mcp: unknown flag ${a}`)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { transport: 'stdio' }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function mcpCmd(args: readonly string[]): Promise<number> {
|
|
44
|
-
parseMcpArgs(args) // validate flags; defaults to stdio
|
|
45
|
-
let StdioServerTransport: any
|
|
46
|
-
let McpServer: any
|
|
47
|
-
try {
|
|
48
|
-
const sdkServer = await import('@modelcontextprotocol/sdk/server/index.js')
|
|
49
|
-
const sdkStdio = await import('@modelcontextprotocol/sdk/server/stdio.js')
|
|
50
|
-
McpServer = (sdkServer as { Server: any }).Server
|
|
51
|
-
StdioServerTransport = (sdkStdio as { StdioServerTransport: any }).StdioServerTransport
|
|
52
|
-
} catch {
|
|
53
|
-
throw new UserError(
|
|
54
|
-
`vx mcp requires @modelcontextprotocol/sdk to be installed. ` +
|
|
55
|
-
`Add it to your package.json or install with: bun add @modelcontextprotocol/sdk`,
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const server = new McpServer(
|
|
60
|
-
{ name: 'vx', version: VERSION },
|
|
61
|
-
{ capabilities: { tools: {}, resources: {} } },
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// Tool listing
|
|
65
|
-
const sdkTypes = await import('@modelcontextprotocol/sdk/types.js')
|
|
66
|
-
server.setRequestHandler(
|
|
67
|
-
(sdkTypes as { ListToolsRequestSchema: any }).ListToolsRequestSchema,
|
|
68
|
-
async () => ({
|
|
69
|
-
tools: listMcpTools().map((t: McpToolDef) => ({
|
|
70
|
-
name: t.name,
|
|
71
|
-
description: t.description,
|
|
72
|
-
inputSchema: t.inputSchema,
|
|
73
|
-
})),
|
|
74
|
-
}),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// Tool call dispatch
|
|
78
|
-
server.setRequestHandler(
|
|
79
|
-
(sdkTypes as { CallToolRequestSchema: any }).CallToolRequestSchema,
|
|
80
|
-
async (req: { params: { name: string; arguments?: unknown } }) => {
|
|
81
|
-
const result = await handleMcpRequest(req.params.name, req.params.arguments)
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
const transport = new StdioServerTransport()
|
|
89
|
-
await server.connect(transport)
|
|
90
|
-
await new Promise(() => undefined) // run until stdin closes
|
|
91
|
-
return 0
|
|
92
|
-
}
|
package/src/cli/migrate-nx.ts
DELETED
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
// Nx → vx mapping, from the resolved project-graph snapshot ONLY
|
|
2
|
-
// (.nx/workspace-data/project-graph.json). Static by design: targets
|
|
3
|
-
// that Nx plugins infer at runtime are frozen as the snapshot saw
|
|
4
|
-
// them. Named inputs expand from nx.json when readable; the graph's
|
|
5
|
-
// dependency edges are ignored (vx derives package edges from
|
|
6
|
-
// package.json manifests) except for one report line counting edges
|
|
7
|
-
// with no manifest counterpart.
|
|
8
|
-
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
11
|
-
import type { ProjectMeta } from '../workspace/index.js'
|
|
12
|
-
import type { GeneratedProject, GeneratedTask, MigrationPlan } from './migrate.js'
|
|
13
|
-
|
|
14
|
-
const PLACEHOLDER = "echo 'TODO(vx-migrate): fill in' && exit 1"
|
|
15
|
-
const GRAPH_REL = '.nx/workspace-data/project-graph.json'
|
|
16
|
-
|
|
17
|
-
interface NxTarget {
|
|
18
|
-
executor?: string
|
|
19
|
-
command?: string
|
|
20
|
-
options?: Record<string, unknown>
|
|
21
|
-
inputs?: unknown[]
|
|
22
|
-
outputs?: string[]
|
|
23
|
-
dependsOn?: unknown[]
|
|
24
|
-
cache?: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface NxNode {
|
|
28
|
-
name?: string
|
|
29
|
-
data?: { root?: string; targets?: Record<string, NxTarget> }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type NxEdge = { source?: string; target?: string }
|
|
33
|
-
|
|
34
|
-
function normRel(p: string): string {
|
|
35
|
-
return p === '' || p === '.' ? '.' : p.replace(/\/$/, '')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function migrateNx(
|
|
39
|
-
root: string,
|
|
40
|
-
metas: readonly ProjectMeta[],
|
|
41
|
-
): Promise<MigrationPlan> {
|
|
42
|
-
const graphPath = path.join(root, GRAPH_REL)
|
|
43
|
-
let parsed: unknown
|
|
44
|
-
try {
|
|
45
|
-
parsed = JSON.parse(await Bun.file(graphPath).text())
|
|
46
|
-
} catch (err) {
|
|
47
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
48
|
-
throw new UserError(`failed to parse ${GRAPH_REL}: ${msg}`)
|
|
49
|
-
}
|
|
50
|
-
// Both {graph:{nodes,dependencies}} and top-level {nodes,dependencies}
|
|
51
|
-
// exist across nx versions.
|
|
52
|
-
const g = ((parsed as { graph?: unknown }).graph ?? parsed) as {
|
|
53
|
-
nodes?: unknown
|
|
54
|
-
dependencies?: unknown
|
|
55
|
-
}
|
|
56
|
-
const nodes = g?.nodes
|
|
57
|
-
if (typeof nodes !== 'object' || nodes === null) {
|
|
58
|
-
throw new UserError(
|
|
59
|
-
`${GRAPH_REL}: unrecognized shape — expected { graph: { nodes, dependencies } } ` +
|
|
60
|
-
'or { nodes, dependencies }',
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
const nodeMap = nodes as Record<string, NxNode>
|
|
64
|
-
|
|
65
|
-
const namedInputs = await readNamedInputs(root)
|
|
66
|
-
|
|
67
|
-
const metaByRel = new Map<string, ProjectMeta>()
|
|
68
|
-
for (const meta of metas) metaByRel.set(normRel(relPosix(root, meta.dir)), meta)
|
|
69
|
-
const metaByNode = new Map<string, ProjectMeta>()
|
|
70
|
-
const nodeByMeta = new Map<ProjectMeta, NxNode>()
|
|
71
|
-
for (const [nodeName, node] of Object.entries(nodeMap)) {
|
|
72
|
-
const meta = metaByRel.get(normRel(node?.data?.root ?? ''))
|
|
73
|
-
if (meta) {
|
|
74
|
-
metaByNode.set(nodeName, meta)
|
|
75
|
-
nodeByMeta.set(meta, node)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Graph nodes with no discovered counterpart: the ROOT project is
|
|
80
|
-
// the common case — `packages/*` discovery never lists the
|
|
81
|
-
// workspace root, but Nx routinely defines root targets. Synthesize
|
|
82
|
-
// a meta for any unmatched node whose root resolves inside the
|
|
83
|
-
// workspace, so its targets migrate too.
|
|
84
|
-
const allMetas: ProjectMeta[] = [...metas]
|
|
85
|
-
for (const [nodeName, node] of Object.entries(nodeMap)) {
|
|
86
|
-
if (metaByNode.has(nodeName)) continue
|
|
87
|
-
const relRoot = normRel(node?.data?.root ?? '')
|
|
88
|
-
if (node?.data?.targets === undefined) continue
|
|
89
|
-
const dir = relRoot === '' || relRoot === '.' ? root : path.join(root, relRoot)
|
|
90
|
-
let pkg: ProjectMeta['packageJson'] = { name: nodeName }
|
|
91
|
-
try {
|
|
92
|
-
pkg = (await Bun.file(path.join(dir, 'package.json')).json()) as ProjectMeta['packageJson']
|
|
93
|
-
} catch {
|
|
94
|
-
// No manifest at the node root — keep the synthetic one.
|
|
95
|
-
}
|
|
96
|
-
const synthetic: ProjectMeta = {
|
|
97
|
-
name: pkg.name || nodeName,
|
|
98
|
-
dir,
|
|
99
|
-
packageJson: pkg,
|
|
100
|
-
configPath: null,
|
|
101
|
-
}
|
|
102
|
-
allMetas.push(synthetic)
|
|
103
|
-
metaByNode.set(nodeName, synthetic)
|
|
104
|
-
nodeByMeta.set(synthetic, node)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const projects: GeneratedProject[] = []
|
|
108
|
-
for (const meta of allMetas) {
|
|
109
|
-
const node = nodeByMeta.get(meta)
|
|
110
|
-
const targets = node?.data?.targets
|
|
111
|
-
if (!targets) continue
|
|
112
|
-
const tasks: GeneratedTask[] = []
|
|
113
|
-
for (const [targetName, target] of Object.entries(targets)) {
|
|
114
|
-
const t = buildTask(root, meta, targetName, target, namedInputs, metaByNode)
|
|
115
|
-
// task === null → no vx representation; surface the reason in
|
|
116
|
-
// the report but emit nothing into the config.
|
|
117
|
-
if (t.task === null && t.todos.length > 0) {
|
|
118
|
-
tasks.push(t)
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
tasks.push(t)
|
|
122
|
-
}
|
|
123
|
-
projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const notes: string[] = []
|
|
127
|
-
const implicit = countImplicitDeps(g?.dependencies, metaByNode)
|
|
128
|
-
if (implicit > 0) {
|
|
129
|
-
notes.push(
|
|
130
|
-
`${implicit} implicit Nx dep${implicit === 1 ? '' : 's'} not representable; review dependsOn`,
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
headerNotes: [
|
|
136
|
-
'migrating from the resolved project-graph snapshot — plugin-inferred targets ' +
|
|
137
|
-
'are frozen as static config',
|
|
138
|
-
],
|
|
139
|
-
projects,
|
|
140
|
-
extraFiles: [],
|
|
141
|
-
notes,
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function readNamedInputs(root: string): Promise<Record<string, unknown[]> | null> {
|
|
146
|
-
const file = Bun.file(path.join(root, 'nx.json'))
|
|
147
|
-
if (!(await file.exists())) return null
|
|
148
|
-
try {
|
|
149
|
-
const parsed = Bun.JSONC.parse(await file.text()) as { namedInputs?: unknown }
|
|
150
|
-
const named = parsed?.namedInputs
|
|
151
|
-
if (typeof named !== 'object' || named === null) return null
|
|
152
|
-
return named as Record<string, unknown[]>
|
|
153
|
-
} catch {
|
|
154
|
-
// Unreadable nx.json just degrades named-input refs to TODOs.
|
|
155
|
-
return null
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function buildTask(
|
|
160
|
-
root: string,
|
|
161
|
-
meta: ProjectMeta,
|
|
162
|
-
targetName: string,
|
|
163
|
-
target: NxTarget,
|
|
164
|
-
namedInputs: Record<string, unknown[]> | null,
|
|
165
|
-
metaByNode: ReadonlyMap<string, ProjectMeta>,
|
|
166
|
-
): GeneratedTask {
|
|
167
|
-
const todos: string[] = []
|
|
168
|
-
const options = target.options ?? {}
|
|
169
|
-
const projectRel = normRel(relPosix(root, meta.dir))
|
|
170
|
-
const scripts =
|
|
171
|
-
(meta.packageJson as unknown as { scripts?: Record<string, string> }).scripts ?? {}
|
|
172
|
-
|
|
173
|
-
const command = mapCommand(targetName, target, options, projectRel, scripts, todos)
|
|
174
|
-
|
|
175
|
-
const files: string[] = []
|
|
176
|
-
const wsFiles: string[] = []
|
|
177
|
-
const envNames: string[] = []
|
|
178
|
-
const expandInput = (entry: unknown, seen: Set<string>): void => {
|
|
179
|
-
if (typeof entry === 'string') {
|
|
180
|
-
let s = entry
|
|
181
|
-
let neg = ''
|
|
182
|
-
if (s.startsWith('!')) {
|
|
183
|
-
neg = '!'
|
|
184
|
-
s = s.slice(1)
|
|
185
|
-
}
|
|
186
|
-
if (s.startsWith('{projectRoot}/')) {
|
|
187
|
-
files.push(neg + s.slice('{projectRoot}/'.length))
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
if (s.startsWith('{workspaceRoot}/')) {
|
|
191
|
-
wsFiles.push(neg + s.slice('{workspaceRoot}/'.length))
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
if (s.startsWith('^')) {
|
|
195
|
-
todos.push(
|
|
196
|
-
`deps-input ${JSON.stringify(entry)}: vx folds upstream outputs into the cache ` +
|
|
197
|
-
'key via dependsOn automatically — usually safe to drop',
|
|
198
|
-
)
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
if (s.includes('{')) {
|
|
202
|
-
todos.push(`input ${JSON.stringify(entry)} uses a token vx does not support`)
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
// Bare string = named-input reference.
|
|
206
|
-
const named = namedInputs?.[s]
|
|
207
|
-
if (named === undefined) {
|
|
208
|
-
todos.push(
|
|
209
|
-
`named input ${JSON.stringify(s)} not found in nx.json — declare its globs manually`,
|
|
210
|
-
)
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
if (seen.has(s)) return
|
|
214
|
-
seen.add(s)
|
|
215
|
-
for (const e of named) expandInput(e, seen)
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
if (entry && typeof entry === 'object') {
|
|
219
|
-
const o = entry as Record<string, unknown>
|
|
220
|
-
if (typeof o.env === 'string') {
|
|
221
|
-
envNames.push(o.env)
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
if (typeof o.fileset === 'string') {
|
|
225
|
-
expandInput(o.fileset, seen)
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
if (o.externalDependencies !== undefined) {
|
|
229
|
-
todos.push(
|
|
230
|
-
`input {externalDependencies: ${JSON.stringify(o.externalDependencies)}}: vx hashes ` +
|
|
231
|
-
"the project's package.json into every key — usually safe to drop",
|
|
232
|
-
)
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
if (o.dependentTasksOutputFiles !== undefined) {
|
|
236
|
-
todos.push(
|
|
237
|
-
'input {dependentTasksOutputFiles: …}: vx folds upstream outputs into the cache ' +
|
|
238
|
-
'key via dependsOn automatically',
|
|
239
|
-
)
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
if (typeof o.input === 'string') {
|
|
243
|
-
if (o.dependencies === true || o.projects !== undefined) {
|
|
244
|
-
todos.push(
|
|
245
|
-
`deps-input ${JSON.stringify(entry)}: vx folds upstream via dependsOn automatically`,
|
|
246
|
-
)
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
expandInput(o.input, seen)
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
todos.push(`input ${JSON.stringify(entry)} not representable in vx`)
|
|
254
|
-
}
|
|
255
|
-
for (const entry of target.inputs ?? []) expandInput(entry, new Set())
|
|
256
|
-
|
|
257
|
-
const outFiles: string[] = []
|
|
258
|
-
const wsOutFiles: string[] = []
|
|
259
|
-
// Heuristic: a bare directory path captures its whole subtree.
|
|
260
|
-
const dirGlob = (rel: string): string => {
|
|
261
|
-
const last = rel.split('/').at(-1)!
|
|
262
|
-
return !rel.includes('*') && !last.includes('.') ? `${rel}/**` : rel
|
|
263
|
-
}
|
|
264
|
-
const pushOut = (rel: string): void => {
|
|
265
|
-
outFiles.push(dirGlob(rel))
|
|
266
|
-
}
|
|
267
|
-
for (const o of target.outputs ?? []) {
|
|
268
|
-
let s = o
|
|
269
|
-
const optTok = /\{options\.([^}]+)\}/.exec(s)
|
|
270
|
-
if (optTok) {
|
|
271
|
-
const v = options[optTok[1]!]
|
|
272
|
-
if (typeof v !== 'string') {
|
|
273
|
-
todos.push(
|
|
274
|
-
`output ${JSON.stringify(o)}: option ${JSON.stringify(optTok[1])} is not a literal ` +
|
|
275
|
-
'string — resolve manually',
|
|
276
|
-
)
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
s = s.replace(optTok[0], v)
|
|
280
|
-
}
|
|
281
|
-
if (s.startsWith('{projectRoot}/')) {
|
|
282
|
-
pushOut(s.slice('{projectRoot}/'.length))
|
|
283
|
-
continue
|
|
284
|
-
}
|
|
285
|
-
if (s.startsWith('{workspaceRoot}/')) {
|
|
286
|
-
wsOutFiles.push(dirGlob(s.slice('{workspaceRoot}/'.length)))
|
|
287
|
-
continue
|
|
288
|
-
}
|
|
289
|
-
if (s.includes('{')) {
|
|
290
|
-
todos.push(`output ${JSON.stringify(o)} uses a token vx does not support`)
|
|
291
|
-
continue
|
|
292
|
-
}
|
|
293
|
-
// Plain paths resolve against the workspace root in nx.
|
|
294
|
-
if (projectRel === '.') pushOut(s)
|
|
295
|
-
else if (s.startsWith(`${projectRel}/`)) pushOut(s.slice(projectRel.length + 1))
|
|
296
|
-
else {
|
|
297
|
-
todos.push(
|
|
298
|
-
`output ${JSON.stringify(o)} falls outside the project dir — declare it in ` +
|
|
299
|
-
'cache.outputs.workspaceFiles (workspace-root-relative) if intended',
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const deps: string[] = []
|
|
305
|
-
for (const d of target.dependsOn ?? []) {
|
|
306
|
-
if (typeof d === 'string') {
|
|
307
|
-
deps.push(d)
|
|
308
|
-
continue
|
|
309
|
-
}
|
|
310
|
-
if (d && typeof d === 'object') {
|
|
311
|
-
const o = d as Record<string, unknown>
|
|
312
|
-
const t = typeof o.target === 'string' ? o.target : undefined
|
|
313
|
-
if (t === undefined) {
|
|
314
|
-
todos.push(`dependsOn ${JSON.stringify(d)} has no target — dropped`)
|
|
315
|
-
continue
|
|
316
|
-
}
|
|
317
|
-
if (o.params !== undefined) {
|
|
318
|
-
todos.push(
|
|
319
|
-
`dependsOn ${JSON.stringify(t)}: params forwarding is not supported — forward args ` +
|
|
320
|
-
'via `vx run … -- args` instead',
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
const projects = o.projects ?? (o.dependencies === true ? 'dependencies' : undefined)
|
|
324
|
-
if (projects === undefined || projects === 'self') deps.push(t)
|
|
325
|
-
else if (projects === 'dependencies') deps.push(`^${t}`)
|
|
326
|
-
else if (Array.isArray(projects)) {
|
|
327
|
-
for (const p of projects) {
|
|
328
|
-
const m = typeof p === 'string' ? metaByNode.get(p) : undefined
|
|
329
|
-
if (m) deps.push(`${m.name}#${t}`)
|
|
330
|
-
else {
|
|
331
|
-
todos.push(
|
|
332
|
-
`dependsOn project ${JSON.stringify(p)} is not a workspace package — edge dropped`,
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
} else todos.push(`dependsOn ${JSON.stringify(d)} not representable in vx`)
|
|
337
|
-
continue
|
|
338
|
-
}
|
|
339
|
-
todos.push(`dependsOn ${JSON.stringify(d)} not representable in vx`)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const cacheEnabled =
|
|
343
|
-
target.cache === true ||
|
|
344
|
-
(target.cache === undefined && (target.inputs !== undefined || target.outputs !== undefined))
|
|
345
|
-
|
|
346
|
-
if (command === null) {
|
|
347
|
-
// nx:noop → vx group task: dependsOn only, no exec, no cache
|
|
348
|
-
// (vx forbids cache on groups). A noop with nothing to chain has
|
|
349
|
-
// no vx representation — skipped with a report line.
|
|
350
|
-
if (deps.length === 0) {
|
|
351
|
-
todos.push('nx:noop target with no dependsOn — nothing to represent; skipped')
|
|
352
|
-
return { name: targetName, task: null, todos }
|
|
353
|
-
}
|
|
354
|
-
return { name: targetName, task: { dependsOn: deps }, todos }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const exec: Record<string, unknown> = { command }
|
|
358
|
-
if (envNames.length > 0) exec.env = { passThrough: envNames }
|
|
359
|
-
const task: Record<string, unknown> = { exec }
|
|
360
|
-
if (deps.length > 0) task.dependsOn = deps
|
|
361
|
-
if (cacheEnabled) {
|
|
362
|
-
if (target.inputs === undefined && files.length === 0) {
|
|
363
|
-
files.push('**/*')
|
|
364
|
-
todos.push(
|
|
365
|
-
"cache enabled with no declared inputs — defaulting to ['**/*']; narrow to the real input set",
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
|
-
const inputs: Record<string, unknown> = { files }
|
|
369
|
-
if (wsFiles.length > 0) inputs.workspaceFiles = wsFiles
|
|
370
|
-
if (envNames.length > 0) inputs.env = envNames
|
|
371
|
-
const outputs: Record<string, unknown> = { files: outFiles }
|
|
372
|
-
if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
|
|
373
|
-
task.cache = { inputs, outputs }
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return { name: targetName, todos, task }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function mapCommand(
|
|
380
|
-
targetName: string,
|
|
381
|
-
target: NxTarget,
|
|
382
|
-
options: Record<string, unknown>,
|
|
383
|
-
projectRel: string,
|
|
384
|
-
scripts: Record<string, string>,
|
|
385
|
-
todos: string[],
|
|
386
|
-
): string | null {
|
|
387
|
-
const executor = target.executor
|
|
388
|
-
if (executor === 'nx:noop') {
|
|
389
|
-
// No command by definition — the vx equivalent is a group task
|
|
390
|
-
// (handled by the caller; nothing to map here).
|
|
391
|
-
return null
|
|
392
|
-
}
|
|
393
|
-
if (executor === 'nx:run-commands') {
|
|
394
|
-
if (typeof options.cwd === 'string' && normRel(options.cwd) !== projectRel) {
|
|
395
|
-
todos.push(
|
|
396
|
-
`run-commands cwd ${JSON.stringify(options.cwd)} differs from the project root — vx ` +
|
|
397
|
-
'runs commands from the project dir; adjust the command',
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
const cmds = options.commands
|
|
401
|
-
if (Array.isArray(cmds) && cmds.length > 0) {
|
|
402
|
-
const parts = cmds
|
|
403
|
-
.map((c) => (typeof c === 'string' ? c : ((c as { command?: unknown }).command as string)))
|
|
404
|
-
.filter((c): c is string => typeof c === 'string' && c.length > 0)
|
|
405
|
-
if (parts.length > 0) return parts.join(' && ')
|
|
406
|
-
}
|
|
407
|
-
if (typeof options.command === 'string') return options.command
|
|
408
|
-
todos.push(`nx:run-commands target has no command — options: ${JSON.stringify(options)}`)
|
|
409
|
-
return PLACEHOLDER
|
|
410
|
-
}
|
|
411
|
-
if (executor === 'nx:run-script') {
|
|
412
|
-
const script = typeof options.script === 'string' ? options.script : targetName
|
|
413
|
-
const body = scripts[script]
|
|
414
|
-
if (body !== undefined) return body
|
|
415
|
-
todos.push(`nx:run-script: package.json has no ${JSON.stringify(script)} script`)
|
|
416
|
-
return PLACEHOLDER
|
|
417
|
-
}
|
|
418
|
-
if (executor === undefined && typeof target.command === 'string') return target.command
|
|
419
|
-
todos.push(
|
|
420
|
-
`executor ${JSON.stringify(executor ?? '(none)')} has no shell equivalent — fill in the ` +
|
|
421
|
-
`CLI command. options: ${JSON.stringify(options)}`,
|
|
422
|
-
)
|
|
423
|
-
return PLACEHOLDER
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function countImplicitDeps(
|
|
427
|
-
dependencies: unknown,
|
|
428
|
-
metaByNode: ReadonlyMap<string, ProjectMeta>,
|
|
429
|
-
): number {
|
|
430
|
-
if (typeof dependencies !== 'object' || dependencies === null) return 0
|
|
431
|
-
let count = 0
|
|
432
|
-
for (const [source, edges] of Object.entries(dependencies as Record<string, NxEdge[]>)) {
|
|
433
|
-
const sm = metaByNode.get(source)
|
|
434
|
-
if (!sm || !Array.isArray(edges)) continue
|
|
435
|
-
const manifest = {
|
|
436
|
-
...sm.packageJson.dependencies,
|
|
437
|
-
...sm.packageJson.devDependencies,
|
|
438
|
-
...sm.packageJson.peerDependencies,
|
|
439
|
-
...sm.packageJson.optionalDependencies,
|
|
440
|
-
}
|
|
441
|
-
const seen = new Set<string>()
|
|
442
|
-
for (const edge of edges) {
|
|
443
|
-
const tm = typeof edge?.target === 'string' ? metaByNode.get(edge.target) : undefined
|
|
444
|
-
if (!tm || tm === sm || seen.has(tm.name)) continue
|
|
445
|
-
seen.add(tm.name)
|
|
446
|
-
if (manifest[tm.name] === undefined) count++
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return count
|
|
450
|
-
}
|