diff --git a/apps/server/package.json b/apps/server/package.json index 0d4477ad258f2..3680e1096fe0a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -68,6 +68,7 @@ "playwright-core": "1.60.0", "proper-lockfile": "4.1.2", "saxes": "6.0.0", + "smol-toml": "1.8.0", "stream-chain": "^4.2.5", "stream-json": "3.6.0", "undici": "catalog:", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 871377b808f90..1af4d767099c4 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -81,6 +81,7 @@ import * as PortScanner from "../../preview/PortScanner.ts"; import { attachmentRelativePath } from "../../attachmentStore.ts"; import { WorkflowAgentTranscriptReadObserver } from "../workflowAgentTranscript.ts"; import * as ServerConfig from "../../config.ts"; +import * as ServerSettings from "../../serverSettings.ts"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; import { T3_CODE_DEVICE_TOOL_INSTRUCTIONS_WITH_TOOL_SEARCH, @@ -1202,31 +1203,75 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => { ); // regression: #15073 the SDK replaced the --settings launch argument with T3's settings - it.effect.each([ - `--settings '{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false}'`, - "--settings user-settings.json", - ])("merges %s beneath T3's settings", (launchArgs) => - Effect.scoped( - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-settings-" }); - yield* fileSystem.writeFileString( - NodePath.join(cwd, "user-settings.json"), - '{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false}', - ); - const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd); - assert.isTrue(Exit.isSuccess(exit)); - assert.deepInclude(opened[0]!.settings, { - skillOverrides: { lint: "off" }, - showThinkingSummaries: true, - }); - assert.isUndefined(opened[0]!.extraArgs?.["settings"]); - }).pipe( - Effect.provide( - Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + it.effect.each(["inline JSON", "a file"])( + "merges --settings %s beneath T3's settings", + (source) => + Effect.scoped( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-settings-" }); + const launchSettings = { + skillOverrides: { lint: "off" }, + showThinkingSummaries: false, + enabledPlugins: { + "pstack@launch": true, + "pstack@launch-array": ["^1.0.0"], + "pstack@launch-object": { version: "^1.0.0" }, + "pstack@disabled": false, + "tracedecay@local": true, + }, + }; + const launchArgs = + source === "inline JSON" + ? `--settings '${JSON.stringify(launchSettings)}'` + : "--settings user-settings.json"; + yield* fileSystem.writeFileString( + NodePath.join(cwd, "user-settings.json"), + JSON.stringify(launchSettings), + ); + yield* fileSystem.makeDirectory(NodePath.join(cwd, ".claude")); + yield* fileSystem.writeFileString( + NodePath.join(cwd, ".claude", "settings.json"), + JSON.stringify({ + enabledPlugins: { + "pstack@settings": true, + "pstack@settings-array": ["^1.0.0"], + "pstack@settings-object": { version: "^1.0.0" }, + "pstack@disabled": true, + }, + }), + ); + for (const pstackEnabled of [false, true]) { + const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd).pipe( + Effect.provide(ServerSettings.layerTest({ pstackEnabled })), + ); + assert.isTrue(Exit.isSuccess(exit)); + assert.deepInclude(opened[0]!.settings, { + skillOverrides: { lint: "off" }, + showThinkingSummaries: true, + enabledPlugins: { + "pstack@launch": !pstackEnabled, + "pstack@launch-array": pstackEnabled ? false : ["^1.0.0"], + "pstack@launch-object": pstackEnabled ? false : { version: "^1.0.0" }, + "pstack@disabled": false, + "tracedecay@local": true, + ...(pstackEnabled + ? { + "pstack@settings": false, + "pstack@settings-array": false, + "pstack@settings-object": false, + } + : {}), + }, + }); + assert.isUndefined(opened[0]!.extraArgs?.["settings"]); + } + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), ), ), - ), ); // regression: #20413 malformed append-system-prompt launch args fail before a query opens diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index eeafd041f2239..8db0a989d5d0a 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -143,7 +143,9 @@ import { import { claudeLaunchArgDirectories, discoverClaudeSkills, + readClaudeEnabledPlugins, } from "../../provider/Drivers/ClaudeSkills.ts"; +import { claudePstackPluginOverrides } from "../../pstack/nativePlugins.ts"; import { compileClaudeModelSelection, type CompiledClaudeModelSelection, @@ -10366,6 +10368,39 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( Effect.orElseSucceed(() => true), ) : true; + // T3's pstack pack replaces a native install; two copies register conflicting skills. + const pstackPluginOverrides = + Option.isSome(serverSettings) && + (yield* serverSettings.value.getSettings.pipe( + Effect.map((settings) => settings.pstackEnabled), + Effect.orElseSucceed(() => false), + )) + ? claudePstackPluginOverrides( + yield* readClaudeEnabledPlugins( + adapterOptions.settings, + turnInput.runtimePolicy.cwd ?? undefined, + adapterOptions.environment, + ).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + ), + launchArgSettings?.enabledPlugins, + ) + : undefined; + const launchSdkSettings = + launchArgSettings === undefined + ? undefined + : claudeSdkLaunchArgSettings(launchArgSettings); + const sdkSettings = + pstackPluginOverrides === undefined + ? launchSdkSettings + : ({ + ...launchSdkSettings, + enabledPlugins: { + ...launchSdkSettings?.enabledPlugins, + ...pstackPluginOverrides, + }, + } as ClaudeSdkSettings); const queryOptions = makeClaudeQueryOptions({ platform, modelSelection: turnInput.modelSelection, @@ -10375,9 +10410,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( cwd: turnInput.runtimePolicy.cwd, attachmentsDir, settings: adapterOptions.settings, - ...(launchArgSettings === undefined - ? {} - : { sdkSettings: claudeSdkLaunchArgSettings(launchArgSettings) }), + ...(sdkSettings === undefined ? {} : { sdkSettings }), environment: claudeQueryEnvironment( adapterOptions.environment, turnInput.appThread, diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 5aeac9ec1a488..23358be7f8d33 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -118,6 +118,8 @@ import { } from "../../provider/EventNdjsonLogger.ts"; import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; import { codexAppServerArgs, resolveCodexLaunchArgs } from "../../provider/codexLaunchArgs.ts"; +import { codexPstackPluginOverrideArgs } from "../../pstack/nativePlugins.ts"; +import * as ServerSettings from "../../serverSettings.ts"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; @@ -1524,6 +1526,20 @@ export const layerAppServerClientFactory: Layer.Layer< const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers; const agentScope = yield* AgentScope; + const serverSettings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); + const fileSystem = yield* Effect.serviceOption(FileSystem.FileSystem); + const pstackPluginOverrideArgs = Effect.fn("CodexAdapterV2.pstackPluginOverrideArgs")( + function* (codexHome: string | undefined) { + if (Option.isNone(serverSettings) || Option.isNone(fileSystem)) return []; + const settings = yield* serverSettings.value.getSettings; + if (!settings.pstackEnabled) return []; + const home = codexHome ?? `${yield* HostProcess.HomeDirectory}/.codex`; + return codexPstackPluginOverrideArgs( + yield* fileSystem.value.readFileString(`${home}/config.toml`), + ); + }, + Effect.orElseSucceed((): ReadonlyArray => []), + ); return CodexAppServerClientFactory.of({ open: Effect.fn("CodexAdapterV2.open")(function* (input) { @@ -1534,9 +1550,13 @@ export const layerAppServerClientFactory: Layer.Layer< }; const command = yield* makeCodexAppServerSpawnCommand({ command: input.settings.binaryPath || "codex", - args: codexAppServerArgs( - resolveCodexLaunchArgs(input.settings.launchArgs, input.environment), - ), + args: [ + ...codexAppServerArgs( + resolveCodexLaunchArgs(input.settings.launchArgs, input.environment), + ), + // T3's pstack pack replaces a native install; two copies register conflicting skills. + ...(yield* pstackPluginOverrideArgs(environment.CODEX_HOME)), + ], env: environment, threadId: input.threadId, }).pipe(Effect.provideService(AgentScope, agentScope)); diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index 680a6e7511b13..d677a884198a9 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -161,7 +161,7 @@ const readSkillSettings = Effect.fn("readSkillSettings")(function* ( const path = yield* Path.Path; const platform = yield* HostProcess.Platform; const overridesByName = new Map(); - const enabledPlugins = new Map(); + const enabledPlugins = new Map(); const repositoryRoot = cwd === undefined ? undefined : yield* findRepositoryRoot(cwd); for (const settingsPath of claudeSettingsPaths( @@ -192,12 +192,7 @@ const readSkillSettings = Effect.fn("readSkillSettings")(function* ( overridesByName.set(name, parseSkillOverride(value)); } for (const [key, enabled] of Object.entries(parsed?.enabledPlugins ?? {})) { - if (typeof enabled === "boolean") { - enabledPlugins.set(key, enabled); - } else { - // Extended formats replace lower-precedence values without enabling the plugin here. - enabledPlugins.delete(key); - } + enabledPlugins.set(key, enabled); } } @@ -240,7 +235,7 @@ function rebaseClaudePluginPath(path: Path.Path, pluginsPath: string, installPat const discoverPluginSkillRoots = Effect.fn("discoverPluginSkillRoots")(function* ( configDirPath: string, cwd: string | undefined, - enabledPlugins: ReadonlyMap, + enabledPlugins: ReadonlyMap, environment: NodeJS.ProcessEnv, ) { const fileSystem = yield* FileSystem.FileSystem; @@ -267,6 +262,7 @@ const discoverPluginSkillRoots = Effect.fn("discoverPluginSkillRoots")(function* workspace === undefined ? undefined : yield* findPluginRepositoryRoot(workspace); for (const [key, entries] of Object.entries(installed.plugins).sort()) { + // Version-constrained entries are left to the native plugin resolver. if (enabledPlugins.get(key) !== true) continue; const applicable = yield* Effect.filter( entries @@ -417,6 +413,16 @@ export function claudeLaunchArgDirectories(launchArgs?: string): ReadonlyArray, + cwd: string | undefined, + environment: NodeJS.ProcessEnv, +): Effect.fn.Return, never, FileSystem.FileSystem | Path.Path> { + const configDirPath = yield* resolveClaudeConfigDirPath(config, environment, cwd); + return (yield* readSkillSettings(configDirPath, cwd, environment)).enabledPlugins; +}); + /** First name wins, matching Claude Code's precedence of user skills over project skills. */ export const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* ( config: Pick & Partial>, diff --git a/apps/server/src/pstack/nativePlugins.test.ts b/apps/server/src/pstack/nativePlugins.test.ts new file mode 100644 index 0000000000000..a568988a8091c --- /dev/null +++ b/apps/server/src/pstack/nativePlugins.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { claudePstackPluginOverrides, codexPstackPluginOverrideArgs } from "./nativePlugins.ts"; + +describe("native pstack plugins", () => { + it("disables every addressable Codex pstack plugin and nothing else", () => { + const config = [ + '[plugins."pstack@pstack-codex"]', + "enabled = true", + ' [plugins."pstack@other"]', + "enabled = false", + '[plugins."pstack-extra@local"]', + '[plugins."tracedecay@local"]', + '[plugins."pstack@dotted.market"]', + "[marketplaces.pstack-codex]", + ].join("\n"); + expect(codexPstackPluginOverrideArgs(config)).toEqual([ + "-c", + "plugins.pstack@pstack-codex.enabled=false", + "-c", + "plugins.pstack@other.enabled=false", + ]); + expect(codexPstackPluginOverrideArgs('[plugins."tracedecay@local"]\n')).toEqual([]); + for (const config of [ + "[plugins.'pstack@local']\nenabled = true\n", + 'plugins."pstack@local".enabled = true\n', + '[plugins]\n"pstack@local" = { enabled = true }\n', + 'plugins = { "pstack@local" = { enabled = true } }\n', + ]) { + expect(codexPstackPluginOverrideArgs(config)).toEqual([ + "-c", + "plugins.pstack@local.enabled=false", + ]); + } + expect( + codexPstackPluginOverrideArgs('instructions = """\n[plugins."pstack@fake"]\n"""\n'), + ).toEqual([]); + expect(codexPstackPluginOverrideArgs('[plugins."pstack@broken"')).toEqual([]); + }); + + it("disables only Claude pstack plugins that settings enable", () => { + expect( + claudePstackPluginOverrides( + new Map([ + ["pstack@cursor-plugins", true], + ["pstack@off", false], + ["tracedecay@local", true], + ]), + ), + ).toEqual({ "pstack@cursor-plugins": false }); + expect(claudePstackPluginOverrides(new Map([["tracedecay@local", true]]))).toBeUndefined(); + }); +}); diff --git a/apps/server/src/pstack/nativePlugins.ts b/apps/server/src/pstack/nativePlugins.ts new file mode 100644 index 0000000000000..01f2e26167896 --- /dev/null +++ b/apps/server/src/pstack/nativePlugins.ts @@ -0,0 +1,34 @@ +import { parse } from "smol-toml"; + +// A harness-native install (`pstack@`) that T3's pack replaces. +const isNativePstackPluginKey = (key: string) => key.startsWith("pstack@"); + +/** Codex app-server `-c` overrides that switch off the pstack plugins a config.toml declares. */ +export function codexPstackPluginOverrideArgs(configToml: string): ReadonlyArray { + let plugins: unknown; + try { + plugins = parse(configToml).plugins; + } catch { + return []; + } + if (typeof plugins !== "object" || plugins === null || Array.isArray(plugins)) return []; + return ( + Object.keys(plugins) + // Codex splits -c keys on every dot and keeps quotes, so only dot-free keys are addressable. + .filter((key) => isNativePstackPluginKey(key) && !key.includes(".")) + .flatMap((key) => ["-c", `plugins.${key}.enabled=false`]) + ); +} + +/** Claude flag-settings `enabledPlugins` entries that switch off enabled pstack plugins. */ +export function claudePstackPluginOverrides( + enabledPlugins: ReadonlyMap, + launchPlugins: Readonly> = {}, +): Record | undefined { + const merged = new Map([...enabledPlugins, ...Object.entries(launchPlugins)]); + const keys = [...merged] + // Claude also enables plugins through version-constrained arrays and objects. + .filter(([key, enabled]) => enabled !== false && isNativePstackPluginKey(key)) + .map(([key]) => key); + return keys.length === 0 ? undefined : Object.fromEntries(keys.map((key) => [key, false])); +} diff --git a/docs/user/pstack.md b/docs/user/pstack.md index 1075d299dd398..b0c30d6c7d98f 100644 --- a/docs/user/pstack.md +++ b/docs/user/pstack.md @@ -48,5 +48,7 @@ the current capabilities before choosing another target. no longer need from the scheduled tasks list. - Steps that need a Cursor-only feature, such as Bugbot setup or a Cursor cloud machine, are skipped with a one-line note in the reply. -- If you also installed pstack directly in Claude Code or Cursor, the T3 Code `pstack:` skills - replace those in the composer. Only the T3 Code skills can mix providers. +- If you also installed pstack as a Codex or Claude Code plugin, T3 Code turns that plugin off in + the sessions it starts while pstack is on, so agents see one copy. Your own Codex and Claude Code + sessions outside T3 Code keep it. A Cursor install is only replaced in the composer. Only the + T3 Code skills can mix providers. diff --git a/packages/pstack/src/check.test.ts b/packages/pstack/src/check.test.ts index 69ab7dec97a59..b2db1c5089f20 100644 --- a/packages/pstack/src/check.test.ts +++ b/packages/pstack/src/check.test.ts @@ -5,11 +5,12 @@ import * as NodePath from "node:path"; import * as NodeURL from "node:url"; import { describe, expect, it } from "vite-plus/test"; import { checkPack } from "./check.ts"; -import { fileFingerprint, packFiles, type UpstreamManifest } from "./files.ts"; +import { fileFingerprint, packFiles, readManifest, type UpstreamManifest } from "./files.ts"; import { parsePstackRoleDefaults, pstackContentHash, pstackPackEntries, + PSTACK_PACKAGED_PRUNED_DIR, PSTACK_SKILL_PREFIX, PSTACK_LAYOUT_VERSION, } from "./index.ts"; @@ -183,17 +184,21 @@ describe("checkPack", () => { }); describe("pack interface", () => { - it("omits gitignore metadata from delivery while verifying its upstream bytes", () => { + it("omits gitignore metadata and desktop-pruned fixtures from delivery while verifying 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.write("upstream/skills/statechart/scripts/statechart.ts", "export {};\n"); + pack.write("upstream/skills/statechart/scripts/test/chart.test.ts", "test\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"); + expect(paths).toContain("skills/statechart/scripts/statechart.ts"); + expect(paths).not.toContain("skills/statechart/scripts/test/chart.test.ts"); pack.write("upstream/.gitignore", "changed\n"); expect(checkPack(pack.root)).toContain("Hash mismatch: .gitignore"); NodeFS.rmSync(NodePath.join(pack.root, "upstream/.gitignore")); @@ -201,6 +206,28 @@ describe("pack interface", () => { expect(() => pstackPackEntries(pack.root)).not.toThrow(); }); + it("delivers a complete pack when packaging pruned the statechart test corpus", () => { + using pack = fixture(); + pack.write("upstream/skills/statechart/scripts/statechart.ts", "export {};\n"); + pack.write(`upstream/${PSTACK_PACKAGED_PRUNED_DIR}/corpus/Counter/chart.mmd`, "chart\n"); + pack.seal(); + expect(checkPack(pack.root)).toEqual([]); + NodeFS.rmSync(NodePath.join(pack.root, "upstream", PSTACK_PACKAGED_PRUNED_DIR), { + recursive: true, + }); + const entries = pstackPackEntries(pack.root); + expect(entries.map(({ packPath }) => packPath)).toContain( + "skills/statechart/scripts/statechart.ts", + ); + expect(entries.filter(({ sourcePath }) => !NodeFS.existsSync(sourcePath))).toEqual([]); + }); + + it("prunes a corpus the shipped snapshot actually carries", () => { + const pruned = (packPath: string) => packPath.startsWith(`${PSTACK_PACKAGED_PRUNED_DIR}/`); + expect(Object.keys(readManifest(packageDir).files).some(pruned)).toBe(true); + expect(pstackPackEntries(packageDir).some(({ packPath }) => pruned(packPath))).toBe(false); + }); + it("invalidates the delivery stamp from the adapter layout", () => { using pack = fixture(); expect(pstackContentHash(pack.root)).not.toBe(pstackContentHash(pack.root, 2)); diff --git a/packages/pstack/src/index.ts b/packages/pstack/src/index.ts index 2fa542c84258c..969570a50b795 100644 --- a/packages/pstack/src/index.ts +++ b/packages/pstack/src/index.ts @@ -8,7 +8,12 @@ export { parsePstackRoleDefaults, PSTACK_PANEL_ROLES, type PstackRoleDefault } f export const PSTACK_SKILL_PREFIX = "pstack:"; export const PSTACK_LAYOUT_VERSION = 3; -const isRuntimePackFile = (packPath: string) => NodePath.posix.basename(packPath) !== ".gitignore"; +/** Upstream test corpus that packaged desktop builds delete; the runtime pack never needs it. */ +export const PSTACK_PACKAGED_PRUNED_DIR = "skills/statechart/scripts/test"; + +const isRuntimePackFile = (packPath: string) => + NodePath.posix.basename(packPath) !== ".gitignore" && + !packPath.startsWith(`${PSTACK_PACKAGED_PRUNED_DIR}/`); /** Files relative to a source root containing upstream/ and UPSTREAM.json. */ export function pstackPackEntries(sourceRoot: string): ReadonlyArray<{ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ad25945372d1..6111c5a59103a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -934,6 +934,9 @@ importers: saxes: specifier: 6.0.0 version: 6.0.0 + smol-toml: + specifier: 1.8.0 + version: 1.8.0 stream-chain: specifier: ^4.2.5 version: 4.2.5 @@ -2428,6 +2431,9 @@ importers: '@t3tools/contracts': specifier: workspace:* version: link:../packages/contracts + '@t3tools/pstack': + specifier: workspace:* + version: link:../packages/pstack '@t3tools/shared': specifier: workspace:* version: link:../packages/shared diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index c653113088f7d..c17e129fca354 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -71,6 +71,7 @@ import { ReleasePackageManifestError, releasePackageFiles, } from "./update-release-package-versions.ts"; +import { pstackPackEntries } from "@t3tools/pstack"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; @@ -1377,10 +1378,17 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { path.join(import.meta.dirname, "../packages/pstack/upstream"), path.join(packagedPstackDir, "upstream"), ); + yield* fs.copy( + path.join(import.meta.dirname, "../packages/pstack/UPSTREAM.json"), + path.join(packagedPstackDir, "UPSTREAM.json"), + ); yield* prunePackagedPstackTestFixtures(packagedPstackDir); const scriptsDir = path.join(packagedPstackDir, "upstream/skills/statechart/scripts"); assert.isFalse(yield* fs.exists(path.join(scriptsDir, "test"))); assert.isTrue(yield* fs.exists(path.join(scriptsDir, "statechart.ts"))); + for (const { sourcePath } of pstackPackEntries(packagedPstackDir)) { + assert.isTrue(yield* fs.exists(sourcePath), sourcePath); + } yield* stageSelfContainedPlatformPackages( path.join(import.meta.dirname, "../apps/server/node_modules"), path.join(fixture.packagedAppDir, "resources/node_modules"), diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index b1fc34f126fdb..fcb905dab3904 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -16,6 +16,7 @@ import { type DirectoryRecord, } from "@electron/asar"; +import { PSTACK_PACKAGED_PRUNED_DIR } from "@t3tools/pstack"; import { fromYaml } from "@t3tools/shared/schemaYaml"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; @@ -3106,7 +3107,7 @@ export const prunePackagedPstackTestFixtures = Effect.fn("prunePackagedPstackTes function* (pstackDir: string) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const fixtureDir = path.join(pstackDir, "upstream/skills/statechart/scripts/test"); + const fixtureDir = path.join(pstackDir, "upstream", PSTACK_PACKAGED_PRUNED_DIR); yield* fs.remove(fixtureDir, { recursive: true, force: true }); }, ); diff --git a/scripts/package.json b/scripts/package.json index d5f34a0756f33..fadf0ac9087e3 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -11,6 +11,7 @@ "@electron/asar": "^4.3.0", "@electron/osx-sign": "2.7.0", "@t3tools/contracts": "workspace:*", + "@t3tools/pstack": "workspace:*", "@t3tools/shared": "workspace:*", "@t3tools/tailscale": "workspace:*", "agent-device": "0.21.32",