pilotswarm-cli 0.1.11 → 0.1.13
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/bin/tui.js +4 -276
- package/package.json +16 -11
- package/src/app.js +595 -0
- package/src/bootstrap-env.js +187 -0
- package/src/embedded-workers.js +65 -0
- package/src/index.js +152 -0
- package/src/node-sdk-transport.js +702 -0
- package/src/platform.js +899 -0
- package/tui-splash.txt +11 -0
- package/cli/tui.js +0 -7279
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const defaultTuiSplashPath = path.join(pkgRoot, "tui-splash.txt");
|
|
10
|
+
|
|
11
|
+
function readPluginMetadata(pluginDir) {
|
|
12
|
+
if (!pluginDir) return null;
|
|
13
|
+
const pluginJsonPath = path.join(pluginDir, "plugin.json");
|
|
14
|
+
if (!fs.existsSync(pluginJsonPath)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolvePluginDir(flags) {
|
|
23
|
+
if (flags.plugin) return path.resolve(flags.plugin);
|
|
24
|
+
if (process.env.PLUGIN_DIRS) {
|
|
25
|
+
const dirs = process.env.PLUGIN_DIRS.split(",").map((value) => value.trim()).filter(Boolean);
|
|
26
|
+
return dirs[0] || null;
|
|
27
|
+
}
|
|
28
|
+
const cwdPlugin = path.resolve("plugins");
|
|
29
|
+
if (fs.existsSync(cwdPlugin)) return cwdPlugin;
|
|
30
|
+
const bundledPlugin = path.join(pkgRoot, "plugins");
|
|
31
|
+
if (fs.existsSync(bundledPlugin)) return bundledPlugin;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveSystemMessage(flags) {
|
|
36
|
+
if (flags.system) {
|
|
37
|
+
if (fs.existsSync(flags.system)) {
|
|
38
|
+
return fs.readFileSync(flags.system, "utf-8").trim();
|
|
39
|
+
}
|
|
40
|
+
return flags.system;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pluginDir = resolvePluginDir(flags);
|
|
44
|
+
if (pluginDir) {
|
|
45
|
+
const systemMd = path.join(pluginDir, "system.md");
|
|
46
|
+
if (fs.existsSync(systemMd)) {
|
|
47
|
+
return fs.readFileSync(systemMd, "utf-8").trim();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.env.SYSTEM_MESSAGE) return process.env.SYSTEM_MESSAGE;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveTuiBranding(pluginDir) {
|
|
56
|
+
const pluginMeta = readPluginMetadata(pluginDir);
|
|
57
|
+
const tui = pluginMeta?.tui;
|
|
58
|
+
let defaultSplash = "{bold}{cyan-fg}PilotSwarm{/cyan-fg}{/bold}";
|
|
59
|
+
if (fs.existsSync(defaultTuiSplashPath)) {
|
|
60
|
+
defaultSplash = fs.readFileSync(defaultTuiSplashPath, "utf-8").trimEnd();
|
|
61
|
+
}
|
|
62
|
+
if (!tui || typeof tui !== "object") {
|
|
63
|
+
return { title: "PilotSwarm", splash: defaultSplash };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const title = typeof tui.title === "string" && tui.title.trim() ? tui.title.trim() : "PilotSwarm";
|
|
67
|
+
let splash = defaultSplash;
|
|
68
|
+
if (typeof tui.splash === "string" && tui.splash.trim()) {
|
|
69
|
+
splash = tui.splash;
|
|
70
|
+
} else if (typeof tui.splashFile === "string" && tui.splashFile.trim()) {
|
|
71
|
+
const splashPath = path.resolve(pluginDir, tui.splashFile);
|
|
72
|
+
if (!fs.existsSync(splashPath)) {
|
|
73
|
+
throw new Error(`TUI splash file not found: ${splashPath}`);
|
|
74
|
+
}
|
|
75
|
+
splash = fs.readFileSync(splashPath, "utf-8").trimEnd();
|
|
76
|
+
}
|
|
77
|
+
return { title, splash };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadEnvFile(envFile) {
|
|
81
|
+
if (!fs.existsSync(envFile)) return;
|
|
82
|
+
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
83
|
+
for (const line of envContent.split("\n")) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
86
|
+
const eqIdx = trimmed.indexOf("=");
|
|
87
|
+
if (eqIdx === -1) continue;
|
|
88
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
89
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
90
|
+
if (!process.env[key]) {
|
|
91
|
+
process.env[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureGithubToken() {
|
|
97
|
+
if (process.env.GITHUB_TOKEN) return;
|
|
98
|
+
try {
|
|
99
|
+
const token = execFileSync("gh", ["auth", "token"], {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
102
|
+
}).trim();
|
|
103
|
+
if (token) {
|
|
104
|
+
process.env.GITHUB_TOKEN = token;
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseCliIntoEnv(argv) {
|
|
110
|
+
const { values: flags, positionals } = parseArgs({
|
|
111
|
+
options: {
|
|
112
|
+
store: { type: "string", short: "s" },
|
|
113
|
+
env: { type: "string", short: "e" },
|
|
114
|
+
plugin: { type: "string", short: "p" },
|
|
115
|
+
worker: { type: "string", short: "w" },
|
|
116
|
+
workers: { type: "string", short: "n" },
|
|
117
|
+
model: { type: "string", short: "m" },
|
|
118
|
+
system: { type: "string" },
|
|
119
|
+
context: { type: "string", short: "c" },
|
|
120
|
+
namespace: { type: "string" },
|
|
121
|
+
label: { type: "string" },
|
|
122
|
+
help: { type: "boolean", short: "h" },
|
|
123
|
+
},
|
|
124
|
+
allowPositionals: true,
|
|
125
|
+
strict: false,
|
|
126
|
+
args: argv,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (flags.help) {
|
|
130
|
+
console.log(`
|
|
131
|
+
pilotswarm — PilotSwarm terminal UI
|
|
132
|
+
|
|
133
|
+
USAGE
|
|
134
|
+
npx pilotswarm [local|remote] [flags]
|
|
135
|
+
|
|
136
|
+
FLAGS
|
|
137
|
+
-e, --env <file> Env file
|
|
138
|
+
-p, --plugin <dir> Plugin directory
|
|
139
|
+
-w, --worker <module> Worker tools module
|
|
140
|
+
-n, --workers <count> Embedded worker count
|
|
141
|
+
-m, --model <name> Initial model
|
|
142
|
+
-c, --context <ctx> K8s context for remote mode
|
|
143
|
+
--namespace <ns> K8s namespace
|
|
144
|
+
--label <selector> K8s pod label
|
|
145
|
+
-h, --help Show help
|
|
146
|
+
`.trim());
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const mode = positionals[0] === "remote" ? "remote" : "local";
|
|
151
|
+
const envFile = flags.env || (mode === "remote" ? ".env.remote" : ".env");
|
|
152
|
+
loadEnvFile(envFile);
|
|
153
|
+
ensureGithubToken();
|
|
154
|
+
|
|
155
|
+
process.env.DATABASE_URL = flags.store || process.env.DATABASE_URL || "sqlite::memory:";
|
|
156
|
+
process.env.WORKERS = mode === "remote" ? "0" : (flags.workers ?? process.env.WORKERS ?? "4");
|
|
157
|
+
process.env.COPILOT_MODEL = flags.model || process.env.COPILOT_MODEL || "";
|
|
158
|
+
process.env.K8S_CONTEXT = flags.context || process.env.K8S_CONTEXT || "";
|
|
159
|
+
process.env.K8S_NAMESPACE = flags.namespace || process.env.K8S_NAMESPACE || "copilot-runtime";
|
|
160
|
+
process.env.K8S_POD_LABEL = flags.label || process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
|
|
161
|
+
|
|
162
|
+
const pluginDir = resolvePluginDir(flags);
|
|
163
|
+
if (pluginDir) {
|
|
164
|
+
process.env.PLUGIN_DIRS = pluginDir;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const branding = resolveTuiBranding(pluginDir);
|
|
168
|
+
process.env._TUI_TITLE = branding.title;
|
|
169
|
+
process.env._TUI_SPLASH = branding.splash;
|
|
170
|
+
|
|
171
|
+
const systemMessage = resolveSystemMessage(flags);
|
|
172
|
+
if (systemMessage) process.env._TUI_SYSTEM_MESSAGE = systemMessage;
|
|
173
|
+
|
|
174
|
+
if (mode === "local" && flags.worker) {
|
|
175
|
+
const resolvedWorker = path.resolve(flags.worker);
|
|
176
|
+
if (!fs.existsSync(resolvedWorker)) {
|
|
177
|
+
throw new Error(`Worker module not found: ${resolvedWorker}`);
|
|
178
|
+
}
|
|
179
|
+
process.env._TUI_WORKER_MODULE = resolvedWorker;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
mode,
|
|
184
|
+
store: process.env.DATABASE_URL,
|
|
185
|
+
branding,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { PilotSwarmWorker } from "pilotswarm-sdk";
|
|
6
|
+
|
|
7
|
+
export async function startEmbeddedWorkers({ count, store }) {
|
|
8
|
+
const workers = [];
|
|
9
|
+
if (!count || count <= 0) return workers;
|
|
10
|
+
|
|
11
|
+
const defaultPluginDir = path.resolve(process.cwd(), "packages/cli/plugins");
|
|
12
|
+
const pluginDirs = process.env.PLUGIN_DIRS
|
|
13
|
+
? process.env.PLUGIN_DIRS.split(",").map((value) => value.trim()).filter(Boolean)
|
|
14
|
+
: (fs.existsSync(defaultPluginDir) ? [defaultPluginDir] : []);
|
|
15
|
+
|
|
16
|
+
let workerModuleConfig = {};
|
|
17
|
+
if (process.env._TUI_WORKER_MODULE) {
|
|
18
|
+
const imported = await import(process.env._TUI_WORKER_MODULE);
|
|
19
|
+
workerModuleConfig = imported.default || imported;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sessionStateDir = process.env.SESSION_STATE_DIR || path.join(os.homedir(), ".copilot", "session-state");
|
|
23
|
+
const originalConsole = {
|
|
24
|
+
log: console.log,
|
|
25
|
+
warn: console.warn,
|
|
26
|
+
error: console.error,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < count; index++) {
|
|
30
|
+
console.log = () => {};
|
|
31
|
+
console.warn = () => {};
|
|
32
|
+
console.error = () => {};
|
|
33
|
+
try {
|
|
34
|
+
const worker = new PilotSwarmWorker({
|
|
35
|
+
store,
|
|
36
|
+
githubToken: process.env.GITHUB_TOKEN,
|
|
37
|
+
logLevel: process.env.LOG_LEVEL || "error",
|
|
38
|
+
sessionStateDir,
|
|
39
|
+
workerNodeId: `local-${index}`,
|
|
40
|
+
systemMessage: workerModuleConfig.systemMessage || process.env._TUI_SYSTEM_MESSAGE || undefined,
|
|
41
|
+
pluginDirs,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const workerTools = typeof workerModuleConfig.createTools === "function"
|
|
45
|
+
? await workerModuleConfig.createTools({ workerNodeId: `local-${index}`, workerIndex: index })
|
|
46
|
+
: workerModuleConfig.tools;
|
|
47
|
+
if (workerTools?.length) {
|
|
48
|
+
worker.registerTools(workerTools);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await worker.start();
|
|
52
|
+
workers.push(worker);
|
|
53
|
+
} finally {
|
|
54
|
+
console.log = originalConsole.log;
|
|
55
|
+
console.warn = originalConsole.warn;
|
|
56
|
+
console.error = originalConsole.error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return workers;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function stopEmbeddedWorkers(workers) {
|
|
64
|
+
await Promise.allSettled((workers || []).map((worker) => worker.stop()));
|
|
65
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import {
|
|
6
|
+
PilotSwarmUiController,
|
|
7
|
+
appReducer,
|
|
8
|
+
createInitialState,
|
|
9
|
+
createStore,
|
|
10
|
+
} from "pilotswarm-ui-core";
|
|
11
|
+
import { PilotSwarmTuiApp } from "./app.js";
|
|
12
|
+
import { createTuiPlatform } from "./platform.js";
|
|
13
|
+
import { NodeSdkTransport } from "./node-sdk-transport.js";
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
function setupTuiHostRuntime() {
|
|
18
|
+
const logFile = "/tmp/duroxide-tui.log";
|
|
19
|
+
const originalConsole = {
|
|
20
|
+
log: console.log.bind(console),
|
|
21
|
+
warn: console.warn.bind(console),
|
|
22
|
+
error: console.error.bind(console),
|
|
23
|
+
};
|
|
24
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
25
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
fs.writeFileSync(logFile, "");
|
|
29
|
+
} catch {}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { initTracing } = require("duroxide");
|
|
33
|
+
initTracing({
|
|
34
|
+
logFile,
|
|
35
|
+
logLevel: process.env.LOG_LEVEL || "info",
|
|
36
|
+
logFormat: "compact",
|
|
37
|
+
});
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
40
|
+
const appendLog = (...parts) => {
|
|
41
|
+
try {
|
|
42
|
+
const text = parts
|
|
43
|
+
.map((part) => {
|
|
44
|
+
if (typeof part === "string") return part;
|
|
45
|
+
if (part instanceof Error) return part.stack || part.message;
|
|
46
|
+
try { return JSON.stringify(part); } catch { return String(part); }
|
|
47
|
+
})
|
|
48
|
+
.join(" ");
|
|
49
|
+
fs.appendFileSync(logFile, `${text}\n`);
|
|
50
|
+
} catch {}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
console.log = (...args) => appendLog(...args);
|
|
54
|
+
console.warn = (...args) => appendLog(...args);
|
|
55
|
+
console.error = (...args) => appendLog(...args);
|
|
56
|
+
|
|
57
|
+
process.stderr.write = (chunk, encoding, cb) => {
|
|
58
|
+
try {
|
|
59
|
+
const text = typeof chunk === "string" ? chunk : chunk?.toString?.(encoding || "utf8");
|
|
60
|
+
if (text) appendLog(text.trimEnd());
|
|
61
|
+
} catch {}
|
|
62
|
+
if (typeof cb === "function") cb();
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
process.emitWarning = (warning, ...args) => {
|
|
67
|
+
appendLog("[warning]", warning, ...args);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
console.log = originalConsole.log;
|
|
72
|
+
console.warn = originalConsole.warn;
|
|
73
|
+
console.error = originalConsole.error;
|
|
74
|
+
process.stderr.write = originalStderrWrite;
|
|
75
|
+
process.emitWarning = originalEmitWarning;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearTerminalScreen() {
|
|
80
|
+
if (!process.stdout?.isTTY) return;
|
|
81
|
+
try {
|
|
82
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function startTuiApp(config) {
|
|
87
|
+
const restoreHostRuntime = setupTuiHostRuntime();
|
|
88
|
+
const platform = createTuiPlatform();
|
|
89
|
+
const transport = new NodeSdkTransport({
|
|
90
|
+
store: config.store,
|
|
91
|
+
mode: config.mode,
|
|
92
|
+
});
|
|
93
|
+
const store = createStore(appReducer, createInitialState({
|
|
94
|
+
mode: config.mode,
|
|
95
|
+
branding: config.branding,
|
|
96
|
+
}));
|
|
97
|
+
const controller = new PilotSwarmUiController({ store, transport });
|
|
98
|
+
let tuiApp;
|
|
99
|
+
let finalized = false;
|
|
100
|
+
let resolveExit;
|
|
101
|
+
const exitPromise = new Promise((resolve) => {
|
|
102
|
+
resolveExit = resolve;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const finalizeHost = () => {
|
|
106
|
+
if (finalized) return;
|
|
107
|
+
finalized = true;
|
|
108
|
+
try {
|
|
109
|
+
tuiApp?.cleanup?.();
|
|
110
|
+
} catch {}
|
|
111
|
+
restoreHostRuntime();
|
|
112
|
+
clearTerminalScreen();
|
|
113
|
+
resolveExit?.();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const requestExit = async () => {
|
|
117
|
+
const forceExitTimer = setTimeout(() => {
|
|
118
|
+
finalizeHost();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}, 5000);
|
|
121
|
+
forceExitTimer.unref?.();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await controller.stop();
|
|
125
|
+
} catch {}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(forceExitTimer);
|
|
128
|
+
try {
|
|
129
|
+
tuiApp?.clear?.();
|
|
130
|
+
} catch {}
|
|
131
|
+
try {
|
|
132
|
+
tuiApp?.unmount();
|
|
133
|
+
} catch {}
|
|
134
|
+
finalizeHost();
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
tuiApp = render(React.createElement(PilotSwarmTuiApp, {
|
|
140
|
+
controller,
|
|
141
|
+
platform,
|
|
142
|
+
onRequestExit: requestExit,
|
|
143
|
+
}), {
|
|
144
|
+
exitOnCtrlC: false,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await exitPromise;
|
|
149
|
+
} finally {
|
|
150
|
+
finalizeHost();
|
|
151
|
+
}
|
|
152
|
+
}
|