@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.
Files changed (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. 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
- }
@@ -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
- }