Skip to content

feat: Separate dev commands to project-builder-dev#782

Merged
kingston merged 9 commits into
mainfrom
kingston/eng-1032-refactor-the-project-builder-cli-to-enable-separate-dev-and
Feb 26, 2026
Merged

feat: Separate dev commands to project-builder-dev#782
kingston merged 9 commits into
mainfrom
kingston/eng-1032-refactor-the-project-builder-cli-to-enable-separate-dev-and

Conversation

@kingston

@kingston kingston commented Feb 25, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • New developer-focused CLI and baseplate-dev binary with templates, snapshot, diff, sync-examples, sync, serve and full dev server.
    • Programmatic project generation API for tests/automation.
    • Plugins are auto-discovered from project configuration.
  • Bug Fixes

    • Removed INCLUDE_EXAMPLES flag and example discovery; project listing behavior simplified.
  • Chores

    • Tooling split into separate end-user and developer CLIs and shared utilities consolidated.
    • Updated example Docker env files.

@changeset-bot

changeset-bot Bot commented Feb 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a6ad042

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@baseplate-dev/project-builder-cli Patch
@baseplate-dev/project-builder-dev Patch
@baseplate-dev/project-builder-server Patch
@baseplate-dev/create-project Patch
@baseplate-dev/project-builder-test Patch
@baseplate-dev/project-builder-common Patch
@baseplate-dev/project-builder-web Patch
@baseplate-dev/code-morph Patch
@baseplate-dev/core-generators Patch
@baseplate-dev/fastify-generators Patch
@baseplate-dev/project-builder-lib Patch
@baseplate-dev/react-generators Patch
@baseplate-dev/sync Patch
@baseplate-dev/tools Patch
@baseplate-dev/ui-components Patch
@baseplate-dev/utils Patch
@baseplate-dev/plugin-auth Patch
@baseplate-dev/plugin-email Patch
@baseplate-dev/plugin-queue Patch
@baseplate-dev/plugin-rate-limit Patch
@baseplate-dev/plugin-storage Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • tests/simple/docker/.env is excluded by !tests/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Extracts developer tooling into a new @baseplate-dev/project-builder-dev package (dev CLI, templates, snapshot, dev-server, sync-examples, full MCP), preserves a reduced end-user CLI (@baseplate-dev/project-builder-cli), centralizes utilities (version, user-config, paths), removes INCLUDE_EXAMPLES flows, and exposes generateProject() and USER_SERVICE_ACTIONS.

Changes

