pilotswarm-cli 0.1.3

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 ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * pilotswarm — CLI for the pilotswarm.
5
+ *
6
+ * Modes:
7
+ * local Embedded workers + TUI in one process (default)
8
+ * remote Client-only TUI, workers run elsewhere (AKS, separate process)
9
+ *
10
+ * Usage:
11
+ * npx pilotswarm local --plugin ./plugin
12
+ * npx pilotswarm remote --store postgresql://... --context toygres-aks
13
+ * npx pilotswarm --env .env --plugin ./my-plugin --workers 4
14
+ *
15
+ * All flags can also be set via environment variables (CLI flags take precedence).
16
+ */
17
+
18
+ import { parseArgs } from "node:util";
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const pkgRoot = path.resolve(__dirname, "..");
25
+ const defaultTuiSplashPath = path.join(pkgRoot, "cli", "tui-splash.txt");
26
+
27
+ function readPluginMetadata(pluginDir) {
28
+ if (!pluginDir) return null;
29
+ const pluginJsonPath = path.join(pluginDir, "plugin.json");
30
+ if (!fs.existsSync(pluginJsonPath)) return null;
31
+ try {
32
+ return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
33
+ } catch (err) {
34
+ console.error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${err.message}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ // ─── Parse CLI args ──────────────────────────────────────────────
40
+
41
+ const { values: flags, positionals } = parseArgs({
42
+ options: {
43
+ // Connection
44
+ store: { type: "string", short: "s" },
45
+ env: { type: "string", short: "e" },
46
+
47
+ // Local mode
48
+ plugin: { type: "string", short: "p" },
49
+ worker: { type: "string", short: "w" },
50
+ workers: { type: "string", short: "n" },
51
+ model: { type: "string", short: "m" },
52
+ system: { type: "string" },
53
+
54
+ // Remote mode
55
+ context: { type: "string", short: "c" },
56
+ namespace: { type: "string" },
57
+ label: { type: "string" },
58
+
59
+ // General
60
+ "log-level": { type: "string" },
61
+ help: { type: "boolean", short: "h" },
62
+ },
63
+ allowPositionals: true,
64
+ strict: false,
65
+ });
66
+
67
+ if (flags.help) {
68
+ console.log(`
69
+ pilotswarm — TUI for pilotswarm apps
70
+
71
+ USAGE
72
+ npx pilotswarm [local|remote] [flags]
73
+
74
+ MODES
75
+ local Embed workers in the TUI process (default)
76
+ remote Client-only — connect to remote workers via kubectl logs
77
+
78
+ FLAGS ENV VAR EQUIVALENT
79
+ -s, --store <url> Database URL DATABASE_URL
80
+ -e, --env <file> Env file (default: .env / .env.remote)
81
+
82
+ -p, --plugin <dir> Plugin dir PLUGIN_DIRS
83
+ -w, --worker <module> Tool module WORKER_MODULE
84
+ -n, --workers <count> Worker count WORKERS (default: 4)
85
+ -m, --model <name> LLM model COPILOT_MODEL
86
+ --system <msg|file> System msg SYSTEM_MESSAGE (or plugin/system.md)
87
+
88
+ -c, --context <ctx> K8s context K8S_CONTEXT
89
+ --namespace <ns> K8s namespace K8S_NAMESPACE (default: copilot-runtime)
90
+ --label <selector> Pod label K8S_POD_LABEL
91
+ --log-level <level> Trace level LOG_LEVEL
92
+ -h, --help Show help
93
+
94
+ All flags can be set via the corresponding env var (in .env or exported).
95
+ CLI flags take precedence over env vars.
96
+
97
+ EXAMPLES
98
+ # Plugin-only (no code), env from file
99
+ npx pilotswarm --env .env --plugin ./my-plugin
100
+
101
+ # Custom tools via worker module
102
+ npx pilotswarm --env .env --plugin ./plugin --worker ./tools.js
103
+
104
+ # All config in .env (zero flags)
105
+ echo "DATABASE_URL=postgresql://..." >> .env
106
+ echo "GITHUB_TOKEN=ghu_..." >> .env
107
+ echo "PLUGIN_DIRS=./plugin" >> .env
108
+ npx pilotswarm
109
+
110
+ # Client-only, workers on AKS
111
+ npx pilotswarm remote --store postgresql://... --namespace my-app
112
+ `.trim());
113
+ process.exit(0);
114
+ }
115
+
116
+ // ─── Determine mode ─────────────────────────────────────────────
117
+
118
+ const mode = positionals[0] === "remote" ? "remote" : "local";
119
+
120
+ // ─── Load env file ───────────────────────────────────────────────
121
+
122
+ const envFile = flags.env
123
+ || (mode === "remote" ? ".env.remote" : ".env");
124
+ if (fs.existsSync(envFile)) {
125
+ // Parse env file manually (KEY=VALUE lines)
126
+ const envContent = fs.readFileSync(envFile, "utf-8");
127
+ for (const line of envContent.split("\n")) {
128
+ const trimmed = line.trim();
129
+ if (!trimmed || trimmed.startsWith("#")) continue;
130
+ const eqIdx = trimmed.indexOf("=");
131
+ if (eqIdx === -1) continue;
132
+ const key = trimmed.slice(0, eqIdx).trim();
133
+ const value = trimmed.slice(eqIdx + 1).trim();
134
+ // CLI flags take precedence, env file fills in gaps
135
+ if (!process.env[key]) {
136
+ process.env[key] = value;
137
+ }
138
+ }
139
+ }
140
+
141
+ // ─── Resolve system message ──────────────────────────────────────
142
+
143
+ const DEFAULT_SYSTEM_MESSAGE = `You are a helpful assistant running in a durable execution environment. Be concise.
144
+
145
+ CRITICAL RULES:
146
+ 1. You have a 'wait' tool. You MUST use it whenever you need to wait, pause, sleep, delay, poll, check back later, schedule a future action, or implement any recurring/periodic task.
147
+ 2. NEVER say you cannot wait or set timers. You CAN — use the 'wait' tool.
148
+ 3. NEVER use bash sleep, setTimeout, setInterval, cron, or any other timing mechanism.
149
+ 4. The 'wait' tool enables durable timers that survive process restarts and node migrations.
150
+ 5. For recurring tasks: use the 'wait' tool in a loop — complete the action, then call wait(seconds), then repeat.`;
151
+
152
+ function resolveSystemMessage() {
153
+ // 1. CLI flag (string or file path)
154
+ if (flags.system) {
155
+ if (fs.existsSync(flags.system)) {
156
+ return fs.readFileSync(flags.system, "utf-8").trim();
157
+ }
158
+ return flags.system;
159
+ }
160
+
161
+ // 2. plugin/system.md (convention)
162
+ const pluginDir = resolvePluginDir();
163
+ if (pluginDir) {
164
+ const systemMd = path.join(pluginDir, "system.md");
165
+ if (fs.existsSync(systemMd)) {
166
+ return fs.readFileSync(systemMd, "utf-8").trim();
167
+ }
168
+ }
169
+
170
+ // 3. SYSTEM_MESSAGE env var
171
+ if (process.env.SYSTEM_MESSAGE) {
172
+ return process.env.SYSTEM_MESSAGE;
173
+ }
174
+
175
+ // 4. Default
176
+ return DEFAULT_SYSTEM_MESSAGE;
177
+ }
178
+
179
+ function resolvePluginDir() {
180
+ if (flags.plugin) return path.resolve(flags.plugin);
181
+ if (process.env.PLUGIN_DIRS) {
182
+ const dirs = process.env.PLUGIN_DIRS.split(",").map(d => d.trim()).filter(Boolean);
183
+ return dirs[0] || null;
184
+ }
185
+ // Auto-detect: ./plugins in cwd, then bundled plugins in CLI package
186
+ const cwdPlugin = path.resolve("plugins");
187
+ if (fs.existsSync(cwdPlugin)) return cwdPlugin;
188
+ const bundledPlugin = path.join(pkgRoot, "plugins");
189
+ if (fs.existsSync(bundledPlugin)) return bundledPlugin;
190
+ return null;
191
+ }
192
+
193
+ function resolveTuiBranding(pluginDir) {
194
+ const pluginMeta = readPluginMetadata(pluginDir);
195
+ const tui = pluginMeta?.tui;
196
+ let defaultSplash = "";
197
+ if (fs.existsSync(defaultTuiSplashPath)) {
198
+ defaultSplash = fs.readFileSync(defaultTuiSplashPath, "utf-8").trimEnd();
199
+ }
200
+ if (!tui || typeof tui !== "object") {
201
+ return {
202
+ title: "PilotSwarm",
203
+ splash: defaultSplash,
204
+ };
205
+ }
206
+
207
+ const title = typeof tui.title === "string" && tui.title.trim()
208
+ ? tui.title.trim()
209
+ : "PilotSwarm";
210
+
211
+ let splash = defaultSplash;
212
+ if (typeof tui.splash === "string" && tui.splash.trim()) {
213
+ splash = tui.splash;
214
+ } else if (typeof tui.splashFile === "string" && tui.splashFile.trim()) {
215
+ const splashPath = path.resolve(pluginDir, tui.splashFile);
216
+ if (!fs.existsSync(splashPath)) {
217
+ console.error(`TUI splash file not found: ${splashPath}`);
218
+ process.exit(1);
219
+ }
220
+ splash = fs.readFileSync(splashPath, "utf-8").trimEnd();
221
+ }
222
+
223
+ return { title, splash };
224
+ }
225
+
226
+ // ─── Build TUI config and set env vars ───────────────────────────
227
+
228
+ // Store
229
+ const store = flags.store || process.env.DATABASE_URL || "sqlite::memory:";
230
+ process.env.DATABASE_URL = store;
231
+
232
+ // Workers
233
+ if (mode === "remote") {
234
+ process.env.WORKERS = "0";
235
+ } else {
236
+ process.env.WORKERS = flags.workers ?? process.env.WORKERS ?? "4";
237
+ }
238
+
239
+ // Plugin dirs
240
+ const pluginDir = resolvePluginDir();
241
+ if (pluginDir) {
242
+ process.env.PLUGIN_DIRS = pluginDir;
243
+ }
244
+ const tuiBranding = resolveTuiBranding(pluginDir);
245
+
246
+ // Model
247
+ process.env.COPILOT_MODEL = flags.model || process.env.COPILOT_MODEL || "";
248
+
249
+ // Log level
250
+ process.env.LOG_LEVEL = flags["log-level"] || process.env.LOG_LEVEL || "";
251
+
252
+ // Context, namespace and label for remote mode (kubectl log streaming)
253
+ process.env.K8S_CONTEXT = flags.context || process.env.K8S_CONTEXT || "";
254
+ process.env.K8S_NAMESPACE = flags.namespace || process.env.K8S_NAMESPACE || "copilot-runtime";
255
+ process.env.K8S_POD_LABEL = flags.label || process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
256
+
257
+ // System message
258
+ process.env._TUI_SYSTEM_MESSAGE = resolveSystemMessage();
259
+ process.env._TUI_TITLE = tuiBranding.title;
260
+ process.env._TUI_SPLASH = tuiBranding.splash;
261
+
262
+ // ─── Load custom worker module (local mode only) ─────────────────
263
+
264
+ const workerModulePath = flags.worker || process.env.WORKER_MODULE || "";
265
+ if (mode === "local" && workerModulePath) {
266
+ const resolved = path.resolve(workerModulePath);
267
+ if (!fs.existsSync(resolved)) {
268
+ console.error(`Worker module not found: ${resolved}`);
269
+ process.exit(1);
270
+ }
271
+ process.env._TUI_WORKER_MODULE = resolved;
272
+ }
273
+
274
+ // ─── Launch TUI ──────────────────────────────────────────────────
275
+
276
+ // The TUI is the same file, but now reads config from env vars set above
277
+ // instead of relying on the user to set them manually.
278
+ const tuiPath = path.join(pkgRoot, "cli", "tui.js");
279
+ await import(tuiPath);