@dbx-tools/projen 0.6.223 → 0.6.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,37 @@ project.synth();
37
37
  Every `src`-bearing folder under the configured roots becomes a
38
38
  `DBXToolsTypeScriptProject`. Folder path drives package name and runtime tags.
39
39
 
40
+ Use `DBXToolsTypeScriptProject` itself as the root when one package needs the
41
+ same Bun workspace, repository, task, and release defaults without a separate
42
+ non-compiling root. It preserves Projen's native compiling `tsconfig.json`
43
+ instead of creating a second owner. An explicit `outdir` is the workspace root
44
+ for naming, Git metadata, package discovery, codegen, and barrels even when the
45
+ calling process has a different current directory.
46
+
47
+ The unified dbx-tools release surface is enabled by default. Set
48
+ `releaseMode: "disabled"` to omit its workflow and bump/version/release tasks.
49
+ The inherited Projen `release` and `releaseTrigger` options are intentionally
50
+ not part of this engine's public options because they create a competing release
51
+ workflow.
52
+
53
+ Repository policy stays in the consuming `.projenrc.ts`:
54
+
55
+ - `releaseDocs` supplies repository-defined preparation and build steps plus
56
+ the Pages artifact path. The engine adds release checkout, Bun caching,
57
+ artifact upload, and deployment without naming a docs script or output tree.
58
+ - `releasePythonRoot` passes the actual Python package root to local release
59
+ preparation. Omit it when the workspace has no standard Python packages.
60
+ - `releaseSummary` controls optional AI release notes. It defaults to enabled
61
+ with Cursor, Codex, Claude fallback order; pass `false` to disable or provide
62
+ a `providers` array to choose the order/subset.
63
+ - `pullRequestTitlePolicy` configures semantic title types and scope policy.
64
+ Omit it or pass `false` to disable the title job.
65
+ - `workflowCacheIgnorePaths` excludes generated output trees that may contain
66
+ package manifests from the dependency-only Bun cache key.
67
+ - `extraWorkspaceMembers` declares self-synthesizing tooling packages outside
68
+ `packageRoots`. Their version, generated entrypoint, formatting, linting, and
69
+ workspace membership are derived from that declaration.
70
+
40
71
  Dependency installation runs once from this root. The default-on
41
72
  `ROOT_INSTALL_ONLY_MIXIN` clears child `install` / `install:ci` task steps during
42
73
  root pre-synthesis, including packages attached after root construction. Set
@@ -53,9 +84,9 @@ new package membership.
53
84
  without turning Python packages into JavaScript workspace projects. It generates
54
85
  the root and member `pyproject.toml` files, standard `py:*` tasks, the VS Code
55
86
  interpreter setting, and an optional manual PyPI trusted-publishing workflow.
56
- Every generated TOML file is parsed and reserialized with `smol-toml`, so nested
57
- tables and arrays use consistent formatting instead of projen's indented table
58
- headers.
87
+ Projen's native `PyprojectTomlFile` owns generated TOML formatting. Temporary
88
+ publication stamping uses parsed TOML, changes versions and sibling
89
+ dependencies structurally, and restores the original files byte-for-byte.
59
90
 
60
91
  ```ts
61
92
  import { project, projectPy } from "@dbx-tools/projen";
@@ -121,6 +152,10 @@ are library-local. Release wheels replace sibling Git requirements with matching
121
152
  native-wheel versions, and dependent publishers wait for their dependencies.
122
153
  Each crate builds its own `<crate>-uniffi-bindgen` executable so a workspace
123
154
  build has no colliding binary outputs. Packaging runs that prebuilt executable.
155
+ Cargo manifests, target config, and UniFFI config are generated from structured
156
+ Projen `TomlFile` objects. Local and release Python generation share one
157
+ dependency-free helper for generator arguments, target-specific executable
158
+ names, generated headers, empty `__init__.py`, and native-library placement.
124
159
 
125
160
  `sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
