orkestrate 0.1.14 → 0.2.0

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.
Files changed (73) hide show
  1. package/AGENTS.md +56 -0
  2. package/CONTRIBUTING.md +35 -0
  3. package/README.md +38 -58
  4. package/SECURITY.md +24 -0
  5. package/bin/orkestrate.ts +2 -0
  6. package/docs/concepts.md +119 -0
  7. package/docs/demo-extension-builder.md +82 -0
  8. package/docs/extensions/adapters.md +57 -0
  9. package/docs/extensions/architecture.md +49 -0
  10. package/docs/extensions/introduction.md +26 -0
  11. package/docs/getting-started.md +85 -0
  12. package/docs/hosted-registry.md +90 -0
  13. package/docs/pack-authoring.md +75 -0
  14. package/docs/roadmap.md +59 -0
  15. package/docs/troubleshooting.md +28 -0
  16. package/extensions/opencode-adapter/index.ts +106 -0
  17. package/extensions/opencode-adapter/orkestrate.extension.json +17 -0
  18. package/package.json +40 -33
  19. package/packs/coding/harnesses/opencode/agents/coding.md +8 -0
  20. package/packs/coding/harnesses/opencode/opencode.json +24 -0
  21. package/packs/coding/harnesses/opencode/skills/orkestrate/SKILL.md +57 -0
  22. package/packs/coding/pack.yaml +5 -0
  23. package/packs/extension-builder/harnesses/opencode/agents/extension-builder.md +8 -0
  24. package/packs/extension-builder/harnesses/opencode/opencode.json +31 -0
  25. package/packs/extension-builder/harnesses/opencode/skills/orkestrate/SKILL.md +54 -0
  26. package/packs/extension-builder/harnesses/opencode/skills/orkestrate-pack-author/SKILL.md +59 -0
  27. package/packs/extension-builder/pack.yaml +5 -0
  28. package/src/cli/cmd/extension-submit.ts +267 -0
  29. package/src/cli/cmd/pack-create.ts +43 -0
  30. package/src/cli/cmd/pack.ts +53 -0
  31. package/src/cli/cmd/profile-create.ts +199 -0
  32. package/src/cli/cmd/profile-submit.ts +236 -0
  33. package/src/cli/cmd/profile-validate.ts +5 -0
  34. package/src/cli/cmd/registry.ts +66 -0
  35. package/src/cli/cmd/run.ts +37 -0
  36. package/src/cli/index.ts +163 -0
  37. package/src/cli/tui.ts +355 -0
  38. package/src/cli/ui/welcome.ts +73 -0
  39. package/src/cli.ts +1 -0
  40. package/src/sdk/cross-platform.ts +25 -0
  41. package/src/sdk/extensions/loader.ts +89 -0
  42. package/src/sdk/extensions/manifest.ts +193 -0
  43. package/src/sdk/extensions/types.ts +12 -0
  44. package/src/sdk/harness/sync-slice.ts +57 -0
  45. package/src/sdk/launch/broker.ts +87 -0
  46. package/src/sdk/launch/runner.ts +57 -0
  47. package/src/sdk/launch/terminal.ts +75 -0
  48. package/src/sdk/launch/types.ts +7 -0
  49. package/src/sdk/launch/windows.ts +109 -0
  50. package/src/sdk/packs/catalog.ts +172 -0
  51. package/src/sdk/packs/create.ts +99 -0
  52. package/src/sdk/packs/fs.ts +52 -0
  53. package/src/sdk/packs/github.ts +249 -0
  54. package/src/sdk/packs/paths.ts +19 -0
  55. package/src/sdk/packs/registry.ts +40 -0
  56. package/src/sdk/packs/schema.ts +51 -0
  57. package/src/sdk/packs/store.ts +172 -0
  58. package/src/sdk/profiles/catalog.ts +199 -0
  59. package/src/sdk/profiles/github.ts +177 -0
  60. package/src/sdk/profiles/install.ts +161 -0
  61. package/src/sdk/profiles/load.ts +209 -0
  62. package/src/sdk/profiles/materialize.ts +85 -0
  63. package/src/sdk/profiles/pack.ts +128 -0
  64. package/src/sdk/profiles/schema.ts +201 -0
  65. package/src/sdk/registry.ts +19 -0
  66. package/src/sdk/runs/registry.ts +142 -0
  67. package/src/sdk/runs/types.ts +15 -0
  68. package/src/sdk/types.ts +39 -0
  69. package/src/version.ts +3 -0
  70. package/dist/cli.js +0 -1668
  71. package/dist/cli.js.map +0 -1
  72. package/dist/mcp-entry.js +0 -181
  73. package/dist/mcp-entry.js.map +0 -1
