Skip to content

build(desktop): stabilize Windows dev and distribution workflows#449

Open
PerishCode wants to merge 48 commits intomainfrom
feat/windows-distribution-smoke
Open

build(desktop): stabilize Windows dev and distribution workflows#449
PerishCode wants to merge 48 commits intomainfrom
feat/windows-distribution-smoke

Conversation

@PerishCode
Copy link
Copy Markdown
Contributor

@PerishCode PerishCode commented Mar 24, 2026

What

Stabilize the Windows desktop path end to end across local development, packaged distribution, CI validation, and installer UX, while continuing the desktop platform/lifecycle cleanup that supports those flows.

Why

This branch grew into the main Windows stabilization line for the desktop product. Before these changes, the Windows path still had gaps in several places: local startup could be brittle, packaged runtime behavior still depended on platform-specific assumptions, Windows packaging was only partially covered in CI, and repeat installer runs did not clearly communicate upgrade, reinstall, or downgrade behavior. In parallel, parts of the desktop runtime lifecycle were still too spread across launchd- and platform-specific codepaths, which made the Windows work harder to extend safely.

How

  • stabilize Windows local desktop startup and packaged runtime behavior
    • harden Windows startup and runtime install flows
    • fix packaged runtime startup behavior and Windows-specific shell polish
    • refine desktop web layout details needed for Windows usability
  • add and refine Windows distribution support
    • add the dist:win packaging flow and smoke-distribution support
    • move Windows sidecar archives to zip-compatible handling
    • align Windows packaging with NSIS defaults and improve installer UX
    • restore packaged executable resource editing so the Nexu icon is written correctly
    • clean Windows release intermediates after packaging
  • strengthen repeat-install behavior in the Windows installer
    • detect a running app before install continues
    • confirm upgrade and same-version reinstall flows
    • block downgrades by default
  • extend CI coverage for packaged Windows artifacts
    • add a Windows packaging job to desktop-ci-dist-full
    • verify installer output, unpacked app output, and OpenClaw archive metadata
    • default CI desktop artifacts to auto-update disabled, with a manual override when that behavior needs to be exercised
  • improve local-dev tooling and cross-platform desktop orchestration
    • streamline the scripts/dev flow and unify desktop dev launch under scripts/dev
    • unify scripts/dev logging while preserving the HMR-first control surface
  • continue desktop platform/lifecycle cleanup that supports the new flows
    • stabilize launchd encapsulation and desktop platform boundaries
    • move more platform/runtime responsibilities behind explicit platform capability layers
  • add smoke/debug support for adjacent desktop runtime issues
    • add the standalone Feishu websocket smoke harness and improve its diagnostics

Affected areas

  • Desktop app (Electron shell)
  • Controller (backend / API)
  • Web dashboard (React UI)
  • OpenClaw runtime
  • Skills
  • Shared schemas / packages
  • Build / CI / Tooling

Checklist

  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes
  • pnpm generate-types run (if API routes/schemas changed)
  • No credentials or tokens in code or logs
  • No any types introduced (use unknown with narrowing)

Notes for reviewers

  • This is a long-lived branch, so the PR intentionally covers the full Windows dev/distribution stabilization line plus the platform/lifecycle cleanup merged into the branch along the way.
  • Desktop CI Dist Full was manually triggered and passed with the added Windows packaging job.
  • pnpm --filter @nexu/desktop dist:win was rerun locally after the latest icon, installer, and release-cleanup changes.
  • Manual Windows validation covered local startup, packaged install/run/uninstall, icon behavior, and repeat-install prompts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

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

Walkthrough

Migrates desktop dev tooling from Bash to a Node.js CLI, makes runtime manifest creation asynchronous, adds archive-based sidecar extraction (tar.gz and zip) with cache fingerprinting and Windows symlink fallbacks, introduces Windows packaging and smoke-test tooling, and adds EOL normalization via .gitattributes.

Changes

Cohort / File(s) Summary
Git Configuration
\.gitattributes
Adds EOL normalization: * text=auto eol=lf and forces CRLF for Windows script patterns (*.bat, *.cmd, *.ps1).
Desktop CLI & Launcher
apps/desktop/dev.sh, apps/desktop/scripts/dev-cli.mjs, package.json, scripts/dev.mjs
Replaces Bash dev orchestration with a Node CLI (dev-cli.mjs) and thin dev.sh wrapper; adds single-instance locking, session state, start/stop/reset/status/logs/control commands, and updates npm scripts to call the new CLI.
Runtime Init & Manifests
apps/desktop/main/index.ts, apps/desktop/main/runtime/manifests.ts
Makes createRuntimeUnitManifests async and awaited; adds archive metadata handling and async extraction for packaged OpenClaw sidecars (tar.gz or zip) with zip path-traversal protection and permission fixing.
Sidecar Prep & Path Handling
apps/desktop/scripts/prepare-openclaw-sidecar.mjs, apps/desktop/scripts/lib/sidecar-paths.mjs, apps/desktop/scripts/prepare-runtime-sidecars.mjs
Adds SHA‑256 prepare-cache for OpenClaw sidecar, switches payload archives to zip (yazl), implements Windows symlink→copy fallbacks, copyDirectoryTree helper, timed-step logging, and Windows-aware pnpm invocation.
Build & Distribution
apps/desktop/scripts/dist-win.mjs, apps/desktop/package.json, apps/desktop/scripts/prepare-controller-sidecar.mjs, apps/desktop/scripts/prepare-web-sidecar.mjs
Adds Windows distribution script and electron-builder Windows config; adds yauzl/yazl deps; introduces timed-step wrappers around prepare scripts and dereferences pnpm sharp symlinks for packaging.
OpenClaw Runtime & Installer
openclaw-runtime/install-runtime.mjs, openclaw-runtime/postinstall.mjs, openclaw-runtime/package.json, apps/desktop/build/installer.nsh
Adds install-runtime.mjs with full/pruned modes and integrates it into postinstall scripts; adds NSIS installer macros for init/uninstall async cleanup and tombstone handling.
Runtime Scripts & Cleanup
scripts/clean-runtime-plugin-installs.mjs, scripts/setup-git-hooks.mjs
Adds cleanup script for nested runtime plugin installs and a Node-based git-hook installer (copies and chmods pre-commit hook).
CI / Checks / Packaging Checks
.github/workflows/.../desktop-ci-*.yml, scripts/desktop-check-dev.mjs, scripts/desktop-check-dist.mjs, scripts/desktop-ci-check.mjs
Expands CI to macOS+Windows matrix, switches steps to PowerShell on Windows, adds platform-aware packaged app checks, and replaces tmux-based checks with file/state-based liveness and Windows port detection.
Smoke Tests
smoke/feishu-ws-smoke.mjs, smoke/package.json, smoke/Dockerfile, smoke/README.md
Adds Feishu WebSocket smoke test script, smoke package manifest, Dockerfile, and README with usage and env/flag options.
Docs & Guides
AGENTS.md, specs/guides/desktop-runtime-guide.md
Updates desktop local-dev guidance to use pnpm start / dev-cli, documents prepare-cache paths/escape hatches, expands reset-state/troubleshooting notes, and points detailed rules to the runtime guide.
Bootstrap / Packaged Paths
apps/desktop/main/bootstrap.ts
Adjusts packaged user-data path selection on Windows, adds migration from legacy path to new standard path, creates nexuHome, and logs additional path info.
Miscellaneous
openclaw-runtime/postinstall-cache.mjs, openclaw-runtime/postinstall.mjs, apps/desktop/scripts/lib/sidecar-paths.mjs (exports)
Small updates: include install-runtime.mjs in fingerprint inputs and export new copyDirectoryTree(...) helper used by sidecar prep.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as dev-cli.mjs
    participant Lock as Lock Manager
    participant Cache as Sidecar Cache
    participant Extract as Archive Extractor
    participant Build as Build Pipeline
    participant Electron as Electron Process

    User->>CLI: start
    CLI->>Lock: acquire single-instance lock
    Lock-->>CLI: lock obtained
    CLI->>Cache: check sidecar cache fingerprint
    alt cache hit
        Cache-->>CLI: cached sidecar usable
    else cache miss
        CLI->>Extract: extract packaged sidecar (tar.gz or zip)
        Extract-->>CLI: sidecar prepared
        CLI->>Cache: write cache metadata
    end
    CLI->>Build: check/run pnpm build tasks (unless reuse)
    Build-->>CLI: build artifacts ready
    CLI->>Electron: spawn detached electron
    Electron-->>CLI: PID recorded
    CLI->>Lock: persist session state and release lock
    CLI-->>User: started (PID/logs)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • lefarcen
  • PerishCode

