pilotswarm-cli 0.1.12 → 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.
@@ -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
+ }