@@ -0,0 +1,193 @@
1
+ export type ExtensionType =
2
+ | "adapter"
3
+ | "profile-pack"
4
+ | "skill-pack"
5
+ | "mcp-pack"
6
+ | "command-pack";
7
+
8
+ export interface ExtensionManifest {
9
+ id: string;
10
+ name: string;
11
+ version: string;
12
+ type: ExtensionType;
13
+ description: string;
14
+ author?: string;
15
+ repository?: string;
16
+ license?: string;
17
+ keywords?: string[];
18
+
19
+ // Entry point
20
+ entry: string;
21
+
22
+ // Type-specific fields
23
+ harness?: string; // for adapter type
24
+ capabilities?: { // for adapter type
25
+ mcp?: boolean;
26
+ systemPrompt?: boolean;
27
+ skills?: boolean;
28
+ modelSelection?: boolean;
29
+ };
30
+
31
+ profiles?: string[]; // for profile-pack type
32
+ skills?: string[]; // for skill-pack type
33
+ mcpServers?: string[]; // for mcp-pack type
34
+ commands?: string[]; // for command-pack type
35
+
36
+ // Dependencies
37
+ dependencies?: Record<string, string>;
38
+ peerDependencies?: Record<string, string>;
39
+
40
+ // Compatibility
41
+ orkestrateVersion?: string;
42
+ minOrkestrateVersion?: string;
43
+ }
44
+
45
+ export const EXTENSION_MANIFEST_SCHEMA = {
46
+ type: "object",
47
+ required: ["id", "name", "version", "type", "description", "entry"],
48
+ properties: {
49
+ id: { type: "string", pattern: "^orkestrate\\.[a-z-]+\\.[a-z0-9-]+$" },
50
+ name: { type: "string", minLength: 1 },
51
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+(-.*)?$" },
52
+ type: { type: "string", enum: ["adapter", "profile-pack", "skill-pack", "mcp-pack", "command-pack"] },
53
+ description: { type: "string", minLength: 10 },
54
+ author: { type: "string" },
55
+ repository: { type: "string", format: "uri" },
56
+ license: { type: "string" },
57
+ keywords: { type: "array", items: { type: "string" } },
58
+ entry: { type: "string", minLength: 1 },
59
+ harness: { type: "string" },
60
+ capabilities: {
61
+ type: "object",
62
+ properties: {
63
+ mcp: { type: "boolean" },
64
+ systemPrompt: { type: "boolean" },
65
+ skills: { type: "boolean" },
66
+ modelSelection: { type: "boolean" },
67
+ },
68
+ },
69
+ profiles: { type: "array", items: { type: "string" } },
70
+ skills: { type: "array", items: { type: "string" } },
71
+ mcpServers: { type: "array", items: { type: "string" } },
72
+ commands: { type: "array", items: { type: "string" } },
73
+ dependencies: { type: "object", additionalProperties: { type: "string" } },
74
+ peerDependencies: { type: "object", additionalProperties: { type: "string" } },
75
+ orkestrateVersion: { type: "string" },
76
+ minOrkestrateVersion: { type: "string" },
77
+ },
78
+ } as const;
79
+
80
+ export function validateManifest(manifest: unknown): { valid: boolean; errors: string[] } {
81
+ const errors: string[] = [];
82
+
83
+ if (!manifest || typeof manifest !== "object") {
84
+ return { valid: false, errors: ["Manifest must be an object"] };
85
+ }
86
+
87
+ const m = manifest as Record<string, unknown>;
88
+
89
+ // Required fields
90
+ for (const field of ["id", "name", "version", "type", "description", "entry"]) {
91
+ if (!m[field] || typeof m[field] !== "string") {
92
+ errors.push(`Missing or invalid required field: ${field}`);
93
+ }
94
+ }
95
+
96
+ // Validate ID format
97
+ if (m.id && typeof m.id === "string") {
98
+ if (!/^orkestrate\.[a-z-]+\.[a-z0-9-]+$/.test(m.id)) {
99
+ errors.push('ID must follow format: orkestrate.<category>.<name> (e.g., "orkestrate.adapter.opencode")');
100
+ }
101
+ }
102
+
103
+ // Validate version
104
+ if (m.version && typeof m.version === "string") {
105
+ if (!/^\d+\.\d+\.\d+(-.*)?$/.test(m.version)) {
106
+ errors.push('Version must follow semver (e.g., "1.0.0" or "1.0.0-beta")');
107
+ }
108
+ }
109
+
110
+ // Validate type
111
+ const validTypes: ExtensionType[] = ["adapter", "profile-pack", "skill-pack", "mcp-pack", "command-pack"];
112
+ if (m.type && !validTypes.includes(m.type as ExtensionType)) {
113
+ errors.push(`Invalid type. Must be one of: ${validTypes.join(", ")}`);
114
+ }
115
+
116
+ // Type-specific validation
117
+ if (m.type === "adapter") {
118
+ if (!m.harness || typeof m.harness !== "string") {
119
+ errors.push('Adapter type requires "harness" field (e.g., "opencode")');
120
+ }
121
+ if (!m.capabilities || typeof m.capabilities !== "object") {
122
+ errors.push('Adapter type requires "capabilities" object');
123
+ }
124
+ }
125
+
126
+ if (m.type === "profile-pack") {
127
+ if (!m.profiles || !Array.isArray(m.profiles) || m.profiles.length === 0) {
128
+ errors.push('Profile-pack type requires non-empty "profiles" array');
129
+ }
130
+ }
131
+
132
+ if (m.type === "skill-pack") {
133
+ if (!m.skills || !Array.isArray(m.skills) || m.skills.length === 0) {
134
+ errors.push('Skill-pack type requires non-empty "skills" array');
135
+ }
136
+ }
137
+
138
+ return { valid: errors.length === 0, errors };
139
+ }
140
+
141
+ export function createManifestTemplate(type: ExtensionType): Partial<ExtensionManifest> {
142
+ const base = {
143
+ id: `orkestrate.${type}.my-extension`,
144
+ name: "My Extension",
145
+ version: "1.0.0",
146
+ type,
147
+ description: "A detailed description of what this extension does.",
148
+ author: "Your Name",
149
+ repository: "https://github.com/yourusername/your-repo",
150
+ license: "MIT",
151
+ keywords: ["orkestrate", type],
152
+ entry: "index.ts",
153
+ };
154
+
155
+ switch (type) {
156
+ case "adapter":
157
+ return {
158
+ ...base,
159
+ id: "orkestrate.adapter.myharness",
160
+ harness: "myharness",
161
+ capabilities: {
162
+ mcp: true,
163
+ systemPrompt: true,
164
+ skills: true,
165
+ modelSelection: true,
166
+ },
167
+ };
168
+ case "profile-pack":
169
+ return {
170
+ ...base,
171
+ id: "orkestrate.profile-pack.mypack",
172
+ profiles: ["profile1", "profile2"],
173
+ };
174
+ case "skill-pack":
175
+ return {
176
+ ...base,
177
+ id: "orkestrate.skill-pack.myskills",
178
+ skills: ["skill1", "skill2"],
179
+ };
180
+ case "mcp-pack":
181
+ return {
182
+ ...base,
183
+ id: "orkestrate.mcp-pack.mymcps",
184
+ mcpServers: ["server1", "server2"],
185
+ };
186
+ case "command-pack":
187
+ return {
188
+ ...base,
189
+ id: "orkestrate.command-pack.mycommands",
190
+ commands: ["command1", "command2"],
191
+ };
192
+ }
193
+ }
@@ -0,0 +1,12 @@
1
+ import type { HarnessDriver } from "../types";
2
+
3
+ export interface ExtensionContext {
4
+ registerAdapter(harness: string, adapter: HarnessDriver): void;
5
+ }
6
+
7
+ export interface OrkExtension {
8
+ id: string;
9
+ name: string;
10
+ version: string;
11
+ activate(ctx: ExtensionContext): void | Promise<void>;
12
+ }
@@ -0,0 +1,57 @@
1
+ import { cp, mkdir, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { pathExists } from "../packs/fs";
4
+
5
+ /**
6
+ * Seed OpenCode config from a pack harness slice without clobbering user edits.
7
+ * - First launch: copy full slice.
8
+ * - Later launches: add missing agents/skills/plugins only; never overwrite opencode.json.
9
+ */
10
+ export async function seedHarnessSlice(sliceDir: string, destDir: string): Promise<void> {
11
+ await mkdir(destDir, { recursive: true });
12
+
13
+ const destConfig = join(destDir, "opencode.json");
14
+ const isFirstSeed = !(await pathExists(destConfig));
15
+
16
+ if (isFirstSeed) {
17
+ await cp(sliceDir, destDir, { recursive: true, force: true });
18
+ return;
19
+ }
20
+
21
+ await mergeMissingSubtree(join(sliceDir, "agents"), join(destDir, "agents"));
22
+ await mergeMissingSubtree(join(sliceDir, "skills"), join(destDir, "skills"));
23
+ await mergeMissingSubtree(join(sliceDir, "plugins"), join(destDir, "plugins"));
24
+
25
+ for (const file of ["AGENTS.md", "README.md"]) {
26
+ const src = join(sliceDir, file);
27
+ const dst = join(destDir, file);
28
+ if ((await pathExists(src)) && !(await pathExists(dst))) {
29
+ await cp(src, dst, { force: true });
30
+ }
31
+ }
32
+ }
33
+
34
+ async function mergeMissingSubtree(srcDir: string, destDir: string): Promise<void> {
35
+ if (!(await pathExists(srcDir))) return;
36
+ await mkdir(destDir, { recursive: true });
37
+ await copyMissingRecursive(srcDir, destDir);
38
+ }
39
+
40
+ async function copyMissingRecursive(src: string, dest: string): Promise<void> {
41
+ const entries = await readdir(src, { withFileTypes: true });
42
+ for (const entry of entries) {
43
+ const srcPath = join(src, entry.name);
44
+ const destPath = join(dest, entry.name);
45
+ if (entry.isDirectory()) {
46
+ if (!(await pathExists(destPath))) {
47
+ await mkdir(destPath, { recursive: true });
48
+ }
49
+ await copyMissingRecursive(srcPath, destPath);
50
+ continue;
51
+ }
52
+ if (!(await pathExists(destPath))) {
53
+ await mkdir(join(destPath, ".."), { recursive: true });
54
+ await cp(srcPath, destPath, { force: true });
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,87 @@
1
+ import { getAdapter } from "../registry";
2
+ import type { Pack } from "../packs/schema";
3
+ import { resolvePack } from "../packs/store";
4
+ import { createRun, runDir, setRunState, updateRun } from "../runs/registry";
5
+ import { packHomePath } from "../packs/paths";
6
+ import { join } from "node:path";
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import type { RunRecord } from "../runs/types";
9
+ import { openInNewTerminal } from "./terminal";
10
+ import type { LaunchPlan } from "./types";
11
+ import { crossPlatformUtility } from "../cross-platform";
12
+
13
+ export type LaunchOptions = {
14
+ cwd?: string;
15
+ packId?: string;
16
+ };
17
+
18
+ export async function launchPack(packId: string, options: LaunchOptions = {}): Promise<RunRecord> {
19
+ const pack = await resolvePack(packId);
20
+ const cwd = options.cwd ?? process.cwd();
21
+ const adapter = getAdapter(pack.harness);
22
+ if (!adapter?.compile) {
23
+ throw new Error(`No driver registered for harness "${pack.harness}"`);
24
+ }
25
+
26
+ const { newRunId } = await import("../runs/registry");
27
+ const runId = newRunId();
28
+ const run = await createRun({
29
+ packId: pack.id,
30
+ harness: pack.harness,
31
+ cwd,
32
+ title: `orkestrate: ${pack.id} · ${runId}`,
33
+ runId,
34
+ });
35
+
36
+ const packHome = packHomePath(pack.id);
37
+ await mkdir(packHome, { recursive: true });
38
+
39
+ let plan: LaunchPlan;
40
+ try {
41
+ plan = await adapter.compile(pack, {
42
+ cwd,
43
+ runId: run.id,
44
+ packHome,
45
+ crossPlatform: crossPlatformUtility,
46
+ });
47
+ plan.title = run.title;
48
+ } catch (error) {
49
+ await setRunState(run.id, "failed", {
50
+ endedAt: new Date().toISOString(),
51
+ });
52
+ throw error;
53
+ }
54
+
55
+ const runnerScript = join(import.meta.dir, "runner.ts");
56
+ const dir = runDir(run.id);
57
+ await writeFile(join(dir, "launch.json"), JSON.stringify(plan, null, 2) + "\n");
58
+
59
+ try {
60
+ await openInNewTerminal({
61
+ plan,
62
+ runId: run.id,
63
+ runnerScript,
64
+ launchCmdPath: join(dir, "launch.cmd"),
65
+ });
66
+ return updateRun(run.id, { state: "starting" });
67
+ } catch (error) {
68
+ await setRunState(run.id, "failed", { endedAt: new Date().toISOString() });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export async function stopRun(runId: string): Promise<RunRecord> {
74
+ const { getRun } = await import("../runs/registry");
75
+ const run = await getRun(runId);
76
+ if (run.pid) {
77
+ try {
78
+ process.kill(run.pid);
79
+ } catch {
80
+ // process may already be gone
81
+ }
82
+ }
83
+ return updateRun(runId, {
84
+ state: "stopped",
85
+ endedAt: new Date().toISOString(),
86
+ });
87
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Executed inside the new terminal window. Tracks the real agent PID and exit state.
3
+ * Usage: bun runner.ts <runId>
4
+ */
5
+ import { spawn } from "node:child_process";
6
+ import { readFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { updateRun, runDir } from "../runs/registry";
9
+ import type { LaunchPlan } from "./types";
10
+
11
+ const runId = process.argv[2];
12
+ if (!runId) {
13
+ console.error("Usage: bun runner.ts <runId>");
14
+ process.exit(1);
15
+ }
16
+
17
+ const planPath = join(runDir(runId), "launch.json");
18
+ const plan = JSON.parse(await readFile(planPath, "utf-8")) as LaunchPlan;
19
+
20
+ await updateRun(runId, { state: "running" });
21
+
22
+ const child = spawn(plan.command, plan.args, {
23
+ cwd: plan.cwd,
24
+ env: { ...process.env, ...plan.env },
25
+ stdio: "inherit",
26
+ shell: process.platform === "win32",
27
+ });
28
+
29
+ if (child.pid) {
30
+ await updateRun(runId, { pid: child.pid });
31
+ }
32
+
33
+ child.on("exit", (code, signal) => {
34
+ void (async () => {
35
+ const endedAt = new Date().toISOString();
36
+ const exitCode = code ?? undefined;
37
+ const failed = signal != null || (code != null && code !== 0);
38
+ await updateRun(runId, {
39
+ state: failed ? "failed" : "exited",
40
+ exitCode,
41
+ endedAt,
42
+ });
43
+ if (process.platform === "win32" && failed) {
44
+ // Keep window open on failure so the user can read errors.
45
+ return;
46
+ }
47
+ process.exit(code ?? 1);
48
+ })();
49
+ });
50
+
51
+ child.on("error", (error) => {
52
+ console.error(error);
53
+ void updateRun(runId, {
54
+ state: "failed",
55
+ endedAt: new Date().toISOString(),
56
+ }).finally(() => process.exit(1));
57
+ });
@@ -0,0 +1,75 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { LaunchPlan } from "./types";
3
+ import { openWindowsTerminal, writeLaunchCmd } from "./windows";
4
+
5
+ export type TerminalLaunch = {
6
+ plan: LaunchPlan;
7
+ runId: string;
8
+ runnerScript: string;
9
+ launchCmdPath: string;
10
+ };
11
+
12
+ export async function openInNewTerminal(inv: TerminalLaunch): Promise<void> {
13
+ const { plan, runId, runnerScript } = inv;
14
+ const platform = process.platform;
15
+
16
+ if (platform === "win32") {
17
+ await openInNewTerminalWindows(inv);
18
+ return;
19
+ }
20
+
21
+ const bunExe = process.execPath;
22
+ const shellLine = `${bunExe} ${quoteUnix(runnerScript)} ${runId}`;
23
+
24
+ if (platform === "darwin") {
25
+ const script = `tell application "Terminal" to do script "cd ${escapeApple(plan.cwd)} && ${escapeApple(shellLine)}"`;
26
+ const proc = spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" });
27
+ proc.unref();
28
+ return;
29
+ }
30
+
31
+ const shellCmd = `cd ${quoteUnix(plan.cwd)} && ${shellLine}`;
32
+ const candidates = [
33
+ ["gnome-terminal", ["--", "bash", "-lc", shellCmd]],
34
+ ["konsole", ["-e", "bash", "-lc", shellCmd]],
35
+ ["xterm", ["-e", "bash", "-lc", shellCmd]],
36
+ ] as const;
37
+
38
+ for (const [bin, args] of candidates) {
39
+ try {
40
+ const proc = spawn(bin, [...args], { detached: true, stdio: "ignore" });
41
+ proc.unref();
42
+ return;
43
+ } catch {
44
+ continue;
45
+ }
46
+ }
47
+
48
+ throw new Error("No supported terminal emulator found (try Windows Terminal, Terminal.app, or gnome-terminal)");
49
+ }
50
+
51
+ async function openInNewTerminalWindows(inv: TerminalLaunch): Promise<void> {
52
+ const launchCmdPath = inv.launchCmdPath;
53
+ await writeLaunchCmd({
54
+ launchCmdPath,
55
+ bunExe: process.execPath,
56
+ runnerScript: inv.runnerScript,
57
+ runId: inv.runId,
58
+ cwd: inv.plan.cwd,
59
+ });
60
+
61
+ await openWindowsTerminal({
62
+ plan: inv.plan,
63
+ runId: inv.runId,
64
+ runnerScript: inv.runnerScript,
65
+ launchCmdPath,
66
+ });
67
+ }
68
+
69
+ function quoteUnix(value: string): string {
70
+ return `'${value.replace(/'/g, "'\\''")}'`;
71
+ }
72
+
73
+ function escapeApple(value: string): string {
74
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
75
+ }
@@ -0,0 +1,7 @@
1
+ export type LaunchPlan = {
2
+ command: string;
3
+ args: string[];
4
+ cwd: string;
5
+ env: Record<string, string>;
6
+ title: string;
7
+ };
@@ -0,0 +1,109 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import type { LaunchPlan } from "./types";
5
+
6
+ export type WindowsLaunch = {
7
+ plan: LaunchPlan;
8
+ runId: string;
9
+ runnerScript: string;
10
+ launchCmdPath: string;
11
+ };
12
+
13
+ /** Escape a value for a line inside a .cmd file. */
14
+ function quoteBatch(value: string): string {
15
+ return `"${value.replace(/"/g, '""')}"`;
16
+ }
17
+
18
+ /** Write launch.cmd with fully quoted paths (safe for spaces). */
19
+ export async function writeLaunchCmd(input: {
20
+ launchCmdPath: string;
21
+ bunExe: string;
22
+ runnerScript: string;
23
+ runId: string;
24
+ cwd: string;
25
+ }): Promise<void> {
26
+ const lines = [
27
+ "@echo off",
28
+ "setlocal",
29
+ `cd /d ${quoteBatch(input.cwd)}`,
30
+ `${quoteBatch(input.bunExe)} ${quoteBatch(input.runnerScript)} ${input.runId}`,
31
+ "set EXITCODE=%ERRORLEVEL%",
32
+ "if %EXITCODE% neq 0 (",
33
+ " echo.",
34
+ " echo Orkestrate session failed with exit code %EXITCODE%.",
35
+ " pause",
36
+ ")",
37
+ "endlocal",
38
+ "exit /b %EXITCODE%",
39
+ "",
40
+ ];
41
+ await mkdir(dirname(input.launchCmdPath), { recursive: true });
42
+ await writeFile(input.launchCmdPath, lines.join("\r\n"), "utf-8");
43
+ }
44
+
45
+ function spawnDetached(command: string, args: string[]): Promise<void> {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(command, args, {
48
+ detached: true,
49
+ stdio: "ignore",
50
+ windowsHide: true,
51
+ });
52
+
53
+ child.once("error", reject);
54
+ child.once("spawn", () => {
55
+ child.unref();
56
+ resolve();
57
+ });
58
+ });
59
+ }
60
+
61
+ /** Prefer direct executable args; fall back to launch.cmd. */
62
+ export async function openWindowsTerminal(inv: WindowsLaunch): Promise<void> {
63
+ const { plan, runId, runnerScript, launchCmdPath } = inv;
64
+ const bunExe = process.execPath;
65
+
66
+ const wtArgsDirect = [
67
+ "-w",
68
+ "-1",
69
+ "nt",
70
+ "-d",
71
+ plan.cwd,
72
+ "--title",
73
+ plan.title,
74
+ bunExe,
75
+ runnerScript,
76
+ runId,
77
+ ];
78
+
79
+ try {
80
+ await spawnDetached("wt.exe", wtArgsDirect);
81
+ return;
82
+ } catch {
83
+ // wt missing or failed — try batch launcher
84
+ }
85
+
86
+ const wtArgsBatch = [
87
+ "-w",
88
+ "-1",
89
+ "nt",
90
+ "-d",
91
+ plan.cwd,
92
+ "--title",
93
+ plan.title,
94
+ "cmd.exe",
95
+ "/d",
96
+ "/c",
97
+ launchCmdPath,
98
+ ];
99
+
100
+ try {
101
+ await spawnDetached("wt.exe", wtArgsBatch);
102
+ return;
103
+ } catch {
104
+ // fall through
105
+ }
106
+
107
+ // Last resort: new console via cmd start + launch.cmd
108
+ await spawnDetached("cmd.exe", ["/c", "start", plan.title, "cmd.exe", "/k", launchCmdPath]);
109
+ }