Poem

🐰 Hops and bits, a nimble cheer,

From Bash to Node the path grows clear.
Sidecars zipped and tarred with care,
Windows copies when links won't fare—
A tiny hop for every gear!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes Windows smoke distribution support, but the changeset includes far broader modifications: a complete desktop local-dev startup flow replacement with Node launcher, cross-platform improvements, startup diagnostics, cache reuse, reset behavior, log sanitizing, and a Feishu websocket smoke harness. The title is overly narrow for the actual scope. Revise title to reflect broader changes: e.g., 'feat(desktop): refactor startup flow to Node-based launcher with Windows support and smoke tests' or similar.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description adequately summarizes key changes (Node launcher replacement, Windows distribution, sidecar handling, zip extraction, Feishu smoke), covers testing performed, and matches the template structure with summary and testing sections. Some template sections are unused but not required.

✏️ 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 feat/windows-distribution-smoke

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd6ed36760

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/scripts/prepare-openclaw-sidecar.mjs (1)

990-1016: ⚠️ Potential issue | 🟠 Major

Give openclaw.cmd the same bundled-runner fallback as the POSIX launcher.

The Windows wrapper always executes node ..., while the shell launcher below falls back to OPENCLAW_ELECTRON_EXECUTABLE. If PATH does not expose the expected Node binary, dist:win hard-fails even though the packaged app already has a bundled runner available.

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

In `@apps/desktop/scripts/prepare-openclaw-sidecar.mjs` around lines 990 - 1016,
Update the Windows wrapper written by the writeFile call that creates
"openclaw.cmd" so it mirrors the POSIX wrapper fallback: instead of
unconditionally executing node with packagedOpenclawEntry, first check for node
on PATH (e.g. using where/node check) and exec node if found; otherwise, if the
OPENCLAW_ELECTRON_EXECUTABLE env var is set and points to an executable, run it
with ELECTRON_RUN_AS_NODE=1 and the same packagedOpenclawEntry and arguments as
the POSIX launcher. Keep the rest of the batch wrapper logic consistent with the
POSIX script (use packagedOpenclawEntry and preserve %* argument passthrough).
🧹 Nitpick comments (5)
apps/desktop/scripts/lib/sidecar-paths.mjs (1)

67-84: Consider logging when falling back to copy on Windows.

The Windows symlink failure fallback silently switches from symlink to copy semantics. While this works correctly for the current use cases, a log message would aid debugging when the two behaviors diverge (symlinks reflect source mutations; copies are snapshots).

🔧 Optional: Add fallback logging
     } catch (error) {
       if (process.platform !== "win32") {
         throw error;
       }
 
+      console.log(
+        `[sidecar-paths] symlink failed, falling back to copy: ${sourcePath} -> ${targetPath}`,
+      );
       await cp(sourcePath, targetPath, {
         recursive: true,
         dereference: true,
         filter: (source) => basename(source) !== ".bin",
       });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/lib/sidecar-paths.mjs` around lines 67 - 84, The catch
block for symlink failure silently falls back to cp, so add a log entry before
performing the copy to indicate the symlink failed and we're using a copy
fallback; specifically, inside the catch(error) branch where process.platform
=== "win32" before calling cp(sourcePath, targetPath, ...), call the existing
logger (or process.stdout/processLogger) to record a message containing the
error and the sourcePath/targetPath and that a recursive copy will be used as
fallback for symlink in the symlink => cp fallback code path.
apps/desktop/scripts/prepare-runtime-sidecars.mjs (1)

14-23: Same argument quoting consideration as openclaw-runtime/postinstall.mjs.

The join(" ") pattern doesn't quote arguments. Current usage (["run", script]) is safe, but the pattern should be consistent across both files if one is updated.

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

In `@apps/desktop/scripts/prepare-runtime-sidecars.mjs` around lines 14 - 23, The
createCommandSpec function builds a Windows cmd.exe invocation using ["pnpm",
...args].join(" ") which doesn't quote individual args; update createCommandSpec
to safely quote or escape each argument when composing the single string for
cmd.exe (handle spaces and special chars), e.g. transform args into a properly
quoted string before passing it as the single /c argument, keeping the same
branch for command === "pnpm" || command === "pnpm.cmd" and leaving the
non-Windows return { command, args } unchanged.
openclaw-runtime/postinstall.mjs (1)

14-23: Arguments containing spaces or special characters would break the command.

The join(" ") approach on line 18 doesn't quote arguments. While current usage only passes simple flags (ci, --no-audit, etc.), this pattern is fragile if future callers pass arguments with spaces or shell metacharacters.

🛡️ Optional: Quote arguments for robustness
 function createCommandSpec(command, args) {
   if (process.platform === "win32" && (command === "npm" || command === "npm.cmd")) {
+    const quotedArgs = args.map((arg) =>
+      arg.includes(" ") || arg.includes('"') ? `"${arg.replace(/"/g, '\\"')}"` : arg
+    );
     return {
       command: "cmd.exe",
-      args: ["/d", "/s", "/c", ["npm", ...args].join(" ")],
+      args: ["/d", "/s", "/c", ["npm", ...quotedArgs].join(" ")],
     };
   }

   return { command, args };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-runtime/postinstall.mjs` around lines 14 - 23, The Windows branch in
createCommandSpec builds a single command string with ["npm", ...args].join(" ")
which fails for arguments containing spaces or shell metacharacters; change this
to safely escape and quote each argument before joining (e.g., replace the join
with args.map(escapeAndQuote).join(" ") where escapeAndQuote properly escapes
backslashes/quotes and wraps the arg in double-quotes when needed) so the
returned args array contains a single, robust cmd.exe /c string that preserves
argument boundaries.
smoke/Dockerfile (1)

1-5: Drop root for the smoke image.

This container only needs to run a Node script; keeping the default root user adds avoidable privilege and can leave root-owned files in the mounted workspace.

Possible fix
 FROM node:22-bookworm
 
 WORKDIR /workspace/smoke
+USER node
 
 CMD ["node", "./feishu-ws-smoke.mjs"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@smoke/Dockerfile` around lines 1 - 5, The Dockerfile currently runs as root
