openportal 0.1.26 → 0.1.29

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 (29) hide show
  1. package/dist/index.js +637 -0
  2. package/package.json +2 -3
  3. package/web/nitro.json +1 -1
  4. package/web/server/_chunks/renderer-template.mjs +1 -1
  5. package/web/server/_routes/api/hello.mjs +1 -1
  6. package/web/server/_routes/api/instances.mjs +1 -1
  7. package/web/server/_routes/api/lib/opencode_client.mjs +3445 -132
  8. package/web/server/_routes/api/lib/validation.mjs +3633 -0
  9. package/web/server/_routes/api/opencode/[port]/agents.mjs +3 -4
  10. package/web/server/_routes/api/opencode/[port]/config.mjs +3 -4
  11. package/web/server/_routes/api/opencode/[port]/files/search.mjs +4 -3
  12. package/web/server/_routes/api/opencode/[port]/git/diff.mjs +5 -6
  13. package/web/server/_routes/api/opencode/[port]/health.mjs +3 -3
  14. package/web/server/_routes/api/opencode/[port]/permission/[requestId]/reply.mjs +22 -0
  15. package/web/server/_routes/api/opencode/[port]/permissions.mjs +7 -0
  16. package/web/server/_routes/api/opencode/[port]/project/current.mjs +3 -4
  17. package/web/server/_routes/api/opencode/[port]/providers.mjs +3 -4
  18. package/web/server/_routes/api/opencode/[port]/question/[requestId]/reject.mjs +9 -0
  19. package/web/server/_routes/api/opencode/[port]/question/[requestId]/reply.mjs +14 -0
  20. package/web/server/_routes/api/opencode/[port]/questions.mjs +7 -0
  21. package/web/server/_routes/api/opencode/[port]/session/[id]/abort.mjs +9 -0
  22. package/web/server/_routes/api/opencode/[port]/session/[id]/messages.mjs +4 -5
  23. package/web/server/_routes/api/opencode/[port]/session/[id]/prompt.mjs +34 -18
  24. package/web/server/_routes/api/opencode/[port]/session/[id].mjs +4 -5
  25. package/web/server/_routes/api/opencode/[port]/session/[id]2.mjs +4 -5
  26. package/web/server/_routes/api/opencode/[port]/session/create.mjs +10 -6
  27. package/web/server/_routes/api/opencode/[port]/sessions.mjs +3 -4
  28. package/web/server/_routes/api/system/hostname.mjs +1 -1
  29. package/web/server/index.mjs +751 -672