Cohort / File(s) Summary
New Dev package scaffold
packages/project-builder-dev/package.json, packages/project-builder-dev/tsconfig*.json, packages/project-builder-dev/README.md, packages/project-builder-dev/LICENSE, packages/project-builder-dev/.env, packages/project-builder-dev/.gitignore, packages/project-builder-dev/.prettierignore
Adds a new developer CLI package with metadata, build/ts configs, docs, license, env, and ignore rules.
Dev CLI entry & bin
packages/project-builder-dev/bin/baseplate-dev.js, packages/project-builder-dev/src/cli.ts, packages/project-builder-dev/src/index.ts
New CLI launcher, main entry registering dev commands, and re-export of generateProject.
Dev commands
packages/project-builder-dev/src/commands/*
(diff.ts, sync.ts, templates.ts, snapshot.ts, serve.ts, mcp.ts, dev-server.ts)
Implements developer-focused commands (diff/sync/templates/snapshot/serve/mcp/dev-server), dynamic plugin discovery, and full MCP action exposure.
Dev utilities & services
packages/project-builder-dev/src/utils/*, packages/project-builder-dev/src/services/logger.ts
Adds env parsing, project listing/resolution, example discovery, module resolution, service-action context creation, and a pino-based logger.
Programmatic generation
packages/project-builder-dev/src/runner/generate.ts
Implements generateProject(projectDirectory) for test/automation flows using discovered plugins and syncProject.
CLI package cleanup
packages/project-builder-cli/package.json, packages/project-builder-cli/src/index.ts
Removes dev-related scripts/commands from end-user CLI and feature-flag gated command wiring.
CLI centralization of utilities
packages/project-builder-cli/src/utils/*, packages/project-builder-cli/src/services/user-config.ts, packages/project-builder-cli/src/utils/version.ts
Removes local version/user-config/find-examples implementations in favor of shared utilities (getPackageVersion, server user-config, path helpers).
CLI command adjustments
packages/project-builder-cli/src/commands/*
(projects.ts, config.ts, server.ts, mcp.ts)
Removes include-examples option/INCLUDE_EXAMPLES handling; server/config use centralized getUserConfig and getPackageVersion; MCP now passes USER_SERVICE_ACTIONS.
Server actions & MCP
packages/project-builder-server/src/actions/registry.ts, packages/project-builder-server/src/actions/index.ts, packages/project-builder-server/src/dev-server/mcp/stdio.ts
Adds USER_SERVICE_ACTIONS (end-user subset), composes ALL_SERVICE_ACTIONS from it, and makes startMcpStdioServer accept an actions parameter (default ALL).
Server user-config
packages/project-builder-server/src/user-config/user-config-schema.ts
Adds persistent getUserConfig and writeUserConfig APIs using shared fs utilities.
Shared utilities
packages/utils/src/fs/get-package-version.ts, packages/utils/src/fs/index.ts, packages/utils/src/paths/index.ts, packages/utils/src/fs/read-json-with-schema.ts
Adds getPackageVersion(cwd), expands fs/path re-exports, and adds an early empty-file ENOENT check for read-json-with-schema.
Tests & test package updates
packages/project-builder-cli/src/utils/project-resolver.unit.test.ts, packages/project-builder-test/*
Removes tests/mocks and code for INCLUDE_EXAMPLES/find-examples; project-builder-test now depends on project-builder-dev and imports generateProject.
Repo config & workflows
.gitignore, .github/workflows/verify-examples-sync.yml, package.json, tsconfig.build.json, AGENTS.md, knip.config.js, examples/*, .changeset/add-project-builder-dev.md
Narrow root .env ignore, update workflow to use examples path, update scripts to reference project-builder-dev, add tsconfig reference, update docs, add example .env files, and register new workspace in knip config.

Sequence Diagram(s)

mermaid
sequenceDiagram
rect rgba(18,115,184,0.5)
participant DevCLI as baseplate-dev (Dev CLI)
end
rect rgba(22,160,133,0.5)
participant MCP as MCP Stdio Server
end
rect rgba(142,68,173,0.5)
participant Actions as Service Actions
end
DevCLI->>MCP: startMcpStdioServer(context, ALL_SERVICE_ACTIONS)
MCP->>Actions: register ALL_SERVICE_ACTIONS
Actions-->>MCP: ready
MCP-->>DevCLI: STDIO begun

mermaid
sequenceDiagram
rect rgba(18,115,184,0.5)
participant UserCLI as baseplate (End-user CLI)
end
rect rgba(22,160,133,0.5)
participant MCPu as MCP Stdio Server (user)
end
rect rgba(142,68,173,0.5)
participant UserActions as USER_SERVICE_ACTIONS
end
UserCLI->>MCPu: startMcpStdioServer(context, USER_SERVICE_ACTIONS)
MCPu->>UserActions: register USER_SERVICE_ACTIONS
UserActions-->>MCPu: ready
MCPu-->>UserCLI: STDIO begun

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: separating developer-focused commands from project-builder-cli into a new project-builder-dev package, which is the primary objective of this refactoring PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kingston/eng-1032-refactor-the-project-builder-cli-to-enable-separate-dev-and

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/project-builder-cli/src/utils/project-resolver.ts (1)

171-175: ⚠️ Potential issue | 🟡 Minor

Incorrect error message references package.json instead of project-definition.json.

Line 173 says "No package.json found" but loadProjectInfo reads baseplate/project-definition.json (line 138-142). The ENOENT error would occur when the project definition file is missing, not package.json.

Fix error message
     } catch (error) {
       if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
-        throw new Error(`No package.json found in ${directory}`);
+        throw new Error(`No project definition found at ${path.join(directory, 'baseplate', 'project-definition.json')}`);
       }
       throw error;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-cli/src/utils/project-resolver.ts` around lines 171
- 175, The catch block in loadProjectInfo incorrectly reports "No package.json
found" when handling ENOENT; update the thrown Error message in the catch of
loadProjectInfo (in project-resolver.ts) to reference the actual file path used
(baseplate/project-definition.json) or a clearer message like "No
project-definition.json found in baseplate for <directory>" so the error matches
the file being read; keep the same ENOENT check (error instanceof Error &&
'code' in error && error.code === 'ENOENT') and only change the string passed to
new Error(...) to mention project-definition.json/baseplate.
packages/project-builder-dev/src/runner/generate.ts (1)

22-32: ⚠️ Potential issue | 🔴 Critical

Hardcoded id, name, and isInternalExample break the generated project.

The hardcoded values passed to createNodeSchemaParserContext are test-specific and incorrect for production use:

  • id: 'test-project' — All projects get the same ID instead of a unique identifier (should use generateProjectId(directory))
  • name: 'Test Project' — All projects get the same name instead of reading from the actual project's project-definition.json (should use projectDef.settings.general.name)
  • isInternalExample: trueExcludes @baseplate-dev/project-builder-cli from dev dependencies (confirmed in root-package-compiler.ts: the CLI package is only included when isInternalExample is false)

The correct pattern is already established in project-discovery.ts and create-project/project-generator.ts: load these values from the actual project directory and definition. Apply the same approach here by reading the project definition and using isExampleProject(directory) to determine the internal example flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/runner/generate.ts` around lines 22 - 32,
The call to createNodeSchemaParserContext is using test-only hardcoded values
(id, name, isInternalExample); replace them by deriving the real values from the
project directory: call generateProjectId(projectDirectory) for the id, read the
project's definition to use projectDef.settings.general.name for the name, and
use isExampleProject(projectDirectory) for isInternalExample; pass these
computed values (alongside the existing logger, plugins, and version) into
createNodeSchemaParserContext so the generated project gets correct ID, name,
and dev-dependency behavior.
🧹 Nitpick comments (8)
examples/blog-with-auth/docker/.env (1)

2-4: Optional: reorder Postgres keys to satisfy dotenv-linter ordering.

This is only for lint cleanliness.

Suggested reordering
 COMPOSE_PROJECT_NAME=blog-with-auth-dev
-POSTGRES_PORT=5432
-POSTGRES_PASSWORD=blog-with-auth-password
 POSTGRES_DB=blog-with-auth
+POSTGRES_PASSWORD=blog-with-auth-password
+POSTGRES_PORT=5432
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/blog-with-auth/docker/.env` around lines 2 - 4, Reorder the Postgres
environment keys in the .env to satisfy dotenv-linter ordering: change the
sequence of POSTGRES_PORT, POSTGRES_PASSWORD, POSTGRES_DB to alphabetical order
(POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_PORT); update the file where those
three keys are defined so the linter passes.
examples/todo-with-auth0/docker/.env (1)

2-6: Optional: reorder keys to clear dotenv-linter UnorderedKey warnings.

No runtime issue, but reordering avoids noisy lint output.

Suggested reordering
 COMPOSE_PROJECT_NAME=todo-with-auth0-dev
-POSTGRES_PORT=6432
-POSTGRES_PASSWORD=todo-with-auth0-password
 POSTGRES_DB=todo-with-auth0
-REDIS_PORT=6379
+POSTGRES_PASSWORD=todo-with-auth0-password
+POSTGRES_PORT=6432
 REDIS_PASSWORD=todo-with-auth0-password
+REDIS_PORT=6379
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/todo-with-auth0/docker/.env` around lines 2 - 6, Reorder the
environment variable lines in the .env file to satisfy dotenv-linter's
UnorderedKey rule by sorting the keys alphabetically; specifically arrange the
POSTGRES_* and REDIS_* entries so the keys are in alphabetical order (e.g.,
POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_PORT, REDIS_PASSWORD, REDIS_PORT) to
remove the lint warnings.
packages/project-builder-dev/src/commands/templates.ts (1)

146-151: Repeated discoverPlugins(process.cwd(), logger) calls across handlers.

discoverPlugins is invoked identically in handleListTemplates, handleDeleteTemplate, handleExtractTemplates, and handleGenerateTemplates. Since each handler runs independently per CLI invocation, this is functionally fine — but if plugin discovery becomes expensive, consider extracting it to a shared lazy-initialized helper within this module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/commands/templates.ts` around lines 146 -
151, Multiple handlers (handleListTemplates, handleDeleteTemplate,
handleExtractTemplates, handleGenerateTemplates) each call
discoverPlugins(process.cwd(), logger) redundantly; extract this into a
module-scoped lazy helper (e.g., getPlugins or cachedDiscoverPlugins) that
caches the Promise/result on first call and returns it thereafter so subsequent
handler calls reuse the discovered plugins; update each handler to call the new
helper instead of calling discoverPlugins directly to avoid repeating expensive
discovery.
packages/project-builder-cli/src/utils/project-resolver.ts (1)

102-126: Different path-detection heuristic than the dev package's resolveProject.

The CLI's resolveProject uses includes('/') or includes('\\') to detect paths, while packages/project-builder-dev/src/utils/list-projects.ts uses startsWith('.'), path.isAbsolute(), and startsWith('~'). This means my-project would be a name lookup in both, but subfolder/project would be treated as a path in the CLI but a name lookup in the dev package.

This may be intentional since the two packages serve different audiences, but it's worth noting the inconsistency if users might expect uniform behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-cli/src/utils/project-resolver.ts` around lines 102
- 126, The CLI's resolveProject uses includes('/') or includes('\\') to detect
paths which differs from the dev package heuristic; update resolveProject to use
the same checks as packages/project-builder-dev (use path.isAbsolute(nameOrPath)
|| nameOrPath.startsWith('.') || nameOrPath.startsWith('~')) before calling
expandPathWithTilde/loadProjectInfo so path detection is consistent; locate
resolveProject and replace the includes-based branch with those predicates (you
can still call expandPathWithTilde and loadProjectInfo as before).
packages/project-builder-dev/src/utils/list-projects.ts (1)

15-15: Module-level getEnvConfig() call runs at import time.

getEnvConfig() parses process.env with a Zod schema synchronously at module load. If the schema has any required fields or strict validation, this will throw before any function in this module is called, which could produce confusing errors. Ensure this is intentional and that the schema is permissive enough for all import contexts (e.g., tests).

Consider lazy initialization if needed
-const config = getEnvConfig();
+let _config: ReturnType<typeof getEnvConfig> | undefined;
+function config(): ReturnType<typeof getEnvConfig> {
+  if (!_config) {
+    _config = getEnvConfig();
+  }
+  return _config;
+}

Then replace config.X with config().X throughout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/utils/list-projects.ts` at line 15, The
module-level call to getEnvConfig() (assigned to config) runs at import time and
can throw; change to lazy initialization by replacing the top-level const config
= getEnvConfig() with a function (e.g., getConfig() that calls getEnvConfig()
when invoked) and update all usages in this file to call getConfig().X (or call
getConfig() where needed) so parsing happens at runtime inside functions like
listProjects (or other exported functions) rather than at module import; ensure
getConfig() returns the same shaped object and keep getEnvConfig() unchanged.
packages/project-builder-dev/src/index.ts (1)

2-2: Consider using new Command() instead of the global program singleton.

Importing the program singleton from commander and mutating it inside runDevCli() makes the function non-idempotent. Calling parseAsync multiple times on the same Command instance is a known gotcha, because internally all parsed options are cached. While this is fine for a binary that runs once per process, the fact that runDevCli() is a named export in the public API (used by cli.ts and potentially tests) means a future caller could accidentally invoke it twice, leading to duplicate command registration and stale cached state.

♻️ Suggested refactor
-import { program } from 'commander';
+import { Command } from 'commander';

 export async function runDevCli(): Promise<void> {
   const version = (await getPackageVersion(import.meta.dirname)) ?? '0.0.0';
+  const program = new Command();

   program.version(version, '-v, --version');
   // ... rest unchanged

Also applies to: 20-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/index.ts` at line 2, The code uses the
global singleton `program` from commander which makes `runDevCli()`
non-idempotent; change `import { program } from 'commander'` to create a fresh
instance via `new Command()` inside `runDevCli()` (or a helper that returns it)
so each invocation gets a clean `Command` instance, move all command/registering
logic (the registrations currently applied to `program`) to use that local
`Command` instance, and call `parseAsync` on the new instance; ensure exported
`runDevCli()` returns or awaits the parse result as before and remove any
reliance on the global `program` so multiple calls won’t share cached parsed
state.
packages/project-builder-dev/src/utils/create-service-action-context.ts (1)

34-38: Include the caught error in plugin-discovery warnings.

The current warning drops the underlying exception, which makes triage harder when plugin roots are misconfigured.

Suggested improvement
     ...extraPluginDirs.map(async (dir) => {
       try {
         return await discoverPlugins(dir, logger);
-      } catch {
-        logger.warn(`Could not discover plugins from ${dir}`);
+      } catch (error) {
+        logger.warn(
+          { dir, error },
+          'Could not discover plugins from configured plugin root',
+        );
         return [] as PluginMetadataWithPaths[];
       }
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/utils/create-service-action-context.ts`
around lines 34 - 38, The catch block swallowing errors when calling
discoverPlugins(dir, logger) should capture the thrown error (e.g., catch (err))
and include it in the logger warning so the exception details are preserved;
update the catch for discoverPlugins to log a descriptive message and the caught
error via logger.warn (referencing discoverPlugins and logger.warn) before
returning the empty PluginMetadataWithPaths[].
packages/project-builder-dev/src/commands/diff.ts (1)

41-46: Avoid spreading CLI-only flags into action input.

failOnDifferences is a command-level behavior, not action input. Keep the action payload explicit to avoid leaking command-only options into the service-action contract.

Suggested refactor
         const result = await invokeServiceActionAsCli(
           diffProjectAction,
           {
             project: resolvedProject.name,
-            ...options,
+            compact: options.compact,
+            packages: options.packages,
+            include: options.include,
           },
           context,
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/commands/diff.ts` around lines 41 - 46, The
current call to invokeServiceActionAsCli(diffProjectAction, { project:
resolvedProject.name, ...options }) is leaking CLI-only flags (e.g.,
failOnDifferences) into the action payload; change it to pass an explicit
payload object containing only the action inputs required (start with { project:
resolvedProject.name } and add only service-relevant fields) or destructure
options to omit failOnDifferences before spreading, so diffProjectAction
receives only its intended inputs while the command retains failOnDifferences as
a local behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Line 58: The .gitignore change narrowed ignoring to "/.env", which only
ignores the repo-root file and risks committing nested .env files; revert to a
global ignore by restoring ".env" instead of "/.env" and, if you need a
checked-in template, explicitly unignore that template (e.g., add an allow rule
like "!<template-name>.env" or ".env.example") so only the safe template is
tracked; update the .gitignore entry accordingly.

In `@AGENTS.md`:
- Line 34: The documentation lists for the package
`@baseplate-dev/project-builder-common` are inconsistent: the summary mentions
plugin-email as part of the default bundle exposed by getDefaultPlugins(), but
the Plugins section omits it; update the Plugins section to include plugin-email
(or remove plugin-email from the summary if it is not actually returned by
getDefaultPlugins()) so both lists match and reflect the real output of
getDefaultPlugins().

In `@packages/create-project/src/version.ts`:
- Around line 6-7: getPackageVersion expects a package directory but the code
passes the resolved entry file path from require.resolve(...) into
getPackageVersion (cliPackagePath); fix by converting the resolved path to the
package directory (use path.dirname on the require.resolve result) before
calling getPackageVersion so that getPackageVersion receives the package folder
rather than a file path.

In `@packages/project-builder-dev/.env`:
- Line 2: Document or fix resolution of relative paths for
PLUGIN_ROOT_DIRECTORIES: either update README to state that environment-variable
relative paths (e.g., PLUGIN_ROOT_DIRECTORIES) are resolved against the launch
directory (process.cwd()), or change expandPathWithTilde to resolve relative
paths against the .env file location by deriving the .env file directory and
using that as the base instead of process.cwd() when calling path.resolve;
reference the PLUGIN_ROOT_DIRECTORIES env var and the expandPathWithTilde helper
to implement the chosen approach.

In `@packages/project-builder-dev/LICENSE`:
- Around line 275-315: The LICENSE file has Sections 6 and 7 mangled into
hyphen-prefixed/escaped lines; restore the canonical MPL-2.0 text for "6.
Disclaimer of Warranty" and "7. Limitation of Liability" (or replace the entire
LICENSE with the exact upstream MPL-2.0 text plus your explicit exception
addendum) so the clauses are normal, intact paragraphs rather than
hyphen-escaped lines; locate the malformed blocks by the headers "Disclaimer of
Warranty" and "Limitation of Liability" and update the text to match the
official MPL-2.0 wording verbatim (or drop in the full upstream license file and
then append the project-specific exception) to remove legal ambiguity.

In `@packages/project-builder-dev/src/commands/serve.ts`:
- Around line 55-58: The code computes portOffset and effectivePort without
validating numeric parsing, which can produce NaN and pass an invalid port to
startWebServer; update the logic around portOffset, port (the CLI --port value),
and effectivePort to validate parsed integers (use Number.isFinite or
Number.isNaN checks) and fall back to DEFAULT_SERVER_PORT or 0 when parsing
fails, and ensure you validate both process.env.PORT_OFFSET and the incoming
port before calling startWebServer so only a safe numeric port is passed.

In `@packages/project-builder-dev/src/commands/sync.ts`:
- Around line 24-27: The CLI currently allows --snapshot without requiring
--overwrite but still forwards snapshotDirectory; update the option validation
so that when snapshotDirectory (the '--snapshot' option) is provided the
overwrite flag (the '--overwrite' option / overwrite variable) must be true: add
a check after parsing options in sync.ts that throws or exits with a clear error
if snapshotDirectory is set and overwrite is false, and only forward
snapshotDirectory to downstream calls (e.g., where snapshotDirectory is passed
into the sync function or syncProject) when overwrite is true; reference the
snapshotDirectory and overwrite variables and the place where snapshotDirectory
is forwarded to ensure the dependency is enforced.

In `@packages/project-builder-dev/src/utils/config.ts`:
- Line 5: The EXCLUDE_EXAMPLES schema entry currently uses
z.coerce.boolean().optional(), which will treat non-empty strings like "false"
as true; update the EXCLUDE_EXAMPLES definition to use the string-to-boolean
codec (z.stringbool().optional()) so that common env string values
("true"/"false", "1"/"0") are parsed correctly and preserve optionality; locate
the EXCLUDE_EXAMPLES symbol in the config.ts and replace the z.coerce.boolean()
usage with z.stringbool() (keeping .optional()) throughout any related parsing
logic.

In `@packages/project-builder-dev/src/utils/find-examples-directories.ts`:
- Around line 9-10: The code promises an EXAMPLES_DIRECTORIES env override but
never uses it—update the findExamplesDirectories function to check
process.env.EXAMPLES_DIRECTORIES at the top and if present parse it as a
comma-separated list (trim entries, filter out empties), resolve/normalize paths
as needed, and immediately return that array instead of performing the
filesystem walk-up; keep the existing discovery logic unchanged for the no-env
case and ensure the early-return prevents further directory discovery.

In `@packages/project-builder-dev/src/utils/list-projects.ts`:
- Around line 70-90: The resolveProject function misclassifies bare relative
paths like "subfolder/project" as project names; update its path-detection logic
(in resolveProject) to treat any input containing a path separator as a
directory path (e.g., check for path.sep or '/' in projectNameOrDirectory) in
addition to the existing startsWith('.') / path.isAbsolute / startsWith('~')
checks, then expand via expandPathWithTilde and call loadProjectFromDirectory;
keep the existing fallback to listProjects and name/id lookup (listProjects and
the project find logic) unchanged.

In `@packages/utils/src/fs/get-package-version.ts`:
- Around line 17-18: The function that parses package.json (in
get-package-version.ts) currently casts JSON to { version?: string } and returns
packageJson.version directly; instead validate at runtime that
packageJson.version is a string before returning it. Update the return logic in
the function (e.g., in getPackageVersion / the packageJson variable handling) to
check typeof packageJson.version === 'string' and return that string, otherwise
return null, ensuring non-string values (numbers, objects, etc.) don't violate
the function contract.

---

Outside diff comments:
In `@packages/project-builder-cli/src/utils/project-resolver.ts`:
- Around line 171-175: The catch block in loadProjectInfo incorrectly reports
"No package.json found" when handling ENOENT; update the thrown Error message in
the catch of loadProjectInfo (in project-resolver.ts) to reference the actual
file path used (baseplate/project-definition.json) or a clearer message like "No
project-definition.json found in baseplate for <directory>" so the error matches
the file being read; keep the same ENOENT check (error instanceof Error &&
'code' in error && error.code === 'ENOENT') and only change the string passed to
new Error(...) to mention project-definition.json/baseplate.

In `@packages/project-builder-dev/src/runner/generate.ts`:
- Around line 22-32: The call to createNodeSchemaParserContext is using
test-only hardcoded values (id, name, isInternalExample); replace them by
deriving the real values from the project directory: call
generateProjectId(projectDirectory) for the id, read the project's definition to
use projectDef.settings.general.name for the name, and use
isExampleProject(projectDirectory) for isInternalExample; pass these computed
values (alongside the existing logger, plugins, and version) into
createNodeSchemaParserContext so the generated project gets correct ID, name,
and dev-dependency behavior.

---

Nitpick comments:
In `@examples/blog-with-auth/docker/.env`:
- Around line 2-4: Reorder the Postgres environment keys in the .env to satisfy
dotenv-linter ordering: change the sequence of POSTGRES_PORT, POSTGRES_PASSWORD,
POSTGRES_DB to alphabetical order (POSTGRES_DB, POSTGRES_PASSWORD,
POSTGRES_PORT); update the file where those three keys are defined so the linter
passes.

In `@examples/todo-with-auth0/docker/.env`:
- Around line 2-6: Reorder the environment variable lines in the .env file to
satisfy dotenv-linter's UnorderedKey rule by sorting the keys alphabetically;
specifically arrange the POSTGRES_* and REDIS_* entries so the keys are in
alphabetical order (e.g., POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_PORT,
REDIS_PASSWORD, REDIS_PORT) to remove the lint warnings.

In `@packages/project-builder-cli/src/utils/project-resolver.ts`:
- Around line 102-126: The CLI's resolveProject uses includes('/') or
includes('\\') to detect paths which differs from the dev package heuristic;
update resolveProject to use the same checks as packages/project-builder-dev
(use path.isAbsolute(nameOrPath) || nameOrPath.startsWith('.') ||
nameOrPath.startsWith('~')) before calling expandPathWithTilde/loadProjectInfo
so path detection is consistent; locate resolveProject and replace the
includes-based branch with those predicates (you can still call
expandPathWithTilde and loadProjectInfo as before).

In `@packages/project-builder-dev/src/commands/diff.ts`:
- Around line 41-46: The current call to
invokeServiceActionAsCli(diffProjectAction, { project: resolvedProject.name,
...options }) is leaking CLI-only flags (e.g., failOnDifferences) into the
action payload; change it to pass an explicit payload object containing only the
action inputs required (start with { project: resolvedProject.name } and add
only service-relevant fields) or destructure options to omit failOnDifferences
before spreading, so diffProjectAction receives only its intended inputs while
the command retains failOnDifferences as a local behavior.

In `@packages/project-builder-dev/src/commands/templates.ts`:
- Around line 146-151: Multiple handlers (handleListTemplates,
handleDeleteTemplate, handleExtractTemplates, handleGenerateTemplates) each call
discoverPlugins(process.cwd(), logger) redundantly; extract this into a
module-scoped lazy helper (e.g., getPlugins or cachedDiscoverPlugins) that
caches the Promise/result on first call and returns it thereafter so subsequent
handler calls reuse the discovered plugins; update each handler to call the new
helper instead of calling discoverPlugins directly to avoid repeating expensive
discovery.

In `@packages/project-builder-dev/src/index.ts`:
- Line 2: The code uses the global singleton `program` from commander which
makes `runDevCli()` non-idempotent; change `import { program } from 'commander'`
to create a fresh instance via `new Command()` inside `runDevCli()` (or a helper
that returns it) so each invocation gets a clean `Command` instance, move all
command/registering logic (the registrations currently applied to `program`) to
use that local `Command` instance, and call `parseAsync` on the new instance;
ensure exported `runDevCli()` returns or awaits the parse result as before and
remove any reliance on the global `program` so multiple calls won’t share cached
parsed state.

In `@packages/project-builder-dev/src/utils/create-service-action-context.ts`:
- Around line 34-38: The catch block swallowing errors when calling
discoverPlugins(dir, logger) should capture the thrown error (e.g., catch (err))
and include it in the logger warning so the exception details are preserved;
update the catch for discoverPlugins to log a descriptive message and the caught
error via logger.warn (referencing discoverPlugins and logger.warn) before
returning the empty PluginMetadataWithPaths[].

In `@packages/project-builder-dev/src/utils/list-projects.ts`:
- Line 15: The module-level call to getEnvConfig() (assigned to config) runs at
import time and can throw; change to lazy initialization by replacing the
top-level const config = getEnvConfig() with a function (e.g., getConfig() that
calls getEnvConfig() when invoked) and update all usages in this file to call
getConfig().X (or call getConfig() where needed) so parsing happens at runtime
inside functions like listProjects (or other exported functions) rather than at
module import; ensure getConfig() returns the same shaped object and keep
getEnvConfig() unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1225fda and 7791f4c.

⛔ Files ignored due to path filters (5)
  • examples/blog-with-auth/baseplate/generated/docker/.env is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/baseplate/generated/docker/.env is excluded by !**/generated/**, !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/simple/baseplate/generated/docker/.env is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/docker/.env is excluded by !tests/**
📒 Files selected for processing (64)
  • .changeset/add-project-builder-dev.md
  • .github/workflows/verify-examples-sync.yml
  • .gitignore
  • AGENTS.md
  • examples/blog-with-auth/docker/.env
  • examples/todo-with-auth0/docker/.env
  • package.json
  • packages/create-project/src/create-baseplate-project.ts
  • packages/create-project/src/version.ts
  • packages/project-builder-cli/package.json
  • packages/project-builder-cli/src/commands/config.ts
  • packages/project-builder-cli/src/commands/mcp.ts
  • packages/project-builder-cli/src/commands/projects.ts
  • packages/project-builder-cli/src/commands/server.ts
  • packages/project-builder-cli/src/index.ts
  • packages/project-builder-cli/src/services/user-config.ts
  • packages/project-builder-cli/src/utils/config.ts
  • packages/project-builder-cli/src/utils/create-service-action-context.ts
  • packages/project-builder-cli/src/utils/list-projects.ts
  • packages/project-builder-cli/src/utils/project-resolver.ts
  • packages/project-builder-cli/src/utils/project-resolver.unit.test.ts
  • packages/project-builder-cli/src/utils/version.ts
  • packages/project-builder-dev/.env
  • packages/project-builder-dev/.gitignore
  • packages/project-builder-dev/.prettierignore
  • packages/project-builder-dev/LICENSE
  • packages/project-builder-dev/README.md
  • packages/project-builder-dev/bin/baseplate-dev.js
  • packages/project-builder-dev/eslint.config.js
  • packages/project-builder-dev/package.json
  • packages/project-builder-dev/prettier.config.js
  • packages/project-builder-dev/src/cli.ts
  • packages/project-builder-dev/src/commands/dev-server.ts
  • packages/project-builder-dev/src/commands/diff.ts
  • packages/project-builder-dev/src/commands/mcp.ts
  • packages/project-builder-dev/src/commands/serve.ts
  • packages/project-builder-dev/src/commands/snapshot.ts
  • packages/project-builder-dev/src/commands/sync-examples.ts
  • packages/project-builder-dev/src/commands/sync.ts
  • packages/project-builder-dev/src/commands/templates.ts
  • packages/project-builder-dev/src/index.ts
  • packages/project-builder-dev/src/runner/generate.ts
  • packages/project-builder-dev/src/services/logger.ts
  • packages/project-builder-dev/src/utils/config.ts
  • packages/project-builder-dev/src/utils/create-service-action-context.ts
  • packages/project-builder-dev/src/utils/find-examples-directories.ts
  • packages/project-builder-dev/src/utils/list-projects.ts
  • packages/project-builder-dev/src/utils/resolve.ts
  • packages/project-builder-dev/tsconfig.app.json
  • packages/project-builder-dev/tsconfig.build.json
  • packages/project-builder-dev/tsconfig.json
  • packages/project-builder-server/src/actions/index.ts
  • packages/project-builder-server/src/actions/registry.ts
  • packages/project-builder-server/src/dev-server/mcp/stdio.ts
  • packages/project-builder-server/src/user-config/user-config-schema.ts
  • packages/project-builder-test/package.json
  • packages/project-builder-test/src/commands/generate.ts
  • packages/project-builder-test/src/commands/serve.ts
  • packages/project-builder-test/src/runner/runner.ts
  • packages/utils/src/fs/get-package-version.ts
  • packages/utils/src/fs/index.ts
  • packages/utils/src/paths/expand-path-with-tilde.ts
  • packages/utils/src/paths/index.ts
  • tsconfig.build.json
💤 Files with no reviewable changes (5)
  • packages/project-builder-cli/src/utils/version.ts
  • packages/project-builder-cli/package.json
  • packages/project-builder-cli/src/services/user-config.ts
  • packages/project-builder-cli/src/utils/config.ts
  • packages/project-builder-cli/src/utils/project-resolver.unit.test.ts

Comment thread .gitignore

# environment variable files
.env
/.env

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential secret-leak regression from narrowing .env ignore scope.

Switching to /.env only ignores the repository-root .env and allows nested .env files to be accidentally committed. If nested env files are not intended to be tracked, keep .env ignored globally and unignore only explicit safe templates.

Suggested adjustment
-/.env
+.env
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/.env
.env
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 58, The .gitignore change narrowed ignoring to "/.env",
which only ignores the repo-root file and risks committing nested .env files;
revert to a global ignore by restoring ".env" instead of "/.env" and, if you
need a checked-in template, explicitly unignore that template (e.g., add an
allow rule like "!<template-name>.env" or ".env.example") so only the safe
template is tracked; update the .gitignore entry accordingly.

Comment thread AGENTS.md
Comment thread packages/create-project/src/version.ts Outdated
@@ -0,0 +1,2 @@
NO_BROWSER=true
PLUGIN_ROOT_DIRECTORIES=../../packages/project-builder-common

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, understand the structure and find relevant config files
find packages/project-builder-dev -type f \( -name "*.ts" -o -name "*.js" -o -name ".env*" \) | head -20

Repository: halfdomelabs/baseplate

Length of output: 1098


🏁 Script executed:

# Search for config loading and PLUGIN_ROOT_DIRECTORIES usage
rg -n "PLUGIN_ROOT_DIRECTORIES|getEnvConfig|dotenv|loadEnv" packages/project-builder-dev --type ts --type js

Repository: halfdomelabs/baseplate

Length of output: 804


🏁 Script executed:

# Find the .env file and any config parsing logic
find packages/project-builder-dev -name ".env*" -o -name "*config*" | grep -E "\.(ts|js|env)" | head -20

Repository: halfdomelabs/baseplate

Length of output: 381


🏁 Script executed:

cat -n packages/project-builder-dev/src/utils/config.ts

Repository: halfdomelabs/baseplate

Length of output: 561


🏁 Script executed:

# Also check the context where PLUGIN_ROOT_DIRECTORIES is used
cat -n packages/project-builder-dev/src/utils/create-service-action-context.ts | head -40

Repository: halfdomelabs/baseplate

Length of output: 1704


🏁 Script executed:

# Find expandPathWithTilde implementation
find packages -name "*.ts" -type f | xargs rg -l "expandPathWithTilde" | grep -E "(utils|node)" | head -5

Repository: halfdomelabs/baseplate

Length of output: 359


🏁 Script executed:

# Find discoverPlugins implementation
find packages -name "*.ts" -type f | xargs rg -l "discoverPlugins" | grep -E "project-builder-server" | head -5

Repository: halfdomelabs/baseplate

Length of output: 416


🏁 Script executed:

# Search for the actual implementations
rg -n "export.*expandPathWithTilde|function expandPathWithTilde" packages

Repository: halfdomelabs/baseplate

Length of output: 187


🏁 Script executed:

# Also check project-builder-server for discoverPlugins
rg -n "export.*discoverPlugins|function discoverPlugins" packages

Repository: halfdomelabs/baseplate

Length of output: 171


🏁 Script executed:

cat -n packages/utils/src/paths/expand-path-with-tilde.ts

Repository: halfdomelabs/baseplate

Length of output: 404


🏁 Script executed:

cat -n packages/project-builder-server/src/plugins/plugin-discovery.ts | head -60

Repository: halfdomelabs/baseplate

Length of output: 2581


🏁 Script executed:

# Look for .env.example or documentation
find packages/project-builder-dev -name ".env*" -o -name "README*" | xargs cat 2>/dev/null

Repository: halfdomelabs/baseplate

Length of output: 2083


🏁 Script executed:

# Check if there's any documentation about running the dev tool
rg -n "PLUGIN_ROOT_DIRECTORIES|launch|cwd|working.*directory|run from" packages/project-builder-dev --type ts --type md | head -20

Repository: halfdomelabs/baseplate

Length of output: 2411


🏁 Script executed:

# Check package.json scripts to see expected launch directory
cat -n packages/project-builder-dev/package.json | grep -A5 -B5 "scripts"

Repository: halfdomelabs/baseplate

Length of output: 602


Document required working directory for relative paths in PLUGIN_ROOT_DIRECTORIES.

Line 2 uses a relative path that is resolved against process.cwd() (via path.resolve() in expandPathWithTilde). This breaks if the tool is not run from the repository root.

Update the README to explicitly document that relative paths in environment variables are resolved from the launch directory, or add safeguards (e.g., resolve relative to the .env file location instead of process.cwd()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/.env` at line 2, Document or fix resolution of
relative paths for PLUGIN_ROOT_DIRECTORIES: either update README to state that
environment-variable relative paths (e.g., PLUGIN_ROOT_DIRECTORIES) are resolved
against the launch directory (process.cwd()), or change expandPathWithTilde to
resolve relative paths against the .env file location by deriving the .env file
directory and using that as the base instead of process.cwd() when calling
path.resolve; reference the PLUGIN_ROOT_DIRECTORIES env var and the
expandPathWithTilde helper to implement the chosen approach.

Comment on lines +275 to +315
- *
- 6. Disclaimer of Warranty \*
- ------------------------- \*
- *
- Covered Software is provided under this License on an "as is" \*
- basis, without warranty of any kind, either expressed, implied, or \*
- statutory, including, without limitation, warranties that the \*
- Covered Software is free of defects, merchantable, fit for a \*
- particular purpose or non-infringing. The entire risk as to the \*
- quality and performance of the Covered Software is with You. \*
- Should any Covered Software prove defective in any respect, You \*
- (not any Contributor) assume the cost of any necessary servicing, \*
- repair, or correction. This disclaimer of warranty constitutes an \*
- essential part of this License. No use of any Covered Software is \*
- authorized under this License except under this disclaimer. \*
- *

---

---

- *
- 7. Limitation of Liability \*
- -------------------------- \*
- *
- Under no circumstances and under no legal theory, whether tort \*
- (including negligence), contract, or otherwise, shall any \*
- Contributor, or anyone who distributes Covered Software as \*
- permitted above, be liable to You for any direct, indirect, \*
- special, incidental, or consequential damages of any character \*
- including, without limitation, damages for lost profits, loss of \*
- goodwill, work stoppage, computer failure or malfunction, or any \*
- and all other commercial damages or losses, even if such party \*
- shall have been informed of the possibility of such damages. This \*
- limitation of liability shall not apply to liability for death or \*
- personal injury resulting from such party's negligence to the \*
- extent applicable law prohibits such limitation. Some \*
- jurisdictions do not allow the exclusion or limitation of \*
- incidental or consequential damages, so this exclusion and \*
- limitation may not apply to You. \*
- *

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore Sections 6 and 7 as valid license text (currently malformed/removed).

Disclaimer of Warranty and Limitation of Liability are currently represented as hyphen-prefixed/escaped lines, which makes this MPL body non-canonical and legally ambiguous. Please include these sections as normal, intact license clauses (or replace the full file with the exact upstream MPL-2.0 text plus your explicit exception addendum).

Proposed fix direction
- -                                                                      *
- - 6.  Disclaimer of Warranty \*
- - ------------------------- \*
- ...
- - 7.  Limitation of Liability \*
- - -------------------------- \*
- ...
+6. Disclaimer of Warranty
+Covered Software is provided under this License on an "as is" basis, ...
+
+7. Limitation of Liability
+Under no circumstances and under no legal theory, whether tort ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/LICENSE` around lines 275 - 315, The LICENSE
file has Sections 6 and 7 mangled into hyphen-prefixed/escaped lines; restore
the canonical MPL-2.0 text for "6. Disclaimer of Warranty" and "7. Limitation of
Liability" (or replace the entire LICENSE with the exact upstream MPL-2.0 text
plus your explicit exception addendum) so the clauses are normal, intact
paragraphs rather than hyphen-escaped lines; locate the malformed blocks by the
headers "Disclaimer of Warranty" and "Limitation of Liability" and update the
text to match the official MPL-2.0 wording verbatim (or drop in the full
upstream license file and then append the project-specific exception) to remove
legal ambiguity.

Comment thread packages/project-builder-dev/src/commands/sync.ts
Comment thread packages/project-builder-dev/src/utils/config.ts Outdated
Comment on lines +9 to +10
* Override with EXAMPLES_DIRECTORIES env var (comma-separated paths) to skip
* discovery entirely.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

EXAMPLES_DIRECTORIES override is documented but not implemented.

Lines 9-10 promise env-based override, but the function always performs filesystem walk-up. This breaks explicit config flows.

🔧 Suggested fix
-import { dirExists } from '@baseplate-dev/utils/node';
+import { dirExists, expandPathWithTilde } from '@baseplate-dev/utils/node';
@@
 export async function findExamplesDirectories(): Promise<string[]> {
+  const explicitDirectories = process.env.EXAMPLES_DIRECTORIES?.split(',')
+    .map((d) => d.trim())
+    .filter(Boolean);
+
+  if (explicitDirectories && explicitDirectories.length > 0) {
+    return explicitDirectories.map((d) => expandPathWithTilde(d));
+  }
+
   let dir = process.cwd();

Also applies to: 15-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/utils/find-examples-directories.ts` around
lines 9 - 10, The code promises an EXAMPLES_DIRECTORIES env override but never
uses it—update the findExamplesDirectories function to check
process.env.EXAMPLES_DIRECTORIES at the top and if present parse it as a
comma-separated list (trim entries, filter out empties), resolve/normalize paths
as needed, and immediately return that array instead of performing the
filesystem walk-up; keep the existing discovery logic unchanged for the no-env
case and ensure the early-return prevents further directory discovery.

Comment thread packages/project-builder-dev/src/utils/list-projects.ts
Comment thread packages/utils/src/fs/get-package-version.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/project-builder-dev/src/commands/serve.ts (1)

55-58: PORT_OFFSET NaN not validated — effectivePort can be NaN.

This is a carry-over from the previous review cycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/commands/serve.ts` around lines 55 - 58, The
current PORT_OFFSET parsing can yield NaN and make effectivePort NaN; update the
logic around process.env.PORT_OFFSET and portOffset so you parse and validate it
(use Number.parseInt/process.env.PORT_OFFSET and check Number.isFinite or
!Number.isNaN) and fall back to 0 on invalid values before computing
effectivePort; ensure the calculation of effectivePort (using
DEFAULT_SERVER_PORT, port, and portOffset) only uses the validated numeric
portOffset so effectivePort can never be NaN.
🧹 Nitpick comments (1)
packages/project-builder-dev/src/commands/serve.ts (1)

87-91: Use an explicit radix wrapper instead of passing Number.parseInt directly.

Commander invokes the parser as fn(valueString, previousValue), so the radix argument to Number.parseInt will be whatever Commander passes as previousValue (usually undefined, which defaults to base-10, but that's implicit and fragile). Wrapping it also makes NaN handling easier.

♻️ Proposed refactor
     .option(
       '--port <number>',
       'Port to listen on',
-      Number.parseInt,
+      (v: string) => Number.parseInt(v, 10),
       process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/commands/serve.ts` around lines 87 - 91,
Replace the direct use of Number.parseInt as the Commander parser for the
'--port <number>' option with an explicit wrapper that calls
Number.parseInt(value, 10) (and normalizes NaN to undefined or a safe default);
update the option definition in serve.ts where Number.parseInt is currently
passed so the parser signature is fn(valueString, previousValue) and you control
the radix and NaN handling for the port value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/project-builder-dev/src/commands/serve.ts`:
- Around line 77-79: Update the description string passed to the serve command's
.description(...) to remove references to the removed environment flags
EXCLUDE_EXAMPLES and EXAMPLES_DIRECTORIES; locate the .description call on the
serve command (the serve command definition in serve.ts) and replace the stale
text with a brief accurate description such as "Starts the project builder web
service for discovered projects" or otherwise reflect current behavior (e.g.,
mention INCLUDE_EXAMPLES only if still applicable).
- Around line 33-42: resolveModule(...) can throw when the module isn't
installed, so wrap the resolveModule call used to compute projectBuilderWebDir
in a try/catch and handle the error before calling packageDirectory or
getPackageVersion; specifically, catch errors from
resolveModule(import.meta.resolve...) inside the code that sets
projectBuilderWebDir (the resolveModule call referenced alongside
packageDirectory and projectBuilderWebDir), and either set projectBuilderWebDir
to undefined or rethrow a new Error with the same user-friendly message (`Unable
to find project-builder-web package to host website`) so the null guard that
checks projectBuilderWebDir (and getPackageVersion usage) never runs into an
uncaught import.meta.resolve exception.

---

Duplicate comments:
In `@packages/project-builder-dev/src/commands/serve.ts`:
- Around line 55-58: The current PORT_OFFSET parsing can yield NaN and make
effectivePort NaN; update the logic around process.env.PORT_OFFSET and
portOffset so you parse and validate it (use
Number.parseInt/process.env.PORT_OFFSET and check Number.isFinite or
!Number.isNaN) and fall back to 0 on invalid values before computing
effectivePort; ensure the calculation of effectivePort (using
DEFAULT_SERVER_PORT, port, and portOffset) only uses the validated numeric
portOffset so effectivePort can never be NaN.

---

Nitpick comments:
In `@packages/project-builder-dev/src/commands/serve.ts`:
- Around line 87-91: Replace the direct use of Number.parseInt as the Commander
parser for the '--port <number>' option with an explicit wrapper that calls
Number.parseInt(value, 10) (and normalizes NaN to undefined or a safe default);
update the option definition in serve.ts where Number.parseInt is currently
passed so the parser signature is fn(valueString, previousValue) and you control
the radix and NaN handling for the port value.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7791f4c and f9526d1.

📒 Files selected for processing (7)
  • knip.config.js
  • packages/project-builder-cli/package.json
  • packages/project-builder-cli/src/utils/find-examples-directories.ts
  • packages/project-builder-dev/README.md
  • packages/project-builder-dev/src/commands/serve.ts
  • packages/project-builder-dev/src/services/logger.ts
  • packages/project-builder-dev/tsconfig.json
💤 Files with no reviewable changes (2)
  • packages/project-builder-cli/package.json
  • packages/project-builder-cli/src/utils/find-examples-directories.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/project-builder-dev/tsconfig.json
  • packages/project-builder-dev/README.md

Comment thread packages/project-builder-dev/src/commands/serve.ts Outdated
Comment thread packages/project-builder-dev/src/commands/serve.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/project-builder-dev/src/commands/serve.ts (1)

92-97: ⚠️ Potential issue | 🟡 Minor

Validate parsed --port and PORT env values to prevent NaN.

The port parsing at lines 95-96 can yield NaN if given non-numeric input. Unlike PORT_OFFSET (which now has NaN handling at line 63), invalid --port or PORT values will pass NaN through to startWebServer.

🛡️ Proposed fix to add validation
+function parsePort(value: string): number | undefined {
+  const parsed = Number.parseInt(value, 10);
+  return Number.isNaN(parsed) ? undefined : parsed;
+}
+
 export function addServeCommand(program: Command): void {
   program
     .command('serve')
@@
     .option(
       '--port <number>',
       'Port to listen on',
-      (v: string) => Number.parseInt(v, 10),
-      process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined,
+      (v: string) => parsePort(v),
+      process.env.PORT ? parsePort(process.env.PORT) : undefined,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/commands/serve.ts` around lines 92 - 97, The
parsed port value from the command option '--port' and process.env.PORT can
become NaN and be forwarded into startWebServer; update the option handler in
serve.ts to validate the parsed number (e.g., after parsing check
Number.isFinite or Number.isInteger and !Number.isNaN and that it’s within valid
port range 1–65535) and return undefined or a safe default when invalid so
startWebServer never receives NaN; ensure the same validation logic is applied
where process.env.PORT is read and mention the validated symbol names '--port'
option and startWebServer so you can locate the change.
🧹 Nitpick comments (2)
packages/project-builder-dev/src/runner/generate.ts (2)

24-29: Consider replacing hardcoded version with a dynamic value.

The CLI version '0.1.0' is hardcoded, which may become stale as the package evolves. Consider importing the version from the package's package.json or using a centralized version constant to keep it synchronized with the actual package version.

♻️ Suggested approach
+import { version } from '../../package.json' with { type: 'json' };
+// or import from a centralized version utility if available
+
 const nodeSchemaParserContext = await createNodeSchemaParserContext(
   projectInfo,
   logger,
   plugins,
-  '0.1.0',
+  version,
 );

Alternatively, if there's a centralized version utility in the monorepo (as mentioned in the PR summary about centralized utilities), that would be preferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/runner/generate.ts` around lines 24 - 29,
Replace the hardcoded CLI version string passed to createNodeSchemaParserContext
('0.1.0') with a dynamic value: import the package version from this package's
package.json (or use the centralized version constant/utility if one exists in
the monorepo) and pass that variable into createNodeSchemaParserContext so the
runtime always uses the actual package/CLI version; update the call site where
nodeSchemaParserContext is created to use the imported/centralized version
variable instead of the literal.

37-40: Consider using pino's standard error logging pattern.

For better error serialization (including stack traces in structured logs), pino recommends passing the error object as the first argument with err key:

♻️ Suggested fix
   } catch (error) {
-    logger.error('Project sync failed:', error);
+    logger.error({ err: error }, 'Project sync failed');
     throw error;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-dev/src/runner/generate.ts` around lines 37 - 40, In
the catch block inside the generate runner, change the logger.error call to use
pino's structured error pattern by passing the error object as the first
argument (e.g., logger.error({ err: error }, 'Project sync failed')) so the
stack and error properties are serialized correctly; keep rethrowing the error
after logging. Target the catch block containing logger.error('Project sync
failed:', error) in the generate.ts file and replace that call with the
structured-object form using the same error variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/project-builder-dev/src/commands/serve.ts`:
- Around line 92-97: The parsed port value from the command option '--port' and
process.env.PORT can become NaN and be forwarded into startWebServer; update the
option handler in serve.ts to validate the parsed number (e.g., after parsing
check Number.isFinite or Number.isInteger and !Number.isNaN and that it’s within
valid port range 1–65535) and return undefined or a safe default when invalid so
startWebServer never receives NaN; ensure the same validation logic is applied
where process.env.PORT is read and mention the validated symbol names '--port'
option and startWebServer so you can locate the change.

---

Nitpick comments:
In `@packages/project-builder-dev/src/runner/generate.ts`:
- Around line 24-29: Replace the hardcoded CLI version string passed to
createNodeSchemaParserContext ('0.1.0') with a dynamic value: import the package
version from this package's package.json (or use the centralized version
constant/utility if one exists in the monorepo) and pass that variable into
createNodeSchemaParserContext so the runtime always uses the actual package/CLI
version; update the call site where nodeSchemaParserContext is created to use
the imported/centralized version variable instead of the literal.
- Around line 37-40: In the catch block inside the generate runner, change the
logger.error call to use pino's structured error pattern by passing the error
object as the first argument (e.g., logger.error({ err: error }, 'Project sync
failed')) so the stack and error properties are serialized correctly; keep
rethrowing the error after logging. Target the catch block containing
logger.error('Project sync failed:', error) in the generate.ts file and replace
that call with the structured-object form using the same error variable.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f9526d1 and f438d65.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • AGENTS.md
  • knip.config.js
  • packages/create-project/src/version.ts
  • packages/project-builder-cli/src/utils/project-resolver.ts
  • packages/project-builder-cli/src/utils/project-resolver.unit.test.ts
  • packages/project-builder-dev/README.md
  • packages/project-builder-dev/package.json
  • packages/project-builder-dev/src/commands/serve.ts
  • packages/project-builder-dev/src/commands/sync.ts
  • packages/project-builder-dev/src/runner/generate.ts
  • packages/project-builder-dev/src/utils/config.ts
  • packages/project-builder-dev/src/utils/find-examples-directories.ts
  • packages/project-builder-dev/src/utils/list-projects.ts
  • packages/utils/src/fs/get-package-version.ts
  • packages/utils/src/fs/read-json-with-schema.ts
  • packages/utils/src/fs/read-json-with-schema.unit.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/project-builder-dev/src/commands/sync.ts
  • packages/utils/src/fs/get-package-version.ts
  • packages/project-builder-cli/src/utils/project-resolver.ts
  • packages/project-builder-dev/src/utils/find-examples-directories.ts
  • knip.config.js
  • packages/project-builder-dev/package.json
  • packages/project-builder-dev/src/utils/config.ts
  • packages/project-builder-dev/src/utils/list-projects.ts
  • packages/project-builder-cli/src/utils/project-resolver.unit.test.ts

@kingston kingston merged commit a136dc9 into main Feb 26, 2026
12 checks passed
@kingston kingston deleted the kingston/eng-1032-refactor-the-project-builder-cli-to-enable-separate-dev-and branch February 26, 2026 11:55
@github-actions github-actions Bot mentioned this pull request Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant