Skip to content

refactor(cli): migrate off Bun runtime to Node#44

Merged
yashmehrotra merged 5 commits into
mainfrom
refactor/cli-bun-to-node
Jun 22, 2026
Merged

refactor(cli): migrate off Bun runtime to Node#44
yashmehrotra merged 5 commits into
mainfrom
refactor/cli-bun-to-node

Conversation

@moshloop

@moshloop moshloop commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

Ports the facet CLI off the Bun runtime so it can ship as a real, Node-installable npm package (`@flanksource/facet-cli`). Previously the CLI relied on Bun APIs (`Bun.serve`, Bun shell `$`, `Bun.spawn`, `import ... with { type: 'file' }`, `bun:test`) and was distributed as a bun-compiled standalone binary, which made it impossible to `npm install -g`.

Changes

  • Shell: new `src/utils/shell.ts` Node `$` tagged-template helper (spawn via `/bin/sh -c`), replacing Bun shell across 6 files.
  • Server: `preview.ts` serves over `node:http` with Web Request/Response adapters (SSE preserved) instead of `Bun.serve`.
  • Assets: new `src/utils/assets.ts` resolves `package.json`/`openapi.yaml`/`styles.css`/vite loaders via `import.meta.url`, replacing Bun's `with { type: 'file' }` imports; `scripts/copy-assets.cjs` stages them into `dist/assets/`.
  • Spawn: `node:child_process` (`spawn`/`execFileSync`) replaces `Bun.spawn`/`Bun.spawnSync`; `resolveTsxBin` runs the vite loaders via bundled `tsx` (no bun).
  • Loaders: `vite-ssr-loader.ts` gets a `createRequire` shim for ESM.
  • Build: tsdown produces a Node ESM bundle (`dist/cli.mjs` + chunks); dropped `bun build --compile`.
  • Tests: all `bun:test` → `vitest`; full suite (183 tests) passes on Node, render-api browser tests verified end-to-end.
  • Release: `release.yml` drops the bun-compile binary matrix and publishes both `@flanksource/facet` and `@flanksource/facet-cli`; the GitHub release is cut only after both publish.
  • Docker: drops both bun installs; the final stage `npm install -g` the packed CLI tarball (pulls tsx/puppeteer/vite, exposes the `facet` bin).

Verification

`node dist/cli.mjs pdf SimpleReport.tsx --data simple-data.json` produces a valid 52KB PDF-1.7 with no Bun runtime present. `vitest run`: 183 passed; the 2 sandbox-only failures are macOS sandbox blocking Chrome (pass when Chrome can launch).

Summary by CodeRabbit

Release Notes

  • New Features
    • Added --live mode for HTML/PDF rendering using a live browser environment snapshot
    • Enhanced facet doctor with --json/--fix/--verbose, including more automated remediation and environment warnings
    • Added serve --cache-dir to choose the render cache location
    • Added local facet overrides via FACET_PACKAGE_PATH
    • Added new diagram components (Diagram, BoxNode, Arrow) and palette support
    • MatrixTable now supports row/label/cell styling plus rowHeight and labelPadding
  • Bug Fixes
    • Fixed Badge label rendering when the label is an empty string
  • Documentation
    • Updated facet doctor documentation and improved installation guidance
  • Dependencies
    • Switched to puppeteer-core and added react-xarrows

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds a diagram component library (Diagram, BoxNode, Arrow, color palette, content helpers) backed by react-xarrows; introduces a live browser-render pipeline using a spawned Vite dev server, Puppeteer HTML snapshot, and FACET_PACKAGE_PATH local override support in FacetDirectory; refactors the doctor command with a CHECK_REGISTRY and auto-fix pipeline; consolidates Tailwind invocation into a shared utility; migrates all Puppeteer imports to puppeteer-core; and applies targeted styling and ESM-compatibility fixes.

Changes

Diagram component library

Layer / File(s) Summary
Color palette, Arrow, BoxNode, Diagram, and content components
src/components/diagram/colors.ts, src/components/diagram/Arrow.tsx, src/components/diagram/BoxNode.tsx, src/components/diagram/Diagram.tsx, src/components/diagram/content.tsx, src/components/diagram/Arrow.test.tsx, src/components/diagram/BoxNode.test.tsx
DiagramColors and COLORS define the palette; Arrow wraps react-xarrows with four variant presets; BoxNode renders a header/body node with Tailwind border derivation; Diagram scopes ids, gates SSR, and delays data-facet-ready=true by two rAF frames; NodePill, NodeSection, and SectionDivider provide node content primitives; tests verify variant props and border-class derivation.
Barrel exports, dependency, and DataFlowDiagram example
src/components/diagram/index.ts, src/components/index.tsx, package.json, examples/kitchen-sink/DataFlowDiagram.tsx
diagram/index.ts and the main components barrel re-export all diagram symbols; react-xarrows added as a root dependency; DataFlowDiagram demonstrates three-node layout with two primary Arrow connections via the Diagram render-prop id factory.

Live rendering pipeline and local facet override

Layer / File(s) Summary
FacetPackageOverride types, staleness helpers, and fingerprinting
cli/src/builders/facet-directory.ts
FacetPackageOverride interface and resolveFacetPackageOverride, needsLocalFacetComponentsBuild, needsLocalFacetCssBuild, localFacetBuildFingerprint were exported; file:/link:/portal: local-protocol references are anchored to absolute paths; workspace: specs are rejected.
FacetDirectory client scaffold and generatePackageJson override wiring
cli/src/builders/facet-directory.ts
generateClientScaffold produces live-render client entry, injected data, index.html, vite.client.config.ts, and postcss.config.js; generatePackageJson prefers local override metadata, pins @flanksource/facet to file:, records facetLocalOverride fingerprint, forwards consumer pnpm fields, adds react-xarrows to required deps, removes stale installs, and enforces .npmrc 0o600 permissions; copyViteDevLoader was added.
FacetDirectory override and staleness tests
cli/src/builders/facet-directory.test.ts
Tests cover directory/tarball resolution, local asset propagation into .facet, facetLocalOverride fingerprint in package.json, fingerprint invalidation on dist change, stale component/CSS mtime detection, and removal of stale installed override plus pnpm-lock.yaml.
vite-dev-loader subprocess entry
cli/vite-dev-loader.ts
Reads --facet-root=, boots a Vite dev server at ephemeral port, redirects console.log to stderr, emits FACET_DEV_URL=<url> handshake on stdout, and registers SIGTERM/SIGINT shutdown handlers.
ViteServer orchestration and handshake
cli/src/bundler/vite-server.ts, cli/src/bundler/vite-builder.ts
startViteServer scaffolds .facet, installs deps via installWithRetry (now exported), spawns bun run vite-dev-loader.ts, reads the FACET_DEV_URL handshake from stdout with a 60s timeout, and returns { url, close() }.
Live snapshot capture and generateHTML --live branch
cli/src/bundler/live-snapshot.ts, cli/src/generators/html.ts, cli/src/types.ts, cli/src/cli.ts
snapshotHTML detects Vite errors, waits for data-facet-ready=true, verifies #facet-root has children, captures page HTML, and strips dev-only scripts; generateHTML gains an options.live branch that calls startViteServer + snapshotHTML; GenerateOptions.live?: boolean was added; CLI forwards --live to html and pdf generators.

CLI enhancements: doctor, Tailwind utility, server config, and package manager

Layer / File(s) Summary
Tailwind binary utility and route/html integration
cli/src/utils/tailwind.ts, cli/src/utils/tailwind.test.ts, cli/src/server/routes.ts, cli/src/generators/html.ts
TailwindBinNotFoundError, resolveTailwindBin, tailwindBinExists, and runTailwind were added; routes.ts and html.ts replaced inline pnpm exec tailwindcss calls with runTailwind; tests cover POSIX/Windows paths, binary existence, and missing-bin errors.
Server cacheDir config and preview resolution
cli/src/server/config.ts, cli/src/server/preview.ts, cli/src/cli.ts, cli/test/render-api.test.ts
ServerConfig/ServerCLIFlags gained cacheDir; loadConfig populates it from flag or FACET_CACHE_DIR; preview.ts resolves it as absolute; CLI serve command wires --cache-dir; render-api tests create an isolated per-run cache dir via mkdtemp.
Doctor check registry, auto-fix pipeline, and CLI --fix
cli/src/commands/doctor.ts, cli/src/commands/doctor.test.ts, cli/src/cli.ts, cli/README.md
CHECK_REGISTRY replaces hardcoded push order; runDoctor optionally calls applyFixes; tryFix implements auto-fix for pnpm/chromium/native-bindings/npmrc-leakage; Rosetta probe switched to Bun.spawnSync; optional-bin checks for git/tar/tsx and a Tailwind presence check were added; DoctorOptions.fix? and CLI --fix were wired; README documents doctor usage, JSON schema, exit codes, and remediation table.
Package manager resolution and structured probe messaging
cli/src/utils/package-manager.ts, cli/src/utils/package-manager.test.ts
resolvePackageManager memoizes a single cachedVersion probe; per-cwd pin scanning is always performed; tryProbePnpmVersion and tryCorepackEnablePnpm helpers were added; error messages include the underlying probe error; tests verify thrown message patterns for missing pnpm with and without a version pin.

Housekeeping: puppeteer-core, ESM migration, CI, and component styling

Layer / File(s) Summary
puppeteer → puppeteer-core across package and type imports
cli/package.json, cli/tsdown.config.ts, cli/src/utils/pdf-generator.ts, cli/src/utils/pdf-multipass.ts, cli/src/utils/debug-annotations.ts, cli/src/server/worker-pool.ts
All Puppeteer imports and the build external list are switched from puppeteer to puppeteer-core with no runtime logic changes.
ESM path migration and Node fs compatibility in tests
cli/test/pdf-bleed.test.ts, cli/test/pdf-security.test.ts, cli/test/render-api.test.ts
__dirname replaced with import.meta.dirname; render-api replaced Bun.file with Node readFileSync/statSync/readFile and constructs archive upload FormData from raw disk bytes.
CI credential hardening, gitignore, and gen-version comments
.github/workflows/release.yml, .github/workflows/test.yml, .gitignore, cli/.gitignore, cli/scripts/gen-version.cjs
Release workflow checkouts add persist-credentials: false; test.yml action steps pinned to commit SHAs; .gitignore and cli/.gitignore ignore .facet/ paths; gen-version.cjs adds comments explaining intentional stderr suppression.
Component style and logic fixes
src/components/CompactTable.tsx, src/components/LogoGrid.tsx, src/components/MatrixTable.tsx, src/components/Badge.tsx, src/components/ListTable.tsx, src/components/StatCard.tsx, src/styles.css, examples/kitchen-sink/*
CompactTable/LogoGrid thead classes changed to table-header; MatrixTable gained rowHeight/labelPadding and per-row/cell style overrides; Badge.renderLabel treats only null/undefined as absent; ListTable.splitColorClasses passes non-color tokens through to Badge className; StatCard gets shrink JSDoc; @import url(...) syntax updated; kitchen-sink header zero-padding and title text fixes.

Sequence Diagram

sequenceDiagram
  participant CLI as facet html --live
  participant generateHTML
  participant startViteServer
  participant FacetDirectory
  participant vite_dev_loader as vite-dev-loader.ts (subprocess)
  participant snapshotHTML
  participant Puppeteer

  CLI->>generateHTML: options.live=true
  generateHTML->>startViteServer: templatePath, data
  startViteServer->>FacetDirectory: scaffold + generateClientScaffold + installWithRetry
  startViteServer->>vite_dev_loader: bun run --facet-root=.facet/
  vite_dev_loader-->>startViteServer: stdout: FACET_DEV_URL=http://127.0.0.1:PORT/
  startViteServer-->>generateHTML: { url, close() }
  generateHTML->>snapshotHTML: url
  snapshotHTML->>Puppeteer: launchBrowser, navigate(url)
  Puppeteer-->>snapshotHTML: page loaded
  snapshotHTML->>Puppeteer: wait data-facet-ready=true on Diagram
  snapshotHTML->>Puppeteer: page.content()
  snapshotHTML-->>generateHTML: cleaned HTML (dev scripts stripped)
  generateHTML->>generateHTML: write outputName.html
  generateHTML->>startViteServer: close()
Loading

Possibly related PRs

  • flanksource/facet#25: Modifies Badge.tsx to add wrap behavior and className styling hooks alongside label/value class customization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(cli): migrate off Bun runtime to Node' accurately captures the main objective of this pull request—porting the facet CLI from Bun to Node.js to enable distribution as an npm package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/cli-bun-to-node
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/cli-bun-to-node

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.

Comment thread cli/src/bundler/live-snapshot.ts Fixed
Comment thread cli/src/bundler/live-snapshot.ts Fixed
Comment thread src/components/Document.tsx Fixed

@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: 18

Caution

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

⚠️ Outside diff range comments (6)
src/components/Badge.tsx (1)

432-434: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Nullish check regression drops valid label={0} in non-label variants.

Line 433 still uses a truthy guard (!label) even though label now supports React.ReactNode. This hides valid values like 0 (and can misalign label/value rendering). Use a nullish check instead.

Proposed fix
-    if (!label && !Icon) return null;
+    if (label == null && !Icon) return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Badge.tsx` around lines 432 - 434, The renderLabel function
currently uses a truthy guard (!label) which hides valid values like 0; update
the conditional in renderLabel to use a nullish check (label == null) so that
falsy-but-valid nodes (e.g., 0 or empty string) are rendered, keeping the Icon
behavior unchanged when both are absent.
src/styles.css (1)

415-421: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid forcing left alignment for all .table-header columns.

Line [420] (text-align: left) overrides per-column alignment utilities due to higher specificity, so headers like src/components/LogoGrid.tsx Lines [121-124] won't stay centered. Move alignment control to component classes (or make this rule non-opinionated).

Proposed fix
 .table-header th {
   `@apply` font-semibold text-zinc-800;
 
   font-size: calc(1em + 1pt);
   padding: 2mm;
-  text-align: left;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles.css` around lines 415 - 421, The CSS rule ".table-header th" is
forcing left alignment via "text-align: left", which overrides per-column
alignment utilities (e.g., header cells in LogoGrid.tsx); remove or neutralize
that property so alignment is controlled by component-level classes—either
delete the "text-align: left" declaration or replace it with a non-opinionated
value like "text-align: inherit" on the ".table-header th" selector so
per-column utilities can take effect.
cli/src/bundler/vite-builder.ts (1)

61-65: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Stop composing the loader command as raw shell text.

Line 142 builds a raw /bin/sh -c command from sandbox settings and tsx path tokens. This is vulnerable to shell metacharacter injection and also breaks valid paths containing spaces. Build argv arrays and execute without a shell.

As per coding guidelines, injection risks should be fixed at the root cause (unsafe command construction).

🛠️ Safer argv-based direction
-async function runLoaderOnce(args: LoaderArgs): Promise<{ stderr?: Buffer | string; }> {
-  const prefix = srtPrefix(args.sandbox);
-  const tsx = resolveTsxBin(args.facetRoot).join(' ');
-  return await $`${{ raw: prefix }}${{ raw: tsx }} ${args.loaderPath} --facet-root=${args.facetRoot} --data-file=${args.dataFilePath} --output-file=${args.resultFilePath}`.quiet();
-}
+async function runLoaderOnce(args: LoaderArgs): Promise<{ stderr?: Buffer | string; }> {
+  const tsxArgv = resolveTsxBin(args.facetRoot);
+  const baseArgv = [
+    ...tsxArgv,
+    args.loaderPath,
+    `--facet-root=${args.facetRoot}`,
+    `--data-file=${args.dataFilePath}`,
+    `--output-file=${args.resultFilePath}`,
+  ];
+  const argv = args.sandbox
+    ? ['srt', '--settings', typeof args.sandbox === 'string' ? args.sandbox : '/etc/facet/srt-settings.json', ...baseArgv]
+    : baseArgv;
+  // Execute argv without shell expansion (spawn/execFile wrapper).
+  return await runCommandArgv(argv).quiet();
+}

Also applies to: 140-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/bundler/vite-builder.ts` around lines 61 - 65, The code currently
composes a shell command string (using srtPrefix and then joining with tsx path
tokens) which is vulnerable to shell injection and breaks on paths with spaces;
instead construct an argv array and run the binary without a shell: change
srtPrefix to return the settings token (or undefined) rather than a shell
fragment and in the caller (the code that builds and runs the loader command
around the tsx path tokens) assemble an argv like ['srt', '--settings',
settings, ...tsxArgs] or ['tsx', tsxPath, ...] as appropriate, then invoke the
process using a shell-less API (child_process.spawn or execFile / execa with
shell: false) so each argument is passed as a distinct argv element and not
interpolated into /bin/sh -c. Ensure you preserve handling of sandbox being
boolean or string and include settings only when provided.
cli/src/generators/html.ts (1)

65-72: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

clearCache behavior no longer matches its documented intent.

Line 65-72 now clears only .facet/, so the “node_modules cache” part of the --clear-cache description is stale unless that cleanup happens elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/generators/html.ts` around lines 65 - 72, The clearCache branch
currently only removes the .facet directory, which no longer matches the flag’s
documented intent to also clear the node_modules cache; update the
options.clearCache handling (the block that computes root using consumerRoot ||
resolve(dirname(options.template))) to also detect and remove the node_modules
cache under the same root (e.g., compute nodeModulesDir = join(root,
'node_modules') and if existsSync(nodeModulesDir) then rmSync(nodeModulesDir, {
recursive: true, force: true }) and log the removal), or if you prefer to change
behavior instead, update the CLI docs/help to remove the “node_modules cache”
claim — adjust the code around facetDir and the logger calls accordingly.
Taskfile.yml (1)

69-74: ⚠️ Potential issue | 🟠 Major

Fix test:unit to use Vitest instead of Bun

Taskfile.yml’s test:unit task still runs bun test (and the description says “with bun”), but cli/package.json defines the unit test command as vitest run ("test": "vitest run"). Replace the cmds: - bun test entry with the appropriate Vitest invocation (e.g., pnpm test from dir: cli) and update the desc accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Taskfile.yml` around lines 69 - 74, The Taskfile.yml task currently describes
and runs Bun but the CLI package uses Vitest; update the task's desc from "Run
cli unit tests with bun" to "Run cli unit tests with Vitest" and replace the
cmds entry that currently runs "bun test" with the proper Vitest
invocation—e.g., run "pnpm test" (keeping the existing dir: cli and deps:
gen-version) so the task executes the CLI package.json "test": "vitest run"
command.
cli/vite-ssr-loader.ts (1)

109-116: ⚠️ Potential issue | 🟠 Major

Use path.delimiter for NODE_PATH in cli/vite-ssr-loader.ts.
Line 112 hardcodes : when building process.env.NODE_PATH; on Windows NODE_PATH entries are separated with ; (and : can corrupt drive-letter paths), so module resolution can fail. Replace the concatenation to use path.delimiter instead (e.g., ${facetNodeModules}${delimiter}${originalNodePath}).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/vite-ssr-loader.ts` around lines 109 - 116, The NODE_PATH concatenation
in vite-ssr-loader uses a hardcoded ':' which breaks on Windows; update the
logic that builds process.env.NODE_PATH (using facetNodeModules and
originalNodePath) to join entries with path.delimiter instead of ':' and ensure
path is imported/available, then call require('module').Module._initPaths() as
before to reload resolution; adjust the ternary that sets process.env.NODE_PATH
to use `${facetNodeModules}${path.delimiter}${originalNodePath}` when
originalNodePath is non-empty.
🧹 Nitpick comments (2)
.github/workflows/test.yml (1)

36-40: ⚡ Quick win

Verify whether Bun is still needed in CI after the Node migration.

Line 39 still provisions Bun. If no remaining unit-test/build path calls Bun, remove it to reduce CI setup time and prevent hidden runtime coupling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 36 - 40, The workflow still
provisions Bun in the "Setup tools" step (uses: flanksource/deps) by listing
bun@v1.3.10 in the tools input; check all CI/workflow steps and repo scripts
(package.json scripts, task files, any workflow jobs) for references to bun or
bun-specific commands, and if none remain after the Node migration remove
bun@v1.3.10 from the tools list (leaving task@v3.39.2) in the Setup tools step
to shorten setup time and eliminate hidden coupling, then run the CI locally or
trigger the workflow to verify no job fails due to the removal.
cli/test/shell.test.ts (1)

15-19: ⚡ Quick win

Add regression coverage for array interpolation and signal termination

The suite validates scalar interpolation, but it misses the two high-risk behaviors used in production: array interpolation and signal-terminated process handling. Adding those tests will lock in the expected contract of $.

Also applies to: 41-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/shell.test.ts` around lines 15 - 19, Add two regression tests
alongside the existing "shell-escapes interpolated values" test in
cli/test/shell.test.ts: one that verifies array interpolation is escaped
correctly by invoking the same $ template (e.g., using an array like
['a','b;echo pwned'] interpolated into $`printf '%s\n' ${arr}`) and asserting
combined stdout matches expected values, and another that verifies
signal-terminated child handling by spawning a process via the $ template,
sending it a signal (SIGTERM/SIGINT) and asserting the returned result indicates
termination by signal (or the promise rejects with the expected signal info);
reuse the existing test harness/quiet() invocation and assertion style so
behavior is locked in for both scalar/array interpolation and signal termination
cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 66-69: The checkout step using actions/checkout@v4 currently sets
only ref: ${{ needs.version-bump.outputs.new_tag }}; update that checkout
configuration (the step with name "Checkout code" and uses: actions/checkout@v4)
to include persist-credentials: false so the publish job does not leave Git
credentials in local git config.
- Around line 27-30: The workflow uses mutable action tags (e.g. "uses:
pnpm/action-setup@v4", "uses: actions/checkout@v4", "uses:
actions/setup-node@v4"); replace each "`@v4`" tag with the corresponding pinned
commit SHA from the action's GitHub repository (use the commit SHA for the
release you want) so the workflow references an exact immutable version, and
update all occurrences of those three actions in the file.

In @.github/workflows/test.yml:
- Around line 22-29: The workflow uses mutable tags pnpm/action-setup@v4 and
actions/setup-node@v4 which should be pinned to immutable commit SHAs; update
the uses lines to reference the exact commit SHAs for pnpm/action-setup and
actions/setup-node (e.g., pnpm/action-setup@<commit-sha> and
actions/setup-node@<commit-sha>) by locating the repositories' latest stable
commit for the desired v4 behavior, replace the `@v4` tags with those SHAs, and
verify the workflow still sets the same version inputs (version: 8.13.1 for pnpm
and the Node.js version input) before committing.

In `@cli/README.md`:
- Around line 23-24: Update the global install commands in cli/README.md to use
the actual published package name; replace occurrences of "`@flanksource/facet`"
with "`@flanksource/facet-cli`" in the lines showing pnpm/npm install commands so
they match cli/package.json's "name": "`@flanksource/facet-cli`" and the release
workflow/notes that install the CLI package.

In `@cli/src/builders/facet-directory.ts`:
- Around line 833-835: The generated .npmrc is written with default permissions;
update the write operations (the writeFileSync calls that use npmrcPath and
npmrcContent) to write with restrictive permissions (mode 0o600) and also ensure
existing files that are overwritten have their mode set to 0o600 (e.g., via
fs.chmodSync) so inherited auth tokens are not world-readable; apply the same
change to the other writeFileSync block referenced around the 840-842 region and
reference the npmrcPath/writeFileSync calls when making the edit.
- Around line 508-509: The current code builds dataScript using
JSON.stringify(data) and writes it via writeFileSync to join(this.facetRoot,
'facet-data.js'), which allows script-breakout sequences like </script>;
instead, escape the serialized JSON before embedding: call JSON.stringify(data)
into a variable, then replace unsafe sequences (at minimum </script>, <!--, -->
and the line separator characters U+2028 and U+2029) with safe escaped
equivalents (e.g., replace '</script>' with '<\\/script>' and replace
U+2028/U+2029 with '\\u2028'/'\\u2029'), then build dataScript from the escaped
string and write it using the same writeFileSync to avoid raw injection in
facet-data.js.
- Around line 955-958: The synchronous pnpm invocation using spawnSync in
facet-directory.ts (the call that assigns to result) lacks a timeout and can
hang; update the spawnSync invocation to pass a timeout option (e.g., timeout:
<ms>) and handle the case where result.error or result.status is null/undefined
or where result.signal indicates a timeout by logging/throwing a clear error and
exiting the command. Ensure you reference the spawnSync call that produces the
const result and add logic after that call to detect result.error or a timeout
and fail fast with a helpful message.

In `@cli/src/commands/doctor.ts`:
- Around line 521-532: The checkTsx function currently uses
probeOptionalBin('tsx', ...) which only checks PATH and yields false warnings;
update checkTsx to probe the same resolver the runtime uses instead (e.g., use
the project's runtime resolver or bundled tsx lookup used elsewhere—replace
probeOptionalBin call with the resolver function used by the runtime, such as
resolve/bundled tsx path or getBundledTsxPath), and if that resolver returns a
path, report status 'pass' with its version output; if not found, downgrade the
result to non-failing (change status to 'info' or remove the warn) and keep the
hint only if truly required. Ensure you modify references inside checkTsx and
stop relying solely on probeOptionalBin so the doctor reflects the migrated
bundled/resolved tsx behavior.

In `@cli/src/generators/html.ts`:
- Around line 88-103: The live mode branch (when options.live is true) currently
takes the server from startViteServer, snapshots HTML via snapshotHTML and
writes it out without applying the CSS scoping flow, so --css-scope is ignored;
either apply the same CSS scoping routine used in the SSR path to the captured
html before writing (invoke the existing scoping function used elsewhere) or
explicitly guard and error when options.cssScope is set in conjunction with
options.live (e.g., throw a user-facing error and avoid writing output). Locate
the live branch around options.live, startViteServer, snapshotHTML, outputPath
and writeFile and implement one of those two fixes so scoping behavior matches
the non-live path.

In `@cli/src/server/preview.ts`:
- Around line 156-158: The stream-forwarding loop in writeWebResponse currently
writes each chunk from response.body directly into the Node ServerResponse
without honoring backpressure; modify the loop that iterates over response.body
(the for await (const chunk of response.body ...) block) to check the boolean
result of res.write(chunk) and, if it returns false, await a Promise that
resolves on res.once('drain') before continuing, and ensure res.end() is still
called after the loop completes and any pending backpressure awaits are
resolved; this prevents unbounded buffering when clients are slow.

In `@cli/src/utils/shell.ts`:
- Around line 34-47: The buildCommand currently stringifies interpolated arrays
into one comma-separated token which breaks callers that expect multiple shell
arguments; update buildCommand (and reuse escape/isRaw) so when a value is an
array (Array.isArray(value)) you map each element to either element.raw if
isRaw(element) is true or escape(element) otherwise, then join those mapped
pieces with spaces and append into cmd (instead of treating the whole array as a
single value); leave non-array handling unchanged so single values still use
isRaw(value) ? value.raw : escape(value).
- Around line 90-104: The close handler in cli/src/utils/shell.ts currently
coerces a null code to 0 and treats signal-terminated children as success;
update the child.on('close', ...) callback to accept both (code, signal), treat
any non-null signal as a failure (rejecting with a ShellError that includes
stdout, stderr, signal and a null/undefined exitCode), and only treat numeric
code === 0 as success; modify the logic around the existing Error/ShellError
construction (referencing this.command and ShellError) to include signal details
when rejecting.

In `@examples/kitchen-sink/HeaderSolid.tsx`:
- Around line 54-55: Update the example page title strings that incorrectly
state "10mm" to reflect the configured default header height of "14mm"; find the
JSX elements in HeaderSolid.tsx where the title prop is set to "Page 2 — 10mm
compact chrome" (and the similar title strings on the other occurrences) and
change those strings to "Page 2 — 14mm compact chrome" (and the corresponding
changes for the other two title props so all titles match the 14mm default
header).

In `@README.md`:
- Around line 21-22: Update the global install examples to use the new CLI
package name by replacing occurrences of the string "`@flanksource/facet`" with
"`@flanksource/facet-cli`" in the README examples (the two global install lines
using "pnpm add -g" and "npm install -g"); ensure both commands show the new
package name so users install the CLI package rather than the old library
package.

In `@src/components/diagram/Arrow.tsx`:
- Around line 15-18: The bidirectional arrow preset fails to render tails
because showTail is not included in VariantProps and showTail: true is not set
on the preset; update the VariantProps type to include 'showTail' alongside the
other picked props (so TypeScript will accept it) and add showTail: true to the
bidirectional preset object (alongside its tailShape: 'circle') in Arrow.tsx to
enable tail endpoint rendering.

In `@src/components/diagram/content.tsx`:
- Around line 27-29: The JSX in items.map uses key={item} which can collide for
duplicate labels; update the mapping in the component that renders NodePill so
each element uses a stable unique key (for example combine the item value with
its index or an explicit id) by changing the key expression in the items.map
callback (the NodePill render) to something like `${item}-${index}` or use an
item.id property if available to ensure unique React keys and stable
reconciliation.

In `@src/components/ListTable.tsx`:
- Around line 127-138: splitColorClasses currently drops any non-color tokens so
tagMapping consumers lose layout/typography/shape classes; update
splitColorClasses (and how its result is used in the badge rendering around the
tagMapping code at lines that build badges) to preserve passthrough classes by
collecting all tokens that are not text-*/bg-*/border-* into a passthrough field
(e.g. passthrough?: string[] or passthrough?: string) and return it alongside
textColor/color/borderColor, then update the badge rendering logic (the
tagMapping usage) to append those passthrough classes back into the badge
className so existing layout/shape/typography classes are not dropped.

In `@src/styles.css`:
- Line 4: Replace the `@import` url(...) statement in styles.css with the string
import notation to satisfy stylelint; locate the `@import` statement (the Google
Fonts import at the top of styles.css) and change its syntax from
url("https://fonts.googleapis.com/...") to use a quoted string import (i.e.,
`@import` "https://fonts.googleapis.com/..." ) so the linter no longer flags an
import-notation violation.

---

Outside diff comments:
In `@cli/src/bundler/vite-builder.ts`:
- Around line 61-65: The code currently composes a shell command string (using
srtPrefix and then joining with tsx path tokens) which is vulnerable to shell
injection and breaks on paths with spaces; instead construct an argv array and
run the binary without a shell: change srtPrefix to return the settings token
(or undefined) rather than a shell fragment and in the caller (the code that
builds and runs the loader command around the tsx path tokens) assemble an argv
like ['srt', '--settings', settings, ...tsxArgs] or ['tsx', tsxPath, ...] as
appropriate, then invoke the process using a shell-less API (child_process.spawn
or execFile / execa with shell: false) so each argument is passed as a distinct
argv element and not interpolated into /bin/sh -c. Ensure you preserve handling
of sandbox being boolean or string and include settings only when provided.

