From 216e50d30cdd463cf57d94d2936973caa7e8d9e8 Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:04:27 +0900 Subject: [PATCH 1/6] fix: keep linked runtime aligned with source --- bin/devspace-agentd.js | 4 +- bin/devspace.js | 4 +- bin/run-entrypoint.js | 20 +++++++++ src/bin-launcher.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++ src/config-migration.ts | 4 +- src/user-config.test.ts | 10 +++++ 6 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 bin/run-entrypoint.js create mode 100644 src/bin-launcher.test.ts diff --git a/bin/devspace-agentd.js b/bin/devspace-agentd.js index 268c36097..e6c54ca1b 100755 --- a/bin/devspace-agentd.js +++ b/bin/devspace-agentd.js @@ -1,2 +1,4 @@ #!/usr/bin/env node -import "../dist/local-agent-daemon-main.js"; +import { runEntrypoint } from "./run-entrypoint.js"; + +await runEntrypoint("../src/local-agent-daemon-main.ts", "../dist/local-agent-daemon-main.js"); diff --git a/bin/devspace.js b/bin/devspace.js index 8fb127218..4e4788a1b 100755 --- a/bin/devspace.js +++ b/bin/devspace.js @@ -1,2 +1,4 @@ #!/usr/bin/env node -import "../dist/cli.js"; +import { runEntrypoint } from "./run-entrypoint.js"; + +await runEntrypoint("../src/cli.ts", "../dist/cli.js"); diff --git a/bin/run-entrypoint.js b/bin/run-entrypoint.js new file mode 100644 index 000000000..f32983a1c --- /dev/null +++ b/bin/run-entrypoint.js @@ -0,0 +1,20 @@ +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +export async function runEntrypoint(sourcePath, distPath) { + const sourceUrl = new URL(sourcePath, import.meta.url); + if (existsSync(fileURLToPath(sourceUrl))) { + try { + await import("tsx/esm"); + } catch (error) { + throw new Error( + "DevSpace source checkout detected, but tsx is unavailable. Run `pnpm install` in the checkout; refusing to fall back to potentially stale dist output.", + { cause: error }, + ); + } + await import(sourceUrl.href); + return; + } + + await import(new URL(distPath, import.meta.url).href); +} diff --git a/src/bin-launcher.test.ts b/src/bin-launcher.test.ts new file mode 100644 index 000000000..00b43a99f --- /dev/null +++ b/src/bin-launcher.test.ts @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { writeTestDevspaceConfig } from "./test-support/config.test.js"; + +const projectRoot = fileURLToPath(new URL("..", import.meta.url)); +const tsxRoot = join(projectRoot, "node_modules", "tsx"); + +for (const entrypoint of [ + { + bin: "devspace.js", + source: "src/cli.ts", + dist: "dist/cli.js", + }, + { + bin: "devspace-agentd.js", + source: "src/local-agent-daemon-main.ts", + dist: "dist/local-agent-daemon-main.js", + }, +]) { + testLauncher(entrypoint); +} + +testLinkedCheckoutReadsCurrentConfig(); +testMissingSourceRuntimeFailsClosed(); + +function testLinkedCheckoutReadsCurrentConfig(): void { + const root = mkdtempSync(join(tmpdir(), "devspace-bin-config-test-")); + try { + const env = writeTestDevspaceConfig(root, { tools: { mode: "codex" } }); + const output = execFileSync(process.execPath, [join(projectRoot, "bin", "devspace.js"), "config", "get"], { + encoding: "utf8", + env: { ...process.env, ...env }, + }); + const config = JSON.parse(output) as { tools?: { mode?: string } }; + assert.equal(config.tools?.mode, "codex"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +function testMissingSourceRuntimeFailsClosed(): void { + const root = mkdtempSync(join(tmpdir(), "devspace-bin-missing-tsx-test-")); + try { + cpSync(join(projectRoot, "bin"), join(root, "bin"), { recursive: true }); + mkdirSync(join(root, "src"), { recursive: true }); + mkdirSync(join(root, "dist"), { recursive: true }); + writeFileSync(join(root, "package.json"), JSON.stringify({ type: "module" })); + writeFileSync(join(root, "src", "cli.ts"), 'console.log("source");\n'); + writeFileSync(join(root, "dist", "cli.js"), 'console.log("stale-dist");\n'); + + assert.throws( + () => execFileSync(process.execPath, [join(root, "bin", "devspace.js")], { encoding: "utf8", stdio: "pipe" }), + /source checkout.*tsx.*pnpm install/is, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +function testLauncher(entrypoint: { bin: string; source: string; dist: string }): void { + const root = mkdtempSync(join(tmpdir(), "devspace-bin-launcher-test-")); + try { + cpSync(join(projectRoot, "bin"), join(root, "bin"), { recursive: true }); + mkdirSync(dirname(join(root, entrypoint.source)), { recursive: true }); + mkdirSync(dirname(join(root, entrypoint.dist)), { recursive: true }); + mkdirSync(join(root, "node_modules"), { recursive: true }); + symlinkSync(tsxRoot, join(root, "node_modules", "tsx"), process.platform === "win32" ? "junction" : "dir"); + writeFileSync(join(root, "package.json"), JSON.stringify({ type: "module" })); + writeFileSync(join(root, entrypoint.source), 'console.log("source");\n'); + writeFileSync(join(root, entrypoint.dist), 'console.log("dist");\n'); + + const sourceOutput = execFileSync(process.execPath, [join(root, "bin", entrypoint.bin)], { + encoding: "utf8", + }).trim(); + assert.equal(sourceOutput, "source", `${entrypoint.bin} must prefer source in a linked checkout`); + + rmSync(join(root, entrypoint.source)); + const packagedOutput = execFileSync(process.execPath, [join(root, "bin", entrypoint.bin)], { + encoding: "utf8", + }).trim(); + assert.equal(packagedOutput, "dist", `${entrypoint.bin} must use dist in a published package`); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} diff --git a/src/config-migration.ts b/src/config-migration.ts index f24851adb..d6192a8cc 100644 --- a/src/config-migration.ts +++ b/src/config-migration.ts @@ -22,6 +22,7 @@ const legacyConfigSchema = z.object({ tools: z.object({ mode: z.enum(["claude", "codex"]).optional(), }).strict().optional(), + "tools.mode": z.enum(["claude", "codex"]).optional(), ui: z.object({ enabled: z.boolean().optional(), }).strict().optional(), @@ -40,6 +41,7 @@ const LEGACY_CONFIG_KEYS = new Set([ "agentDir", "subagents", "tools", + "tools.mode", "ui", ]); @@ -65,7 +67,7 @@ export function migrateLegacyConfig(value: unknown): DevspaceConfig { worktreeRoot: legacy.worktreeRoot, }), storage: definedEntries({ stateDir: legacy.stateDir }), - tools: definedEntries({ mode: legacy.tools?.mode }), + tools: definedEntries({ mode: legacy.tools?.mode ?? legacy["tools.mode"] }), ui: definedEntries({ enabled: legacy.ui?.enabled }), artifacts: definedEntries({ enabled: legacy.artifactsEnabled, diff --git a/src/user-config.test.ts b/src/user-config.test.ts index 8e09b9122..0729a7a67 100644 --- a/src/user-config.test.ts +++ b/src/user-config.test.ts @@ -46,6 +46,16 @@ withConfigDir((configDir, env) => { assert.equal(nextLoad.migratedLegacyConfig, false); }); +withConfigDir((configDir, env) => { + writeFileSync(join(configDir, "config.json"), JSON.stringify({ + "tools.mode": "claude", + })); + + const files = loadDevspaceFiles(env); + assert.equal(files.migratedLegacyConfig, true); + assert.equal(files.config.tools.mode, "claude"); +}); + await withConfigDirAsync(async (configDir) => { writeFileSync(join(configDir, "config.json"), JSON.stringify({ port: 8787, From 62812de83b9f407cb4cd09cebe60a32397422d80 Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:24:22 +0900 Subject: [PATCH 2/6] test: cover migrated codex package paths --- src/bin-launcher.test.ts | 80 +++++++++++++++++++++++++++++++++++++++- src/user-config.test.ts | 15 ++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/bin-launcher.test.ts b/src/bin-launcher.test.ts index 00b43a99f..4be0a36f3 100644 --- a/src/bin-launcher.test.ts +++ b/src/bin-launcher.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -26,6 +26,7 @@ for (const entrypoint of [ testLinkedCheckoutReadsCurrentConfig(); testMissingSourceRuntimeFailsClosed(); +testPackedPackageLaunchers(); function testLinkedCheckoutReadsCurrentConfig(): void { const root = mkdtempSync(join(tmpdir(), "devspace-bin-config-test-")); @@ -61,6 +62,59 @@ function testMissingSourceRuntimeFailsClosed(): void { } } +function testPackedPackageLaunchers(): void { + const root = mkdtempSync(join(tmpdir(), "devspace-packed-bin-test-")); + const installRoot = join(root, "install"); + try { + mkdirSync(installRoot, { recursive: true }); + execFileSync(npmExecutable(), ["pack", "--silent", "--pack-destination", root], { + cwd: projectRoot, + encoding: "utf8", + stdio: "pipe", + shell: process.platform === "win32", + }); + const archive = readdirSync(root).find((name) => name.endsWith(".tgz")); + assert.ok(archive, "npm pack must produce a package archive"); + + execFileSync(npmExecutable(), [ + "install", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--no-save", + "--omit=optional", + join(root, archive), + ], { + cwd: installRoot, + encoding: "utf8", + stdio: "pipe", + shell: process.platform === "win32", + }); + + const configRoot = join(root, "config"); + const env = writeTestDevspaceConfig(configRoot, { + storage: { stateDir: join(root, "state") }, + workspaces: { allowedRoots: [root], worktreeRoot: join(root, "worktrees") }, + skills: { agentDir: join(root, "agents") }, + }); + const cliOutput = execInstalledBin(installRoot, "devspace", ["config", "get"], { + ...process.env, + ...env, + }); + const config = JSON.parse(cliOutput) as { tools?: { mode?: string } }; + assert.equal(config.tools?.mode, "codex"); + + execInstalledBin(installRoot, "devspace-agentd", [], { + ...process.env, + ...env, + DEVSPACE_AGENTD_IDLE_TIMEOUT_MS: "0", + DEVSPACE_AGENTD_SHUTDOWN_TIMEOUT_MS: "1000", + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + function testLauncher(entrypoint: { bin: string; source: string; dist: string }): void { const root = mkdtempSync(join(tmpdir(), "devspace-bin-launcher-test-")); try { @@ -87,3 +141,27 @@ function testLauncher(entrypoint: { bin: string; source: string; dist: string }) rmSync(root, { recursive: true, force: true }); } } + +function npmExecutable(): string { + return process.platform === "win32" ? "npm.cmd" : "npm"; +} + +function execInstalledBin( + installRoot: string, + name: string, + args: string[], + env: NodeJS.ProcessEnv, +): string { + const executable = join( + installRoot, + "node_modules", + ".bin", + process.platform === "win32" ? `${name}.cmd` : name, + ); + return execFileSync(executable, args, { + encoding: "utf8", + env, + stdio: "pipe", + shell: process.platform === "win32", + }); +} diff --git a/src/user-config.test.ts b/src/user-config.test.ts index 0729a7a67..088926091 100644 --- a/src/user-config.test.ts +++ b/src/user-config.test.ts @@ -9,6 +9,8 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { loadConfig } from "./config.js"; +import { getToolSurface } from "./tool-surfaces/index.js"; import { loadDevspaceFiles, setDevspaceConfigValue, @@ -56,6 +58,19 @@ withConfigDir((configDir, env) => { assert.equal(files.config.tools.mode, "claude"); }); +withConfigDir((configDir, env) => { + writeFileSync(join(configDir, "config.json"), JSON.stringify({ + "tools.mode": "codex", + })); + + const config = loadConfig({ + ...env, + DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", + }); + assert.equal(config.toolMode, "codex"); + assert.strictEqual(getToolSurface(config.toolMode), getToolSurface("codex")); +}); + await withConfigDirAsync(async (configDir) => { writeFileSync(join(configDir, "config.json"), JSON.stringify({ port: 8787, From f582c693b87a62fb3ebb815f0adcabb1c1b04062 Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:32:06 +0900 Subject: [PATCH 3/6] test: assert codex surface registration --- src/user-config.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/user-config.test.ts b/src/user-config.test.ts index 088926091..f8fd169cc 100644 --- a/src/user-config.test.ts +++ b/src/user-config.test.ts @@ -11,6 +11,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; import { getToolSurface } from "./tool-surfaces/index.js"; +import type { ToolRegistrationContext } from "./tool-surfaces/types.js"; import { loadDevspaceFiles, setDevspaceConfigValue, @@ -68,7 +69,19 @@ withConfigDir((configDir, env) => { DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", }); assert.equal(config.toolMode, "codex"); - assert.strictEqual(getToolSurface(config.toolMode), getToolSurface("codex")); + + const registeredTools: string[] = []; + getToolSurface(config.toolMode).register({ + server: { + registerTool(name: string) { + registeredTools.push(name); + }, + }, + config, + workspaces: {}, + processSessions: {}, + } as unknown as ToolRegistrationContext); + assert.deepEqual(registeredTools, ["apply_patch", "exec_command", "write_stdin"]); }); await withConfigDirAsync(async (configDir) => { From a971bef55b7c3fb9ce554bdac4001ce114dcdb03 Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:14:17 +0900 Subject: [PATCH 4/6] fix: drop unshipped tool mode migration --- src/config-migration.ts | 4 +--- src/user-config.test.ts | 38 -------------------------------------- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/src/config-migration.ts b/src/config-migration.ts index d6192a8cc..f24851adb 100644 --- a/src/config-migration.ts +++ b/src/config-migration.ts @@ -22,7 +22,6 @@ const legacyConfigSchema = z.object({ tools: z.object({ mode: z.enum(["claude", "codex"]).optional(), }).strict().optional(), - "tools.mode": z.enum(["claude", "codex"]).optional(), ui: z.object({ enabled: z.boolean().optional(), }).strict().optional(), @@ -41,7 +40,6 @@ const LEGACY_CONFIG_KEYS = new Set([ "agentDir", "subagents", "tools", - "tools.mode", "ui", ]); @@ -67,7 +65,7 @@ export function migrateLegacyConfig(value: unknown): DevspaceConfig { worktreeRoot: legacy.worktreeRoot, }), storage: definedEntries({ stateDir: legacy.stateDir }), - tools: definedEntries({ mode: legacy.tools?.mode ?? legacy["tools.mode"] }), + tools: definedEntries({ mode: legacy.tools?.mode }), ui: definedEntries({ enabled: legacy.ui?.enabled }), artifacts: definedEntries({ enabled: legacy.artifactsEnabled, diff --git a/src/user-config.test.ts b/src/user-config.test.ts index f8fd169cc..8e09b9122 100644 --- a/src/user-config.test.ts +++ b/src/user-config.test.ts @@ -9,9 +9,6 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { loadConfig } from "./config.js"; -import { getToolSurface } from "./tool-surfaces/index.js"; -import type { ToolRegistrationContext } from "./tool-surfaces/types.js"; import { loadDevspaceFiles, setDevspaceConfigValue, @@ -49,41 +46,6 @@ withConfigDir((configDir, env) => { assert.equal(nextLoad.migratedLegacyConfig, false); }); -withConfigDir((configDir, env) => { - writeFileSync(join(configDir, "config.json"), JSON.stringify({ - "tools.mode": "claude", - })); - - const files = loadDevspaceFiles(env); - assert.equal(files.migratedLegacyConfig, true); - assert.equal(files.config.tools.mode, "claude"); -}); - -withConfigDir((configDir, env) => { - writeFileSync(join(configDir, "config.json"), JSON.stringify({ - "tools.mode": "codex", - })); - - const config = loadConfig({ - ...env, - DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", - }); - assert.equal(config.toolMode, "codex"); - - const registeredTools: string[] = []; - getToolSurface(config.toolMode).register({ - server: { - registerTool(name: string) { - registeredTools.push(name); - }, - }, - config, - workspaces: {}, - processSessions: {}, - } as unknown as ToolRegistrationContext); - assert.deepEqual(registeredTools, ["apply_patch", "exec_command", "write_stdin"]); -}); - await withConfigDirAsync(async (configDir) => { writeFileSync(join(configDir, "config.json"), JSON.stringify({ port: 8787, From f7577f171d63910ef54558ad8f15e98dfdc20c15 Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:14:17 +0900 Subject: [PATCH 5/6] test: separate package install smoke coverage --- .github/workflows/ci.yml | 3 + package.json | 1 + src/bin-launcher.test.ts | 80 +-------------------------- test/package-install-smoke.test.ts | 88 ++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 79 deletions(-) create mode 100644 test/package-install-smoke.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be5a8f3d5..1cb828446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,9 @@ jobs: DEVSPACE_REQUIRE_PI_SANDBOX: ${{ matrix.os == 'ubuntu-latest' && '1' || '0' }} run: pnpm test + - name: Package install smoke test + run: pnpm test:package-install + - name: Build run: pnpm build diff --git a/package.json b/package.json index 9a4c2a83a..1835ba80d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "schema:config": "tsx scripts/generate-config-schema.ts", "start": "node dist/cli.js serve", "test": "tsx --test --test-concurrency=1 \"src/**/*.test.ts\"", + "test:package-install": "tsx --test --test-concurrency=1 \"test/package-install-smoke.test.ts\"", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [], diff --git a/src/bin-launcher.test.ts b/src/bin-launcher.test.ts index 4be0a36f3..00b43a99f 100644 --- a/src/bin-launcher.test.ts +++ b/src/bin-launcher.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -26,7 +26,6 @@ for (const entrypoint of [ testLinkedCheckoutReadsCurrentConfig(); testMissingSourceRuntimeFailsClosed(); -testPackedPackageLaunchers(); function testLinkedCheckoutReadsCurrentConfig(): void { const root = mkdtempSync(join(tmpdir(), "devspace-bin-config-test-")); @@ -62,59 +61,6 @@ function testMissingSourceRuntimeFailsClosed(): void { } } -function testPackedPackageLaunchers(): void { - const root = mkdtempSync(join(tmpdir(), "devspace-packed-bin-test-")); - const installRoot = join(root, "install"); - try { - mkdirSync(installRoot, { recursive: true }); - execFileSync(npmExecutable(), ["pack", "--silent", "--pack-destination", root], { - cwd: projectRoot, - encoding: "utf8", - stdio: "pipe", - shell: process.platform === "win32", - }); - const archive = readdirSync(root).find((name) => name.endsWith(".tgz")); - assert.ok(archive, "npm pack must produce a package archive"); - - execFileSync(npmExecutable(), [ - "install", - "--no-audit", - "--no-fund", - "--no-package-lock", - "--no-save", - "--omit=optional", - join(root, archive), - ], { - cwd: installRoot, - encoding: "utf8", - stdio: "pipe", - shell: process.platform === "win32", - }); - - const configRoot = join(root, "config"); - const env = writeTestDevspaceConfig(configRoot, { - storage: { stateDir: join(root, "state") }, - workspaces: { allowedRoots: [root], worktreeRoot: join(root, "worktrees") }, - skills: { agentDir: join(root, "agents") }, - }); - const cliOutput = execInstalledBin(installRoot, "devspace", ["config", "get"], { - ...process.env, - ...env, - }); - const config = JSON.parse(cliOutput) as { tools?: { mode?: string } }; - assert.equal(config.tools?.mode, "codex"); - - execInstalledBin(installRoot, "devspace-agentd", [], { - ...process.env, - ...env, - DEVSPACE_AGENTD_IDLE_TIMEOUT_MS: "0", - DEVSPACE_AGENTD_SHUTDOWN_TIMEOUT_MS: "1000", - }); - } finally { - rmSync(root, { recursive: true, force: true }); - } -} - function testLauncher(entrypoint: { bin: string; source: string; dist: string }): void { const root = mkdtempSync(join(tmpdir(), "devspace-bin-launcher-test-")); try { @@ -141,27 +87,3 @@ function testLauncher(entrypoint: { bin: string; source: string; dist: string }) rmSync(root, { recursive: true, force: true }); } } - -function npmExecutable(): string { - return process.platform === "win32" ? "npm.cmd" : "npm"; -} - -function execInstalledBin( - installRoot: string, - name: string, - args: string[], - env: NodeJS.ProcessEnv, -): string { - const executable = join( - installRoot, - "node_modules", - ".bin", - process.platform === "win32" ? `${name}.cmd` : name, - ); - return execFileSync(executable, args, { - encoding: "utf8", - env, - stdio: "pipe", - shell: process.platform === "win32", - }); -} diff --git a/test/package-install-smoke.test.ts b/test/package-install-smoke.test.ts new file mode 100644 index 000000000..c53477fd8 --- /dev/null +++ b/test/package-install-smoke.test.ts @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { writeTestDevspaceConfig } from "../src/test-support/config.test.js"; + +const projectRoot = fileURLToPath(new URL("..", import.meta.url)); + +testPackedPackageLaunchers(); + +function testPackedPackageLaunchers(): void { + const root = mkdtempSync(join(tmpdir(), "devspace-packed-bin-test-")); + const installRoot = join(root, "install"); + try { + mkdirSync(installRoot, { recursive: true }); + execFileSync(npmExecutable(), ["pack", "--silent", "--pack-destination", root], { + cwd: projectRoot, + encoding: "utf8", + stdio: "pipe", + shell: process.platform === "win32", + }); + const archive = readdirSync(root).find((name) => name.endsWith(".tgz")); + assert.ok(archive, "npm pack must produce a package archive"); + + execFileSync(npmExecutable(), [ + "install", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--no-save", + "--omit=optional", + join(root, archive), + ], { + cwd: installRoot, + encoding: "utf8", + stdio: "pipe", + shell: process.platform === "win32", + }); + + const configRoot = join(root, "config"); + const env = writeTestDevspaceConfig(configRoot, { + storage: { stateDir: join(root, "state") }, + workspaces: { allowedRoots: [root], worktreeRoot: join(root, "worktrees") }, + skills: { agentDir: join(root, "agents") }, + }); + const cliOutput = execInstalledBin(installRoot, "devspace", ["config", "get"], { + ...process.env, + ...env, + }); + const config = JSON.parse(cliOutput) as { tools?: { mode?: string } }; + assert.equal(config.tools?.mode, "codex"); + + execInstalledBin(installRoot, "devspace-agentd", [], { + ...process.env, + ...env, + DEVSPACE_AGENTD_IDLE_TIMEOUT_MS: "0", + DEVSPACE_AGENTD_SHUTDOWN_TIMEOUT_MS: "1000", + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +function npmExecutable(): string { + return process.platform === "win32" ? "npm.cmd" : "npm"; +} + +function execInstalledBin( + installRoot: string, + name: string, + args: string[], + env: NodeJS.ProcessEnv, +): string { + const executable = join( + installRoot, + "node_modules", + ".bin", + process.platform === "win32" ? `${name}.cmd` : name, + ); + return execFileSync(executable, args, { + encoding: "utf8", + env, + stdio: "pipe", + shell: process.platform === "win32", + }); +} From cf5d1097d124aa1b216f2a72cbc93592e3745d0d Mon Sep 17 00:00:00 2001 From: Rokurolize <1701388+Rokurolize@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:02:48 +0900 Subject: [PATCH 6/6] ci: avoid duplicate release build --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb828446..62cfc8b46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,8 +63,5 @@ jobs: - name: Package install smoke test run: pnpm test:package-install - - name: Build - run: pnpm build - - name: Doctor run: node dist/cli.js doctor