From 174d876e13303c6cdcc9f0292625f0a37093e27e Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Mon, 11 May 2026 20:58:04 -0500 Subject: [PATCH 1/5] refactor: migrate shell layer to tinyexec + per-command e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Big single landing for three intertwined improvements that were prepped on this branch: 1. **clibuddy: replace zx with tinyexec.** The new `ShellService` API is `run(cmd, args, opts?)` (verbose, streams stdio) and `runCaptured(cmd, args, opts?)` (silent, returns Output), both throwing `NonZeroExitError` by default. Drops `quote/isRaw/ defaultQuote/getPreferLocal/localBaseBinPath/mute()/quiet()/ isProcessOutput` and the `./test-helpers` export. Adds `resolveBinPath(pkg, { from, binPath?, binName? })` to find the absolute path to a package's bin even when its `exports` map is restrictive (oxlint) or absent (@biomejs/biome). 2. **run-run: use the new shell + tighten exec to string[].** Every `ToolService` now resolves its bin path with `resolveBinPath` and passes it to `ShellService.run` with `display: `, bypassing the `node_modules/.bin/` shims that run-run itself publishes (tools/biome, tools/oxlint, tools/oxfmt, tools/tsdown) which would otherwise loop. `ToolService.exec` now accepts only `string[]`. `tscheck` runs pre-scripts with `shell: true` so they can use `&&`, pipes, env-var substitution. 3. **vland init: prompt for install/git + fix git commit.** Adds default-yes confirms when the user doesn't pass --install/--no- install / --git/--no-git on the CLI. The git-commit failure (`pathspec 'initial' did not match`) caused by missing $'…' wrapping in the old custom quote() goes away naturally with array- based exec. **Tests reorganised.** Drop the clibuddy/test-helpers mock entirely. run-run integration tests are split into one file per command (cli, jsc, lint, format, tsc, build-lib), each spawning the real `rr` binary against a temp fixture (`makeFixture` helper) and asserting on observable output. vland init keeps its existing integration coverage (commit message exact-match, no pathspec errors, prompt-default behaviour in non-TTY). Verification: pnpm test (22 tests across 7 files, all green) and pnpm test:types (4 packages clean). Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/clibuddy-tinyexec.md | 27 ++++++ .changeset/run-run-shell-migration.md | 14 +++ .changeset/vland-init-prompts-and-git-fix.md | 7 ++ packages/clibuddy/package.json | 7 +- packages/clibuddy/src/run.ts | 5 +- packages/clibuddy/src/shell/create.ts | 47 +--------- packages/clibuddy/src/shell/index.ts | 1 + packages/clibuddy/src/shell/resolve-bin.ts | 53 +++++++++++ packages/clibuddy/src/shell/shell.ts | 91 ++++++++++-------- packages/clibuddy/src/shell/types.ts | 17 ++-- packages/clibuddy/src/shell/utils.ts | 17 +--- packages/clibuddy/test-helpers/index.ts | 35 ------- .../src/program/commands/test-static.ts | 2 +- .../run-run/src/program/commands/tscheck.ts | 9 +- packages/run-run/src/program/commands/x.ts | 3 +- packages/run-run/src/services/biome.ts | 36 +++---- packages/run-run/src/services/ctx.ts | 5 +- packages/run-run/src/services/oxfmt.ts | 12 +-- packages/run-run/src/services/oxlint.ts | 12 +-- packages/run-run/src/services/tool.ts | 54 +++-------- packages/run-run/src/services/tsc.ts | 6 +- packages/run-run/src/services/tsdown.ts | 6 +- packages/run-run/src/types/tool.ts | 2 +- packages/run-run/test/helpers.ts | 48 ++++++++-- .../test/integration/build-lib.test.ts | 23 +++++ packages/run-run/test/integration/cli.test.ts | 23 +++++ .../run-run/test/integration/doctor.test.ts | 44 --------- .../run-run/test/integration/format.test.ts | 32 +++++++ packages/run-run/test/integration/jsc.test.ts | 40 ++++++++ .../run-run/test/integration/lint.test.ts | 32 +++++++ .../test/integration/node-compat.test.ts | 12 --- packages/run-run/test/integration/tsc.test.ts | 59 ++++++++++++ packages/run-run/test/setup.ts | 2 - packages/vland/src/actions/init.ts | 48 ++++++---- packages/vland/src/program/commands/init.ts | 8 +- packages/vland/src/services/ctx.ts | 4 +- packages/vland/test/helpers.ts | 49 ++++++++++ .../vland/test/integration/init-git.test.ts | 50 ++++++++++ .../test/integration/init-install.test.ts | 22 +++++ .../test/integration/init-templates.test.ts | 69 ++++++++++++++ packages/vland/test/integration/init.test.ts | 93 ------------------- pnpm-lock.yaml | 19 ++-- 42 files changed, 709 insertions(+), 436 deletions(-) create mode 100644 .changeset/clibuddy-tinyexec.md create mode 100644 .changeset/run-run-shell-migration.md create mode 100644 .changeset/vland-init-prompts-and-git-fix.md create mode 100644 packages/clibuddy/src/shell/resolve-bin.ts delete mode 100644 packages/clibuddy/test-helpers/index.ts create mode 100644 packages/run-run/test/integration/build-lib.test.ts create mode 100644 packages/run-run/test/integration/cli.test.ts delete mode 100644 packages/run-run/test/integration/doctor.test.ts create mode 100644 packages/run-run/test/integration/format.test.ts create mode 100644 packages/run-run/test/integration/jsc.test.ts create mode 100644 packages/run-run/test/integration/lint.test.ts delete mode 100644 packages/run-run/test/integration/node-compat.test.ts create mode 100644 packages/run-run/test/integration/tsc.test.ts create mode 100644 packages/vland/test/helpers.ts create mode 100644 packages/vland/test/integration/init-git.test.ts create mode 100644 packages/vland/test/integration/init-install.test.ts create mode 100644 packages/vland/test/integration/init-templates.test.ts delete mode 100644 packages/vland/test/integration/init.test.ts diff --git a/.changeset/clibuddy-tinyexec.md b/.changeset/clibuddy-tinyexec.md new file mode 100644 index 0000000..9c20971 --- /dev/null +++ b/.changeset/clibuddy-tinyexec.md @@ -0,0 +1,27 @@ +--- +"@vlandoss/clibuddy": minor +--- + +**Breaking:** Replace `zx` with `tinyexec` and redesign `ShellService` around array-based exec. + +The previous tagged-template API (`shell.$\`...\``) and its surrounding helpers (`quote`, `isRaw`, `defaultQuote`, `getPreferLocal`, `localBaseBinPath`, `mute()`, `quiet()`, `isProcessOutput`, `./test-helpers` export) are gone. They duplicated zx internals, introduced a quoting bug for whitespace strings, and surfaced inconsistent `node_modules/.bin` resolution. + +New surface: + +- `shell.run(cmd, args, opts?)` — streams stdio to the terminal and prints `$ ` in verbose mode. Throws `NonZeroExitError` on non-zero exit by default. +- `shell.runCaptured(cmd, args, opts?)` — silent, returns the captured `Output { stdout, stderr, exitCode }`. Same throw-by-default semantics. +- `shell.at(cwd)` / `shell.child(opts)` — child shells with merged options. +- `RunOptions`: `cwd`, `env`, `verbose`, `throwOnError`, `shell` (pass-through `shell: true` for `&&`/pipes), `stdin`, `display` (override the verbose-printed name without affecting what's spawned). +- `resolveBinPath(pkg, { from, binPath?, binName? })` — resolves the absolute path to an installed package's binary even when its `exports` map is restrictive (oxlint) or absent (`@biomejs/biome`). +- `isNonZeroExitError(value)` — replaces `isProcessOutput`. + +`tinyexec` automatically prepends every parent `node_modules/.bin` to `PATH`, so `localBaseBinPath` / `getPreferLocal` are no longer needed. + +**Migration** + +- `await shell.$\`git init\`` → `await shell.run("git", ["init"])` +- `await shell.$\`git config\`.nothrow()` → `await shell.runCaptured("git", ["config", ...], { throwOnError: false })` +- `shell.mute()` → call `runCaptured` instead (silent by default). +- `createShellService({ localBaseBinPath: [dir] })` → drop the option; tinyexec walks up automatically. +- `isProcessOutput(err)` → `isNonZeroExitError(err)`. +- Tools wrapping a npm package (e.g. biome, tsdown) should resolve the bin path via `resolveBinPath` and pass it as the `cmd` with `display: ""` to avoid `node_modules/.bin/` shim loops. diff --git a/.changeset/run-run-shell-migration.md b/.changeset/run-run-shell-migration.md new file mode 100644 index 0000000..518a7aa --- /dev/null +++ b/.changeset/run-run-shell-migration.md @@ -0,0 +1,14 @@ +--- +"@vlandoss/run-run": patch +--- + +Internal migration to the new tinyexec-backed `ShellService` (see `@vlandoss/clibuddy`). + +- `ToolService.exec` now accepts only `string[]` (the `string` overload that silently word-split on spaces is gone). All tool services (`biome`, `oxlint`, `oxfmt`, `tsdown`, `tsc`) build their flags as arrays so each flag survives as its own argv entry. +- All tool services resolve their binary via `resolveBinPath` and pass the absolute path to `ShellService.run`. Doing so bypasses the `node_modules/.bin/` shims that run-run itself publishes (`tools/biome` etc.), which would otherwise loop back through `rr tools ` indefinitely. +- The verbose `$ ` line is preserved by passing `display: ` so users still see `$ biome check ...` instead of an absolute resolved path. +- `tscheck` runs `pretsc` / `pretypecheck` package scripts through `shell: true` so they can use `&&`, pipes, and env-var substitution. + +Tests reorganised into one e2e file per command (`cli`, `jsc`, `lint`, `format`, `tsc`, `build-lib`). Each spawns the real `rr` binary against a temp fixture (`makeFixture` helper) and asserts on observable output, so we no longer rely on a `clibuddy/test-helpers` mock. + +End-user CLI behaviour is unchanged. diff --git a/.changeset/vland-init-prompts-and-git-fix.md b/.changeset/vland-init-prompts-and-git-fix.md new file mode 100644 index 0000000..d3cccf1 --- /dev/null +++ b/.changeset/vland-init-prompts-and-git-fix.md @@ -0,0 +1,7 @@ +--- +"@vlandoss/vland": minor +--- + +`vland init` now prompts (default-yes) for installing dependencies and initialising a git repository when those flags aren't passed on the CLI. Use `--install` / `--no-install` and `--git` / `--no-git` to skip the prompts; in non-interactive contexts both default to `true`. + +Also fixes the git initialisation step: the commit message was being split on whitespace by the underlying shell layer, producing errors like `pathspec 'initial' did not match any file(s)` and leaving the repo half-initialised. The migration to `tinyexec` (via `@vlandoss/clibuddy`) makes each argv entry survive as a separate token, so the canonical first commit `chore: initial commit from vland` now lands cleanly. diff --git a/packages/clibuddy/package.json b/packages/clibuddy/package.json index a66b795..10f6a5d 100644 --- a/packages/clibuddy/package.json +++ b/packages/clibuddy/package.json @@ -15,8 +15,7 @@ "author": "rcrd ", "type": "module", "exports": { - ".": "./src/index.ts", - "./test-helpers": "./test-helpers/index.ts" + ".": "./src/index.ts" }, "files": [ "dist", @@ -35,8 +34,8 @@ "ansis": "4.2.0", "pkg-types": "2.3.0", "std-env": "3.9.0", - "yaml": "2.8.4", - "zx": "8.8.5" + "tinyexec": "1.1.2", + "yaml": "2.8.4" }, "publishConfig": { "access": "public", diff --git a/packages/clibuddy/src/run.ts b/packages/clibuddy/src/run.ts index 6e7e61d..9ef666d 100644 --- a/packages/clibuddy/src/run.ts +++ b/packages/clibuddy/src/run.ts @@ -1,4 +1,4 @@ -import { isProcessOutput } from "./shell/utils.ts"; +import { isNonZeroExitError } from "./shell/utils.ts"; function hasMessage(error: unknown): error is { message: string } { return ( @@ -18,7 +18,8 @@ export async function run(fn: () => Promise, logger: { error: (...args: un try { await fn(); } catch (error) { - if (!isProcessOutput(error)) { + // The subprocess already streamed its own stderr; don't double-print. + if (!isNonZeroExitError(error)) { logger.error(formatError(error)); } process.exit(1); diff --git a/packages/clibuddy/src/shell/create.ts b/packages/clibuddy/src/shell/create.ts index b4334c5..e3596dd 100644 --- a/packages/clibuddy/src/shell/create.ts +++ b/packages/clibuddy/src/shell/create.ts @@ -1,50 +1,9 @@ import fs from "node:fs"; import { ShellService } from "./shell.ts"; -import type { CreateOptions } from "./types.ts"; -import { getPreferLocal } from "./utils.ts"; +import type { ShellOptions } from "./types.ts"; export const cwd = fs.realpathSync(process.cwd()); -// Inspired by https://dub.sh/6tiHVgn -export function quote(arg: string) { - if (/^[\w./:=@-]+$/i.test(arg) || arg === "") { - return arg; - } - - return arg - .replace(/\\/g, "\\\\") - .replace(/'/g, "\\'") - .replace(/\f/g, "\\f") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\t/g, "\\t") - .replace(/\v/g, "\\v") - .replace(/\0/g, "\\0"); -} - -export const isRaw = (arg: unknown): arg is { stdout: string } => - typeof arg === "object" && arg !== null && "stdout" in arg && typeof arg.stdout === "string"; - -function defaultQuote(arg: unknown) { - if (typeof arg === "string") { - return quote(arg); - } - - if (isRaw(arg)) { - return arg.stdout; - } - - throw TypeError(`Unsupported argument type: ${typeof arg}`); -} - -export function createShellService(options: CreateOptions = {}) { - const preferLocal = getPreferLocal(options.localBaseBinPath); - - return new ShellService({ - verbose: true, - cwd, - preferLocal, - quote: defaultQuote, - ...options, - }); +export function createShellService(options: ShellOptions = {}): ShellService { + return new ShellService({ cwd, ...options }); } diff --git a/packages/clibuddy/src/shell/index.ts b/packages/clibuddy/src/shell/index.ts index d0263ca..8afd329 100644 --- a/packages/clibuddy/src/shell/index.ts +++ b/packages/clibuddy/src/shell/index.ts @@ -1,4 +1,5 @@ export * from "./create.ts"; +export * from "./resolve-bin.ts"; export * from "./shell.ts"; export * from "./types.ts"; export * from "./utils.ts"; diff --git a/packages/clibuddy/src/shell/resolve-bin.ts b/packages/clibuddy/src/shell/resolve-bin.ts new file mode 100644 index 0000000..cca6d92 --- /dev/null +++ b/packages/clibuddy/src/shell/resolve-bin.ts @@ -0,0 +1,53 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +export function resolveBinPath(pkg: string, options: { from: string; binPath?: string; binName?: string }): string { + const require = createRequire(options.from); + const pkgRoot = findPackageRoot(require, pkg); + + if (options.binPath) { + return path.join(pkgRoot, options.binPath); + } + + const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")) as { + bin?: string | Record; + }; + const bin = pkgJson.bin; + if (!bin) throw new Error(`Package ${pkg} has no "bin" field`); + + if (typeof bin === "string") return path.join(pkgRoot, bin); + + const wantName = options.binName ?? pkg.replace(/^@[^/]+\//, ""); + const rel = bin[wantName] ?? Object.values(bin)[0]; + if (!rel) throw new Error(`No bin entry found for ${pkg} (asked for ${wantName})`); + return path.join(pkgRoot, rel); +} + +// Two-step lookup tolerates packages that don't expose `./package.json` in +// their `exports` map (e.g. oxlint) and packages with no `main`/`exports` at +// all (e.g. @biomejs/biome) — `require.resolve(pkg)` fails for the latter. +function findPackageRoot(require: NodeJS.Require, pkg: string): string { + try { + return path.dirname(require.resolve(`${pkg}/package.json`)); + } catch { + // fall through to manual walk + } + + const mainPath = require.resolve(pkg); + let dir = path.dirname(mainPath); + const fsRoot = path.parse(dir).root; + while (dir !== fsRoot) { + const pkgJsonPath = path.join(dir, "package.json"); + if (fs.existsSync(pkgJsonPath)) { + try { + const data = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as { name?: string }; + if (data.name === pkg) return dir; + } catch { + // not a valid package.json — keep walking + } + } + dir = path.dirname(dir); + } + throw new Error(`Could not find package root for ${pkg} from ${mainPath}`); +} diff --git a/packages/clibuddy/src/shell/shell.ts b/packages/clibuddy/src/shell/shell.ts index ece6efb..b9d8827 100644 --- a/packages/clibuddy/src/shell/shell.ts +++ b/packages/clibuddy/src/shell/shell.ts @@ -1,65 +1,76 @@ -import { $ as make$ } from "zx"; -import type { Shell, ShellOptions } from "./types.ts"; -import { getPreferLocal } from "./utils.ts"; +import { type Output, x as tinyexec } from "tinyexec"; +import { palette } from "../colors.ts"; +import type { RunOptions, ShellOptions } from "./types.ts"; export class ShellService { - #shell: Shell; #options: ShellOptions; - constructor(options: ShellOptions) { - this.#options = Object.freeze(options); - this.#shell = make$(options); + constructor(options: ShellOptions = {}) { + this.#options = Object.freeze({ + cwd: options.cwd ?? process.cwd(), + env: options.env, + verbose: options.verbose ?? true, + }); } - get options() { + get options(): ShellOptions { return this.#options; } - get $() { - return this.#shell; + at(cwd: string): ShellService { + return this.child({ cwd }); } - child(options: ShellOptions) { + child(options: ShellOptions): ShellService { return new ShellService({ ...this.#options, ...options, + env: options.env ? { ...this.#options.env, ...options.env } : this.#options.env, }); } - quiet(options?: ShellOptions) { - return this.child({ - ...options, - verbose: false, - }); + quiet(): ShellService { + return this.child({ verbose: false }); } - mute(options?: ShellOptions) { - return this.quiet({ - ...options, - stdio: "pipe", + async run(cmd: string, args: string[] = [], opts: RunOptions = {}): Promise { + const merged = this.#mergeRunOpts(opts); + if (merged.verbose) printCmdLine(opts.display ?? cmd, args); + return tinyexec(cmd, args, { + throwOnError: opts.throwOnError ?? true, + ...(opts.stdin !== undefined && { stdin: opts.stdin }), + nodeOptions: { + cwd: merged.cwd, + ...(merged.env && { env: merged.env }), + stdio: "inherit", + ...(opts.shell && { shell: true }), + }, }); } - at(cwd: string, options?: ShellOptions) { - const getLocals = (locals: boolean | string | string[] | undefined) => - // NOTE: the boolean handling is done outside when determining preferLocal - typeof locals === "boolean" ? [] : typeof locals === "undefined" ? [] : Array.isArray(locals) ? locals : [locals]; - - const cwdPreferLocal = getPreferLocal(cwd); - - const preferLocal = - options?.preferLocal === false - ? false - : [ - ...getLocals(this.#options.preferLocal), - ...getLocals(options?.preferLocal), - ...(cwdPreferLocal ? cwdPreferLocal : []), - ]; - - return this.child({ - ...options, - cwd, - preferLocal, + async runCaptured(cmd: string, args: string[] = [], opts: RunOptions = {}): Promise { + const merged = this.#mergeRunOpts(opts); + return tinyexec(cmd, args, { + throwOnError: opts.throwOnError ?? true, + ...(opts.stdin !== undefined && { stdin: opts.stdin }), + nodeOptions: { + cwd: merged.cwd, + ...(merged.env && { env: merged.env }), + ...(opts.shell && { shell: true }), + }, }); } + + #mergeRunOpts(opts: RunOptions) { + return { + cwd: opts.cwd ?? this.#options.cwd ?? process.cwd(), + env: opts.env ? { ...this.#options.env, ...opts.env } : this.#options.env, + verbose: opts.verbose ?? this.#options.verbose ?? true, + }; + } +} + +function printCmdLine(cmd: string, args: string[]): void { + const tail = args.length === 0 ? "" : ` ${args.join(" ")}`; + process.stderr.write(`${palette.muted("$")} ${palette.highlight(cmd)}${tail}\n`); } diff --git a/packages/clibuddy/src/shell/types.ts b/packages/clibuddy/src/shell/types.ts index 7e32549..96d258b 100644 --- a/packages/clibuddy/src/shell/types.ts +++ b/packages/clibuddy/src/shell/types.ts @@ -1,9 +1,12 @@ -import type { Options as ZxOptions, Shell as ZxShell } from "zx"; - -export type Shell = ZxShell; - -export type ShellOptions = Partial; +export type ShellOptions = { + cwd?: string; + env?: NodeJS.ProcessEnv; + verbose?: boolean; +}; -export type CreateOptions = ShellOptions & { - localBaseBinPath?: string | Array; +export type RunOptions = ShellOptions & { + throwOnError?: boolean; + shell?: boolean; + stdin?: string; + display?: string; }; diff --git a/packages/clibuddy/src/shell/utils.ts b/packages/clibuddy/src/shell/utils.ts index df8eb8e..879ac05 100644 --- a/packages/clibuddy/src/shell/utils.ts +++ b/packages/clibuddy/src/shell/utils.ts @@ -1,16 +1,7 @@ -import path from "node:path"; -import { ProcessOutput } from "zx"; +import { NonZeroExitError } from "tinyexec"; -export function isProcessOutput(value: unknown): value is ProcessOutput { - return value instanceof ProcessOutput; -} - -const getLocalBinPath = (dirPath: string) => path.join(dirPath, "node_modules", ".bin"); +export { NonZeroExitError }; -export function getPreferLocal(localBaseBinPath: string | Array | undefined) { - return !localBaseBinPath - ? undefined - : Array.isArray(localBaseBinPath) - ? localBaseBinPath.map(getLocalBinPath) - : [localBaseBinPath].map(getLocalBinPath); +export function isNonZeroExitError(value: unknown): value is NonZeroExitError { + return value instanceof NonZeroExitError; } diff --git a/packages/clibuddy/test-helpers/index.ts b/packages/clibuddy/test-helpers/index.ts deleted file mode 100644 index 09093e8..0000000 --- a/packages/clibuddy/test-helpers/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { vi } from "vitest"; -import { isRaw } from "../src/index.ts"; - -vi.mock("zx", async (importOriginal) => { - const originalZx = await importOriginal(); - - const $$ = vi.fn(function fakeShell(strs: TemplateStringsArray, ...args: unknown[]) { - let output = ""; - let argsIndex = 0; - - const stringifyArg = (arg: unknown) => (isRaw(arg) ? (arg as { stdout: string }).stdout : arg); - - for (const str of strs) { - if (str === "") { - if (args[argsIndex]) { - output += stringifyArg(args[argsIndex]); - argsIndex += 1; - } - } else { - output += str; - } - } - - return output; - }); - - const $ = vi.fn(function make$() { - return $$; - }); - - return { - ...originalZx, - $, - }; -}); diff --git a/packages/run-run/src/program/commands/test-static.ts b/packages/run-run/src/program/commands/test-static.ts index 59fd13c..3778a4e 100644 --- a/packages/run-run/src/program/commands/test-static.ts +++ b/packages/run-run/src/program/commands/test-static.ts @@ -9,6 +9,6 @@ export function createTestStaticCommand(ctx: Context) { "Runs static tests, including linting, formatting checks, and TypeScript type checking, to ensure code quality and correctness without executing the code.", ) .action(async function testStaticAction() { - await ctx.shell.$`rr x jscheck tscheck`; + await ctx.shell.run("rr", ["x", "jscheck", "tscheck"]); }); } diff --git a/packages/run-run/src/program/commands/tscheck.ts b/packages/run-run/src/program/commands/tscheck.ts index ba7a054..f309320 100644 --- a/packages/run-run/src/program/commands/tscheck.ts +++ b/packages/run-run/src/program/commands/tscheck.ts @@ -34,7 +34,9 @@ async function typecheckAt({ dir, scripts, log, shell, run }: TypecheckAtOptions const preScript = getPreScript(scripts); if (preScript) { log.start(`Running pre-script: ${preScript}`); - await shellAt.$`${preScript}`; + // Pre-scripts come from package.json and may contain shell features + // (`&&`, pipes, env-var substitution) — run them through `/bin/sh -c`. + await shellAt.run(preScript, [], { shell: true }); log.success("Pre-script completed"); } @@ -71,11 +73,10 @@ export function createTsCheckCommand(ctx: Context) { const runTypecheck = async (shell: ShellService) => { if (config.future?.oxc) { const oxlint = new OxlintService(shell); - await oxlint.exec(`--type-aware --type-check --report-unused-disable-directives`); + await oxlint.exec(["--type-aware", "--type-check", "--report-unused-disable-directives"]); } else { const tsc = new TscService(shell); - await tsc.exec(`--noEmit`); - // await shell.$`tsc --noEmit`; + await tsc.exec(["--noEmit"]); } }; diff --git a/packages/run-run/src/program/commands/x.ts b/packages/run-run/src/program/commands/x.ts index 58ff54d..74d6f0d 100644 --- a/packages/run-run/src/program/commands/x.ts +++ b/packages/run-run/src/program/commands/x.ts @@ -7,8 +7,7 @@ export function createXCommand(ctx: Context) { .description("Run multiple rr subcommands concurrently (e.g. `rr x jsc tsc`).") .argument("", "rr subcommands to execute concurrently") .action(async function runXAction(cmds: string[]) { - const { $ } = ctx.shell; - const results = await Promise.allSettled(cmds.map((cmd) => $`rr ${cmd}`)); + const results = await Promise.allSettled(cmds.map((cmd) => ctx.shell.run("rr", [cmd]))); if (results.some((r) => r.status === "rejected")) { process.exitCode = 1; } diff --git a/packages/run-run/src/services/biome.ts b/packages/run-run/src/services/biome.ts index d220df4..e2c5321 100644 --- a/packages/run-run/src/services/biome.ts +++ b/packages/run-run/src/services/biome.ts @@ -1,48 +1,38 @@ -import { createRequire } from "node:module"; -import { isCI, type ShellService } from "@vlandoss/clibuddy"; +import { isCI, resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { FormatOptions, Formatter, Linter, LintOptions, StaticChecker, StaticCheckerOptions } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; +const COMMON_FLAGS = ["--colors=force", "--no-errors-on-unmatched"]; + export class BiomeService extends ToolService implements Formatter, Linter, StaticChecker { constructor(shellService: ShellService) { super({ bin: "biome", ui: TOOL_LABELS.BIOME, shellService }); } override getBinDir() { - const require = createRequire(import.meta.url); - return require.resolve("@biomejs/biome/bin/biome"); + return resolveBinPath("@biomejs/biome", { from: import.meta.url, binName: "biome" }); } async format(options: FormatOptions) { - const commonOptions = "format --colors=force --no-errors-on-unmatched"; - - if (options.fix) { - await this.exec(`${commonOptions} --fix`); - } else { - await this.exec(`${commonOptions}`); - } + const args = ["format", ...COMMON_FLAGS]; + if (options.fix) args.push("--fix"); + await this.exec(args); } async lint(options: LintOptions) { - const commonOptions = "check --colors=force --no-errors-on-unmatched --formatter-enabled=false"; - - if (options.fix) { - await this.exec(`${commonOptions} --fix --unsafe`); - } else { - await this.exec(`${commonOptions}`); - } + const args = ["check", ...COMMON_FLAGS, "--formatter-enabled=false"]; + if (options.fix) args.push("--fix", "--unsafe"); + await this.exec(args); } async check(options: StaticCheckerOptions): Promise { - const commonOptions = (cmd = "check") => `${cmd} --colors=force --no-errors-on-unmatched`; - if (options.fix) { - await this.exec(`${commonOptions()} --fix`); + await this.exec(["check", ...COMMON_FLAGS, "--fix"]); } else if (options.fixStaged) { - await this.exec(`${commonOptions()} --fix --staged`); + await this.exec(["check", ...COMMON_FLAGS, "--fix", "--staged"]); } else { - await this.exec(`${commonOptions(isCI ? "ci" : "check")}`); + await this.exec([isCI ? "ci" : "check", ...COMMON_FLAGS]); } } } diff --git a/packages/run-run/src/services/ctx.ts b/packages/run-run/src/services/ctx.ts index 6d6663b..784b659 100644 --- a/packages/run-run/src/services/ctx.ts +++ b/packages/run-run/src/services/ctx.ts @@ -32,10 +32,7 @@ export async function createContext(binDir: string): Promise { debug("app pkg info: %O", appPkg.info()); debug("bin pkg info: %O", binPkg.info()); - const shell = createShellService({ - localBaseBinPath: [binDir], - stdio: "inherit", - }); + const shell = createShellService(); debug("shell service options: %O", shell.options); diff --git a/packages/run-run/src/services/oxfmt.ts b/packages/run-run/src/services/oxfmt.ts index d05b0df..5ad557d 100644 --- a/packages/run-run/src/services/oxfmt.ts +++ b/packages/run-run/src/services/oxfmt.ts @@ -1,4 +1,4 @@ -import type { ShellService } from "@vlandoss/clibuddy"; +import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { FormatOptions, Formatter } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; @@ -9,16 +9,10 @@ export class OxfmtService extends ToolService implements Formatter { } override getBinDir() { - return require.resolve("oxfmt/bin/oxfmt"); + return resolveBinPath("oxfmt", { from: import.meta.url }); } async format(options: FormatOptions) { - const commonOptions = "--no-error-on-unmatched-pattern"; - - if (options.fix) { - await this.exec(`${commonOptions} --fix`); - } else { - await this.exec(`${commonOptions} --check`); - } + await this.exec(["--no-error-on-unmatched-pattern", options.fix ? "--fix" : "--check"]); } } diff --git a/packages/run-run/src/services/oxlint.ts b/packages/run-run/src/services/oxlint.ts index 3f48d0c..20eef3c 100644 --- a/packages/run-run/src/services/oxlint.ts +++ b/packages/run-run/src/services/oxlint.ts @@ -1,4 +1,4 @@ -import type { ShellService } from "@vlandoss/clibuddy"; +import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { Linter, LintOptions } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; @@ -9,16 +9,10 @@ export class OxlintService extends ToolService implements Linter { } override getBinDir() { - return require.resolve("oxlint/bin/oxlint"); + return resolveBinPath("oxlint", { from: import.meta.url }); } async lint(options: LintOptions) { - const commonOptions = "--report-unused-disable-directives"; - - if (options.fix) { - await this.exec(`${commonOptions} --fix`); - } else { - await this.exec(`${commonOptions} --check`); - } + await this.exec(["--report-unused-disable-directives", options.fix ? "--fix" : "--check"]); } } diff --git a/packages/run-run/src/services/tool.ts b/packages/run-run/src/services/tool.ts index d4ce216..37db8f7 100644 --- a/packages/run-run/src/services/tool.ts +++ b/packages/run-run/src/services/tool.ts @@ -1,7 +1,4 @@ -import fs from "node:fs"; -import path from "node:path"; import type { ShellService } from "@vlandoss/clibuddy"; -import memoize from "memoize"; import type { DoctorResult } from "#src/types/tool.ts"; type CreateOptions = { @@ -21,51 +18,22 @@ export abstract class ToolService { this.#shellService = shellService; } - getBinDir?(): string; + // Must return an absolute path so we bypass the `node_modules/.bin/` + // shims that run-run itself publishes (`tools/biome`, etc) — otherwise + // calling the friendly name loops back through `rr tools `. + abstract getBinDir(): string; - async exec(args?: string | string[]) { - const shell = this.#shell(); - return this.#run(shell, args); + async exec(args: string[] = []) { + return this.#shellService.run(this.getBinDir(), args, { display: this.#bin }); } async doctor(): Promise { - const shell = this.#shell().mute(); - - const output = await this.#run(shell, "--help"); + const output = await this.#shellService.runCaptured(this.getBinDir(), ["--help"], { throwOnError: false }); const ok = output.exitCode === 0; - - return { ok, output }; - } - - #shell = memoize((cwd?: string) => { - const preferLocal = this.#getPreferLocal(); - - return this.#shellService.child({ - ...(cwd && { cwd }), - ...(preferLocal && { preferLocal }), - }); - }); - - #run(shell: ShellService, args?: string | string[]) { - if (!args) { - return shell.$`${this.#bin}`; - } - - return shell.$`${this.#bin} ${typeof args === "string" ? args : args.join(" ")}`; - } - - #getPreferLocal() { - if (!this.getBinDir) { - return undefined; - } - - try { - const binPath = this.getBinDir(); - const isDir = fs.statSync(binPath).isDirectory(); - return isDir ? binPath : path.dirname(binPath); - } catch { - return undefined; - } + return { + ok, + output: { stdout: output.stdout, stderr: output.stderr, exitCode: output.exitCode }, + }; } get bin() { diff --git a/packages/run-run/src/services/tsc.ts b/packages/run-run/src/services/tsc.ts index 172ff77..6da12b5 100644 --- a/packages/run-run/src/services/tsc.ts +++ b/packages/run-run/src/services/tsc.ts @@ -1,4 +1,4 @@ -import type { ShellService } from "@vlandoss/clibuddy"; +import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import { ToolService } from "./tool.ts"; @@ -6,4 +6,8 @@ export class TscService extends ToolService { constructor(shellService: ShellService) { super({ bin: "tsc", ui: TOOL_LABELS.TSC, shellService }); } + + override getBinDir() { + return resolveBinPath("typescript", { from: import.meta.url, binName: "tsc" }); + } } diff --git a/packages/run-run/src/services/tsdown.ts b/packages/run-run/src/services/tsdown.ts index dc85eff..4134d3f 100644 --- a/packages/run-run/src/services/tsdown.ts +++ b/packages/run-run/src/services/tsdown.ts @@ -1,4 +1,4 @@ -import type { ShellService } from "@vlandoss/clibuddy"; +import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import { ToolService } from "./tool.ts"; @@ -7,6 +7,10 @@ export class TsdownService extends ToolService { super({ bin: "tsdown", ui: TOOL_LABELS.TSDOWN, shellService }); } + override getBinDir() { + return resolveBinPath("tsdown", { from: import.meta.url }); + } + async buildLib() { await this.exec(); } diff --git a/packages/run-run/src/types/tool.ts b/packages/run-run/src/types/tool.ts index 17450a4..2ab8a9d 100644 --- a/packages/run-run/src/types/tool.ts +++ b/packages/run-run/src/types/tool.ts @@ -14,7 +14,7 @@ export type StaticCheckerOptions = { export type DoctorOutput = { stdout: string; stderr: string; - exitCode: number | null; + exitCode: number | undefined; }; export type DoctorResult = { diff --git a/packages/run-run/test/helpers.ts b/packages/run-run/test/helpers.ts index d4b115c..47e5989 100644 --- a/packages/run-run/test/helpers.ts +++ b/packages/run-run/test/helpers.ts @@ -1,17 +1,22 @@ import { spawnSync } from "node:child_process"; -import { resolve } from "node:path"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path, { resolve } from "node:path"; import { dirnameOf } from "@vlandoss/clibuddy"; -export function createTestCli(mode: "dev" | "prod" = "prod") { - const bin = resolve(dirnameOf(import.meta), "../bin"); +const BIN = resolve(dirnameOf(import.meta), "../bin"); - // NODE_ENV=test and TEST=true (injected by vitest) cause consola to suppress - // all output. Override them so the subprocess behaves like a real invocation. - // NO_COLOR=1 keeps assertions stable across color-library detection differences - // — tests check content, not terminal escape sequences. - return function cli(cmd: string) { - return spawnSync(bin, cmd.split(" "), { +type CliMode = "dev" | "prod"; + +type CliOptions = { + cwd?: string; +}; + +export function createTestCli(mode: CliMode = "prod") { + return function cli(cmd: string, opts: CliOptions = {}) { + return spawnSync(BIN, cmd.split(" ").filter(Boolean), { encoding: "utf8", + cwd: opts.cwd, env: mode === "dev" ? process.env @@ -24,3 +29,28 @@ export function createTestCli(mode: "dev" | "prod" = "prod") { }); }; } + +type FixtureFiles = Record; + +export function makeFixture(name: string, files: FixtureFiles): { dir: string; cleanup: () => void } { + const dir = mkdtempSync(path.join(tmpdir(), `rr-${name}-`)); + for (const [rel, content] of Object.entries(files)) { + const abs = path.join(dir, rel); + mkdirSync(path.dirname(abs), { recursive: true }); + writeFileSync(abs, content); + } + return { + dir, + cleanup: () => { + rmSync(dir, { recursive: true, force: true }); + }, + }; +} + +export const fixtures = { + pkg: (name = "rr-test-fixture") => `${JSON.stringify({ name, version: "0.0.0", private: true }, null, 2)}\n`, + biomeNoop: () => + `${JSON.stringify({ formatter: { enabled: false }, linter: { enabled: false }, assist: { enabled: false } }, null, 2)}\n`, + tsconfig: () => + `${JSON.stringify({ compilerOptions: { target: "es2020", module: "esnext", moduleResolution: "bundler", strict: true, noEmit: true, skipLibCheck: true } }, null, 2)}\n`, +}; diff --git a/packages/run-run/test/integration/build-lib.test.ts b/packages/run-run/test/integration/build-lib.test.ts new file mode 100644 index 0000000..079ccd6 --- /dev/null +++ b/packages/run-run/test/integration/build-lib.test.ts @@ -0,0 +1,23 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, fixtures, makeFixture } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr build:lib", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeFixture("build-lib", { + "package.json": fixtures.pkg(), + }); + }); + + afterEach(() => fixture.cleanup()); + + test("doctor: exits 0 and reports tsdown ok", () => { + const r = cli("build:lib doctor", { cwd: fixture.dir }); + expect(r.stderr).toBe(""); + expect(r.stdout).toContain("tsdown ok"); + expect(r.status).toBe(0); + }); +}); diff --git a/packages/run-run/test/integration/cli.test.ts b/packages/run-run/test/integration/cli.test.ts new file mode 100644 index 0000000..a0f90c1 --- /dev/null +++ b/packages/run-run/test/integration/cli.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; +import { createTestCli } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr (cli surface)", () => { + test("--help prints usage and exits 0", () => { + const r = cli("--help"); + expect(r.status).toBe(0); + expect(r.stdout).toContain("Usage:"); + }); + + test("--version prints a semver-shaped string", () => { + const r = cli("--version"); + expect(r.status).toBe(0); + expect(r.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + }); + + test("an unknown command exits non-zero", () => { + const r = cli("definitely-not-a-real-command"); + expect(r.status).not.toBe(0); + }); +}); diff --git a/packages/run-run/test/integration/doctor.test.ts b/packages/run-run/test/integration/doctor.test.ts deleted file mode 100644 index ebb3d49..0000000 --- a/packages/run-run/test/integration/doctor.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, test } from "vitest"; -import { createTestCli } from "#test/helpers.ts"; - -const cli = createTestCli(); - -test("jsc doctor: exits 0 and reports biome ok", () => { - const result = cli("jsc doctor"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("biome ok"); - expect(result.status).toBe(0); -}); - -test("tsc doctor: exits 0 and reports tsc ok", () => { - const result = cli("tsc doctor"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("tsc ok"); - expect(result.status).toBe(0); -}); - -test("lint doctor: exits 0 and reports biome ok", () => { - const result = cli("lint doctor"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("biome ok"); - expect(result.status).toBe(0); -}); - -test("format doctor: exits 0 and reports biome ok", () => { - const result = cli("format doctor"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("biome ok"); - expect(result.status).toBe(0); -}); - -test("build:lib doctor: exits 0 and reports tsdown ok", () => { - const result = cli("build:lib doctor"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("tsdown ok"); - expect(result.status).toBe(0); -}); diff --git a/packages/run-run/test/integration/format.test.ts b/packages/run-run/test/integration/format.test.ts new file mode 100644 index 0000000..b4fe0a9 --- /dev/null +++ b/packages/run-run/test/integration/format.test.ts @@ -0,0 +1,32 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, fixtures, makeFixture } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr format", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeFixture("format", { + "package.json": fixtures.pkg(), + "biome.json": fixtures.biomeNoop(), + "src/ok.ts": "export const ok = 1;\n", + }); + }); + + afterEach(() => fixture.cleanup()); + + test("doctor: exits 0 and reports biome ok", () => { + const r = cli("format doctor", { cwd: fixture.dir }); + expect(r.stderr).toBe(""); + expect(r.stdout).toContain("biome ok"); + expect(r.status).toBe(0); + }); + + test("runs biome format end-to-end", () => { + const r = cli("format", { cwd: fixture.dir }); + const combined = r.stdout + r.stderr; + expect(combined).toMatch(/\$ biome format/); + expect(r.status).toBe(0); + }); +}); diff --git a/packages/run-run/test/integration/jsc.test.ts b/packages/run-run/test/integration/jsc.test.ts new file mode 100644 index 0000000..001b279 --- /dev/null +++ b/packages/run-run/test/integration/jsc.test.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, fixtures, makeFixture } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr jsc", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeFixture("jsc", { + "package.json": fixtures.pkg(), + "biome.json": fixtures.biomeNoop(), + "src/ok.ts": "export const ok = 1;\n", + }); + }); + + afterEach(() => fixture.cleanup()); + + test("doctor: exits 0 and reports biome ok", () => { + const r = cli("jsc doctor", { cwd: fixture.dir }); + expect(r.stderr).toBe(""); + expect(r.stdout).toContain("biome ok"); + expect(r.status).toBe(0); + }); + + test("runs biome check end-to-end on a clean fixture", () => { + const r = cli("jsc", { cwd: fixture.dir }); + const combined = r.stdout + r.stderr; + expect(combined).toMatch(/\$ biome check/); + expect(combined).not.toMatch(/expected `COMMAND/); + expect(r.status).toBe(0); + }); + + test("forwards each biome flag as its own argv entry", () => { + const r = cli("jsc", { cwd: fixture.dir }); + const combined = r.stdout + r.stderr; + expect(combined).toMatch(/\$ biome check --colors=force --no-errors-on-unmatched/); + expect(r.status).toBe(0); + }); +}); diff --git a/packages/run-run/test/integration/lint.test.ts b/packages/run-run/test/integration/lint.test.ts new file mode 100644 index 0000000..209048d --- /dev/null +++ b/packages/run-run/test/integration/lint.test.ts @@ -0,0 +1,32 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, fixtures, makeFixture } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr lint", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeFixture("lint", { + "package.json": fixtures.pkg(), + "biome.json": fixtures.biomeNoop(), + "src/ok.ts": "export const ok = 1;\n", + }); + }); + + afterEach(() => fixture.cleanup()); + + test("doctor: exits 0 and reports biome ok", () => { + const r = cli("lint doctor", { cwd: fixture.dir }); + expect(r.stderr).toBe(""); + expect(r.stdout).toContain("biome ok"); + expect(r.status).toBe(0); + }); + + test("runs biome check with formatter disabled end-to-end", () => { + const r = cli("lint", { cwd: fixture.dir }); + const combined = r.stdout + r.stderr; + expect(combined).toMatch(/\$ biome check .*--formatter-enabled=false/); + expect(r.status).toBe(0); + }); +}); diff --git a/packages/run-run/test/integration/node-compat.test.ts b/packages/run-run/test/integration/node-compat.test.ts deleted file mode 100644 index 4ee6eb5..0000000 --- a/packages/run-run/test/integration/node-compat.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from "vitest"; -import { createTestCli } from "#test/helpers.ts"; - -const cli = createTestCli(); - -test("node bin.mjs --help exits 0", () => { - const result = cli("--help"); - - expect(result.stderr).toBe(""); - expect(result.stdout).toContain("Usage:"); - expect(result.status).toBe(0); -}); diff --git a/packages/run-run/test/integration/tsc.test.ts b/packages/run-run/test/integration/tsc.test.ts new file mode 100644 index 0000000..051f9ff --- /dev/null +++ b/packages/run-run/test/integration/tsc.test.ts @@ -0,0 +1,59 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, fixtures, makeFixture } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("rr tsc", () => { + let fixture: { dir: string; cleanup: () => void }; + + afterEach(() => fixture?.cleanup()); + + describe("doctor", () => { + beforeEach(() => { + fixture = makeFixture("tsc-doctor", { + "package.json": fixtures.pkg(), + "tsconfig.json": fixtures.tsconfig(), + }); + }); + + test("exits 0 and reports tsc ok", () => { + const r = cli("tsc doctor", { cwd: fixture.dir }); + expect(r.stderr).toBe(""); + expect(r.stdout).toContain("tsc ok"); + expect(r.status).toBe(0); + }); + }); + + describe("happy path", () => { + beforeEach(() => { + fixture = makeFixture("tsc-ok", { + "package.json": fixtures.pkg(), + "tsconfig.json": fixtures.tsconfig(), + "src/ok.ts": "export const ok: number = 1;\n", + }); + }); + + test("exits 0 when types check cleanly", () => { + const r = cli("tsc", { cwd: fixture.dir }); + const combined = r.stdout + r.stderr; + expect(combined).toMatch(/\$ tsc --noEmit/); + expect(r.status).toBe(0); + }); + }); + + describe("type error path", () => { + beforeEach(() => { + fixture = makeFixture("tsc-bad", { + "package.json": fixtures.pkg(), + "tsconfig.json": fixtures.tsconfig(), + "src/bad.ts": 'export const bad: number = "not a number";\n', + }); + }); + + test("surfaces the type error and exits non-zero", () => { + const r = cli("tsc", { cwd: fixture.dir }); + expect(r.status).not.toBe(0); + expect(r.stdout + r.stderr).toMatch(/Type 'string' is not assignable to type 'number'/); + }); + }); +}); diff --git a/packages/run-run/test/setup.ts b/packages/run-run/test/setup.ts index 6f9223c..fe167b6 100644 --- a/packages/run-run/test/setup.ts +++ b/packages/run-run/test/setup.ts @@ -1,7 +1,5 @@ import { vi } from "vitest"; -// import "@vlandoss/clibuddy/test-helpers"; - // required to make the version command work independently of the package.json version process.env.VERSION = "0.0.0-test"; diff --git a/packages/vland/src/actions/init.ts b/packages/vland/src/actions/init.ts index 2e4831b..afc27a4 100644 --- a/packages/vland/src/actions/init.ts +++ b/packages/vland/src/actions/init.ts @@ -1,6 +1,6 @@ import { readdir } from "node:fs/promises"; import { isAbsolute, resolve } from "node:path"; -import { cancel, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts"; +import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts"; import { hasTTY, palette } from "@vlandoss/clibuddy"; import { detectPackageManager, installDependencies } from "nypm"; import type { Context } from "#src/services/ctx.ts"; @@ -13,8 +13,8 @@ export type InitOptions = { template?: TemplateName; dir?: string; pm?: "npm" | "pnpm" | "yarn" | "bun"; - install: boolean; - git: boolean; + install?: boolean; + git?: boolean; force: boolean; }; @@ -46,8 +46,8 @@ async function isDirEmpty(dir: string): Promise { async function readGitAuthor(shell: Context["shell"]): Promise { try { const [name, email] = await Promise.all([ - shell.$`git config --get user.name`.nothrow(), - shell.$`git config --get user.email`.nothrow(), + shell.runCaptured("git", ["config", "--get", "user.name"], { throwOnError: false }), + shell.runCaptured("git", ["config", "--get", "user.email"], { throwOnError: false }), ]); const trimmedName = name.stdout.trim(); const trimmedEmail = email.stdout.trim(); @@ -67,8 +67,7 @@ export async function runInit(ctx: Context, options: InitOptions) { const debug = logger.subdebug("init"); debug("options: %O", options); - // Mute zx so the @clack/prompts UI stays clean while git output is captured. - const shell = ctx.shell.mute(); + const shell = ctx.shell; intro(`${palette.label(" vland init ")}`); @@ -153,8 +152,12 @@ export async function runInit(ctx: Context, options: InitOptions) { await updateRootPackageName(dir, name); placeholderSpin.stop("Placeholders applied"); - // 7. Install deps - if (options.install) { + // 7. Resolve install / git decisions (prompt with default-yes when not set on CLI) + const shouldInstall = await resolveYesNo(options.install, "Install dependencies?"); + const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?"); + + // 8. Install deps + if (shouldInstall) { const detected = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm"; const installSpin = spinner(); installSpin.start(`Installing dependencies with ${palette.highlight(detected)}`); @@ -167,36 +170,35 @@ export async function runInit(ctx: Context, options: InitOptions) { debug("install error: %O", error); } } else { - log.info(`Skipping ${palette.highlight("install")} (--no-install).`); + log.info(`Skipping ${palette.highlight("install")}.`); } - // 8. Git init - if (options.git) { + // 9. Git init + if (shouldGit) { const gitSpin = spinner(); gitSpin.start("Initialising git repository"); try { const gitShell = shell.at(dir).child({ env: { - ...process.env, GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland", GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land", GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland", GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land", }, }); - await gitShell.$`git init`; - await gitShell.$`git add -A`; - await gitShell.$`git commit -m ${"chore: initial commit from vland"}`; + await gitShell.runCaptured("git", ["init"]); + await gitShell.runCaptured("git", ["add", "-A"]); + await gitShell.runCaptured("git", ["commit", "-m", "chore: initial commit from vland"]); gitSpin.stop("Initialised git repository"); } catch (error) { gitSpin.stop("Failed to initialise git", 1); debug("git error: %O", error); } } else { - log.info(`Skipping ${palette.highlight("git init")} (--no-git).`); + log.info(`Skipping ${palette.highlight("git init")}.`); } - // 9. Outro with next steps + // 10. Outro with next steps const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm"; outro( [ @@ -204,7 +206,15 @@ export async function runInit(ctx: Context, options: InitOptions) { "", palette.muted("Next steps:"), ` cd ${name}`, - options.install ? ` ${detectedPm} dev` : ` ${detectedPm} install && ${detectedPm} dev`, + shouldInstall ? ` ${detectedPm} dev` : ` ${detectedPm} install && ${detectedPm} dev`, ].join("\n"), ); } + +async function resolveYesNo(explicit: boolean | undefined, message: string): Promise { + if (typeof explicit === "boolean") return explicit; + if (!hasTTY) return true; + const value = await confirm({ message, initialValue: true }); + if (isCancel(value)) abort("Cancelled."); + return value as boolean; +} diff --git a/packages/vland/src/program/commands/init.ts b/packages/vland/src/program/commands/init.ts index 8f6683c..6134252 100644 --- a/packages/vland/src/program/commands/init.ts +++ b/packages/vland/src/program/commands/init.ts @@ -21,14 +21,20 @@ export function createInitCommand(ctx: Context) { .addOption(new Option("-t, --template ", "template to use").choices([...TEMPLATES])) .addOption(new Option("-d, --dir ", "target directory (default: ./)")) .addOption(new Option("--pm ", "package manager to use").choices(["npm", "pnpm", "yarn", "bun"])) + .addOption(new Option("--install", "install dependencies (skip prompt)")) .addOption(new Option("--no-install", "skip dependency installation")) + .addOption(new Option("--git", "initialise git repository (skip prompt)")) .addOption(new Option("--no-git", "skip git init")) .addOption(new Option("-f, --force", "overwrite existing directory").default(false)) - .action(async (name: string | undefined, options: InitOptions) => { + .action(async function (this: import("commander").Command, name: string | undefined, options: InitOptions) { console.log(getBannerText(ctx.binPkg.version)); + const installSource = this.getOptionValueSource("install"); + const gitSource = this.getOptionValueSource("git"); await runInit(ctx, { name, ...options, + install: installSource === "cli" ? options.install : undefined, + git: gitSource === "cli" ? options.git : undefined, }); }); } diff --git a/packages/vland/src/services/ctx.ts b/packages/vland/src/services/ctx.ts index fa2972b..5946207 100644 --- a/packages/vland/src/services/ctx.ts +++ b/packages/vland/src/services/ctx.ts @@ -22,9 +22,7 @@ export async function createContext(binDir: string): Promise { debug("bin pkg info: %O", binPkg.info()); - const shell = createShellService({ - localBaseBinPath: [binDir], - }); + const shell = createShellService(); debug("shell service options: %O", shell.options); diff --git a/packages/vland/test/helpers.ts b/packages/vland/test/helpers.ts new file mode 100644 index 0000000..65fe2ce --- /dev/null +++ b/packages/vland/test/helpers.ts @@ -0,0 +1,49 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +export { existsSync as pathExists }; + +const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..", ".."); +const TEMPLATES_DIR = path.join(REPO_ROOT, "templates"); +const VLAND_BIN = path.join(REPO_ROOT, "packages", "vland", "bin"); + +type CliMode = "dev" | "prod"; + +type CliOptions = { + cwd?: string; +}; + +export function createTestCli(mode: CliMode = "prod") { + return function cli(cmd: string, opts: CliOptions = {}) { + return spawnSync(VLAND_BIN, cmd.split(" ").filter(Boolean), { + encoding: "utf8", + cwd: opts.cwd, + env: + mode === "dev" + ? { ...process.env, VLAND_TEMPLATES_DIR: TEMPLATES_DIR } + : { + ...process.env, + NODE_ENV: "production", + TEST: undefined, + NO_COLOR: "1", + VLAND_TEMPLATES_DIR: TEMPLATES_DIR, + }, + }); + }; +} + +export function makeTmpDir(name: string): { dir: string; cleanup: () => void } { + const dir = mkdtempSync(path.join(tmpdir(), `vland-${name}-`)); + return { + dir, + cleanup: () => { + rmSync(dir, { recursive: true, force: true }); + }, + }; +} + +export function gitOutput(cwd: string, args: string[]): string { + return spawnSync("git", args, { cwd, encoding: "utf8" }).stdout.trim(); +} diff --git a/packages/vland/test/integration/init-git.test.ts b/packages/vland/test/integration/init-git.test.ts new file mode 100644 index 0000000..8a6a02f --- /dev/null +++ b/packages/vland/test/integration/init-git.test.ts @@ -0,0 +1,50 @@ +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, gitOutput, makeTmpDir, pathExists } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("vland init (git initialisation)", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeTmpDir("init-git"); + }); + + afterEach(() => fixture.cleanup()); + + test("--git: creates a repo with the canonical first commit", async () => { + const name = "git-on"; + const r = cli(`init ${name} -t library --no-install --git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + const combined = r.stdout + r.stderr; + expect(combined).not.toMatch(/pathspec/i); + expect(combined).not.toMatch(/Failed to initialise git/); + + const projectDir = path.join(fixture.dir, name); + expect(pathExists(path.join(projectDir, ".git"))).toBe(true); + expect(gitOutput(projectDir, ["log", "-1", "--pretty=%s"])).toBe("chore: initial commit from vland"); + expect(gitOutput(projectDir, ["rev-list", "--count", "HEAD"])).toBe("1"); + }); + + test("defaults to git init in non-interactive runs when --no-git is omitted", async () => { + const name = "git-default"; + const r = cli(`init ${name} -t library --no-install`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + expect(r.stdout + r.stderr).not.toMatch(/pathspec/i); + + const projectDir = path.join(fixture.dir, name); + expect(pathExists(path.join(projectDir, ".git"))).toBe(true); + expect(gitOutput(projectDir, ["log", "-1", "--pretty=%s"])).toBe("chore: initial commit from vland"); + }); + + test("--no-git: skips and prints a Skipping note", async () => { + const name = "git-off"; + const r = cli(`init ${name} -t library --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + expect(r.stdout + r.stderr).toMatch(/Skipping.*git init/); + + const projectDir = path.join(fixture.dir, name); + expect(pathExists(path.join(projectDir, ".git"))).toBe(false); + }); +}); diff --git a/packages/vland/test/integration/init-install.test.ts b/packages/vland/test/integration/init-install.test.ts new file mode 100644 index 0000000..54d0ed8 --- /dev/null +++ b/packages/vland/test/integration/init-install.test.ts @@ -0,0 +1,22 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, makeTmpDir } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("vland init (install resolution)", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeTmpDir("init-install"); + }); + + afterEach(() => fixture.cleanup()); + + test("--no-install: skips and recommends `install && dev` next", () => { + const r = cli("init install-off -t library --no-install --no-git", { cwd: fixture.dir }); + expect(r.status).toBe(0); + const out = r.stdout + r.stderr; + expect(out).toMatch(/Skipping.*install/); + expect(out).toMatch(/install && pnpm dev/); + }); +}); diff --git a/packages/vland/test/integration/init-templates.test.ts b/packages/vland/test/integration/init-templates.test.ts new file mode 100644 index 0000000..2425223 --- /dev/null +++ b/packages/vland/test/integration/init-templates.test.ts @@ -0,0 +1,69 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createTestCli, makeTmpDir } from "#test/helpers.ts"; + +const cli = createTestCli(); + +describe("vland init (template scaffolding)", () => { + let fixture: { dir: string; cleanup: () => void }; + + beforeEach(() => { + fixture = makeTmpDir("init-templates"); + }); + + afterEach(() => fixture.cleanup()); + + test("library: replaces placeholders and sets the package name", async () => { + const name = "my-lib"; + const r = cli(`init ${name} -t library --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + const projectDir = path.join(fixture.dir, name); + const pkg = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")); + expect(pkg.name).toBe(name); + + const license = await readFile(path.join(projectDir, "LICENSE"), "utf8"); + expect(license).not.toContain("{{"); + + const readme = await readFile(path.join(projectDir, "README.md"), "utf8"); + expect(readme).toContain(name); + expect(readme).not.toContain("{{"); + }); + + test("backend: clears placeholders and wires the logger service name", async () => { + const name = "my-api"; + const r = cli(`init ${name} -t backend --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + const projectDir = path.join(fixture.dir, name); + const pkg = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")); + expect(pkg.name).toBe(name); + expect(pkg.dependencies).toMatchObject({ elysia: expect.any(String), evlog: expect.any(String) }); + + const logger = await readFile(path.join(projectDir, "src", "logger.ts"), "utf8"); + expect(logger).toContain(`service: "${name}"`); + }); + + test("monorepo: rewrites workspace package names", async () => { + const name = "my-mono"; + const r = cli(`init ${name} -t monorepo --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + const projectDir = path.join(fixture.dir, name); + const apiPkg = JSON.parse(await readFile(path.join(projectDir, "apps", "api", "package.json"), "utf8")); + expect(apiPkg.name).toBe(`@${name}/api`); + expect(apiPkg.dependencies).toMatchObject({ [`@${name}/types`]: "workspace:*" }); + + const typesPkg = JSON.parse(await readFile(path.join(projectDir, "packages", "types", "package.json"), "utf8")); + expect(typesPkg.name).toBe(`@${name}/types`); + }); + + test("refuses to overwrite a non-empty target dir without --force", () => { + const name = "dup-lib"; + cli(`init ${name} -t library --no-install --no-git`, { cwd: fixture.dir }); + const second = cli(`init ${name} -t library --no-install --no-git`, { cwd: fixture.dir }); + expect(second.status).not.toBe(0); + expect(second.stdout + second.stderr).toMatch(/--force/); + }); +}); diff --git a/packages/vland/test/integration/init.test.ts b/packages/vland/test/integration/init.test.ts deleted file mode 100644 index b04dd81..0000000 --- a/packages/vland/test/integration/init.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..", "..", ".."); -const TEMPLATES_DIR = path.join(REPO_ROOT, "templates"); -const VLAND_BIN = path.join(REPO_ROOT, "packages", "vland", "bin"); - -// Same shape as run-run/test/helpers.ts: spawnSync with NODE_ENV=production, -// TEST cleared, NO_COLOR=1 so assertions don't depend on terminal capability. -function runVland(cwd: string, args: string[]) { - return spawnSync(VLAND_BIN, args, { - encoding: "utf8", - cwd, - env: { - ...process.env, - NODE_ENV: "production", - TEST: undefined, - NO_COLOR: "1", - VLAND_TEMPLATES_DIR: TEMPLATES_DIR, - }, - }); -} - -describe("vland init (against local templates/)", () => { - let tmp: string; - - beforeEach(async () => { - tmp = await mkdtemp(path.join(tmpdir(), "vland-it-")); - }); - - afterEach(async () => { - if (tmp) await rm(tmp, { recursive: true, force: true }); - }); - - it("scaffolds a library with placeholders replaced and the package name set", async () => { - const projectName = "my-lib"; - const result = runVland(tmp, ["init", projectName, "-t", "library", "--no-install", "--no-git"]); - expect(result.status).toBe(0); - - const projectDir = path.join(tmp, projectName); - await expect(stat(projectDir)).resolves.toMatchObject({}); - - const pkg = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")); - expect(pkg.name).toBe(projectName); - - // Placeholders are replaced everywhere - const license = await readFile(path.join(projectDir, "LICENSE"), "utf8"); - expect(license).not.toContain("{{"); - - const readme = await readFile(path.join(projectDir, "README.md"), "utf8"); - expect(readme).toContain(projectName); - expect(readme).not.toContain("{{"); - }); - - it("fails clearly on a non-empty target dir without --force", () => { - const projectName = "dup-lib"; - runVland(tmp, ["init", projectName, "-t", "library", "--no-install", "--no-git"]); - const second = runVland(tmp, ["init", projectName, "-t", "library", "--no-install", "--no-git"]); - expect(second.status).not.toBe(0); - expect(second.stdout + second.stderr).toMatch(/--force/); - }); - - it("scaffolds a backend template with all placeholders cleared", async () => { - const projectName = "my-api"; - const result = runVland(tmp, ["init", projectName, "-t", "backend", "--no-install", "--no-git"]); - expect(result.status).toBe(0); - - const projectDir = path.join(tmp, projectName); - const pkg = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")); - expect(pkg.name).toBe(projectName); - expect(pkg.dependencies).toMatchObject({ elysia: expect.any(String), evlog: expect.any(String) }); - - const logger = await readFile(path.join(projectDir, "src", "logger.ts"), "utf8"); - expect(logger).toContain(`service: "${projectName}"`); - }); - - it("scaffolds a monorepo with workspace package names rewritten", async () => { - const projectName = "my-mono"; - const result = runVland(tmp, ["init", projectName, "-t", "monorepo", "--no-install", "--no-git"]); - expect(result.status).toBe(0); - - const projectDir = path.join(tmp, projectName); - const apiPkg = JSON.parse(await readFile(path.join(projectDir, "apps", "api", "package.json"), "utf8")); - expect(apiPkg.name).toBe(`@${projectName}/api`); - expect(apiPkg.dependencies).toMatchObject({ [`@${projectName}/types`]: "workspace:*" }); - - const typesPkg = JSON.parse(await readFile(path.join(projectDir, "packages", "types", "package.json"), "utf8")); - expect(typesPkg.name).toBe(`@${projectName}/types`); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b804e4..fc37db6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,12 +56,12 @@ importers: std-env: specifier: 3.9.0 version: 3.9.0 + tinyexec: + specifier: 1.1.2 + version: 1.1.2 yaml: specifier: 2.8.4 version: 2.8.4 - zx: - specifier: 8.8.5 - version: 8.8.5 devDependencies: '@vlandoss/tsdown-config': specifier: workspace:^ @@ -1849,6 +1849,10 @@ packages: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -2072,11 +2076,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - zx@8.8.5: - resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==} - engines: {node: '>= 12.17.0'} - hasBin: true - snapshots: '@babel/code-frame@7.29.0': @@ -3561,6 +3560,8 @@ snapshots: tinyexec@1.1.1: {} + tinyexec@1.1.2: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -3717,5 +3718,3 @@ snapshots: write-file-atomic: 5.0.1 yaml@2.8.4: {} - - zx@8.8.5: {} From 87ebfca328f146e13362a0ab2ac54dc4dbc2e235 Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Mon, 11 May 2026 23:37:30 -0500 Subject: [PATCH 2/5] test(run-run): tolerate `biome ci` in CI environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BiomeService.check()` calls `biome ci` instead of `biome check` when `isCI` is true (std-env). The jsc tests asserted on `check` only, which made them green locally but red on GitHub Actions. Relax the regex to match either subcommand — the test's intent is that biome receives properly tokenized flags, not which subcommand it's invoked with. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/run-run/test/integration/jsc.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/run-run/test/integration/jsc.test.ts b/packages/run-run/test/integration/jsc.test.ts index 001b279..49faa2c 100644 --- a/packages/run-run/test/integration/jsc.test.ts +++ b/packages/run-run/test/integration/jsc.test.ts @@ -23,10 +23,11 @@ describe("rr jsc", () => { expect(r.status).toBe(0); }); - test("runs biome check end-to-end on a clean fixture", () => { + test("runs biome end-to-end on a clean fixture", () => { const r = cli("jsc", { cwd: fixture.dir }); const combined = r.stdout + r.stderr; - expect(combined).toMatch(/\$ biome check/); + // `check` locally, `ci` in CI — both are valid and exercise the same path. + expect(combined).toMatch(/\$ biome (check|ci)/); expect(combined).not.toMatch(/expected `COMMAND/); expect(r.status).toBe(0); }); @@ -34,7 +35,7 @@ describe("rr jsc", () => { test("forwards each biome flag as its own argv entry", () => { const r = cli("jsc", { cwd: fixture.dir }); const combined = r.stdout + r.stderr; - expect(combined).toMatch(/\$ biome check --colors=force --no-errors-on-unmatched/); + expect(combined).toMatch(/\$ biome (check|ci) --colors=force --no-errors-on-unmatched/); expect(r.status).toBe(0); }); }); From a483516f88ff5cf15bbb1fa81b1819e1975422a9 Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Mon, 11 May 2026 23:46:25 -0500 Subject: [PATCH 3/5] fix(vland): silence pnpm install output during init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clack spinner already says "Installing dependencies with pnpm…" but `installDependencies` from nypm was running with stdio inherited, so deprecation warnings (DEP0169 url.parse), upstream `unrun` ENOENT warnings, and pnpm's `Ignored build scripts` notice all leaked into the terminal — confusing UX next to the spinner. nypm exposes `silent: true` which pipes stdio. If install fails the spinner already flips to "Failed to install dependencies" and we debug-log the captured error. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/vland/src/actions/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vland/src/actions/init.ts b/packages/vland/src/actions/init.ts index afc27a4..4fba926 100644 --- a/packages/vland/src/actions/init.ts +++ b/packages/vland/src/actions/init.ts @@ -162,7 +162,7 @@ export async function runInit(ctx: Context, options: InitOptions) { const installSpin = spinner(); installSpin.start(`Installing dependencies with ${palette.highlight(detected)}`); try { - await installDependencies({ cwd: dir, packageManager: { name: detected, command: detected } }); + await installDependencies({ cwd: dir, packageManager: { name: detected, command: detected }, silent: true }); installSpin.stop(`Installed with ${palette.highlight(detected)}`); } catch (error) { installSpin.stop("Failed to install dependencies", 1); From f368be963ae2cdd20fa7e9a92c1b75642dab0b41 Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Mon, 11 May 2026 23:50:42 -0500 Subject: [PATCH 4/5] feat(vland): template-aware project name placeholder + outro script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project-name prompt always suggested `my-app` regardless of which template was picked, and the outro always recommended `pnpm dev` even for the `library` template (which has no `dev` script in its package.json). Add a `TEMPLATE_META` map keyed by template name with: - `placeholder` — used in the project-name `text()` prompt - `runScript` — appended to the outro's next-steps line Mappings: - library → `my-lib`, next: `pnpm test` (libraries don't have a dev server) - backend → `my-api`, next: `pnpm dev` - monorepo → `my-mono`, next: `pnpm dev` (turbo run dev) Add a backend-specific install-resolution test alongside the existing library one to lock in both branches. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/vland/src/actions/init.ts | 7 ++++--- packages/vland/src/actions/template.ts | 6 ++++++ packages/vland/test/integration/init-install.test.ts | 10 ++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/vland/src/actions/init.ts b/packages/vland/src/actions/init.ts index 4fba926..f5924ca 100644 --- a/packages/vland/src/actions/init.ts +++ b/packages/vland/src/actions/init.ts @@ -6,7 +6,7 @@ import { detectPackageManager, installDependencies } from "nypm"; import type { Context } from "#src/services/ctx.ts"; import { logger } from "#src/services/logger.ts"; import { replacePlaceholders, updateRootPackageName } from "./placeholders.ts"; -import { fetchTemplate, TEMPLATES, type TemplateName } from "./template.ts"; +import { fetchTemplate, TEMPLATE_META, TEMPLATES, type TemplateName } from "./template.ts"; export type InitOptions = { name?: string; @@ -92,7 +92,7 @@ export async function runInit(ctx: Context, options: InitOptions) { if (!hasTTY) abort("Project name is required in non-interactive environments. Pass it as the first argument."); const value = await text({ message: "Project name", - placeholder: "my-app", + placeholder: TEMPLATE_META[template].placeholder, validate: (input) => validateProjectName(input ?? ""), }); if (isCancel(value)) abort("Cancelled."); @@ -200,13 +200,14 @@ export async function runInit(ctx: Context, options: InitOptions) { // 10. Outro with next steps const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm"; + const runScript = TEMPLATE_META[template].runScript; outro( [ palette.success("Done!"), "", palette.muted("Next steps:"), ` cd ${name}`, - shouldInstall ? ` ${detectedPm} dev` : ` ${detectedPm} install && ${detectedPm} dev`, + shouldInstall ? ` ${detectedPm} ${runScript}` : ` ${detectedPm} install && ${detectedPm} ${runScript}`, ].join("\n"), ); } diff --git a/packages/vland/src/actions/template.ts b/packages/vland/src/actions/template.ts index f90c29c..460b5b3 100644 --- a/packages/vland/src/actions/template.ts +++ b/packages/vland/src/actions/template.ts @@ -6,6 +6,12 @@ import { logger } from "#src/services/logger.ts"; export const TEMPLATES = ["library", "backend", "monorepo"] as const; export type TemplateName = (typeof TEMPLATES)[number]; +export const TEMPLATE_META: Record = { + library: { placeholder: "my-lib", runScript: "test" }, + backend: { placeholder: "my-api", runScript: "dev" }, + monorepo: { placeholder: "my-mono", runScript: "dev" }, +}; + const GITHUB_SOURCE = "github:variableland/dx"; const GITHUB_REF = "main"; diff --git a/packages/vland/test/integration/init-install.test.ts b/packages/vland/test/integration/init-install.test.ts index 54d0ed8..393495e 100644 --- a/packages/vland/test/integration/init-install.test.ts +++ b/packages/vland/test/integration/init-install.test.ts @@ -12,11 +12,17 @@ describe("vland init (install resolution)", () => { afterEach(() => fixture.cleanup()); - test("--no-install: skips and recommends `install && dev` next", () => { + test("library: --no-install recommends `install && test` (no dev script in libraries)", () => { const r = cli("init install-off -t library --no-install --no-git", { cwd: fixture.dir }); expect(r.status).toBe(0); const out = r.stdout + r.stderr; expect(out).toMatch(/Skipping.*install/); - expect(out).toMatch(/install && pnpm dev/); + expect(out).toMatch(/install && pnpm test/); + }); + + test("backend: --no-install recommends `install && dev`", () => { + const r = cli("init install-off -t backend --no-install --no-git", { cwd: fixture.dir }); + expect(r.status).toBe(0); + expect(r.stdout + r.stderr).toMatch(/install && pnpm dev/); }); }); From 137024d6d488625cc2d4e8ccf8c19e0b26bfbf9b Mon Sep 17 00:00:00 2001 From: "Ricardo Q. Bazan" Date: Tue, 12 May 2026 17:15:33 -0500 Subject: [PATCH 5/5] refactor(clibuddy): replace resolve-bin walk-up with pkg-types + memoize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compress the bin-resolution helper from ~50 LOC of custom walk-up logic to ~20 LOC leveraging `pkg-types` (already a clibuddy dep) plus `memoize`. pkg-types' `resolvePackageJSON` handles oxlint/oxfmt-style restrictive `exports` maps (the `.` entry is exposed → resolves to a file inside the package → `findNearestFile` walks back up to package .json). A one-line `createRequire(from).resolve(/package.json)` fallback covers `@biomejs/biome` (no `main`/`exports` at all). API changes in clibuddy: - Rename file `shell/resolve-bin.ts` → `shell/resolve-package-bin.ts`. - Rename function `resolveBinPath` → `resolvePackageBin`. - Drop the unused `binPath` option. - Function is now async (pkg-types is async). - Wrap with `memoize` keyed by `pkg|from|binName` so concurrent calls share the in-flight promise and `rr x jsc tsc` doesn't re-resolve. run-run side: - ToolService now provides a concrete `getBinDir()` that reads `{ pkg, bin?, ui }` from the constructor. Subclasses (biome, oxlint, oxfmt, tsdown, tsc) collapse to a single `super(...)` call plus their tool-specific operations — no more per-subclass `override getBinDir`. - Bump tsdown 0.21.10 → 0.22.0 in run-run + tsdown-config peer dep. 0.21.x pulled in `unrun@0.2.38` whose tarball is missing `dist/`, producing `WARN Failed to create bin … unrun` on every install. 0.22 dropped unrun from dependencies. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/clibuddy-tinyexec.md | 6 +- .changeset/run-run-shell-migration.md | 4 +- packages/clibuddy/package.json | 1 + packages/clibuddy/src/shell/index.ts | 2 +- packages/clibuddy/src/shell/resolve-bin.ts | 53 --- .../clibuddy/src/shell/resolve-package-bin.ts | 26 ++ packages/run-run/README.md | 2 +- packages/run-run/package.json | 2 +- packages/run-run/src/services/biome.ts | 8 +- packages/run-run/src/services/oxfmt.ts | 8 +- packages/run-run/src/services/oxlint.ts | 8 +- packages/run-run/src/services/tool.ts | 58 +-- packages/run-run/src/services/tsc.ts | 8 +- packages/run-run/src/services/tsdown.ts | 8 +- packages/tsdown-config/package.json | 2 +- pnpm-lock.yaml | 354 +++++++++++++++++- 16 files changed, 433 insertions(+), 117 deletions(-) delete mode 100644 packages/clibuddy/src/shell/resolve-bin.ts create mode 100644 packages/clibuddy/src/shell/resolve-package-bin.ts diff --git a/.changeset/clibuddy-tinyexec.md b/.changeset/clibuddy-tinyexec.md index 9c20971..1ee1f0a 100644 --- a/.changeset/clibuddy-tinyexec.md +++ b/.changeset/clibuddy-tinyexec.md @@ -12,11 +12,13 @@ New surface: - `shell.runCaptured(cmd, args, opts?)` — silent, returns the captured `Output { stdout, stderr, exitCode }`. Same throw-by-default semantics. - `shell.at(cwd)` / `shell.child(opts)` — child shells with merged options. - `RunOptions`: `cwd`, `env`, `verbose`, `throwOnError`, `shell` (pass-through `shell: true` for `&&`/pipes), `stdin`, `display` (override the verbose-printed name without affecting what's spawned). -- `resolveBinPath(pkg, { from, binPath?, binName? })` — resolves the absolute path to an installed package's binary even when its `exports` map is restrictive (oxlint) or absent (`@biomejs/biome`). +- `resolvePackageBin(pkg, { from, binName? })` — async resolver that returns the absolute path to an installed package's binary, tolerating restrictive `exports` maps (oxlint) and packages without `main`/`exports` at all (`@biomejs/biome`). Memoised per `(pkg, from, binName)`. - `isNonZeroExitError(value)` — replaces `isProcessOutput`. `tinyexec` automatically prepends every parent `node_modules/.bin` to `PATH`, so `localBaseBinPath` / `getPreferLocal` are no longer needed. +New dependencies: `tinyexec` (replaces `zx`), `memoize` (for `resolvePackageBin`). + **Migration** - `await shell.$\`git init\`` → `await shell.run("git", ["init"])` @@ -24,4 +26,4 @@ New surface: - `shell.mute()` → call `runCaptured` instead (silent by default). - `createShellService({ localBaseBinPath: [dir] })` → drop the option; tinyexec walks up automatically. - `isProcessOutput(err)` → `isNonZeroExitError(err)`. -- Tools wrapping a npm package (e.g. biome, tsdown) should resolve the bin path via `resolveBinPath` and pass it as the `cmd` with `display: ""` to avoid `node_modules/.bin/` shim loops. +- Tools wrapping a npm package (e.g. biome, tsdown) should resolve the bin path via `resolvePackageBin` and pass it as the `cmd` with `display: ""` to avoid `node_modules/.bin/` shim loops. diff --git a/.changeset/run-run-shell-migration.md b/.changeset/run-run-shell-migration.md index 518a7aa..3df1f0d 100644 --- a/.changeset/run-run-shell-migration.md +++ b/.changeset/run-run-shell-migration.md @@ -5,9 +5,9 @@ Internal migration to the new tinyexec-backed `ShellService` (see `@vlandoss/clibuddy`). - `ToolService.exec` now accepts only `string[]` (the `string` overload that silently word-split on spaces is gone). All tool services (`biome`, `oxlint`, `oxfmt`, `tsdown`, `tsc`) build their flags as arrays so each flag survives as its own argv entry. -- All tool services resolve their binary via `resolveBinPath` and pass the absolute path to `ShellService.run`. Doing so bypasses the `node_modules/.bin/` shims that run-run itself publishes (`tools/biome` etc.), which would otherwise loop back through `rr tools ` indefinitely. -- The verbose `$ ` line is preserved by passing `display: ` so users still see `$ biome check ...` instead of an absolute resolved path. +- Bin resolution moves into the base `ToolService`: subclasses declare `{ pkg, bin?, ui }` in the constructor and the base resolves the absolute path via `resolvePackageBin` (memoised). The verbose `$ ` line is preserved via the `display` option so users still see `$ biome check ...` instead of an absolute resolved path. Resolving to the absolute path bypasses the `node_modules/.bin/` shims that run-run itself publishes (`tools/biome` etc.), which would otherwise loop back through `rr tools ` indefinitely. - `tscheck` runs `pretsc` / `pretypecheck` package scripts through `shell: true` so they can use `&&`, pipes, and env-var substitution. +- Bump `tsdown` from `0.21.10` to `0.22.0`. `tsdown@0.21.x` depends on `unrun@^0.2.37`, which pnpm resolved to `0.2.38` — whose published tarball is missing `dist/`, producing `WARN Failed to create bin … unrun` on every install. `tsdown@0.22.0` dropped `unrun` from `dependencies` (now an optional peer), erradicating the warning. Tests reorganised into one e2e file per command (`cli`, `jsc`, `lint`, `format`, `tsc`, `build-lib`). Each spawns the real `rr` binary against a temp fixture (`makeFixture` helper) and asserts on observable output, so we no longer rely on a `clibuddy/test-helpers` mock. diff --git a/packages/clibuddy/package.json b/packages/clibuddy/package.json index 10f6a5d..db64b79 100644 --- a/packages/clibuddy/package.json +++ b/packages/clibuddy/package.json @@ -32,6 +32,7 @@ "@pnpm/fs.find-packages": "1000.0.24", "@pnpm/types": "1001.3.0", "ansis": "4.2.0", + "memoize": "10.2.0", "pkg-types": "2.3.0", "std-env": "3.9.0", "tinyexec": "1.1.2", diff --git a/packages/clibuddy/src/shell/index.ts b/packages/clibuddy/src/shell/index.ts index 8afd329..2875250 100644 --- a/packages/clibuddy/src/shell/index.ts +++ b/packages/clibuddy/src/shell/index.ts @@ -1,5 +1,5 @@ export * from "./create.ts"; -export * from "./resolve-bin.ts"; +export * from "./resolve-package-bin.ts"; export * from "./shell.ts"; export * from "./types.ts"; export * from "./utils.ts"; diff --git a/packages/clibuddy/src/shell/resolve-bin.ts b/packages/clibuddy/src/shell/resolve-bin.ts deleted file mode 100644 index cca6d92..0000000 --- a/packages/clibuddy/src/shell/resolve-bin.ts +++ /dev/null @@ -1,53 +0,0 @@ -import fs from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; - -export function resolveBinPath(pkg: string, options: { from: string; binPath?: string; binName?: string }): string { - const require = createRequire(options.from); - const pkgRoot = findPackageRoot(require, pkg); - - if (options.binPath) { - return path.join(pkgRoot, options.binPath); - } - - const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8")) as { - bin?: string | Record; - }; - const bin = pkgJson.bin; - if (!bin) throw new Error(`Package ${pkg} has no "bin" field`); - - if (typeof bin === "string") return path.join(pkgRoot, bin); - - const wantName = options.binName ?? pkg.replace(/^@[^/]+\//, ""); - const rel = bin[wantName] ?? Object.values(bin)[0]; - if (!rel) throw new Error(`No bin entry found for ${pkg} (asked for ${wantName})`); - return path.join(pkgRoot, rel); -} - -// Two-step lookup tolerates packages that don't expose `./package.json` in -// their `exports` map (e.g. oxlint) and packages with no `main`/`exports` at -// all (e.g. @biomejs/biome) — `require.resolve(pkg)` fails for the latter. -function findPackageRoot(require: NodeJS.Require, pkg: string): string { - try { - return path.dirname(require.resolve(`${pkg}/package.json`)); - } catch { - // fall through to manual walk - } - - const mainPath = require.resolve(pkg); - let dir = path.dirname(mainPath); - const fsRoot = path.parse(dir).root; - while (dir !== fsRoot) { - const pkgJsonPath = path.join(dir, "package.json"); - if (fs.existsSync(pkgJsonPath)) { - try { - const data = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as { name?: string }; - if (data.name === pkg) return dir; - } catch { - // not a valid package.json — keep walking - } - } - dir = path.dirname(dir); - } - throw new Error(`Could not find package root for ${pkg} from ${mainPath}`); -} diff --git a/packages/clibuddy/src/shell/resolve-package-bin.ts b/packages/clibuddy/src/shell/resolve-package-bin.ts new file mode 100644 index 0000000..1d68cd7 --- /dev/null +++ b/packages/clibuddy/src/shell/resolve-package-bin.ts @@ -0,0 +1,26 @@ +import { createRequire } from "node:module"; +import path from "node:path"; +import memoize from "memoize"; +import { readPackageJSON, resolvePackageJSON } from "pkg-types"; + +type Options = { from: string; binName?: string }; + +// pkg-types covers any package whose `.` entry is in `exports` (including +// restrictive ones like oxlint). The fallback handles packages without +// `main`/`exports` at all (e.g. @biomejs/biome). +async function _resolvePackageBin(pkg: string, { from, binName }: Options): Promise { + let pkgJsonPath: string; + try { + pkgJsonPath = await resolvePackageJSON(pkg, { from }); + } catch { + pkgJsonPath = createRequire(from).resolve(`${pkg}/package.json`); + } + const { bin } = await readPackageJSON(pkgJsonPath); + const rel = typeof bin === "string" ? bin : bin?.[binName ?? pkg.replace(/^@[^/]+\//, "")]; + if (!rel) throw new Error(`No bin "${binName ?? pkg}" in ${pkg}`); + return path.join(path.dirname(pkgJsonPath), rel); +} + +export const resolvePackageBin = memoize(_resolvePackageBin, { + cacheKey: ([pkg, opts]) => `${pkg}|${opts.from}|${opts.binName ?? ""}`, +}); diff --git a/packages/run-run/README.md b/packages/run-run/README.md index 09337a8..030e4d8 100644 --- a/packages/run-run/README.md +++ b/packages/run-run/README.md @@ -4,7 +4,7 @@ CLI toolbox to fullstack common scripts in [Variable Land](https://variable.land ## Prerequisites -- Node.js >= 24.0.0 +- Node.js >= 20.0.0 ## Toolbox diff --git a/packages/run-run/package.json b/packages/run-run/package.json index 501742c..6e5bd60 100644 --- a/packages/run-run/package.json +++ b/packages/run-run/package.json @@ -76,7 +76,7 @@ "oxlint": "1.50.0", "oxlint-tsgolint": "0.15.0", "rimraf": "6.1.3", - "tsdown": "0.21.10", + "tsdown": "0.22.0", "typescript": "6.0.3" }, "devDependencies": { diff --git a/packages/run-run/src/services/biome.ts b/packages/run-run/src/services/biome.ts index e2c5321..357d533 100644 --- a/packages/run-run/src/services/biome.ts +++ b/packages/run-run/src/services/biome.ts @@ -1,4 +1,4 @@ -import { isCI, resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; +import { isCI, type ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { FormatOptions, Formatter, Linter, LintOptions, StaticChecker, StaticCheckerOptions } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; @@ -7,11 +7,7 @@ const COMMON_FLAGS = ["--colors=force", "--no-errors-on-unmatched"]; export class BiomeService extends ToolService implements Formatter, Linter, StaticChecker { constructor(shellService: ShellService) { - super({ bin: "biome", ui: TOOL_LABELS.BIOME, shellService }); - } - - override getBinDir() { - return resolveBinPath("@biomejs/biome", { from: import.meta.url, binName: "biome" }); + super({ pkg: "@biomejs/biome", bin: "biome", ui: TOOL_LABELS.BIOME, shellService }); } async format(options: FormatOptions) { diff --git a/packages/run-run/src/services/oxfmt.ts b/packages/run-run/src/services/oxfmt.ts index 5ad557d..fc598e6 100644 --- a/packages/run-run/src/services/oxfmt.ts +++ b/packages/run-run/src/services/oxfmt.ts @@ -1,15 +1,11 @@ -import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; +import type { ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { FormatOptions, Formatter } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; export class OxfmtService extends ToolService implements Formatter { constructor(shellService: ShellService) { - super({ bin: "oxfmt", ui: TOOL_LABELS.OXFMT, shellService }); - } - - override getBinDir() { - return resolveBinPath("oxfmt", { from: import.meta.url }); + super({ pkg: "oxfmt", ui: TOOL_LABELS.OXFMT, shellService }); } async format(options: FormatOptions) { diff --git a/packages/run-run/src/services/oxlint.ts b/packages/run-run/src/services/oxlint.ts index 20eef3c..42ff58d 100644 --- a/packages/run-run/src/services/oxlint.ts +++ b/packages/run-run/src/services/oxlint.ts @@ -1,15 +1,11 @@ -import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; +import type { ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import type { Linter, LintOptions } from "#src/types/tool.ts"; import { ToolService } from "./tool.ts"; export class OxlintService extends ToolService implements Linter { constructor(shellService: ShellService) { - super({ bin: "oxlint", ui: TOOL_LABELS.OXLINT, shellService }); - } - - override getBinDir() { - return resolveBinPath("oxlint", { from: import.meta.url }); + super({ pkg: "oxlint", ui: TOOL_LABELS.OXLINT, shellService }); } async lint(options: LintOptions) { diff --git a/packages/run-run/src/services/tool.ts b/packages/run-run/src/services/tool.ts index 37db8f7..41f58f3 100644 --- a/packages/run-run/src/services/tool.ts +++ b/packages/run-run/src/services/tool.ts @@ -1,46 +1,60 @@ -import type { ShellService } from "@vlandoss/clibuddy"; +import { resolvePackageBin, type ShellService } from "@vlandoss/clibuddy"; import type { DoctorResult } from "#src/types/tool.ts"; type CreateOptions = { - bin: string; - ui?: string; + pkg: string; + bin?: string; + ui: string; shellService: ShellService; }; -export abstract class ToolService { +export class ToolService { #shellService: ShellService; + #pkg: string; #bin: string; #ui: string; - constructor({ bin, ui, shellService }: CreateOptions) { - this.#bin = bin; - this.#ui = ui ?? bin; + get bin() { + return this.#bin; + } + + get ui() { + return this.#ui; + } + + get pkg() { + return this.#pkg; + } + + constructor({ pkg, bin, ui, shellService }: CreateOptions) { + this.#pkg = pkg; + this.#bin = bin ?? pkg; + this.#ui = ui; this.#shellService = shellService; } - // Must return an absolute path so we bypass the `node_modules/.bin/` - // shims that run-run itself publishes (`tools/biome`, etc) — otherwise - // calling the friendly name loops back through `rr tools `. - abstract getBinDir(): string; + async getBinDir() { + return resolvePackageBin(this.#pkg, { + from: import.meta.url, + binName: this.#bin, + }); + } async exec(args: string[] = []) { - return this.#shellService.run(this.getBinDir(), args, { display: this.#bin }); + return this.#shellService.run(await this.getBinDir(), args, { display: this.#bin }); } async doctor(): Promise { - const output = await this.#shellService.runCaptured(this.getBinDir(), ["--help"], { throwOnError: false }); + const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false }); const ok = output.exitCode === 0; + return { ok, - output: { stdout: output.stdout, stderr: output.stderr, exitCode: output.exitCode }, + output: { + stdout: output.stdout, + stderr: output.stderr, + exitCode: output.exitCode, + }, }; } - - get bin() { - return this.#bin; - } - - get ui() { - return this.#ui; - } } diff --git a/packages/run-run/src/services/tsc.ts b/packages/run-run/src/services/tsc.ts index 6da12b5..2cefd76 100644 --- a/packages/run-run/src/services/tsc.ts +++ b/packages/run-run/src/services/tsc.ts @@ -1,13 +1,9 @@ -import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; +import type { ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import { ToolService } from "./tool.ts"; export class TscService extends ToolService { constructor(shellService: ShellService) { - super({ bin: "tsc", ui: TOOL_LABELS.TSC, shellService }); - } - - override getBinDir() { - return resolveBinPath("typescript", { from: import.meta.url, binName: "tsc" }); + super({ pkg: "typescript", bin: "tsc", ui: TOOL_LABELS.TSC, shellService }); } } diff --git a/packages/run-run/src/services/tsdown.ts b/packages/run-run/src/services/tsdown.ts index 4134d3f..f1a4958 100644 --- a/packages/run-run/src/services/tsdown.ts +++ b/packages/run-run/src/services/tsdown.ts @@ -1,14 +1,10 @@ -import { resolveBinPath, type ShellService } from "@vlandoss/clibuddy"; +import type { ShellService } from "@vlandoss/clibuddy"; import { TOOL_LABELS } from "#src/program/ui.ts"; import { ToolService } from "./tool.ts"; export class TsdownService extends ToolService { constructor(shellService: ShellService) { - super({ bin: "tsdown", ui: TOOL_LABELS.TSDOWN, shellService }); - } - - override getBinDir() { - return resolveBinPath("tsdown", { from: import.meta.url }); + super({ pkg: "tsdown", ui: TOOL_LABELS.TSDOWN, shellService }); } async buildLib() { diff --git a/packages/tsdown-config/package.json b/packages/tsdown-config/package.json index fccf26c..131a882 100644 --- a/packages/tsdown-config/package.json +++ b/packages/tsdown-config/package.json @@ -29,6 +29,6 @@ "node": ">=20.0.0" }, "peerDependencies": { - "tsdown": "^0.21.10" + "tsdown": "^0.22.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc37db6..2ff7196 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: ansis: specifier: 4.2.0 version: 4.2.0 + memoize: + specifier: 10.2.0 + version: 10.2.0 pkg-types: specifier: 2.3.0 version: 2.3.0 @@ -134,8 +137,8 @@ importers: specifier: 6.1.3 version: 6.1.3 tsdown: - specifier: 0.21.10 - version: 0.21.10(typescript@6.0.3) + specifier: 0.22.0 + version: 0.22.0(typescript@6.0.3)(unrun@0.2.37) typescript: specifier: 6.0.3 version: 6.0.3 @@ -147,8 +150,8 @@ importers: packages/tsdown-config: dependencies: tsdown: - specifier: ^0.21.10 - version: 0.21.10(typescript@6.0.3) + specifier: ^0.22.0 + version: 0.22.0(typescript@6.0.3)(unrun@0.2.37) packages/vland: dependencies: @@ -188,10 +191,18 @@ packages: resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/generator@8.0.0-rc.4': + resolution: {integrity: sha512-YZ+FuIgkj7KrIb2a2X1XiY0QYgDxAbVbYP64SjwJzOK3euCsUerzenh2oqdsmKuPSlhzmFOOklnxzHAzXagvpw==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-string-parser@8.0.0-rc.3': resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-string-parser@8.0.0-rc.5': + resolution: {integrity: sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -200,11 +211,24 @@ packages: resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-identifier@8.0.0-rc.4': + resolution: {integrity: sha512-HTD3bskipk5MSm08twTW6832jzIXUhxMddy4NPPzIMuyMEsrs0ZgwAaMj5ubB5+6hMlUjDu17vNconEmwsmpYg==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@8.0.0-rc.5': + resolution: {integrity: sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/parser@8.0.0-rc.3': resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + '@babel/parser@8.0.0-rc.4': + resolution: {integrity: sha512-0S/1yefMa15N4i2v3t8Fw9pgMHhf2gF6Lc1UEXI96Ls6FNAjqvHHZouZ2ZS/deqLhbMFtmfVeFac6iTsvFbLwA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -213,6 +237,10 @@ packages: resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/types@8.0.0-rc.5': + resolution: {integrity: sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==} + engines: {node: ^22.18.0 || >=24.11.0} + '@bgotink/kdl@0.4.0': resolution: {integrity: sha512-F0uJCjo5FQvFdcGF5QbYVNfcGiRWlocuzyIdQxottZF2+gu6L2xjMGEu9PIpse2hifAca/19vIospgaETCKxIg==} @@ -554,6 +582,9 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.129.0': + resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} + '@oxfmt/binding-android-arm-eabi@0.35.0': resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -873,95 +904,187 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rolldown/binding-android-arm64@1.0.0': + resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@rolldown/binding-darwin-arm64@1.0.0': + resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0': + resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.17': resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0': + resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': + resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0': + resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0': + resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-ppc64-gnu@1.0.0': + resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + '@rolldown/binding-linux-s390x-gnu@1.0.0': + resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0': + resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0': + resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-openharmony-arm64@1.0.0': + resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0': + resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0': + resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0': + resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0': + resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} + '@rolldown/pluginutils@1.0.0-rc.17': resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} @@ -1285,6 +1408,15 @@ packages: oxc-resolver: optional: true + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1371,6 +1503,10 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -1413,6 +1549,10 @@ packages: resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} engines: {node: '>=20.19.0'} + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1754,6 +1894,30 @@ packages: vue-tsc: optional: true + rolldown-plugin-dts@0.25.0: + resolution: {integrity: sha512-GE3uDZgUuA9l6g+1u928TRmadd5IVhaWiwpWast2kCyLv9tYJJCC6E5HHkV0HGmwC5ZL73xh12/PRZI+KZ2vdQ==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0 + typescript: ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0: + resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rolldown@1.0.0-rc.17: resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1904,6 +2068,40 @@ packages: unplugin-unused: optional: true + tsdown@0.22.0: + resolution: {integrity: sha512-FgW0hHb27nGQA/+F3d5+U9wKXkfilk9DVkc5+7x/ZqF03g+Hoz/eeApT32jqxATt9eRoR+1jxk7MUMON+O4CXw==} + engines: {node: ^22.18.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.0 + '@tsdown/exe': 0.22.0 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2093,16 +2291,35 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/generator@8.0.0-rc.4': + dependencies: + '@babel/parser': 8.0.0-rc.4 + '@babel/types': 8.0.0-rc.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + '@babel/helper-string-parser@8.0.0-rc.3': {} + '@babel/helper-string-parser@8.0.0-rc.5': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-identifier@8.0.0-rc.3': {} + '@babel/helper-validator-identifier@8.0.0-rc.4': {} + + '@babel/helper-validator-identifier@8.0.0-rc.5': {} + '@babel/parser@8.0.0-rc.3': dependencies: '@babel/types': 8.0.0-rc.3 + '@babel/parser@8.0.0-rc.4': + dependencies: + '@babel/types': 8.0.0-rc.5 + '@babel/runtime@7.29.2': {} '@babel/types@8.0.0-rc.3': @@ -2110,6 +2327,11 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.3 '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/types@8.0.0-rc.5': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.5 + '@babel/helper-validator-identifier': 8.0.0-rc.5 + '@bgotink/kdl@0.4.0': {} '@biomejs/biome@2.4.4': @@ -2472,6 +2694,8 @@ snapshots: '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.129.0': {} + '@oxfmt/binding-android-arm-eabi@0.35.0': optional: true @@ -2684,42 +2908,85 @@ snapshots: dependencies: quansync: 1.0.0 + '@rolldown/binding-android-arm64@1.0.0': + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true + '@rolldown/binding-darwin-arm64@1.0.0': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true + '@rolldown/binding-darwin-x64@1.0.0': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true + '@rolldown/binding-freebsd-x64@1.0.0': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true + '@rolldown/binding-wasm32-wasi@1.0.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: '@emnapi/core': 1.10.0 @@ -2727,12 +2994,20 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0': + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true + '@rolldown/pluginutils@1.0.0': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} '@rollup/rollup-android-arm-eabi@4.60.2': @@ -2983,6 +3258,8 @@ snapshots: dts-resolver@2.1.3: {} + dts-resolver@3.0.0: {} + empathic@2.0.0: {} encoding@0.1.13: @@ -3090,6 +3367,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -3137,6 +3418,8 @@ snapshots: import-without-cache@0.3.3: {} + import-without-cache@0.4.0: {} + imurmurhash@0.1.4: {} individual@3.0.0: {} @@ -3451,6 +3734,43 @@ snapshots: transitivePeerDependencies: - oxc-resolver + rolldown-plugin-dts@0.25.0(rolldown@1.0.0)(typescript@6.0.3): + dependencies: + '@babel/generator': 8.0.0-rc.4 + '@babel/helper-validator-identifier': 8.0.0-rc.4 + '@babel/parser': 8.0.0-rc.4 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.1 + rolldown: 1.0.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0: + dependencies: + '@oxc-project/types': 0.129.0 + '@rolldown/pluginutils': 1.0.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0 + '@rolldown/binding-darwin-arm64': 1.0.0 + '@rolldown/binding-darwin-x64': 1.0.0 + '@rolldown/binding-freebsd-x64': 1.0.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0 + '@rolldown/binding-linux-arm64-gnu': 1.0.0 + '@rolldown/binding-linux-arm64-musl': 1.0.0 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0 + '@rolldown/binding-linux-s390x-gnu': 1.0.0 + '@rolldown/binding-linux-x64-gnu': 1.0.0 + '@rolldown/binding-linux-x64-musl': 1.0.0 + '@rolldown/binding-openharmony-arm64': 1.0.0 + '@rolldown/binding-wasm32-wasi': 1.0.0 + '@rolldown/binding-win32-arm64-msvc': 1.0.0 + '@rolldown/binding-win32-x64-msvc': 1.0.0 + rolldown@1.0.0-rc.17: dependencies: '@oxc-project/types': 0.127.0 @@ -3606,6 +3926,32 @@ snapshots: - synckit - vue-tsc + tsdown@0.22.0(typescript@6.0.3)(unrun@0.2.37): + dependencies: + ansis: 4.2.0 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.0 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0 + rolldown-plugin-dts: 0.25.0(rolldown@1.0.0)(typescript@6.0.3) + semver: 7.7.4 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + optionalDependencies: + typescript: 6.0.3 + unrun: 0.2.37 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - vue-tsc + tslib@2.8.1: optional: true