@stigmer/cli 3.14.1 → 3.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.d.ts +20 -1
- package/client/client.d.ts.map +1 -1
- package/client/client.js +32 -5
- package/client/client.js.map +1 -1
- package/commands/auth/index.d.ts.map +1 -1
- package/commands/auth/index.js +7 -36
- package/commands/auth/index.js.map +1 -1
- package/commands/auth/login.d.ts +13 -0
- package/commands/auth/login.d.ts.map +1 -0
- package/commands/auth/login.js +58 -0
- package/commands/auth/login.js.map +1 -0
- package/commands/auth/whoami.d.ts +18 -0
- package/commands/auth/whoami.d.ts.map +1 -0
- package/commands/auth/whoami.js +58 -0
- package/commands/auth/whoami.js.map +1 -0
- package/commands/draft.d.ts.map +1 -1
- package/commands/draft.js +24 -8
- package/commands/draft.js.map +1 -1
- package/commands/internal-daemon.d.ts.map +1 -1
- package/commands/internal-daemon.js +2 -9
- package/commands/internal-daemon.js.map +1 -1
- package/commands/run.d.ts.map +1 -1
- package/commands/run.js +15 -6
- package/commands/run.js.map +1 -1
- package/commands/up.d.ts.map +1 -1
- package/commands/up.js +37 -8
- package/commands/up.js.map +1 -1
- package/local/artifact.d.ts +13 -3
- package/local/artifact.d.ts.map +1 -1
- package/local/artifact.js +31 -10
- package/local/artifact.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +2 -0
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/host.d.ts +8 -0
- package/local/daemon/host.d.ts.map +1 -1
- package/local/daemon/host.js +42 -1
- package/local/daemon/host.js.map +1 -1
- package/local/daemon/index.d.ts +3 -3
- package/local/daemon/index.d.ts.map +1 -1
- package/local/daemon/index.js +2 -2
- package/local/daemon/index.js.map +1 -1
- package/local/daemon/launch.d.ts +29 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +73 -23
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts +16 -2
- package/local/daemon/process.d.ts.map +1 -1
- package/local/daemon/process.js +16 -2
- package/local/daemon/process.js.map +1 -1
- package/local/daemon/types.d.ts +2 -0
- package/local/daemon/types.d.ts.map +1 -1
- package/local/paths.d.ts +14 -4
- package/local/paths.d.ts.map +1 -1
- package/local/paths.js +17 -4
- package/local/paths.js.map +1 -1
- package/local/runtime/runner.d.ts +4 -0
- package/local/runtime/runner.d.ts.map +1 -1
- package/local/runtime/runner.js +10 -6
- package/local/runtime/runner.js.map +1 -1
- package/local/runtime/server.d.ts +5 -0
- package/local/runtime/server.d.ts.map +1 -1
- package/local/runtime/server.js +13 -8
- package/local/runtime/server.js.map +1 -1
- package/local/seedpack/content.d.ts +4 -0
- package/local/seedpack/content.d.ts.map +1 -1
- package/local/seedpack/content.js +11 -11
- package/local/seedpack/content.js.map +1 -1
- package/local/state/lock.d.ts +3 -2
- package/local/state/lock.d.ts.map +1 -1
- package/local/state/lock.js +23 -9
- package/local/state/lock.js.map +1 -1
- package/local/state/proc.d.ts +13 -0
- package/local/state/proc.d.ts.map +1 -1
- package/local/state/proc.js +15 -0
- package/local/state/proc.js.map +1 -1
- package/local/temporal/download.d.ts +7 -1
- package/local/temporal/download.d.ts.map +1 -1
- package/local/temporal/download.js +21 -11
- package/local/temporal/download.js.map +1 -1
- package/local/temporal/index.d.ts +3 -3
- package/local/temporal/index.d.ts.map +1 -1
- package/local/temporal/index.js +2 -2
- package/local/temporal/index.js.map +1 -1
- package/local/temporal/inspect.d.ts +10 -3
- package/local/temporal/inspect.d.ts.map +1 -1
- package/local/temporal/inspect.js +52 -13
- package/local/temporal/inspect.js.map +1 -1
- package/local/temporal/manager.d.ts +16 -4
- package/local/temporal/manager.d.ts.map +1 -1
- package/local/temporal/manager.js +19 -6
- package/local/temporal/manager.js.map +1 -1
- package/package.json +5 -5
- package/resources/run/prepare.d.ts +10 -8
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +18 -4
- package/resources/run/prepare.js.map +1 -1
- package/src/client/client.test.ts +89 -1
- package/src/client/client.ts +82 -9
- package/src/commands/auth/index.ts +7 -56
- package/src/commands/auth/login.test.ts +52 -0
- package/src/commands/auth/login.ts +70 -0
- package/src/commands/auth/whoami.test.ts +96 -0
- package/src/commands/auth/whoami.ts +87 -0
- package/src/commands/draft.ts +85 -26
- package/src/commands/internal-daemon.ts +2 -10
- package/src/commands/run.ts +77 -35
- package/src/commands/up.ts +52 -8
- package/src/local/artifact.test.ts +27 -1
- package/src/local/artifact.ts +35 -13
- package/src/local/daemon/components.ts +2 -0
- package/src/local/daemon/daemon.integration.test.ts +33 -0
- package/src/local/daemon/host.ts +50 -1
- package/src/local/daemon/index.ts +3 -3
- package/src/local/daemon/launch.test.ts +136 -0
- package/src/local/daemon/launch.ts +108 -27
- package/src/local/daemon/process.ts +31 -4
- package/src/local/daemon/supervisor.test.ts +1 -1
- package/src/local/daemon/types.ts +2 -0
- package/src/local/paths.ts +17 -4
- package/src/local/runtime/runner.ts +10 -6
- package/src/local/runtime/runtime.test.ts +15 -0
- package/src/local/runtime/server.test.ts +37 -0
- package/src/local/runtime/server.ts +17 -12
- package/src/local/seedpack/content.test.ts +21 -0
- package/src/local/seedpack/content.ts +15 -16
- package/src/local/state/lock.ts +24 -9
- package/src/local/state/proc.ts +16 -0
- package/src/local/state/state.test.ts +25 -1
- package/src/local/temporal/download.test.ts +79 -11
- package/src/local/temporal/download.ts +24 -11
- package/src/local/temporal/index.ts +11 -3
- package/src/local/temporal/inspect.test.ts +83 -0
- package/src/local/temporal/inspect.ts +60 -12
- package/src/local/temporal/manager.test.ts +34 -1
- package/src/local/temporal/manager.ts +22 -8
- package/src/resources/run/prepare.test.ts +28 -26
- package/src/resources/run/prepare.ts +48 -18
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// `up` needs a `temporal` binary and the upstream release is a small, public,
|
|
4
4
|
// stable artifact. The fetch/gunzip/untar/write mechanics are shared with the
|
|
5
|
-
//
|
|
6
|
-
// asset-naming and the default version.
|
|
5
|
+
// other release downloaders in `../artifact.ts`; this module only owns
|
|
6
|
+
// Temporal's asset-naming and the default version.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Every download is verified against the release's `checksums.txt` (the
|
|
9
|
+
// `sha256sum` file Temporal publishes beside its archives) before the binary is
|
|
10
|
+
// written: the integrity guarantee is the published digest, not merely HTTPS
|
|
11
|
+
// plus GitHub's release immutability. The same code path stages the binary the
|
|
12
|
+
// all-in-one image bakes, so one pin and one verification serve every channel.
|
|
11
13
|
|
|
12
14
|
import { existsSync } from "node:fs";
|
|
13
15
|
import { fetchTarballBinary, mapReleaseArch, mapReleaseOs } from "../artifact.js";
|
|
@@ -19,6 +21,9 @@ export { extractTarEntry } from "../artifact.js";
|
|
|
19
21
|
/** Default Temporal CLI version (matches the Go CLI's `DefaultTemporalVersion`). */
|
|
20
22
|
export const DEFAULT_TEMPORAL_VERSION = "1.5.1";
|
|
21
23
|
|
|
24
|
+
/** The release's `sha256sum`-format digest file, one line per published asset. */
|
|
25
|
+
export const TEMPORAL_CHECKSUMS_FILE = "checksums.txt";
|
|
26
|
+
|
|
22
27
|
export interface TemporalDownloadTarget {
|
|
23
28
|
/** Temporal CLI version, e.g. "1.5.1". */
|
|
24
29
|
version: string;
|
|
@@ -32,15 +37,23 @@ export interface TemporalDownloadTarget {
|
|
|
32
37
|
fetchImpl?: typeof fetch;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
/**
|
|
40
|
+
/** The release asset name for a version on a platform, as Temporal publishes it. */
|
|
41
|
+
export function temporalArchiveName(version: string, platform: NodeJS.Platform, arch: string): string {
|
|
42
|
+
return `temporal_cli_${version}_${mapReleaseOs(platform)}_${mapReleaseArch(arch)}.tar.gz`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The GitHub release download URL for one asset of a version. */
|
|
46
|
+
export function temporalReleaseAssetUrl(version: string, asset: string): string {
|
|
47
|
+
return `https://github.com/temporalio/cli/releases/download/v${version}/${asset}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Download, verify and install the Temporal CLI binary to `binPath`. */
|
|
36
51
|
export async function downloadTemporalCli(target: TemporalDownloadTarget): Promise<void> {
|
|
37
|
-
const
|
|
38
|
-
const goarch = mapReleaseArch(target.arch ?? process.arch);
|
|
39
|
-
const archive = `temporal_cli_${target.version}_${goos}_${goarch}.tar.gz`;
|
|
40
|
-
const url = `https://github.com/temporalio/cli/releases/download/v${target.version}/${archive}`;
|
|
52
|
+
const archive = temporalArchiveName(target.version, target.platform ?? process.platform, target.arch ?? process.arch);
|
|
41
53
|
|
|
42
54
|
await fetchTarballBinary({
|
|
43
|
-
url,
|
|
55
|
+
url: temporalReleaseAssetUrl(target.version, archive),
|
|
56
|
+
checksumUrl: temporalReleaseAssetUrl(target.version, TEMPORAL_CHECKSUMS_FILE),
|
|
44
57
|
entryName: "temporal",
|
|
45
58
|
binPath: target.binPath,
|
|
46
59
|
label: "Temporal CLI",
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// Public surface of the Temporal subsystem.
|
|
2
2
|
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export {
|
|
4
|
+
DEFAULT_TEMPORAL_VERSION,
|
|
5
|
+
TEMPORAL_CHECKSUMS_FILE,
|
|
6
|
+
downloadTemporalCli,
|
|
7
|
+
extractTarEntry,
|
|
8
|
+
isTemporalInstalled,
|
|
9
|
+
temporalArchiveName,
|
|
10
|
+
temporalReleaseAssetUrl,
|
|
11
|
+
} from "./download.js";
|
|
12
|
+
export { isLikelyTemporal, processCommandLine, type ProcessInspectOptions } from "./inspect.js";
|
|
13
|
+
export { TEMPORAL_BIN_ENV, TemporalManager, type TemporalManagerOptions } from "./manager.js";
|
|
6
14
|
export { type SupervisedTarget, type SupervisorOptions, TemporalSupervisor } from "./supervisor.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Pins the process-identity reader: the kernel's /proc answer comes first, `ps`
|
|
2
|
+
// is the fallback, and neither's absence is mistaken for "not Temporal" when the
|
|
3
|
+
// other can answer. The /proc arm is what keeps a minimal container (no `ps`)
|
|
4
|
+
// from restarting a healthy Temporal every supervisor tick.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
10
|
+
import { isLikelyTemporal, processCommandLine } from "./inspect.js";
|
|
11
|
+
|
|
12
|
+
function procFixture(entries: Record<number, string[] | "">): string {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "stigmer-proc-"));
|
|
14
|
+
for (const [pid, argv] of Object.entries(entries)) {
|
|
15
|
+
mkdirSync(join(root, pid));
|
|
16
|
+
// /proc/<pid>/cmdline: argv joined by NUL, NUL-terminated; empty for a zombie.
|
|
17
|
+
writeFileSync(join(root, pid, "cmdline"), argv === "" ? "" : `${argv.join("\0")}\0`);
|
|
18
|
+
}
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Hide `ps` so the fallback cannot answer; tests that need it restore PATH.
|
|
23
|
+
let savedPath: string | undefined;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
savedPath = process.env.PATH;
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (savedPath === undefined) delete process.env.PATH;
|
|
29
|
+
else process.env.PATH = savedPath;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("processCommandLine", () => {
|
|
33
|
+
it("reads argv from <procRoot>/<pid>/cmdline, joining the NUL-separated arguments", () => {
|
|
34
|
+
const root = procFixture({ 42: ["/opt/stigmer/bin/temporal", "server", "start-dev", "--port", "7233"] });
|
|
35
|
+
process.env.PATH = "";
|
|
36
|
+
expect(processCommandLine(42, { procRoot: root })).toBe("/opt/stigmer/bin/temporal server start-dev --port 7233");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("falls back to ps when the pid has no cmdline entry", () => {
|
|
40
|
+
const root = procFixture({});
|
|
41
|
+
// Our own process is the one PID whose command line `ps` can always report.
|
|
42
|
+
const line = processCommandLine(process.pid, { procRoot: root });
|
|
43
|
+
expect(line).not.toBeNull();
|
|
44
|
+
expect(line).toMatch(/node|vitest/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("treats an empty cmdline (a zombie) as unreadable and falls back to ps", () => {
|
|
48
|
+
const root = procFixture({ [process.pid]: "" });
|
|
49
|
+
expect(processCommandLine(process.pid, { procRoot: root })).toMatch(/node|vitest/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns null when neither /proc nor ps can answer", () => {
|
|
53
|
+
process.env.PATH = "";
|
|
54
|
+
expect(processCommandLine(process.pid, { procRoot: null })).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("isLikelyTemporal", () => {
|
|
59
|
+
it("recognises the managed dev server by its binary path", () => {
|
|
60
|
+
const root = procFixture({ 7: ["/home/u/.stigmer/bin/temporal", "server", "start-dev"] });
|
|
61
|
+
process.env.PATH = "";
|
|
62
|
+
expect(isLikelyTemporal(7, "/home/u/.stigmer/bin/temporal", { procRoot: root })).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("recognises a temporal server started from another path by its subcommand", () => {
|
|
66
|
+
const root = procFixture({ 8: ["/usr/local/bin/temporal", "server", "start-dev"] });
|
|
67
|
+
process.env.PATH = "";
|
|
68
|
+
expect(isLikelyTemporal(8, "/elsewhere/temporal", { procRoot: root })).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects an unrelated process that reused the pid", () => {
|
|
72
|
+
const root = procFixture({ 9: ["/usr/bin/node", "/app/main.js"] });
|
|
73
|
+
process.env.PATH = "";
|
|
74
|
+
expect(isLikelyTemporal(9, "/home/u/.stigmer/bin/temporal", { procRoot: root })).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// The defect the /proc arm fixes: with `ps` gone and no /proc reader, a live
|
|
78
|
+
// Temporal read as "not Temporal" and the supervisor restarted it forever.
|
|
79
|
+
it("is false, not an exception, when no reader can answer", () => {
|
|
80
|
+
process.env.PATH = "";
|
|
81
|
+
expect(isLikelyTemporal(process.pid, "/x/temporal", { procRoot: null })).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -3,30 +3,78 @@
|
|
|
3
3
|
// A PID file can outlive the process it named; the OS may then assign that PID
|
|
4
4
|
// to something unrelated. Before trusting a PID as "our Temporal", we confirm
|
|
5
5
|
// the live process's command line actually looks like the Temporal dev server.
|
|
6
|
-
//
|
|
6
|
+
//
|
|
7
|
+
// Two readers, in order. On Linux the kernel publishes every process's command
|
|
8
|
+
// line at /proc/<pid>/cmdline (NUL-separated argv), which needs no external
|
|
9
|
+
// program — the reader that works inside a minimal container. Everywhere else
|
|
10
|
+
// (and on a Linux where /proc is unreadable) we shell out to `ps`, the
|
|
11
|
+
// POSIX-portable answer. Returns null on any uncertainty; callers treat null
|
|
12
|
+
// as "not Temporal", which is the safe direction for a PID-reuse guard.
|
|
13
|
+
//
|
|
14
|
+
// Why the /proc reader exists: `node:22-slim` and its kin ship no `ps`. With
|
|
15
|
+
// `ps` as the only reader, every liveness probe answered "not Temporal", the
|
|
16
|
+
// supervisor restarted a healthy dev server every tick, and each restart spawned
|
|
17
|
+
// a second process that failed to bind while the first kept the port. A
|
|
18
|
+
// dependency on a userland tool is the wrong shape for a liveness check that
|
|
19
|
+
// the kernel can answer directly.
|
|
7
20
|
|
|
8
21
|
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
9
24
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
/** Where the kernel publishes process metadata on Linux; null where it does not. */
|
|
26
|
+
const DEFAULT_PROC_ROOT: string | null = process.platform === "linux" ? "/proc" : null;
|
|
27
|
+
|
|
28
|
+
export interface ProcessInspectOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Root of a procfs tree to read `<pid>/cmdline` from. Defaults to `/proc` on
|
|
31
|
+
* Linux and to none elsewhere; tests point it at a fixture directory.
|
|
32
|
+
*/
|
|
33
|
+
procRoot?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Full command line of a process, or null if it cannot be read. */
|
|
37
|
+
export function processCommandLine(pid: number, options: ProcessInspectOptions = {}): string | null {
|
|
38
|
+
const procRoot = options.procRoot === undefined ? DEFAULT_PROC_ROOT : options.procRoot;
|
|
39
|
+
if (procRoot !== null) {
|
|
40
|
+
const fromProc = readProcCmdline(procRoot, pid);
|
|
41
|
+
if (fromProc !== null) return fromProc;
|
|
19
42
|
}
|
|
43
|
+
return psCommandLine(pid);
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
/**
|
|
23
47
|
* Heuristically confirm that `pid` is the Temporal server we manage: its command
|
|
24
48
|
* line mentions "temporal" and either our binary path or a "server" subcommand.
|
|
25
49
|
*/
|
|
26
|
-
export function isLikelyTemporal(pid: number, binPath: string): boolean {
|
|
27
|
-
const cmd = processCommandLine(pid);
|
|
50
|
+
export function isLikelyTemporal(pid: number, binPath: string, options: ProcessInspectOptions = {}): boolean {
|
|
51
|
+
const cmd = processCommandLine(pid, options);
|
|
28
52
|
if (cmd === null) return false;
|
|
29
53
|
const lower = cmd.toLowerCase();
|
|
30
54
|
if (!lower.includes("temporal")) return false;
|
|
31
55
|
return cmd.includes(binPath) || lower.includes("server");
|
|
32
56
|
}
|
|
57
|
+
|
|
58
|
+
// /proc/<pid>/cmdline is argv joined by NUL bytes (with a trailing NUL). An
|
|
59
|
+
// empty file means a zombie or a kernel thread — nothing we can name, so null
|
|
60
|
+
// lets the caller fall through to `ps`.
|
|
61
|
+
function readProcCmdline(procRoot: string, pid: number): string | null {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(join(procRoot, String(pid), "cmdline"), "utf8");
|
|
64
|
+
const argv = raw.split("\0").filter((arg) => arg !== "");
|
|
65
|
+
return argv.length === 0 ? null : argv.join(" ");
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function psCommandLine(pid: number): string | null {
|
|
72
|
+
try {
|
|
73
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
76
|
+
}).trim();
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
@@ -28,4 +28,37 @@ describe("TemporalManager", () => {
|
|
|
28
28
|
const manager = TemporalManager.forHome(tempHome());
|
|
29
29
|
expect(await manager.isRunning()).toBe(false);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
// A stale PID file naming THIS process (the restarted container's fresh PID
|
|
33
|
+
// namespace reused it) can never be a running Temporal.
|
|
34
|
+
it("reports not-running when the PID file names the current process", async () => {
|
|
35
|
+
const home = tempHome();
|
|
36
|
+
writePidFile(temporalPidFile(home), process.pid);
|
|
37
|
+
expect(await TemporalManager.forHome(home).isRunning()).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("STIGMER_TEMPORAL_BIN", () => {
|
|
41
|
+
it("relocates only the binary; data, log, PID and lock stay under the home", async () => {
|
|
42
|
+
const home = tempHome();
|
|
43
|
+
const binPath = join(tempHome(), "opt", "temporal");
|
|
44
|
+
mkdirSync(join(binPath, ".."), { recursive: true });
|
|
45
|
+
writeFileSync(binPath, "#!/bin/sh\n");
|
|
46
|
+
|
|
47
|
+
const manager = TemporalManager.forHome(home, { STIGMER_TEMPORAL_BIN: binPath });
|
|
48
|
+
expect(manager.binPath).toBe(binPath);
|
|
49
|
+
|
|
50
|
+
// Nothing to download: the pre-installed binary is what ensureInstalled checks for.
|
|
51
|
+
await manager.ensureInstalled();
|
|
52
|
+
expect(existsSync(join(home, ".stigmer", "bin", "temporal"))).toBe(false);
|
|
53
|
+
// State still lives in the home layout.
|
|
54
|
+
writePidFile(temporalPidFile(home), 5151);
|
|
55
|
+
expect(manager.getPid()).toBe(5151);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("is ignored when empty, keeping the ~/.stigmer/bin default", () => {
|
|
59
|
+
const home = tempHome();
|
|
60
|
+
const manager = TemporalManager.forHome(home, { STIGMER_TEMPORAL_BIN: "" });
|
|
61
|
+
expect(manager.binPath).toBe(join(home, ".stigmer", "bin", "temporal"));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
31
64
|
});
|
|
@@ -19,7 +19,7 @@ import { binDir, logDir, temporalDataDir, temporalLockFile, temporalPidFile } fr
|
|
|
19
19
|
import { TEMPORAL_PORT, TEMPORAL_UI_PORT } from "../constants.js";
|
|
20
20
|
import { type FileLock, acquireLock } from "../state/lock.js";
|
|
21
21
|
import { readPidFile, removePidFile, writePidFile } from "../state/pidfile.js";
|
|
22
|
-
import { findProcessByPort,
|
|
22
|
+
import { findProcessByPort, isOtherLiveProcess, killProcessGroup } from "../state/proc.js";
|
|
23
23
|
import { DEFAULT_TEMPORAL_VERSION, downloadTemporalCli, isTemporalInstalled } from "./download.js";
|
|
24
24
|
import { isLikelyTemporal } from "./inspect.js";
|
|
25
25
|
|
|
@@ -27,8 +27,16 @@ const READY_TIMEOUT_MS = 10_000;
|
|
|
27
27
|
const STOP_POLL_MS = 500;
|
|
28
28
|
const STOP_TIMEOUT_MS = 10_000;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Env var naming a pre-installed `temporal` binary to use instead of the one
|
|
32
|
+
* the CLI downloads into ~/.stigmer/bin — the STIGMER_NODE_BIN precedent. The
|
|
33
|
+
* all-in-one image bakes the binary read-only off its data volume and points
|
|
34
|
+
* here; `ensureInstalled` then has nothing to download.
|
|
35
|
+
*/
|
|
36
|
+
export const TEMPORAL_BIN_ENV = "STIGMER_TEMPORAL_BIN";
|
|
37
|
+
|
|
30
38
|
export interface TemporalManagerOptions {
|
|
31
|
-
/** Path to the temporal binary (~/.stigmer/bin/temporal). */
|
|
39
|
+
/** Path to the temporal binary (~/.stigmer/bin/temporal, or STIGMER_TEMPORAL_BIN). */
|
|
32
40
|
binPath: string;
|
|
33
41
|
/** Temporal dev-server data directory (~/.stigmer/temporal-data). */
|
|
34
42
|
dataDir: string;
|
|
@@ -44,7 +52,8 @@ export interface TemporalManagerOptions {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export class TemporalManager {
|
|
47
|
-
|
|
55
|
+
/** The `temporal` binary this manager runs (downloaded, or pre-installed via STIGMER_TEMPORAL_BIN). */
|
|
56
|
+
readonly binPath: string;
|
|
48
57
|
private readonly dataDir: string;
|
|
49
58
|
private readonly logFile: string;
|
|
50
59
|
private readonly pidFile: string;
|
|
@@ -65,10 +74,15 @@ export class TemporalManager {
|
|
|
65
74
|
this.uiPort = options.uiPort ?? TEMPORAL_UI_PORT;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Build a manager from the standard ~/.stigmer layout. The binary alone may
|
|
79
|
+
* be relocated through STIGMER_TEMPORAL_BIN; the dev server's data, log,
|
|
80
|
+
* PID and lock stay under the home, because they are state, not software.
|
|
81
|
+
*/
|
|
82
|
+
static forHome(home: string = homedir(), env: NodeJS.ProcessEnv = process.env): TemporalManager {
|
|
83
|
+
const binOverride = env[TEMPORAL_BIN_ENV];
|
|
70
84
|
return new TemporalManager({
|
|
71
|
-
binPath: join(binDir(home), "temporal"),
|
|
85
|
+
binPath: binOverride !== undefined && binOverride !== "" ? binOverride : join(binDir(home), "temporal"),
|
|
72
86
|
dataDir: temporalDataDir(home),
|
|
73
87
|
logFile: join(logDir(home), "temporal.log"),
|
|
74
88
|
pidFile: temporalPidFile(home),
|
|
@@ -183,7 +197,7 @@ export class TemporalManager {
|
|
|
183
197
|
async isRunning(): Promise<boolean> {
|
|
184
198
|
const pid = this.getPid();
|
|
185
199
|
if (pid === null) return false;
|
|
186
|
-
if (!
|
|
200
|
+
if (!isOtherLiveProcess(pid)) return false;
|
|
187
201
|
if (!isLikelyTemporal(pid, this.binPath)) return false;
|
|
188
202
|
return this.isPortInUse();
|
|
189
203
|
}
|
|
@@ -229,7 +243,7 @@ export class TemporalManager {
|
|
|
229
243
|
private cleanupStaleProcesses(): void {
|
|
230
244
|
const pid = this.getPid();
|
|
231
245
|
if (pid !== null) {
|
|
232
|
-
if (!
|
|
246
|
+
if (!isOtherLiveProcess(pid) || !isLikelyTemporal(pid, this.binPath)) {
|
|
233
247
|
removePidFile(this.pidFile);
|
|
234
248
|
}
|
|
235
249
|
return;
|
|
@@ -149,8 +149,8 @@ function countingClient(preferences: StubPreferences): { client: Stigmer; calls:
|
|
|
149
149
|
return { client, calls: () => calls };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/** prepareAgentExec options as `run` passes them:
|
|
153
|
-
const
|
|
152
|
+
/** prepareAgentExec options as `run` passes them: the kind is served here + harness opt-in. */
|
|
153
|
+
const SERVED_RUN_OPTIONS = { accountPreferencesAvailable: true, applyAccountHarnessDefault: true } as const;
|
|
154
154
|
|
|
155
155
|
describe("prepareAgentExec env injection", () => {
|
|
156
156
|
it("injects STIGMER_ORG_ID when absent", async () => {
|
|
@@ -193,7 +193,7 @@ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", (
|
|
|
193
193
|
clientWithPreference("claude-sonnet-4.6"),
|
|
194
194
|
"acme",
|
|
195
195
|
undefined,
|
|
196
|
-
{
|
|
196
|
+
{ accountPreferencesAvailable: true },
|
|
197
197
|
);
|
|
198
198
|
expect(prepared.model).toBe("claude-sonnet-4.6");
|
|
199
199
|
});
|
|
@@ -204,20 +204,22 @@ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", (
|
|
|
204
204
|
clientWithPreference("claude-sonnet-4.6"),
|
|
205
205
|
"acme",
|
|
206
206
|
undefined,
|
|
207
|
-
{
|
|
207
|
+
{ accountPreferencesAvailable: true },
|
|
208
208
|
);
|
|
209
209
|
expect(prepared.model).toBe("gpt-5.3");
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
it("never consults identity
|
|
213
|
-
// The failing stub doubles as a call detector:
|
|
214
|
-
// attempt whoAmI, so a rejecting
|
|
212
|
+
it("never consults identity where the kind is not served", async () => {
|
|
213
|
+
// The failing stub doubles as a call detector: a backend that does not
|
|
214
|
+
// serve identity accounts must not even attempt whoAmI, so a rejecting
|
|
215
|
+
// client cannot affect the result. (20260911.11 A3: the gate is the
|
|
216
|
+
// kind's tier, not the backend type — client/edition.ts resourceServedOn.)
|
|
215
217
|
const prepared = await prepareAgentExec(
|
|
216
218
|
BASE_FLAGS,
|
|
217
219
|
clientWithFailingWhoAmI(),
|
|
218
220
|
"acme",
|
|
219
221
|
undefined,
|
|
220
|
-
{
|
|
222
|
+
{ accountPreferencesAvailable: false },
|
|
221
223
|
);
|
|
222
224
|
expect(prepared.model).toBe("");
|
|
223
225
|
});
|
|
@@ -228,7 +230,7 @@ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", (
|
|
|
228
230
|
clientWithFailingWhoAmI(),
|
|
229
231
|
"acme",
|
|
230
232
|
undefined,
|
|
231
|
-
{
|
|
233
|
+
{ accountPreferencesAvailable: true },
|
|
232
234
|
);
|
|
233
235
|
expect(prepared.model).toBe("");
|
|
234
236
|
});
|
|
@@ -239,7 +241,7 @@ describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", (
|
|
|
239
241
|
clientWithPreference(""),
|
|
240
242
|
"acme",
|
|
241
243
|
undefined,
|
|
242
|
-
{
|
|
244
|
+
{ accountPreferencesAvailable: true },
|
|
243
245
|
);
|
|
244
246
|
expect(prepared.model).toBe("");
|
|
245
247
|
});
|
|
@@ -252,7 +254,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
252
254
|
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
253
255
|
"acme",
|
|
254
256
|
undefined,
|
|
255
|
-
|
|
257
|
+
SERVED_RUN_OPTIONS,
|
|
256
258
|
);
|
|
257
259
|
expect(prepared.harness).toBe("cursor");
|
|
258
260
|
});
|
|
@@ -265,7 +267,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
265
267
|
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
266
268
|
"acme",
|
|
267
269
|
undefined,
|
|
268
|
-
|
|
270
|
+
SERVED_RUN_OPTIONS,
|
|
269
271
|
);
|
|
270
272
|
expect(prepared.harness).toBe("native");
|
|
271
273
|
});
|
|
@@ -279,7 +281,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
279
281
|
clientWithPreferences({ defaultHarness: "cursor" }),
|
|
280
282
|
"acme",
|
|
281
283
|
undefined,
|
|
282
|
-
{
|
|
284
|
+
{ accountPreferencesAvailable: true },
|
|
283
285
|
);
|
|
284
286
|
expect(prepared.harness).toBe("");
|
|
285
287
|
});
|
|
@@ -290,7 +292,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
290
292
|
STUB_CLIENT,
|
|
291
293
|
"acme",
|
|
292
294
|
undefined,
|
|
293
|
-
{
|
|
295
|
+
{ accountPreferencesAvailable: false },
|
|
294
296
|
);
|
|
295
297
|
expect(prepared.harness).toBe("cursor");
|
|
296
298
|
});
|
|
@@ -303,7 +305,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
303
305
|
clientWithPreferences({ defaultHarness: "devin" }),
|
|
304
306
|
"acme",
|
|
305
307
|
undefined,
|
|
306
|
-
|
|
308
|
+
SERVED_RUN_OPTIONS,
|
|
307
309
|
);
|
|
308
310
|
expect(prepared.harness).toBe("");
|
|
309
311
|
});
|
|
@@ -314,7 +316,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
314
316
|
clientWithFailingWhoAmI(),
|
|
315
317
|
"acme",
|
|
316
318
|
undefined,
|
|
317
|
-
|
|
319
|
+
SERVED_RUN_OPTIONS,
|
|
318
320
|
);
|
|
319
321
|
expect(prepared.harness).toBe("");
|
|
320
322
|
expect(prepared.model).toBe("");
|
|
@@ -327,7 +329,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
327
329
|
clientWithFailingWhoAmI(),
|
|
328
330
|
"acme",
|
|
329
331
|
undefined,
|
|
330
|
-
{
|
|
332
|
+
{ accountPreferencesAvailable: false, applyAccountHarnessDefault: true },
|
|
331
333
|
);
|
|
332
334
|
expect(prepared.harness).toBe("");
|
|
333
335
|
});
|
|
@@ -338,7 +340,7 @@ describe("prepareAgentExec harness resolution (oss#293)", () => {
|
|
|
338
340
|
clientWithPreferences({}),
|
|
339
341
|
"acme",
|
|
340
342
|
undefined,
|
|
341
|
-
|
|
343
|
+
SERVED_RUN_OPTIONS,
|
|
342
344
|
);
|
|
343
345
|
expect(prepared.harness).toBe("");
|
|
344
346
|
});
|
|
@@ -356,7 +358,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
356
358
|
clientWithPreferences(BOTH_MODELS),
|
|
357
359
|
"acme",
|
|
358
360
|
undefined,
|
|
359
|
-
|
|
361
|
+
SERVED_RUN_OPTIONS,
|
|
360
362
|
);
|
|
361
363
|
expect(prepared.model).toBe("composer-2.5");
|
|
362
364
|
});
|
|
@@ -367,7 +369,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
367
369
|
clientWithPreferences({ ...BOTH_MODELS, defaultHarness: "cursor" }),
|
|
368
370
|
"acme",
|
|
369
371
|
undefined,
|
|
370
|
-
|
|
372
|
+
SERVED_RUN_OPTIONS,
|
|
371
373
|
);
|
|
372
374
|
expect(prepared.harness).toBe("cursor");
|
|
373
375
|
expect(prepared.model).toBe("composer-2.5");
|
|
@@ -379,7 +381,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
379
381
|
clientWithPreferences(BOTH_MODELS),
|
|
380
382
|
"acme",
|
|
381
383
|
undefined,
|
|
382
|
-
|
|
384
|
+
SERVED_RUN_OPTIONS,
|
|
383
385
|
);
|
|
384
386
|
expect(explicit.model).toBe("claude-sonnet-4.6");
|
|
385
387
|
|
|
@@ -388,7 +390,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
388
390
|
clientWithPreferences(BOTH_MODELS),
|
|
389
391
|
"acme",
|
|
390
392
|
undefined,
|
|
391
|
-
|
|
393
|
+
SERVED_RUN_OPTIONS,
|
|
392
394
|
);
|
|
393
395
|
expect(unset.model).toBe("claude-sonnet-4.6");
|
|
394
396
|
});
|
|
@@ -399,7 +401,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
399
401
|
clientWithPreferences(BOTH_MODELS),
|
|
400
402
|
"acme",
|
|
401
403
|
undefined,
|
|
402
|
-
|
|
404
|
+
SERVED_RUN_OPTIONS,
|
|
403
405
|
);
|
|
404
406
|
expect(prepared.model).toBe("gpt-5.3");
|
|
405
407
|
});
|
|
@@ -412,7 +414,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
412
414
|
clientWithPreferences(BOTH_MODELS),
|
|
413
415
|
"acme",
|
|
414
416
|
undefined,
|
|
415
|
-
{
|
|
417
|
+
{ accountPreferencesAvailable: true },
|
|
416
418
|
);
|
|
417
419
|
expect(prepared.harness).toBe("cursor");
|
|
418
420
|
expect(prepared.model).toBe("composer-2.5");
|
|
@@ -420,7 +422,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
420
422
|
|
|
421
423
|
it("serves both fills from one whoAmI round trip, and none when both flags are explicit", async () => {
|
|
422
424
|
const single = countingClient({ ...BOTH_MODELS, defaultHarness: "cursor" });
|
|
423
|
-
const filled = await prepareAgentExec(BASE_FLAGS, single.client, "acme", undefined,
|
|
425
|
+
const filled = await prepareAgentExec(BASE_FLAGS, single.client, "acme", undefined, SERVED_RUN_OPTIONS);
|
|
424
426
|
expect(filled.harness).toBe("cursor");
|
|
425
427
|
expect(filled.model).toBe("composer-2.5");
|
|
426
428
|
expect(single.calls(), "harness + model fills share one whoAmI").toBe(1);
|
|
@@ -431,7 +433,7 @@ describe("prepareAgentExec harness-aware model fill (oss#293)", () => {
|
|
|
431
433
|
skipped.client,
|
|
432
434
|
"acme",
|
|
433
435
|
undefined,
|
|
434
|
-
|
|
436
|
+
SERVED_RUN_OPTIONS,
|
|
435
437
|
);
|
|
436
438
|
expect(explicit.harness).toBe("native");
|
|
437
439
|
expect(explicit.model).toBe("gpt-5.3");
|