@dbx-tools/cli 0.1.112 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ {
2
+ "dependencies": [
3
+ {
4
+ "name": "@types/node",
5
+ "version": "catalog:",
6
+ "type": "build"
7
+ },
8
+ {
9
+ "name": "tsx",
10
+ "version": "^4.23.0",
11
+ "type": "build"
12
+ },
13
+ {
14
+ "name": "typescript",
15
+ "type": "build"
16
+ },
17
+ {
18
+ "name": "@clack/prompts",
19
+ "version": "catalog:",
20
+ "type": "runtime"
21
+ },
22
+ {
23
+ "name": "@dbx-tools/core",
24
+ "version": "workspace:*",
25
+ "type": "runtime"
26
+ },
27
+ {
28
+ "name": "@dbx-tools/shared-core",
29
+ "version": "workspace:*",
30
+ "type": "runtime"
31
+ },
32
+ {
33
+ "name": "commander",
34
+ "version": "catalog:",
35
+ "type": "runtime"
36
+ },
37
+ {
38
+ "name": "pnpm",
39
+ "type": "runtime"
40
+ }
41
+ ],
42
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
43
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "files": [
3
+ ".projen/deps.json",
4
+ ".projen/files.json",
5
+ ".projen/tasks.json",
6
+ "test/tsconfig.json",
7
+ "tsconfig.json"
8
+ ],
9
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
10
+ }
@@ -0,0 +1,121 @@
1
+ {
2
+ "manifestVersion": 3,
3
+ "env": {
4
+ "PATH": "$(pnpm -c exec \"node --print process.env.PATH\")"
5
+ },
6
+ "tasks": {
7
+ "build": {
8
+ "name": "build",
9
+ "description": "Full release build",
10
+ "steps": [
11
+ {
12
+ "spawn": "pre-compile"
13
+ },
14
+ {
15
+ "spawn": "compile"
16
+ },
17
+ {
18
+ "spawn": "post-compile"
19
+ },
20
+ {
21
+ "spawn": "test"
22
+ },
23
+ {
24
+ "spawn": "package"
25
+ }
26
+ ]
27
+ },
28
+ "compile": {
29
+ "name": "compile",
30
+ "description": "Only compile",
31
+ "steps": [
32
+ {
33
+ "execArgs": [
34
+ "tsc",
35
+ "--build"
36
+ ]
37
+ }
38
+ ]
39
+ },
40
+ "default": {
41
+ "name": "default",
42
+ "description": "Synthesize project files",
43
+ "steps": [
44
+ {
45
+ "exec": "pnpm exec projen default",
46
+ "cwd": "../../.."
47
+ }
48
+ ]
49
+ },
50
+ "install": {
51
+ "name": "install",
52
+ "description": "Install project dependencies and update lockfile (non-frozen)",
53
+ "steps": [
54
+ {
55
+ "exec": "pnpm i --no-frozen-lockfile"
56
+ }
57
+ ]
58
+ },
59
+ "install:ci": {
60
+ "name": "install:ci",
61
+ "description": "Install project dependencies using frozen lockfile",
62
+ "steps": [
63
+ {
64
+ "exec": "pnpm i --frozen-lockfile"
65
+ }
66
+ ]
67
+ },
68
+ "package": {
69
+ "name": "package",
70
+ "description": "Creates the distribution package",
71
+ "steps": [
72
+ {
73
+ "execArgs": [
74
+ "mkdir",
75
+ "-p",
76
+ "dist/js"
77
+ ]
78
+ },
79
+ {
80
+ "execArgs": [
81
+ "pnpm",
82
+ "pack",
83
+ "--pack-destination",
84
+ "dist/js"
85
+ ]
86
+ }
87
+ ]
88
+ },
89
+ "post-compile": {
90
+ "name": "post-compile",
91
+ "description": "Runs after successful compilation"
92
+ },
93
+ "pre-compile": {
94
+ "name": "pre-compile",
95
+ "description": "Prepare the project for compilation"
96
+ },
97
+ "test": {
98
+ "name": "test",
99
+ "description": "Run tests",
100
+ "steps": [
101
+ {
102
+ "exec": "tsx --test 'test/**/*.test.ts'"
103
+ }
104
+ ]
105
+ },
106
+ "watch": {
107
+ "name": "watch",
108
+ "description": "Watch & compile in the background",
109
+ "steps": [
110
+ {
111
+ "execArgs": [
112
+ "tsc",
113
+ "--build",
114
+ "-w"
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ },
120
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
121
+ }
package/README.md CHANGED
@@ -1,88 +1,74 @@
1
- # @dbx-tools/cli
1
+ # dbx-tools
2
2
 
3
- Scaffold, formatting, codegen, verification, build, and release helpers for Bun +
4
- workspaces monorepos. It wraps the shared `tsdown` config for building and a
5
- Bun-based publish flow; `dbxtools` keeps only the workspace automation that is
6
- orthogonal to writing package code.
3
+ CLI package for the dbx-tools projen workspace engine.
7
4
 
8
- ## Installation
5
+ Run the `dbxtools` command to bootstrap a repo, synthesize generated files,
6
+ rebuild barrels, generate OpenAPI clients, or clean generated output. Import the
7
+ package modules when custom tooling needs the same root detection, pnpm
8
+ delegation, or CLI program behavior.
9
9
 
10
- ```bash
11
- bun add -d @dbx-tools/cli
12
- ```
10
+ Key features:
13
11
 
14
- Then point your root `package.json` scripts at the `dbxtools` bin for the helper
15
- commands you want:
16
-
17
- ```jsonc
18
- {
19
- "scripts": {
20
- "format": "dbxtools format",
21
- "build": "dbxtools build",
22
- "codegen": "dbxtools codegen",
23
- "verify": "dbxtools verify",
24
- "create": "dbxtools create",
25
- "release": "dbxtools release",
26
- "tag": "dbxtools tag",
27
- },
28
- }
29
- ```
12
+ - `sync` bootstrap path for empty folders plus normal projen synthesis for
13
+ existing workspaces.
14
+ - Focused `--watch` loop for projenrc changes, barrel generation, and OpenAPI
15
+ generation.
16
+ - Standalone barrel and OpenAPI commands for post-synth workflows.
17
+ - Generated-file cleanup with an interactive or non-interactive mode.
18
+ - Importable CLI/root/pnpm helpers for tests and thin wrapper commands.
30
19
 
31
- ## Commands
20
+ ## Bootstrap Or Sync A Workspace
21
+
22
+ ```sh
23
+ dbxtools sync
24
+ dbxtools sync --watch
25
+ ```
32
26
 
33
- | Command | What it does |
34
- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
35
- | `dbxtools format` | `syncpack format`, regroup lifecycle hooks, then `prettier --write`. |
36
- | `dbxtools build` | Compile every publishable package with the shared `tsdown` config. |
37
- | `dbxtools codegen` | Regenerate each package's `generated/` zod tree from the `.d.ts` inputs its `package.json` declares. |
38
- | `dbxtools verify [--workspace-deps]` | Workspace verify pass; `--workspace-deps` fails on undeclared sibling imports. |
39
- | `dbxtools create [--plugin\|--shared] <slug>` | Scaffold a new package under `packages/<slug>/`. |
40
- | `dbxtools release [--dry-run]` | Build, then publish each package with a stamped (complete) manifest. |
41
- | `dbxtools tag [patch\|minor\|major]` | Version bump, commit, tag, push, and create a GitHub Release. `--notes-since v0.1.75` widens the notes baseline; default is the previous tag. `--no-ai-notes` skips Codex release notes. |
42
- | `dbxtools agent [prompt]` | Run `ucode codex exec` (`-t` / `--timeout` seconds). Prompt via args or stdin. Requires `ucode codex --version`. |
27
+ In an empty folder, `sync` creates the minimum pnpm/projen structure needed for
28
+ `@dbx-tools/projen`. In an existing workspace, it runs projen and the post-synth
29
+ generators. `--watch` starts focused watchers for structural changes, barrels,
30
+ and OpenAPI generation.
43
31
 
44
- Typecheck stays a plain `tsc` call:
32
+ ## Generate Barrels And OpenAPI Clients
45
33
 
46
- ```bash
47
- tsc --noEmit -p tsconfig.json
34
+ ```sh
35
+ dbxtools barrels
36
+ dbxtools openapi
48
37
  ```
49
38
 
50
- ## Configuration
39
+ `barrels` rebuilds package-root `index.ts` files from exported modules under
40
+ `src/`. `openapi` scans tsoa controllers and writes generated OpenAPI packages.
51
41
 
52
- Configuration is optional. Everything is auto-derived from the workspace; the
53
- only knobs live under a `dbxtools` key in the root `package.json`:
42
+ ## Clean Generated Output
54
43
 
55
- ```jsonc
56
- {
57
- "dbxtools": {
58
- "scope": "@acme", // npm scope used by `create` (default: most common scope in the workspace)
59
- "repo": "acme/widgets", // owner/name for release links (default: the `origin` git remote)
60
- },
61
- }
44
+ ```sh
45
+ dbxtools clean
46
+ dbxtools clean -y
62
47
  ```
63
48
 
64
- Each package's optional `codegen.inputs` field is read by convention.
49
+ `clean` removes generated read-only files and can also remove install output. Use
50
+ it before debugging synthesis drift or validating that the repo can regenerate
51
+ from source.
65
52
 
66
- ## Library API
67
-
68
- The same commands are exported as functions for projects that want to compose
69
- their own automation:
53
+ ## Use The CLI Internals
70
54
 
71
55
  ```ts
72
- import { build, codegen, create, release, tag, verify } from "@dbx-tools/cli";
73
-
74
- await build();
75
- await codegen();
76
- await verify();
77
- await create({ slug: "example" });
78
- await release({ dryRun: true });
79
- await tag({ bump: "patch", publish: false });
56
+ import { cli, root, pnpm } from "dbx-tools";
57
+
58
+ await cli.runCli(["sync"]);
59
+ const workspaceRoot = await root.findWorkspaceRoot();
60
+ pnpm.runProjen(["barrels"], workspaceRoot);
80
61
  ```
81
62
 
82
- Also exported: `format`, plus the workspace helpers
83
- (`discoverPackages`, `discoverPackageJsons`, `writeJson`, `getProject`, `sh`,
84
- `git`, ...).
63
+ Importing internals is mainly useful for tests or wrapper scripts; most users
64
+ should run the `dbxtools` bin.
65
+
66
+ ## Modules
85
67
 
86
- ## License
68
+ - `cli` - Commander entrypoint and `runCli()`.
69
+ - `bootstrap` - empty-workspace bootstrap.
70
+ - `root` - workspace-root detection and bootstrap/install checks.
71
+ - `pnpm` - pnpm/projen command resolution and delegation.
87
72
 
88
- Apache-2.0
73
+ The reusable project classes and generators live in
74
+ [`@dbx-tools/projen`](../../../projen).
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * `dbxtools` bootstraps uninitialized workspaces, then forwards to projen.
4
+ */
5
+ import { runCli } from "../src/cli";
6
+
7
+ runCli(process.argv).catch((err: unknown) => {
8
+ console.error(err);
9
+ process.exit(1);
10
+ });
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as bootstrap from "./src/bootstrap";
6
+ export * as cli from "./src/cli";
7
+ export * as pnpm from "./src/pnpm";
8
+ export * as root from "./src/root";
package/package.json CHANGED
@@ -1,41 +1,53 @@
1
1
  {
2
2
  "name": "@dbx-tools/cli",
3
- "version": "0.1.112",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/reggie-db/dbx-tools.git",
6
+ "directory": "workspaces/cli/dbx-tools"
7
+ },
4
8
  "bin": {
5
- "dbxtools": "./dist/bin/dbxtools.js"
9
+ "dbxtools": "./bin/dbxtools.ts"
10
+ },
11
+ "devDependencies": {
12
+ "@types/node": "^24.6.0",
13
+ "tsx": "^4.23.0",
14
+ "typescript": "^5.9.3"
6
15
  },
7
16
  "dependencies": {
8
- "@types/semver": "^7.7.1",
17
+ "@clack/prompts": "^1.7.0",
9
18
  "commander": "^15.0.0",
10
- "consola": "^3.4.2",
11
- "p-memoize": "^8.0.0",
12
- "pacwich": "^0.1.2",
13
- "prettier": "^3.9.3",
14
- "prettier-plugin-organize-imports": "^4.3.0",
15
- "semver": "^7.8.5",
16
- "ts-to-zod": "^5.1.0",
17
- "typescript": "^5.9.3"
19
+ "pnpm": "^11.0.6",
20
+ "@dbx-tools/core": "0.3.1",
21
+ "@dbx-tools/shared-core": "0.3.1"
18
22
  },
23
+ "main": "index.ts",
24
+ "license": "UNLICENSED",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "version": "0.3.1",
29
+ "types": "index.ts",
30
+ "type": "module",
19
31
  "exports": {
20
- ".": {
21
- "types": "./dist/index.d.ts",
22
- "default": "./dist/index.js"
23
- }
32
+ ".": "./index.ts",
33
+ "./pnpm": "./src/pnpm.ts",
34
+ "./package.json": "./package.json"
24
35
  },
25
- "knip": {
26
- "ignoreDependencies": [
27
- "prettier",
28
- "prettier-plugin-organize-imports"
36
+ "dbxToolsConfig": {
37
+ "tags": [
38
+ "cli"
29
39
  ]
30
40
  },
31
- "type": "module",
32
- "main": "./dist/index.js",
33
- "types": "./dist/index.d.ts",
34
- "files": [
35
- "dist"
36
- ],
37
- "license": "Apache-2.0",
38
- "publishConfig": {
39
- "access": "public"
41
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
42
+ "scripts": {
43
+ "build": "projen build",
44
+ "compile": "projen compile",
45
+ "default": "projen default",
46
+ "package": "projen package",
47
+ "post-compile": "projen post-compile",
48
+ "pre-compile": "projen pre-compile",
49
+ "test": "projen test",
50
+ "watch": "projen watch",
51
+ "projen": "projen"
40
52
  }
41
- }
53
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Bootstrap a brand-new folder into a working dbx-tools workspace before projen runs.
3
+ */
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { intro, outro } from "@clack/prompts";
7
+ import { exec } from "@dbx-tools/core";
8
+ import { resolvePnpmArgv, runPnpm } from "./pnpm";
9
+ import { rootLabel } from "./root";
10
+
11
+ // Pin to `@latest` explicitly: a bare `@dbx-tools/projen` can land on a stray
12
+ // `0.0.0` (whose `^0.0.0` caret then can't reach any real release), leaving the
13
+ // workspace on a stale engine. `@latest` always takes the newest published.
14
+ const DEFAULT_PROJEN_SPECIFIER = "@dbx-tools/projen@latest";
15
+
16
+ const PROJENRC_TEMPLATE = `import { DBXToolsNodeProject } from "@dbx-tools/projen";
17
+
18
+ const project = new DBXToolsNodeProject();
19
+ project.synth();
20
+ `;
21
+
22
+ /** Seed `pnpm-workspace.yaml` so the first \`pnpm add\` can allow esbuild non-interactively. */
23
+ const WORKSPACE_SEED = `packages: []
24
+ allowBuilds:
25
+ esbuild: true
26
+ `;
27
+
28
+ /**
29
+ * Turn a folder into a functioning dbx-tools workspace: `pnpm init`, seed
30
+ * `pnpm-workspace.yaml`, add `projen`/`typescript`/`tsx` + the engine package,
31
+ * write a minimal `.projenrc.ts`, synth once (with `PROJEN_DISABLE_POST`), then
32
+ * install. Does not run barrels - run `dbxtools barrels` or a full projen synth
33
+ * post-install to generate package barrels.
34
+ *
35
+ * Every step is idempotent and self-guarded, so this is safe to run against a
36
+ * folder that ALREADY has a hand-authored `.projenrc.ts` (and even a committed
37
+ * `package.json`) but is missing the installed toolchain - e.g. a freshly copied
38
+ * project whose generated files (including manifests) are gitignored. In that
39
+ * case `pnpm init` and the `.projenrc.ts` scaffold are skipped, but the engine +
40
+ * projen + tsx are (re)installed so the subsequent synth can regenerate
41
+ * everything. See {@link seedToolchain}.
42
+ */
43
+ export function bootstrapWorkspace(
44
+ root: string,
45
+ projenSpecifier: string = DEFAULT_PROJEN_SPECIFIER,
46
+ ): void {
47
+ intro(`Bootstrapping dbx-tools workspace in ${rootLabel(root)}`);
48
+
49
+ seedToolchain(root, projenSpecifier);
50
+
51
+ const projenrc = join(root, ".projenrc.ts");
52
+ if (!existsSync(projenrc)) {
53
+ writeFileSync(projenrc, PROJENRC_TEMPLATE);
54
+ }
55
+
56
+ runInitialSynth(root);
57
+
58
+ runPnpm(["install", "--no-frozen-lockfile", "--force"], root);
59
+ outro("Workspace ready - re-run dbxtools or add packages under workspaces/");
60
+ }
61
+
62
+ /**
63
+ * Install the toolchain a synth needs (`projen`, `typescript`, `tsx`, and the
64
+ * dbx-tools engine), seeding a `package.json` and `pnpm-workspace.yaml` first
65
+ * when absent. Idempotent: run it whenever the engine is missing, so a copied
66
+ * project with a `.projenrc.ts` but no `node_modules`/manifests can be brought
67
+ * up to a synth-ready state without full bootstrapping.
68
+ */
69
+ export function seedToolchain(
70
+ root: string,
71
+ projenSpecifier: string = DEFAULT_PROJEN_SPECIFIER,
72
+ ): void {
73
+ const manifestPath = join(root, "package.json");
74
+ if (!existsSync(manifestPath)) {
75
+ runPnpm(["init"], root);
76
+ // pnpm 11 `init` writes `devEngines.packageManager: { name: "pnpm",
77
+ // onFail: "download" }`. Any npm-based tool later in the chain (an
78
+ // `npx`/dlx fallback) then refuses with EBADDEVENGINES because its own
79
+ // runner is npm, not pnpm. projen regenerates the whole manifest at synth
80
+ // anyway, so drop the block from this throwaway seed.
81
+ stripDevEngines(manifestPath);
82
+ }
83
+
84
+ const workspaceFile = join(root, "pnpm-workspace.yaml");
85
+ if (!existsSync(workspaceFile)) {
86
+ writeFileSync(workspaceFile, WORKSPACE_SEED);
87
+ }
88
+
89
+ runPnpm(["add", "-D", "projen", "typescript@^5.9.3", "tsx@^4.23.0", projenSpecifier], root);
90
+ }
91
+
92
+ /** Remove the `devEngines` block pnpm `init` seeds, so npm-based tooling doesn't reject the manifest. */
93
+ function stripDevEngines(manifestPath: string): void {
94
+ try {
95
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
96
+ if (manifest.devEngines === undefined) return;
97
+ delete manifest.devEngines;
98
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
99
+ } catch {
100
+ // A malformed/absent manifest here just means the later `pnpm add` recreates it.
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Run the initial synth by executing `.projenrc.ts` directly with tsx (with
106
+ * `PROJEN_DISABLE_POST` set), NOT `projen <task>`. Use right after seeding a
107
+ * fresh workspace: the projen TASKS (`sync`, `barrels`, ...) only exist once
108
+ * `.projenrc.ts` has run once, so `projen sync` can't be the bootstrapping step.
109
+ */
110
+ export function runInitialSynth(root: string): void {
111
+ const [command, ...prefix] = resolvePnpmArgv();
112
+ exec.spawnSync(command, [...prefix, "exec", "tsx", ".projenrc.ts"], {
113
+ cwd: root,
114
+ env: { ...process.env, PROJEN_DISABLE_POST: "true" },
115
+ check: true,
116
+ });
117
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `dbxtools` commander entry: detect root, bootstrap or install, forward to projen.
3
+ */
4
+ import { Command } from "commander";
5
+ import { bootstrapWorkspace, runInitialSynth, seedToolchain } from "./bootstrap";
6
+ import { ensureWorkspaceReady, runPnpm, runProjen } from "./pnpm";
7
+ import { findWorkspaceRoot, needsBootstrap, needsToolchain } from "./root";
8
+
9
+ /**
10
+ * Prepare the workspace at `root`, then run `pnpm exec projen` with `projenArgs`.
11
+ *
12
+ * Three cases, in order:
13
+ * - no `.projenrc.ts` at all -> full bootstrap (scaffold + install + synth),
14
+ * which already runs the initial synth; nothing more to forward.
15
+ * - a `.projenrc.ts` but the engine/toolchain isn't installed (e.g. a freshly
16
+ * copied project whose generated files + manifests are gitignored) -> seed
17
+ * the toolchain, then run the INITIAL synth directly (the projen tasks the
18
+ * args would name, like `sync`, don't exist until `.projenrc.ts` has run
19
+ * once), and install. Don't forward `projenArgs` - the synth is the work.
20
+ * - otherwise (established workspace) -> ensure deps, then forward to projen.
21
+ */
22
+ async function prepareAndRunProjen(projenArgs: string[], startDir?: string): Promise<void> {
23
+ const root = await findWorkspaceRoot(startDir);
24
+ if (needsBootstrap(root)) {
25
+ bootstrapWorkspace(root);
26
+ return;
27
+ }
28
+ if (needsToolchain(root)) {
29
+ seedToolchain(root);
30
+ runInitialSynth(root);
31
+ runPnpm(["install", "--no-frozen-lockfile", "--force"], root);
32
+ return;
33
+ }
34
+ ensureWorkspaceReady(root);
35
+ runProjen(projenArgs, root);
36
+ }
37
+
38
+ /** Parse `argv` with commander and forward remaining args to projen. */
39
+ export async function runCli(argv: string[]): Promise<void> {
40
+ const program = new Command()
41
+ .name("dbxtools")
42
+ .description("Bootstrap dbx-tools workspaces and forward to projen")
43
+ .allowUnknownOption()
44
+ .allowExcessArguments()
45
+ .showHelpAfterError()
46
+ .helpOption("-h, --help", "Show dbxtools help");
47
+
48
+ program.parse(argv);
49
+ await prepareAndRunProjen(program.args);
50
+ }
package/src/pnpm.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * pnpm discovery, workspace install, and projen forwarding for the `dbxtools` CLI.
3
+ */
4
+ import { createRequire } from "node:module";
5
+ import { dirname, join } from "node:path";
6
+ import { exec } from "@dbx-tools/core";
7
+ import { functionModule } from "@dbx-tools/shared-core";
8
+ import { needsInstall } from "./root";
9
+
10
+ /** A package.json `bin` field: either a single command string, or a name -> path map. */
11
+ type BinField = string | Record<string, string>;
12
+
13
+ /** True when `pnpm --version` runs, i.e. pnpm is already on PATH. */
14
+ function pnpmOnPath(): boolean {
15
+ try {
16
+ exec.spawnSync("pnpm", ["--version"], {
17
+ stderr: "ignore",
18
+ stdin: "ignore",
19
+ stdout: "ignore",
20
+ check: true,
21
+ });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function resolvePnpmArgvImpl(): string[] {
29
+ // 1. A resolvable `pnpm` dependency (the normal in-workspace case): run its
30
+ // bin directly with the current node - no PATH or package-manager shim.
31
+ try {
32
+ const require = createRequire(import.meta.url);
33
+ const pkgJsonPath = require.resolve("pnpm/package.json");
34
+ const pkg = require(pkgJsonPath) as { bin: BinField };
35
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.pnpm;
36
+ return [process.execPath, join(dirname(pkgJsonPath), bin)];
37
+ } catch {
38
+ // fall through
39
+ }
40
+
41
+ // 2. A bare `pnpm` already on PATH (e.g. running under `pnpm dlx`). Prefer
42
+ // this over the corepack/npx fallbacks so we never shell through npm -
43
+ // `npx -y pnpm` runs under npm, which rejects a bootstrapped
44
+ // `devEngines.packageManager: pnpm` manifest with EBADDEVENGINES.
45
+ if (pnpmOnPath()) return ["pnpm"];
46
+
47
+ // 3. Try to enable pnpm via corepack, then use it.
48
+ try {
49
+ exec.spawnSync("corepack", ["enable", "pnpm"], {
50
+ stderr: "ignore",
51
+ stdin: "ignore",
52
+ stdout: "ignore",
53
+ check: true,
54
+ });
55
+ if (pnpmOnPath()) return ["pnpm"];
56
+ } catch {
57
+ // fall through
58
+ }
59
+
60
+ // 4. Last resort: fetch pnpm on demand via npx. Pass `--engine-strict=false`
61
+ // (and skip npm's devEngines gate) so npm doesn't refuse to run just
62
+ // because the target manifest declares `devEngines.packageManager: pnpm`.
63
+ return ["npx", "-y", "--engine-strict=false", "pnpm"];
64
+ }
65
+
66
+ /** Memoized `[command, ...prefix]` argv prefix to run pnpm (resolved install, else corepack, else npx). */
67
+ export const resolvePnpmArgv = functionModule.memoize(resolvePnpmArgvImpl);
68
+
69
+ /** Run pnpm with inherited stdio from `cwd`. */
70
+ export function runPnpm(args: string[], cwd: string): void {
71
+ const [command, ...prefix] = resolvePnpmArgv();
72
+ exec.spawnSync(command, [...prefix, ...args], { cwd, check: true });
73
+ }
74
+
75
+ /** Install workspace dependencies when `node_modules` or projen is missing. */
76
+ export function ensureWorkspaceReady(root: string): void {
77
+ if (needsInstall(root)) {
78
+ runPnpm(["install", "--no-frozen-lockfile"], root);
79
+ }
80
+ }
81
+
82
+ /** Run `pnpm exec projen` with the given args from `root`. */
83
+ export function runProjen(args: string[], root: string): void {
84
+ runPnpm(["exec", "projen", ...args], root);
85
+ }