From da3620dcf771573672d3291e5f6b525e901c225d Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:29:17 -0700 Subject: [PATCH] fix(desktop): ship the pstack pack outside app.asar so packaged builds keep every file electron-builder drops .gitignore and bun.lock from app.asar, so the packaged server could not materialize the pack and pstack never appeared. Ship the pack as an extra resource the desktop points the server at, and leave upstream's .gitignore out of the delivered pack since npm strips it too. Co-Authored-By: Claude Opus 5.5 --- .../DesktopBackendConfiguration.test.ts | 45 ++++++++++ .../backend/DesktopBackendConfiguration.ts | 16 +++- .../src/wsl/DesktopWslServerTree.test.ts | 26 ++++++ apps/desktop/src/wsl/DesktopWslServerTree.ts | 4 + apps/server/src/config.ts | 5 ++ apps/server/src/pstack/PstackPack.test.ts | 62 +++++++++++++ apps/server/src/pstack/PstackPack.testkit.ts | 4 + packages/pstack/src/check.test.ts | 23 +++++ packages/pstack/src/index.ts | 16 ++-- packages/shared/package.json | 4 + packages/shared/src/pstack.ts | 1 + scripts/build-desktop-artifact.test.ts | 90 +++++++++++++++++++ scripts/build-desktop-artifact.ts | 37 ++++++-- 13 files changed, 321 insertions(+), 12 deletions(-) create mode 100644 packages/shared/src/pstack.ts diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index 7464d8fbe9e2f..55fa1e9109ead 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -218,6 +218,51 @@ const withPackagedWslHarness = ( }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)); describe("DesktopBackendConfiguration", () => { + it.effect.each( + (["darwin", "linux", "win32"] as const).flatMap((platform) => + (["packaged", "missing", "file", "development"] as const).map((source) => ({ + platform, + source, + })), + ), + )( + "passes the external pstack source only for a packaged directory ($platform, $source)", + ({ platform, source }) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pstack-config-" }); + const sourceRoot = path.join(baseDir, "pstack"); + if (source === "file") yield* fs.writeFileString(sourceRoot, "not a directory"); + else if (source !== "missing") yield* fs.makeDirectory(sourceRoot); + yield* Effect.gen(function* () { + const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; + const config = yield* configuration.resolvePrimary; + assert.equal( + config.env?.T3CODE_PSTACK_SOURCE_ROOT, + source === "packaged" ? sourceRoot : undefined, + ); + }).pipe( + Effect.provide( + layerBackendConfiguration().pipe( + Layer.provideMerge(layerServerExposure), + Layer.provideMerge(DesktopAppSettings.layerTest()), + Layer.provideMerge(DesktopWslServerTree.layerTest()), + Layer.provideMerge(DesktopWslEnvironment.layerTest()), + Layer.provideMerge( + layerEnvironment(baseDir, { + platform, + resourcesPath: baseDir, + isPackaged: source !== "development", + ...(source === "development" ? { devServerUrl: "http://localhost:5173" } : {}), + }), + ), + ), + ), + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + it.effect("resolvePrimary produces a stable scoped bootstrap token", () => withHarness( Effect.gen(function* () { diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index ca69f5d025899..dd15cf6a3615c 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -2,6 +2,7 @@ import * as NodeOS from "node:os"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken"; +import { PSTACK_SOURCE_ROOT_ENV } from "@t3tools/shared/pstack"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; @@ -490,11 +491,22 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv ): Effect.fn.Return< DesktopBackendManager.DesktopBackendStartConfig, never, - DesktopEnvironment.DesktopEnvironment | DesktopServerExposure.DesktopServerExposure + | DesktopEnvironment.DesktopEnvironment + | DesktopServerExposure.DesktopServerExposure + | FileSystem.FileSystem > { const environment = yield* DesktopEnvironment.DesktopEnvironment; const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const backendExposure = yield* serverExposure.backendConfig; + const fs = yield* FileSystem.FileSystem; + const pstackSourceRoot = environment.path.join(environment.resourcesPath, "pstack"); + const hasPstackSource = + environment.isPackaged && + !environment.isDevelopment && + (yield* fs.stat(pstackSourceRoot).pipe( + Effect.map((stat) => stat.type === "Directory"), + Effect.orElseSucceed(() => false), + )); const bootstrap = { mode: "desktop" as const, @@ -533,6 +545,7 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv ...backendChildEnvPatch(), ELECTRON_RUN_AS_NODE: "1", T3CODE_CLI_PATH: Option.getOrUndefined(input.cliPath), + [PSTACK_SOURCE_ROOT_ENV]: hasPstackSource ? pstackSourceRoot : undefined, }, extendEnv: true, bootstrap, @@ -791,6 +804,7 @@ export const make = Effect.gen(function* () { }).pipe( Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment), Effect.provideService(DesktopServerExposure.DesktopServerExposure, serverExposure), + Effect.provideService(FileSystem.FileSystem, fileSystem), ); }); diff --git a/apps/desktop/src/wsl/DesktopWslServerTree.test.ts b/apps/desktop/src/wsl/DesktopWslServerTree.test.ts index 7eaf362a326aa..68442ba874f58 100644 --- a/apps/desktop/src/wsl/DesktopWslServerTree.test.ts +++ b/apps/desktop/src/wsl/DesktopWslServerTree.test.ts @@ -71,6 +71,32 @@ const ensureWith = (input: { ); describe("DesktopWslServerTree", () => { + it.effect("includes the external pstack pack in the extracted WSL server tree", () => + withTempDir((tempDir) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const resourcesPath = path.join(tempDir, "resources"); + yield* fs.makeDirectory(path.join(resourcesPath, "server.asar/apps/server/dist"), { + recursive: true, + }); + const lockfile = "pstack/upstream/skills/poteto-mode/scripts/bun.lock"; + yield* fs.makeDirectory(path.dirname(path.join(resourcesPath, lockfile)), { + recursive: true, + }); + yield* fs.writeFileString(path.join(resourcesPath, lockfile), "lockfile\n"); + const result = yield* ensureWith({ baseDir: tempDir, resourcesPath }); + assert.isTrue(result.ok); + assert.equal( + yield* fs.readFileString( + path.join(result.ok ? result.root : "", "apps/server/dist", lockfile), + ), + "lockfile\n", + ); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("bounds entry work across an eight-way nested tree", () => Effect.gen(function* () { const active = yield* Ref.make(0); diff --git a/apps/desktop/src/wsl/DesktopWslServerTree.ts b/apps/desktop/src/wsl/DesktopWslServerTree.ts index 4107527284379..cb1f5baff8c01 100644 --- a/apps/desktop/src/wsl/DesktopWslServerTree.ts +++ b/apps/desktop/src/wsl/DesktopWslServerTree.ts @@ -130,6 +130,10 @@ export const make = Effect.gen(function* () { }); yield* Effect.gen(function* () { yield* copyTree(fs, join, serverRoot, partialDir); + const pstackSource = join(environment.resourcesPath, "pstack"); + if (yield* fs.exists(pstackSource)) { + yield* copyTree(fs, join, pstackSource, join(partialDir, "apps/server/dist/pstack")); + } const markerJson = yield* encodeMarker({ version }); yield* fs.writeFileString(join(partialDir, MARKER_FILE_NAME), `${markerJson}\n`); yield* fs.remove(versionDir, { recursive: true }).pipe(Effect.ignore); diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index f9004825fcd11..4bad1772a3a2d 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -1,4 +1,6 @@ import * as Context from "effect/Context"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import { PSTACK_SOURCE_ROOT_ENV } from "@t3tools/shared/pstack"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -283,6 +285,9 @@ export const resolvePstackSourceRoot = Effect.fn("ServerConfig.resolvePstackSour function* () { const { join, resolve } = yield* Path.Path; const fs = yield* FileSystem.FileSystem; + const environment = yield* HostProcess.Environment; + const override = environment[PSTACK_SOURCE_ROOT_ENV]; + if (override && (yield* fs.exists(join(override, "UPSTREAM.json")))) return resolve(override); const bundled = resolve(join(import.meta.dirname, "pstack")); if (yield* fs.exists(join(bundled, "UPSTREAM.json"))) return bundled; return resolve(join(import.meta.dirname, "../../../packages/pstack")); diff --git a/apps/server/src/pstack/PstackPack.test.ts b/apps/server/src/pstack/PstackPack.test.ts index 857f5c9e81a53..1f9ebeb2d88d9 100644 --- a/apps/server/src/pstack/PstackPack.test.ts +++ b/apps/server/src/pstack/PstackPack.test.ts @@ -1,4 +1,5 @@ import { resolvePstackSourceRoot } from "../config.ts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Layer from "effect/Layer"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; @@ -23,6 +24,67 @@ it.layer(NodeServices.layer)("PstackPack", (it) => { yield* resolvePstackSourceRoot(), path.resolve(import.meta.dirname, "../../../../packages/pstack"), ); + }).pipe(Effect.provideService(HostProcess.Environment, {})), + ); + + it.effect("prefers an existing external pstack source over the bundled source", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const fixture = yield* makeFixture(); + assert.equal( + yield* resolvePstackSourceRoot().pipe( + Effect.provideService(HostProcess.Environment, { + T3CODE_PSTACK_SOURCE_ROOT: fixture.sourceRoot, + }), + Effect.provideService(FileSystem.FileSystem, { + ...fs, + exists: () => Effect.succeed(true), + }), + ), + fixture.sourceRoot, + ); + }), + ); + + it.effect("falls back to the bundled source when the external source is missing", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const bundled = path.resolve(import.meta.dirname, "../pstack"); + assert.equal( + yield* resolvePstackSourceRoot().pipe( + Effect.provideService(HostProcess.Environment, { + T3CODE_PSTACK_SOURCE_ROOT: "/missing/pstack", + }), + Effect.provideService(FileSystem.FileSystem, { + ...fs, + exists: (file) => Effect.succeed(file === path.join(bundled, "UPSTREAM.json")), + }), + ), + bundled, + ); + }), + ); + + it.effect("materializes a published pack without gitignore metadata and preserves bun.lock", () => + Effect.gen(function* () { + const fixture = yield* makeFixture(); + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(`${fixture.sourceRoot}/upstream/.gitignore`); + yield* Effect.gen(function* () { + const catalog = yield* (yield* PstackPack.PstackPack).catalog; + assert.lengthOf(catalog!, 2); + const packDir = `${fixture.stateDir}/pstack/pack`; + assert.isFalse(yield* fs.exists(`${packDir}/.gitignore`)); + assert.equal( + yield* fs.readFileString(`${packDir}/skills/poteto-mode/scripts/bun.lock`), + "lockfile\n", + ); + assert.equal( + yield* fs.readFileString(`${packDir}/.t3-pstack-hash`), + pstackContentHash(fixture.sourceRoot), + ); + }).pipe(Effect.provide(fixture.layer)); }), ); diff --git a/apps/server/src/pstack/PstackPack.testkit.ts b/apps/server/src/pstack/PstackPack.testkit.ts index 428e74a320968..9d0d1451ecbd8 100644 --- a/apps/server/src/pstack/PstackPack.testkit.ts +++ b/apps/server/src/pstack/PstackPack.testkit.ts @@ -50,12 +50,16 @@ export const makeFixture = Effect.fn("PstackPackTest.makeFixture")(function* ( subtreeHash: "b".repeat(40), version: "test", files: Object.fromEntries([ + [".gitignore", { sha256: "c".repeat(64), executable: false }], + ["skills/poteto-mode/scripts/bun.lock", { sha256: "c".repeat(64), executable: false }], ["skills/architect/SKILL.md", { sha256: "c".repeat(64), executable: false }], ["skills/setup-pstack/SKILL.md", { sha256: "c".repeat(64), executable: false }], ["scripts/helper.sh", { sha256: "c".repeat(64), executable: true }], ]), }), "t3-code.md": "# T3 harness", + "upstream/.gitignore": "node_modules/\n", + "upstream/skills/poteto-mode/scripts/bun.lock": "lockfile\n", "upstream/skills/architect/SKILL.md": "---\nname: architect\ndescription: Design: a system.\ndisable-model-invocation: true\n---\nArchitect", "upstream/skills/setup-pstack/SKILL.md": diff --git a/packages/pstack/src/check.test.ts b/packages/pstack/src/check.test.ts index aaaa0e450bfd2..b8c547e2d670a 100644 --- a/packages/pstack/src/check.test.ts +++ b/packages/pstack/src/check.test.ts @@ -192,6 +192,29 @@ describe("checkPack", () => { }); describe("pack interface", () => { + it("omits gitignore metadata from delivery while verifying its upstream bytes", () => { + using pack = fixture(); + pack.write("upstream/.gitignore", "node_modules/\n"); + pack.write("upstream/skills/.gitignore", "local/\n"); + pack.write("upstream/skills/bun.lock", "lockfile\n"); + pack.seal(); + expect(checkPack(pack.root)).toEqual([]); + const paths = pstackPackEntries(pack.root).map(({ packPath }) => packPath); + expect(paths).not.toContain(".gitignore"); + expect(paths).not.toContain("skills/.gitignore"); + expect(paths).toContain("skills/bun.lock"); + pack.write("upstream/.gitignore", "changed\n"); + expect(checkPack(pack.root)).toContain("Hash mismatch: .gitignore"); + NodeFS.rmSync(NodePath.join(pack.root, "upstream/.gitignore")); + expect(checkPack(pack.root)).toContain("Missing upstream file: .gitignore"); + expect(() => pstackPackEntries(pack.root)).not.toThrow(); + }); + + it("invalidates the delivery stamp from the layout that included gitignore files", () => { + using pack = fixture(); + expect(pstackContentHash(pack.root)).not.toBe(pstackContentHash(pack.root, 1)); + }); + it("uses manifest executable flags despite archive modes and keeps T3 files non-executable", () => { using pack = fixture(); NodeFS.chmodSync(NodePath.join(pack.root, "upstream/helper.sh"), 0o644); diff --git a/packages/pstack/src/index.ts b/packages/pstack/src/index.ts index ddad7713abbae..705ce6d176caf 100644 --- a/packages/pstack/src/index.ts +++ b/packages/pstack/src/index.ts @@ -7,7 +7,9 @@ import { readManifest } from "./files.ts"; export { parsePstackRoleDefaults, type PstackRoleDefault } from "./roles.ts"; export const PSTACK_SKILL_PREFIX = "pstack:"; -export const PSTACK_LAYOUT_VERSION = 1; +export const PSTACK_LAYOUT_VERSION = 2; + +const isRuntimePackFile = (packPath: string) => NodePath.posix.basename(packPath) !== ".gitignore"; /** Files relative to a source root containing upstream/, t3-code.md, and UPSTREAM.json. */ export function pstackPackEntries(sourceRoot: string): ReadonlyArray<{ @@ -17,11 +19,13 @@ export function pstackPackEntries(sourceRoot: string): ReadonlyArray<{ }> { const root = NodePath.resolve(sourceRoot); return [ - ...Object.entries(readManifest(root).files).map(([packPath, { executable }]) => ({ - packPath, - sourcePath: NodePath.join(root, "upstream", packPath), - executable, - })), + ...Object.entries(readManifest(root).files) + .filter(([packPath]) => isRuntimePackFile(packPath)) + .map(([packPath, { executable }]) => ({ + packPath, + sourcePath: NodePath.join(root, "upstream", packPath), + executable, + })), { packPath: "docs/harness/t3-code.md", sourcePath: NodePath.join(root, "t3-code.md"), diff --git a/packages/shared/package.json b/packages/shared/package.json index 50d4aa1739ae4..8f97ac58de5b6 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -3,6 +3,10 @@ "private": true, "type": "module", "exports": { + "./pstack": { + "types": "./src/pstack.ts", + "import": "./src/pstack.ts" + }, "./changedFileKind": { "types": "./src/changedFileKind.ts", "import": "./src/changedFileKind.ts" diff --git a/packages/shared/src/pstack.ts b/packages/shared/src/pstack.ts new file mode 100644 index 0000000000000..50de500f50a43 --- /dev/null +++ b/packages/shared/src/pstack.ts @@ -0,0 +1 @@ +export const PSTACK_SOURCE_ROOT_ENV = "T3CODE_PSTACK_SOURCE_ROOT"; diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 862a5d29dea23..d88c860dd11ae 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -29,6 +29,8 @@ import { readWorkspaceConfig, createBuildConfig, DESKTOP_FILE_EXCLUSIONS, + DESKTOP_EXTRA_RESOURCES, + stageDesktopServerDist, InvalidMacPasskeyRpDomainError, InvalidMacPasskeyPublishableKeyError, UnsupportedDesktopBuildArchitectureError, @@ -212,6 +214,72 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu }); it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { + it.effect.each(["mac", "linux", "win"] as const)( + "stages pstack at the extraResources source on %s", + (platform) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pstack-stage-" }); + const serverDistDir = path.join(root, "dist"); + const stageAppDir = path.join(root, "app"); + const files = [ + "UPSTREAM.json", + "upstream/.gitignore", + "upstream/skills/poteto-mode/scripts/bun.lock", + ]; + for (const file of files) { + const source = path.join(serverDistDir, "pstack", file); + yield* fs.makeDirectory(path.dirname(source), { recursive: true }); + yield* fs.writeFileString(source, file); + } + yield* fs.writeFileString(path.join(serverDistDir, "bin.mjs"), "server"); + yield* stageDesktopServerDist({ platform, serverDistDir, stageAppDir }); + for (const file of files) { + assert.equal( + yield* fs.readFileString(path.join(stageAppDir, "apps/server/dist/pstack", file)), + file, + ); + } + assert.equal( + yield* fs.exists(path.join(stageAppDir, "apps/server/dist/bin.mjs")), + platform !== "win", + ); + }).pipe(Effect.scoped), + ); + + it.effect.each(["mac", "linux", "win"] as const)( + "ships pstack outside asar without filtering runtime lockfiles on %s", + (platform) => + Effect.gen(function* () { + const config = yield* createBuildConfig( + platform, + "dir", + "1.2.3", + false, + false, + undefined, + undefined, + ); + const resource = { from: "apps/server/dist/pstack", to: "pstack" }; + assert.deepInclude(DESKTOP_EXTRA_RESOURCES, resource); + assert.deepInclude(config.extraResources as unknown[], resource); + for (const file of ["UPSTREAM.json", "upstream/skills/poteto-mode/scripts/bun.lock"]) { + const packagedPath = `${resource.from}/${file}`; + assert.isTrue( + DESKTOP_FILE_EXCLUSIONS.some((glob) => + NodePath.matchesGlob(packagedPath, glob.slice(1)), + ), + ); + assert.isTrue( + WINDOWS_SERVER_ASAR_IGNORE_GLOBS.some((glob) => + NodePath.matchesGlob(packagedPath, glob), + ), + ); + } + }), + ); + it.effect("rejects overlapping builds from another process until restoration finishes", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -1264,6 +1332,28 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ), ); + it.effect("allows the vendored pstack files within the Windows payload budget", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const fixture = yield* makeWindowsPayloadFixture({ copyUnpackedNatives: true }); + yield* fs.copy( + path.join(import.meta.dirname, "../packages/pstack/upstream"), + path.join(fixture.packagedAppDir, "resources/pstack/upstream"), + ); + yield* validateWindowsPackagedPayload({ + stageDistDir: fixture.stageDistDir, + appExecutableName: fixture.appExecutableName, + targetArch: "arm64", + appVersion: WINDOWS_PAYLOAD_FIXTURE_VERSION, + }); + }).pipe( + Effect.scoped, + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(HostProcess.Architecture, "x64"), + ), + ); + it.effect("rejects a sidecar whose extracted server bundle cannot resolve", () => Effect.scoped( Effect.gen(function* () { diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 5d828bb24c4fe..002d362cff05e 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -981,6 +981,8 @@ export const STAGE_INSTALL_ARGS = [ ] as const; export const DESKTOP_ELECTRON_LANGUAGES = ["en-US"] as const; export const DESKTOP_FILE_EXCLUSIONS = [ + "!apps/server/dist/pstack", + "!apps/server/dist/pstack/**/*", // Cursor walks from argv[1] to platform assets; helpers and native addons need real paths outside ASAR. "!**/node_modules/@cursor/sdk-*/**/*", "!apps/desktop/prod-resources/cursor-sdk", @@ -1034,6 +1036,8 @@ export const WINDOWS_SERVER_ASAR_UNPACK_GLOBS = [ ] as const; // Keep sidecar exclusions aligned with the app; .bin symlinks are unused and unsupported by ASAR extraction. export const WINDOWS_SERVER_ASAR_IGNORE_GLOBS = [ + "apps/server/dist/pstack", + "apps/server/dist/pstack/**", "**/node_modules/@cursor/sdk-*", "**/node_modules/@cursor/sdk-*/**", "**/node_modules/@anthropic-ai/claude-agent-sdk-*", @@ -1057,7 +1061,8 @@ export function resolveWindowsServerAsarIgnoreGlobs(arch: typeof BuildArch.Type) ]; } -export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 80; +// The external pstack pack contributes about 200 files in addition to the server's native sidecars. +export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 300; export const WINDOWS_SERVER_RESOURCE_SOURCE_DIR = "apps/desktop/prod-resources/windows-server"; export const WINDOWS_SERVER_EXTRA_RESOURCES = [ { @@ -1087,6 +1092,11 @@ export const WSL_RUNTIME_EXTRA_RESOURCES = [ WSL_RUNTIME_ARCHIVE_HASH_EXTRA_RESOURCE, ] as const; export const DESKTOP_EXTRA_RESOURCES = [ + // The main app matcher strips bun.lock; extraResources preserves the runtime pack. + { + from: "apps/server/dist/pstack", + to: "pstack", + }, { from: "apps/desktop/prod-resources/cursor-sdk", to: "node_modules/@cursor", @@ -3073,6 +3083,22 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function } }); +export const stageDesktopServerDist = Effect.fn("stageDesktopServerDist")(function* (input: { + readonly platform: typeof BuildPlatform.Type; + readonly serverDistDir: string; + readonly stageAppDir: string; +}) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const destination = path.join(input.stageAppDir, "apps/server/dist"); + yield* fs.makeDirectory(destination, { recursive: true }); + if (input.platform === "win") { + yield* fs.copy(path.join(input.serverDistDir, "pstack"), path.join(destination, "pstack")); + } else { + yield* fs.copy(input.serverDistDir, destination); + } +}); + export const stageWindowsServerSidecar = Effect.fn("stageWindowsServerSidecar")(function* (input: { readonly stageRoot: string; readonly repoRoot: string; @@ -3703,9 +3729,6 @@ const buildDesktopArtifactUnlocked = Effect.fn("buildDesktopArtifactUnlocked")(f yield* validateBundledClientAssets(path.dirname(bundledClientEntry)); yield* fs.makeDirectory(path.join(stageAppDir, "apps/desktop"), { recursive: true }); - if (options.platform !== "win") { - yield* fs.makeDirectory(path.join(stageAppDir, "apps/server"), { recursive: true }); - } yield* Effect.log("[desktop-artifact] Staging release app..."); yield* fs.copy(distDirs.desktopDist, path.join(stageAppDir, "apps/desktop/dist-electron")); @@ -3727,8 +3750,12 @@ const buildDesktopArtifactUnlocked = Effect.fn("buildDesktopArtifactUnlocked")(f options.verbose, ); } + yield* stageDesktopServerDist({ + platform: options.platform, + serverDistDir: distDirs.serverDist, + stageAppDir, + }); if (options.platform !== "win") { - yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); yield* stageProjectMemory({ repoRoot, outputDir: path.join(stageAppDir, "apps/server/dist/project-memory"),