In `@cli/src/generators/html.ts`:
- Around line 65-72: The clearCache branch currently only removes the .facet
directory, which no longer matches the flag’s documented intent to also clear
the node_modules cache; update the options.clearCache handling (the block that
computes root using consumerRoot || resolve(dirname(options.template))) to also
detect and remove the node_modules cache under the same root (e.g., compute
nodeModulesDir = join(root, 'node_modules') and if existsSync(nodeModulesDir)
then rmSync(nodeModulesDir, { recursive: true, force: true }) and log the
removal), or if you prefer to change behavior instead, update the CLI docs/help
to remove the “node_modules cache” claim — adjust the code around facetDir and
the logger calls accordingly.

In `@cli/vite-ssr-loader.ts`:
- Around line 109-116: The NODE_PATH concatenation in vite-ssr-loader uses a
hardcoded ':' which breaks on Windows; update the logic that builds
process.env.NODE_PATH (using facetNodeModules and originalNodePath) to join
entries with path.delimiter instead of ':' and ensure path is
imported/available, then call require('module').Module._initPaths() as before to
reload resolution; adjust the ternary that sets process.env.NODE_PATH to use
`${facetNodeModules}${path.delimiter}${originalNodePath}` when originalNodePath
is non-empty.

In `@src/components/Badge.tsx`:
- Around line 432-434: The renderLabel function currently uses a truthy guard
(!label) which hides valid values like 0; update the conditional in renderLabel
to use a nullish check (label == null) so that falsy-but-valid nodes (e.g., 0 or
empty string) are rendered, keeping the Icon behavior unchanged when both are
absent.

In `@src/styles.css`:
- Around line 415-421: The CSS rule ".table-header th" is forcing left alignment
via "text-align: left", which overrides per-column alignment utilities (e.g.,
header cells in LogoGrid.tsx); remove or neutralize that property so alignment
is controlled by component-level classes—either delete the "text-align: left"
declaration or replace it with a non-opinionated value like "text-align:
inherit" on the ".table-header th" selector so per-column utilities can take
effect.

In `@Taskfile.yml`:
- Around line 69-74: The Taskfile.yml task currently describes and runs Bun but
the CLI package uses Vitest; update the task's desc from "Run cli unit tests
with bun" to "Run cli unit tests with Vitest" and replace the cmds entry that
currently runs "bun test" with the proper Vitest invocation—e.g., run "pnpm
test" (keeping the existing dir: cli and deps: gen-version) so the task executes
the CLI package.json "test": "vitest run" command.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 36-40: The workflow still provisions Bun in the "Setup tools" step
(uses: flanksource/deps) by listing bun@v1.3.10 in the tools input; check all
CI/workflow steps and repo scripts (package.json scripts, task files, any
workflow jobs) for references to bun or bun-specific commands, and if none
remain after the Node migration remove bun@v1.3.10 from the tools list (leaving
task@v3.39.2) in the Setup tools step to shorten setup time and eliminate hidden
coupling, then run the CI locally or trigger the workflow to verify no job fails
due to the removal.

In `@cli/test/shell.test.ts`:
- Around line 15-19: Add two regression tests alongside the existing
"shell-escapes interpolated values" test in cli/test/shell.test.ts: one that
verifies array interpolation is escaped correctly by invoking the same $
template (e.g., using an array like ['a','b;echo pwned'] interpolated into
$`printf '%s\n' ${arr}`) and asserting combined stdout matches expected values,
and another that verifies signal-terminated child handling by spawning a process
via the $ template, sending it a signal (SIGTERM/SIGINT) and asserting the
returned result indicates termination by signal (or the promise rejects with the
expected signal info); reuse the existing test harness/quiet() invocation and
assertion style so behavior is locked in for both scalar/array interpolation and
signal termination cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9906502f-d338-4183-8467-04f3bd9550cf

📥 Commits

Reviewing files that changed from the base of the PR and between 798be81 and 308d15d.

⛔ Files ignored due to path filters (4)
  • cli/package-lock.json is excluded by !**/package-lock.json
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (96)
  • .gavel.yaml
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • .gitignore
  • .npmrc
  • Dockerfile
  • README.md
  • Taskfile.yml
  • chart/test/.ginkgo/ginkgo-report-.-.-1777281827467563000.json
  • cli/.gitignore
  • cli/.npmrc
  • cli/ARCHITECTURE.md
  • cli/README.md
  • cli/STATUS.md
  • cli/package.json
  • cli/scripts/copy-assets.cjs
  • cli/scripts/gen-version.cjs
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/live-snapshot.ts
  • cli/src/bundler/vite-builder.test.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/bundler/vite-server.ts
  • cli/src/cli.ts
  • cli/src/commands/doctor.test.ts
  • cli/src/commands/doctor.ts
  • cli/src/generators/html.ts
  • cli/src/server/archive.ts
  • cli/src/server/facet-cache.ts
  • cli/src/server/facet-types.ts
  • cli/src/server/preview.ts
  • cli/src/server/routes.ts
  • cli/src/types.ts
  • cli/src/utils/assets.ts
  • cli/src/utils/data-loader.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/src/utils/remote-resolver.ts
  • cli/src/utils/shell.ts
  • cli/src/utils/tailwind.test.ts
  • cli/src/utils/tailwind.ts
  • cli/test/jsx-parser.test.ts
  • cli/test/lint-rules.test.ts
  • cli/test/pdf-bleed.test.ts
  • cli/test/pdf-security.test.ts
  • cli/test/render-api.test.ts
  • cli/test/resolve-output.test.ts
  • cli/test/shell.test.ts
  • cli/tsconfig.json
  • cli/tsconfig.test.json
  • cli/tsdown.config.ts
  • cli/vite-dev-loader.ts
  • cli/vite-ssr-loader.ts
  • cli/vitest.config.ts
  • examples/kitchen-sink/BadgeExamples.tsx
  • examples/kitchen-sink/BleedTest.tsx
  • examples/kitchen-sink/DataFlowDiagram.tsx
  • examples/kitchen-sink/FindingsReport.tsx
  • examples/kitchen-sink/FlanksourceHeader.tsx
  • examples/kitchen-sink/HeaderSolid.tsx
  • examples/kitchen-sink/PageSizeTest.tsx
  • examples/kitchen-sink/package.json
  • examples/quickstart/README.md
  • package.json
  • src/components/Avatar.tsx
  • src/components/AvatarGroup.tsx
  • src/components/Badge.tsx
  • src/components/BulletList.tsx
  • src/components/CalloutBox.tsx
  • src/components/CompactTable.tsx
  • src/components/Document.tsx
  • src/components/Finding.tsx
  • src/components/Header.tsx
  • src/components/ListTable.tsx
  • src/components/LogoGrid.tsx
  • src/components/MatrixTable.tsx
  • src/components/ProgressBar.tsx
  • src/components/ScoreGauge.tsx
  • src/components/SeverityStatCard.tsx
  • src/components/Shield/Shield.tsx
  • src/components/StatCard.tsx
  • src/components/diagram/Arrow.test.tsx
  • src/components/diagram/Arrow.tsx
  • src/components/diagram/BoxNode.test.tsx
  • src/components/diagram/BoxNode.tsx
  • src/components/diagram/Diagram.tsx
  • src/components/diagram/colors.ts
  • src/components/diagram/content.tsx
  • src/components/diagram/index.ts
  • src/components/index.tsx
  • src/components/utils/resolveSizeVariant.ts
  • src/stories/components/Badge.stories.tsx
  • src/styles.css
  • tailwind.config.js
💤 Files with no reviewable changes (6)
  • cli/test/resolve-output.test.ts
  • cli/ARCHITECTURE.md
  • cli/STATUS.md
  • cli/test/jsx-parser.test.ts
  • cli/test/lint-rules.test.ts
  • cli/src/server/facet-cache.ts

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/test.yml
Comment thread cli/README.md Outdated
Comment thread cli/src/builders/facet-directory.ts Outdated
Comment thread README.md
Comment on lines +21 to +22
pnpm add -g @flanksource/facet
# or: npm install -g @flanksource/facet

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 | ⚡ Quick win

Use the CLI package name in global install docs.

Line 21 and Line 22 still install @flanksource/facet, but this migration publishes the CLI as @flanksource/facet-cli. Users following this command can install the wrong package.

Proposed fix
-pnpm add -g `@flanksource/facet`
-# or: npm install -g `@flanksource/facet`
+pnpm add -g `@flanksource/facet-cli`
+# or: npm install -g `@flanksource/facet-cli`
📝 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
pnpm add -g @flanksource/facet
# or: npm install -g @flanksource/facet
pnpm add -g `@flanksource/facet-cli`
# or: npm install -g `@flanksource/facet-cli`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 21 - 22, Update the global install examples to use
the new CLI package name by replacing occurrences of the string
"`@flanksource/facet`" with "`@flanksource/facet-cli`" in the README examples (the
two global install lines using "pnpm add -g" and "npm install -g"); ensure both
commands show the new package name so users install the CLI package rather than
the old library package.

Comment thread src/components/diagram/Arrow.tsx
Comment thread src/components/diagram/content.tsx Outdated
Comment thread src/components/ListTable.tsx Outdated
Comment thread src/styles.css 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.

♻️ Duplicate comments (2)
.github/workflows/release.yml (2)

111-115: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable persisted git credentials in build-binaries checkout.

This job only needs read access to build/upload artifacts; keep credentials out of local git config by setting persist-credentials: false.

Suggested hardening
       - name: Checkout code
         uses: actions/checkout@v4
         with:
           ref: ${{ needs.version-bump.outputs.new_tag }}
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 111 - 115, The checkout step
using actions/checkout@v4 in the build-binaries job is persisting git
credentials; update that step (the Checkout code step) to include
persist-credentials: false so credentials are not stored in the local git
config. Locate the step that uses actions/checkout@v4 and add the with key
persist-credentials: false alongside the existing ref: ${{
needs.version-bump.outputs.new_tag }}.

Source: Linters/SAST tools


112-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin all changed uses: actions to immutable commit SHAs.

These steps still use mutable tags (@v4, @v1.0.24), which weakens workflow supply-chain integrity. Pin each action reference to a full commit SHA.

#!/bin/bash
set -euo pipefail
# Expectation after fix: no output lines from this workflow.
rg -n '^\s*uses:\s*[^#\s]+@v?[0-9]+(\.[0-9]+)?(\.[0-9]+)?\s*$' .github/workflows/release.yml

Also applies to: 117-117, 122-122, 127-127, 149-149, 161-161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 112, Several workflow steps still
reference mutable action tags (e.g. uses: actions/checkout@v4 and other uses:
lines flagged), so replace each mutable tag with the corresponding action's
immutable full commit SHA (40-char) — for example swap uses: actions/checkout@v4
to uses: actions/checkout@<full-commit-sha> and do the same for
actions/setup-node, aws-actions/configure-aws-credentials,
aws-actions/amazon-ecr-login, actions/upload-artifact (and any other uses:
entries flagged). Retrieve the correct SHA from the action's GitHub repo (select
the commit that matches the tag you want) and update the uses: values to the
full SHA, then run the provided regex check to ensure there are no remaining
numeric tags.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In @.github/workflows/release.yml:
- Around line 111-115: The checkout step using actions/checkout@v4 in the
build-binaries job is persisting git credentials; update that step (the Checkout
code step) to include persist-credentials: false so credentials are not stored
in the local git config. Locate the step that uses actions/checkout@v4 and add
the with key persist-credentials: false alongside the existing ref: ${{
needs.version-bump.outputs.new_tag }}.
- Line 112: Several workflow steps still reference mutable action tags (e.g.
uses: actions/checkout@v4 and other uses: lines flagged), so replace each
mutable tag with the corresponding action's immutable full commit SHA (40-char)
— for example swap uses: actions/checkout@v4 to uses:
actions/checkout@<full-commit-sha> and do the same for actions/setup-node,
aws-actions/configure-aws-credentials, aws-actions/amazon-ecr-login,
actions/upload-artifact (and any other uses: entries flagged). Retrieve the
correct SHA from the action's GitHub repo (select the commit that matches the
tag you want) and update the uses: values to the full SHA, then run the provided
regex check to ensure there are no remaining numeric tags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 016c5db9-2603-41c9-ad57-597dcd938007

📥 Commits

Reviewing files that changed from the base of the PR and between 308d15d and 400cfa8.

⛔ Files ignored due to path filters (1)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • .github/workflows/release.yml
  • cli/package.json
  • cli/src/bundler/live-snapshot.ts
  • cli/src/cli.bun.ts
  • cli/src/server/worker-pool.ts
  • cli/src/utils/assets.bun.ts
  • cli/src/utils/assets.ts
  • cli/src/utils/debug-annotations.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/tsconfig.json
  • cli/tsdown.config.ts
✅ Files skipped from review due to trivial changes (3)
  • cli/src/cli.bun.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/utils/debug-annotations.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • cli/src/utils/pdf-generator.ts
  • cli/tsdown.config.ts
  • cli/tsconfig.json
  • cli/src/bundler/live-snapshot.ts

@moshloop moshloop force-pushed the refactor/cli-bun-to-node branch 2 times, most recently from bef4f06 to c6353f2 Compare June 7, 2026 07:37
…peteer-core

Rebased onto main (0.1.41), dropping commits main already absorbed
(pnpm migration, component work) and re-applying the bun->node migration
on top of main's newer CLI code.

CLI runtime migration:
- Node $ shell helper (utils/shell.ts) replaces Bun shell across the CLI,
  including main's newer files (package-manager.ts, vite-builder.ts,
  doctor.ts, routes.ts, generators/html.ts, remote-resolver.ts, etc.)
- node:http server with Web Request/Response adapters replaces Bun.serve
- node:child_process (spawn/execFileSync) replaces Bun.spawn*; tsx runs
  the vite loaders via resolveTsxBin (no bun runtime)
- assets resolved via import.meta.url (utils/assets.ts) instead of Bun's
  `with { type: 'file' }`; all tests run under vitest, not bun:test

