Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Detailed rules and learnings are in the `rules/` directory. Read the relevant fi
| [rules/git-workflow.md](rules/git-workflow.md) | Pushing branches, creating PRs, or dealing with fork/upstream remotes |
| [rules/base-ui-components.md](rules/base-ui-components.md) | Using TooltipTrigger, ToggleGroupItem, or other Base UI wrapper components |
| [rules/database-drizzle.md](rules/database-drizzle.md) | Modifying the database schema, generating migrations, or resolving migration conflicts |
| [rules/native-modules.md](rules/native-modules.md) | Adding Electron native modules or binaries that must survive Forge packaging/rebuild |
| [rules/typescript-strict-mode.md](rules/typescript-strict-mode.md) | Debugging type errors from `npm run ts` (tsgo) that pass normal tsc |
| [rules/openai-reasoning-models.md](rules/openai-reasoning-models.md) | Working with OpenAI reasoning model (o1/o3/o4-mini) conversation history |
| [rules/adding-settings.md](rules/adding-settings.md) | Adding a new user-facing setting or toggle to the Settings page |
Expand Down
13 changes: 11 additions & 2 deletions forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ const ignore = (file: string) => {
if (file.startsWith("/node_modules/better-sqlite3")) {
return false;
}
if (file.startsWith("/node_modules/node-pty")) {
return false;
}
if (file.startsWith("/node_modules/node-addon-api")) {
return false;
}
if (file.startsWith("/node_modules/bindings")) {
return false;
}
Expand Down Expand Up @@ -94,13 +100,16 @@ const config: ForgeConfig = {
appleIdPassword: process.env.APPLE_PASSWORD!,
teamId: process.env.APPLE_TEAM_ID!,
},
asar: true,
asar: {
// node-pty loads helper binaries like spawn-helper and winpty-agent from disk.
unpackDir: "node_modules/node-pty",
},
ignore,
extraResource: ["node_modules/dugite/git", "node_modules/@vscode"],
// ignore: [/node_modules\/(?!(better-sqlite3|bindings|file-uri-to-path)\/)/],
},
rebuildConfig: {
extraModules: ["better-sqlite3"],
extraModules: ["better-sqlite3", "node-pty"],
force: true,
},
makers: [
Expand Down
21 changes: 19 additions & 2 deletions package-lock.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "dyad",
"version": "0.42.0",
"version": "0.43.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.42.0",
"version": "0.43.0-beta.1",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46",
Expand Down Expand Up @@ -67,6 +67,7 @@
"lexical-beautiful-mentions": "^0.1.47",
"lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2",
"node-pty": "^1.1.0",
"perfect-freehand": "^1.2.2",
"posthog-js": "^1.265.1",
"react": "^19.2.4",
Expand Down Expand Up @@ -18288,6 +18289,12 @@
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-api-version": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
Expand Down Expand Up @@ -18339,6 +18346,16 @@
}
}
},
"node_modules/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.1.0"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"lexical-beautiful-mentions": "^0.1.47",
"lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2",
"node-pty": "^1.1.0",
"perfect-freehand": "^1.2.2",
"posthog-js": "^1.265.1",
"react": "^19.2.4",
Expand Down
1 change: 1 addition & 0 deletions rules/e2e-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ If this happens:

## Real Socket Firewall E2E tests

- If you change the add-dependency/socket-firewall command launch path (for example `spawn` vs PTY execution), proactively run `npm run e2e e2e-tests/socket_firewall.spec.ts` after `npm run build`. Unit tests and package builds do not cover the real packaged-Electron Socket Firewall flow.
- When exercising the real `sfw` binary in E2E, set fresh per-test `npm_config_cache`, `npm_config_store_dir`, and `pnpm_config_store_dir` in the launch hooks. Reused caches/stores can make Socket Firewall report that it did not detect package fetches, which turns blocked-package tests into false negatives.
- For real-path blocked-package coverage, prefer `axois` over `lodahs`. `lodahs` can resolve to `0.0.1-security` and install successfully under `pnpm`, so it does not reliably reach the blocked-package UI.

Expand Down
8 changes: 8 additions & 0 deletions rules/native-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Native Modules

Read this when adding Electron native dependencies such as `node-pty`, or any package that ships `.node` binaries, helper executables, or rebuild-time headers.

- This repo's `forge.config.ts` uses a deny-by-default `ignore` filter for most `node_modules` content. When adding a native dependency, explicitly allowlist the runtime package and any rebuild-time helper packages it requires (for example `node-addon-api`), or Electron Forge can fail during `Preparing native dependencies` with errors like `Cannot find module 'node-addon-api'`.
- Add native runtime packages to `vite.main.config.mts` `build.rollupOptions.external` so Vite does not bundle them into the main-process build.
- Add native runtime packages to `forge.config.ts` `rebuildConfig.extraModules` so Electron Forge rebuilds them against the packaged Electron version.
- If the package loads helper binaries from disk at runtime (for example `node-pty` loading `spawn-helper` or `winpty-agent` next to its native module), unpack the whole package directory with `packagerConfig.asar.unpackDir`; auto-unpacking `.node` files alone is not enough.
152 changes: 147 additions & 5 deletions src/ipc/processors/executeAddDependency.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
ADD_DEPENDENCY_INSTALL_TIMEOUT_MS,
CommandExecutionError,
SOCKET_FIREWALL_WARNING_MESSAGE,
} from "@/ipc/utils/socket_firewall";
Expand Down Expand Up @@ -95,14 +96,15 @@ describe("executeAddDependency", () => {
});
});

