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.
- package/AGENTS.md +56 -0
- package/CONTRIBUTING.md +35 -0
- package/README.md +38 -58
- package/SECURITY.md +24 -0
- package/bin/orkestrate.ts +2 -0
- package/docs/concepts.md +119 -0
- package/docs/demo-extension-builder.md +82 -0
- package/docs/extensions/adapters.md +57 -0
- package/docs/extensions/architecture.md +49 -0
- package/docs/extensions/introduction.md +26 -0
- package/docs/getting-started.md +85 -0
- package/docs/hosted-registry.md +90 -0
- package/docs/pack-authoring.md +75 -0
- package/docs/roadmap.md +59 -0
- package/docs/troubleshooting.md +28 -0
- package/extensions/opencode-adapter/index.ts +106 -0
- package/extensions/opencode-adapter/orkestrate.extension.json +17 -0
- package/package.json +40 -33
- package/packs/coding/harnesses/opencode/agents/coding.md +8 -0
- package/packs/coding/harnesses/opencode/opencode.json +24 -0
- package/packs/coding/harnesses/opencode/skills/orkestrate/SKILL.md +57 -0
- package/packs/coding/pack.yaml +5 -0
- package/packs/extension-builder/harnesses/opencode/agents/extension-builder.md +8 -0
- package/packs/extension-builder/harnesses/opencode/opencode.json +31 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate/SKILL.md +54 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate-pack-author/SKILL.md +59 -0
- package/packs/extension-builder/pack.yaml +5 -0
- package/src/cli/cmd/extension-submit.ts +267 -0
- package/src/cli/cmd/pack-create.ts +43 -0
- package/src/cli/cmd/pack.ts +53 -0
- package/src/cli/cmd/profile-create.ts +199 -0
- package/src/cli/cmd/profile-submit.ts +236 -0
- package/src/cli/cmd/profile-validate.ts +5 -0
- package/src/cli/cmd/registry.ts +66 -0
- package/src/cli/cmd/run.ts +37 -0
- package/src/cli/index.ts +163 -0
- package/src/cli/tui.ts +355 -0
- package/src/cli/ui/welcome.ts +73 -0
- package/src/cli.ts +1 -0
- package/src/sdk/cross-platform.ts +25 -0
- package/src/sdk/extensions/loader.ts +89 -0
- package/src/sdk/extensions/manifest.ts +193 -0
- package/src/sdk/extensions/types.ts +12 -0
- package/src/sdk/harness/sync-slice.ts +57 -0
- package/src/sdk/launch/broker.ts +87 -0
- package/src/sdk/launch/runner.ts +57 -0
- package/src/sdk/launch/terminal.ts +75 -0
- package/src/sdk/launch/types.ts +7 -0
- package/src/sdk/launch/windows.ts +109 -0
- package/src/sdk/packs/catalog.ts +172 -0
- package/src/sdk/packs/create.ts +99 -0
- package/src/sdk/packs/fs.ts +52 -0
- package/src/sdk/packs/github.ts +249 -0
- package/src/sdk/packs/paths.ts +19 -0
- package/src/sdk/packs/registry.ts +40 -0
- package/src/sdk/packs/schema.ts +51 -0
- package/src/sdk/packs/store.ts +172 -0
- package/src/sdk/profiles/catalog.ts +199 -0
- package/src/sdk/profiles/github.ts +177 -0
- package/src/sdk/profiles/install.ts +161 -0
- package/src/sdk/profiles/load.ts +209 -0
- package/src/sdk/profiles/materialize.ts +85 -0
- package/src/sdk/profiles/pack.ts +128 -0
- package/src/sdk/profiles/schema.ts +201 -0
- package/src/sdk/registry.ts +19 -0
- package/src/sdk/runs/registry.ts +142 -0
- package/src/sdk/runs/types.ts +15 -0
- package/src/sdk/types.ts +39 -0
- package/src/version.ts +3 -0
- package/dist/cli.js +0 -1668
- package/dist/cli.js.map +0 -1
- package/dist/mcp-entry.js +0 -181
- 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,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
|
+
}
|