Distribution:
- published as @flanksource/facet-cli (Node ESM bundle, dist/cli.mjs bin)
- standalone bun binary restored via build:bin -> src/cli.bun.ts, which
  embeds assets (cli.bun.ts/assets.bun.ts use bun's file imports) so the
  compiled binary resolves them from /$bunfs; release.yml builds 3 targets
  and publishes both @flanksource/facet and @flanksource/facet-cli

puppeteer -> puppeteer-core:
- no Chromium auto-download; always use system Chrome via resolveChromePath()

Render cache:
- configurable cacheDir (--cache-dir / FACET_CACHE_DIR); render-api test
  uses an isolated temp cache so the /render/stream test renders cold

Verified: node bundle and bun binary both render SimpleReport.tsx to a
valid PDF-1.7; vitest 203 passed; tsc 11 errors (all pre-existing on main).
@yashmehrotra yashmehrotra force-pushed the refactor/cli-bun-to-node branch from c6353f2 to a8b51d5 Compare June 16, 2026 04:13
@socket-security

socket-security Bot commented Jun 16, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
cli/src/bundler/vite-builder.ts (1)

61-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid raw shell concatenation for loader execution.

runLoaderOnce() injects both srtPrefix() and resolveTsxBin(...).join(' ') as raw shell text. This breaks valid paths with spaces and can alter command parsing when paths contain shell metacharacters. Build an argv token list and quote each token (or execute via spawn/execFile) instead of joining raw strings.

Proposed fix
+function shellQuote(arg: string): string {
+  return `'${arg.replace(/'/g, `'\\''`)}'`;
+}
+
-function srtPrefix(sandbox?: string | boolean): string {
-  if (!sandbox) return '';
-  const settings = typeof sandbox === 'string' ? sandbox : '/etc/facet/srt-settings.json';
-  return `srt --settings ${settings} `;
+function srtPrefix(sandbox?: string | boolean): string[] {
+  if (!sandbox) return [];
+  const settings = typeof sandbox === 'string' ? sandbox : '/etc/facet/srt-settings.json';
+  return ['srt', '--settings', settings];
 }
@@
 async function runLoaderOnce(args: LoaderArgs): Promise<{ stderr?: Buffer | string; }> {
-  const prefix = srtPrefix(args.sandbox);
-  const tsx = resolveTsxBin(args.facetRoot).join(' ');
-  return await $`${{ raw: prefix }}${{ raw: tsx }} ${args.loaderPath} --facet-root=${args.facetRoot} --data-file=${args.dataFilePath} --output-file=${args.resultFilePath}`.quiet();
+  const argv = [
+    ...srtPrefix(args.sandbox),
+    ...resolveTsxBin(args.facetRoot),
+    args.loaderPath,
+    `--facet-root=${args.facetRoot}`,
+    `--data-file=${args.dataFilePath}`,
+    `--output-file=${args.resultFilePath}`,
+  ];
+  const cmd = argv.map(shellQuote).join(' ');
+  return await $`${{ raw: cmd }}`.quiet();
 }

Also applies to: 139-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/bundler/vite-builder.ts` around lines 61 - 65, The srtPrefix function
and the resolveTsxBin call are both producing raw strings that get concatenated
for shell execution in runLoaderOnce(), which fails when paths contain spaces or
shell metacharacters. Instead of concatenating raw strings, refactor srtPrefix
to return an array of command tokens (e.g., ['srt', '--settings', settings]) and
similarly refactor how resolveTsxBin is handled to build an argv array. Then
pass this argv array to spawn or execFile instead of using shell string
concatenation, or if using shell execution is necessary, properly quote each
token in the final command string.
🧹 Nitpick comments (3)
cli/src/commands/doctor.ts (1)

484-491: 💤 Low value

Array interpolation in shell template may produce malformed commands.

The template `${name} ${args}` converts the args array via .toString(), which joins elements with commas. For multi-element arrays (e.g., ['--version', '--quiet']), this produces git --version,--quiet rather than git --version --quiet. Current call sites use single-element arrays so this works, but it's fragile.

🛠️ Safer alternative using join
 async function probeOptionalBin(name: string, args: string[]): Promise<{ ok: true; output: string } | { ok: false }> {
   try {
-    const out = await $`${name} ${args}`.quiet();
+    const out = await $`${name} ${args.join(' ')}`.quiet();
     return { ok: true, output: out.stdout.toString().trim() };
   } catch {
     return { ok: false };
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/commands/doctor.ts` around lines 484 - 491, The probeOptionalBin
function interpolates the args array directly into the shell template, which
calls .toString() on the array and joins elements with commas rather than
spaces, potentially producing malformed commands like "git --version,--quiet"
instead of "git --version --quiet". Fix this by joining the args array with
spaces before interpolating it into the template literal so that multi-element
arrays are properly formatted as separate command arguments.
.github/workflows/release.yml (1)

35-35: ⚡ Quick win

Node version inconsistency across jobs.

version-bump and build-binaries use Node 20 while publish-npm uses Node 22. This inconsistency could cause subtle issues with lockfile formats or behavior differences. Consider aligning all jobs to the same Node version.

♻️ Align Node versions
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
-          node-version: "22"
+          node-version: "20"
           registry-url: "https://registry.npmjs.org"

Or update all jobs to Node 22 if that's the intended target.

Also applies to: 85-85, 178-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 35, The GitHub Actions workflow file
has inconsistent Node versions across multiple jobs: the version-bump job uses
Node 20 at line 35, the build-binaries job uses Node 20 at line 85, and the
publish-npm job uses Node 22 at line 178. Update all three node-version
specifications to use the same version (either all to Node 20 or all to Node 22)
to ensure consistent behavior and avoid potential lockfile format or behavior
differences across the build pipeline.
cli/src/utils/pdf-generator.ts (1)

15-15: Add explicit browser-path preflight check to fail fast with clear diagnostics when using puppeteer-core

When puppeteer-core is unable to resolve a browser path, it will fail at runtime with a generic error rather than a clear preflight diagnostic. Additionally, the current fallback message "Puppeteer bundled Chromium" is misleading since puppeteer-core includes no bundled binary and requires an explicit executablePath to launch.

Implement the validation in both launchBrowser() (line 473) and inline at line 421 to catch missing Chrome/Chromium paths immediately with the suggested error message. This improves diagnostics and guides users toward resolution.

Proposed adjustment
   const chromePath = resolveChromePath();
-  log.debug(`Using browser: ${chromePath ?? 'Puppeteer bundled Chromium'}`);
+  if (!chromePath) {
+    throw new Error(
+      'No Chromium/Chrome binary found. Set PUPPETEER_EXECUTABLE_PATH or CHROME_PATH, or install one via `npx puppeteer browsers install chrome`.'
+    );
+  }
+  log.debug(`Using browser: ${chromePath}`);
 
   const browser = await puppeteer.launch({
     headless: true,
     executablePath: chromePath,
     args: ['--no-sandbox', '--disable-setuid-sandbox'],
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/pdf-generator.ts` at line 15, Add explicit browser-path
preflight validation to fail fast when using puppeteer-core without an available
browser executable. In the launchBrowser() function (around line 473) and inline
at line 421, implement checks to verify that a valid Chrome/Chromium executable
path is available before attempting to launch. Replace the misleading fallback
message "Puppeteer bundled Chromium" with a clear error that explains
puppeteer-core requires an explicit executablePath configuration. This ensures
missing browser paths are caught immediately with diagnostic information that
guides users toward resolution, rather than failing at runtime with a generic
error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 72-75: The checkout step in the build-binaries job should add the
`persist-credentials: false` parameter to the actions/checkout@v4 action
(currently at lines 72-75). This parameter disables Git credential persistence
since the build-binaries job only reads the tagged commit and uploads artifacts
without performing any Git push operations, following the least-privilege
security principle.

In `@cli/package.json`:
- Line 22: The build:bin script in cli/package.json is unused dead code that
contradicts the PR's migration away from Bun. Since there are no references to
this script anywhere in the codebase, remove the entire line defining build:bin
from the scripts section of cli/package.json.

In `@cli/src/bundler/vite-builder.ts`:
- Around line 118-121: The `/ERR_MODULE_NOT_FOUND/` regex pattern in
RUNTIME_REINSTALL_PATTERNS is too broad and incorrectly matches relative-path
template errors in addition to package-miss cases, causing unnecessary
reinstalls. Replace this catch-all regex with a more specific pattern that
matches the exact package-miss error wording only, excluding relative-path
misses. Additionally, update the corresponding test assertion for
ERR_MODULE_NOT_FOUND in the cli/src/bundler/vite-builder.test.ts file to
validate the narrowed behavior and ensure it no longer triggers reinstalls for
relative-path errors.

In `@cli/src/utils/package-manager.ts`:
- Line 11: The shell module being imported here uses an unconditional `/bin/sh`
path that does not exist on Windows, causing ENOENT errors when executing
commands like pnpm version checks. In the shell module being imported, replace
the hardcoded `/bin/sh` with cross-platform compatible shell detection that uses
`cmd.exe` on Windows systems and `/bin/sh` on Unix-like systems. This will
ensure the $ function works correctly across all platforms and fix the broken
pnpm resolution flows in vite-builder, template resolution, and doctor commands
that depend on resolvePackageManager().

In `@cli/test/pdf-security.test.ts`:
- Line 13: The assignment of process.env['FACET_PACKAGE_PATH'] at module scope
in cli/test/pdf-security.test.ts at line 13 and cli/test/render-api.test.ts at
line 21 causes environment pollution for subsequent tests in the same worker
process. For each file, save the previous value of
process.env['FACET_PACKAGE_PATH'] before the assignment (store it in a const
like savedPath), and add or update an afterAll block to restore the original
state by either deleting the environment variable if it was previously
undefined, or restoring the saved value if it existed.

---

Outside diff comments:
In `@cli/src/bundler/vite-builder.ts`:
- Around line 61-65: The srtPrefix function and the resolveTsxBin call are both
producing raw strings that get concatenated for shell execution in
runLoaderOnce(), which fails when paths contain spaces or shell metacharacters.
Instead of concatenating raw strings, refactor srtPrefix to return an array of
command tokens (e.g., ['srt', '--settings', settings]) and similarly refactor
how resolveTsxBin is handled to build an argv array. Then pass this argv array
to spawn or execFile instead of using shell string concatenation, or if using
shell execution is necessary, properly quote each token in the final command
string.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Line 35: The GitHub Actions workflow file has inconsistent Node versions
across multiple jobs: the version-bump job uses Node 20 at line 35, the
build-binaries job uses Node 20 at line 85, and the publish-npm job uses Node 22
at line 178. Update all three node-version specifications to use the same
version (either all to Node 20 or all to Node 22) to ensure consistent behavior
and avoid potential lockfile format or behavior differences across the build
pipeline.

In `@cli/src/commands/doctor.ts`:
- Around line 484-491: The probeOptionalBin function interpolates the args array
directly into the shell template, which calls .toString() on the array and joins
elements with commas rather than spaces, potentially producing malformed
commands like "git --version,--quiet" instead of "git --version --quiet". Fix
this by joining the args array with spaces before interpolating it into the
template literal so that multi-element arrays are properly formatted as separate
command arguments.

In `@cli/src/utils/pdf-generator.ts`:
- Line 15: Add explicit browser-path preflight validation to fail fast when
using puppeteer-core without an available browser executable. In the
launchBrowser() function (around line 473) and inline at line 421, implement
checks to verify that a valid Chrome/Chromium executable path is available
before attempting to launch. Replace the misleading fallback message "Puppeteer
bundled Chromium" with a clear error that explains puppeteer-core requires an
explicit executablePath configuration. This ensures missing browser paths are
caught immediately with diagnostic information that guides users toward
resolution, rather than failing at runtime with a generic error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8de4e19-1826-41e8-9f42-1563fe819fdc

📥 Commits

Reviewing files that changed from the base of the PR and between 400cfa8 and a8b51d5.

⛔ Files ignored due to path filters (2)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (68)
  • .github/workflows/release.yml
  • .gitignore
  • Dockerfile
  • cli/.gitignore
  • cli/README.md
  • cli/package.json
  • cli/scripts/copy-assets.cjs
  • cli/scripts/gen-version.cjs
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/live-snapshot.ts
  • cli/src/bundler/vite-builder.test.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/bundler/vite-server.ts
  • cli/src/cli.bun.ts
  • cli/src/cli.ts
  • cli/src/commands/doctor.test.ts
  • cli/src/commands/doctor.ts
  • cli/src/generators/html.ts
  • cli/src/server/archive.ts
  • cli/src/server/config.ts
  • cli/src/server/preview.ts
  • cli/src/server/routes.ts
  • cli/src/server/worker-pool.ts
  • cli/src/types.ts
  • cli/src/utils/assets.bun.ts
  • cli/src/utils/assets.ts
  • cli/src/utils/data-loader.ts
  • cli/src/utils/debug-annotations.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/src/utils/remote-resolver.ts
  • cli/src/utils/shell.ts
  • cli/src/utils/tailwind.test.ts
  • cli/src/utils/tailwind.ts
  • cli/test/jsx-parser.test.ts
  • cli/test/lint-rules.test.ts
  • cli/test/pdf-bleed.test.ts
  • cli/test/pdf-security.test.ts
  • cli/test/render-api.test.ts
  • cli/test/resolve-output.test.ts
  • cli/test/shell.test.ts
  • cli/tsconfig.json
  • cli/tsconfig.test.json
  • cli/tsdown.config.ts
  • cli/vite-dev-loader.ts
  • cli/vite-ssr-loader.ts
  • cli/vitest.config.ts
  • examples/kitchen-sink/BleedTest.tsx
  • examples/kitchen-sink/DataFlowDiagram.tsx
  • examples/kitchen-sink/PageSizeTest.tsx
  • package.json
  • src/components/CompactTable.tsx
  • src/components/LogoGrid.tsx
  • src/components/MatrixTable.tsx
  • src/components/StatCard.tsx
  • src/components/diagram/Arrow.test.tsx
  • src/components/diagram/Arrow.tsx
  • src/components/diagram/BoxNode.test.tsx
  • src/components/diagram/BoxNode.tsx
  • src/components/diagram/Diagram.tsx
  • src/components/diagram/colors.ts
  • src/components/diagram/content.tsx
  • src/components/diagram/index.ts
  • src/components/index.tsx
💤 Files with no reviewable changes (2)
  • cli/test/lint-rules.test.ts
  • cli/test/jsx-parser.test.ts
✅ Files skipped from review due to trivial changes (8)
  • src/components/CompactTable.tsx
  • cli/vitest.config.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/server/worker-pool.ts
  • cli/src/utils/debug-annotations.ts
  • src/components/LogoGrid.tsx
  • src/components/diagram/index.ts
  • cli/scripts/gen-version.cjs
🚧 Files skipped from review as they are similar to previous changes (38)
  • .gitignore
  • cli/src/server/archive.ts
  • src/components/diagram/colors.ts
  • cli/src/types.ts
  • cli/src/cli.bun.ts
  • cli/tsconfig.test.json
  • cli/.gitignore
  • src/components/diagram/content.tsx
  • cli/scripts/copy-assets.cjs
  • src/components/diagram/Arrow.test.tsx
  • cli/src/utils/assets.bun.ts
  • cli/src/utils/assets.ts
  • cli/test/pdf-bleed.test.ts
  • cli/src/utils/data-loader.ts
  • src/components/diagram/Diagram.tsx
  • src/components/index.tsx
  • cli/src/commands/doctor.test.ts
  • examples/kitchen-sink/PageSizeTest.tsx
  • src/components/MatrixTable.tsx
  • cli/tsdown.config.ts
  • cli/src/cli.ts
  • src/components/diagram/BoxNode.test.tsx
  • src/components/diagram/BoxNode.tsx
  • cli/src/utils/tailwind.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/shell.ts
  • src/components/diagram/Arrow.tsx
  • cli/src/utils/remote-resolver.test.ts
  • cli/README.md
  • cli/src/bundler/vite-server.ts
  • cli/test/shell.test.ts
  • cli/src/server/routes.ts
  • cli/src/utils/tailwind.test.ts
  • Dockerfile
  • cli/vite-ssr-loader.ts
  • cli/src/server/preview.ts
  • cli/src/bundler/live-snapshot.ts
  • cli/src/builders/facet-directory.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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
cli/src/bundler/vite-builder.ts (1)

61-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid raw shell concatenation for loader execution.

runLoaderOnce() injects both srtPrefix() and resolveTsxBin(...).join(' ') as raw shell text. This breaks valid paths with spaces and can alter command parsing when paths contain shell metacharacters. Build an argv token list and quote each token (or execute via spawn/execFile) instead of joining raw strings.

Proposed fix
+function shellQuote(arg: string): string {
+  return `'${arg.replace(/'/g, `'\\''`)}'`;
+}
+
-function srtPrefix(sandbox?: string | boolean): string {
-  if (!sandbox) return '';
-  const settings = typeof sandbox === 'string' ? sandbox : '/etc/facet/srt-settings.json';
-  return `srt --settings ${settings} `;
+function srtPrefix(sandbox?: string | boolean): string[] {
+  if (!sandbox) return [];
+  const settings = typeof sandbox === 'string' ? sandbox : '/etc/facet/srt-settings.json';
+  return ['srt', '--settings', settings];
 }
@@
 async function runLoaderOnce(args: LoaderArgs): Promise<{ stderr?: Buffer | string; }> {
-  const prefix = srtPrefix(args.sandbox);
-  const tsx = resolveTsxBin(args.facetRoot).join(' ');
-  return await $`${{ raw: prefix }}${{ raw: tsx }} ${args.loaderPath} --facet-root=${args.facetRoot} --data-file=${args.dataFilePath} --output-file=${args.resultFilePath}`.quiet();
+  const argv = [
+    ...srtPrefix(args.sandbox),
+    ...resolveTsxBin(args.facetRoot),
+    args.loaderPath,
+    `--facet-root=${args.facetRoot}`,
+    `--data-file=${args.dataFilePath}`,
+    `--output-file=${args.resultFilePath}`,
+  ];
+  const cmd = argv.map(shellQuote).join(' ');
+  return await $`${{ raw: cmd }}`.quiet();
 }

Also applies to: 139-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/bundler/vite-builder.ts` around lines 61 - 65, The srtPrefix function
and the resolveTsxBin call are both producing raw strings that get concatenated
for shell execution in runLoaderOnce(), which fails when paths contain spaces or
shell metacharacters. Instead of concatenating raw strings, refactor srtPrefix
to return an array of command tokens (e.g., ['srt', '--settings', settings]) and
similarly refactor how resolveTsxBin is handled to build an argv array. Then
pass this argv array to spawn or execFile instead of using shell string
concatenation, or if using shell execution is necessary, properly quote each
token in the final command string.
🧹 Nitpick comments (3)
cli/src/commands/doctor.ts (1)

484-491: 💤 Low value

Array interpolation in shell template may produce malformed commands.

The template `${name} ${args}` converts the args array via .toString(), which joins elements with commas. For multi-element arrays (e.g., ['--version', '--quiet']), this produces git --version,--quiet rather than git --version --quiet. Current call sites use single-element arrays so this works, but it's fragile.

🛠️ Safer alternative using join
 async function probeOptionalBin(name: string, args: string[]): Promise<{ ok: true; output: string } | { ok: false }> {
   try {
-    const out = await $`${name} ${args}`.quiet();
+    const out = await $`${name} ${args.join(' ')}`.quiet();
     return { ok: true, output: out.stdout.toString().trim() };
   } catch {
     return { ok: false };
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/commands/doctor.ts` around lines 484 - 491, The probeOptionalBin
function interpolates the args array directly into the shell template, which
calls .toString() on the array and joins elements with commas rather than
spaces, potentially producing malformed commands like "git --version,--quiet"
instead of "git --version --quiet". Fix this by joining the args array with
spaces before interpolating it into the template literal so that multi-element
arrays are properly formatted as separate command arguments.
.github/workflows/release.yml (1)

35-35: ⚡ Quick win

Node version inconsistency across jobs.

version-bump and build-binaries use Node 20 while publish-npm uses Node 22. This inconsistency could cause subtle issues with lockfile formats or behavior differences. Consider aligning all jobs to the same Node version.

♻️ Align Node versions
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
-          node-version: "22"
+          node-version: "20"
           registry-url: "https://registry.npmjs.org"

Or update all jobs to Node 22 if that's the intended target.

Also applies to: 85-85, 178-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 35, The GitHub Actions workflow file
has inconsistent Node versions across multiple jobs: the version-bump job uses
Node 20 at line 35, the build-binaries job uses Node 20 at line 85, and the
publish-npm job uses Node 22 at line 178. Update all three node-version
specifications to use the same version (either all to Node 20 or all to Node 22)
to ensure consistent behavior and avoid potential lockfile format or behavior
differences across the build pipeline.
cli/src/utils/pdf-generator.ts (1)

15-15: Add explicit browser-path preflight check to fail fast with clear diagnostics when using puppeteer-core

When puppeteer-core is unable to resolve a browser path, it will fail at runtime with a generic error rather than a clear preflight diagnostic. Additionally, the current fallback message "Puppeteer bundled Chromium" is misleading since puppeteer-core includes no bundled binary and requires an explicit executablePath to launch.

Implement the validation in both launchBrowser() (line 473) and inline at line 421 to catch missing Chrome/Chromium paths immediately with the suggested error message. This improves diagnostics and guides users toward resolution.

Proposed adjustment
   const chromePath = resolveChromePath();
-  log.debug(`Using browser: ${chromePath ?? 'Puppeteer bundled Chromium'}`);
+  if (!chromePath) {
+    throw new Error(
+      'No Chromium/Chrome binary found. Set PUPPETEER_EXECUTABLE_PATH or CHROME_PATH, or install one via `npx puppeteer browsers install chrome`.'
+    );
+  }
+  log.debug(`Using browser: ${chromePath}`);
 
   const browser = await puppeteer.launch({
     headless: true,
     executablePath: chromePath,
     args: ['--no-sandbox', '--disable-setuid-sandbox'],
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/pdf-generator.ts` at line 15, Add explicit browser-path
preflight validation to fail fast when using puppeteer-core without an available
browser executable. In the launchBrowser() function (around line 473) and inline
at line 421, implement checks to verify that a valid Chrome/Chromium executable
path is available before attempting to launch. Replace the misleading fallback
message "Puppeteer bundled Chromium" with a clear error that explains
puppeteer-core requires an explicit executablePath configuration. This ensures
missing browser paths are caught immediately with diagnostic information that
guides users toward resolution, rather than failing at runtime with a generic
error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 72-75: The checkout step in the build-binaries job should add the
`persist-credentials: false` parameter to the actions/checkout@v4 action
(currently at lines 72-75). This parameter disables Git credential persistence
since the build-binaries job only reads the tagged commit and uploads artifacts
without performing any Git push operations, following the least-privilege
security principle.

In `@cli/package.json`:
- Line 22: The build:bin script in cli/package.json is unused dead code that
contradicts the PR's migration away from Bun. Since there are no references to
this script anywhere in the codebase, remove the entire line defining build:bin
from the scripts section of cli/package.json.

In `@cli/src/bundler/vite-builder.ts`:
- Around line 118-121: The `/ERR_MODULE_NOT_FOUND/` regex pattern in
RUNTIME_REINSTALL_PATTERNS is too broad and incorrectly matches relative-path
template errors in addition to package-miss cases, causing unnecessary
reinstalls. Replace this catch-all regex with a more specific pattern that
matches the exact package-miss error wording only, excluding relative-path
misses. Additionally, update the corresponding test assertion for
ERR_MODULE_NOT_FOUND in the cli/src/bundler/vite-builder.test.ts file to
validate the narrowed behavior and ensure it no longer triggers reinstalls for
relative-path errors.

In `@cli/src/utils/package-manager.ts`:
- Line 11: The shell module being imported here uses an unconditional `/bin/sh`
path that does not exist on Windows, causing ENOENT errors when executing
commands like pnpm version checks. In the shell module being imported, replace
the hardcoded `/bin/sh` with cross-platform compatible shell detection that uses
`cmd.exe` on Windows systems and `/bin/sh` on Unix-like systems. This will
ensure the $ function works correctly across all platforms and fix the broken
pnpm resolution flows in vite-builder, template resolution, and doctor commands
that depend on resolvePackageManager().

In `@cli/test/pdf-security.test.ts`:
- Line 13: The assignment of process.env['FACET_PACKAGE_PATH'] at module scope
in cli/test/pdf-security.test.ts at line 13 and cli/test/render-api.test.ts at
line 21 causes environment pollution for subsequent tests in the same worker
process. For each file, save the previous value of
process.env['FACET_PACKAGE_PATH'] before the assignment (store it in a const
like savedPath), and add or update an afterAll block to restore the original
state by either deleting the environment variable if it was previously
undefined, or restoring the saved value if it existed.

---

Outside diff comments:
In `@cli/src/bundler/vite-builder.ts`:
- Around line 61-65: The srtPrefix function and the resolveTsxBin call are both
producing raw strings that get concatenated for shell execution in
runLoaderOnce(), which fails when paths contain spaces or shell metacharacters.
Instead of concatenating raw strings, refactor srtPrefix to return an array of
command tokens (e.g., ['srt', '--settings', settings]) and similarly refactor
how resolveTsxBin is handled to build an argv array. Then pass this argv array
to spawn or execFile instead of using shell string concatenation, or if using
shell execution is necessary, properly quote each token in the final command
string.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Line 35: The GitHub Actions workflow file has inconsistent Node versions
across multiple jobs: the version-bump job uses Node 20 at line 35, the
build-binaries job uses Node 20 at line 85, and the publish-npm job uses Node 22
at line 178. Update all three node-version specifications to use the same
version (either all to Node 20 or all to Node 22) to ensure consistent behavior
and avoid potential lockfile format or behavior differences across the build
pipeline.

In `@cli/src/commands/doctor.ts`:
- Around line 484-491: The probeOptionalBin function interpolates the args array
directly into the shell template, which calls .toString() on the array and joins
elements with commas rather than spaces, potentially producing malformed
commands like "git --version,--quiet" instead of "git --version --quiet". Fix
this by joining the args array with spaces before interpolating it into the
template literal so that multi-element arrays are properly formatted as separate
command arguments.

In `@cli/src/utils/pdf-generator.ts`:
- Line 15: Add explicit browser-path preflight validation to fail fast when
using puppeteer-core without an available browser executable. In the
launchBrowser() function (around line 473) and inline at line 421, implement
checks to verify that a valid Chrome/Chromium executable path is available
before attempting to launch. Replace the misleading fallback message "Puppeteer
bundled Chromium" with a clear error that explains puppeteer-core requires an
explicit executablePath configuration. This ensures missing browser paths are
caught immediately with diagnostic information that guides users toward
resolution, rather than failing at runtime with a generic error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8de4e19-1826-41e8-9f42-1563fe819fdc

📥 Commits

Reviewing files that changed from the base of the PR and between 400cfa8 and a8b51d5.

⛔ Files ignored due to path filters (2)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (68)
  • .github/workflows/release.yml
  • .gitignore
  • Dockerfile
  • cli/.gitignore
  • cli/README.md
  • cli/package.json
  • cli/scripts/copy-assets.cjs
  • cli/scripts/gen-version.cjs
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/live-snapshot.ts
  • cli/src/bundler/vite-builder.test.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/bundler/vite-server.ts
  • cli/src/cli.bun.ts
  • cli/src/cli.ts
  • cli/src/commands/doctor.test.ts
  • cli/src/commands/doctor.ts
  • cli/src/generators/html.ts
  • cli/src/server/archive.ts
  • cli/src/server/config.ts
  • cli/src/server/preview.ts
  • cli/src/server/routes.ts
  • cli/src/server/worker-pool.ts
  • cli/src/types.ts
  • cli/src/utils/assets.bun.ts
  • cli/src/utils/assets.ts
  • cli/src/utils/data-loader.ts
  • cli/src/utils/debug-annotations.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/src/utils/remote-resolver.ts
  • cli/src/utils/shell.ts
  • cli/src/utils/tailwind.test.ts
  • cli/src/utils/tailwind.ts
  • cli/test/jsx-parser.test.ts
  • cli/test/lint-rules.test.ts
  • cli/test/pdf-bleed.test.ts
  • cli/test/pdf-security.test.ts
  • cli/test/render-api.test.ts
  • cli/test/resolve-output.test.ts
  • cli/test/shell.test.ts
  • cli/tsconfig.json
  • cli/tsconfig.test.json
  • cli/tsdown.config.ts
  • cli/vite-dev-loader.ts
  • cli/vite-ssr-loader.ts
  • cli/vitest.config.ts
  • examples/kitchen-sink/BleedTest.tsx
  • examples/kitchen-sink/DataFlowDiagram.tsx
  • examples/kitchen-sink/PageSizeTest.tsx
  • package.json
  • src/components/CompactTable.tsx
  • src/components/LogoGrid.tsx
  • src/components/MatrixTable.tsx
  • src/components/StatCard.tsx
  • src/components/diagram/Arrow.test.tsx
  • src/components/diagram/Arrow.tsx
  • src/components/diagram/BoxNode.test.tsx
  • src/components/diagram/BoxNode.tsx
  • src/components/diagram/Diagram.tsx
  • src/components/diagram/colors.ts
  • src/components/diagram/content.tsx
  • src/components/diagram/index.ts
  • src/components/index.tsx
💤 Files with no reviewable changes (2)
  • cli/test/lint-rules.test.ts
  • cli/test/jsx-parser.test.ts
✅ Files skipped from review due to trivial changes (8)
  • src/components/CompactTable.tsx
  • cli/vitest.config.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/server/worker-pool.ts
  • cli/src/utils/debug-annotations.ts
  • src/components/LogoGrid.tsx
  • src/components/diagram/index.ts
  • cli/scripts/gen-version.cjs
🚧 Files skipped from review as they are similar to previous changes (38)
  • .gitignore
  • cli/src/server/archive.ts
  • src/components/diagram/colors.ts
  • cli/src/types.ts
  • cli/src/cli.bun.ts
  • cli/tsconfig.test.json
  • cli/.gitignore
  • src/components/diagram/content.tsx
  • cli/scripts/copy-assets.cjs
  • src/components/diagram/Arrow.test.tsx
  • cli/src/utils/assets.bun.ts
  • cli/src/utils/assets.ts
  • cli/test/pdf-bleed.test.ts
  • cli/src/utils/data-loader.ts
  • src/components/diagram/Diagram.tsx
  • src/components/index.tsx
  • cli/src/commands/doctor.test.ts
  • examples/kitchen-sink/PageSizeTest.tsx
  • src/components/MatrixTable.tsx
  • cli/tsdown.config.ts
  • cli/src/cli.ts
  • src/components/diagram/BoxNode.test.tsx
  • src/components/diagram/BoxNode.tsx
  • cli/src/utils/tailwind.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/shell.ts
  • src/components/diagram/Arrow.tsx
  • cli/src/utils/remote-resolver.test.ts
  • cli/README.md
  • cli/src/bundler/vite-server.ts
  • cli/test/shell.test.ts
  • cli/src/server/routes.ts
  • cli/src/utils/tailwind.test.ts
  • Dockerfile
  • cli/vite-ssr-loader.ts
  • cli/src/server/preview.ts
  • cli/src/bundler/live-snapshot.ts
  • cli/src/builders/facet-directory.ts
🛑 Comments failed to post (5)
.github/workflows/release.yml (1)

72-75: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Disable credential persistence in build-binaries checkout.

The build-binaries job only reads the tagged commit and uploads artifacts; it doesn't push to Git. Add persist-credentials: false to follow least-privilege principles consistent with the publish-npm job.

🔐 Suggested fix
       - name: Checkout code
         uses: actions/checkout@v4
         with:
           ref: ${{ needs.version-bump.outputs.new_tag }}
+          persist-credentials: false
📝 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.

      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.version-bump.outputs.new_tag }}
          persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 72-75: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 73-73: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 72 - 75, The checkout step in the
build-binaries job should add the `persist-credentials: false` parameter to the
actions/checkout@v4 action (currently at lines 72-75). This parameter disables
Git credential persistence since the build-binaries job only reads the tagged
commit and uploads artifacts without performing any Git push operations,
following the least-privilege security principle.

Source: Linters/SAST tools

cli/package.json (1)

22-22: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether `build:bin` (or bun build) is still referenced by docs/workflows/scripts.
rg -n -C2 '"build:bin"|bun build|pnpm run build:bin|npm run build:bin'

Repository: flanksource/facet

Length of output: 779


Remove the unused build:bin script.

Line 22 defines a Bun-only script that contradicts the PR objective to migrate off Bun. Verification found no references to build:bin in the codebase—it is dead code. Remove it entirely or gate it with a clear comment explaining it is deprecated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` at line 22, The build:bin script in cli/package.json is
unused dead code that contradicts the PR's migration away from Bun. Since there
are no references to this script anywhere in the codebase, remove the entire
line defining build:bin from the scripts section of cli/package.json.
cli/src/bundler/vite-builder.ts (1)

118-121: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow ERR_MODULE_NOT_FOUND matching to package-miss cases only.

The catch-all /ERR_MODULE_NOT_FOUND/ triggers reinstall for relative-path template errors too, which defeats the package-only intent and causes unnecessary .facet nukes/reinstalls. Match package-miss wording explicitly and keep relative-path misses out.

Proposed fix
 const RUNTIME_REINSTALL_PATTERNS = [
   /Cannot find module ['"][^.\/][^'"]*['"]/i,
-  /ERR_MODULE_NOT_FOUND/,
+  /Cannot find package ['"][^.\/][^'"]*['"]/i,
   /Failed to resolve import ['"][^.\/][^'"]*['"]/i,
   /Failed to resolve entry/i,
   /Could not resolve ['"][^.\/][^'"]*['"] from/i,
   /Rollup failed to resolve(?: import)? ['"][^.\/][^'"]*['"]/i,
 ];

Also update the cli/src/bundler/vite-builder.test.ts ERR_MODULE_NOT_FOUND assertion to reflect the narrowed behavior.

📝 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.

const RUNTIME_REINSTALL_PATTERNS = [
  /Cannot find module ['"][^.\/][^'"]*['"]/i,
  /Cannot find package ['"][^.\/][^'"]*['"]/i,
  /Failed to resolve import ['"][^.\/][^'"]*['"]/i,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/bundler/vite-builder.ts` around lines 118 - 121, The
`/ERR_MODULE_NOT_FOUND/` regex pattern in RUNTIME_REINSTALL_PATTERNS is too
broad and incorrectly matches relative-path template errors in addition to
package-miss cases, causing unnecessary reinstalls. Replace this catch-all regex
with a more specific pattern that matches the exact package-miss error wording
only, excluding relative-path misses. Additionally, update the corresponding
test assertion for ERR_MODULE_NOT_FOUND in the
cli/src/bundler/vite-builder.test.ts file to validate the narrowed behavior and
ensure it no longer triggers reinstalls for relative-path errors.
cli/src/utils/package-manager.ts (1)

11-11: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the shell helper is POSIX-only and where this resolver depends on it.
set -euo pipefail

echo "== shell helper implementation =="
fd -i 'shell.ts' cli/src --exec sed -n '1,240p' {}

echo
echo "== shell helper platform assumptions =="
rg -n "(/bin/sh|cmd\\.exe|powershell|spawn\\(|execFile\\()" cli/src/utils/shell.ts -n -C 3 || true

echo
echo "== package-manager probe callsites =="
rg -n "\\$`(pnpm --version|corepack enable pnpm)" cli/src/utils/package-manager.ts cli/src/utils/remote-resolver.ts -n -C 2

Repository: flanksource/facet

Length of output: 4263


🏁 Script executed:

# Read package-manager.ts to see how the shell wrapper is used
cat -n cli/src/utils/package-manager.ts

Repository: flanksource/facet

Length of output: 5401


🏁 Script executed:

# Check for Windows-specific handling and platform detection
echo "== Platform detection in codebase =="
rg "process\.platform|win32|windows" cli/src --type ts -i

echo
echo "== Where resolvePackageManager is called =="
rg "resolvePackageManager" cli/src --type ts -B 2 -A 2

echo
echo "== Check if shell.ts has any platform detection =="
rg "process\.|os\.|platform" cli/src/utils/shell.ts --type ts

Repository: flanksource/facet

Length of output: 7731


Fix shell wrapper cross-platform compatibility on Windows

The shell module at line 11 uses /bin/sh unconditionally, which does not exist on Windows. This blocks pnpm --version and corepack enable pnpm commands, preventing pnpm resolution from starting on Windows. The issue affects critical flows: install operations in vite-builder, remote template resolution, and the doctor diagnostic command all depend on resolvePackageManager() which will fail immediately with ENOENT on Windows systems.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/package-manager.ts` at line 11, The shell module being imported
here uses an unconditional `/bin/sh` path that does not exist on Windows,
causing ENOENT errors when executing commands like pnpm version checks. In the
shell module being imported, replace the hardcoded `/bin/sh` with cross-platform
compatible shell detection that uses `cmd.exe` on Windows systems and `/bin/sh`
on Unix-like systems. This will ensure the $ function works correctly across all
platforms and fix the broken pnpm resolution flows in vite-builder, template
resolution, and doctor commands that depend on resolvePackageManager().
cli/test/pdf-security.test.ts (1)

13-13: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect global FACET_PACKAGE_PATH mutations and whether tests restore them.
rg -n -C3 "FACET_PACKAGE_PATH|process\\.env\\[" cli/test cli/src

Repository: flanksource/facet

Length of output: 20900


🏁 Script executed:

# Read the full pdf-security.test.ts file to check for afterAll cleanup
wc -l cli/test/pdf-security.test.ts

Repository: flanksource/facet

Length of output: 95


🏁 Script executed:

# Read the full render-api.test.ts file to check for afterAll cleanup
wc -l cli/test/render-api.test.ts

Repository: flanksource/facet

Length of output: 93


🏁 Script executed:

cat -n cli/test/pdf-security.test.ts

Repository: flanksource/facet

Length of output: 9626


🏁 Script executed:

cat -n cli/test/render-api.test.ts | head -60

Repository: flanksource/facet

Length of output: 2423


Save and restore FACET_PACKAGE_PATH in module cleanup to prevent test leakage.

Line 13 sets FACET_PACKAGE_PATH at module scope without restoration, which pollutes the environment for tests running in the same worker process. Store the previous value and restore it in afterAll:

const savedPath = process.env['FACET_PACKAGE_PATH'];
process.env['FACET_PACKAGE_PATH'] = REPO_ROOT;

// In afterAll:
if (savedPath === undefined) delete process.env['FACET_PACKAGE_PATH'];
else process.env['FACET_PACKAGE_PATH'] = savedPath;

The same issue exists in cli/test/render-api.test.ts at line 21.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/pdf-security.test.ts` at line 13, The assignment of
process.env['FACET_PACKAGE_PATH'] at module scope in
cli/test/pdf-security.test.ts at line 13 and cli/test/render-api.test.ts at line
21 causes environment pollution for subsequent tests in the same worker process.
For each file, save the previous value of process.env['FACET_PACKAGE_PATH']
before the assignment (store it in a const like savedPath), and add or update an
afterAll block to restore the original state by either deleting the environment
variable if it was previously undefined, or restoring the saved value if it
existed.

yashmehrotra and others added 2 commits June 17, 2026 09:24
- shell.ts: array interpolation escapes each element as a separate arg;
  signal-terminated children now reject with signal details
- vite-builder/vite-ssr-loader: shell-safe tsx argv; NODE_PATH uses delimiter
- facet-directory: .npmrc written 0o600 (may carry auth tokens)
- doctor: checkTsx probes the runtime resolver, not bare PATH
- preview: stream loop honors res.write backpressure
- Badge: nullish label guard so 0/"" render; ListTable: preserve passthrough
  classes; Arrow: showTail on bidirectional; content: unique React keys
- styles.css @import string notation; HeaderSolid title 14mm; CI/docs cleanup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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: 7

Caution

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

⚠️ Outside diff range comments (3)
cli/src/utils/tailwind.test.ts (2)

49-54: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the fixture binary name platform-aware.

Lines 52–54 only create tailwindcss, but Windows resolution uses tailwindcss.cmd. This test can fail on win32.

Suggested fix
   const binDir = join(facetRoot, 'node_modules', '.bin');
   mkdirSync(binDir, { recursive: true });
-  writeFileSync(join(binDir, 'tailwindcss'), '#!/bin/sh\n');
-  chmodSync(join(binDir, 'tailwindcss'), 0o755);
+  const binName = process.platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss';
+  writeFileSync(join(binDir, binName), '#!/bin/sh\n');
+  if (process.platform !== 'win32') chmodSync(join(binDir, binName), 0o755);
   expect(tailwindBinExists(facetRoot)).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/tailwind.test.ts` around lines 49 - 54, The test fixture in the
tailwindBinExists test only creates the tailwindcss binary file, but on Windows
systems, the binary resolution uses tailwindcss.cmd. Make the binary name
platform-aware by detecting the current operating system and creating the
appropriate binary file name: use tailwindcss.cmd on win32 platforms and
tailwindcss on other platforms. This ensures the test passes consistently across
both Unix-like systems and Windows.

19-30: ⚠️ Potential issue | 🟠 Major

Replace Object.defineProperty mocking with dependency injection to avoid global state pollution during parallel test execution.

The current approach of using Object.defineProperty to mock process.platform is inherently unstable. While the code includes an afterEach restoration hook, this pattern relies on global mutation that can cause race conditions and unpredictable behavior when tests run in parallel (which Bun does by default). Additionally, mocking process.platform does not simulate the actual environment—underlying OS-specific behaviors remain unchanged.

Refactor resolveTailwindBin() to accept an optional platform parameter or create a wrapper function that abstracts platform detection, allowing tests to inject the desired value without mutating global state:

export function resolveTailwindBin(facetRoot: string, platform: string = process.platform): string {
  const name = platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss';
  return join(facetRoot, 'node_modules', '.bin', name);
}

Then tests can simply call resolveTailwindBin(path, 'linux') or resolveTailwindBin(path, 'win32') without modifying global state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/tailwind.test.ts` around lines 19 - 30, The resolveTailwindBin
function currently relies on global process.platform, which causes tests to
mutate global state through Object.defineProperty, leading to race conditions
during parallel execution. Refactor resolveTailwindBin to accept an optional
platform parameter (defaulting to process.platform if not provided) and use that
parameter to determine whether to return the windows or POSIX path. Then update
the test cases to pass the platform value directly as a second argument to
resolveTailwindBin (e.g., resolveTailwindBin('/tmp/.facet', 'linux') and
resolveTailwindBin('C:\\proj\\.facet', 'win32')) and remove the
Object.defineProperty mocking calls to eliminate global state pollution.
cli/package.json (1)

8-10: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

bin points to an artifact this build does not produce.

Line 9 maps the executable to ./dist/cli.mjs, but Line 19 writes ../dist/facet, and cli/tsdown.config.ts Line 5 only builds src/index.ts. This will make the published CLI entrypoint missing or stale.

Also applies to: 19-23

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` around lines 8 - 10, The bin field in package.json points
to ./dist/cli.mjs as the CLI executable entry point, but this does not match the
actual build output path or what is configured in the build tool. Review the
tsdown.config.ts file to determine what file is actually being built and where
the build output is placed (referenced on line 19 as ../dist/facet), then update
the bin entry in package.json to point to the correct built artifact path and
filename that the build system actually produces. Ensure all references are
consistent across package.json, the build configuration, and any build output
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/package.json`:
- Line 2: The package name field in the cli package.json file is set to "facet"
but the release target expects the scoped package name "`@flanksource/facet-cli`".
Update the "name" property to use the correct scoped package name
"`@flanksource/facet-cli`" to ensure the release automation publishes the correct
package and consumers can access it with the expected name.
- Around line 18-26: The build and test scripts in the package.json
(specifically `build:cli`, `test`, and `test:watch`) are hardcoded to use Bun as
the only runtime, which creates friction in environments where Bun is not
installed. To address this, either provide alternative scripts using Node/npm
that are compatible with non-Bun environments, or add clear documentation (such
as a README section or inline comment in package.json) explicitly stating that
Bun is a required dependency for building and testing this project. Given that
the codebase already contains Bun-specific APIs like `import { $ } from 'bun'`
and `Bun.spawn`, documenting the requirement is the recommended approach.

In `@cli/src/builders/facet-directory.test.ts`:
- Line 1: The import statement in the facet-directory.test.ts file is importing
test utilities from 'bun:test' but the PR objective requires migration to
Vitest. Replace the import statement that currently imports describe, it,
expect, beforeEach, and afterEach from 'bun:test' with an equivalent import from
'vitest' instead. The imported functions remain the same, only the import source
needs to be changed.

In `@cli/src/commands/doctor.ts`:
- Line 10: The doctor command currently uses Bun-specific runtime APIs that are
incompatible with Node.js. Remove the import statement `import { $ } from 'bun'`
from the top of the file and replace the `Bun.spawnSync` call on line 229 with
the Node.js equivalent from the child_process module (use `execSync` or
`spawnSync` from child_process). Update the method call and any related
parameters to match Node.js child_process API conventions to ensure the doctor
command works correctly under both Node.js and Bun runtimes.
- Line 20: The import statement for rootPackageJson uses Bun-specific syntax
with the `type: 'file'` attribute which is not supported in Node.js. Replace the
import statement with a Node.js-compatible approach by using fileURLToPath and
import.meta.url to create a file path string. Create a constant using new URL()
to resolve the relative path '../../../package.json' against import.meta.url,
then pass it to fileURLToPath() to get an absolute path that can be used with
readFileSync() at line 214 where rootPackageJson is consumed.

In `@cli/src/utils/package-manager.ts`:
- Line 11: The code currently imports the Bun-specific `$` utility from 'bun' on
line 11 of package-manager.ts and line 6 of remote-resolver.ts, making these
modules incompatible with Node.js runtime despite the CLI's Node.js shebang and
engine requirements. Create a runtime-agnostic shell abstraction module that
conditionally uses Bun's `$` when running under Bun or Node.js's `child_process`
module when running under Node.js, then update the imports in both
package-manager.ts and remote-resolver.ts to use this abstraction instead of
directly importing from 'bun'. Apply the abstraction to all shell command
executions within the functions tryProbePnpmVersion(), tryCorepackEnablePnpm(),
resolvePackageManager(), and any other functions that invoke shell commands
through the `$` utility.

In `@cli/tsdown.config.ts`:
- Line 12: The library targets Node.js but exports startServer and createServer
that exclusively use Bun.serve() with no Node.js fallback. This creates a
fundamental design mismatch. You need to resolve this by choosing one of three
approaches: (1) implement Node.js-compatible versions of startServer and
createServer using the http module alongside the Bun implementations, (2) remove
these functions from the public exports and keep them as internal utilities for
CLI use only, or (3) change the build configuration to target Bun explicitly
instead of Node.js. Simply having 'bun' in the external array does not resolve
the core issue since bun is not listed as a dependency and the bundler cannot
resolve it anyway.

---

Outside diff comments:
In `@cli/package.json`:
- Around line 8-10: The bin field in package.json points to ./dist/cli.mjs as
the CLI executable entry point, but this does not match the actual build output
path or what is configured in the build tool. Review the tsdown.config.ts file
to determine what file is actually being built and where the build output is
placed (referenced on line 19 as ../dist/facet), then update the bin entry in
package.json to point to the correct built artifact path and filename that the
build system actually produces. Ensure all references are consistent across
package.json, the build configuration, and any build output paths.

In `@cli/src/utils/tailwind.test.ts`:
- Around line 49-54: The test fixture in the tailwindBinExists test only creates
the tailwindcss binary file, but on Windows systems, the binary resolution uses
tailwindcss.cmd. Make the binary name platform-aware by detecting the current
operating system and creating the appropriate binary file name: use
tailwindcss.cmd on win32 platforms and tailwindcss on other platforms. This
ensures the test passes consistently across both Unix-like systems and Windows.
- Around line 19-30: The resolveTailwindBin function currently relies on global
process.platform, which causes tests to mutate global state through
Object.defineProperty, leading to race conditions during parallel execution.
Refactor resolveTailwindBin to accept an optional platform parameter (defaulting
to process.platform if not provided) and use that parameter to determine whether
to return the windows or POSIX path. Then update the test cases to pass the
platform value directly as a second argument to resolveTailwindBin (e.g.,
resolveTailwindBin('/tmp/.facet', 'linux') and
resolveTailwindBin('C:\\proj\\.facet', 'win32')) and remove the
Object.defineProperty mocking calls to eliminate global state pollution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1b45e5c-3ae1-491c-afd6-5606f9696b21

📥 Commits

Reviewing files that changed from the base of the PR and between a8b51d5 and 3ae4f1b.

⛔ Files ignored due to path filters (1)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .github/workflows/release.yml
  • cli/README.md
  • cli/package.json
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/bundler/vite-server.ts
  • cli/src/commands/doctor.test.ts
  • cli/src/commands/doctor.ts
  • cli/src/server/preview.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/tailwind.test.ts
  • cli/src/utils/tailwind.ts
  • cli/tsdown.config.ts
  • cli/vite-dev-loader.ts
  • examples/kitchen-sink/HeaderSolid.tsx
  • src/components/Badge.tsx
  • src/components/ListTable.tsx
  • src/components/diagram/Arrow.tsx
  • src/components/diagram/content.tsx
  • src/styles.css
💤 Files with no reviewable changes (1)
  • cli/README.md
✅ Files skipped from review due to trivial changes (4)
  • examples/kitchen-sink/HeaderSolid.tsx
  • src/styles.css
  • cli/src/bundler/vite-builder.ts
  • cli/src/server/preview.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/diagram/content.tsx
  • cli/src/utils/tailwind.ts
  • src/components/diagram/Arrow.tsx
  • cli/vite-dev-loader.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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (3)
cli/src/utils/tailwind.test.ts (2)

49-54: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the fixture binary name platform-aware.

Lines 52–54 only create tailwindcss, but Windows resolution uses tailwindcss.cmd. This test can fail on win32.

Suggested fix
   const binDir = join(facetRoot, 'node_modules', '.bin');
   mkdirSync(binDir, { recursive: true });
-  writeFileSync(join(binDir, 'tailwindcss'), '#!/bin/sh\n');
-  chmodSync(join(binDir, 'tailwindcss'), 0o755);
+  const binName = process.platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss';
+  writeFileSync(join(binDir, binName), '#!/bin/sh\n');
+  if (process.platform !== 'win32') chmodSync(join(binDir, binName), 0o755);
   expect(tailwindBinExists(facetRoot)).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/tailwind.test.ts` around lines 49 - 54, The test fixture in the
tailwindBinExists test only creates the tailwindcss binary file, but on Windows
systems, the binary resolution uses tailwindcss.cmd. Make the binary name
platform-aware by detecting the current operating system and creating the
appropriate binary file name: use tailwindcss.cmd on win32 platforms and
tailwindcss on other platforms. This ensures the test passes consistently across
both Unix-like systems and Windows.

19-30: ⚠️ Potential issue | 🟠 Major

Replace Object.defineProperty mocking with dependency injection to avoid global state pollution during parallel test execution.

The current approach of using Object.defineProperty to mock process.platform is inherently unstable. While the code includes an afterEach restoration hook, this pattern relies on global mutation that can cause race conditions and unpredictable behavior when tests run in parallel (which Bun does by default). Additionally, mocking process.platform does not simulate the actual environment—underlying OS-specific behaviors remain unchanged.

Refactor resolveTailwindBin() to accept an optional platform parameter or create a wrapper function that abstracts platform detection, allowing tests to inject the desired value without mutating global state:

export function resolveTailwindBin(facetRoot: string, platform: string = process.platform): string {
  const name = platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss';
  return join(facetRoot, 'node_modules', '.bin', name);
}

Then tests can simply call resolveTailwindBin(path, 'linux') or resolveTailwindBin(path, 'win32') without modifying global state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/tailwind.test.ts` around lines 19 - 30, The resolveTailwindBin
function currently relies on global process.platform, which causes tests to
mutate global state through Object.defineProperty, leading to race conditions
during parallel execution. Refactor resolveTailwindBin to accept an optional
platform parameter (defaulting to process.platform if not provided) and use that
parameter to determine whether to return the windows or POSIX path. Then update
the test cases to pass the platform value directly as a second argument to
resolveTailwindBin (e.g., resolveTailwindBin('/tmp/.facet', 'linux') and
resolveTailwindBin('C:\\proj\\.facet', 'win32')) and remove the
Object.defineProperty mocking calls to eliminate global state pollution.
cli/package.json (1)

8-10: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

bin points to an artifact this build does not produce.

Line 9 maps the executable to ./dist/cli.mjs, but Line 19 writes ../dist/facet, and cli/tsdown.config.ts Line 5 only builds src/index.ts. This will make the published CLI entrypoint missing or stale.

Also applies to: 19-23

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` around lines 8 - 10, The bin field in package.json points
to ./dist/cli.mjs as the CLI executable entry point, but this does not match the
actual build output path or what is configured in the build tool. Review the
tsdown.config.ts file to determine what file is actually being built and where
the build output is placed (referenced on line 19 as ../dist/facet), then update
the bin entry in package.json to point to the correct built artifact path and
filename that the build system actually produces. Ensure all references are
consistent across package.json, the build configuration, and any build output
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/package.json`:
- Line 2: The package name field in the cli package.json file is set to "facet"
but the release target expects the scoped package name "`@flanksource/facet-cli`".
Update the "name" property to use the correct scoped package name
"`@flanksource/facet-cli`" to ensure the release automation publishes the correct
package and consumers can access it with the expected name.
- Around line 18-26: The build and test scripts in the package.json
(specifically `build:cli`, `test`, and `test:watch`) are hardcoded to use Bun as
the only runtime, which creates friction in environments where Bun is not
installed. To address this, either provide alternative scripts using Node/npm
that are compatible with non-Bun environments, or add clear documentation (such
as a README section or inline comment in package.json) explicitly stating that
Bun is a required dependency for building and testing this project. Given that
the codebase already contains Bun-specific APIs like `import { $ } from 'bun'`
and `Bun.spawn`, documenting the requirement is the recommended approach.

In `@cli/src/builders/facet-directory.test.ts`:
- Line 1: The import statement in the facet-directory.test.ts file is importing
test utilities from 'bun:test' but the PR objective requires migration to
Vitest. Replace the import statement that currently imports describe, it,
expect, beforeEach, and afterEach from 'bun:test' with an equivalent import from
'vitest' instead. The imported functions remain the same, only the import source
needs to be changed.

In `@cli/src/commands/doctor.ts`:
- Line 10: The doctor command currently uses Bun-specific runtime APIs that are
incompatible with Node.js. Remove the import statement `import { $ } from 'bun'`
from the top of the file and replace the `Bun.spawnSync` call on line 229 with
the Node.js equivalent from the child_process module (use `execSync` or
`spawnSync` from child_process). Update the method call and any related
parameters to match Node.js child_process API conventions to ensure the doctor
command works correctly under both Node.js and Bun runtimes.
- Line 20: The import statement for rootPackageJson uses Bun-specific syntax
with the `type: 'file'` attribute which is not supported in Node.js. Replace the
import statement with a Node.js-compatible approach by using fileURLToPath and
import.meta.url to create a file path string. Create a constant using new URL()
to resolve the relative path '../../../package.json' against import.meta.url,
then pass it to fileURLToPath() to get an absolute path that can be used with
readFileSync() at line 214 where rootPackageJson is consumed.

In `@cli/src/utils/package-manager.ts`:
- Line 11: The code currently imports the Bun-specific `$` utility from 'bun' on
line 11 of package-manager.ts and line 6 of remote-resolver.ts, making these
modules incompatible with Node.js runtime despite the CLI's Node.js shebang and
engine requirements. Create a runtime-agnostic shell abstraction module that
conditionally uses Bun's `$` when running under Bun or Node.js's `child_process`
module when running under Node.js, then update the imports in both
package-manager.ts and remote-resolver.ts to use this abstraction instead of
directly importing from 'bun'. Apply the abstraction to all shell command
executions within the functions tryProbePnpmVersion(), tryCorepackEnablePnpm(),
resolvePackageManager(), and any other functions that invoke shell commands
through the `$` utility.

In `@cli/tsdown.config.ts`:
- Line 12: The library targets Node.js but exports startServer and createServer
that exclusively use Bun.serve() with no Node.js fallback. This creates a
fundamental design mismatch. You need to resolve this by choosing one of three
approaches: (1) implement Node.js-compatible versions of startServer and
createServer using the http module alongside the Bun implementations, (2) remove
these functions from the public exports and keep them as internal utilities for
CLI use only, or (3) change the build configuration to target Bun explicitly
instead of Node.js. Simply having 'bun' in the external array does not resolve
the core issue since bun is not listed as a dependency and the bundler cannot
resolve it anyway.

---

Outside diff comments:
In `@cli/package.json`:
- Around line 8-10: The bin field in package.json points to ./dist/cli.mjs as
the CLI executable entry point, but this does not match the actual build output
path or what is configured in the build tool. Review the tsdown.config.ts file
to determine what file is actually being built and where the build output is
placed (referenced on line 19 as ../dist/facet), then update the bin entry in
package.json to point to the correct built artifact path and filename that the
build system actually produces. Ensure all references are consistent across
package.json, the build configuration, and any build output paths.

In `@cli/src/utils/tailwind.test.ts`:
- Around line 49-54: The test fixture in the tailwindBinExists test only creates
the tailwindcss binary file, but on Windows systems, the binary resolution uses
tailwindcss.cmd. Make the binary name platform-aware by detecting the current
operating system and creating the appropriate binary file name: use
tailwindcss.cmd on win32 platforms and tailwindcss on other platforms. This
ensures the test passes consistently across both Unix-like systems and Windows.
- Around line 19-30: The resolveTailwindBin function currently relies on global
process.platform, which causes tests to mutate global state through
Object.defineProperty, leading to race conditions during parallel execution.
Refactor resolveTailwindBin to accept an optional platform parameter (defaulting
to process.platform if not provided) and use that parameter to determine whether
to return the windows or POSIX path. Then update the test cases to pass the
platform value directly as a second argument to resolveTailwindBin (e.g.,
resolveTailwindBin('/tmp/.facet', 'linux') and
resolveTailwindBin('C:\\proj\\.facet', 'win32')) and remove the
Object.defineProperty mocking calls to eliminate global state pollution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1b45e5c-3ae1-491c-afd6-5606f9696b21

📥 Commits

Reviewing files that changed from the base of the PR and between a8b51d5 and 3ae4f1b.

⛔ Files ignored due to path filters (1)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .github/workflows/release.yml
  • cli/README.md
  • cli/package.json
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/bundler/vite-server.ts
  • cli/src/commands/doctor.test.ts
  • cli/src/commands/doctor.ts
  • cli/src/server/preview.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/tailwind.test.ts
  • cli/src/utils/tailwind.ts
  • cli/tsdown.config.ts
  • cli/vite-dev-loader.ts
  • examples/kitchen-sink/HeaderSolid.tsx
  • src/components/Badge.tsx
  • src/components/ListTable.tsx
  • src/components/diagram/Arrow.tsx
  • src/components/diagram/content.tsx
  • src/styles.css
💤 Files with no reviewable changes (1)
  • cli/README.md
✅ Files skipped from review due to trivial changes (4)
  • examples/kitchen-sink/HeaderSolid.tsx
  • src/styles.css
  • cli/src/bundler/vite-builder.ts
  • cli/src/server/preview.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/diagram/content.tsx
  • cli/src/utils/tailwind.ts
  • src/components/diagram/Arrow.tsx
  • cli/vite-dev-loader.ts
🛑 Comments failed to post (7)
cli/package.json (2)

2-2: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Package name diverges from the release target.

Line 2 sets "name": "facet", but the release objective specifies publishing @flanksource/facet-cli. This can publish the wrong package and break release automation/consumers expecting the scoped name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` at line 2, The package name field in the cli package.json
file is set to "facet" but the release target expects the scoped package name
"`@flanksource/facet-cli`". Update the "name" property to use the correct scoped
package name "`@flanksource/facet-cli`" to ensure the release automation publishes
the correct package and consumers can access it with the expected name.

18-26: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify Bun is now required by package scripts
rg -n '"(build:cli|test|test:watch)"\s*:\s*"[^"]*\bbun\b' cli/package.json

# Verify Bun API usage still exists in source/test trees
rg -nP '\bfrom\s+[\"\x27]bun[\"\x27]|\brequire\([\"\x27]bun[\"\x27]\)|\bBun\.' cli/src cli/test cli || true

Repository: flanksource/facet

Length of output: 1978


🏁 Script executed:

# Check line 72 of cli/package.json
sed -n '70,75p' cli/package.json

# Look for migration goals or Node requirements in package.json
cat -n cli/package.json | head -80

Repository: flanksource/facet

Length of output: 2979


🏁 Script executed:

# Search for any migration-related comments or goals in the codebase
rg -i "node.*migration|migrate.*node|node.*contract" --max-count=20

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Look for README or migration documentation
find . -maxdepth 2 -type f \( -name "README*" -o -name "CONTRIBUTING*" -o -name "*.md" \) | head -20

# Check root README for any migration discussion
cat README.md 2>/dev/null | head -100 || echo "No root README found"

# Check for docs directory
ls -la docs/ 2>/dev/null || echo "No docs directory"

Repository: flanksource/facet

Length of output: 2796


🏁 Script executed:

# Search for migration-related context in any files
rg -i "migrate\s+to\s+node|move\s+to\s+node|node-only|npm-only" --max-count=10

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

cat CONTRIBUTING.md 2>/dev/null | head -50 || echo "File not found or empty"

Repository: flanksource/facet

Length of output: 43


Bun is required in build/test scripts despite environment constraints.

Lines 19, 25-26 establish bun build, bun test, and bun test:watch as the only available commands, while line 72 adds @types/bun as a devDependency. This hard requirement on Bun can create friction in CI systems or contributor environments where Bun is not installed, and the codebase already uses Bun-specific APIs extensively (e.g., import { $ } from 'bun', Bun.spawn, Bun.serve across multiple files). Consider providing Node/npm equivalents or documenting the Bun requirement explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/package.json` around lines 18 - 26, The build and test scripts in the
package.json (specifically `build:cli`, `test`, and `test:watch`) are hardcoded
to use Bun as the only runtime, which creates friction in environments where Bun
is not installed. To address this, either provide alternative scripts using
Node/npm that are compatible with non-Bun environments, or add clear
documentation (such as a README section or inline comment in package.json)
explicitly stating that Bun is a required dependency for building and testing
this project. Given that the codebase already contains Bun-specific APIs like
`import { $ } from 'bun'` and `Bun.spawn`, documenting the requirement is the
recommended approach.
cli/src/builders/facet-directory.test.ts (1)

1-1: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test imports bun:test but PR objective states migration to Vitest.

The PR summary indicates "migrates bun:testvitest", but this file imports from bun:test. This will fail when running tests under Node.js with Vitest.

Proposed fix
-import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
📝 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.

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/builders/facet-directory.test.ts` at line 1, The import statement in
the facet-directory.test.ts file is importing test utilities from 'bun:test' but
the PR objective requires migration to Vitest. Replace the import statement that
currently imports describe, it, expect, beforeEach, and afterEach from
'bun:test' with an equivalent import from 'vitest' instead. The imported
functions remain the same, only the import source needs to be changed.
cli/src/commands/doctor.ts (2)

10-10: ⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Bun runtime API usage still present in CLI sources.
rg -n "from 'bun'|from \"bun\"|\\bBun\\." cli/src

Repository: flanksource/facet

Length of output: 940


Remove Bun runtime APIs from doctor command to ensure Node.js compatibility.

The import on line 10 (import { $ } from 'bun') and the Bun API call on line 229 (Bun.spawnSync) require the Bun runtime and will cause the doctor command to fail when run under Node.js.

Replace with Node.js equivalents:

Suggested fix
-import { $ } from 'bun';
+import { $ } from '../utils/shell.js';
+import { spawnSync } from 'node:child_process';
...
-    hostArch = Bun.spawnSync(['uname', '-m']).stdout.toString().trim();
+    hostArch = spawnSync('uname', ['-m'], { encoding: 'utf-8' }).stdout.trim();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/commands/doctor.ts` at line 10, The doctor command currently uses
Bun-specific runtime APIs that are incompatible with Node.js. Remove the import
statement `import { $ } from 'bun'` from the top of the file and replace the
`Bun.spawnSync` call on line 229 with the Node.js equivalent from the
child_process module (use `execSync` or `spawnSync` from child_process). Update
the method call and any related parameters to match Node.js child_process API
conventions to ensure the doctor command works correctly under both Node.js and
Bun runtimes.

20-20: ⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant lines
if [ -f "cli/src/commands/doctor.ts" ]; then
  wc -l cli/src/commands/doctor.ts
  echo "=== Lines 1-30 ==="
  sed -n '1,30p' cli/src/commands/doctor.ts | cat -n
  echo "=== Lines 210-220 ==="
  sed -n '210,220p' cli/src/commands/doctor.ts | cat -n
else
  echo "File not found"
fi

Repository: flanksource/facet

Length of output: 1708


🌐 Web query:

Node.js import attributes with type file syntax compatibility

💡 Result:

In Node.js, the syntax for importing files—specifically JSON modules—has evolved from "import assertions" to the standardized "import attributes" [1][2][3]. Current Syntax (Standardized) The current, standardized syntax uses the with keyword [4][5][2]: import data from './data.json' with { type: 'json' }; const { default: barData } = await import('./bar.json', { with: { type: 'json' } }); Compatibility Summary - Node.js v22.0.0 and later: Only the with syntax is supported [4][5][2]. Support for the older assert keyword was explicitly removed [4][2]. - Node.js v18.20.0 and v20.10.0: Support for the with syntax (import attributes) was added [4][1]. - Prior Versions (e.g., v16.x, v17.x, older v18.x): These versions used the assert keyword (import assertions) [6][3], which is now deprecated and incompatible with modern Node.js environments [5][2]. Key Details - Mandatory Attribute: The type: 'json' attribute is required when importing JSON files [4][7][8]. Node.js currently only supports the type attribute for this purpose [4][1][3]. - Migration: If you are upgrading a codebase from older Node.js versions, you must replace all instances of assert with with [5][2]. A codemod is available via npx codemod nodejs/import-assertions-to-attributes to automate this transition [5][9].

Citations:


🌐 Web query:

Bun import attributes type file JSON import syntax

💡 Result:

In Bun, you can import JSON files natively using standard ES module syntax [1][2]. While the .json extension is often sufficient for automatic handling [2][3], you can explicitly specify the type using import attributes [1][4]. Current Import Syntax To import a JSON file, use the following syntax [1]: import data from "./data.json"; For explicit typing—which can be useful for clarity or when working with files that do not have a.json extension—you can use the with keyword, which follows the Stage 3 ECMA Script proposal for import attributes [1][5]: import data from "./data.json" with { type: "json" }; Dynamic Import Syntax For dynamic imports, use the with option [2][6]: const data = await import("./data.json", { with: { type: "json" } }); Key Details - Import Attributes vs. Assertions: Bun supports the modern with syntax (Import Attributes) [1][5]. While an older syntax using assert previously existed (Import Assertions), it has been abandoned by the TC39 proposal in favor of with [5][7]. - Bundler Compatibility: When bundling with Bun, JSON files imported in this manner are inlined into the resulting JavaScript bundle as objects [2][3]. - Other Supported Types: The same with { type: "..." } syntax is supported by Bun for other loaders, including toml, text, and file [6][4]. For scenarios where you do not need the file parsed as a module, you can also use Bun.file() to read JSON files lazily [8].

Citations:


with { type: 'file' } is Bun-specific and incompatible with Node.js.

Line 20 uses a Bun-only import attribute syntax. Node.js does not support type: 'file' (it only supports type: 'json' for import attributes). Since line 214 expects rootPackageJson to be a file path for readFileSync(), convert to a Node.js-compatible path using fileURLToPath and import.meta.url:

Suggested fix
-import rootPackageJson from '../../../package.json' with { type: 'file' };
+import { fileURLToPath } from 'node:url';
+const rootPackageJson = fileURLToPath(new URL('../../../package.json', import.meta.url));
📝 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.

import { fileURLToPath } from 'node:url';
const rootPackageJson = fileURLToPath(new URL('../../../package.json', import.meta.url));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/commands/doctor.ts` at line 20, The import statement for
rootPackageJson uses Bun-specific syntax with the `type: 'file'` attribute which
is not supported in Node.js. Replace the import statement with a
Node.js-compatible approach by using fileURLToPath and import.meta.url to create
a file path string. Create a constant using new URL() to resolve the relative
path '../../../package.json' against import.meta.url, then pass it to
fileURLToPath() to get an absolute path that can be used with readFileSync() at
line 214 where rootPackageJson is consumed.
cli/src/utils/package-manager.ts (1)

11-11: ⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check the structure of the cli/src/utils directory
ls -la cli/src/utils/ | head -20

Repository: flanksource/facet

Length of output: 1329


🏁 Script executed:

# Read the package-manager.ts file to understand the imports and usage
cat -n cli/src/utils/package-manager.ts

Repository: flanksource/facet

Length of output: 5394


🏁 Script executed:

# Check if shell.js exists in the same directory
ls -la cli/src/utils/shell.js 2>&1 || echo "File not found"

Repository: flanksource/facet

Length of output: 146


🏁 Script executed:

# Search for resolvePackageManager function to understand its purpose
rg "resolvePackageManager" cli/src/utils/package-manager.ts -A 5

Repository: flanksource/facet

Length of output: 416


🏁 Script executed:

# Search for shell.js anywhere in the project
find . -name "shell.js" -o -name "shell.ts" 2>/dev/null

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check if there's a shell wrapper or command execution utility elsewhere
rg "export.*\$.*from" --type ts cli/src/utils/ 2>/dev/null || echo "No matches"

Repository: flanksource/facet

Length of output: 72


🏁 Script executed:

# Look for shell command utilities in the entire cli/src
fd -e ts -e js cli/src | xargs rg "shell|cmd|exec" --type ts 2>/dev/null | head -20

Repository: flanksource/facet

Length of output: 2222


🏁 Script executed:

# Check the package.json to understand the runtime target
head -30 package.json

Repository: flanksource/facet

Length of output: 827


🏁 Script executed:

# Look at the test file to understand how this module is used
cat -n cli/src/utils/package-manager.test.ts

Repository: flanksource/facet

Length of output: 5047


🏁 Script executed:

# Find all imports of resolvePackageManager
rg "resolvePackageManager|from.*package-manager" --type ts --type js

Repository: flanksource/facet

Length of output: 1809


🏁 Script executed:

# Check the CLI entry point and its configuration
cat -n cli/package.json 2>/dev/null || echo "No cli/package.json"

Repository: flanksource/facet

Length of output: 2980


🏁 Script executed:

# Look for any Node-based entry points or CLI scripts that might use this
rg "bin\|entry\|main" cli/ --type json 2>/dev/null | head -20

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check build configuration (tsconfig, vite, esbuild, etc.)
ls -la cli/ | grep -E "tsconfig|vite|esbuild|build"

Repository: flanksource/facet

Length of output: 340


🏁 Script executed:

# See if there's a shell.ts wrapper elsewhere or if they use a different approach
rg "\$\s*from" --type ts -B 2 -A 2 | head -30

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check remote-resolver.ts to see how resolvePackageManager is used
cat -n cli/src/utils/remote-resolver.ts | head -50

Repository: flanksource/facet

Length of output: 1816


🏁 Script executed:

# Check if there's any indication this package is meant to work with Node.js directly
rg "bun|node" cli/package.json -A 2 -B 2

Repository: flanksource/facet

Length of output: 989


🏁 Script executed:

# Look at the CLI entry point to understand the runtime context
cat -n cli/src/cli.ts | head -30

Repository: flanksource/facet

Length of output: 1484


The Bun import is real but the suggested fix references a non-existent file.

Line 11 does import $ from bun, and this is used in tryProbePnpmVersion() and tryCorepackEnablePnpm(). However, the issue is more nuanced:

  • The CLI is compiled with bun build --compile --target=bun into a standalone Bun binary (see cli/package.json), so the distributed dist/facet binary works correctly.
  • The CLI entry point (cli/src/cli.ts) has a Node.js shebang and package.json specifies engines: { node: >=20.19.0 }, suggesting Node.js compatibility intent.
  • The suggested fix imports from ./shell.js, which does not exist in the codebase.
  • remote-resolver.ts has the same Bun import on line 6, also affecting resolvePackageManager() usage there.

The underlying concern about Bun-specific dependencies is valid, but the fix is incomplete. Creating a working shell abstraction that supports both runtimes (or using Node's child_process conditionally) would be required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils/package-manager.ts` at line 11, The code currently imports the
Bun-specific `$` utility from 'bun' on line 11 of package-manager.ts and line 6
of remote-resolver.ts, making these modules incompatible with Node.js runtime
despite the CLI's Node.js shebang and engine requirements. Create a
runtime-agnostic shell abstraction module that conditionally uses Bun's `$` when
running under Bun or Node.js's `child_process` module when running under
Node.js, then update the imports in both package-manager.ts and
remote-resolver.ts to use this abstraction instead of directly importing from
'bun'. Apply the abstraction to all shell command executions within the
functions tryProbePnpmVersion(), tryCorepackEnablePnpm(),
resolvePackageManager(), and any other functions that invoke shell commands
through the `$` utility.
cli/tsdown.config.ts (1)

12-12: 🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Detect Bun-specific imports/APIs that would leak into a Node runtime
rg -nP '\bfrom\s+[\"\x27]bun[\"\x27]|\brequire\([\"\x27]bun[\"\x27]\)|\bBun\.' cli/src cli/test cli || true

Repository: flanksource/facet

Length of output: 1819


🏁 Script executed:

cat -n cli/tsdown.config.ts

Repository: flanksource/facet

Length of output: 636


🏁 Script executed:

# Check if there's a package.json that explains the project purpose
fd package.json cli -x cat {}

Repository: flanksource/facet

Length of output: 2364


🏁 Script executed:

# Check for any build or runtime configuration that indicates Node vs Bun
rg -l "bun|node" cli --type json --type ts --type js | head -20

Repository: flanksource/facet

Length of output: 603


🏁 Script executed:

cat -n cli/src/index.ts

Repository: flanksource/facet

Length of output: 519


🏁 Script executed:

# Also check the directory structure to understand separation
fd -t f '\.(ts|tsx)$' cli/src --max-depth 2 | grep -E '^cli/src/[^/]+\.ts$' | head -20

Repository: flanksource/facet

Length of output: 110


🏁 Script executed:

cat -n cli/src/server/preview.ts | head -60

Repository: flanksource/facet

Length of output: 3309


🏁 Script executed:

# Check if there's any conditional imports or Node alternatives
rg -B5 -A10 "Bun\.serve|startServer" cli/src/server/preview.ts

Repository: flanksource/facet

Length of output: 1121


🏁 Script executed:

rg -B2 -A15 "export.*startServer|export.*createServer" cli/src

Repository: flanksource/facet

Length of output: 2394


🏁 Script executed:

# Check if there's any documentation about server usage
fd -t f '\.(md|txt)' cli | head -10 | xargs grep -l "startServer\|server" 2>/dev/null || true

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check the actual exports to see what's documented/intended as public API
rg "export" cli/src/index.ts

Repository: flanksource/facet

Length of output: 470


🏁 Script executed:

# Check if 'bun' is available as an npm package or just the runtime
npm view bun --json 2>/dev/null | head -20 || echo "Checking package.json dependencies..."

Repository: flanksource/facet

Length of output: 402


🏁 Script executed:

# Check package.json to see if 'bun' is listed anywhere
rg "\"bun\"" cli/package.json

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Also check if there's any try-catch or platform detection around Bun usage
rg -B5 -A5 "typeof Bun|Bun\?" cli/src/server/preview.ts

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check if 'bun' is in package.json dependencies (not just devDependencies)
cat cli/package.json | grep -A50 "dependencies"

Repository: flanksource/facet

Length of output: 1075


🏁 Script executed:

# Look at how startServer is actually used - maybe it's only for Bun runtime
rg "startServer|createServer" cli/src/cli.ts -B2 -A5

Repository: flanksource/facet

Length of output: 384


🏁 Script executed:

# Check the README or docs to see if startServer is documented as public API
fd -t f '\.(md|rst)' cli

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Check what the CLI actually imports - is it using startServer from the library or locally?
rg "import.*startServer" cli/src/cli.ts

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Let's see what gets exported in the compiled output - check if there's a dist
ls -la cli/dist/ 2>/dev/null | head -20 || echo "No dist directory"

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Most important: check if this library is actually meant to be consumed by Node.js users
# Look at the main entry point in package.json
grep -A2 -B2 '"main"\|"type"' cli/package.json

Repository: flanksource/facet

Length of output: 441


🏁 Script executed:

# Check if startServer is documented/promoted as a public API in any README
find cli -name "README*" -type f -exec cat {} \; 2>/dev/null | head -100

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Final check: does the library build actually include startServer or is it tree-shaken?
# Look at what tsdown produces - check for any bundled output or source maps
find cli -path "*/dist/*" -name "*.mjs" -o -name "*.d.ts" | head -10

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Let's verify if bun module/types are actually available at build time
# The key question: if tsdown tries to resolve 'bun' as a module, would it fail?
rg "from.*['\"]bun['\"]|import.*bun" cli/src/server/preview.ts

Repository: flanksource/facet

Length of output: 43


🏁 Script executed:

# Final critical check: Does preview.ts have any fallback or Node.js server implementation?
cat -n cli/src/server/preview.ts | tail -80

Repository: flanksource/facet

Length of output: 3450


🏁 Script executed:

# Check if bun namespace is globally available without import, or if there's a type definition setup
rg "declare.*Bun|global.*Bun" cli/src

Repository: flanksource/facet

Length of output: 43


Reconsider the architectural approach for startServer export in a Node-targeted library.

The library publicly exports startServer (and implicitly createServer) which exclusively uses Bun.serve() with no fallback. Since this bundle targets Node.js (platform: 'node', target: 'node18'), exporting Bun-only APIs is fundamentally misaligned.

Removing 'bun' from external won't succeed either—bun is not in the package dependencies, so the bundler will fail to resolve it. The real issue is that these server functions should either:

  1. Have a Node.js implementation (e.g., using http module) alongside or instead of Bun.serve()
  2. Be removed from public exports and marked internal to CLI only
  3. Have the library target Bun explicitly instead of Node.js

The current configuration allows accidental imports but doesn't address the core design mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/tsdown.config.ts` at line 12, The library targets Node.js but exports
startServer and createServer that exclusively use Bun.serve() with no Node.js
fallback. This creates a fundamental design mismatch. You need to resolve this
by choosing one of three approaches: (1) implement Node.js-compatible versions
of startServer and createServer using the http module alongside the Bun
implementations, (2) remove these functions from the public exports and keep
them as internal utilities for CLI use only, or (3) change the build
configuration to target Bun explicitly instead of Node.js. Simply having 'bun'
in the external array does not resolve the core issue since bun is not listed as
a dependency and the bundler cannot resolve it anyway.

@yashmehrotra

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yashmehrotra yashmehrotra merged commit de81247 into main Jun 22, 2026
15 checks passed
@yashmehrotra yashmehrotra deleted the refactor/cli-bun-to-node branch June 22, 2026 06:04
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.

4 participants