126
161
  inside an existing UniFFI crate regenerate that crate and its dependent bindings
@@ -409,7 +444,17 @@ it. `--message` sets the source commit message. The task never merges
409
444
  the PR unless `--approve` is passed; that option merges the release branch
410
445
  directly through GitHub's merge API so no PR checks are created. A repository
411
446
  that blocks direct merges falls back to an immediate admin-merged PR. The task
412
- then fast-forwards the still-active source branch to the release commit. npm
447
+ then fast-forwards the still-active source branch to the release commit.
448
+
449
+ After validation, release preparation optionally generates an immutable
450
+ `docs/releases/v<version>.md`. It tries the installed Cursor agent first, then
451
+ Codex, then Claude, all in non-interactive read-only modes against bounded Git
452
+ context. An unavailable, unauthenticated, failed, or empty provider falls
453
+ through to the next; if none works, release preparation continues without a
454
+ summary. The same file is included in the release PR and prepended to GitHub's
455
+ generated release notes, so repository and GitHub views share one source.
456
+
457
+ npm
413
458
  uses `npm config get registry` and publishes to a local Verdaccio automatically.
414
459
  Publishable JavaScript members compile once
415
460
  from the root in parallel, then upload through a bounded pool without rerunning
@@ -425,10 +470,12 @@ Use `--local-registry false` or `--local-pypi false` to disable either local
425
470
  publish. An explicit `--local-pypi http://localhost:3141/user/index/` overrides
426
471
  auto-detection; `--python-root` defaults to `packages/py`.
427
472
 
428
- The GitHub PR workflow runs synth plus the workspace TypeScript compile rather
429
- than the complete root build. Release preparation has already run Rust tests,
430
- workspace type-checking, and local package preflight before opening the PR.
431
- JavaScript behavior tests remain an explicit developer task.
473
+ The GitHub PR workflow runs the explicit `pr:validate` task through Projen's
474
+ public `BuildWorkflow` `buildTask` option. That task runs synth plus the
475
+ workspace TypeScript compile rather than the complete root build. Release
476
+ preparation has already run Rust tests, workspace type-checking, and local
477
+ package preflight before opening the PR. JavaScript behavior tests remain an
478
+ explicit developer task.
432
479
 
433
480
  The configured release branch publishes only when a reviewed PR changes
434
481
  `VERSION`. Its workflow creates the annotated `v*` public release boundary.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.6.223";
6
+ export const PACKAGE_VERSION = "0.6.225";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -25,6 +25,7 @@ export * as projectRs from "./src/project-rs.ts";
25
25
  export * as publish from "./src/publish.ts";
26
26
  export * as release from "./src/release.ts";
27
27
  export * as releaseDispatch from "./src/release-dispatch.ts";
28
+ export * as releaseGithub from "./src/release-github.ts";
28
29
  export * as scaffold from "./src/scaffold.ts";
29
30
  export * as tags from "./src/tags.ts";
30
31
  export * as tsconfig from "./src/tsconfig.ts";
@@ -47,15 +48,17 @@ export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
47
48
  export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
48
49
  export type { DBXToolsProjectLanguage, DBXToolsProjectOptions, DBXToolsProject, ApplyToProjectsOptions } from "./src/project.ts";
49
50
  export { PackageIdentifier, PROJEN_VERSION, DBX_TOOLS_LICENSE, DBXToolsNodeProject, ROOT_INSTALL_ONLY_MIXIN, DBXToolsTypeScriptProject } from "./src/project-js.ts";
