* feat(desktop): open existing agents from links Register a stable agent deep link and route it through the existing Desktop window. Add a matching CLI command that resolves the local server and activates the requested agent without creating or messaging it. * fix(desktop): recover agent link delivery
150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
|
|
function writeExecutable(filePath: string, contents: string): void {
|
|
writeFileSync(filePath, contents, "utf8");
|
|
chmodSync(filePath, 0o755);
|
|
}
|
|
|
|
function createFakeMacBundle(options: { includeHelper: boolean }): {
|
|
root: string;
|
|
shimPath: string;
|
|
} {
|
|
const root = mkdtempSync(join(tmpdir(), "paseo-cli-shim-test-"));
|
|
const appPath = join(root, "Paseo.app");
|
|
const contentsPath = join(appPath, "Contents");
|
|
const resourcesPath = join(contentsPath, "Resources");
|
|
const shimPath = join(resourcesPath, "bin", "paseo");
|
|
const mainPath = join(contentsPath, "MacOS", "Paseo");
|
|
const helperPath = join(
|
|
contentsPath,
|
|
"Frameworks",
|
|
"Paseo Helper.app",
|
|
"Contents",
|
|
"MacOS",
|
|
"Paseo Helper",
|
|
);
|
|
|
|
mkdirSync(dirname(shimPath), { recursive: true });
|
|
mkdirSync(dirname(mainPath), { recursive: true });
|
|
copyFileSync(join(packageRoot, "bin", "paseo"), shimPath);
|
|
chmodSync(shimPath, 0o755);
|
|
|
|
writeExecutable(mainPath, "#!/bin/sh\necho main-executable\n");
|
|
|
|
if (options.includeHelper) {
|
|
mkdirSync(dirname(helperPath), { recursive: true });
|
|
writeExecutable(
|
|
helperPath,
|
|
[
|
|
"#!/bin/sh",
|
|
'printf "helper env=%s/%s cli=%s\\n" "$ELECTRON_RUN_AS_NODE" "$PASEO_NODE_ENV" "$PASEO_CLI"',
|
|
'printf "args=%s\\n" "$*"',
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
}
|
|
|
|
return { root, shimPath };
|
|
}
|
|
|
|
describe("desktop packaging", () => {
|
|
it("unpacks server zsh shell integration files for external shells", () => {
|
|
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
|
|
|
|
expect(config).toContain(
|
|
"node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/*",
|
|
);
|
|
expect(config).not.toContain(
|
|
"node_modules/@getpaseo/server/dist/src/terminal/shell-integration/**/*",
|
|
);
|
|
});
|
|
|
|
it("excludes package debug/source files from the packaged app", () => {
|
|
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
|
|
|
|
expect(config).toContain("!**/*.map");
|
|
expect(config).toContain("!node_modules/@getpaseo/*/src/**");
|
|
expect(config).toContain("!node_modules/@getpaseo/**/*.test.*");
|
|
expect(config).toContain("!node_modules/@getpaseo/**/*.spec.*");
|
|
});
|
|
|
|
it("excludes the bundled daemon web UI from the packaged app", () => {
|
|
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
|
|
|
|
expect(config).toContain("!node_modules/@getpaseo/server/dist/server/web-ui/**");
|
|
});
|
|
|
|
it("registers Paseo agent links with the operating system", () => {
|
|
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
|
|
|
|
expect(config).toContain("name: Paseo agent link");
|
|
expect(config).toContain("- paseo");
|
|
});
|
|
|
|
// electron-builder packs production dependencies declared in package.json into
|
|
// app.asar. Runtime code in runtime-paths.ts and bin/paseo dynamically resolves
|
|
// these workspace packages by string, so static analysis (TypeScript, Knip) cannot
|
|
// see the link. If a runtime-required workspace dep is dropped from
|
|
// dependencies, the build still succeeds but ships a broken bundle. This
|
|
// assertion is the safety net.
|
|
it("declares all workspace packages required at runtime", () => {
|
|
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as {
|
|
dependencies?: Record<string, string>;
|
|
};
|
|
const deps = pkg.dependencies ?? {};
|
|
|
|
for (const required of ["@getpaseo/cli", "@getpaseo/server"]) {
|
|
expect(deps[required], `${required} must be declared in dependencies`).toBe("*");
|
|
}
|
|
});
|
|
|
|
it("launches the packaged macOS CLI through Helper instead of the main app executable", () => {
|
|
if (process.platform === "win32") return;
|
|
|
|
const bundle = createFakeMacBundle({ includeHelper: true });
|
|
try {
|
|
const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" });
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toContain(`helper env=1/production cli=${bundle.shimPath}`);
|
|
expect(result.stdout).toContain("node-entrypoint-runner.js");
|
|
expect(result.stdout).toContain("node-script");
|
|
expect(result.stdout).toContain("@getpaseo/cli/dist/index.js");
|
|
expect(result.stdout).toContain("--version");
|
|
expect(result.stdout).not.toContain("main-executable");
|
|
} finally {
|
|
rmSync(bundle.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("fails packaged macOS CLI startup when Helper is missing", () => {
|
|
if (process.platform === "win32") return;
|
|
|
|
const bundle = createFakeMacBundle({ includeHelper: false });
|
|
try {
|
|
const result = spawnSync(bundle.shimPath, ["--version"], { encoding: "utf8" });
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Bundled Paseo Helper executable not found");
|
|
expect(result.stdout).not.toContain("main-executable");
|
|
} finally {
|
|
rmSync(bundle.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|