package/dist/index.js ADDED
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/index.ts
5
+ import { existsSync, readFileSync, writeFileSync } from "fs";
6
+
7
+ // ../../node_modules/.bun/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs
8
+ import { createServer, Server } from "net";
9
+ import { networkInterfaces, tmpdir } from "os";
10
+ var unsafePorts = /* @__PURE__ */ new Set([
11
+ 1,
12
+ 7,
13
+ 9,
14
+ 11,
15
+ 13,
16
+ 15,
17
+ 17,
18
+ 19,
19
+ 20,
20
+ 21,
21
+ 22,
22
+ 23,
23
+ 25,
24
+ 37,
25
+ 42,
26
+ 43,
27
+ 53,
28
+ 69,
29
+ 77,
30
+ 79,
31
+ 87,
32
+ 95,
33
+ 101,
34
+ 102,
35
+ 103,
36
+ 104,
37
+ 109,
38
+ 110,
39
+ 111,
40
+ 113,
41
+ 115,
42
+ 117,
43
+ 119,
44
+ 123,
45
+ 135,
46
+ 137,
47
+ 139,
48
+ 143,
49
+ 161,
50
+ 179,
51
+ 389,
52
+ 427,
53
+ 465,
54
+ 512,
55
+ 513,
56
+ 514,
57
+ 515,
58
+ 526,
59
+ 530,
60
+ 531,
61
+ 532,
62
+ 540,
63
+ 548,
64
+ 554,
65
+ 556,
66
+ 563,
67
+ 587,
68
+ 601,
69
+ 636,
70
+ 989,
71
+ 990,
72
+ 993,
73
+ 995,
74
+ 1719,
75
+ 1720,
76
+ 1723,
77
+ 2049,
78
+ 3659,
79
+ 4045,
80
+ 5060,
81
+ 5061,
82
+ 6000,
83
+ 6566,
84
+ 6665,
85
+ 6666,
86
+ 6667,
87
+ 6668,
88
+ 6669,
89
+ 6697,
90
+ 10080
91
+ ]);
92
+ function isUnsafePort(port) {
93
+ return unsafePorts.has(port);
94
+ }
95
+ function isSafePort(port) {
96
+ return !isUnsafePort(port);
97
+ }
98
+
99
+ class GetPortError extends Error {
100
+ constructor(message, opts) {
101
+ super(message, opts);
102
+ this.message = message;
103
+ }
104
+ name = "GetPortError";
105
+ }
106
+ function _log(verbose, message) {
107
+ if (verbose) {
108
+ console.log(`[get-port] ${message}`);
109
+ }
110
+ }
111
+ function _generateRange(from, to) {
112
+ if (to < from) {
113
+ return [];
114
+ }
115
+ const r = [];
116
+ for (let index = from;index <= to; index++) {
117
+ r.push(index);
118
+ }
119
+ return r;
120
+ }
121
+ function _tryPort(port, host) {
122
+ return new Promise((resolve) => {
123
+ const server = createServer();
124
+ server.unref();
125
+ server.on("error", () => {
126
+ resolve(false);
127
+ });
128
+ server.listen({ port, host }, () => {
129
+ const { port: port2 } = server.address();
130
+ server.close(() => {
131
+ resolve(isSafePort(port2) && port2);
132
+ });
133
+ });
134
+ });
135
+ }
136
+ function _getLocalHosts(additional) {
137
+ const hosts = new Set(additional);
138
+ for (const _interface of Object.values(networkInterfaces())) {
139
+ for (const config of _interface || []) {
140
+ if (config.address && !config.internal && !config.address.startsWith("fe80::") && !config.address.startsWith("169.254")) {
141
+ hosts.add(config.address);
142
+ }
143
+ }
144
+ }
145
+ return [...hosts];
146
+ }
147
+ async function _findPort(ports, host) {
148
+ for (const port of ports) {
149
+ const r = await _tryPort(port, host);
150
+ if (r) {
151
+ return r;
152
+ }
153
+ }
154
+ }
155
+ function _fmtOnHost(hostname) {
156
+ return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
157
+ }
158
+ var HOSTNAME_RE = /^(?!-)[\d.:A-Za-z-]{1,63}(?<!-)$/;
159
+ function _validateHostname(hostname, _public, verbose) {
160
+ if (hostname && !HOSTNAME_RE.test(hostname)) {
161
+ const fallbackHost = _public ? "0.0.0.0" : "127.0.0.1";
162
+ _log(verbose, `Invalid hostname: ${JSON.stringify(hostname)}. Using ${JSON.stringify(fallbackHost)} as fallback.`);
163
+ return fallbackHost;
164
+ }
165
+ return hostname;
166
+ }
167
+ async function getPort(_userOptions = {}) {
168
+ if (typeof _userOptions === "number" || typeof _userOptions === "string") {
169
+ _userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
170
+ }
171
+ const _port = Number(_userOptions.port ?? process.env.PORT);
172
+ const _userSpecifiedAnyPort = Boolean(_userOptions.port || _userOptions.ports?.length || _userOptions.portRange?.length);
173
+ const options = {
174
+ random: _port === 0,
175
+ ports: [],
176
+ portRange: [],
177
+ alternativePortRange: _userSpecifiedAnyPort ? [] : [3000, 3100],
178
+ verbose: false,
179
+ ..._userOptions,
180
+ port: _port,
181
+ host: _validateHostname(_userOptions.host ?? process.env.HOST, _userOptions.public, _userOptions.verbose)
182
+ };
183
+ if (options.random && !_userSpecifiedAnyPort) {
184
+ return getRandomPort(options.host);
185
+ }
186
+ const portsToCheck = [
187
+ options.port,
188
+ ...options.ports,
189
+ ..._generateRange(...options.portRange)
190
+ ].filter((port) => {
191
+ if (!port) {
192
+ return false;
193
+ }
194
+ if (!isSafePort(port)) {
195
+ _log(options.verbose, `Ignoring unsafe port: ${port}`);
196
+ return false;
197
+ }
198
+ return true;
199
+ });
200
+ if (portsToCheck.length === 0) {
201
+ portsToCheck.push(3000);
202
+ }
203
+ let availablePort = await _findPort(portsToCheck, options.host);
204
+ if (!availablePort && options.alternativePortRange.length > 0) {
205
+ availablePort = await _findPort(_generateRange(...options.alternativePortRange), options.host);
206
+ if (portsToCheck.length > 0) {
207
+ let message = `Unable to find an available port (tried ${portsToCheck.join("-")} ${_fmtOnHost(options.host)}).`;
208
+ if (availablePort) {
209
+ message += ` Using alternative port ${availablePort}.`;
210
+ }
211
+ _log(options.verbose, message);
212
+ }
213
+ }
214
+ if (!availablePort && _userOptions.random !== false) {
215
+ availablePort = await getRandomPort(options.host);
216
+ if (availablePort) {
217
+ _log(options.verbose, `Using random port ${availablePort}`);
218
+ }
219
+ }
220
+ if (!availablePort) {
221
+ const triedRanges = [
222
+ options.port,
223
+ options.portRange.join("-"),
224
+ options.alternativePortRange.join("-")
225
+ ].filter(Boolean).join(", ");
226
+ throw new GetPortError(`Unable to find an available port ${_fmtOnHost(options.host)} (tried ${triedRanges})`);
227
+ }
228
+ return availablePort;
229
+ }
230
+ async function getRandomPort(host) {
231
+ const port = await checkPort(0, host);
232
+ if (port === false) {
233
+ throw new GetPortError(`Unable to find a random port ${_fmtOnHost(host)}`);
234
+ }
235
+ return port;
236
+ }
237
+ async function checkPort(port, host = process.env.HOST, verbose) {
238
+ if (!host) {
239
+ host = _getLocalHosts([undefined, "0.0.0.0"]);
240
+ }
241
+ if (!Array.isArray(host)) {
242
+ return _tryPort(port, host);
243
+ }
244
+ for (const _host of host) {
245
+ const _port = await _tryPort(port, _host);
246
+ if (_port === false) {
247
+ if (port < 1024 && verbose) {
248
+ _log(verbose, `Unable to listen to the privileged port ${port} ${_fmtOnHost(_host)}`);
249
+ }
250
+ return false;
251
+ }
252
+ if (port === 0 && _port !== 0) {
253
+ port = _port;
254
+ }
255
+ }
256
+ return port;
257
+ }
258
+
259
+ // src/index.ts
260
+ import { homedir } from "os";
261
+ import { join, resolve, dirname } from "path";
262
+ import { fileURLToPath } from "url";
263
+ var __filename2 = fileURLToPath(import.meta.url);
264
+ var __dirname2 = dirname(__filename2);
265
+ var CONFIG_PATH = join(homedir(), ".portal.json");
266
+ var DEFAULT_HOSTNAME = "0.0.0.0";
267
+ var DEFAULT_PORT = 3000;
268
+ var DEFAULT_OPENCODE_PORT = 4000;
269
+ var WEB_SERVER_PATH = join(__dirname2, "..", "web", "server", "index.mjs");
270
+ function readConfig() {
271
+ try {
272
+ if (existsSync(CONFIG_PATH)) {
273
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
274
+ config.instances = config.instances.map((instance) => ({
275
+ ...instance,
276
+ opencodePid: instance.opencodePid ?? null,
277
+ webPid: instance.webPid ?? null
278
+ }));
279
+ return config;
280
+ }
281
+ } catch (error) {
282
+ console.warn(`[config] Failed to read config file, using empty config:`, error instanceof Error ? error.message : error);
283
+ }
284
+ return { instances: [] };
285
+ }
286
+ function writeConfig(config) {
287
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
288
+ }
289
+ function generateId() {
290
+ return Math.random().toString(36).substring(2, 10);
291
+ }
292
+ function isProcessRunning(pid) {
293
+ if (pid === null)
294
+ return false;
295
+ try {
296
+ process.kill(pid, 0);
297
+ return true;
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+ function isInstanceRunning(instance) {
303
+ return isProcessRunning(instance.opencodePid) || isProcessRunning(instance.webPid);
304
+ }
305
+ function printHelp() {
306
+ console.log(`
307
+ OpenPortal CLI - Run OpenCode with a web UI
308
+
309
+ Usage: openportal [command] [options]
310
+
311
+ Commands:
312
+ (default) Start OpenCode server and Web UI
313
+ run [options] Start only OpenCode server (no Web UI)
314
+ stop Stop running instances
315
+ list, ls List running instances
316
+ clean Clean up stale entries
317
+
318
+ Options:
319
+ -h, --help Show this help message
320
+ -d, --directory <path> Working directory (default: current directory)
321
+ -p, --port <port> Web UI port (default: 3000)
322
+ --opencode-port <port> OpenCode server port (default: 4000)
323
+ --hostname <host> Hostname to bind (default: 0.0.0.0)
324
+ --name <name> Instance name
325
+
326
+ Examples:
327
+ openportal Start OpenCode + Web UI
328
+ openportal . Start OpenCode + Web UI in current dir
329
+ openportal run Start only OpenCode server
330
+ openportal run -d ./my-project Start OpenCode in specific directory
331
+ openportal --port 8080 Use custom web UI port
332
+ openportal stop Stop running instances
333
+ openportal list List running instances
334
+ `);
335
+ }
336
+ function parseArgs() {
337
+ const args = [];
338
+ const flags = {};
339
+ for (let i = 2;i < process.argv.length; i++) {
340
+ const arg = process.argv[i];
341
+ if (arg.startsWith("--")) {
342
+ const [key, value] = arg.substring(2).split("=");
343
+ if (value !== undefined) {
344
+ flags[key.toLowerCase()] = value;
345
+ } else {
346
+ const next = process.argv[i + 1];
347
+ if (next && !next.startsWith("-")) {
348
+ flags[key.toLowerCase()] = next;
349
+ i++;
350
+ } else {
351
+ flags[key.toLowerCase()] = true;
352
+ }
353
+ }
354
+ } else if (arg.startsWith("-")) {
355
+ const short = arg.substring(1);
356
+ const next = process.argv[i + 1];
357
+ if (next && !next.startsWith("-")) {
358
+ flags[short.toLowerCase()] = next;
359
+ i++;
360
+ } else {
361
+ flags[short.toLowerCase()] = true;
362
+ }
363
+ } else {
364
+ args.push(arg);
365
+ }
366
+ }
367
+ return { args, flags };
368
+ }
369
+ async function startOpenCodeServer(directory, opencodePort, hostname) {
370
+ console.log(`Starting OpenCode server...`);
371
+ const proc = Bun.spawn([
372
+ "opencode",
373
+ "serve",
374
+ "--port",
375
+ String(opencodePort),
376
+ "--hostname",
377
+ hostname
378
+ ], {
379
+ cwd: directory,
380
+ stdio: ["ignore", "pipe", "pipe"],
381
+ env: { ...process.env }
382
+ });
383
+ return proc.pid;
384
+ }
385
+ async function startWebServer(port, hostname) {
386
+ console.log(`Starting Web UI server...`);
387
+ const proc = Bun.spawn(["bun", "run", WEB_SERVER_PATH], {
388
+ cwd: dirname(WEB_SERVER_PATH),
389
+ stdio: ["ignore", "pipe", "pipe"],
390
+ env: {
391
+ ...process.env,
392
+ PORT: String(port),
393
+ HOST: hostname,
394
+ NITRO_PORT: String(port),
395
+ NITRO_HOST: hostname
396
+ }
397
+ });
398
+ return proc.pid;
399
+ }
400
+ async function cmdDefault(options) {
401
+ const hostname = options.hostname || DEFAULT_HOSTNAME;
402
+ const directory = resolve(options.directory || options.d || process.cwd());
403
+ const name = options.name || directory.split("/").pop() || "opencode";
404
+ const port = options.port || options.p ? parseInt(options.port || options.p, 10) : await getPort({ host: hostname, port: DEFAULT_PORT });
405
+ const opencodePort = options["opencode-port"] ? parseInt(options["opencode-port"], 10) : await getPort({ host: hostname, port: DEFAULT_OPENCODE_PORT });
406
+ const config = readConfig();
407
+ const existingIndex = config.instances.findIndex((i) => i.directory === directory);
408
+ if (existingIndex !== -1) {
409
+ const existing = config.instances[existingIndex];
410
+ const running = isInstanceRunning(existing);
411
+ if (running || isProcessRunning(existing.webPid)) {
412
+ console.log(`OpenPortal is already running for this directory.`);
413
+ console.log(` Name: ${existing.name}`);
414
+ console.log(` Web UI Port: ${existing.port ?? "N/A"}`);
415
+ console.log(` OpenCode Port: ${existing.opencodePort}`);
416
+ if (existing.port) {
417
+ console.log(`
418
+ \uD83D\uDCF1 Access OpenPortal at http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${existing.port}`);
419
+ }
420
+ console.log(`\uD83D\uDD27 OpenCode API at http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${existing.opencodePort}`);
421
+ return;
422
+ }
423
+ config.instances.splice(existingIndex, 1);
424
+ }
425
+ if (!existsSync(WEB_SERVER_PATH)) {
426
+ console.error(`\u274C Web server not found at ${WEB_SERVER_PATH}`);
427
+ console.error(` The web app may not be bundled correctly.`);
428
+ process.exit(1);
429
+ }
430
+ console.log(`Starting OpenPortal...`);
431
+ console.log(` Name: ${name}`);
432
+ console.log(` Directory: ${directory}`);
433
+ console.log(` Web UI Port: ${port}`);
434
+ console.log(` OpenCode Port: ${opencodePort}`);
435
+ console.log(` Hostname: ${hostname}`);
436
+ try {
437
+ const opencodePid = await startOpenCodeServer(directory, opencodePort, hostname);
438
+ const webPid = await startWebServer(port, hostname);
439
+ const instance = {
440
+ id: generateId(),
441
+ name,
442
+ directory,
443
+ port,
444
+ opencodePort,
445
+ hostname,
446
+ opencodePid,
447
+ webPid,
448
+ startedAt: new Date().toISOString()
449
+ };
450
+ config.instances.push(instance);
451
+ writeConfig(config);
452
+ const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
453
+ console.log(`
454
+ \u2705 OpenPortal started!`);
455
+ console.log(` OpenCode PID: ${opencodePid}`);
456
+ console.log(` Web UI PID: ${webPid}`);
457
+ console.log(`
458
+ \uD83D\uDCF1 Access OpenPortal at http://${displayHost}:${port}`);
459
+ console.log(`\uD83D\uDD27 OpenCode API at http://${displayHost}:${opencodePort}`);
460
+ } catch (error) {
461
+ if (error instanceof Error) {
462
+ console.error(`
463
+ \u274C Failed to start OpenPortal: ${error.message}`);
464
+ }
465
+ process.exit(1);
466
+ }
467
+ }
468
+ async function cmdRun(options) {
469
+ const hostname = options.hostname || DEFAULT_HOSTNAME;
470
+ const directory = resolve(options.directory || options.d || process.cwd());
471
+ const name = options.name || directory.split("/").pop() || "opencode";
472
+ const opencodePort = options["opencode-port"] ? parseInt(options["opencode-port"], 10) : await getPort({ host: hostname, port: DEFAULT_OPENCODE_PORT });
473
+ const config = readConfig();
474
+ const existingIndex = config.instances.findIndex((i) => i.directory === directory);
475
+ if (existingIndex !== -1) {
476
+ const existing = config.instances[existingIndex];
477
+ const running = isInstanceRunning(existing);
478
+ if (running) {
479
+ console.log(`OpenCode is already running for this directory.`);
480
+ console.log(` Name: ${existing.name}`);
481
+ console.log(` OpenCode Port: ${existing.opencodePort}`);
482
+ console.log(`\uD83D\uDD27 OpenCode API at http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${existing.opencodePort}`);
483
+ return;
484
+ }
485
+ config.instances.splice(existingIndex, 1);
486
+ }
487
+ console.log(`Starting OpenCode server...`);
488
+ console.log(` Name: ${name}`);
489
+ console.log(` Directory: ${directory}`);
490
+ console.log(` OpenCode Port: ${opencodePort}`);
491
+ console.log(` Hostname: ${hostname}`);
492
+ try {
493
+ const opencodePid = await startOpenCodeServer(directory, opencodePort, hostname);
494
+ const instance = {
495
+ id: generateId(),
496
+ name,
497
+ directory,
498
+ port: null,
499
+ opencodePort,
500
+ hostname,
501
+ opencodePid,
502
+ webPid: null,
503
+ startedAt: new Date().toISOString()
504
+ };
505
+ config.instances.push(instance);
506
+ writeConfig(config);
507
+ const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
508
+ console.log(`
509
+ \u2705 OpenCode server started!`);
510
+ console.log(` OpenCode PID: ${opencodePid}`);
511
+ console.log(`\uD83D\uDD27 OpenCode API at http://${displayHost}:${opencodePort}`);
512
+ } catch (error) {
513
+ if (error instanceof Error) {
514
+ console.error(`
515
+ \u274C Failed to start OpenCode: ${error.message}`);
516
+ }
517
+ process.exit(1);
518
+ }
519
+ }
520
+ async function cmdStop(options) {
521
+ const config = readConfig();
522
+ const directory = options.directory || options.d ? resolve(options.directory || options.d) : process.cwd();
523
+ const instance = options.name ? config.instances.find((i) => i.name === options.name) : config.instances.find((i) => i.directory === directory);
524
+ if (!instance) {
525
+ console.error("No instance found.");
526
+ process.exit(1);
527
+ }
528
+ if (instance.opencodePid !== null) {
529
+ try {
530
+ process.kill(instance.opencodePid, "SIGTERM");
531
+ console.log(`Stopped OpenCode (PID: ${instance.opencodePid})`);
532
+ } catch {
533
+ console.log("OpenCode was already stopped.");
534
+ }
535
+ }
536
+ if (instance.webPid !== null) {
537
+ try {
538
+ process.kill(instance.webPid, "SIGTERM");
539
+ console.log(`Stopped Web UI (PID: ${instance.webPid})`);
540
+ } catch {
541
+ console.log("Web UI was already stopped.");
542
+ }
543
+ }
544
+ config.instances = config.instances.filter((i) => i.id !== instance.id);
545
+ writeConfig(config);
546
+ console.log(`
547
+ Stopped: ${instance.name}`);
548
+ }
549
+ async function cmdList() {
550
+ const config = readConfig();
551
+ if (config.instances.length === 0) {
552
+ console.log("No OpenPortal instances running.");
553
+ return;
554
+ }
555
+ console.log(`
556
+ OpenPortal Instances:
557
+ `);
558
+ console.log("ID\t\tNAME\t\t\tPORT\tOPENCODE\tSTATUS\t\tDIRECTORY");
559
+ console.log("-".repeat(110));
560
+ const validInstances = [];
561
+ for (const instance of config.instances) {
562
+ const opencodeRunning = isProcessRunning(instance.opencodePid);
563
+ const webRunning = isProcessRunning(instance.webPid);
564
+ let status = "stopped";
565
+ if (opencodeRunning && webRunning)
566
+ status = "running";
567
+ else if (opencodeRunning)
568
+ status = "opencode";
569
+ else if (webRunning)
570
+ status = "web only";
571
+ if (opencodeRunning || webRunning) {
572
+ validInstances.push(instance);
573
+ }
574
+ const portDisplay = instance.port ?? "-";
575
+ console.log(`${instance.id} ${instance.name.padEnd(16)} ${String(portDisplay).padEnd(4)} ${instance.opencodePort} ${status.padEnd(12)} ${instance.directory}`);
576
+ }
577
+ if (validInstances.length !== config.instances.length) {
578
+ config.instances = validInstances;
579
+ writeConfig(config);
580
+ }
581
+ }
582
+ async function cmdClean() {
583
+ const config = readConfig();
584
+ const validInstances = [];
585
+ for (const instance of config.instances) {
586
+ const running = isInstanceRunning(instance);
587
+ if (running || isProcessRunning(instance.webPid)) {
588
+ validInstances.push(instance);
589
+ } else {
590
+ console.log(`Removed stale entry: ${instance.name}`);
591
+ }
592
+ }
593
+ config.instances = validInstances;
594
+ writeConfig(config);
595
+ console.log(`
596
+ Config cleaned. ${validInstances.length} active instance(s).`);
597
+ }
598
+ async function main() {
599
+ const { args, flags } = parseArgs();
600
+ if (flags.help || flags.h) {
601
+ printHelp();
602
+ return;
603
+ }
604
+ const command = args[0]?.toLowerCase();
605
+ if (!command || command === "." || command.startsWith("/") || command.startsWith("./")) {
606
+ if (command && command !== ".") {
607
+ flags.directory = command;
608
+ }
609
+ await cmdDefault(flags);
610
+ return;
611
+ }
612
+ switch (command) {
613
+ case "run":
614
+ await cmdRun(flags);
615
+ break;
616
+ case "stop":
617
+ await cmdStop(flags);
618
+ break;
619
+ case "list":
620
+ case "ls":
621
+ await cmdList();
622
+ break;
623
+ case "clean":
624
+ await cmdClean();
625
+ break;
626
+ default:
627
+ if (existsSync(command)) {
628
+ flags.directory = command;
629
+ await cmdDefault(flags);
630
+ } else {
631
+ console.log(`Unknown command: ${command}`);
632
+ console.log("Use --help to see available commands.");
633
+ process.exit(1);
634
+ }
635
+ }
636
+ }
637
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openportal",
3
- "version": "0.1.26",
3
+ "version": "0.1.29",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,15 +17,14 @@
17
17
  "scripts": {
18
18
  "dev": "bun run src/index.ts",
19
19
  "build": "bun build src/index.ts --outdir dist --target bun",
20
+ "prepublishOnly": "bun run build",
20
21
  "typecheck": "tsc --noEmit"
21
22
  },
22
23
  "dependencies": {
23
- "dockerode": "^4.0.9",
24
24
  "get-port-please": "^3.2.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/bun": "^1.3.5",
28
- "@types/dockerode": "^3.3.47",
29
28
  "typescript": "^5.9.3"
30
29
  },
31
30
  "peerDependencies": {
package/web/nitro.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "date": "2026-03-16T04:20:17.032Z",
2
+ "date": "2026-04-04T19:06:03.494Z",
3
3
  "preset": "bun",
4
4
  "framework": {
5
5
  "name": "nitro",
@@ -1,4 +1,4 @@
1
- import { t as HTTPResponse } from "../index.mjs";
1
+ import { n as HTTPResponse } from "../index.mjs";
2
2
  const rendererTemplate = () => new HTTPResponse("<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/nitro.svg\" />\n <title>OpenCode Portal</title>\n <script>\n (function () {\n const storageKey = \"intentui-theme\";\n const theme = localStorage.getItem(storageKey);\n const systemDark = window.matchMedia(\n \"(prefers-color-scheme: dark)\",\n ).matches;\n if (\n theme === \"dark\" ||\n (theme === \"system\" && systemDark) ||\n (!theme && systemDark)\n ) {\n document.documentElement.classList.add(\"dark\");\n }\n })();\n <\/script>\n <script type=\"module\" crossorigin src=\"/assets/index-DrFs4I00.js\"><\/script>\n <link rel=\"stylesheet\" crossorigin href=\"/assets/index-D3N_OGS8.css\">\n </head>\n <body>\n <main>\n <div id=\"root\"></div>\n </main>\n </body>\n</html>\n", { headers: { "content-type": "text/html; charset=utf-8" } });
3
3
  function renderIndexHTML(event) {
4
4
  return rendererTemplate(event.req);
@@ -1,4 +1,4 @@
1
- import { n as defineHandler } from "../../index.mjs";
1
+ import { r as defineHandler } from "../../index.mjs";
2
2
  var hello_default = defineHandler(() => {
3
3
  return {
4
4
  message: "Hello from Nitro!",
@@ -1,4 +1,4 @@
1
- import { n as defineHandler } from "../../index.mjs";
1
+ import { r as defineHandler } from "../../index.mjs";
2
2
  import { isContainerRunning } from "./lib/docker.mjs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";