50
- export type { DBXToolsJavaScriptProject, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
51
+ export type { DBXToolsJavaScriptProject, DBXToolsReleaseMode, PullRequestTitlePolicyOptions, DBXToolsJavaScriptProjectOptions, DBXToolsTypeScriptProjectOptions } from "./src/project-js.ts";
51
52
  export { DBXToolsPythonProject, DBXToolsPythonWorkspace } from "./src/project-py.ts";
52
53
  export type { PythonRepositoryOptions, PythonPackageOptions, PythonTrustedPublisherOptions, DBXToolsPythonProjectOptions, PythonReleaseOptions, DBXToolsPythonWorkspaceOptions } from "./src/project-py.ts";
53
54
  export { RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustProject, DBXToolsRustWorkspace } from "./src/project-rs.ts";
54
55
  export type { CargoDependencyOptions, CargoDependency, RustCliOptions, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping, RustReleaseBinaryAssetMapping, RustReleaseBinaryMapping } from "./src/project-rs.ts";
55
56
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
56
57
  export { DBXToolsRelease } from "./src/release.ts";
57
- export type { ReleaseStage, ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
58
- export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION } from "./src/release-dispatch.ts";
58
+ export type { ReleaseStage, ReleaseDocsOptions, ReleaseSummaryOptions, DBXToolsReleaseOptions } from "./src/release.ts";
59
+ export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION, RELEASE_SUMMARY_DIRECTORY, RELEASE_SUMMARY_PROVIDER_NAMES, RELEASE_SUMMARY_FILE } from "./src/release-dispatch.ts";
60
+ export type { ReleaseSummaryProviderName } from "./src/release-dispatch.ts";
61
+ export type { GithubRepositoryIdentity } from "./src/release-github.ts";
59
62
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
60
63
  export type { PackageTag } from "./src/tags.ts";
61
64
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
package/package.json CHANGED
@@ -5,13 +5,13 @@
5
5
  "clobber": "projen clobber",
6
6
  "compile": "projen compile",
7
7
  "default": "projen default",
8
- "demo": "projen demo",
9
8
  "eject": "projen eject",
10
9
  "package": "projen package",
11
10
  "post-compile": "projen post-compile",
12
11
  "post-upgrade": "projen post-upgrade",
13
12
  "pre-compile": "projen pre-compile",
14
13
  "test": "projen test",
14
+ "test:external-consumer": "projen test:external-consumer",
15
15
  "upgrade": "projen upgrade",
16
16
  "watch": "projen watch",
17
17
  "projen": "projen"
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@clack/prompts": "^1.7.0",
28
- "@dbx-tools/core": "0.6.223",
29
- "@dbx-tools/path": "0.6.223",
30
- "@dbx-tools/shared-core": "0.6.223",
28
+ "@dbx-tools/core": "0.6.225",
29
+ "@dbx-tools/path": "0.6.225",
30
+ "@dbx-tools/shared-core": "0.6.225",
31
31
  "commander": "^15.0.0",
32
32
  "concurrently": "^10.0.3",
33
33
  "constructs": "^10.6.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "main": "index.ts",
49
49
  "license": "Apache-2.0",
50
- "version": "0.6.223",
50
+ "version": "0.6.225",
51
51
  "types": "index.ts",
52
52
  "type": "module",