it("includes socket stderr verdict details when sfw blocks a dependency", async () => {
it("uses the most relevant combined PTY output line as the display summary", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: true,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "pnpm blocked",
stderr: "Socket Firewall blocked react\nPolicy: malware",
stdout:
"Progress: resolved 12, reused 0, downloaded 0, added 0\nSocket Firewall blocked react\nPolicy: malware",
exitCode: 1,
}),
);
Expand Down Expand Up @@ -130,14 +132,151 @@ describe("executeAddDependency", () => {
});
});

it("filters PTY progress noise out of expanded display details", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: false,
warningMessage: SOCKET_FIREWALL_WARNING_MESSAGE,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "npm install failed",
stdout: [
"Progress: resolved 1, reused 0, downloaded 0, added 0",
"npm warn deprecated left-pad@1.3.0: use String.prototype.padStart()",
"npm ERR! code ERESOLVE",
"npm ERR! ERESOLVE unable to resolve dependency tree",
"npm ERR! A complete log of this run can be found in:",
"npm ERR! /Users/me/.npm/_logs/2026-04-08-debug-0.log",
].join("\n"),
exitCode: 1,
}),
);

await expect(
executeAddDependency({
packages: ["react"],
message: {
id: 1,
content:
'<dyad-add-dependency packages="react"></dyad-add-dependency>',
} as any,
appPath: "/tmp/app",
}),
).rejects.toMatchObject({
displayDetails:
"npm ERR! code ERESOLVE\nnpm ERR! ERESOLVE unable to resolve dependency tree",
displaySummary: "npm ERR! ERESOLVE unable to resolve dependency tree",
warningMessages: [SOCKET_FIREWALL_WARNING_MESSAGE],
});
});

it("falls back to the error message when PTY output only contains progress noise", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: true,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "Command 'pnpm add react' was terminated by signal 15",
stdout: [
"Progress: resolved 50, reused 0, downloaded 0, added 0",
"Packages: +1",
].join("\n"),
exitCode: 0,
}),
);

await expect(
executeAddDependency({
packages: ["react"],
message: {
id: 1,
content:
'<dyad-add-dependency packages="react"></dyad-add-dependency>',
} as any,
appPath: "/tmp/app",
}),
).rejects.toMatchObject({
displayDetails: "Command 'pnpm add react' was terminated by signal 15",
displaySummary: "Command 'pnpm add react' was terminated by signal 15",
warningMessages: [],
});
});

it("ignores npm log-noise lines and keeps the actionable npm ERR summary", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: false,
warningMessage: SOCKET_FIREWALL_WARNING_MESSAGE,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "npm install failed",
stdout: [
"npm ERR! code ERESOLVE",
"npm ERR! ERESOLVE unable to resolve dependency tree",
"npm ERR! A complete log of this run can be found in:",
"npm ERR! /Users/me/.npm/_logs/2026-04-08-debug-0.log",
].join("\n"),
exitCode: 1,
}),
);

await expect(
executeAddDependency({
packages: ["react"],
message: {
id: 1,
content:
'<dyad-add-dependency packages="react"></dyad-add-dependency>',
} as any,
appPath: "/tmp/app",
}),
).rejects.toMatchObject({
displaySummary: "npm ERR! ERESOLVE unable to resolve dependency tree",
warningMessages: [SOCKET_FIREWALL_WARNING_MESSAGE],
});
});

it("keeps ERR_PNPM summaries instead of falling back to progress output", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: false,
warningMessage: SOCKET_FIREWALL_WARNING_MESSAGE,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "pnpm add failed",
stdout: [
"Progress: resolved 1, reused 0, downloaded 0, added 0",
"ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/react: Not Found",
].join("\n"),
exitCode: 1,
}),
);

await expect(
executeAddDependency({
packages: ["react"],
message: {
id: 1,
content:
'<dyad-add-dependency packages="react"></dyad-add-dependency>',
} as any,
appPath: "/tmp/app",
}),
).rejects.toMatchObject({
displaySummary:
"ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/react: Not Found",
warningMessages: [SOCKET_FIREWALL_WARNING_MESSAGE],
});
});

it("does not fall back to a direct install when the real sfw cli blocks a dependency", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({
available: true,
});
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "pnpm blocked",
stderr:
stdout:
" - blocked npm package: name: axois; version: 0.0.1-security; reason: malware (critical)",
exitCode: 1,
}),
Expand Down Expand Up @@ -169,7 +308,7 @@ describe("executeAddDependency", () => {
runCommandMock.mockRejectedValueOnce(
new CommandExecutionError({
message: "sfw pnpm failed",
stderr: "Socket Firewall timed out",
stdout: "Socket Firewall timed out",
exitCode: 1,
}),
);
Expand Down Expand Up @@ -214,7 +353,10 @@ describe("executeAddDependency", () => {
expect(runCommandMock).toHaveBeenCalledWith(
"npm",
["install", "--legacy-peer-deps", "react"],
{ cwd: "/tmp/app" },
{
cwd: "/tmp/app",
timeoutMs: ADD_DEPENDENCY_INSTALL_TIMEOUT_MS,
},
);
expect(runCommandMock).toHaveBeenCalledTimes(1);
expect(result).toMatchObject({
Expand Down
Loading
Loading