@venn-lang/cli 0.1.0

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/dist/bin/venn.mjs +16 -0
  4. package/dist/cli.mjs +56378 -0
  5. package/dist/index.d.mts +98 -0
  6. package/dist/index.d.mts.map +1 -0
  7. package/dist/index.mjs +916 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +67 -0
  10. package/src/bin/venn.ts +14 -0
  11. package/src/cli.ts +299 -0
  12. package/src/commands/build.ts +97 -0
  13. package/src/commands/check.ts +143 -0
  14. package/src/commands/deps.ts +228 -0
  15. package/src/commands/fmt.ts +55 -0
  16. package/src/commands/index.ts +2 -0
  17. package/src/commands/inside-workspace.ts +20 -0
  18. package/src/commands/list.ts +53 -0
  19. package/src/commands/new.ts +66 -0
  20. package/src/commands/run.ts +149 -0
  21. package/src/commands/script.ts +127 -0
  22. package/src/commands/upgrade.ts +86 -0
  23. package/src/commands/verify-plugin.ts +35 -0
  24. package/src/index.ts +7 -0
  25. package/src/manifest/index.ts +2 -0
  26. package/src/manifest/load-env.ts +70 -0
  27. package/src/manifest/load-manifest.ts +42 -0
  28. package/src/project/command-targets.ts +53 -0
  29. package/src/project/index.ts +9 -0
  30. package/src/project/resolve-targets.ts +70 -0
  31. package/src/project/select-packages.ts +37 -0
  32. package/src/project/targets-or-exit.ts +28 -0
  33. package/src/reporters/colors.ts +17 -0
  34. package/src/reporters/dot-sink.ts +18 -0
  35. package/src/reporters/error-line.ts +14 -0
  36. package/src/reporters/index.ts +9 -0
  37. package/src/reporters/junit-sink.ts +39 -0
  38. package/src/reporters/ndjson-stdout.ts +13 -0
  39. package/src/reporters/pick-reporter.ts +28 -0
  40. package/src/reporters/pretty/diff-lines.ts +53 -0
  41. package/src/reporters/pretty/index.ts +1 -0
  42. package/src/reporters/pretty/pretty-reporter.ts +122 -0
  43. package/src/reporters/pretty/pretty.types.ts +29 -0
  44. package/src/reporters/pretty/render.ts +88 -0
  45. package/src/reporters/problem-reporter.ts +13 -0
  46. package/src/reporters/problem-sink.ts +20 -0
  47. package/src/reporters/reporter.types.ts +21 -0
  48. package/src/run/collect-files.ts +43 -0
  49. package/src/run/ending.types.ts +17 -0
  50. package/src/run/exit-code.ts +15 -0
  51. package/src/run/node-io.ts +27 -0
  52. package/src/run/npm-loader.ts +41 -0
  53. package/src/run/package-types.ts +66 -0
  54. package/src/run/run-file.ts +106 -0
  55. package/src/run/should-leave.ts +11 -0
  56. package/src/run/step-titles.ts +61 -0
  57. package/src/shutdown/create-leave.ts +42 -0
  58. package/src/shutdown/create-shutdown.ts +34 -0
  59. package/src/shutdown/index.ts +7 -0
  60. package/src/shutdown/install-exit-hook.ts +21 -0
  61. package/src/shutdown/install-fault-hooks.ts +30 -0
  62. package/src/shutdown/install-hooks.ts +36 -0
  63. package/src/shutdown/install-signal-hooks.ts +26 -0
  64. package/src/shutdown/shutdown.types.ts +20 -0
  65. package/src/title/index.ts +2 -0
  66. package/src/title/program-title.ts +15 -0
  67. package/src/title/set-program-title.ts +15 -0
  68. package/src/upgrade/index.ts +6 -0
  69. package/src/upgrade/install-site.ts +70 -0
  70. package/src/upgrade/latest-version.ts +50 -0
  71. package/src/upgrade/upgrade-command.ts +32 -0
  72. package/src/upgrade/upgrade-plan.ts +36 -0
  73. package/src/upgrade/upgrade.types.ts +17 -0
  74. package/src/upgrade/version.ts +21 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["write","process"],"sources":["../src/manifest/load-env.ts","../src/manifest/load-manifest.ts","../src/reporters/dot-sink.ts","../src/reporters/error-line.ts","../src/reporters/junit-sink.ts","../src/reporters/ndjson-stdout.ts","../src/reporters/colors.ts","../src/reporters/pretty/diff-lines.ts","../src/reporters/pretty/render.ts","../src/reporters/pretty/pretty-reporter.ts","../src/reporters/pick-reporter.ts","../src/reporters/problem-reporter.ts","../src/run/collect-files.ts","../src/run/node-io.ts","../src/run/npm-loader.ts","../src/run/run-file.ts","../src/shutdown/create-leave.ts","../src/shutdown/create-shutdown.ts","../src/shutdown/install-exit-hook.ts","../src/shutdown/install-fault-hooks.ts","../src/shutdown/install-signal-hooks.ts","../src/shutdown/install-hooks.ts","../src/title/program-title.ts","../src/title/set-program-title.ts","../src/commands/run.ts","../src/commands/verify-plugin.ts"],"sourcesContent":["// biome-ignore-all lint/suspicious/noTemplateCurlyInString: ${name} is a placeholder in a dotenv path, not a JavaScript template.\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { dotenvFiles, type Manifest, parseDotenv } from \"@venn-lang/contracts\";\n\nexport interface EnvArgs {\n manifest: Manifest | undefined;\n /** The selected environment: `--env staging`, or `local`. */\n name: string;\n /** Any file the manifest points at is resolved from here. */\n dir: string;\n /** The process environment. Injected so a test can hand over its own. */\n processEnv?: Record<string, string | undefined>;\n}\n\n/**\n * Every variable a run can see, lowest precedence first:\n *\n * 1. `[env.<name>]` in `venn.toml`, the documented default, committed.\n * 2. the dotenv files, in the order they are listed.\n * 3. the real environment the process was started with.\n *\n * The real environment wins because that is how CI passes a token in, and a\n * value set on the command line should never lose to a file in the repository.\n */\nexport async function loadEnv(args: EnvArgs): Promise<Record<string, unknown>> {\n const declared = { ...(args.manifest?.env[args.name] ?? {}), ...(await readFiles(args)) };\n const real = overrides(declared, args.processEnv ?? process.env);\n return { ...declared, ...real, name: args.name };\n}\n\n/**\n * The real environment overrides what was declared. It does not add to it.\n *\n * A shell holds hundreds of entries; letting them all become `env.*` would put\n * `PATH` and `TEMP` in the editor's completion and let a typo silently read\n * something from the machine. Declaring a name, in `venn.toml` or a dotenv file\n * and with any value, is what says \"this program reads this\". A value that\n * exists only in CI and is never declared belongs to `secrets.*`, which reads\n * the environment directly and redacts what it returns.\n */\nfunction overrides(\n declared: Record<string, string>,\n source: Record<string, string | undefined>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const name of Object.keys(declared)) {\n const value = source[name];\n if (value !== undefined) out[name] = value;\n }\n return out;\n}\n\nasync function readFiles(args: EnvArgs): Promise<Record<string, string>> {\n const names = dotenvFiles({ configured: args.manifest?.envFiles, name: args.name });\n const out: Record<string, string> = {};\n for (const each of names) Object.assign(out, await readOne(resolve(args.dir, each)));\n return out;\n}\n\n/** A file that is not there is not a problem: most projects have none of them. */\nasync function readOne(path: string): Promise<Record<string, string>> {\n const content = await readFile(path, \"utf8\").catch(() => undefined);\n return content === undefined ? {} : parseDotenv(content);\n}\n\n/** Where dotenv files are looked for: the folder holding the flow. */\nexport function envDirOf(sourceUri: string): string {\n return dirname(sourceUri);\n}\n","import { dirname, resolve } from \"node:path\";\nimport type { Manifest } from \"@venn-lang/contracts\";\nimport { createNodeFs } from \"@venn-lang/contracts/node\";\nimport { findProject, isInside, normalise, type Package, type Project } from \"@venn-lang/project\";\n\nexport interface LoadedManifest {\n manifest: Manifest;\n /**\n * The directory the settings are anchored to.\n *\n * Carried because a `[paths]` alias is written relative to the project, not\n * to the folder the `.vn` file happens to sit in: a member of a workspace\n * reads the aliases its root declared.\n */\n dir: string;\n}\n\n/**\n * The manifest that governs a `.vn` file.\n *\n * Found by walking up to the project the file belongs to, rather than by\n * reading whatever happens to sit in the same folder. That is what makes a\n * command work from anywhere: `venn test packages/api/src/login.vn` sees the\n * same environments and aliases as running it from inside `packages/api`.\n *\n * Undefined when nothing governs the file: living outside a project is normal.\n */\nexport async function loadManifest(sourceUri: string): Promise<LoadedManifest | undefined> {\n const file = normalise(resolve(sourceUri));\n const { project } = await findProject({ fs: createNodeFs(), from: normalise(dirname(file)) });\n if (!project) return undefined;\n const owner = owningPackage(project, file);\n if (owner) return { manifest: owner.manifest, dir: owner.dir };\n return { manifest: project.rootManifest, dir: project.root };\n}\n\n/** The innermost member holding this file: the one whose settings are nearest. */\nfunction owningPackage(project: Project, file: string): Package | undefined {\n return [...project.packages]\n .filter((one) => isInside(file, one.dir))\n .sort((a, b) => b.dir.length - a.dir.length)[0];\n}\n","import type { Envelope } from \"@venn-lang/core\";\nimport type { EventSink } from \"@venn-lang/runtime\";\n\n/** Terminal reporter: one char per assertion, a summary line at the end. */\nexport function createDotSink(): EventSink {\n return { emit: (envelope) => write(envelope) };\n}\n\nfunction write(envelope: Envelope): void {\n if (envelope.kind === \"expect.passed\") process.stdout.write(\".\");\n else if (envelope.kind === \"expect.failed\") process.stdout.write(\"F\");\n else if (envelope.kind === \"run.finished\") writeSummary(envelope);\n}\n\nfunction writeSummary(envelope: Envelope): void {\n const data = envelope.data as { passed: number; failed: number; durationMs: number };\n process.stdout.write(`\\n${data.passed} passed, ${data.failed} failed (${data.durationMs}ms)\\n`);\n}\n","import { VennError } from \"@venn-lang/contracts\";\n\n/**\n * One line for one failure, however it reached us.\n *\n * A `VennError` already knows how it wants to read, and leads with its code so\n * the failure is googlable; anything else is a stray from below the language and\n * gets its message, never `[object Object]`.\n */\nexport function errorLine(error: unknown): string {\n if (error instanceof VennError) return `${error.code} ${error.message}`;\n const message = (error as { message?: unknown } | undefined)?.message;\n return typeof message === \"string\" && message !== \"\" ? message : String(error);\n}\n","import type { EventSink } from \"@venn-lang/runtime\";\n\ninterface FlowResult {\n title: string;\n status: string;\n}\n\nconst XML_ESCAPES: Record<string, string> = {\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n \"&\": \"&amp;\",\n '\"': \"&quot;\",\n};\n\n/** Accumulate flow results and emit a JUnit XML document on run.finished. */\nexport function createJunitSink(args: { write: (xml: string) => void }): EventSink {\n const flows: FlowResult[] = [];\n return {\n emit: (envelope) => {\n if (envelope.kind === \"flow.finished\") flows.push(envelope.data as FlowResult);\n else if (envelope.kind === \"run.finished\") args.write(toJunit(flows));\n },\n };\n}\n\nfunction toJunit(flows: readonly FlowResult[]): string {\n const failures = flows.filter((flow) => flow.status === \"failed\").length;\n const cases = flows.map(toCase).join(\"\\n\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<testsuite tests=\"${flows.length}\" failures=\"${failures}\">\\n${cases}\\n</testsuite>\\n`;\n}\n\nfunction toCase(flow: FlowResult): string {\n const body = flow.status === \"failed\" ? \"<failure/>\" : \"\";\n return ` <testcase name=\"${escapeXml(flow.title)}\">${body}</testcase>`;\n}\n\nfunction escapeXml(text: string): string {\n return text.replace(/[<>&\"]/g, (char) => XML_ESCAPES[char] ?? char);\n}\n","import { createNdjsonSink, type EventSink } from \"@venn-lang/runtime\";\n\n/**\n * An event sink that writes each envelope to stdout as one line of NDJSON: the\n * stream a script or a CI job parses.\n */\nexport function createStdoutSink(): EventSink {\n return createNdjsonSink({\n write: (line) => {\n process.stdout.write(line);\n },\n });\n}\n","// Colour only when a human is watching: never when piped, and never against\n// NO_COLOR (https://no-color.org) or a dumb terminal.\nconst ESC = String.fromCharCode(27);\nconst ENABLED =\n Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== \"dumb\";\n\nfunction style(open: number, close: number): (text: string) => string {\n return (text) => (ENABLED ? `${ESC}[${open}m${text}${ESC}[${close}m` : text);\n}\n\nexport const bold: (text: string) => string = style(1, 22);\nexport const dim: (text: string) => string = style(2, 22);\nexport const red: (text: string) => string = style(31, 39);\nexport const green: (text: string) => string = style(32, 39);\nexport const yellow: (text: string) => string = style(33, 39);\nexport const cyan: (text: string) => string = style(36, 39);\nexport const inverse: (text: string) => string = style(7, 27);\n","import { type Diff, type DiffEntry, formatValue } from \"@venn-lang/core\";\nimport { bold, dim, green, red } from \"../colors.js\";\n\nconst INDENT = \" \";\nconst COLUMN = \"expected\".length;\n\n/**\n * The body of a failure: the two sides as the kernel compared them, field by\n * field (§16). The title says what went wrong in one line; this says where.\n */\nexport function diffLines(diff: Diff): string[] {\n if (diff.kind === \"fields\") return fieldLines(diff.label, diff.entries);\n if (diff.kind === \"json\") {\n return [head(diff.path), ...pairLines(formatValue(diff.expected), formatValue(diff.actual))];\n }\n return pairLines(diff.expected, diff.actual);\n}\n\nfunction fieldLines(label: string, entries: readonly DiffEntry[]): string[] {\n const width = Math.max(0, ...entries.map((entry) => entry.path.length));\n const lines = [head(label)];\n for (const [index, entry] of entries.entries()) {\n lines.push(...entryLines({ entry, width, last: index === entries.length - 1 }));\n }\n return lines;\n}\n\n/** One field: a single line when both sides agree, two when they do not. */\nfunction entryLines(args: { entry: DiffEntry; width: number; last: boolean }): string[] {\n const { entry, last, width } = args;\n const branch = `${INDENT}${dim(last ? \"└\" : \"├\")} ${entry.path.padEnd(width)}`;\n if (entry.same) return [`${branch} ${column(\"same\")} ${dim(entry.expected)}`];\n const gutter = `${INDENT}${dim(last ? \" \" : \"│\")} ${\" \".repeat(width)}`;\n return [\n `${branch} ${column(\"expected\")} ${green(entry.expected)}`,\n `${gutter} ${column(\"actual\")} ${red(entry.actual)}`,\n ];\n}\n\nfunction pairLines(expected: string, actual: string): string[] {\n return [\n `${INDENT}${column(\"expected\")} ${green(expected)}`,\n `${INDENT}${column(\"actual\")} ${red(actual)}`,\n ];\n}\n\nfunction head(label: string): string {\n return `${INDENT}${bold(label)}`;\n}\n\nfunction column(label: string): string {\n return dim(label.padEnd(COLUMN));\n}\n","import { relative } from \"node:path\";\nimport type { Problem } from \"@venn-lang/core\";\nimport { bold, cyan, dim, green, inverse, red } from \"../colors.js\";\nimport { diffLines } from \"./diff-lines.js\";\nimport type { Failure } from \"./pretty.types.js\";\n\nconst PASS = \"✓\";\nconst FAIL = \"✗\";\n\n/** The banner naming the file under test. Flows add their own leading blank line. */\nexport function header(file: string): string {\n return `\\n${inverse(cyan(\" RUN \"))} ${dim(shorten(file))}`;\n}\n\n/** A flow opens a branch of the tree. */\nexport function flowLine(title: string): string {\n return `\\n ${dim(\"❯\")} ${bold(title)}`;\n}\n\n/** A step closes with its verdict and how long it took. */\nexport function stepLine(args: { title: string; passed: boolean; ms: number }): string {\n const mark = args.passed ? green(PASS) : red(FAIL);\n return ` ${mark} ${args.title} ${dim(`${args.ms}ms`)}`;\n}\n\n/** Why a step failed, shown inline right under it. */\nexport function reasonLine(failure: Failure): string {\n return ` ${red(\"→\")} ${failure.title}`;\n}\n\n/** A `log` line the flow emitted, shown under its step like console output. */\nexport function logLine(message: string): string {\n const [first = \"\", ...rest] = message.split(\"\\n\");\n const head = ` ${dim(\"›\")} ${first}`;\n return [head, ...rest.map((line) => ` ${line}`)].join(\"\\n\");\n}\n\nexport function failuresBlock(failures: readonly Failure[]): string {\n if (failures.length === 0) return \"\";\n const blocks = failures.map((failure, index) => block(failure, index + 1));\n return `\\n${inverse(red(\" FAILURES \"))}\\n\\n${blocks.join(\"\\n\\n\")}\\n`;\n}\n\nexport function summary(args: {\n passed: number;\n failed: number;\n files?: number;\n ms: number;\n}): string {\n const counts = [\n args.failed > 0 ? red(`${args.failed} failed`) : undefined,\n green(`${args.passed} passed`),\n ];\n const line = counts.filter(Boolean).join(dim(\" | \"));\n const total = dim(`(${args.passed + args.failed})`);\n const files = args.files && args.files > 1 ? `\\n ${dim(\"Files\")} ${args.files}` : \"\";\n return `${files}\\n ${dim(\"Tests\")} ${line} ${total}\\n ${dim(\" Time\")} ${dim(`${args.ms}ms`)}\\n`;\n}\n\n/** `examples/testing/01-first-flow.vn:12:5`, relative to where the CLI was invoked. */\nexport function locationOf(problem: Problem): string | undefined {\n const span = problem.span;\n if (!span?.uri) return undefined;\n return `${shorten(span.uri)}:${span.line}:${span.column}`;\n}\n\nfunction block(failure: Failure, index: number): string {\n const lines = [\n ` ${red(`${index})`)} ${bold(where(failure))}`,\n ` ${red(failure.code)} ${failure.title}`,\n ];\n if (failure.location) lines.push(` ${dim(`at ${failure.location}`)}`);\n if (failure.diff) lines.push(\"\", ...diffLines(failure.diff));\n return lines.join(\"\\n\");\n}\n\n/**\n * Which flow and step it happened in. A hook that failed belongs to neither, so\n * it says so instead of borrowing the last step's name.\n */\nfunction where(failure: Failure): string {\n return [failure.flow, failure.step].filter(Boolean).join(` ${dim(\"›\")} `) || \"lifecycle\";\n}\n\nfunction shorten(file: string): string {\n const path = relative(process.cwd(), file);\n return path && !path.startsWith(\"..\") ? path.replace(/\\\\/g, \"/\") : file;\n}\n","import type { Envelope, Problem } from \"@venn-lang/core\";\nimport type { Reporter, RunTotals } from \"../reporter.types.js\";\nimport type { Failure, PrettyState } from \"./pretty.types.js\";\nimport {\n failuresBlock,\n flowLine,\n header,\n locationOf,\n logLine,\n reasonLine,\n stepLine,\n summary,\n} from \"./render.js\";\n\n/**\n * A live tree: each file opens a banner, each flow a branch, and each step\n * prints its verdict as soon as it settles. Everything that failed across every\n * file is repeated at the end with its code and source location.\n */\nexport function createPrettyReporter(): Reporter {\n const state: PrettyState = {\n flow: \"\",\n step: \"\",\n inStep: false,\n stepStartedAt: 0,\n current: [],\n logs: [],\n failures: [],\n };\n return {\n sink: { emit: (envelope) => handle(envelope, state) },\n beginFile: (file) => {\n state.pendingFile = file;\n },\n finish: (totals) => finishRun(state, totals),\n };\n}\n\nfunction handle(envelope: Envelope, state: PrettyState): void {\n const data = envelope.data as Record<string, unknown>;\n if (envelope.kind === \"flow.started\") beginFlow(state, data);\n else if (envelope.kind === \"step.started\") beginStep(state, envelope, data);\n else if (envelope.kind === \"expect.failed\") failed(state, data);\n else if (envelope.kind === \"step.finished\") endStep(state, envelope, data);\n else if (envelope.kind === \"flow.finished\") state.flow = \"\";\n else if (envelope.kind === \"log\") logged(state, data);\n}\n\n/**\n * A failure inside a step waits for that step's verdict, to print under it. One\n * that arrives between steps (a `setup` or a `teardown` that blew up) has no\n * verdict coming, and holding it would let the next `step.started` wipe it, so\n * it goes straight to the summary.\n */\nfunction failed(state: PrettyState, data: Record<string, unknown>): void {\n const failure = fromProblem(state, data);\n if (state.inStep) state.current.push(failure);\n else state.failures.push(failure);\n}\n\nfunction beginFlow(state: PrettyState, data: Record<string, unknown>): void {\n if (state.pendingFile) {\n write(header(state.pendingFile));\n state.pendingFile = undefined;\n }\n state.flow = String(data.title ?? \"\");\n write(flowLine(state.flow));\n}\n\nfunction beginStep(state: PrettyState, envelope: Envelope, data: Record<string, unknown>): void {\n state.step = String(data.title ?? \"\");\n state.inStep = true;\n state.stepStartedAt = Date.parse(envelope.ts);\n state.current = [];\n state.logs = [];\n}\n\nfunction endStep(state: PrettyState, envelope: Envelope, data: Record<string, unknown>): void {\n const ms = Math.max(0, Date.parse(envelope.ts) - state.stepStartedAt);\n write(stepLine({ title: String(data.title ?? \"\"), passed: data.status === \"passed\", ms }));\n for (const line of state.logs) write(logLine(line));\n for (const failure of state.current) write(reasonLine(failure));\n state.failures.push(...state.current);\n state.current = [];\n state.logs = [];\n state.inStep = false;\n}\n\n/**\n * `log \"…\"` prints under its step, like console output in vitest. An error\n * thrown mid-flow also arrives as a log, and that one is kept for the summary.\n */\nfunction logged(state: PrettyState, data: Record<string, unknown>): void {\n const message = String(data.message ?? \"\");\n if (data.level === \"error\") {\n state.failures.push({ flow: state.flow, step: state.step, code: \"VN7001\", title: message });\n } else {\n state.logs.push(message);\n }\n}\n\nfunction finishRun(state: PrettyState, totals: RunTotals): void {\n write(failuresBlock(state.failures));\n write(summary(totals));\n}\n\nfunction fromProblem(state: PrettyState, data: Record<string, unknown>): Failure {\n const problem = data.problem as Problem;\n return {\n flow: state.flow,\n // A step that already ended is not to blame for what came after it.\n step: state.inStep ? state.step : \"\",\n code: problem.code,\n title: problem.title,\n location: locationOf(problem),\n diff: problem.diff,\n };\n}\n\nfunction write(text: string): void {\n if (text) process.stdout.write(`${text}\\n`);\n}\n","import type { EventSink } from \"@venn-lang/runtime\";\nimport { createDotSink } from \"./dot-sink.js\";\nimport { createJunitSink } from \"./junit-sink.js\";\nimport { createStdoutSink } from \"./ndjson-stdout.js\";\nimport { createPrettyReporter } from \"./pretty/index.js\";\nimport type { Reporter } from \"./reporter.types.js\";\n\n/**\n * Choose a reporter by name. With no `--reporter`, a terminal gets the readable\n * tree and anything piped gets NDJSON, so scripts and CI keep the\n * machine-readable stream they can parse.\n */\nexport function pickReporter(name: string | undefined): Reporter {\n if (name === \"pretty\") return createPrettyReporter();\n if (name === \"ndjson\") return passive(createStdoutSink());\n if (name === \"dot\") return passive(createDotSink());\n if (name === \"junit\") return passive(junit());\n return process.stdout.isTTY ? createPrettyReporter() : passive(createStdoutSink());\n}\n\n// Machine formats say everything they need through the event stream itself.\nfunction passive(sink: EventSink): Reporter {\n return { sink, beginFile: () => {}, finish: () => {} };\n}\n\nfunction junit(): EventSink {\n return createJunitSink({ write: (xml) => process.stdout.write(xml) });\n}\n","import type { Problem } from \"@venn-lang/core\";\n\n/**\n * Print compile-time problems to stderr, one code and title per problem with\n * its source location beneath. The terminal surface of the §16 model.\n */\nexport function reportProblems(problems: readonly Problem[]): void {\n for (const problem of problems) {\n process.stderr.write(`${problem.code} · ${problem.title}\\n`);\n const { uri, line, column } = problem.span;\n process.stderr.write(` at ${uri}:${line}:${column}\\n`);\n }\n}\n","import { readdir, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nconst EXTENSION = \".vn\";\nconst SKIP = new Set([\"node_modules\", \"dist\", \".git\"]);\n\n/**\n * The `.vn` files a path names: the file itself, or every one under a\n * directory, walked recursively and sorted so runs are reproducible.\n */\nexport async function collectSourceFiles(path: string): Promise<string[]> {\n const info = await stat(path).catch(() => undefined);\n if (!info) return [];\n if (info.isFile()) return path.endsWith(EXTENSION) ? [path] : [];\n return (await walk(path)).sort();\n}\n\nasync function walk(directory: string): Promise<string[]> {\n const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);\n const found: string[] = [];\n for (const entry of entries) {\n if (entry.isDirectory() && !SKIP.has(entry.name))\n found.push(...(await walk(join(directory, entry.name))));\n else if (entry.isFile() && entry.name.endsWith(EXTENSION))\n found.push(join(directory, entry.name));\n }\n return found;\n}\n\n/**\n * Every `.vn` under any of these paths, each counted once.\n *\n * Deduplicated because a workspace can name the same directory twice, as a\n * member and as the root that contains it, and checking a file twice reports\n * every problem in it twice.\n */\nexport async function everySourceUnder(paths: readonly string[]): Promise<string[]> {\n const found = new Set<string>();\n for (const path of paths) {\n for (const file of await collectSourceFiles(resolve(path))) found.add(file);\n }\n return [...found];\n}\n","import { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { resolveAlias } from \"@venn-lang/contracts\";\nimport type { ModuleIo } from \"@venn-lang/runtime\";\n\n/** Where a `#alias/…` specifier is resolved from. */\ninterface Roots {\n paths: Record<string, string>;\n rootDir: string;\n}\n\n/**\n * Node-backed module IO: read files, resolve relative specifiers against the\n * importer, and `#alias/…` specifiers through `[paths]` in `venn.toml`.\n */\nexport function createNodeModuleIo(roots: Roots): ModuleIo {\n return {\n read: (uri) => readFile(uri, \"utf8\"),\n resolve: (base, spec) => resolveSpec(spec, base, roots),\n };\n}\n\nfunction resolveSpec(spec: string, base: string, roots: Roots): string {\n if (spec.startsWith(\".\")) return resolve(dirname(base), spec);\n const alias = resolveAlias({ spec, paths: roots.paths });\n return alias ? resolve(roots.rootDir, alias.dir, alias.rest) : spec;\n}\n","import { createRequire } from \"node:module\";\nimport { join } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { NpmModules } from \"@venn-lang/runtime\";\n\n/**\n * Loading an installed package, the way Node loads one.\n *\n * Resolution is Node's own, rooted at `target/`, the directory holding the\n * generated `package.json` and the `node_modules` beside it. `exports` maps,\n * conditions, scoped names and the rest are what a package means, and a second\n * implementation of them would agree with Node right up until it did not.\n */\nexport function createNpmLoader(args: { root: string }): NpmModules {\n const target = join(args.root, \"target\");\n const from = createRequire(pathToFileURL(join(target, \"package.json\")));\n return {\n async load(spec) {\n const found = resolvePath(from, spec);\n if (!found) return undefined;\n return (await import(pathToFileURL(found).href)) as Record<string, unknown>;\n },\n };\n}\n\ntype Resolver = { resolve: (spec: string) => string };\n\n/**\n * Where the package lives, or nothing when it is not installed.\n *\n * Not installed is an ordinary state: the manifest asks for a package and\n * nobody has run `venn install` yet. The import that named it reports that\n * itself, which is a better place to say it than inside a resolver.\n */\nfunction resolvePath(from: Resolver, spec: string): string | undefined {\n try {\n return from.resolve(spec);\n } catch {\n return undefined;\n }\n}\n","import { type Console, ConsolePort, type Host } from \"@venn-lang/contracts\";\nimport { type Problem, parse } from \"@venn-lang/core\";\nimport { type HttpClient, HttpClientPort, type HttpServer, HttpServerPort } from \"@venn-lang/http\";\nimport { createNodeServer } from \"@venn-lang/http/node\";\nimport {\n type CleanupSink,\n checkImports,\n createRunner,\n type EventSink,\n type ModuleIo,\n type NpmModules,\n type RunFilter,\n type RunResult,\n resolveImports,\n} from \"@venn-lang/runtime\";\nimport { allPlugins, stdlibPortBindings } from \"@venn-lang/stdlib\";\n\n/** What one `.vn` file amounted to. */\nexport interface RunFileOutcome {\n /** Everything refused or reported. Empty when the file ran clean. */\n problems: Problem[];\n /** Absent when nothing ran: the parse or the imports stopped it first. */\n result?: RunResult;\n}\n\n/** The source to run, the implementations behind its ports, and how to run it. */\nexport interface RunFileArgs {\n source: string;\n uri: string;\n host: Host;\n sink: EventSink;\n httpClient: HttpClient;\n /** Injected when the caller needs to close the servers itself, as `venn run` does. */\n httpServer?: HttpServer;\n console?: Console;\n filter?: RunFilter;\n bail?: boolean;\n env?: Record<string, unknown>;\n io?: ModuleIo;\n /** How an installed package is loaded, when the host can load one. */\n npm?: NpmModules;\n /** Where the program registers what it opened, so the host can close it. */\n cleanup?: CleanupSink;\n /** \"test\" runs the flows; \"script\" executes the file top to bottom. */\n mode?: \"test\" | \"script\";\n}\n\n/**\n * Parse and run a `.vn` source with the full stdlib loaded.\n *\n * The HttpClient, HttpServer and Console ports take injected implementations\n * (real in the CLI, fakes in tests); every other port takes the binding\n * `@venn-lang/stdlib` supplies.\n *\n * @param args - The source and its uri, the host and the ports, and how to run\n * it: the filter, the mode, the environment, the module and package loaders.\n * @returns The problems and, when anything ran, the run's result. A failing\n * flow is a `Problem` here, not an exception.\n * @throws Whatever an action or a loaded module let escape the runner.\n */\nexport async function runFile(args: RunFileArgs): Promise<RunFileOutcome> {\n const { ast, problems } = parse(args.source, { uri: args.uri });\n if (problems.length > 0) return { problems };\n const io = args.io;\n const resolved = io\n ? await resolveImports({ document: ast, uri: args.uri, io, npm: args.npm })\n : undefined;\n const graph =\n resolved && io\n ? { modules: resolved.modules, resolve: io.resolve, npm: resolved.npm }\n : undefined;\n // Refused before anything runs. An import that names something the other file\n // never published would otherwise surface halfway through, as a value that\n // was quietly `undefined` until something called it.\n const bad = graph ? checkImports({ document: ast, uri: args.uri, graph }) : [];\n if (bad.length > 0) return { problems: bad };\n const runner = createRunner({\n host: args.host,\n plugins: allPlugins,\n sink: args.sink,\n ports: bindings(args),\n uri: args.uri,\n filter: args.filter,\n bail: args.bail,\n env: args.env,\n moduleFragments: resolved?.fragments,\n modules: graph,\n moduleDecos: resolved?.decos,\n cleanup: args.cleanup,\n });\n const result = args.mode === \"script\" ? await runner.script(ast) : await runner.run(ast);\n // The result carries its own problems, a decorator that refused the program\n // among them, so both travel back together rather than one replacing the other.\n return { problems: result.problems ?? [], result };\n}\n\nfunction bindings(args: RunFileArgs) {\n const console = args.console ? [{ port: ConsolePort, impl: args.console }] : [];\n // After the stdlib: the last binding wins, and these are the real ones.\n return [\n { port: HttpClientPort, impl: args.httpClient },\n ...stdlibPortBindings,\n { port: HttpServerPort, impl: args.httpServer ?? createNodeServer() },\n ...console,\n ];\n}\n","import type { Leave, Shutdown } from \"./shutdown.types.js\";\n\n/** How long a close may take before the program stops waiting for it. */\nconst GRACE_MS = 5_000;\n\n/**\n * Leaving, done properly: close what is open, then go.\n *\n * Two things make this more than `await close(); exit()`. A second signal means\n * the user is done asking nicely, so it leaves at once. And a closer that never\n * settles must not hold the program hostage, so the wait has a deadline: an\n * unclean exit beats a process that cannot be stopped.\n */\nexport function createLeave(args: {\n shutdown: Shutdown;\n exit: (code: number) => void;\n graceMs?: number;\n}): Leave {\n let leaving = false;\n return (code) => {\n if (leaving) return args.exit(code);\n leaving = true;\n void finish({ ...args, code });\n };\n}\n\nasync function finish(args: {\n shutdown: Shutdown;\n exit: (code: number) => void;\n graceMs?: number;\n code: number;\n}): Promise<void> {\n await Promise.race([args.shutdown.close(), deadline(args.graceMs ?? GRACE_MS)]);\n args.exit(args.code);\n}\n\n/** A timer that does not itself keep the program alive. */\nfunction deadline(ms: number): Promise<void> {\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms).unref();\n });\n}\n","import type { Closer, Shutdown } from \"./shutdown.types.js\";\n\n/**\n * The registry: what to close, and the promise that it happened.\n *\n * Closing runs newest first, because a thing opened later may be standing on one\n * opened earlier. One closer that throws must not strand the rest: leaving is\n * the goal, and a half-closed program still has to go.\n */\nexport function createShutdown(): Shutdown {\n const closers = new Set<Closer>();\n let closing: Promise<void> | undefined;\n return {\n add: (closer) => {\n closers.add(closer);\n return () => {\n closers.delete(closer);\n };\n },\n close: () => {\n closing ??= closeAll(closers);\n return closing;\n },\n };\n}\n\nasync function closeAll(closers: Set<Closer>): Promise<void> {\n for (const closer of [...closers].reverse()) {\n await Promise.resolve()\n .then(() => closer())\n .catch(() => {});\n }\n closers.clear();\n}\n","import process from \"node:process\";\nimport type { Shutdown, Unregister } from \"./shutdown.types.js\";\n\n/**\n * The quiet ending: a program that simply ran out of things to do.\n *\n * `beforeExit` is the only moment a script without a server gets, so its\n * `teardown` has to run here or it never runs at all. The timer is what buys\n * the closing its time: a promise alone does not hold the loop open, and\n * without it Node could leave mid-cleanup.\n */\nexport function installExitHook(shutdown: Shutdown): Unregister {\n const onBeforeExit = () => {\n const hold = setInterval(() => {}, 1_000);\n void shutdown.close().finally(() => clearInterval(hold));\n };\n process.once(\"beforeExit\", onBeforeExit);\n return () => {\n process.off(\"beforeExit\", onBeforeExit);\n };\n}\n","import process from \"node:process\";\nimport { errorLine } from \"../reporters/index.js\";\nimport type { Leave, Unregister } from \"./shutdown.types.js\";\n\n/**\n * The two ways a program dies without being asked to.\n *\n * Node's default for both is to print a stack and vanish, losing the sockets,\n * the reason and the exit code at once. Catching them means the failure is\n * stated in the language's own voice and the machine gets its resources back on\n * the way out.\n */\nexport function installFaultHooks(args: {\n leave: Leave;\n report?: (message: string) => void;\n}): Unregister {\n const onFault = (cause: unknown) => fault({ ...args, cause });\n process.on(\"uncaughtException\", onFault);\n process.on(\"unhandledRejection\", onFault);\n return () => {\n process.off(\"uncaughtException\", onFault);\n process.off(\"unhandledRejection\", onFault);\n };\n}\n\nfunction fault(args: { leave: Leave; report?: (message: string) => void; cause: unknown }): void {\n const write = args.report ?? ((message: string) => process.stderr.write(`${message}\\n`));\n write(errorLine(args.cause));\n args.leave(1);\n}\n","import { ALL_SIGNALS, type SignalSource, type SystemSignal } from \"@venn-lang/contracts\";\nimport type { Leave, Unregister } from \"./shutdown.types.js\";\n\n/** What the shell means by each signal, in exit codes. */\nconst CODES: Record<SystemSignal, number> = {\n SIGINT: 130,\n SIGTERM: 143,\n SIGBREAK: 130,\n SIGHUP: 129,\n};\n\n/**\n * Every way a system can ask this program to stop, wired to the same ending.\n *\n * `Ctrl+C` is the one everybody knows; `SIGTERM` is how a supervisor or a CI\n * runner asks; `SIGBREAK` is Windows' Ctrl+Break; `SIGHUP` is the terminal\n * closing with the program still inside it. All four deserve the same care.\n */\nexport function installSignalHooks(args: { signals: SignalSource; leave: Leave }): Unregister {\n const offs = ALL_SIGNALS.map((signal) =>\n args.signals.on(signal, () => args.leave(CODES[signal])),\n );\n return () => {\n for (const off of offs) off();\n };\n}\n","import type { SignalSource } from \"@venn-lang/contracts\";\nimport { createLeave } from \"./create-leave.js\";\nimport { installExitHook } from \"./install-exit-hook.js\";\nimport { installFaultHooks } from \"./install-fault-hooks.js\";\nimport { installSignalHooks } from \"./install-signal-hooks.js\";\nimport type { Leave, Shutdown } from \"./shutdown.types.js\";\n\n/** Everything the process-level hooks need from the outside world. */\nexport interface HooksArgs {\n signals: SignalSource;\n shutdown: Shutdown;\n /** How the program leaves. Injected so a test can watch instead of dying. */\n exit: (code: number) => void;\n /** Where a fault is announced. Defaults to stderr. */\n report?: (message: string) => void;\n graceMs?: number;\n}\n\n/**\n * Hand the process its hooks. The three ways a program can end (asked to stop,\n * broken, or simply finished) all end the same way.\n *\n * Returns the `leave` these hooks share, so a command that decides to stop on\n * its own terms unwinds through exactly the same path.\n */\nexport function installHooks(args: HooksArgs): Leave {\n const leave = createLeave({\n shutdown: args.shutdown,\n exit: args.exit,\n graceMs: args.graceMs,\n });\n installSignalHooks({ signals: args.signals, leave });\n installFaultHooks({ leave, report: args.report });\n installExitHook(args.shutdown);\n return leave;\n}\n","import { basename } from \"node:path\";\n\n/** How long a title may get before it stops being readable in a tab. */\nconst MAX = 60;\n\n/**\n * What this process calls itself: `venn run server.vn`, not `node`.\n *\n * The terminal tab is the only place a long-running program announces itself,\n * and \"node\" says nothing about which program, which file, or whose.\n */\nexport function programTitle(args: { command: string; target?: string }): string {\n const name = args.target ? basename(args.target) : \"\";\n return `venn ${args.command}${name ? ` ${name}` : \"\"}`.slice(0, MAX);\n}\n","import process from \"node:process\";\nimport { programTitle } from \"./program-title.js\";\n\n/**\n * Name the process after the program it is running.\n *\n * Until something says otherwise, Windows reports the executable path (the\n * `node` the terminal tab shows) and puts it back when the process exits. So\n * this is set once and never undone: restoring it by hand would write that path\n * over whatever title the terminal had before, which is worse than doing\n * nothing.\n */\nexport function setProgramTitle(args: { command: string; target?: string }): void {\n process.title = programTitle(args);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { createNodeHost, createNodeSignals } from \"@venn-lang/contracts/node\";\nimport { createFetchClient } from \"@venn-lang/http\";\nimport { createNodeServer } from \"@venn-lang/http/node\";\nimport type { RunFilter } from \"@venn-lang/runtime\";\nimport { loadEnv, loadManifest } from \"../manifest/index.js\";\nimport type { Reporter, RunTotals } from \"../reporters/index.js\";\nimport { pickReporter, reportProblems } from \"../reporters/index.js\";\nimport { collectSourceFiles } from \"../run/collect-files.js\";\nimport { createNodeModuleIo } from \"../run/node-io.js\";\nimport { createNpmLoader } from \"../run/npm-loader.js\";\nimport { type RunFileOutcome, runFile } from \"../run/run-file.js\";\nimport { createShutdown, installHooks, type Shutdown } from \"../shutdown/index.js\";\nimport { setProgramTitle } from \"../title/index.js\";\n\n/** Everything `venn run` accepts. */\nexport interface RunOptions {\n file: string;\n reporter?: string;\n tags?: string;\n flow?: string;\n step?: string;\n env?: string;\n bail?: boolean;\n}\n\n/** What one pass over the files needs to carry from start to finish. */\ninterface RunPass {\n files: readonly string[];\n options: RunOptions;\n reporter: Reporter;\n shutdown: Shutdown;\n}\n\n/**\n * `venn test <file|directory>`: run every matching flow, then report.\n *\n * @param options - The file or directory, the reporter, and the filters.\n * @returns The exit code: whatever a flow's `exit` named, else 0 when nothing\n * failed, else 1. Also 1 when the path holds no `.vn` file.\n */\nexport async function runCommand(options: RunOptions): Promise<number> {\n const files = await collectSourceFiles(resolve(options.file));\n if (files.length === 0) return noFiles(options.file);\n const reporter = pickReporter(options.reporter);\n const shutdown = createShutdown();\n setProgramTitle({ command: \"test\", target: options.file });\n installHooks({ signals: createNodeSignals(), shutdown, exit: (code) => process.exit(code) });\n const totals = await runAll({ files, options, reporter, shutdown });\n reporter.finish(totals);\n // A suite that called `exit` named its own verdict, even `exit 0` after a\n // failure, which is the point of saying it.\n return totals.exitCode ?? (totals.failed === 0 ? 0 : 1);\n}\n\nasync function runAll(pass: RunPass): Promise<RunTotals> {\n const totals: RunTotals = { passed: 0, failed: 0, files: 0, ms: 0 };\n const started = Date.now();\n for (const file of pass.files) {\n pass.reporter.beginFile(file);\n await runOne({ pass, file, totals });\n // `exit` ends the command, not just the file it was called in.\n if (totals.exitCode !== undefined) break;\n if (pass.options.bail && totals.failed > 0) break;\n }\n totals.ms = Date.now() - started;\n return totals;\n}\n\n/**\n * One file, and the servers it started closed with it.\n *\n * A suite that leaves a port bound holds it for the whole run, so a hundred\n * files that serve would end up holding a hundred ports. They are also on the\n * shutdown list, for the run that ends by signal instead of by finishing.\n */\nasync function runOne(args: { pass: RunPass; file: string; totals: RunTotals }): Promise<void> {\n const servers = createNodeServer();\n const forget = args.pass.shutdown.add(() => servers.closeAll());\n try {\n tally(args.totals, await runFile(await buildArgs(args.file, args.pass, servers)));\n } finally {\n await servers.closeAll();\n forget();\n }\n}\n\nfunction tally(totals: RunTotals, outcome: RunFileOutcome): void {\n if (outcome.problems.length > 0) {\n reportProblems(outcome.problems);\n totals.files += 1;\n totals.failed += 1;\n return;\n }\n if (outcome.result?.exitCode !== undefined) totals.exitCode = outcome.result.exitCode;\n const passed = outcome.result?.passed ?? 0;\n const failed = outcome.result?.failed ?? 0;\n // A file whose flows were all filtered out is not part of the run.\n if (passed + failed > 0) totals.files += 1;\n totals.passed += passed;\n totals.failed += failed;\n}\n\nasync function buildArgs(\n file: string,\n pass: RunPass,\n httpServer: ReturnType<typeof createNodeServer>,\n) {\n const found = await loadManifest(file);\n const manifest = found?.manifest;\n return {\n source: await readFile(file, \"utf8\"),\n uri: file,\n host: createNodeHost(),\n sink: pass.reporter.sink,\n httpClient: createFetchClient(),\n httpServer,\n filter: filterOf(pass.options),\n bail: pass.options.bail,\n env: await loadEnv({\n manifest,\n name: pass.options.env ?? \"local\",\n dir: found?.dir ?? dirname(file),\n }),\n io: createNodeModuleIo({\n paths: manifest?.paths ?? {},\n rootDir: found?.dir ?? dirname(file),\n }),\n npm: createNpmLoader({ root: found?.dir ?? dirname(file) }),\n };\n}\n\nfunction filterOf(options: RunOptions): RunFilter {\n return { tags: parseTags(options.tags), flow: options.flow, step: options.step };\n}\n\nfunction parseTags(tags: string | undefined): readonly string[] | undefined {\n if (!tags) return undefined;\n return tags\n .split(\",\")\n .map((tag) => tag.trim())\n .filter((tag) => tag !== \"\");\n}\n\nfunction noFiles(path: string): number {\n process.stderr.write(`No .vn files found at ${path}\\n`);\n return 1;\n}\n","import { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { PluginDefinition } from \"@venn-lang/sdk\";\n\n/**\n * `venn verify-plugin <path>`: import a plugin module, print what it declares,\n * and check its shape.\n *\n * @param args - `path` is the module to import, relative to the working\n * directory or absolute.\n * @returns 0 when the plugin names itself and a namespace, 1 when it does not.\n * @throws Error when the module exports nothing that looks like a plugin.\n */\nexport async function verifyPluginCommand(args: { path: string }): Promise<number> {\n const plugin = await loadPlugin(args.path);\n const out = process.stdout;\n out.write(`Plugin: ${plugin.name} (namespace \"${plugin.namespace}\")\\n`);\n out.write(` actions: ${plugin.actions?.length ?? 0}\\n`);\n out.write(` matchers: ${plugin.matchers?.length ?? 0}\\n`);\n out.write(` resources: ${plugin.resources?.length ?? 0}\\n`);\n const ok = Boolean(plugin.name && plugin.namespace);\n out.write(ok ? \"\\n✓ plugin shape is valid\\n\" : \"\\n✗ plugin shape is invalid\\n\");\n return ok ? 0 : 1;\n}\n\nasync function loadPlugin(path: string): Promise<PluginDefinition> {\n const mod = (await import(pathToFileURL(resolve(path)).href)) as Record<string, unknown>;\n const plugin = mod.default ?? Object.values(mod).find(isPlugin);\n if (!isPlugin(plugin)) throw new Error(`No plugin export found in ${path}`);\n return plugin;\n}\n\nfunction isPlugin(value: unknown): value is PluginDefinition {\n return typeof value === \"object\" && value !== null && \"namespace\" in value && \"name\" in value;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@venn-lang/cli",
3
+ "version": "0.1.0",
4
+ "description": "The venn binary: one command for starting, checking, running and testing a Venn project.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "cli",
10
+ "test-runner"
11
+ ],
12
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/cli#readme",
13
+ "bugs": "https://github.com/venn-lang/venn/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/venn-lang/venn.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Vinicius Borges",
21
+ "type": "module",
22
+ "bin": {
23
+ "venn": "./dist/bin/venn.mjs"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "development": "./src/index.ts",
28
+ "types": "./dist/index.d.mts",
29
+ "import": "./dist/index.mjs",
30
+ "default": "./dist/index.mjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.suite.ts"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "citty": "^0.2.2",
44
+ "@venn-lang/assert": "0.1.0",
45
+ "@venn-lang/contracts": "0.1.0",
46
+ "@venn-lang/core": "0.1.0",
47
+ "@venn-lang/dts": "0.1.0",
48
+ "@venn-lang/http": "0.1.0",
49
+ "@venn-lang/io": "0.1.0",
50
+ "@venn-lang/project": "0.1.0",
51
+ "@venn-lang/runtime": "0.1.0",
52
+ "@venn-lang/sdk": "0.1.0",
53
+ "@venn-lang/stdlib": "0.1.0",
54
+ "@venn-lang/types": "0.1.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^24",
58
+ "tsdown": "^0.22.14",
59
+ "typescript": "^7.0.2",
60
+ "vitest": "^4.1.10"
61
+ },
62
+ "scripts": {
63
+ "build": "tsdown",
64
+ "test": "vitest run",
65
+ "typecheck": "tsc --noEmit"
66
+ }
67
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { enableCompileCache } from "node:module";
3
+
4
+ /**
5
+ * The launcher, deliberately tiny.
6
+ *
7
+ * `enableCompileCache` keeps V8's compilation of the bundle on disk between
8
+ * runs, but only for modules loaded after it is called, so the engine has to
9
+ * arrive through a dynamic import rather than a static one. The URL is computed
10
+ * so the bundler leaves it alone: this file must stay separate from the file it
11
+ * loads, or there is nothing left to cache.
12
+ */
13
+ enableCompileCache?.();
14
+ await import(new URL("../cli.mjs", import.meta.url).href);
package/src/cli.ts ADDED
@@ -0,0 +1,299 @@
1
+ import { basename } from "node:path";
2
+ import type { ProxiedVerb, ScaffoldKind } from "@venn-lang/project";
3
+ import { defineCommand, runMain } from "citty";
4
+ import { buildCommand } from "./commands/build.js";
5
+ import { checkCommand } from "./commands/check.js";
6
+ import { depsCommand } from "./commands/deps.js";
7
+ import { fmtCommand } from "./commands/fmt.js";
8
+ import { listCommand } from "./commands/list.js";
9
+ import { newCommand } from "./commands/new.js";
10
+ import { runCommand } from "./commands/run.js";
11
+ import { scriptCommand } from "./commands/script.js";
12
+ import { upgradeCommand } from "./commands/upgrade.js";
13
+ import { verifyPluginCommand } from "./commands/verify-plugin.js";
14
+ import { targetsOrExit, worst } from "./project/index.js";
15
+ import { VERSION } from "./upgrade/version.js";
16
+
17
+ /**
18
+ * Optional, because a command inside a project already knows what it means:
19
+ * `venn test` is the suite, `venn run` is the program. A path given outright
20
+ * always wins: it is an answer, not a hint.
21
+ */
22
+ const TARGET = {
23
+ type: "positional",
24
+ required: false,
25
+ description: "A .vn file or folder (default: what this project says)",
26
+ } as const;
27
+ const PACKAGE = { type: "string", alias: "p", description: "act on one workspace member" } as const;
28
+ const FLOW = { type: "string", description: "only flows whose title contains this" } as const;
29
+ const STEP = { type: "string", description: "only steps whose title contains this" } as const;
30
+
31
+ const run = defineCommand({
32
+ meta: { name: "run", description: "Run a file as a program: its statements, top to bottom" },
33
+ args: {
34
+ target: TARGET,
35
+ package: PACKAGE,
36
+ bin: { type: "string", description: "which program, when the package has several" },
37
+ env: { type: "string", description: "environment from venn.toml (default: local)" },
38
+ },
39
+ run: async ({ args }) => {
40
+ const paths = await targetsOrExit({
41
+ kind: "run",
42
+ target: args.target,
43
+ packageName: args.package,
44
+ binName: args.bin,
45
+ });
46
+ const file = paths?.[0];
47
+ if (!file) return;
48
+ const ending = await scriptCommand({
49
+ file,
50
+ args: args._.slice(1),
51
+ env: args.env,
52
+ });
53
+ // Leaving is the program's call, not the exit code's: a server that ran its
54
+ // last line is still working, and one that said `exit 0` is not.
55
+ if (ending.leave) process.exit(ending.code);
56
+ },
57
+ });
58
+
59
+ const test = defineCommand({
60
+ meta: { name: "test", description: "Run every flow in a file or folder as a test suite" },
61
+ args: {
62
+ target: TARGET,
63
+ reporter: {
64
+ type: "string",
65
+ description:
66
+ "pretty | ndjson | dot | junit (default: pretty on a terminal, ndjson when piped)",
67
+ },
68
+ flow: FLOW,
69
+ step: STEP,
70
+ package: PACKAGE,
71
+ tags: { type: "string", description: "comma-separated @tags filter" },
72
+ env: { type: "string", description: "environment from venn.toml (default: local)" },
73
+ bail: { type: "boolean", description: "stop after the first failing flow" },
74
+ },
75
+ run: async ({ args }) => {
76
+ const paths = await targetsOrExit({
77
+ kind: "test",
78
+ target: args.target,
79
+ packageName: args.package,
80
+ });
81
+ if (!paths) return;
82
+ const codes: number[] = [];
83
+ for (const file of paths) {
84
+ codes.push(
85
+ await runCommand({
86
+ file,
87
+ reporter: args.reporter,
88
+ flow: args.flow,
89
+ step: args.step,
90
+ tags: args.tags,
91
+ env: args.env,
92
+ bail: args.bail,
93
+ }),
94
+ );
95
+ }
96
+ process.exitCode = worst(codes);
97
+ },
98
+ });
99
+
100
+ const list = defineCommand({
101
+ meta: { name: "list", description: "List the flows and steps that would run" },
102
+ args: { target: TARGET, flow: FLOW, step: STEP, package: PACKAGE },
103
+ run: async ({ args }) => {
104
+ const paths = await targetsOrExit({
105
+ kind: "test",
106
+ target: args.target,
107
+ packageName: args.package,
108
+ });
109
+ if (!paths) return;
110
+ const codes: number[] = [];
111
+ for (const file of paths) {
112
+ codes.push(await listCommand({ file, flow: args.flow, step: args.step }));
113
+ }
114
+ process.exitCode = worst(codes);
115
+ },
116
+ });
117
+
118
+ const fmt = defineCommand({
119
+ meta: { name: "fmt", description: "Format .vn files in place" },
120
+ args: {
121
+ target: TARGET,
122
+ package: PACKAGE,
123
+ check: { type: "boolean", description: "report what would change and fail, for CI" },
124
+ },
125
+ run: async ({ args }) => {
126
+ const paths = await targetsOrExit({
127
+ kind: "check",
128
+ target: args.target,
129
+ packageName: args.package,
130
+ });
131
+ if (paths) process.exitCode = await fmtCommand({ paths, check: args.check });
132
+ },
133
+ });
134
+
135
+ const check = defineCommand({
136
+ meta: { name: "check", description: "Statically check without running" },
137
+ args: { target: TARGET, package: PACKAGE },
138
+ run: async ({ args }) => {
139
+ const paths = await targetsOrExit({
140
+ kind: "check",
141
+ target: args.target,
142
+ packageName: args.package,
143
+ });
144
+ if (paths) process.exitCode = await checkCommand({ paths });
145
+ },
146
+ });
147
+
148
+ const KIND = {
149
+ lib: { type: "boolean", description: "a library: other packages use what it makes pub" },
150
+ bin: { type: "boolean", description: "a program: a CLI, a server, anything with a main" },
151
+ workspace: { type: "boolean", description: "a root that owns members, one lock and one target/" },
152
+ "dry-run": { type: "boolean", description: "print what would be written and write nothing" },
153
+ } as const;
154
+
155
+ /** A program unless told otherwise: it is what most projects turn out to be. */
156
+ function kindOf(args: Record<string, unknown>): ScaffoldKind {
157
+ if (args.workspace === true) return "workspace";
158
+ if (args.lib === true) return "lib";
159
+ return "bin";
160
+ }
161
+
162
+ const newCmd = defineCommand({
163
+ meta: { name: "new", description: "Start a project in a new directory" },
164
+ args: {
165
+ name: { type: "positional", required: true, description: "Name, and the directory to make" },
166
+ ...KIND,
167
+ },
168
+ run: async ({ args }) => {
169
+ process.exitCode = await newCommand({
170
+ dir: args.name,
171
+ name: basename(args.name),
172
+ kind: kindOf(args),
173
+ dryRun: args["dry-run"],
174
+ });
175
+ },
176
+ });
177
+
178
+ const init = defineCommand({
179
+ meta: { name: "init", description: "Start a project in the directory you are in" },
180
+ args: {
181
+ name: { type: "string", description: "Name (default: the directory's own)" },
182
+ ...KIND,
183
+ },
184
+ run: async ({ args }) => {
185
+ process.exitCode = await newCommand({
186
+ dir: ".",
187
+ name: args.name ?? basename(process.cwd()),
188
+ kind: kindOf(args),
189
+ dryRun: args["dry-run"],
190
+ });
191
+ },
192
+ });
193
+
194
+ const build = defineCommand({
195
+ meta: { name: "build", description: "Check every target and record the build" },
196
+ args: {
197
+ package: PACKAGE,
198
+ release: { type: "boolean", description: "the release profile: refuse to record a problem" },
199
+ },
200
+ run: async ({ args }) => {
201
+ process.exitCode = await buildCommand({ release: args.release, packageName: args.package });
202
+ },
203
+ });
204
+
205
+ const DEPS = {
206
+ packages: { type: "positional", required: false, description: "names, e.g. zod hono@^4" },
207
+ package: PACKAGE,
208
+ dev: { type: "boolean", alias: "D", description: "a development dependency" },
209
+ } as const;
210
+
211
+ /** The four verbs a person wants, run by whichever manager `[tooling]` names. */
212
+ function depsRunner(verb: ProxiedVerb) {
213
+ return async ({ args }: { args: Record<string, unknown> & { _: string[] } }) => {
214
+ process.exitCode = await depsCommand({
215
+ verb,
216
+ // Already without the subcommand's own name: `venn add zod hono` gives
217
+ // `["zod", "hono"]`, so slicing here would drop a package.
218
+ packages: args._,
219
+ dev: args.dev === true,
220
+ frozen: args.frozen === true,
221
+ packageName: args.package as string | undefined,
222
+ });
223
+ };
224
+ }
225
+
226
+ const add = defineCommand({
227
+ meta: { name: "add", description: "Add a dependency and install it" },
228
+ args: DEPS,
229
+ run: depsRunner("add"),
230
+ });
231
+
232
+ const remove = defineCommand({
233
+ meta: { name: "remove", description: "Remove a dependency and install without it" },
234
+ args: DEPS,
235
+ run: depsRunner("remove"),
236
+ });
237
+
238
+ const update = defineCommand({
239
+ meta: { name: "update", description: "Update what is installed within the ranges asked for" },
240
+ args: { package: PACKAGE },
241
+ run: depsRunner("update"),
242
+ });
243
+
244
+ const install = defineCommand({
245
+ meta: { name: "install", description: "Install what the manifest asks for" },
246
+ args: {
247
+ package: PACKAGE,
248
+ frozen: { type: "boolean", description: "refuse anything the lock did not record" },
249
+ },
250
+ run: depsRunner("install"),
251
+ });
252
+
253
+ const verifyPlugin = defineCommand({
254
+ meta: { name: "verify-plugin", description: "Inspect and verify a plugin module" },
255
+ args: { path: { type: "positional", required: true, description: "Path to the plugin module" } },
256
+ run: async ({ args }) => {
257
+ process.exitCode = await verifyPluginCommand({ path: args.path });
258
+ },
259
+ });
260
+
261
+ const upgrade = defineCommand({
262
+ meta: { name: "upgrade", description: "Update a global install to the latest version" },
263
+ args: {
264
+ yes: { type: "boolean", description: "Do not ask before upgrading", default: false },
265
+ "dry-run": {
266
+ type: "boolean",
267
+ description: "Print the command without running it",
268
+ default: false,
269
+ },
270
+ },
271
+ run: async ({ args }) => {
272
+ process.exitCode = await upgradeCommand({
273
+ version: VERSION,
274
+ options: { yes: args.yes, dryRun: args["dry-run"] },
275
+ });
276
+ },
277
+ });
278
+
279
+ const main = defineCommand({
280
+ meta: { name: "venn", description: "Venn CLI", version: VERSION },
281
+ subCommands: {
282
+ new: newCmd,
283
+ init,
284
+ add,
285
+ remove,
286
+ update,
287
+ install,
288
+ build,
289
+ run,
290
+ test,
291
+ list,
292
+ fmt,
293
+ check,
294
+ "verify-plugin": verifyPlugin,
295
+ upgrade,
296
+ },
297
+ });
298
+
299
+ void runMain(main);
@@ -0,0 +1,97 @@
1
+ import { createNodeFs } from "@venn-lang/contracts/node";
2
+ import {
3
+ type BuiltTarget,
4
+ join,
5
+ type Package,
6
+ type ProfileName,
7
+ type Project,
8
+ relativeTo,
9
+ writeBuildRecord,
10
+ } from "@venn-lang/project";
11
+ import { selectPackages, unknownPackage } from "../project/index.js";
12
+ import { reportProblems } from "../reporters/index.js";
13
+ import { checkProblems } from "./check.js";
14
+
15
+ export interface BuildArgs {
16
+ release?: boolean;
17
+ packageName?: string;
18
+ }
19
+
20
+ /**
21
+ * `venn build`: check every target of the selected packages, and record it.
22
+ *
23
+ * What lands in `target/<profile>/` is a record of what the project builds and
24
+ * whether it held together, not generated code.
25
+ *
26
+ * A build with problems fails either way. `strict` decides whether the record
27
+ * is written anyway: under `dev` it is, so the editor and the next build can see
28
+ * a half-written project. Under `release` nothing is written over a problem.
29
+ *
30
+ * @returns 0 when the build is clean, 1 when problems were found or no
31
+ * `venn.toml` governs the working directory.
32
+ */
33
+ export async function buildCommand(args: BuildArgs): Promise<number> {
34
+ const selection = await selectPackages({ from: process.cwd(), packageName: args.packageName });
35
+ if (!selection) {
36
+ process.stderr.write("VN2101 · no venn.toml here, or in any folder above it.\n");
37
+ return 1;
38
+ }
39
+ if (args.packageName && selection.packages.length === 0) {
40
+ process.stderr.write(unknownPackage(selection.project, args.packageName));
41
+ return 1;
42
+ }
43
+ const profile: ProfileName = args.release ? "release" : "debug";
44
+ return build({ project: selection.project, packages: selection.packages, profile });
45
+ }
46
+
47
+ async function build(args: {
48
+ project: Project;
49
+ packages: readonly Package[];
50
+ profile: ProfileName;
51
+ }): Promise<number> {
52
+ const found = await checkProblems(args.packages.map((one) => one.dir));
53
+ if (found.problems.length > 0) reportProblems([...found.problems]);
54
+ if (found.problems.length > 0 && isStrict(args.project, args.profile)) return 1;
55
+ const path = await writeBuildRecord({
56
+ fs: createNodeFs(),
57
+ root: args.project.root,
58
+ record: {
59
+ profile: args.profile,
60
+ targets: builtTargets(args.packages, args.project.root),
61
+ files: found.files,
62
+ problems: found.problems.length,
63
+ },
64
+ });
65
+ return announce({ ...args, files: found.files, problems: found.problems.length, path });
66
+ }
67
+
68
+ function builtTargets(packages: readonly Package[], root: string): BuiltTarget[] {
69
+ return packages.flatMap((one) =>
70
+ one.targets.map((target) => ({
71
+ package: one.manifest.name,
72
+ kind: target.kind,
73
+ name: target.name,
74
+ path: relativeTo(join(one.dir, target.path), root),
75
+ })),
76
+ );
77
+ }
78
+
79
+ /** `target/debug` is written by the `dev` profile, the way Cargo names them. */
80
+ function isStrict(project: Project, profile: ProfileName): boolean {
81
+ const key = profile === "debug" ? "dev" : "release";
82
+ return project.rootManifest.profiles[key]?.strict === true;
83
+ }
84
+
85
+ function announce(args: {
86
+ packages: readonly Package[];
87
+ profile: ProfileName;
88
+ files: number;
89
+ problems: number;
90
+ path: string;
91
+ }): number {
92
+ const targets = args.packages.reduce((so, one) => so + one.targets.length, 0);
93
+ process.stdout.write(
94
+ `${args.profile}: ${targets} target(s), ${args.files} file(s) → ${args.path}\n`,
95
+ );
96
+ return args.problems > 0 ? 1 : 0;
97
+ }