53
53
  "exports": {
package/src/barrels.ts CHANGED
@@ -63,7 +63,7 @@ import { json, string } from "@dbx-tools/shared-core";
63
63
  import isIdentifier from "is-identifier";
64
64
  import { header, isGenerated, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
65
65
  import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
66
- import { isModuleFile, toPosix, recordedPackages, repoRoot } from "./packages.ts";
66
+ import { isModuleFile, toPosix, recordedPackages, resolveRepoRoot } from "./packages.ts";
67
67
 
68
68
  /**
69
69
  * A `src`-relative posix path excluded from the root barrel:
@@ -555,8 +555,14 @@ function writeBarrel(file: string, content: string): void {
555
555
  * order, so one unwritable barrel silently left the rest of the repo stale with
556
556
  * nothing in the log to say which packages had been skipped.
557
557
  */
558
- export function generateBarrels(opts: { dirs?: string[] } = {}): number {
559
- const dirs = opts.dirs ?? recordedPackages().map((p) => p.dir);
558
+ export function generateBarrels(
559
+ opts: { dirs?: string[]; includeRoot?: boolean; projectRoot?: string } = {},
560
+ ): number {
561
+ const projectRoot = opts.projectRoot ?? resolveRepoRoot();
562
+ const dirs = opts.dirs ?? [
563
+ ...(opts.includeRoot ? [projectRoot] : []),
564
+ ...recordedPackages(projectRoot).map((p) => p.dir),
565
+ ];
560
566
  let total = 0;
561
567
  const failures: { dir: string; err: unknown }[] = [];
562
568
  for (const dir of dirs) {
@@ -567,7 +573,7 @@ export function generateBarrels(opts: { dirs?: string[] } = {}): number {
567
573
  }
568
574
  }
569
575
  if (failures.length) {
570
- const names = failures.map((f) => relative(repoRoot, f.dir) || f.dir);
576
+ const names = failures.map((f) => relative(projectRoot, f.dir) || f.dir);
571
577
  throw new AggregateError(
572
578
  failures.map((f) => f.err),
573
579
  `${string.pluralize(failures.length, "barrel")} failed: ${names.join(", ")}`,
@@ -3,15 +3,8 @@ import { TextFile, javascript } from "projen";
3
3
 
4
4
  export const BUN_VERSION = "1.3.14";
5
5
 
6
- const cacheKeyScript = `#!/usr/bin/env node
7
- import { createHash } from "node:crypto";
8
- import { readdirSync, readFileSync } from "node:fs";
9
- import { join } from "node:path";
10
-
11
- const root = process.cwd();
12
- const ignored = new Set([
6
+ const DEFAULT_CACHE_IGNORE_PATHS = [
13
7
  ".git",
14
- ".docs-build",
15
8
  ".venv",
16
9
  ".worktrees",
17
10
  "coverage",
@@ -19,12 +12,24 @@ const ignored = new Set([
19
12
  "lib",
20
13
  "node_modules",
21
14
  "target",
22
- ]);
15
+ ] as const;
16
+
17
+ function cacheKeyScript(extraIgnorePaths: readonly string[]): string {
18
+ const ignorePaths = [...new Set(extraIgnorePaths)].sort();
19
+ return `#!/usr/bin/env node
20
+ import { createHash } from "node:crypto";
21
+ import { readdirSync, readFileSync } from "node:fs";
22
+ import { join, sep } from "node:path";
23
+
24
+ const root = process.cwd();
25
+ const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
26
+ const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
23
27
  const manifests = [];
24
28
  const walk = (directory) => {
25
29
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
26
- if (ignored.has(entry.name)) continue;
27
30
  const path = join(directory, entry.name);
31
+ const relativePath = path.slice(root.length + 1).split(sep).join("/");
32
+ if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
28
33
  if (entry.isDirectory()) walk(path);
29
34
  else if (entry.name === "package.json") manifests.push(path);
30
35
  }
@@ -66,15 +71,27 @@ const dependencies = manifests
66
71
  });
67
72
  process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
68
73
  `;
74
+ }
69
75
 
70
- const configured = new WeakSet<javascript.NodeProject>();
76
+ const configured = new WeakMap<javascript.NodeProject, string>();
71
77
 
72
- function ensureCacheKeyScript(project: javascript.NodeProject): void {
73
- if (configured.has(project)) return;
74
- new TextFile(project.root, ".projen/bun-cache-key.mjs", {
75
- lines: cacheKeyScript.trimEnd().split("\n"),
78
+ function ensureCacheKeyScript(
79
+ project: javascript.NodeProject,
80
+ ignorePaths: readonly string[],
81
+ ): void {
82
+ const root = project.root as javascript.NodeProject;
83
+ const key = JSON.stringify([...new Set(ignorePaths)].sort());
84
+ const existing = configured.get(root);
85
+ if (existing !== undefined) {
86
+ if (existing !== key) {
87
+ throw new Error("Bun workflow cache ignore paths must be consistent across one project");
88
+ }
89
+ return;
90
+ }
91
+ new TextFile(root, ".projen/bun-cache-key.mjs", {
92
+ lines: cacheKeyScript(ignorePaths).trimEnd().split("\n"),
76
93
  });
77
- configured.add(project);
94
+ configured.set(root, key);
78
95
  }
79
96
 
80
97
  function stepCondition(condition: string | undefined, cacheMiss = false): string | undefined {
@@ -88,6 +105,8 @@ function stepCondition(condition: string | undefined, cacheMiss = false): string
88
105
  export interface BunWorkflowCacheOptions {
89
106
  readonly setupCondition?: string;
90
107
  readonly condition?: string;
108
+ /** Repository-relative output paths excluded from dependency manifest hashing. */
109
+ readonly ignorePaths?: readonly string[];
91
110
  }
92
111
 
93
112
  /** Set up Bun and restore its global package cache. */
@@ -95,7 +114,7 @@ export function bunCacheRestoreSteps(
95
114
  project: javascript.NodeProject,
96
115
  options: BunWorkflowCacheOptions = {},
97
116
  ): readonly Record<string, unknown>[] {
98
- ensureCacheKeyScript(project);
117
+ ensureCacheKeyScript(project, options.ignorePaths ?? []);
99
118
  const condition = stepCondition(options.condition);
100
119
  const setupCondition = stepCondition(options.setupCondition);
101
120
  return [
package/src/clean.ts CHANGED
@@ -25,7 +25,7 @@ import { existsSync, rmSync, statSync } from "node:fs";
25
25
  import { basename, join, relative } from "node:path";
26
26
  import { find } from "@dbx-tools/path";
27
27
  import { isReadonly, makeWritable } from "./generated.ts";
28
- import { repoRoot, toPosix } from "./packages.ts";
28
+ import { resolveRepoRoot, toPosix } from "./packages.ts";
29
29
 
30
30
  /**
31
31
  * Basenames `clean` never removes even when they are generated/read-only. `.gitignore`
@@ -40,7 +40,7 @@ const CLEAN_SKIP_FILES: ReadonlySet<string> = new Set([".gitignore"]);
40
40
  * ignores AND every dot-prefixed folder (`.projen`, `.vscode`, `.github`, ...), and
41
41
  * {@link CLEAN_SKIP_FILES} entry (`.gitignore`).
42
42
  */
43
- export function listGeneratedFiles(root: string = repoRoot): string[] {
43
+ export function listGeneratedFiles(root: string = resolveRepoRoot()): string[] {
44
44
  const rel = (f: string): string => toPosix(relative(root, f));
45
45
  return [...find.findFiles("**/*", { cwd: root })]
46
46
  .map((f) => join(root, f))
@@ -57,7 +57,7 @@ export function listGeneratedFiles(root: string = repoRoot): string[] {
57
57
  * own - removing the top-level dir takes it along. Other vendor/build/VCS dirs are
58
58
  * skipped for speed.
59
59
  */
60
- export function listNodeModulesDirs(root: string = repoRoot): string[] {
60
+ export function listNodeModulesDirs(root: string = resolveRepoRoot()): string[] {
61
61
  if (!existsSync(root)) return [];
62
62
  const rel = (f: string): string => toPosix(relative(root, f));
63
63
  return [...find.findFiles("**/node_modules", { cwd: root, ignoreOptions: { dot: false } })]
package/src/codegen.ts CHANGED
@@ -50,7 +50,7 @@ import { log, object } from "@dbx-tools/shared-core";
50
50
  import type * as ts from "typescript";
51
51
  import { lazyRequire } from "./_lazy-require.ts";
52
52
  import { header, isReadonly, makeReadonly, makeWritable } from "./generated.ts";
53
- import { readPackageManifest, repoRoot, recordedPackages } from "./packages.ts";
53
+ import { readPackageManifest, recordedPackages, resolveRepoRoot } from "./packages.ts";
54
54
 
55
55
  const logger = log.logger("projen:codegen");
56
56
 
@@ -115,7 +115,7 @@ export function codegenModulePaths(inputs: readonly string[]): string[] {
115
115
  * package or hoisted to the workspace root. Any other source is treated as
116
116
  * repo-root-relative.
117
117
  */
118
- function resolveInputSource(source: string, fromDir: string): string {
118
+ function resolveInputSource(source: string, fromDir: string, projectRoot: string): string {
119
119
  if (source.startsWith("node_modules/")) {
120
120
  let dir = fromDir;
121
121
  for (;;) {
@@ -126,7 +126,7 @@ function resolveInputSource(source: string, fromDir: string): string {
126
126
  dir = parent;
127
127
  }
128
128
  }
129
- return resolve(repoRoot, source);
129
+ return resolve(projectRoot, source);
130
130
  }
131
131
 
132
132
  /**
@@ -276,6 +276,7 @@ function isGeneratedModule(srcDir: string, file: string): boolean {
276
276
  function generatePackage(
277
277
  tsRuntime: typeof ts,
278
278
  generate: typeof import("ts-to-zod").generate,
279
+ projectRoot: string,
279
280
  dir: string,
280
281
  inputs: string[],
281
282
  ): string {
@@ -296,7 +297,7 @@ function generatePackage(
296
297
 
297
298
  let warnings = 0;
298
299
  for (const input of parsed) {
299
- const sourcePath = resolveInputSource(input.source, dir);
300
+ const sourcePath = resolveInputSource(input.source, dir, projectRoot);
300
301
  if (!existsSync(sourcePath)) {
301
302
  throw new Error(`codegen input not found: ${input.source}`);
302
303
  }
@@ -336,9 +337,16 @@ function generatePackage(
336
337
  * `codegen` field. Returns the package dirs it wrote so the caller can rebuild
337
338
  * their barrels. `ts-to-zod` + `typescript` are lazy-loaded.
338
339
  */
339
- export function generateCodegen(): string[] {
340
- const targets = recordedPackages()
341
- .map((p) => ({ dir: p.dir, inputs: codegenInputs(p.dir) }))
340
+ export function generateCodegen(
341
+ projectRoot: string = resolveRepoRoot(),
342
+ options: { includeRoot?: boolean } = {},
343
+ ): string[] {
344
+ const packageDirs = [
345
+ ...(options.includeRoot ? [projectRoot] : []),
346
+ ...recordedPackages(projectRoot).map((pkg) => pkg.dir),
347
+ ];
348
+ const targets = packageDirs
349
+ .map((dir) => ({ dir, inputs: codegenInputs(dir) }))
342
350
  .filter((t): t is { dir: string; inputs: string[] } => t.inputs !== undefined);
343
351
 
344
352
  // Nothing to generate is the NORMAL case for a workspace that declares no
@@ -356,7 +364,7 @@ export function generateCodegen(): string[] {
356
364
 
357
365
  const written: string[] = [];
358
366
  for (const target of targets) {
359
- written.push(generatePackage(tsRuntime, generate, target.dir, target.inputs));
367
+ written.push(generatePackage(tsRuntime, generate, projectRoot, target.dir, target.inputs));
360
368
  }
361
369
  return written;
362
370
  }
package/src/packages.ts CHANGED
@@ -30,12 +30,13 @@ import { find } from "@dbx-tools/path";
30
30
  import { json, object, string } from "@dbx-tools/shared-core";
31
31
  import { parse } from "yaml";
32
32
 
33
- /**
34
- * The repo root: the nearest package/projenrc root, falling back to the current
35
- * working directory. Detection (npm prefix, git top-level, root markers) lives
36
- * in `@dbx-tools/core`'s {@link coreProject.root}.
37
- */
38
- export const repoRoot = coreProject.root() ?? process.cwd();
33
+ /** Resolve the nearest package/projenrc root at call time. */
34
+ export function resolveRepoRoot(cwd: string = process.cwd()): string {
35
+ return coreProject.root(cwd) ?? cwd;
36
+ }
37
+
38
+ /** Current-process repository root for executable task entrypoints. */
39
+ export const repoRoot = resolveRepoRoot();
39
40
 
40
41
  /**
41
42
  * Default package roots. Each is scanned for packages; override via the
@@ -49,8 +50,8 @@ export const DEFAULT_PACKAGE_ROOTS = ["packages"] as const;
49
50
  * {@link coreProject.name}, which is also what a consuming repo's own tooling
50
51
  * sees, so the engine and its host agree on the name.
51
52
  */
52
- export function projectName(): string {
53
- return coreProject.name(repoRoot);
53
+ export function projectName(projectRoot: string = resolveRepoRoot()): string {
54
+ return coreProject.name(projectRoot);
54
55
  }
55
56
 
56
57
  const MODULE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -168,7 +169,7 @@ export class DiscoveredPackage {
168
169
  }
169
170
 
170
171
  /** Read the raw workspace member globs from `pnpm-workspace.yaml` (source of truth). */
171
- function readRecordedMembers(projectRoot: string = repoRoot): string[] {
172
+ function readRecordedMembers(projectRoot: string = resolveRepoRoot()): string[] {
172
173
  const file = resolve(projectRoot, "pnpm-workspace.yaml");
173
174
  if (!existsSync(file)) return [];
174
175
  const doc = parse(readFileSync(file, "utf8")) as {
@@ -218,7 +219,7 @@ function collectPackageDirs(rootAbs: string): string[] {
218
219
  * recorded set. Sorted by member path.
219
220
  */
220
221
  export function scanPackages(
221
- projectRoot: string = repoRoot,
222
+ projectRoot: string = resolveRepoRoot(),
222
223
  roots: readonly string[] = DEFAULT_PACKAGE_ROOTS,
223
224
  ): DiscoveredPackage[] {
224
225
  const out: DiscoveredPackage[] = [];
@@ -285,7 +286,7 @@ function readManifestTags(dir: string): string[] | undefined {
285
286
  * the root `package.json` `dbxToolsConfig.syncResynthPaths` (set via the
286
287
  * {@link DBXToolsProjectOptions.syncResynthPaths} option at synth).
287
288
  */
288
- export function syncResynthPaths(projectRoot: string = repoRoot): string[] {
289
+ export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): string[] {
289
290
  const paths = readDbxToolsConfig(projectRoot)?.syncResynthPaths;
290
291
  return Array.isArray(paths) ? string.parseList(paths.map((p) => String(p))) : [];
291
292
  }
@@ -297,7 +298,7 @@ export function syncResynthPaths(projectRoot: string = repoRoot): string[] {
297
298
  * authoritative, so it reflects the resolved tag set.
298
299
  * Sorted by path.
299
300
  */
300
- export function recordedPackages(projectRoot: string = repoRoot): RecordedPackage[] {
301
+ export function recordedPackages(projectRoot: string = resolveRepoRoot()): RecordedPackage[] {
301
302
  const out: RecordedPackage[] = [];
302
303
  for (const member of readRecordedMembers(projectRoot)) {
303
304
  const pkg = packageOfMember(projectRoot, member);
@@ -318,7 +319,7 @@ export function recordedPackages(projectRoot: string = repoRoot): RecordedPackag
318
319
  * that field fall back to the distinct first segment of every recorded member,
319
320
  * unioned with the defaults.
320
321
  */
321
- export function recordedRoots(projectRoot: string = repoRoot): string[] {
322
+ export function recordedRoots(projectRoot: string = resolveRepoRoot()): string[] {
322
323
  const configured = readDbxToolsConfig(projectRoot)?.packageRoots;
323
324
  if (Array.isArray(configured)) {
324
325
  const roots = string.parseList(configured.map((root) => String(root)));