(base image node:22-bookworm) which is unnecessary; modify the Dockerfile to
create or switch to a non-root user and ensure WORKDIR /workspace/smoke is owned
by that user before switching: either use the existing unprivileged 'node' user
from the base image (add chown -R node:node /workspace/smoke and then USER node)
or add a dedicated user (e.g., addgroup/adduser, chown the WORKDIR, and then
USER smoke); keep the existing CMD ["node", "./feishu-ws-smoke.mjs"] unchanged
but ensure the node binary and script are executable by the non-root user.
apps/desktop/scripts/dist-win.mjs (1)

184-236: Consider adding error handling for the main orchestration.

The main() function runs multiple sequential build steps but has no top-level try/catch. If an early step fails, the error message from run() will propagate, but adding explicit error context could improve debugging for Windows-specific failures.

💡 Optional: Add top-level error handling with context
 async function main() {
+  const startTime = Date.now();
   const env = {
     ...process.env,
     NEXU_WORKSPACE_ROOT: repoRoot,
   };
   // ... rest of function ...
-  await runElectronBuilder(
-    [
-      "--win",
-      // ...
-    ],
-    {
-      cwd: electronRoot,
-      env: {
-        ...env,
-        CSC_IDENTITY_AUTO_DISCOVERY: "false",
-      },
-    },
-  );
+  await runElectronBuilder(
+    [
+      "--win",
+      // ...
+    ],
+    {
+      cwd: electronRoot,
+      env: {
+        ...env,
+        CSC_IDENTITY_AUTO_DISCOVERY: "false",
+      },
+    },
+  );
+  console.log(`[dist:win] completed in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
 }
 
-await main();
+await main().catch((error) => {
+  console.error("[dist:win] build failed:", error.message);
+  process.exit(1);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dist-win.mjs` around lines 184 - 236, Wrap the
orchestration in main() with top-level try/catch (or add a try/catch around the
await calls when invoking main) to capture and log errors with Windows-specific
context before exiting non-zero; catch errors thrown by run(),
runElectronBuilder(), getElectronVersion(), execFileSync, etc., use a clear
processLogger/error console message that includes the step or
buildVersion/electronVersion context, and call process.exit(1) (or rethrow after
logging) to ensure failures produce an explicit, contextualized error and
non-zero exit code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/main/runtime/manifests.ts`:
- Around line 352-353: The branch that falls back to execFileSync("tar",
["-xzf", archivePath, "-C", tempExtractedSidecarRoot]) must be removed: when
archiveMetadata is missing or archiveMetadata.format === "tar.gz" replace the
PATH shell-out with an in-process extraction (e.g., require and call a JS tar
extractor such as the 'tar' module to extract archivePath into
tempExtractedSidecarRoot using streams and zlib) or, if we intentionally no
longer support legacy tar archives, throw a clear migration error referencing
archiveMetadata and archivePath; update the code paths around archiveMetadata,
execFileSync, and tempExtractedSidecarRoot accordingly so no PATH-dependent
external binary is invoked at startup.

In `@apps/desktop/scripts/dev-cli.mjs`:
- Around line 504-512: The cleanup currently kills whatever PID is in
state.electronPid and every PID returned by listListeningPids(defaultPorts);
change this to first validate each candidate PID before calling killPid by
checking its command line and/or working directory to ensure it belongs to this
workspace or is the expected electron binary. Specifically, when handling
state.electronPid (from readState) and each PID from
listListeningPids(defaultPorts), call a helper (e.g.,
getProcessCmdline/getProcessCwd) and only call killPid if the cmdline or cwd
contains the workspace identifier or matches the known electron executable name;
otherwise skip and log a warning so real port conflicts can surface (update the
logic around readState, state.electronPid, listListeningPids, and killPid to
perform this verification).
- Around line 679-689: Replace the manual file:// string assembly with
pathToFileURL to correctly encode Windows drive letters, spaces and UNC paths:
import pathToFileURL from the node 'url' module (e.g., add an ESM import for {
pathToFileURL }) and change the target in the control() function (the line that
currently sets target = `file://${join(appDir, "dist", "index.html")}`) to use
pathToFileURL(join(appDir, "dist", "index.html")).href (or .toString()) so the
spawn calls receive a correct, encoded file URL.
- Around line 26-35: createCommandSpec currently joins args into a single string
for cmd.exe which breaks when paths contain spaces; change it so on Windows you
either (A) quote each argument that contains spaces before joining (preserving
proper escaping) or preferably (B) avoid joining entirely and invoke the
resolved pnpm binary directly with an argument array to spawn/spawnSync.
Specifically, update createCommandSpec to resolve the pnpm binary
programmatically (use require.resolve or equivalent to locate the pnpm CLI
rather than relying on PATH) and return command set to that resolved binary and
args as an array (or, if sticking with cmd.exe, build a correctly quoted command
string by wrapping each arg with quotes when necessary) so that options like
"--dir" with rootDir/appDir containing spaces are passed intact. Ensure you
modify the branch that checks process.platform === "win32" and references "pnpm"
so it no longer constructs an unquoted "pnpm ...".

In `@apps/desktop/scripts/prepare-openclaw-sidecar.mjs`:
- Around line 472-495: The function currently waits for yazl's
ZipFile.outputStream to close but not for the destination write stream, which
can still be flushing; update createZipArchive (and the piping logic around
ZipFile.outputStream and createWriteStream) to capture the write stream (e.g.,
const writeStream = createWriteStream(archivePath)), pipe outputStream into
that, and wait for the write stream's 'finish' (and handle its 'error') before
resolving—i.e., replace or extend the existing completion promise that listens
on outputStream with listeners on the destination writeStream ('finish' and
'error') so the function only resolves once the file has been fully flushed to
disk.

In `@smoke/feishu-ws-smoke.mjs`:
- Around line 78-111: The argument parsing loop fails to check bounds and misses
a continue for the "--domain" flag; inside the function that iterates argv (the
for loop handling flags like "--reply", "--account", "--config", "--app-id",
"--app-secret", "--domain"), add a guard before reading argv[index + 1] (e.g.,
ensure index + 1 < argv.length) and handle the error case (return an error,
throw, or set a default) for each flag that consumes a value, and add the
missing continue after setting options.domain so control flow matches the other
flag cases.

---

Outside diff comments:
In `@apps/desktop/scripts/prepare-openclaw-sidecar.mjs`:
- Around line 990-1016: Update the Windows wrapper written by the writeFile call
that creates "openclaw.cmd" so it mirrors the POSIX wrapper fallback: instead of
unconditionally executing node with packagedOpenclawEntry, first check for node
on PATH (e.g. using where/node check) and exec node if found; otherwise, if the
OPENCLAW_ELECTRON_EXECUTABLE env var is set and points to an executable, run it
with ELECTRON_RUN_AS_NODE=1 and the same packagedOpenclawEntry and arguments as
the POSIX launcher. Keep the rest of the batch wrapper logic consistent with the
POSIX script (use packagedOpenclawEntry and preserve %* argument passthrough).

---

Nitpick comments:
In `@apps/desktop/scripts/dist-win.mjs`:
- Around line 184-236: Wrap the orchestration in main() with top-level try/catch
(or add a try/catch around the await calls when invoking main) to capture and
log errors with Windows-specific context before exiting non-zero; catch errors
thrown by run(), runElectronBuilder(), getElectronVersion(), execFileSync, etc.,
use a clear processLogger/error console message that includes the step or
buildVersion/electronVersion context, and call process.exit(1) (or rethrow after
logging) to ensure failures produce an explicit, contextualized error and
non-zero exit code.

In `@apps/desktop/scripts/lib/sidecar-paths.mjs`:
- Around line 67-84: The catch block for symlink failure silently falls back to
cp, so add a log entry before performing the copy to indicate the symlink failed
and we're using a copy fallback; specifically, inside the catch(error) branch
where process.platform === "win32" before calling cp(sourcePath, targetPath,
...), call the existing logger (or process.stdout/processLogger) to record a
message containing the error and the sourcePath/targetPath and that a recursive
copy will be used as fallback for symlink in the symlink => cp fallback code
path.

In `@apps/desktop/scripts/prepare-runtime-sidecars.mjs`:
- Around line 14-23: The createCommandSpec function builds a Windows cmd.exe
invocation using ["pnpm", ...args].join(" ") which doesn't quote individual
args; update createCommandSpec to safely quote or escape each argument when
composing the single string for cmd.exe (handle spaces and special chars), e.g.
transform args into a properly quoted string before passing it as the single /c
argument, keeping the same branch for command === "pnpm" || command ===
"pnpm.cmd" and leaving the non-Windows return { command, args } unchanged.

In `@openclaw-runtime/postinstall.mjs`:
- Around line 14-23: The Windows branch in createCommandSpec builds a single
command string with ["npm", ...args].join(" ") which fails for arguments
containing spaces or shell metacharacters; change this to safely escape and
quote each argument before joining (e.g., replace the join with
args.map(escapeAndQuote).join(" ") where escapeAndQuote properly escapes
backslashes/quotes and wraps the arg in double-quotes when needed) so the
returned args array contains a single, robust cmd.exe /c string that preserves
argument boundaries.

In `@smoke/Dockerfile`:
- Around line 1-5: The Dockerfile currently runs as root (base image
node:22-bookworm) which is unnecessary; modify the Dockerfile to create or
switch to a non-root user and ensure WORKDIR /workspace/smoke is owned by that
user before switching: either use the existing unprivileged 'node' user from the
base image (add chown -R node:node /workspace/smoke and then USER node) or add a
dedicated user (e.g., addgroup/adduser, chown the WORKDIR, and then USER smoke);
keep the existing CMD ["node", "./feishu-ws-smoke.mjs"] unchanged but ensure the
node binary and script are executable by the non-root user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d323e986-21b4-41fb-a3f1-737e00964bee

📥 Commits

Reviewing files that changed from the base of the PR and between 9c843a2 and dd6ed36.

⛔ Files ignored due to path filters (2)
  • openclaw-runtime/package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .gitattributes
  • AGENTS.md
  • apps/desktop/dev.sh
  • apps/desktop/main/index.ts
  • apps/desktop/main/runtime/manifests.ts
  • apps/desktop/package.json
  • apps/desktop/scripts/dev-cli.mjs
  • apps/desktop/scripts/dist-win.mjs
  • apps/desktop/scripts/lib/sidecar-paths.mjs
  • apps/desktop/scripts/prepare-controller-sidecar.mjs
  • apps/desktop/scripts/prepare-openclaw-sidecar.mjs
  • apps/desktop/scripts/prepare-runtime-sidecars.mjs
  • apps/desktop/scripts/prepare-web-sidecar.mjs
  • openclaw-runtime/postinstall.mjs
  • package.json
  • scripts/dev.mjs
  • scripts/setup-git-hooks.mjs
  • smoke/Dockerfile
  • smoke/README.md
  • smoke/feishu-ws-smoke.mjs
  • smoke/package.json
  • specs/guides/desktop-runtime-guide.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/main/bootstrap.ts`:
- Around line 93-101: Wrap the renameSync call in a try/catch to prevent an
unhandled exception from crashing startup on Windows: inside the existing
conditional that checks process.platform, overrideUserDataPath, userDataPath vs
legacyWindowsUserDataPath and path existence, change userDataPath from const to
let, attempt the renameSync(legacyWindowsUserDataPath, userDataPath) inside a
try block, and in the catch log the error (including error.code like
EBUSY/EPERM) and fall back to continuing startup without renaming or choose an
alternate recovery (e.g., leave legacy path untouched or set a different
userDataPath); ensure error handling references renameSync,
legacyWindowsUserDataPath, and userDataPath so the app no longer crashes on
rename failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8956844-9023-433e-b30f-93254a5b638b

📥 Commits

Reviewing files that changed from the base of the PR and between dd6ed36 and 70150a3.

📒 Files selected for processing (3)
  • apps/desktop/main/bootstrap.ts
  • apps/desktop/package.json
  • apps/desktop/scripts/dist-win.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/package.json
  • apps/desktop/scripts/dist-win.mjs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/scripts/dist-win.mjs (1)

14-23: Windows command arguments with spaces will break.

When repoRoot or other arguments contain spaces, joining args with a single space on Line 18 produces an incorrectly tokenized command. For example, --dir C:/Program Files/project becomes three separate arguments.

♻️ Proposed fix: Quote arguments containing spaces
 function createCommandSpec(command, args) {
   if (process.platform === "win32" && (command === "pnpm" || command === "pnpm.cmd")) {
+    const quotedArgs = args.map((arg) =>
+      /[\s&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg
+    );
     return {
       command: "cmd.exe",
-      args: ["/d", "/s", "/c", ["pnpm", ...args].join(" ")],
+      args: ["/d", "/s", "/c", ["pnpm", ...quotedArgs].join(" ")],
     };
   }

   return { command, args };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dist-win.mjs` around lines 14 - 23, In createCommandSpec
the PNPM command string is built with args.join(" "), which breaks when any arg
(like repoRoot) contains spaces; change the construction to safely quote/escape
individual arguments before joining so cmd.exe receives a single command string
with preserved arguments. Implement a helper (used in createCommandSpec) that
for each arg escapes embedded double quotes/backslashes and wraps the arg in
double quotes if it contains whitespace or special chars, then build args:
["/d","/s","/c", ["pnpm", ...args].map(quoteAndEscape).join(" ")]; this
preserves tokenization on Windows when invoking "cmd.exe".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/scripts/dist-win.mjs`:
- Around line 14-23: In createCommandSpec the PNPM command string is built with
args.join(" "), which breaks when any arg (like repoRoot) contains spaces;
change the construction to safely quote/escape individual arguments before
joining so cmd.exe receives a single command string with preserved arguments.
Implement a helper (used in createCommandSpec) that for each arg escapes
embedded double quotes/backslashes and wraps the arg in double quotes if it
contains whitespace or special chars, then build args: ["/d","/s","/c", ["pnpm",
...args].map(quoteAndEscape).join(" ")]; this preserves tokenization on Windows
when invoking "cmd.exe".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9d3f990-ef5a-4c18-becc-9269def6d6b7

📥 Commits

Reviewing files that changed from the base of the PR and between 70150a3 and 723e5d2.

📒 Files selected for processing (2)
  • apps/desktop/package.json
  • apps/desktop/scripts/dist-win.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/package.json

Switch the NSIS flow to an assisted installer so Windows install and uninstall are visible and data cleanup can be opted into without silent surprises. Restore executable resource editing and use tombstone-based app-data cleanup so shortcuts show the Nexu icon and uninstall stays fast without flashing a console window.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/build/installer.nsh`:
- Around line 53-56: The RunOnce registry value name currently uses only
GetTickCount() (System::Call 'kernel32::GetTickCount() i .r1') which can collide
when multiple tombstones are enqueued (written via WriteRegStr with
NEXU_RUNONCE_VALUE_PREFIX and NEXU_CLEANUP_SCRIPT), causing later writes to
overwrite earlier ones; change the value name generation in both the
single-write block (where GetTickCount() is used) and the multi-enqueue block
(the code handling multiple tombstones) to include a unique suffix per
tombstone—either append the tombstone basename (derived from the tombstone
path/argument like $1) or append an incrementing counter variable that you
increment for each WriteRegStr call—so each WriteRegStr uses
NEXU_RUNONCE_VALUE_PREFIX + uniqueSuffix to avoid collisions.
- Line 8: The installer currently writes and executes a predictable helper at
NEXU_CLEANUP_SCRIPT in %TEMP% and uses GetTickCount() for RunOnce names,
allowing TOCTOU and name-collision replay; fix by removing the mutable .vbs
helper and instead schedule the cleanup inline (for example create a one-shot
delete command in the RunOnce value itself or spawn cmd.exe /c "timeout & del
..." from an installer-owned, non-world-writable location), or if a helper file
is unavoidable write it to a unique, non-temporary, single‑use path (include a
GUID/PID/timestamp in the filename) and mark it non-replayable; also replace
GetTickCount() as the RunOnce key with a cryptographically-unique identifier
(GUID) or include PID+high-resolution timestamp to ensure uniqueness and
one-shot behavior.

In `@apps/desktop/scripts/dist-win.mjs`:
- Around line 169-170: The console.log that prints the entire env-derived config
object (the line using console.log("[dist:win] generated build-config.json from
env:", JSON.stringify(config))) may leak secrets; change it to avoid printing
verbatim config values—use the existing configPath and either log only the
resolved file path (configPath) or a redacted summary such as a list of config
keys or masked values for sensitive keys before or after the await writeFile
call; update the log message referenced here so it no longer includes
JSON.stringify(config) but instead uses a safe summary (e.g., configPath and/or
redactedKeys) to preserve intent without exposing secrets.
- Around line 37-63: The current try/catch blocks around preparing sharp and
`@img` swallow all errors (logging "skipping ...") which can hide failures and
leave stale native binaries; update the error handling in the sharp dereference
logic (around lstat(sharpPath), realpath(sharpPath), rm/sharp cp) and the `@img`
copy logic (pnpmImgPath/sharpImgPath, lstat, rm, cp) to fail fast: after logging
the error include rethrowing the caught error (or simply remove the outer
try/catch so the exception propagates). Ensure you reference the same symbols
(sharpPath, pnpmImgPath, imgPath, lstat, realpath, rm, cp, rmWithRetriesOptions)
so the error bubbles up to packaging instead of silently continuing.
- Around line 14-19: In createCommandSpec, when building the cmd.exe /c command
for pnpm, the current args.join(" ") loses argument boundaries and breaks paths
with spaces; fix this by quoting/escaping each element in the args array before
joining (e.g., wrap each arg in quotes and escape any embedded quotes) so
arguments like --dir "C:\Users\Jane Doe\..." remain one token, or alternatively
avoid cmd.exe entirely and return command: "pnpm" with args: [...args] on
Windows so the spawn call can pass the argument array directly; update the logic
in createCommandSpec to perform one of these two safe strategies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0962f68b-d171-46ef-865b-41b1bba250c5

📥 Commits

Reviewing files that changed from the base of the PR and between 723e5d2 and ad65589.

📒 Files selected for processing (3)
  • apps/desktop/build/installer.nsh
  • apps/desktop/package.json
  • apps/desktop/scripts/dist-win.mjs

* build(desktop): skip exe editing for unsigned Windows dist

* fix(openclaw-runtime): stabilize Windows postinstall

Keep the full install path available for debugging while making pruned installs skip Windows-native optional dependencies that break postinstall flows.

* fix(desktop): align dev health check with runtime state

* fix(openclaw-runtime): refresh cache on installer changes

* build(desktop): add windows CI coverage

* fix(desktop): avoid sidecar copy symlink loops on windows

* fix(desktop): clean runtime plugin self-links on windows

* build(desktop): scope windows CI to build validation

* fix(desktop): restore mac dist executable discovery

---------

Co-authored-by: mRcfps <1402401442@qq.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
scripts/desktop-ci-check.mjs (1)

546-549: ⚠️ Potential issue | 🟡 Minor

Orphaned tmuxSession reference is dead code.

collectAppProcessResults() never sets tmuxSession on the returned object (it only returns mainProcess and auxiliaryProcess). This reference will always evaluate to true via optional chaining fallback.

🧹 Suggested cleanup
 function probesPassed(results, diagnostics) {
   return (
     results.portResults.every((entry) => entry.listening) &&
     results.apiReady.body.includes('"ready":true') &&
     results.webReady.body.includes('"ready":true') &&
     results.webSurface.body.includes('<div id="root"></div>') &&
     results.openclawHealth.ok &&
     results.browserControlListening &&
-    results.appProcessResults.mainProcess.alive &&
-    (results.appProcessResults.tmuxSession?.alive ?? true) &&
+    results.appProcessResults.mainProcess.alive &&
     diagnosticsChecksPassed(diagnostics)
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/desktop-ci-check.mjs` around lines 546 - 549, The condition
references a non-existent tmuxSession property on results.appProcessResults
(collectAppProcessResults only returns mainProcess and auxiliaryProcess), so
remove the orphaned "(results.appProcessResults.tmuxSession?.alive ?? true)"
check; if you intended to verify the auxiliary process instead, replace it with
"results.appProcessResults.auxiliaryProcess?.alive ?? true" so the code uses the
actual property returned by collectAppProcessResults and keeps the existing
mainProcess.alive and diagnosticsChecksPassed(diagnostics) checks.
♻️ Duplicate comments (6)
apps/desktop/scripts/dist-win.mjs (3)

199-204: ⚠️ Potential issue | 🟠 Major

Avoid logging build config verbatim—may leak DSN or credential-bearing URLs.

Line 202 logs the entire config object including NEXU_DESKTOP_SENTRY_DSN and NEXU_UPDATE_FEED_URL if present. This undercuts the log-sanitizing goal of the PR. As per coding guidelines, credentials must never appear in logs.

Suggested fix: log only the file path
   await writeFile(configPath, JSON.stringify(config, null, 2));
-  console.log(
-    "[dist:win] generated build-config.json from env:",
-    JSON.stringify(config),
-  );
+  console.log("[dist:win] generated build-config.json:", configPath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dist-win.mjs` around lines 199 - 204, The current
console.log prints the entire config object (variable config) which can expose
secrets like NEXU_DESKTOP_SENTRY_DSN and NEXU_UPDATE_FEED_URL; update the
logging in the block after writeFile(configPath, ...) so it does not emit the
full JSON: either log only the file path (configPath) or a sanitized version of
config that redacts DSNs/URLs (remove or mask keys NEXU_DESKTOP_SENTRY_DSN and
NEXU_UPDATE_FEED_URL) before stringifying; locate the console.log call in the
dist-win.mjs snippet and replace it accordingly.

41-76: ⚠️ Potential issue | 🟠 Major

Silently skipping sharp/@img failures may cause downstream packaging errors.

Both try/catch blocks log "skipping" for all errors, but the electron-builder config unconditionally bundles these paths as extraResources. A partial dereference leaves stale or missing binaries, causing cryptic failures later. Consider distinguishing ENOENT (acceptable to skip) from other errors (should fail fast).

Suggested fix: fail fast on non-ENOENT errors
+function isEnoent(error) {
+  return error && typeof error === "object" && "code" in error && error.code === "ENOENT";
+}
+
 async function dereferencePnpmSymlinks() {
   // ... sharp handling ...
   } catch (error) {
-    console.log(
-      `[dist:win] skipping sharp: ${error instanceof Error ? error.message : String(error)}`,
-    );
+    if (isEnoent(error)) {
+      console.log(`[dist:win] sharp not present; skipping dereference`);
+    } else {
+      throw new Error(`Failed to materialize sharp: ${error instanceof Error ? error.message : String(error)}`);
+    }
   }
   // ... similar for `@img` ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dist-win.mjs` around lines 41 - 76, The current
try/catch around dereferencing sharp (using lstat(sharpPath), realpath, rm, cp)
and the subsequent block that copies `@img` (using sharpImgPath, lstat, rm, cp)
swallow all errors; change both catch handlers to only silently ignore errors
whose error.code === 'ENOENT' and for any other error rethrow or exit (e.g.,
throw error or process.exit(1)) after logging the full error (include
error.stack or error.message) so failures other than "not found" fail fast and
surface useful diagnostics.

15-27: ⚠️ Potential issue | 🟠 Major

Windows argument quoting loses path boundaries.

The args.join(" ") on line 22 breaks when --dir paths contain spaces (e.g., C:\Users\Jane Doe\...). cmd.exe parses the joined string and splits at spaces. This is a build-time script, so it may work in CI environments, but will fail on developer machines with spaces in usernames.

Suggested fix: quote arguments containing spaces
 function createCommandSpec(command, args) {
   if (
     process.platform === "win32" &&
     (command === "pnpm" || command === "pnpm.cmd")
   ) {
+    const quotedArgs = args.map((arg) =>
+      arg.includes(" ") ? `"${arg}"` : arg
+    );
     return {
       command: "cmd.exe",
-      args: ["/d", "/s", "/c", ["pnpm", ...args].join(" ")],
+      args: ["/d", "/s", "/c", ["pnpm", ...quotedArgs].join(" ")],
     };
   }

   return { command, args };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dist-win.mjs` around lines 15 - 27, In
createCommandSpec, the current use of ["pnpm", ...args].join(" ") loses path
boundaries because args.join(" ") doesn't quote arguments with spaces; update
the logic that builds the cmd.exe invocation so each argument that contains
spaces or special cmd characters is wrapped in quotes (and inner quotes escaped)
before joining, i.e., map over args to quote them when necessary instead of a
raw args.join(" "), so the returned object for the pnpm branch still uses
command: "cmd.exe" and args: ["/d","/s","/c", quotedJoinedString].
apps/desktop/scripts/dev-cli.mjs (3)

770-781: ⚠️ Potential issue | 🟡 Minor

Use pathToFileURL() for correct Windows file URL construction.

file://${join(...)} produces invalid URLs on Windows—the drive letter is interpreted as a protocol and spaces are not encoded. Use pathToFileURL() from node:url which correctly handles drive letters, path separators, and percent-encoding.

Suggested fix
-import { fileURLToPath } from "node:url";
+import { fileURLToPath, pathToFileURL } from "node:url";
 async function control() {
-  const target = `file://${join(appDir, "dist", "index.html")}`;
+  const target = pathToFileURL(join(appDir, "dist", "index.html")).href;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dev-cli.mjs` around lines 770 - 781, The file URL is
being constructed manually in the control() function using
`file://${join(appDir, "dist", "index.html")}`, which produces invalid URLs on
Windows; import `pathToFileURL` from 'url' and replace that manual construction
with `pathToFileURL(join(appDir, "dist", "index.html")).toString()` (or .href)
to produce a correct, encoded file URL; update the top-level imports to include
`pathToFileURL` and use this new `target` variable in the existing spawn calls
inside control().

26-38: ⚠️ Potential issue | 🟠 Major

Windows argument quoting loses path boundaries.

Same issue as other files: args.join(" ") on line 33 breaks paths containing spaces. For pnpm --dir rootDir commands where rootDir may be C:\Users\Jane Doe\..., this causes command parsing failures.

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

In `@apps/desktop/scripts/dev-cli.mjs` around lines 26 - 38, The Windows branch in
createCommandSpec builds a single cmd.exe /c string with args.join(" "), which
loses path boundaries for args containing spaces (e.g., C:\Users\Jane Doe).
Update the pnpm branch in createCommandSpec to safely quote/escape each arg
before joining (e.g., map each arg to a quoted/escaped form like wrap in double
quotes and escape any internal quotes) so you produce ["/d","/s","/c", ["pnpm",
...args.map(escapeAndQuote)].join(" ")]. Ensure the escaping handles existing
quotes in args and use the same createCommandSpec function to locate the change.

547-563: ⚠️ Potential issue | 🟠 Major

Residual process cleanup may kill unrelated services.

killResidualProcesses terminates any process listening on ports 18789/50800/50810 and any PID stored in state.json, without verifying the process belongs to this workspace. A stale state file or an unrelated local service on these ports will be killed. Consider validating the process command line or working directory before terminating.

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

In `@apps/desktop/scripts/dev-cli.mjs` around lines 547 - 563,
killResidualProcesses currently kills any PID from state.json and any PID
returned by listListeningPids(defaultPorts) without confirming ownership; update
it to validate target processes before calling killPid by inspecting each PID's
command line or working directory (use a helper like
getProcessCommand/getProcessCwd or platform ps utilities) and only kill if the
cmd/cwd matches this workspace or clearly belongs to our app (e.g., contains
"electron" or the repo name). For the state.electronPid path (readState ->
state.electronPid) also verify the PID's cmd/cwd matches before killing, and if
a PID is stale/unrelated, avoid killing and consider cleaning state via
removeState only when safe. Ensure you update any helpers and callers
(killResidualProcesses, listListeningPids, killPid, readState, removeState,
defaultPorts) to perform these checks so unrelated services are not terminated.
🧹 Nitpick comments (5)
apps/desktop/scripts/lib/sidecar-paths.mjs (1)

147-156: Consider parallelizing entry processing for large directories.

The sequential await loop processes entries one at a time. For directories with many entries, this could be slow. Parallelizing with bounded concurrency could improve performance.

♻️ Optional: Parallel entry processing with Promise.all
-    for (const entry of entries) {
-      await copyDirectoryTree(
-        resolve(sourcePath, entry),
-        resolve(targetPath, entry),
-        {
-          ...options,
-          baseSourcePath: options.baseSourcePath ?? sourcePath,
-        },
-      );
-    }
+    await Promise.all(
+      entries.map((entry) =>
+        copyDirectoryTree(
+          resolve(sourcePath, entry),
+          resolve(targetPath, entry),
+          {
+            ...options,
+            baseSourcePath: options.baseSourcePath ?? sourcePath,
+          },
+        ),
+      ),
+    );

Note: Sequential processing is safer and may be intentional to avoid file system contention. Only apply if performance profiling shows this is a bottleneck.

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

In `@apps/desktop/scripts/lib/sidecar-paths.mjs` around lines 147 - 156, The
for-await loop in copyDirectoryTree that sequentially awaits copyDirectoryTree
for each entry (the block iterating over entries) should be changed to run
entries in parallel with bounded concurrency: create a concurrency limit (either
add a concurrency option to copyDirectoryTree or use a lightweight limiter like
p-limit/semaphore) and map entries to tasks calling
copyDirectoryTree(resolve(sourcePath, entry), resolve(targetPath, entry),
{...options, baseSourcePath: options.baseSourcePath ?? sourcePath}), then await
Promise.all on the limited tasks; ensure errors propagate and respect existing
behavior (preserve baseSourcePath handling) and avoid unbounded parallelism to
prevent filesystem contention.
.github/workflows/desktop-ci-dev.yml (2)

98-100: Windows step validates build pipeline but not runtime health.

The Windows step runs pnpm --filter @nexu/desktop build which exercises the build tooling but not the full runtime unit health check that macOS performs via pnpm check:dev. This is reasonable for initial Windows smoke testing—consider expanding to runtime checks once the Windows dev-cli flow is stable.

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

In @.github/workflows/desktop-ci-dev.yml around lines 98 - 100, The Windows
workflow step titled "Verify Windows desktop build pipeline" currently only runs
"pnpm --filter `@nexu/desktop` build"; update that step to also run the runtime
health check used on macOS by invoking "pnpm check:dev" (either run it after the
build or combine both commands) so the Windows job validates both the build and
the dev runtime health for the desktop package.

84-90: Consider separating toolchain version checks by platform.

The PowerShell block with embedded matrix.os comparison is functional but could be cleaner with separate platform-specific steps. However, this is a minor concern and the current approach works.

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

In @.github/workflows/desktop-ci-dev.yml around lines 84 - 90, The "Show desktop
toolchain versions" step mixes platform logic inside a pwsh run block using "${{
matrix.os }}"; split this into two clearer steps instead: keep the existing
"Show desktop toolchain versions" step to always run pnpm exec electron
--version (shell: pwsh), and add a separate conditional step (e.g., name "Show
tmux version on macOS") that runs tmux -V with an if: matrix.os == 'macos'
condition and shell: bash/pwsh as appropriate; reference the step name "Show
desktop toolchain versions" and the matrix variable "matrix.os" to locate where
to extract the platform-specific command.
scripts/desktop-check-dist.mjs (1)

34-46: Quoting pattern present but not exercised with path arguments.

The createCommandSpec function has the same args.join(" ") pattern, but in this file it's only used for taskkill and simple commands without user-provided paths. Low risk in current usage.

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

In `@scripts/desktop-check-dist.mjs` around lines 34 - 46, createCommandSpec
currently builds cmd.exe args by joining args with " " which can mis-handle path
arguments with spaces; update the branch that returns {command: "cmd.exe", args:
[...] } so that instead of ["pnpm", ...args].join(" ") you either (a) preserve
argument boundaries by passing each arg as a separate element to cmd.exe (e.g.,
args: ["/d","/s","/c","pnpm", ...args]) if the consumer supports it, or (b)
safely quote/escape each element (map args through a quoting/escaping helper)
before joining, ensuring functions like createCommandSpec, the pnpm branch, and
the cmd.exe arg construction handle path arguments containing spaces correctly.
openclaw-runtime/install-runtime.mjs (1)

9-21: Quoting pattern note for createCommandSpec.

The args.join(" ") pattern on line 16 can break if any argument contains spaces. For this script's usage (npm commands with flags like --omit=peer, --no-audit), this is safe since no user-provided paths are passed. However, this pattern is repeated across multiple files in this PR—consider extracting a shared utility if future use cases need path arguments.

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

In `@openclaw-runtime/install-runtime.mjs` around lines 9 - 21, The current
createCommandSpec function uses args.join(" ") which can break on arguments
containing spaces; replace that ad-hoc join with a shared safe-quoting/joining
utility (e.g., shellQuoteArgs or joinCommandArgs) and use it wherever
args.join(" ") appears across the PR so arguments are correctly escaped when
forming the cmd.exe command string; update createCommandSpec to call that
utility instead of args.join(" "), and add the new utility in a common module to
reuse it in other files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@scripts/desktop-ci-check.mjs`:
- Around line 546-549: The condition references a non-existent tmuxSession
property on results.appProcessResults (collectAppProcessResults only returns
mainProcess and auxiliaryProcess), so remove the orphaned
"(results.appProcessResults.tmuxSession?.alive ?? true)" check; if you intended
to verify the auxiliary process instead, replace it with
"results.appProcessResults.auxiliaryProcess?.alive ?? true" so the code uses the
actual property returned by collectAppProcessResults and keeps the existing
mainProcess.alive and diagnosticsChecksPassed(diagnostics) checks.

---

Duplicate comments:
In `@apps/desktop/scripts/dev-cli.mjs`:
- Around line 770-781: The file URL is being constructed manually in the
control() function using `file://${join(appDir, "dist", "index.html")}`, which
produces invalid URLs on Windows; import `pathToFileURL` from 'url' and replace
that manual construction with `pathToFileURL(join(appDir, "dist",
"index.html")).toString()` (or .href) to produce a correct, encoded file URL;
update the top-level imports to include `pathToFileURL` and use this new
`target` variable in the existing spawn calls inside control().
- Around line 26-38: The Windows branch in createCommandSpec builds a single
cmd.exe /c string with args.join(" "), which loses path boundaries for args
containing spaces (e.g., C:\Users\Jane Doe). Update the pnpm branch in
createCommandSpec to safely quote/escape each arg before joining (e.g., map each
arg to a quoted/escaped form like wrap in double quotes and escape any internal
quotes) so you produce ["/d","/s","/c", ["pnpm",
...args.map(escapeAndQuote)].join(" ")]. Ensure the escaping handles existing
quotes in args and use the same createCommandSpec function to locate the change.
- Around line 547-563: killResidualProcesses currently kills any PID from
state.json and any PID returned by listListeningPids(defaultPorts) without
confirming ownership; update it to validate target processes before calling
killPid by inspecting each PID's command line or working directory (use a helper
like getProcessCommand/getProcessCwd or platform ps utilities) and only kill if
the cmd/cwd matches this workspace or clearly belongs to our app (e.g., contains
"electron" or the repo name). For the state.electronPid path (readState ->
state.electronPid) also verify the PID's cmd/cwd matches before killing, and if
a PID is stale/unrelated, avoid killing and consider cleaning state via
removeState only when safe. Ensure you update any helpers and callers
(killResidualProcesses, listListeningPids, killPid, readState, removeState,
defaultPorts) to perform these checks so unrelated services are not terminated.

In `@apps/desktop/scripts/dist-win.mjs`:
- Around line 199-204: The current console.log prints the entire config object
(variable config) which can expose secrets like NEXU_DESKTOP_SENTRY_DSN and
NEXU_UPDATE_FEED_URL; update the logging in the block after
writeFile(configPath, ...) so it does not emit the full JSON: either log only
the file path (configPath) or a sanitized version of config that redacts
DSNs/URLs (remove or mask keys NEXU_DESKTOP_SENTRY_DSN and NEXU_UPDATE_FEED_URL)
before stringifying; locate the console.log call in the dist-win.mjs snippet and
replace it accordingly.
- Around line 41-76: The current try/catch around dereferencing sharp (using
lstat(sharpPath), realpath, rm, cp) and the subsequent block that copies `@img`
(using sharpImgPath, lstat, rm, cp) swallow all errors; change both catch
handlers to only silently ignore errors whose error.code === 'ENOENT' and for
any other error rethrow or exit (e.g., throw error or process.exit(1)) after
logging the full error (include error.stack or error.message) so failures other
than "not found" fail fast and surface useful diagnostics.
- Around line 15-27: In createCommandSpec, the current use of ["pnpm",
...args].join(" ") loses path boundaries because args.join(" ") doesn't quote
arguments with spaces; update the logic that builds the cmd.exe invocation so
each argument that contains spaces or special cmd characters is wrapped in
quotes (and inner quotes escaped) before joining, i.e., map over args to quote
them when necessary instead of a raw args.join(" "), so the returned object for
the pnpm branch still uses command: "cmd.exe" and args: ["/d","/s","/c",
quotedJoinedString].

---

Nitpick comments:
In @.github/workflows/desktop-ci-dev.yml:
- Around line 98-100: The Windows workflow step titled "Verify Windows desktop
build pipeline" currently only runs "pnpm --filter `@nexu/desktop` build"; update
that step to also run the runtime health check used on macOS by invoking "pnpm
check:dev" (either run it after the build or combine both commands) so the
Windows job validates both the build and the dev runtime health for the desktop
package.
- Around line 84-90: The "Show desktop toolchain versions" step mixes platform
logic inside a pwsh run block using "${{ matrix.os }}"; split this into two
clearer steps instead: keep the existing "Show desktop toolchain versions" step
to always run pnpm exec electron --version (shell: pwsh), and add a separate
conditional step (e.g., name "Show tmux version on macOS") that runs tmux -V
with an if: matrix.os == 'macos' condition and shell: bash/pwsh as appropriate;
reference the step name "Show desktop toolchain versions" and the matrix
variable "matrix.os" to locate where to extract the platform-specific command.

In `@apps/desktop/scripts/lib/sidecar-paths.mjs`:
- Around line 147-156: The for-await loop in copyDirectoryTree that sequentially
awaits copyDirectoryTree for each entry (the block iterating over entries)
should be changed to run entries in parallel with bounded concurrency: create a
concurrency limit (either add a concurrency option to copyDirectoryTree or use a
lightweight limiter like p-limit/semaphore) and map entries to tasks calling
copyDirectoryTree(resolve(sourcePath, entry), resolve(targetPath, entry),
{...options, baseSourcePath: options.baseSourcePath ?? sourcePath}), then await
Promise.all on the limited tasks; ensure errors propagate and respect existing
behavior (preserve baseSourcePath handling) and avoid unbounded parallelism to
prevent filesystem contention.

In `@openclaw-runtime/install-runtime.mjs`:
- Around line 9-21: The current createCommandSpec function uses args.join(" ")
which can break on arguments containing spaces; replace that ad-hoc join with a
shared safe-quoting/joining utility (e.g., shellQuoteArgs or joinCommandArgs)
and use it wherever args.join(" ") appears across the PR so arguments are
correctly escaped when forming the cmd.exe command string; update
createCommandSpec to call that utility instead of args.join(" "), and add the
new utility in a common module to reuse it in other files.

In `@scripts/desktop-check-dist.mjs`:
- Around line 34-46: createCommandSpec currently builds cmd.exe args by joining
args with " " which can mis-handle path arguments with spaces; update the branch
that returns {command: "cmd.exe", args: [...] } so that instead of ["pnpm",
...args].join(" ") you either (a) preserve argument boundaries by passing each
arg as a separate element to cmd.exe (e.g., args: ["/d","/s","/c","pnpm",
...args]) if the consumer supports it, or (b) safely quote/escape each element
(map args through a quoting/escaping helper) before joining, ensuring functions
like createCommandSpec, the pnpm branch, and the cmd.exe arg construction handle
path arguments containing spaces correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d37a11bb-ba0f-4fae-9bb8-728e36541544

📥 Commits

Reviewing files that changed from the base of the PR and between ad65589 and c96eecd.

📒 Files selected for processing (19)
  • .github/workflows/desktop-ci-dev.yml
  • .github/workflows/desktop-ci-dist.yml
  • apps/desktop/scripts/dev-cli.mjs
  • apps/desktop/scripts/dist-win.mjs
  • apps/desktop/scripts/lib/sidecar-paths.mjs
  • apps/desktop/scripts/prepare-controller-sidecar.mjs
  • apps/desktop/scripts/prepare-openclaw-sidecar.mjs
  • apps/desktop/scripts/prepare-runtime-sidecars.mjs
  • openclaw-runtime/README.md
  • openclaw-runtime/install-runtime.mjs
  • openclaw-runtime/package.json
  • openclaw-runtime/postinstall-cache.mjs
  • openclaw-runtime/postinstall.mjs
  • package.json
  • scripts/clean-runtime-plugin-installs.mjs
  • scripts/desktop-check-dev.mjs
  • scripts/desktop-check-dist.mjs
  • scripts/desktop-ci-check.mjs
  • smoke/feishu-ws-smoke.mjs
✅ Files skipped from review due to trivial changes (2)
  • openclaw-runtime/postinstall-cache.mjs
  • openclaw-runtime/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/desktop/scripts/prepare-runtime-sidecars.mjs
  • apps/desktop/scripts/prepare-controller-sidecar.mjs
  • smoke/feishu-ws-smoke.mjs
  • apps/desktop/scripts/prepare-openclaw-sidecar.mjs

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c96eecd4c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9887ae1
Status: ✅  Deploy successful!
Preview URL: https://7e25c78e.nexu-docs.pages.dev
Branch Preview URL: https://feat-windows-distribution-sm.nexu-docs.pages.dev

View logs

PerishCode and others added 7 commits March 28, 2026 20:03
* feat: add local dev supervisor workflow

* feat: refine local dev workflow and desktop runtime scaffolding

* docs: add dev workflow faq

* fix: remove nested controller tsx watcher

* refactor: share ensure guards across dev process helpers

* chore: remove stale task notes

* refactor: centralize local dev path resolution

* refactor: move dev orchestration into scripts/dev

Keep @nexu/dev-utils focused on atomic helpers so service-level controller and web flows stay easier to reason about and recover. Add lightweight session tracing so leaked dev processes can be correlated and cleaned up without heavy self-healing.

* refactor: clarify scripts dev module boundaries

* feat: externalize dev runtime ownership

* feat: split local dev into explicit service controls

* refactor: remove legacy desktop dev launchers

* fix: run desktop local dev through the Vite supervisor

* fix: harden local dev stack flow

* chore: sync workspace lockfiles

* fix: restore desktop dev auth session
…#651)

* chore: introduce shared desktop lifecycle contract

* chore: move desktop platform lifecycle behind adapters

* chore: centralize desktop platform compatibility

* chore: stage patched OpenClaw runtime for local dev

* chore: log staged OpenClaw runtime usage

* chore: speed up windows desktop build iteration

* chore: disable win executable editing for local builds

* chore: ignore local build cache
@PerishCode PerishCode changed the title feat(desktop): add Windows smoke distribution flow fix(desktop): harden windows packaging and installer flow Mar 30, 2026
@PerishCode PerishCode changed the title fix(desktop): harden windows packaging and installer flow build(desktop): stabilize Windows desktop dev and distribution flow Mar 30, 2026
@PerishCode PerishCode changed the title build(desktop): stabilize Windows desktop dev and distribution flow build(desktop): stabilize Windows dev and distribution workflows Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants