botapp-cli 0.2.4 → 0.2.7
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/dist/bin/bot.js +1815 -152
- package/dist/bin/bot.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1807 -152
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/bin/bot.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
10
|
+
import { Command as Command25 } from "commander";
|
|
11
|
+
import pc26 from "picocolors";
|
|
6
12
|
|
|
7
13
|
// src/commands/server.ts
|
|
8
14
|
import { Command } from "commander";
|
|
@@ -153,11 +159,11 @@ function findServerEntry() {
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
// src/commands/daemon.ts
|
|
156
|
-
import { spawn as
|
|
157
|
-
import { randomUUID } from "crypto";
|
|
158
|
-
import { existsSync as
|
|
159
|
-
import { homedir as
|
|
160
|
-
import { join as
|
|
162
|
+
import { spawn as spawn3 } from "child_process";
|
|
163
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
164
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
165
|
+
import { homedir as homedir5 } from "os";
|
|
166
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
161
167
|
import { createInterface as createInterface2 } from "readline";
|
|
162
168
|
import { Command as Command3 } from "commander";
|
|
163
169
|
import { WebSocket } from "ws";
|
|
@@ -189,7 +195,8 @@ function writeYaml(profiles) {
|
|
|
189
195
|
server: p.server,
|
|
190
196
|
daemonId: p.daemonId,
|
|
191
197
|
daemonName: p.daemonName,
|
|
192
|
-
token: p.token
|
|
198
|
+
token: p.token,
|
|
199
|
+
...p.userEmail ? { userEmail: p.userEmail } : {}
|
|
193
200
|
};
|
|
194
201
|
}
|
|
195
202
|
writeFileSync2(DAEMON_FILE, stringify2({ profiles: map }), "utf-8");
|
|
@@ -229,7 +236,8 @@ function loadDaemonProfiles() {
|
|
|
229
236
|
server: normalizeServer(value.server),
|
|
230
237
|
daemonId: value.daemonId,
|
|
231
238
|
daemonName: value.daemonName ?? value.daemonId,
|
|
232
|
-
token: value.token
|
|
239
|
+
token: value.token,
|
|
240
|
+
userEmail: value.userEmail
|
|
233
241
|
});
|
|
234
242
|
}
|
|
235
243
|
}
|
|
@@ -265,11 +273,21 @@ function saveDaemonProfile(input2) {
|
|
|
265
273
|
server,
|
|
266
274
|
daemonId: input2.daemonId,
|
|
267
275
|
daemonName: input2.daemonName ?? input2.daemonId,
|
|
268
|
-
token: input2.token
|
|
276
|
+
token: input2.token,
|
|
277
|
+
userEmail: input2.userEmail
|
|
269
278
|
};
|
|
270
279
|
writeYaml([...remaining, next]);
|
|
271
280
|
return next;
|
|
272
281
|
}
|
|
282
|
+
function removeDaemonProfile(serverOrAlias) {
|
|
283
|
+
const target = findDaemonProfile(serverOrAlias);
|
|
284
|
+
if (!target) return false;
|
|
285
|
+
const remaining = loadDaemonProfiles().filter(
|
|
286
|
+
(p) => p.alias !== target.alias
|
|
287
|
+
);
|
|
288
|
+
writeYaml(remaining);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
273
291
|
|
|
274
292
|
// src/commands/daemon-agent-config.ts
|
|
275
293
|
import { spawnSync } from "child_process";
|
|
@@ -473,7 +491,557 @@ async function daemonSelfRequest(server, token, path, opts) {
|
|
|
473
491
|
return data;
|
|
474
492
|
}
|
|
475
493
|
|
|
494
|
+
// src/rpc/registry.ts
|
|
495
|
+
var RpcRegistry = class {
|
|
496
|
+
handlers = /* @__PURE__ */ new Map();
|
|
497
|
+
register(op, handler) {
|
|
498
|
+
if (this.handlers.has(op)) {
|
|
499
|
+
throw new Error(`RPC op already registered: ${op}`);
|
|
500
|
+
}
|
|
501
|
+
this.handlers.set(op, handler);
|
|
502
|
+
}
|
|
503
|
+
get(op) {
|
|
504
|
+
return this.handlers.get(op);
|
|
505
|
+
}
|
|
506
|
+
has(op) {
|
|
507
|
+
return this.handlers.has(op);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
var InternalRpcContext = class {
|
|
511
|
+
constructor(appName, rpcId, ws) {
|
|
512
|
+
this.appName = appName;
|
|
513
|
+
this.rpcId = rpcId;
|
|
514
|
+
this.ws = ws;
|
|
515
|
+
}
|
|
516
|
+
appName;
|
|
517
|
+
rpcId;
|
|
518
|
+
ws;
|
|
519
|
+
cancelled = false;
|
|
520
|
+
inputCallback = null;
|
|
521
|
+
cancelCallback = null;
|
|
522
|
+
pushChunk(payload) {
|
|
523
|
+
sendJson(this.ws, {
|
|
524
|
+
type: "daemon_rpc_stream",
|
|
525
|
+
rpcId: this.rpcId,
|
|
526
|
+
payload
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
isCancelled() {
|
|
530
|
+
return this.cancelled;
|
|
531
|
+
}
|
|
532
|
+
onInput(callback) {
|
|
533
|
+
this.inputCallback = callback;
|
|
534
|
+
}
|
|
535
|
+
onCancel(callback) {
|
|
536
|
+
this.cancelCallback = callback;
|
|
537
|
+
}
|
|
538
|
+
cancel() {
|
|
539
|
+
if (this.cancelled) return;
|
|
540
|
+
this.cancelled = true;
|
|
541
|
+
const cb = this.cancelCallback;
|
|
542
|
+
this.cancelCallback = null;
|
|
543
|
+
if (cb) {
|
|
544
|
+
try {
|
|
545
|
+
cb();
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
receiveInput(payload) {
|
|
551
|
+
this.inputCallback?.(payload);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var RpcDispatcher = class {
|
|
555
|
+
constructor(registry, ws) {
|
|
556
|
+
this.registry = registry;
|
|
557
|
+
this.ws = ws;
|
|
558
|
+
}
|
|
559
|
+
registry;
|
|
560
|
+
ws;
|
|
561
|
+
inflight = /* @__PURE__ */ new Map();
|
|
562
|
+
/** Handle a `daemon_rpc_request` frame. */
|
|
563
|
+
async dispatchRequest(frame) {
|
|
564
|
+
const handler = this.registry.get(frame.op);
|
|
565
|
+
if (!handler) {
|
|
566
|
+
this.sendResponse(frame.rpcId, {
|
|
567
|
+
ok: false,
|
|
568
|
+
error: `unknown daemon RPC op: ${frame.op}`
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const ctx = new InternalRpcContext(
|
|
573
|
+
frame.appName ?? "unknown",
|
|
574
|
+
frame.rpcId,
|
|
575
|
+
this.ws
|
|
576
|
+
);
|
|
577
|
+
this.inflight.set(frame.rpcId, { ctx });
|
|
578
|
+
try {
|
|
579
|
+
const result = await handler(frame.params ?? {}, ctx);
|
|
580
|
+
if (this.inflight.has(frame.rpcId)) {
|
|
581
|
+
this.sendResponse(frame.rpcId, { ok: true, result });
|
|
582
|
+
}
|
|
583
|
+
} catch (e) {
|
|
584
|
+
if (this.inflight.has(frame.rpcId)) {
|
|
585
|
+
this.sendResponse(frame.rpcId, {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: e?.message ?? String(e)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} finally {
|
|
591
|
+
this.inflight.delete(frame.rpcId);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/** Handle a `daemon_rpc_input` frame. */
|
|
595
|
+
dispatchInput(frame) {
|
|
596
|
+
const call = this.inflight.get(frame.rpcId);
|
|
597
|
+
if (!call) return;
|
|
598
|
+
call.ctx.receiveInput(frame.payload);
|
|
599
|
+
}
|
|
600
|
+
/** Handle a `daemon_rpc_cancel` frame. */
|
|
601
|
+
dispatchCancel(frame) {
|
|
602
|
+
const call = this.inflight.get(frame.rpcId);
|
|
603
|
+
if (!call) return;
|
|
604
|
+
call.ctx.cancel();
|
|
605
|
+
}
|
|
606
|
+
/** Cancel everything (called on socket close). */
|
|
607
|
+
shutdown() {
|
|
608
|
+
for (const [, call] of this.inflight) {
|
|
609
|
+
call.ctx.cancel();
|
|
610
|
+
}
|
|
611
|
+
this.inflight.clear();
|
|
612
|
+
}
|
|
613
|
+
sendResponse(rpcId, body) {
|
|
614
|
+
sendJson(this.ws, { type: "daemon_rpc_response", rpcId, ...body });
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
function sendJson(ws, frame) {
|
|
618
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
619
|
+
ws.send(JSON.stringify(frame));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/rpc/file-handlers.ts
|
|
623
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
624
|
+
import {
|
|
625
|
+
promises as fsp,
|
|
626
|
+
realpathSync,
|
|
627
|
+
statSync
|
|
628
|
+
} from "fs";
|
|
629
|
+
import { homedir as homedir3 } from "os";
|
|
630
|
+
import { dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
|
|
631
|
+
function registerFileHandlers(registry) {
|
|
632
|
+
registry.register("file.tree", fileTree);
|
|
633
|
+
registry.register("file.read", fileRead);
|
|
634
|
+
registry.register("file.write", fileWrite);
|
|
635
|
+
registry.register("file.stat", fileStat);
|
|
636
|
+
registry.register("file.mkdir", fileMkdir);
|
|
637
|
+
registry.register("file.delete", fileDelete);
|
|
638
|
+
registry.register("file.rename", fileRename);
|
|
639
|
+
registry.register("fs.browse", fsBrowse);
|
|
640
|
+
registry.register("fs.home", fsHome);
|
|
641
|
+
registry.register("fs.mkdir", fsMkdir);
|
|
642
|
+
}
|
|
643
|
+
function expandHome(p) {
|
|
644
|
+
if (p === "~") return homedir3();
|
|
645
|
+
if (p.startsWith("~/")) return join3(homedir3(), p.slice(2));
|
|
646
|
+
return p;
|
|
647
|
+
}
|
|
648
|
+
function resolveRoot(root) {
|
|
649
|
+
if (typeof root !== "string" || !root.trim()) {
|
|
650
|
+
throw new Error("file.*: `root` is required");
|
|
651
|
+
}
|
|
652
|
+
const expanded = expandHome(root);
|
|
653
|
+
if (!isAbsolute(expanded)) {
|
|
654
|
+
throw new Error(`file.*: \`root\` must be absolute (got "${root}")`);
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
return realpathSync(expanded);
|
|
658
|
+
} catch {
|
|
659
|
+
return resolve3(expanded);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function jailedPath(root, sub) {
|
|
663
|
+
const canonicalRoot = resolveRoot(root);
|
|
664
|
+
const candidate = sub == null || sub === "" ? canonicalRoot : isAbsolute(sub) ? sub : resolve3(canonicalRoot, sub);
|
|
665
|
+
let realBase = candidate;
|
|
666
|
+
let tail = "";
|
|
667
|
+
while (true) {
|
|
668
|
+
try {
|
|
669
|
+
realBase = realpathSync(realBase);
|
|
670
|
+
break;
|
|
671
|
+
} catch {
|
|
672
|
+
const parent = dirname(realBase);
|
|
673
|
+
if (parent === realBase) {
|
|
674
|
+
realBase = candidate;
|
|
675
|
+
tail = "";
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
tail = tail ? join3(realBase.slice(parent.length + 1), tail) : realBase.slice(parent.length + 1);
|
|
679
|
+
realBase = parent;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const final = tail ? join3(realBase, tail) : realBase;
|
|
683
|
+
const rel = relative(canonicalRoot, final);
|
|
684
|
+
if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
685
|
+
throw new Error(`file.*: path "${sub ?? ""}" escapes workspace root "${root}"`);
|
|
686
|
+
}
|
|
687
|
+
return final;
|
|
688
|
+
}
|
|
689
|
+
var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
|
|
690
|
+
"node_modules",
|
|
691
|
+
".git",
|
|
692
|
+
".next",
|
|
693
|
+
".turbo",
|
|
694
|
+
"dist",
|
|
695
|
+
"build",
|
|
696
|
+
".venv",
|
|
697
|
+
"__pycache__",
|
|
698
|
+
".DS_Store"
|
|
699
|
+
]);
|
|
700
|
+
async function fileTree(params) {
|
|
701
|
+
const root = resolveRoot(params.root);
|
|
702
|
+
const start = jailedPath(params.root, params.path);
|
|
703
|
+
const depth = Math.max(0, params.depth ?? 1);
|
|
704
|
+
const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...params.ignore ?? []]);
|
|
705
|
+
const out = [];
|
|
706
|
+
async function walk2(absDir, level) {
|
|
707
|
+
let entries;
|
|
708
|
+
try {
|
|
709
|
+
entries = await fsp.readdir(absDir, {
|
|
710
|
+
withFileTypes: true,
|
|
711
|
+
encoding: "utf8"
|
|
712
|
+
});
|
|
713
|
+
} catch (e) {
|
|
714
|
+
if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return;
|
|
715
|
+
throw e;
|
|
716
|
+
}
|
|
717
|
+
entries.sort((a, b) => {
|
|
718
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
719
|
+
return a.name.localeCompare(b.name);
|
|
720
|
+
});
|
|
721
|
+
for (const entry of entries) {
|
|
722
|
+
if (ignore.has(entry.name)) continue;
|
|
723
|
+
const abs = join3(absDir, entry.name);
|
|
724
|
+
const rel = relative(root, abs);
|
|
725
|
+
let stat = null;
|
|
726
|
+
try {
|
|
727
|
+
stat = await fsp.stat(abs);
|
|
728
|
+
} catch {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const node = {
|
|
732
|
+
name: entry.name,
|
|
733
|
+
path: rel,
|
|
734
|
+
isDir: entry.isDirectory(),
|
|
735
|
+
size: entry.isFile() ? stat.size : void 0,
|
|
736
|
+
mtimeMs: stat.mtimeMs
|
|
737
|
+
};
|
|
738
|
+
out.push(node);
|
|
739
|
+
if (entry.isDirectory() && level < depth) {
|
|
740
|
+
await walk2(abs, level + 1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
await walk2(start, 1);
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
async function fileRead(params) {
|
|
748
|
+
const abs = jailedPath(params.root, params.path);
|
|
749
|
+
const maxBytes = params.maxBytes ?? 5 * 1024 * 1024;
|
|
750
|
+
const stat = await fsp.stat(abs);
|
|
751
|
+
if (!stat.isFile()) {
|
|
752
|
+
throw new Error(`file.read: not a file: ${params.path}`);
|
|
753
|
+
}
|
|
754
|
+
if (stat.size > maxBytes) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`file.read: file too large (${stat.size} bytes > ${maxBytes}). Pass maxBytes to override or chunk the read.`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const buf = await fsp.readFile(abs);
|
|
760
|
+
const encoding = params.encoding ?? "utf8";
|
|
761
|
+
return {
|
|
762
|
+
path: params.path,
|
|
763
|
+
content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
|
|
764
|
+
encoding,
|
|
765
|
+
size: stat.size,
|
|
766
|
+
mtimeMs: stat.mtimeMs
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async function fileWrite(params) {
|
|
770
|
+
const abs = jailedPath(params.root, params.path);
|
|
771
|
+
if (params.createDirs !== false) {
|
|
772
|
+
await fsp.mkdir(dirname(abs), { recursive: true });
|
|
773
|
+
}
|
|
774
|
+
const buf = (params.encoding ?? "utf8") === "base64" ? Buffer2.from(params.content, "base64") : Buffer2.from(params.content, "utf8");
|
|
775
|
+
await fsp.writeFile(abs, buf);
|
|
776
|
+
const stat = await fsp.stat(abs);
|
|
777
|
+
return { path: params.path, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
778
|
+
}
|
|
779
|
+
async function fileStat(params) {
|
|
780
|
+
let abs;
|
|
781
|
+
try {
|
|
782
|
+
abs = params.path == null || params.path === "" ? expandHome(params.root) : jailedPath(params.root, params.path);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
if (/escapes workspace root/.test(e?.message ?? "")) throw e;
|
|
785
|
+
return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const stat = statSync(abs);
|
|
789
|
+
return {
|
|
790
|
+
exists: true,
|
|
791
|
+
isFile: stat.isFile(),
|
|
792
|
+
isDir: stat.isDirectory(),
|
|
793
|
+
size: stat.size,
|
|
794
|
+
mtimeMs: stat.mtimeMs
|
|
795
|
+
};
|
|
796
|
+
} catch {
|
|
797
|
+
return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function fileMkdir(params) {
|
|
801
|
+
const abs = jailedPath(params.root, params.path);
|
|
802
|
+
await fsp.mkdir(abs, { recursive: true });
|
|
803
|
+
return { path: params.path };
|
|
804
|
+
}
|
|
805
|
+
async function fileDelete(params) {
|
|
806
|
+
const abs = jailedPath(params.root, params.path);
|
|
807
|
+
await fsp.rm(abs, { recursive: !!params.recursive, force: false });
|
|
808
|
+
return { path: params.path };
|
|
809
|
+
}
|
|
810
|
+
async function fileRename(params) {
|
|
811
|
+
const fromAbs = jailedPath(params.root, params.from);
|
|
812
|
+
const toAbs = jailedPath(params.root, params.to);
|
|
813
|
+
await fsp.rename(fromAbs, toAbs);
|
|
814
|
+
return { from: params.from, to: params.to };
|
|
815
|
+
}
|
|
816
|
+
async function fsBrowse(params) {
|
|
817
|
+
const expanded = expandHome(params.path ?? "~");
|
|
818
|
+
if (!isAbsolute(expanded)) {
|
|
819
|
+
throw new Error(`fs.browse: path must be absolute (got "${params.path}")`);
|
|
820
|
+
}
|
|
821
|
+
let resolved;
|
|
822
|
+
try {
|
|
823
|
+
resolved = realpathSync(expanded);
|
|
824
|
+
} catch {
|
|
825
|
+
resolved = resolve3(expanded);
|
|
826
|
+
}
|
|
827
|
+
let raw;
|
|
828
|
+
try {
|
|
829
|
+
raw = await fsp.readdir(resolved, {
|
|
830
|
+
withFileTypes: true,
|
|
831
|
+
encoding: "utf8"
|
|
832
|
+
});
|
|
833
|
+
} catch (e) {
|
|
834
|
+
throw new Error(`fs.browse: cannot read ${resolved}: ${e?.code ?? e?.message ?? e}`);
|
|
835
|
+
}
|
|
836
|
+
const entries = [];
|
|
837
|
+
for (const entry of raw) {
|
|
838
|
+
if (!params.showHidden && entry.name.startsWith(".")) continue;
|
|
839
|
+
const isDir = entry.isDirectory();
|
|
840
|
+
if (!isDir && !params.includeFiles) continue;
|
|
841
|
+
entries.push({
|
|
842
|
+
name: entry.name,
|
|
843
|
+
path: join3(resolved, entry.name),
|
|
844
|
+
isDir
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
entries.sort((a, b) => {
|
|
848
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
849
|
+
return a.name.localeCompare(b.name);
|
|
850
|
+
});
|
|
851
|
+
const parent = dirname(resolved);
|
|
852
|
+
return {
|
|
853
|
+
path: resolved,
|
|
854
|
+
parent: parent === resolved ? null : parent,
|
|
855
|
+
entries
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
async function fsHome() {
|
|
859
|
+
return { home: homedir3(), cwd: process.cwd() };
|
|
860
|
+
}
|
|
861
|
+
async function fsMkdir(params) {
|
|
862
|
+
const expanded = expandHome(params.path ?? "");
|
|
863
|
+
if (!expanded || !isAbsolute(expanded)) {
|
|
864
|
+
throw new Error(`fs.mkdir: path must be absolute (got "${params.path}")`);
|
|
865
|
+
}
|
|
866
|
+
await fsp.mkdir(expanded, { recursive: true });
|
|
867
|
+
return { path: expanded };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/rpc/pty-handlers.ts
|
|
871
|
+
import { spawn as spawn2 } from "child_process";
|
|
872
|
+
import { randomUUID } from "crypto";
|
|
873
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
874
|
+
import { homedir as homedir4, platform } from "os";
|
|
875
|
+
import { join as join4 } from "path";
|
|
876
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
877
|
+
var handles = /* @__PURE__ */ new Map();
|
|
878
|
+
function registerPtyHandlers(registry) {
|
|
879
|
+
registry.register("pty.spawn", ptySpawn);
|
|
880
|
+
registry.register("pty.write", ptyWrite);
|
|
881
|
+
registry.register("pty.resize", ptyResize);
|
|
882
|
+
registry.register("pty.kill", ptyKill);
|
|
883
|
+
}
|
|
884
|
+
async function ptySpawn(params, ctx) {
|
|
885
|
+
if (!params.cwd || typeof params.cwd !== "string" || !isAbsolute2(params.cwd)) {
|
|
886
|
+
throw new Error("pty.spawn: `cwd` is required and must be absolute");
|
|
887
|
+
}
|
|
888
|
+
let cwdStat;
|
|
889
|
+
try {
|
|
890
|
+
cwdStat = statSync2(params.cwd);
|
|
891
|
+
} catch {
|
|
892
|
+
throw new Error(`pty.spawn: cwd does not exist: ${params.cwd}`);
|
|
893
|
+
}
|
|
894
|
+
if (!cwdStat.isDirectory()) {
|
|
895
|
+
throw new Error(`pty.spawn: cwd is not a directory: ${params.cwd}`);
|
|
896
|
+
}
|
|
897
|
+
const command = params.command || defaultShell();
|
|
898
|
+
if (!existsSync4(command)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`pty.spawn: shell binary not found: ${command} (set $SHELL in the daemon's environment, or pass an explicit \`command\`)`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const args = params.args ?? defaultShellArgs();
|
|
904
|
+
const env = { TERM: "xterm-256color" };
|
|
905
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
906
|
+
if (typeof v === "string") env[k] = v;
|
|
907
|
+
}
|
|
908
|
+
for (const [k, v] of Object.entries(params.env ?? {})) {
|
|
909
|
+
env[k] = v;
|
|
910
|
+
}
|
|
911
|
+
const cols = params.cols ?? 80;
|
|
912
|
+
const rows = params.rows ?? 24;
|
|
913
|
+
const handle = await openPty({ command, args, cwd: params.cwd, env, cols, rows, ctx });
|
|
914
|
+
handles.set(handle.handle.ptyId, handle.handle);
|
|
915
|
+
ctx.onInput((payload) => {
|
|
916
|
+
if (typeof payload === "string") handle.handle.write(payload);
|
|
917
|
+
else if (payload && typeof payload.data === "string") {
|
|
918
|
+
handle.handle.write(payload.data);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
ctx.onCancel(() => {
|
|
922
|
+
handle.handle.kill("SIGTERM");
|
|
923
|
+
setTimeout(() => {
|
|
924
|
+
try {
|
|
925
|
+
handle.handle.kill("SIGKILL");
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
}, 5e3).unref();
|
|
929
|
+
});
|
|
930
|
+
ctx.pushChunk({ kind: "mode", mode: handle.mode, ptyId: handle.handle.ptyId });
|
|
931
|
+
const exitCode = await handle.exited;
|
|
932
|
+
handles.delete(handle.handle.ptyId);
|
|
933
|
+
return { ptyId: handle.handle.ptyId, exitCode, mode: handle.mode };
|
|
934
|
+
}
|
|
935
|
+
async function ptyWrite(params) {
|
|
936
|
+
const handle = handles.get(params.ptyId);
|
|
937
|
+
if (!handle) throw new Error(`pty.write: unknown ptyId ${params.ptyId}`);
|
|
938
|
+
handle.write(params.data);
|
|
939
|
+
return { ok: true };
|
|
940
|
+
}
|
|
941
|
+
async function ptyResize(params) {
|
|
942
|
+
const handle = handles.get(params.ptyId);
|
|
943
|
+
if (!handle) throw new Error(`pty.resize: unknown ptyId ${params.ptyId}`);
|
|
944
|
+
handle.resize(params.cols, params.rows);
|
|
945
|
+
return { ok: true };
|
|
946
|
+
}
|
|
947
|
+
async function ptyKill(params) {
|
|
948
|
+
const handle = handles.get(params.ptyId);
|
|
949
|
+
if (!handle) throw new Error(`pty.kill: unknown ptyId ${params.ptyId}`);
|
|
950
|
+
handle.kill(params.signal ?? "SIGTERM");
|
|
951
|
+
return { ok: true };
|
|
952
|
+
}
|
|
953
|
+
async function openPty(opts) {
|
|
954
|
+
const nodePty = await loadNodePty();
|
|
955
|
+
if (nodePty) {
|
|
956
|
+
const proc2 = nodePty.spawn(opts.command, opts.args, {
|
|
957
|
+
name: "xterm-256color",
|
|
958
|
+
cols: opts.cols,
|
|
959
|
+
rows: opts.rows,
|
|
960
|
+
cwd: opts.cwd,
|
|
961
|
+
env: opts.env
|
|
962
|
+
});
|
|
963
|
+
const ptyId2 = randomUUID();
|
|
964
|
+
proc2.onData((data) => {
|
|
965
|
+
opts.ctx.pushChunk({ kind: "data", data });
|
|
966
|
+
});
|
|
967
|
+
const exited2 = new Promise((resolve11) => {
|
|
968
|
+
proc2.onExit(({ exitCode }) => {
|
|
969
|
+
resolve11(exitCode);
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
return {
|
|
973
|
+
mode: "pty",
|
|
974
|
+
exited: exited2,
|
|
975
|
+
handle: {
|
|
976
|
+
ptyId: ptyId2,
|
|
977
|
+
resize: (cols, rows) => proc2.resize(cols, rows),
|
|
978
|
+
write: (data) => proc2.write(data),
|
|
979
|
+
kill: (signal) => proc2.kill(signal)
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const proc = spawn2(
|
|
984
|
+
opts.command,
|
|
985
|
+
opts.args,
|
|
986
|
+
{
|
|
987
|
+
cwd: opts.cwd,
|
|
988
|
+
env: opts.env,
|
|
989
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
const ptyId = randomUUID();
|
|
993
|
+
proc.stdout.on("data", (chunk) => {
|
|
994
|
+
opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
|
|
995
|
+
});
|
|
996
|
+
proc.stderr.on("data", (chunk) => {
|
|
997
|
+
opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
|
|
998
|
+
});
|
|
999
|
+
const exited = new Promise((resolve11) => {
|
|
1000
|
+
proc.on("close", (code) => resolve11(code));
|
|
1001
|
+
});
|
|
1002
|
+
return {
|
|
1003
|
+
mode: "pipe",
|
|
1004
|
+
exited,
|
|
1005
|
+
handle: {
|
|
1006
|
+
ptyId,
|
|
1007
|
+
resize: () => {
|
|
1008
|
+
},
|
|
1009
|
+
write: (data) => {
|
|
1010
|
+
proc.stdin.write(data);
|
|
1011
|
+
},
|
|
1012
|
+
kill: (signal) => {
|
|
1013
|
+
proc.kill(signal ?? "SIGTERM");
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
var nodePtyCache = void 0;
|
|
1019
|
+
async function loadNodePty() {
|
|
1020
|
+
if (nodePtyCache !== void 0) return nodePtyCache;
|
|
1021
|
+
try {
|
|
1022
|
+
const moduleName = "node-pty";
|
|
1023
|
+
nodePtyCache = await import(moduleName);
|
|
1024
|
+
return nodePtyCache;
|
|
1025
|
+
} catch {
|
|
1026
|
+
nodePtyCache = null;
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function defaultShell() {
|
|
1031
|
+
if (platform() === "win32") {
|
|
1032
|
+
return process.env.COMSPEC || "cmd.exe";
|
|
1033
|
+
}
|
|
1034
|
+
return process.env.SHELL || "/bin/bash";
|
|
1035
|
+
}
|
|
1036
|
+
function defaultShellArgs() {
|
|
1037
|
+
if (platform() === "win32") return [];
|
|
1038
|
+
return ["-l", "-i"];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
476
1041
|
// src/commands/daemon.ts
|
|
1042
|
+
var rpcRegistry = new RpcRegistry();
|
|
1043
|
+
registerFileHandlers(rpcRegistry);
|
|
1044
|
+
registerPtyHandlers(rpcRegistry);
|
|
477
1045
|
var daemonCommand = new Command3("daemon").description("Manage and run the local botapp daemon");
|
|
478
1046
|
daemonCommand.command("run").description(
|
|
479
1047
|
"Run paired daemons and wait for server jobs. With no flags, runs every paired profile concurrently \u2014 so a single `bot daemon run` covers both local and remote pairings."
|
|
@@ -517,9 +1085,31 @@ function pickProfilesToRun(alias, server) {
|
|
|
517
1085
|
}
|
|
518
1086
|
return loadDaemonProfiles();
|
|
519
1087
|
}
|
|
1088
|
+
daemonCommand.command("unpair").description("Remove a paired profile from ~/.botapp/daemon.yaml (does not touch the server)").argument("<aliasOrServer>", "Profile alias (e.g. local) or server URL").action((aliasOrServer) => {
|
|
1089
|
+
const removed = removeDaemonProfile(aliasOrServer);
|
|
1090
|
+
if (!removed) {
|
|
1091
|
+
console.error(pc3.red(`No paired profile matching "${aliasOrServer}".`));
|
|
1092
|
+
process.exitCode = 1;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
console.log(pc3.green(`Removed daemon profile: ${aliasOrServer}`));
|
|
1096
|
+
});
|
|
1097
|
+
daemonCommand.command("list").alias("ls").description("List paired daemon profiles from ~/.botapp/daemon.yaml").action(() => {
|
|
1098
|
+
const profiles = loadDaemonProfiles();
|
|
1099
|
+
if (profiles.length === 0) {
|
|
1100
|
+
console.log(pc3.dim("No paired daemons. Run `bot pair` to add one."));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
for (const p of profiles) {
|
|
1104
|
+
console.log(pc3.bold(p.alias ?? p.server));
|
|
1105
|
+
console.log(` Server: ${p.server}`);
|
|
1106
|
+
console.log(` Daemon: ${p.daemonName} ${pc3.dim(`(${p.daemonId})`)}`);
|
|
1107
|
+
console.log(` User: ${p.userEmail ?? pc3.dim("unknown \u2014 re-pair to capture")}`);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
520
1110
|
daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
|
|
521
|
-
const pidFile =
|
|
522
|
-
if (!
|
|
1111
|
+
const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
|
|
1112
|
+
if (!existsSync5(pidFile)) {
|
|
523
1113
|
console.log(pc3.yellow("No background daemon PID file found."));
|
|
524
1114
|
return;
|
|
525
1115
|
}
|
|
@@ -545,16 +1135,17 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
545
1135
|
for (const agent of data.agents ?? []) {
|
|
546
1136
|
console.log(`${pc3.bold(agent.name)} ${pc3.dim(`(${agent.id})`)}`);
|
|
547
1137
|
console.log(` Kind: ${agent.kind}`);
|
|
1138
|
+
if (agent.model) console.log(` Model: ${agent.model}`);
|
|
548
1139
|
console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
|
|
549
1140
|
if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
|
|
550
1141
|
}
|
|
551
1142
|
})
|
|
552
1143
|
).addCommand(createDaemonAgentConfigCommand()).addCommand(
|
|
553
|
-
new Command3("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
|
|
1144
|
+
new Command3("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, kimi, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
|
|
554
1145
|
"--kind <kind>",
|
|
555
|
-
"Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
|
|
1146
|
+
"Agent adapter kind: acp, codex, claude-code, kimi, openclaw, hermes, hermes-agent, or shell",
|
|
556
1147
|
"acp"
|
|
557
|
-
).option("--arg <arg...>", "Argument passed to the executable").option("--cwd <cwd>", "Working directory for the agent process").option("--env <entry...>", "Environment entries in KEY=VALUE form").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
|
|
1148
|
+
).option("--arg <arg...>", "Argument passed to the executable").option("--cwd <cwd>", "Working directory for the agent process").option("--env <entry...>", "Environment entries in KEY=VALUE form").option("--model <model>", "Provider model to pass to the agent runtime").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
|
|
558
1149
|
const profile = requireSelectedProfile(opts);
|
|
559
1150
|
if (!profile) return;
|
|
560
1151
|
const env = parseEnv(opts.env ?? []);
|
|
@@ -570,12 +1161,14 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
570
1161
|
command: opts.command,
|
|
571
1162
|
args: opts.arg ?? [],
|
|
572
1163
|
cwd: opts.cwd,
|
|
573
|
-
env
|
|
1164
|
+
env,
|
|
1165
|
+
model: opts.model
|
|
574
1166
|
}
|
|
575
1167
|
}
|
|
576
1168
|
);
|
|
577
1169
|
console.log(pc3.green(`Registered daemon agent: ${pc3.bold(data.agent.name)}`));
|
|
578
1170
|
console.log(` ID: ${data.agent.id}`);
|
|
1171
|
+
if (data.agent.model) console.log(` Model: ${data.agent.model}`);
|
|
579
1172
|
console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
|
|
580
1173
|
})
|
|
581
1174
|
).addCommand(
|
|
@@ -647,6 +1240,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
|
|
|
647
1240
|
return new Promise((resolveRun) => {
|
|
648
1241
|
const ws = new WebSocket(wsUrl);
|
|
649
1242
|
setActiveWs(ws);
|
|
1243
|
+
const rpcDispatcher = new RpcDispatcher(rpcRegistry, ws);
|
|
650
1244
|
let opened = false;
|
|
651
1245
|
let superseded = false;
|
|
652
1246
|
let settled = false;
|
|
@@ -655,6 +1249,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
|
|
|
655
1249
|
if (settled) return;
|
|
656
1250
|
settled = true;
|
|
657
1251
|
if (ping) clearInterval(ping);
|
|
1252
|
+
rpcDispatcher.shutdown();
|
|
658
1253
|
resolveRun({ opened, superseded });
|
|
659
1254
|
}
|
|
660
1255
|
ws.on("open", () => {
|
|
@@ -666,7 +1261,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
|
|
|
666
1261
|
}, 3e4);
|
|
667
1262
|
});
|
|
668
1263
|
ws.on("message", (raw) => {
|
|
669
|
-
void handleFrame(ws, raw.toString());
|
|
1264
|
+
void handleFrame(ws, raw.toString(), rpcDispatcher);
|
|
670
1265
|
});
|
|
671
1266
|
ws.on("close", (code, reason) => {
|
|
672
1267
|
if (code === 4e3) superseded = true;
|
|
@@ -682,8 +1277,20 @@ function runDaemonSocket(wsUrl, setActiveWs) {
|
|
|
682
1277
|
});
|
|
683
1278
|
});
|
|
684
1279
|
}
|
|
685
|
-
async function handleFrame(ws, raw) {
|
|
1280
|
+
async function handleFrame(ws, raw, rpcDispatcher) {
|
|
686
1281
|
const frame = JSON.parse(raw);
|
|
1282
|
+
if (frame.type === "daemon_rpc_request") {
|
|
1283
|
+
void rpcDispatcher.dispatchRequest(frame);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (frame.type === "daemon_rpc_input") {
|
|
1287
|
+
rpcDispatcher.dispatchInput(frame);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (frame.type === "daemon_rpc_cancel") {
|
|
1291
|
+
rpcDispatcher.dispatchCancel(frame);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
687
1294
|
if (frame.type === "daemon_job") {
|
|
688
1295
|
const job = frame.job;
|
|
689
1296
|
console.log(pc3.blue(`Running ${job.agent.name} job ${job.id}`));
|
|
@@ -727,7 +1334,10 @@ async function runAgentJob(job, update) {
|
|
|
727
1334
|
if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
|
|
728
1335
|
return runClaudeCodeAgent(job, update);
|
|
729
1336
|
}
|
|
730
|
-
if (job.agent.kind === "
|
|
1337
|
+
if (job.agent.kind === "kimi") {
|
|
1338
|
+
return runKimiCodeAgent(job, update);
|
|
1339
|
+
}
|
|
1340
|
+
if (job.agent.kind === "openclaw") {
|
|
731
1341
|
return runOpenClawAgent(job, update);
|
|
732
1342
|
}
|
|
733
1343
|
if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
|
|
@@ -739,7 +1349,7 @@ async function runAgentJob(job, update) {
|
|
|
739
1349
|
return runAcpAgent(job);
|
|
740
1350
|
}
|
|
741
1351
|
async function runShellAgent(job) {
|
|
742
|
-
const child =
|
|
1352
|
+
const child = spawn3(job.agent.command, [...job.agent.args, job.query], {
|
|
743
1353
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
744
1354
|
env: { ...process.env, ...job.agent.env ?? {} }
|
|
745
1355
|
});
|
|
@@ -751,7 +1361,7 @@ async function runShellAgent(job) {
|
|
|
751
1361
|
child.stderr.on("data", (chunk) => {
|
|
752
1362
|
stderr += chunk.toString();
|
|
753
1363
|
});
|
|
754
|
-
const code = await new Promise((
|
|
1364
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
755
1365
|
if (code !== 0) {
|
|
756
1366
|
throw new Error(stderr.trim() || `Agent exited with code ${code}`);
|
|
757
1367
|
}
|
|
@@ -768,6 +1378,9 @@ async function runCodexAgent(job, update) {
|
|
|
768
1378
|
if (!args.includes("--skip-git-repo-check")) {
|
|
769
1379
|
args.push("--skip-git-repo-check");
|
|
770
1380
|
}
|
|
1381
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1382
|
+
args.push("--model", job.agent.model);
|
|
1383
|
+
}
|
|
771
1384
|
if (!hasAnyFlag(args, [
|
|
772
1385
|
"--sandbox",
|
|
773
1386
|
"-s",
|
|
@@ -779,7 +1392,7 @@ async function runCodexAgent(job, update) {
|
|
|
779
1392
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
780
1393
|
}
|
|
781
1394
|
args.push(job.query);
|
|
782
|
-
const child =
|
|
1395
|
+
const child = spawn3(job.agent.command, args, {
|
|
783
1396
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
784
1397
|
env: { ...process.env, ...job.agent.env ?? {} },
|
|
785
1398
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -796,12 +1409,19 @@ async function runCodexAgent(job, update) {
|
|
|
796
1409
|
rawEvents: []
|
|
797
1410
|
};
|
|
798
1411
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1412
|
+
const errors = [];
|
|
799
1413
|
function processLine(line) {
|
|
800
1414
|
if (!line.trim()) return;
|
|
801
1415
|
try {
|
|
802
1416
|
const event = JSON.parse(line);
|
|
803
1417
|
result.rawEvents.push(event);
|
|
804
1418
|
update({ kind: "codex_event", event });
|
|
1419
|
+
if (event?.type === "error" && typeof event.message === "string") {
|
|
1420
|
+
errors.push(event.message);
|
|
1421
|
+
}
|
|
1422
|
+
if (event?.type === "turn.failed" && typeof event.error?.message === "string") {
|
|
1423
|
+
errors.push(event.error.message);
|
|
1424
|
+
}
|
|
805
1425
|
if (event?.type === "item.completed" && event.item?.type === "agent_message") {
|
|
806
1426
|
if (typeof event.item.text === "string") {
|
|
807
1427
|
result.messages.push(event.item.text);
|
|
@@ -829,9 +1449,9 @@ async function runCodexAgent(job, update) {
|
|
|
829
1449
|
}
|
|
830
1450
|
const rl = createInterface2({ input: child.stdout });
|
|
831
1451
|
rl.on("line", processLine);
|
|
832
|
-
const code = await new Promise((
|
|
1452
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
833
1453
|
if (code !== 0) {
|
|
834
|
-
throw new Error(stderr.trim() || `Codex exited with code ${code}`);
|
|
1454
|
+
throw new Error(errors.at(-1) ?? (stderr.trim() || `Codex exited with code ${code}`));
|
|
835
1455
|
}
|
|
836
1456
|
result.text = result.messages.at(-1)?.trim() || "";
|
|
837
1457
|
result.toolCalls = [...toolCalls.values()];
|
|
@@ -848,6 +1468,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
848
1468
|
if (!args.includes("--verbose")) {
|
|
849
1469
|
args.push("--verbose");
|
|
850
1470
|
}
|
|
1471
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model"])) {
|
|
1472
|
+
args.push("--model", job.agent.model);
|
|
1473
|
+
}
|
|
851
1474
|
if (!hasAnyFlag(args, [
|
|
852
1475
|
"--permission-mode",
|
|
853
1476
|
"--dangerously-skip-permissions",
|
|
@@ -859,7 +1482,7 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
859
1482
|
if (resume && !args.includes("--resume")) {
|
|
860
1483
|
args.push("--resume", resume);
|
|
861
1484
|
}
|
|
862
|
-
const child =
|
|
1485
|
+
const child = spawn3(job.agent.command, args, {
|
|
863
1486
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
864
1487
|
env: { ...process.env, ...job.agent.env ?? {} },
|
|
865
1488
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -870,6 +1493,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
870
1493
|
child.stderr.on("data", (chunk) => {
|
|
871
1494
|
stderr += chunk.toString();
|
|
872
1495
|
});
|
|
1496
|
+
const noiseLines = [];
|
|
1497
|
+
const errorEvents = [];
|
|
1498
|
+
const NOISE_TAIL = 6;
|
|
873
1499
|
const result = {
|
|
874
1500
|
kind: "claude-code",
|
|
875
1501
|
text: "",
|
|
@@ -927,11 +1553,19 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
927
1553
|
try {
|
|
928
1554
|
event = JSON.parse(line);
|
|
929
1555
|
} catch {
|
|
1556
|
+
noiseLines.push(line);
|
|
1557
|
+
if (noiseLines.length > NOISE_TAIL) noiseLines.shift();
|
|
930
1558
|
return;
|
|
931
1559
|
}
|
|
932
1560
|
result.rawEvents.push(event);
|
|
933
1561
|
update({ kind: "claude_code_event", event });
|
|
934
1562
|
captureSessionId(event);
|
|
1563
|
+
if (event && (event.is_error === true || event.subtype === "error" || event.type === "result" && event.is_error)) {
|
|
1564
|
+
try {
|
|
1565
|
+
errorEvents.push(JSON.stringify(event).slice(0, 1e3));
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
935
1569
|
if (event.type === "assistant" && event.message?.content) {
|
|
936
1570
|
ingestAssistantContent(
|
|
937
1571
|
Array.isArray(event.message.content) ? event.message.content : []
|
|
@@ -950,9 +1584,148 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
950
1584
|
}
|
|
951
1585
|
const rl = createInterface2({ input: child.stdout });
|
|
952
1586
|
rl.on("line", processLine);
|
|
953
|
-
const code = await new Promise((
|
|
1587
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1588
|
+
if (code !== 0) {
|
|
1589
|
+
const parts = [];
|
|
1590
|
+
if (stderr.trim()) parts.push(stderr.trim());
|
|
1591
|
+
if (errorEvents.length > 0) {
|
|
1592
|
+
parts.push(`Error events: ${errorEvents.join(" | ")}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (noiseLines.length > 0 && parts.length === 0) {
|
|
1595
|
+
parts.push(`Last stdout lines: ${noiseLines.join(" | ").slice(-1500)}`);
|
|
1596
|
+
}
|
|
1597
|
+
if (parts.length === 0) {
|
|
1598
|
+
parts.push(`Claude Code exited with code ${code} (no stderr or stdout output)`);
|
|
1599
|
+
}
|
|
1600
|
+
throw new Error(parts.join(" \u2014 "));
|
|
1601
|
+
}
|
|
1602
|
+
if (!result.text) {
|
|
1603
|
+
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
1604
|
+
}
|
|
1605
|
+
result.toolCalls = [...toolCalls.values()];
|
|
1606
|
+
return JSON.stringify(result);
|
|
1607
|
+
}
|
|
1608
|
+
async function runKimiCodeAgent(job, update) {
|
|
1609
|
+
const args = [...job.agent.args];
|
|
1610
|
+
if (!args.includes("--print") && !args.includes("-p")) {
|
|
1611
|
+
args.push("--print");
|
|
1612
|
+
}
|
|
1613
|
+
if (!args.includes("--output-format")) {
|
|
1614
|
+
args.push("--output-format", "stream-json");
|
|
1615
|
+
}
|
|
1616
|
+
if (!hasAnyFlag(args, ["--yolo", "--yes", "-y"])) {
|
|
1617
|
+
args.push("--yolo");
|
|
1618
|
+
}
|
|
1619
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1620
|
+
args.push("--model", job.agent.model);
|
|
1621
|
+
}
|
|
1622
|
+
const resume = job.resumeSessionId ?? job.agent.env?.KIMI_SESSION_ID ?? null;
|
|
1623
|
+
if (resume && !hasAnyFlag(args, ["--session", "-S", "-r", "--resume", "--continue", "-C"])) {
|
|
1624
|
+
args.push("-r", resume);
|
|
1625
|
+
}
|
|
1626
|
+
const cwd = job.agent.cwd ?? process.cwd();
|
|
1627
|
+
let prompt = job.query;
|
|
1628
|
+
if (!resume) {
|
|
1629
|
+
const agentsMdPath = join5(cwd, "AGENTS.md");
|
|
1630
|
+
if (existsSync5(agentsMdPath)) {
|
|
1631
|
+
try {
|
|
1632
|
+
const primer = readFileSync3(agentsMdPath, "utf-8").trim();
|
|
1633
|
+
if (primer) {
|
|
1634
|
+
prompt = `# System instructions (read carefully before responding)
|
|
1635
|
+
|
|
1636
|
+
${primer}
|
|
1637
|
+
|
|
1638
|
+
---
|
|
1639
|
+
|
|
1640
|
+
# Your task
|
|
1641
|
+
|
|
1642
|
+
${job.query}`;
|
|
1643
|
+
}
|
|
1644
|
+
} catch {
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const child = spawn3(job.agent.command, args, {
|
|
1649
|
+
cwd,
|
|
1650
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
1651
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1652
|
+
});
|
|
1653
|
+
child.stdin.write(prompt);
|
|
1654
|
+
child.stdin.end();
|
|
1655
|
+
let stderr = "";
|
|
1656
|
+
child.stderr.on("data", (chunk) => {
|
|
1657
|
+
stderr += chunk.toString();
|
|
1658
|
+
});
|
|
1659
|
+
const result = {
|
|
1660
|
+
kind: "kimi",
|
|
1661
|
+
text: "",
|
|
1662
|
+
messages: [],
|
|
1663
|
+
toolCalls: [],
|
|
1664
|
+
sessionId: resume,
|
|
1665
|
+
rawEvents: []
|
|
1666
|
+
};
|
|
1667
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1668
|
+
function ingestAssistantContent(blocks) {
|
|
1669
|
+
for (const block of blocks) {
|
|
1670
|
+
if (!block || typeof block !== "object") continue;
|
|
1671
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1672
|
+
result.messages.push(block.text);
|
|
1673
|
+
update({ kind: "message", text: block.text });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function ingestToolCalls(calls) {
|
|
1678
|
+
for (const call of calls) {
|
|
1679
|
+
if (!call || typeof call !== "object") continue;
|
|
1680
|
+
const id = typeof call.id === "string" ? call.id : null;
|
|
1681
|
+
if (!id) continue;
|
|
1682
|
+
const name = String(call.function?.name ?? call.name ?? "tool");
|
|
1683
|
+
let input2 = call.function?.arguments ?? call.arguments;
|
|
1684
|
+
if (typeof input2 === "string") {
|
|
1685
|
+
try {
|
|
1686
|
+
input2 = JSON.parse(input2);
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const next = { ...toolCalls.get(id) ?? {}, id, name, input: input2 };
|
|
1691
|
+
toolCalls.set(id, next);
|
|
1692
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function ingestToolResult(event) {
|
|
1696
|
+
const id = event?.tool_call_id;
|
|
1697
|
+
if (typeof id !== "string") return;
|
|
1698
|
+
const existing = toolCalls.get(id) ?? { id, name: "tool" };
|
|
1699
|
+
const output2 = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
|
|
1700
|
+
const next = { ...existing, output: output2 };
|
|
1701
|
+
toolCalls.set(id, next);
|
|
1702
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1703
|
+
}
|
|
1704
|
+
function processLine(line) {
|
|
1705
|
+
const trimmed = line.trim();
|
|
1706
|
+
if (!trimmed || trimmed[0] !== "{") return;
|
|
1707
|
+
let event;
|
|
1708
|
+
try {
|
|
1709
|
+
event = JSON.parse(trimmed);
|
|
1710
|
+
} catch {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
result.rawEvents.push(event);
|
|
1714
|
+
update({ kind: "kimi_event", event });
|
|
1715
|
+
if (event.role === "assistant") {
|
|
1716
|
+
if (Array.isArray(event.content)) ingestAssistantContent(event.content);
|
|
1717
|
+
if (Array.isArray(event.tool_calls)) ingestToolCalls(event.tool_calls);
|
|
1718
|
+
} else if (event.role === "tool") {
|
|
1719
|
+
ingestToolResult(event);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const rl = createInterface2({ input: child.stdout });
|
|
1723
|
+
rl.on("line", processLine);
|
|
1724
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1725
|
+
const resumeMatch = stderr.match(/To resume this session:\s*kimi\s+-r\s+([A-Za-z0-9_-]+)/);
|
|
1726
|
+
if (resumeMatch) result.sessionId = resumeMatch[1];
|
|
954
1727
|
if (code !== 0) {
|
|
955
|
-
throw new Error(stderr.trim() || `
|
|
1728
|
+
throw new Error(stderr.trim() || `Kimi Code exited with code ${code}`);
|
|
956
1729
|
}
|
|
957
1730
|
if (!result.text) {
|
|
958
1731
|
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
@@ -982,7 +1755,7 @@ async function runOpenClawAgent(job, update) {
|
|
|
982
1755
|
}
|
|
983
1756
|
let requestedSessionId = getFlagValue(args, "--session-id");
|
|
984
1757
|
if (!requestedSessionId) {
|
|
985
|
-
requestedSessionId = job.resumeSessionId || `botapp-${
|
|
1758
|
+
requestedSessionId = job.resumeSessionId || `botapp-${randomUUID2()}`;
|
|
986
1759
|
args.push("--session-id", requestedSessionId);
|
|
987
1760
|
}
|
|
988
1761
|
if (!hasFlag(args, "--verbose")) {
|
|
@@ -992,7 +1765,7 @@ async function runOpenClawAgent(job, update) {
|
|
|
992
1765
|
const state = {
|
|
993
1766
|
startedAtMs: Date.now(),
|
|
994
1767
|
sessionDir,
|
|
995
|
-
sessionStorePath:
|
|
1768
|
+
sessionStorePath: join5(sessionDir, "sessions.json"),
|
|
996
1769
|
requestedSessionId,
|
|
997
1770
|
sessionKey: null,
|
|
998
1771
|
sessionId: null,
|
|
@@ -1016,14 +1789,23 @@ async function runOpenClawAgent(job, update) {
|
|
|
1016
1789
|
};
|
|
1017
1790
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1018
1791
|
let stderr = "";
|
|
1792
|
+
const stdoutLines = [];
|
|
1019
1793
|
let stopPolling = false;
|
|
1020
|
-
const child =
|
|
1794
|
+
const child = spawn3(job.agent.command, args, {
|
|
1021
1795
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
1022
1796
|
env,
|
|
1023
1797
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1024
1798
|
});
|
|
1025
1799
|
const stdout = createInterface2({ input: child.stdout });
|
|
1026
|
-
stdout.on("line", () => {
|
|
1800
|
+
stdout.on("line", (line) => {
|
|
1801
|
+
const text = line.trim();
|
|
1802
|
+
if (!text) return;
|
|
1803
|
+
stdoutLines.push(text);
|
|
1804
|
+
const stdoutText = extractOpenClawStdoutText(text);
|
|
1805
|
+
if (!stdoutText) return;
|
|
1806
|
+
result.text = stdoutText;
|
|
1807
|
+
result.messages.push(stdoutText);
|
|
1808
|
+
update({ kind: "message", text: stdoutText });
|
|
1027
1809
|
});
|
|
1028
1810
|
const stderrReader = createInterface2({ input: child.stderr });
|
|
1029
1811
|
stderrReader.on("line", (line) => {
|
|
@@ -1055,11 +1837,11 @@ async function runOpenClawAgent(job, update) {
|
|
|
1055
1837
|
}
|
|
1056
1838
|
result.sessionFile = state.selectedFile;
|
|
1057
1839
|
result.toolCalls = [...toolCalls.values()];
|
|
1058
|
-
result.text = result.text || result.messages.at(-1)?.trim() || "";
|
|
1840
|
+
result.text = result.text || result.messages.at(-1)?.trim() || extractOpenClawStdoutText(stdoutLines.at(-1) ?? "") || "";
|
|
1059
1841
|
if (code !== 0) {
|
|
1060
1842
|
throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
|
|
1061
1843
|
}
|
|
1062
|
-
if (!result.sessionId) {
|
|
1844
|
+
if (!result.sessionId && !result.text && result.toolCalls.length === 0) {
|
|
1063
1845
|
throw new Error(
|
|
1064
1846
|
[
|
|
1065
1847
|
"OpenClaw completed, but the session key was not resolved from sessions.json.",
|
|
@@ -1080,11 +1862,22 @@ async function runOpenClawAgent(job, update) {
|
|
|
1080
1862
|
}
|
|
1081
1863
|
return JSON.stringify(result);
|
|
1082
1864
|
}
|
|
1865
|
+
function extractOpenClawStdoutText(line) {
|
|
1866
|
+
const text = line.trim();
|
|
1867
|
+
if (!text) return "";
|
|
1868
|
+
try {
|
|
1869
|
+
const parsed = JSON.parse(text);
|
|
1870
|
+
if (parsed && typeof parsed === "object") return JSON.stringify(parsed);
|
|
1871
|
+
if (typeof parsed === "string") return parsed;
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
return text;
|
|
1875
|
+
}
|
|
1083
1876
|
function resolveOpenClawSessionDir(args, env) {
|
|
1084
1877
|
const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
|
|
1085
1878
|
if (configured) return expandPath(configured);
|
|
1086
1879
|
const agentName = resolveOpenClawAgentName(args);
|
|
1087
|
-
return
|
|
1880
|
+
return join5(homedir5(), ".openclaw", "agents", agentName, "sessions");
|
|
1088
1881
|
}
|
|
1089
1882
|
function resolveOpenClawAgentName(args) {
|
|
1090
1883
|
return getFlagValue(args, "--agent") ?? "main";
|
|
@@ -1093,7 +1886,7 @@ function snapshotOpenClawSessionOffsets(sessionDir) {
|
|
|
1093
1886
|
const offsets = /* @__PURE__ */ new Map();
|
|
1094
1887
|
for (const file of listOpenClawSessionFiles(sessionDir)) {
|
|
1095
1888
|
try {
|
|
1096
|
-
offsets.set(file,
|
|
1889
|
+
offsets.set(file, statSync3(file).size);
|
|
1097
1890
|
} catch {
|
|
1098
1891
|
}
|
|
1099
1892
|
}
|
|
@@ -1169,7 +1962,7 @@ function selectOpenClawSessionFile(state) {
|
|
|
1169
1962
|
const files = listOpenClawSessionFiles(state.sessionDir);
|
|
1170
1963
|
if (files.length === 0) return null;
|
|
1171
1964
|
if (state.sessionId) {
|
|
1172
|
-
const exact =
|
|
1965
|
+
const exact = join5(state.sessionDir, `${state.sessionId}.jsonl`);
|
|
1173
1966
|
if (files.includes(exact)) return exact;
|
|
1174
1967
|
const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
|
|
1175
1968
|
if (matching.length > 0) return newestFile(matching);
|
|
@@ -1177,7 +1970,7 @@ function selectOpenClawSessionFile(state) {
|
|
|
1177
1970
|
if (files.length === 1) return files[0];
|
|
1178
1971
|
const recent = files.filter((file) => {
|
|
1179
1972
|
try {
|
|
1180
|
-
return
|
|
1973
|
+
return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
|
|
1181
1974
|
} catch {
|
|
1182
1975
|
return false;
|
|
1183
1976
|
}
|
|
@@ -1186,10 +1979,10 @@ function selectOpenClawSessionFile(state) {
|
|
|
1186
1979
|
}
|
|
1187
1980
|
function listOpenClawSessionFiles(sessionDir) {
|
|
1188
1981
|
try {
|
|
1189
|
-
if (!
|
|
1190
|
-
return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) =>
|
|
1982
|
+
if (!existsSync5(sessionDir)) return [];
|
|
1983
|
+
return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join5(sessionDir, name)).filter((file) => {
|
|
1191
1984
|
try {
|
|
1192
|
-
return
|
|
1985
|
+
return statSync3(file).isFile();
|
|
1193
1986
|
} catch {
|
|
1194
1987
|
return false;
|
|
1195
1988
|
}
|
|
@@ -1202,7 +1995,7 @@ function newestFile(files) {
|
|
|
1202
1995
|
if (files.length === 0) return null;
|
|
1203
1996
|
return files.reduce((selected, file) => {
|
|
1204
1997
|
try {
|
|
1205
|
-
return
|
|
1998
|
+
return statSync3(file).mtimeMs > statSync3(selected).mtimeMs ? file : selected;
|
|
1206
1999
|
} catch {
|
|
1207
2000
|
return selected;
|
|
1208
2001
|
}
|
|
@@ -1326,7 +2119,7 @@ async function runHermesAgent(job, update) {
|
|
|
1326
2119
|
let stderr = "";
|
|
1327
2120
|
let stdoutText = "";
|
|
1328
2121
|
let stopPolling = false;
|
|
1329
|
-
const child =
|
|
2122
|
+
const child = spawn3(job.agent.command, args, {
|
|
1330
2123
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
1331
2124
|
env,
|
|
1332
2125
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1393,10 +2186,10 @@ async function runHermesAgent(job, update) {
|
|
|
1393
2186
|
function resolveHermesSessionDir(env) {
|
|
1394
2187
|
const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
|
|
1395
2188
|
if (configured) return expandPath(configured);
|
|
1396
|
-
return
|
|
2189
|
+
return join5(homedir5(), ".hermes", "sessions");
|
|
1397
2190
|
}
|
|
1398
2191
|
function hermesSessionFile(sessionDir, sessionId) {
|
|
1399
|
-
return
|
|
2192
|
+
return join5(sessionDir, `session_${sessionId}.json`);
|
|
1400
2193
|
}
|
|
1401
2194
|
function parseHermesSessionId(text) {
|
|
1402
2195
|
const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
|
|
@@ -1444,13 +2237,13 @@ function readHermesSessionUpdates(state, result, toolCalls, update, final = fals
|
|
|
1444
2237
|
function selectHermesSessionFile(state) {
|
|
1445
2238
|
if (state.resumeSessionId) {
|
|
1446
2239
|
const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
|
|
1447
|
-
if (
|
|
2240
|
+
if (existsSync5(exact)) return exact;
|
|
1448
2241
|
}
|
|
1449
2242
|
const files = listHermesSessionFiles(state.sessionDir);
|
|
1450
2243
|
if (files.length === 0) return null;
|
|
1451
2244
|
const recent = files.filter((file) => {
|
|
1452
2245
|
try {
|
|
1453
|
-
return
|
|
2246
|
+
return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
|
|
1454
2247
|
} catch {
|
|
1455
2248
|
return false;
|
|
1456
2249
|
}
|
|
@@ -1459,10 +2252,10 @@ function selectHermesSessionFile(state) {
|
|
|
1459
2252
|
}
|
|
1460
2253
|
function listHermesSessionFiles(sessionDir) {
|
|
1461
2254
|
try {
|
|
1462
|
-
if (!
|
|
1463
|
-
return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) =>
|
|
2255
|
+
if (!existsSync5(sessionDir)) return [];
|
|
2256
|
+
return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join5(sessionDir, name)).filter((file) => {
|
|
1464
2257
|
try {
|
|
1465
|
-
return
|
|
2258
|
+
return statSync3(file).isFile();
|
|
1466
2259
|
} catch {
|
|
1467
2260
|
return false;
|
|
1468
2261
|
}
|
|
@@ -1482,7 +2275,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
|
|
|
1482
2275
|
}
|
|
1483
2276
|
if (Array.isArray(message.tool_calls)) {
|
|
1484
2277
|
for (const call of message.tool_calls) {
|
|
1485
|
-
const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ??
|
|
2278
|
+
const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID2());
|
|
1486
2279
|
const name = String(call?.function?.name ?? call?.name ?? "tool");
|
|
1487
2280
|
const next = {
|
|
1488
2281
|
...toolCalls.get(id),
|
|
@@ -1511,7 +2304,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
|
|
|
1511
2304
|
}
|
|
1512
2305
|
}
|
|
1513
2306
|
async function runAcpAgent(job) {
|
|
1514
|
-
const child =
|
|
2307
|
+
const child = spawn3(job.agent.command, job.agent.args, {
|
|
1515
2308
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
1516
2309
|
env: { ...process.env, ...job.agent.env ?? {} },
|
|
1517
2310
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1567,8 +2360,8 @@ Invalid ACP stdout: ${line}`;
|
|
|
1567
2360
|
function request2(method, params) {
|
|
1568
2361
|
const id = nextId++;
|
|
1569
2362
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
1570
|
-
return new Promise((
|
|
1571
|
-
pending.set(id, { resolve:
|
|
2363
|
+
return new Promise((resolve11, reject) => {
|
|
2364
|
+
pending.set(id, { resolve: resolve11, reject });
|
|
1572
2365
|
});
|
|
1573
2366
|
}
|
|
1574
2367
|
child.on("exit", (code) => {
|
|
@@ -1592,7 +2385,7 @@ Invalid ACP stdout: ${line}`;
|
|
|
1592
2385
|
clientInfo: {
|
|
1593
2386
|
name: "botapp-daemon",
|
|
1594
2387
|
title: "botapp daemon",
|
|
1595
|
-
version: "0.2.
|
|
2388
|
+
version: "0.2.7"
|
|
1596
2389
|
}
|
|
1597
2390
|
});
|
|
1598
2391
|
const session = await request2("session/new", {
|
|
@@ -1684,9 +2477,9 @@ function hasAnyFlag(args, flags) {
|
|
|
1684
2477
|
return flags.some((flag) => hasFlag(args, flag));
|
|
1685
2478
|
}
|
|
1686
2479
|
function expandPath(path) {
|
|
1687
|
-
if (path === "~") return
|
|
1688
|
-
if (path.startsWith("~/")) return
|
|
1689
|
-
return
|
|
2480
|
+
if (path === "~") return homedir5();
|
|
2481
|
+
if (path.startsWith("~/")) return join5(homedir5(), path.slice(2));
|
|
2482
|
+
return resolve4(path);
|
|
1690
2483
|
}
|
|
1691
2484
|
function envNumber(env, name, fallback) {
|
|
1692
2485
|
const value = env[name];
|
|
@@ -1742,10 +2535,10 @@ async function waitForChild(child, timeoutMs, label) {
|
|
|
1742
2535
|
|
|
1743
2536
|
// src/commands/launch.ts
|
|
1744
2537
|
import { Command as Command4 } from "commander";
|
|
1745
|
-
import { spawn as
|
|
1746
|
-
import { resolve as
|
|
1747
|
-
import { existsSync as
|
|
1748
|
-
import { homedir as
|
|
2538
|
+
import { spawn as spawn5 } from "child_process";
|
|
2539
|
+
import { resolve as resolve5, join as join7 } from "path";
|
|
2540
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2541
|
+
import { homedir as homedir7 } from "os";
|
|
1749
2542
|
import { createInterface as createInterface3 } from "readline";
|
|
1750
2543
|
import { hostname } from "os";
|
|
1751
2544
|
import pc5 from "picocolors";
|
|
@@ -1753,7 +2546,7 @@ import pc5 from "picocolors";
|
|
|
1753
2546
|
// src/auth/browser-auth.ts
|
|
1754
2547
|
import { createServer } from "http";
|
|
1755
2548
|
import { randomBytes } from "crypto";
|
|
1756
|
-
import { spawn as
|
|
2549
|
+
import { spawn as spawn4 } from "child_process";
|
|
1757
2550
|
import pc4 from "picocolors";
|
|
1758
2551
|
var OK_HTML = `<!doctype html>
|
|
1759
2552
|
<meta charset="utf-8">
|
|
@@ -1848,11 +2641,11 @@ async function startLoopback(expectedState) {
|
|
|
1848
2641
|
let rejectCb = () => {
|
|
1849
2642
|
};
|
|
1850
2643
|
let settled = false;
|
|
1851
|
-
const callbackPromise = new Promise((
|
|
2644
|
+
const callbackPromise = new Promise((resolve11, reject) => {
|
|
1852
2645
|
resolveCb = (p) => {
|
|
1853
2646
|
if (settled) return;
|
|
1854
2647
|
settled = true;
|
|
1855
|
-
|
|
2648
|
+
resolve11(p);
|
|
1856
2649
|
};
|
|
1857
2650
|
rejectCb = (e) => {
|
|
1858
2651
|
if (settled) return;
|
|
@@ -1863,11 +2656,11 @@ async function startLoopback(expectedState) {
|
|
|
1863
2656
|
const server = createServer((req, res) => {
|
|
1864
2657
|
void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
|
|
1865
2658
|
});
|
|
1866
|
-
await new Promise((
|
|
2659
|
+
await new Promise((resolve11, reject) => {
|
|
1867
2660
|
server.once("error", reject);
|
|
1868
2661
|
server.listen(0, "127.0.0.1", () => {
|
|
1869
2662
|
server.removeListener("error", reject);
|
|
1870
|
-
|
|
2663
|
+
resolve11();
|
|
1871
2664
|
});
|
|
1872
2665
|
});
|
|
1873
2666
|
const address = server.address();
|
|
@@ -1979,7 +2772,7 @@ function asString(v) {
|
|
|
1979
2772
|
return typeof v === "string" ? v : void 0;
|
|
1980
2773
|
}
|
|
1981
2774
|
function readJsonBody(req) {
|
|
1982
|
-
return new Promise((
|
|
2775
|
+
return new Promise((resolve11, reject) => {
|
|
1983
2776
|
const chunks = [];
|
|
1984
2777
|
let total = 0;
|
|
1985
2778
|
req.on("data", (c) => {
|
|
@@ -1993,9 +2786,9 @@ function readJsonBody(req) {
|
|
|
1993
2786
|
});
|
|
1994
2787
|
req.on("end", () => {
|
|
1995
2788
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
1996
|
-
if (!raw) return
|
|
2789
|
+
if (!raw) return resolve11(null);
|
|
1997
2790
|
try {
|
|
1998
|
-
|
|
2791
|
+
resolve11(JSON.parse(raw));
|
|
1999
2792
|
} catch (e) {
|
|
2000
2793
|
reject(e);
|
|
2001
2794
|
}
|
|
@@ -2007,20 +2800,20 @@ function openUrl(url) {
|
|
|
2007
2800
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2008
2801
|
const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
2009
2802
|
try {
|
|
2010
|
-
|
|
2803
|
+
spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
2011
2804
|
} catch {
|
|
2012
2805
|
}
|
|
2013
2806
|
}
|
|
2014
2807
|
|
|
2015
2808
|
// src/commands/daemon-supervisor.ts
|
|
2016
|
-
import { existsSync as
|
|
2017
|
-
import { homedir as
|
|
2018
|
-
import { join as
|
|
2809
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
2810
|
+
import { homedir as homedir6 } from "os";
|
|
2811
|
+
import { join as join6 } from "path";
|
|
2019
2812
|
function daemonPidFile() {
|
|
2020
|
-
return
|
|
2813
|
+
return join6(homedir6(), ".botapp", "daemon.pid");
|
|
2021
2814
|
}
|
|
2022
2815
|
function isDaemonRunningLocally(pidFile = daemonPidFile()) {
|
|
2023
|
-
if (!
|
|
2816
|
+
if (!existsSync6(pidFile)) return false;
|
|
2024
2817
|
try {
|
|
2025
2818
|
const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
|
|
2026
2819
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -2167,7 +2960,8 @@ Configured for ${serverUrl}`));
|
|
|
2167
2960
|
server: result.serverUrl ?? serverUrl,
|
|
2168
2961
|
daemonId: data.daemon.id,
|
|
2169
2962
|
daemonName: data.daemon.name,
|
|
2170
|
-
token: data.token
|
|
2963
|
+
token: data.token,
|
|
2964
|
+
userEmail: result.userEmail
|
|
2171
2965
|
});
|
|
2172
2966
|
console.log(
|
|
2173
2967
|
` Daemon: ${pc5.bold(data.daemon.name)} ${pc5.dim(`(${data.daemon.id})`)}` + pc5.dim(` profile=${savedProfile.alias}`)
|
|
@@ -2188,20 +2982,20 @@ async function autoStartDaemon(opts, serverUrl, daemonId) {
|
|
|
2188
2982
|
Next: run \`bot daemon run\` to bring this machine online.`));
|
|
2189
2983
|
return;
|
|
2190
2984
|
}
|
|
2191
|
-
const dir =
|
|
2192
|
-
const pidFile =
|
|
2193
|
-
const logFile =
|
|
2985
|
+
const dir = join7(homedir7(), ".botapp");
|
|
2986
|
+
const pidFile = join7(dir, "daemon.pid");
|
|
2987
|
+
const logFile = join7(dir, "daemon.log");
|
|
2194
2988
|
mkdirSync3(dir, { recursive: true });
|
|
2195
2989
|
if (isDaemonRunningLocally(pidFile)) {
|
|
2196
2990
|
stopExistingDaemon(pidFile);
|
|
2197
2991
|
}
|
|
2198
2992
|
const botBin = process.argv[1];
|
|
2199
|
-
if (!botBin || !
|
|
2993
|
+
if (!botBin || !existsSync7(botBin)) {
|
|
2200
2994
|
console.log(pc5.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
|
|
2201
2995
|
return;
|
|
2202
2996
|
}
|
|
2203
2997
|
const logFd = openSync(logFile, "a");
|
|
2204
|
-
const child =
|
|
2998
|
+
const child = spawn5(process.execPath, [botBin, "daemon", "run"], {
|
|
2205
2999
|
stdio: ["ignore", logFd, logFd],
|
|
2206
3000
|
detached: true
|
|
2207
3001
|
});
|
|
@@ -2306,7 +3100,7 @@ Or run a local server from source:
|
|
|
2306
3100
|
return false;
|
|
2307
3101
|
}
|
|
2308
3102
|
console.log(pc5.blue("Starting local server..."));
|
|
2309
|
-
const child =
|
|
3103
|
+
const child = spawn5("node", ["--import", "tsx", serverEntry], {
|
|
2310
3104
|
env: { ...process.env, PORT: opts.port },
|
|
2311
3105
|
stdio: opts.background ? "ignore" : "inherit",
|
|
2312
3106
|
detached: opts.background
|
|
@@ -2323,12 +3117,12 @@ Or run a local server from source:
|
|
|
2323
3117
|
}
|
|
2324
3118
|
function findServerEntry2() {
|
|
2325
3119
|
const candidates = [
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
3120
|
+
resolve5(process.cwd(), "packages/server/src/index.ts"),
|
|
3121
|
+
resolve5(process.cwd(), "../server/src/index.ts"),
|
|
3122
|
+
resolve5(process.cwd(), "../../packages/server/src/index.ts")
|
|
2329
3123
|
];
|
|
2330
3124
|
for (const c of candidates) {
|
|
2331
|
-
if (
|
|
3125
|
+
if (existsSync7(c)) return c;
|
|
2332
3126
|
}
|
|
2333
3127
|
return null;
|
|
2334
3128
|
}
|
|
@@ -2530,21 +3324,21 @@ var loginCommand = new Command7("login").description("Login to a botapp server")
|
|
|
2530
3324
|
|
|
2531
3325
|
// src/commands/install.ts
|
|
2532
3326
|
import { Command as Command8 } from "commander";
|
|
2533
|
-
import { resolve as
|
|
2534
|
-
import { existsSync as
|
|
2535
|
-
import { homedir as
|
|
2536
|
-
import { join as
|
|
3327
|
+
import { resolve as resolve6, basename } from "path";
|
|
3328
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
|
|
3329
|
+
import { homedir as homedir8 } from "os";
|
|
3330
|
+
import { join as join8 } from "path";
|
|
2537
3331
|
import pc9 from "picocolors";
|
|
2538
|
-
var APPS_DIR =
|
|
3332
|
+
var APPS_DIR = join8(homedir8(), ".botapp", "apps");
|
|
2539
3333
|
var installCommand = new Command8("install").description("Install an app from a local path").argument("<path>", "Path to the app directory").option("--dev", "Install in dev mode (symlink)").action(async (appPath, opts) => {
|
|
2540
|
-
const absPath =
|
|
2541
|
-
if (!
|
|
3334
|
+
const absPath = resolve6(appPath);
|
|
3335
|
+
if (!existsSync8(absPath)) {
|
|
2542
3336
|
console.error(pc9.red(`Path not found: ${absPath}`));
|
|
2543
3337
|
process.exitCode = 1;
|
|
2544
3338
|
return;
|
|
2545
3339
|
}
|
|
2546
|
-
const manifestPath =
|
|
2547
|
-
if (!
|
|
3340
|
+
const manifestPath = join8(absPath, "botapp.app.json");
|
|
3341
|
+
if (!existsSync8(manifestPath)) {
|
|
2548
3342
|
console.error(pc9.red(`No botapp.app.json found in ${absPath}`));
|
|
2549
3343
|
process.exitCode = 1;
|
|
2550
3344
|
return;
|
|
@@ -2558,9 +3352,9 @@ var installCommand = new Command8("install").description("Install an app from a
|
|
|
2558
3352
|
return;
|
|
2559
3353
|
}
|
|
2560
3354
|
const appName = manifest.name || basename(absPath);
|
|
2561
|
-
const targetDir =
|
|
3355
|
+
const targetDir = join8(APPS_DIR, appName);
|
|
2562
3356
|
mkdirSync4(APPS_DIR, { recursive: true });
|
|
2563
|
-
if (
|
|
3357
|
+
if (existsSync8(targetDir)) {
|
|
2564
3358
|
console.log(pc9.yellow(`App "${appName}" is already installed. Reinstalling...`));
|
|
2565
3359
|
const { rmSync: rmSync2 } = await import("fs");
|
|
2566
3360
|
rmSync2(targetDir, { recursive: true, force: true });
|
|
@@ -2579,14 +3373,14 @@ var installCommand = new Command8("install").description("Install an app from a
|
|
|
2579
3373
|
|
|
2580
3374
|
// src/commands/uninstall.ts
|
|
2581
3375
|
import { Command as Command9 } from "commander";
|
|
2582
|
-
import { existsSync as
|
|
2583
|
-
import { homedir as
|
|
2584
|
-
import { join as
|
|
3376
|
+
import { existsSync as existsSync9, rmSync } from "fs";
|
|
3377
|
+
import { homedir as homedir9 } from "os";
|
|
3378
|
+
import { join as join9 } from "path";
|
|
2585
3379
|
import pc10 from "picocolors";
|
|
2586
|
-
var APPS_DIR2 =
|
|
3380
|
+
var APPS_DIR2 = join9(homedir9(), ".botapp", "apps");
|
|
2587
3381
|
var uninstallCommand = new Command9("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
|
|
2588
|
-
const targetDir =
|
|
2589
|
-
if (!
|
|
3382
|
+
const targetDir = join9(APPS_DIR2, name);
|
|
3383
|
+
if (!existsSync9(targetDir)) {
|
|
2590
3384
|
console.error(pc10.red(`App "${name}" is not installed`));
|
|
2591
3385
|
process.exitCode = 1;
|
|
2592
3386
|
return;
|
|
@@ -2598,21 +3392,21 @@ var uninstallCommand = new Command9("uninstall").description("Uninstall an app")
|
|
|
2598
3392
|
|
|
2599
3393
|
// src/commands/dev.ts
|
|
2600
3394
|
import { Command as Command10 } from "commander";
|
|
2601
|
-
import { resolve as
|
|
2602
|
-
import { existsSync as
|
|
2603
|
-
import { homedir as
|
|
2604
|
-
import { spawn as
|
|
3395
|
+
import { resolve as resolve7, basename as basename2, join as join10 } from "path";
|
|
3396
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
|
|
3397
|
+
import { homedir as homedir10 } from "os";
|
|
3398
|
+
import { spawn as spawn6 } from "child_process";
|
|
2605
3399
|
import pc11 from "picocolors";
|
|
2606
|
-
var APPS_DIR3 =
|
|
3400
|
+
var APPS_DIR3 = join10(homedir10(), ".botapp", "apps");
|
|
2607
3401
|
var devCommand = new Command10("dev").description("Start development mode for an app").argument("[path]", "Path to the app directory", ".").option("-p, --port <port>", "Server port", "7100").action(async (appPath, opts) => {
|
|
2608
|
-
const absPath =
|
|
2609
|
-
if (!
|
|
3402
|
+
const absPath = resolve7(appPath);
|
|
3403
|
+
if (!existsSync10(absPath)) {
|
|
2610
3404
|
console.error(pc11.red(`Path not found: ${absPath}`));
|
|
2611
3405
|
process.exitCode = 1;
|
|
2612
3406
|
return;
|
|
2613
3407
|
}
|
|
2614
|
-
const manifestPath =
|
|
2615
|
-
if (!
|
|
3408
|
+
const manifestPath = join10(absPath, "botapp.app.json");
|
|
3409
|
+
if (!existsSync10(manifestPath)) {
|
|
2616
3410
|
console.error(pc11.red(`No botapp.app.json found in ${absPath}`));
|
|
2617
3411
|
process.exitCode = 1;
|
|
2618
3412
|
return;
|
|
@@ -2626,9 +3420,9 @@ var devCommand = new Command10("dev").description("Start development mode for an
|
|
|
2626
3420
|
return;
|
|
2627
3421
|
}
|
|
2628
3422
|
const appName = manifest.name || basename2(absPath);
|
|
2629
|
-
const targetDir =
|
|
3423
|
+
const targetDir = join10(APPS_DIR3, appName);
|
|
2630
3424
|
mkdirSync5(APPS_DIR3, { recursive: true });
|
|
2631
|
-
if (!
|
|
3425
|
+
if (!existsSync10(targetDir)) {
|
|
2632
3426
|
symlinkSync2(absPath, targetDir, "dir");
|
|
2633
3427
|
console.log(pc11.blue(`Linked ${pc11.bold(appName)} \u2192 ${pc11.dim(absPath)}`));
|
|
2634
3428
|
} else {
|
|
@@ -2661,7 +3455,7 @@ Start the server separately, then restart it to load the app:
|
|
|
2661
3455
|
return;
|
|
2662
3456
|
}
|
|
2663
3457
|
console.log(pc11.blue("Starting botapp server..."));
|
|
2664
|
-
const child =
|
|
3458
|
+
const child = spawn6("node", ["--import", "tsx", serverEntry], {
|
|
2665
3459
|
env: { ...process.env, PORT: opts.port },
|
|
2666
3460
|
stdio: "inherit"
|
|
2667
3461
|
});
|
|
@@ -2675,11 +3469,11 @@ Start the server separately, then restart it to load the app:
|
|
|
2675
3469
|
});
|
|
2676
3470
|
function findServerEntry3() {
|
|
2677
3471
|
const candidates = [
|
|
2678
|
-
|
|
2679
|
-
|
|
3472
|
+
resolve7(process.cwd(), "packages/server/src/index.ts"),
|
|
3473
|
+
resolve7(process.cwd(), "../server/src/index.ts")
|
|
2680
3474
|
];
|
|
2681
3475
|
for (const c of candidates) {
|
|
2682
|
-
if (
|
|
3476
|
+
if (existsSync10(c)) return c;
|
|
2683
3477
|
}
|
|
2684
3478
|
return null;
|
|
2685
3479
|
}
|
|
@@ -2913,13 +3707,13 @@ All agents:`);
|
|
|
2913
3707
|
|
|
2914
3708
|
// src/commands/register.ts
|
|
2915
3709
|
import { Command as Command14 } from "commander";
|
|
2916
|
-
import { readFileSync as readFileSync7, existsSync as
|
|
3710
|
+
import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
|
|
2917
3711
|
import pc15 from "picocolors";
|
|
2918
3712
|
var registerCommand = new Command14("register").description("Register an external app via HTTP bridge manifest").argument("<manifest>", "Path to YAML manifest file").option("--adapter <url>", "Override base URL from manifest").action(async (manifestPath, opts, cmd) => {
|
|
2919
3713
|
const globalOpts = cmd.parent?.opts() ?? {};
|
|
2920
3714
|
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
2921
3715
|
const token = resolveToken(globalOpts.token);
|
|
2922
|
-
if (!
|
|
3716
|
+
if (!existsSync11(manifestPath)) {
|
|
2923
3717
|
console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
|
|
2924
3718
|
process.exitCode = 1;
|
|
2925
3719
|
return;
|
|
@@ -2956,13 +3750,13 @@ var registerCommand = new Command14("register").description("Register an externa
|
|
|
2956
3750
|
|
|
2957
3751
|
// src/commands/wrap.ts
|
|
2958
3752
|
import { Command as Command15 } from "commander";
|
|
2959
|
-
import { readFileSync as readFileSync8, existsSync as
|
|
3753
|
+
import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
|
|
2960
3754
|
import pc16 from "picocolors";
|
|
2961
3755
|
var wrapCommand = new Command15("wrap").description("Register CLI tool as app commands via YAML manifest").argument("<manifest>", "Path to CLI wrapper YAML manifest").action(async (manifestPath, _opts, cmd) => {
|
|
2962
3756
|
const globalOpts = cmd.parent?.opts() ?? {};
|
|
2963
3757
|
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
2964
3758
|
const token = resolveToken(globalOpts.token);
|
|
2965
|
-
if (!
|
|
3759
|
+
if (!existsSync12(manifestPath)) {
|
|
2966
3760
|
console.error(pc16.red(`Error: Manifest not found: ${manifestPath}`));
|
|
2967
3761
|
process.exitCode = 1;
|
|
2968
3762
|
return;
|
|
@@ -3226,7 +4020,8 @@ var pairingCommand = new Command18("pairing").alias("pair").description("Pair th
|
|
|
3226
4020
|
server: serverUrl,
|
|
3227
4021
|
daemonId: data.daemon.id,
|
|
3228
4022
|
daemonName: data.daemon.name,
|
|
3229
|
-
token: data.token
|
|
4023
|
+
token: data.token,
|
|
4024
|
+
userEmail: grant.userEmail
|
|
3230
4025
|
});
|
|
3231
4026
|
console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
|
|
3232
4027
|
console.log(` ID: ${data.daemon.id}`);
|
|
@@ -3262,13 +4057,130 @@ async function obtainPairingToken(opts) {
|
|
|
3262
4057
|
return { pairingToken: result.pairingToken, userEmail: result.userEmail };
|
|
3263
4058
|
}
|
|
3264
4059
|
|
|
3265
|
-
// src/commands/
|
|
3266
|
-
import { spawn as spawn6 } from "child_process";
|
|
3267
|
-
import { realpathSync } from "fs";
|
|
4060
|
+
// src/commands/doctor.ts
|
|
3268
4061
|
import { Command as Command19 } from "commander";
|
|
3269
4062
|
import pc20 from "picocolors";
|
|
4063
|
+
var doctorCommand = new Command19("doctor").description("Diagnose paired daemon profiles (server reachability, token validity, account ownership)").action(async () => {
|
|
4064
|
+
const profiles = loadDaemonProfiles();
|
|
4065
|
+
if (profiles.length === 0) {
|
|
4066
|
+
console.log(pc20.dim("No paired daemons in ~/.botapp/daemon.yaml."));
|
|
4067
|
+
console.log(pc20.dim("Run `bot pair` to add one."));
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
let warns = 0;
|
|
4071
|
+
let fails = 0;
|
|
4072
|
+
for (const profile of profiles) {
|
|
4073
|
+
console.log(pc20.bold(`
|
|
4074
|
+
[${profile.alias ?? profile.server}]`));
|
|
4075
|
+
console.log(` Server: ${profile.server}`);
|
|
4076
|
+
console.log(` Daemon: ${profile.daemonName} ${pc20.dim(`(${profile.daemonId})`)}`);
|
|
4077
|
+
const findings = await checkProfile(profile);
|
|
4078
|
+
for (const f of findings) {
|
|
4079
|
+
const tag = f.severity === "ok" ? pc20.green(" \u2713 ") : f.severity === "warn" ? pc20.yellow(" \u26A0 ") : pc20.red(" \u2717 ");
|
|
4080
|
+
console.log(`${tag}${f.message}`);
|
|
4081
|
+
if (f.fix) console.log(pc20.dim(` \u2192 ${f.fix}`));
|
|
4082
|
+
if (f.severity === "warn") warns += 1;
|
|
4083
|
+
if (f.severity === "fail") fails += 1;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
console.log("");
|
|
4087
|
+
if (fails === 0 && warns === 0) {
|
|
4088
|
+
console.log(pc20.green("All checks passed."));
|
|
4089
|
+
} else {
|
|
4090
|
+
const parts = [];
|
|
4091
|
+
if (fails > 0) parts.push(pc20.red(`${fails} failing`));
|
|
4092
|
+
if (warns > 0) parts.push(pc20.yellow(`${warns} warning${warns === 1 ? "" : "s"}`));
|
|
4093
|
+
console.log(parts.join(", ") + ".");
|
|
4094
|
+
if (fails > 0) process.exitCode = 1;
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
async function checkProfile(profile) {
|
|
4098
|
+
const findings = [];
|
|
4099
|
+
let serverOk = false;
|
|
4100
|
+
try {
|
|
4101
|
+
const res = await fetch(`${profile.server}/health`, {
|
|
4102
|
+
signal: AbortSignal.timeout(5e3)
|
|
4103
|
+
});
|
|
4104
|
+
if (res.ok) {
|
|
4105
|
+
findings.push({ severity: "ok", message: "Server reachable" });
|
|
4106
|
+
serverOk = true;
|
|
4107
|
+
} else {
|
|
4108
|
+
findings.push({
|
|
4109
|
+
severity: "fail",
|
|
4110
|
+
message: `Server returned ${res.status} ${res.statusText}`,
|
|
4111
|
+
fix: `Verify the server at ${profile.server} is running.`
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
} catch (e) {
|
|
4115
|
+
findings.push({
|
|
4116
|
+
severity: "fail",
|
|
4117
|
+
message: `Server unreachable: ${e.message ?? e}`,
|
|
4118
|
+
fix: `Start the server, or remove this profile with \`bot daemon unpair ${profile.alias ?? profile.server}\`.`
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
if (!serverOk) return findings;
|
|
4122
|
+
let serverEmail = null;
|
|
4123
|
+
try {
|
|
4124
|
+
const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self");
|
|
4125
|
+
findings.push({ severity: "ok", message: "Token valid" });
|
|
4126
|
+
serverEmail = data.user?.email ?? null;
|
|
4127
|
+
if (serverEmail) {
|
|
4128
|
+
findings.push({
|
|
4129
|
+
severity: "ok",
|
|
4130
|
+
message: `Token paired under ${pc20.bold(serverEmail)}`
|
|
4131
|
+
});
|
|
4132
|
+
} else {
|
|
4133
|
+
findings.push({
|
|
4134
|
+
severity: "warn",
|
|
4135
|
+
message: "Server returned no user record for this daemon",
|
|
4136
|
+
fix: "The owning user may have been deleted. Re-pair with `bot pair`."
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
if (data.daemon?.status === "online") {
|
|
4140
|
+
findings.push({ severity: "ok", message: "Daemon currently online" });
|
|
4141
|
+
} else {
|
|
4142
|
+
findings.push({
|
|
4143
|
+
severity: "warn",
|
|
4144
|
+
message: `Daemon status: ${data.daemon?.status ?? "unknown"}`,
|
|
4145
|
+
fix: `Run \`bot daemon run --server ${profile.server}\` to bring it online.`
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
} catch (e) {
|
|
4149
|
+
const msg = e?.message ?? String(e);
|
|
4150
|
+
if (/unauthor/i.test(msg) || /token/i.test(msg)) {
|
|
4151
|
+
findings.push({
|
|
4152
|
+
severity: "fail",
|
|
4153
|
+
message: `Token rejected: ${msg}`,
|
|
4154
|
+
fix: "The token is stale (likely the server DB was reset). Re-pair with `bot pair`."
|
|
4155
|
+
});
|
|
4156
|
+
return findings;
|
|
4157
|
+
}
|
|
4158
|
+
findings.push({ severity: "fail", message: `Identity probe failed: ${msg}` });
|
|
4159
|
+
return findings;
|
|
4160
|
+
}
|
|
4161
|
+
if (profile.userEmail && serverEmail && profile.userEmail !== serverEmail) {
|
|
4162
|
+
findings.push({
|
|
4163
|
+
severity: "warn",
|
|
4164
|
+
message: `daemon.yaml says \`userEmail: ${profile.userEmail}\` but server says \`${serverEmail}\``,
|
|
4165
|
+
fix: "Re-pair to refresh the recorded email."
|
|
4166
|
+
});
|
|
4167
|
+
} else if (!profile.userEmail && serverEmail) {
|
|
4168
|
+
findings.push({
|
|
4169
|
+
severity: "warn",
|
|
4170
|
+
message: "daemon.yaml has no userEmail recorded",
|
|
4171
|
+
fix: "Re-pair to capture it (cosmetic \u2014 auth still works)."
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
return findings;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
// src/commands/update.ts
|
|
4178
|
+
import { spawn as spawn7 } from "child_process";
|
|
4179
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
4180
|
+
import { Command as Command20 } from "commander";
|
|
4181
|
+
import pc21 from "picocolors";
|
|
3270
4182
|
var PACKAGE_NAME = "botapp-cli";
|
|
3271
|
-
var updateCommand = new
|
|
4183
|
+
var updateCommand = new Command20("update").description("Update the `bot` CLI itself to the latest published version").option(
|
|
3272
4184
|
"--manager <pm>",
|
|
3273
4185
|
"Force a package manager: npm | pnpm | yarn | brew (default: auto-detect)"
|
|
3274
4186
|
).option("--dry-run", "Print the command that would run, but do not execute it").action(async (opts) => {
|
|
@@ -3276,12 +4188,12 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3276
4188
|
const manager = forced ?? detectPackageManager();
|
|
3277
4189
|
if (manager === "npx") {
|
|
3278
4190
|
console.log(
|
|
3279
|
-
|
|
4191
|
+
pc21.green(
|
|
3280
4192
|
"You are running `bot` via `npx -y botapp-cli@latest` \u2014 every invocation already fetches the latest version."
|
|
3281
4193
|
)
|
|
3282
4194
|
);
|
|
3283
4195
|
console.log(
|
|
3284
|
-
|
|
4196
|
+
pc21.dim(
|
|
3285
4197
|
"For a faster startup, install it globally instead:\n npm i -g botapp-cli@latest\n pnpm add -g botapp-cli@latest"
|
|
3286
4198
|
)
|
|
3287
4199
|
);
|
|
@@ -3289,40 +4201,40 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3289
4201
|
}
|
|
3290
4202
|
if (!manager) {
|
|
3291
4203
|
console.error(
|
|
3292
|
-
|
|
4204
|
+
pc21.red(
|
|
3293
4205
|
"Couldn't detect how `bot` was installed. Pick one of these manually:"
|
|
3294
4206
|
)
|
|
3295
4207
|
);
|
|
3296
|
-
console.log(
|
|
3297
|
-
console.log(
|
|
3298
|
-
console.log(
|
|
3299
|
-
console.log(
|
|
4208
|
+
console.log(pc21.cyan(" npm i -g botapp-cli@latest"));
|
|
4209
|
+
console.log(pc21.cyan(" pnpm add -g botapp-cli@latest"));
|
|
4210
|
+
console.log(pc21.cyan(" yarn global add botapp-cli@latest"));
|
|
4211
|
+
console.log(pc21.cyan(" brew upgrade botapp-cli"));
|
|
3300
4212
|
process.exitCode = 1;
|
|
3301
4213
|
return;
|
|
3302
4214
|
}
|
|
3303
4215
|
const { command, args } = updateCommandFor(manager);
|
|
3304
|
-
console.log(
|
|
4216
|
+
console.log(pc21.dim(`$ ${command} ${args.join(" ")}`));
|
|
3305
4217
|
if (opts.dryRun) return;
|
|
3306
|
-
const child =
|
|
3307
|
-
const code = await new Promise((
|
|
4218
|
+
const child = spawn7(command, args, { stdio: "inherit" });
|
|
4219
|
+
const code = await new Promise((resolve11) => {
|
|
3308
4220
|
child.once("error", (err) => {
|
|
3309
|
-
console.error(
|
|
3310
|
-
|
|
4221
|
+
console.error(pc21.red(`Failed to spawn ${command}: ${err.message}`));
|
|
4222
|
+
resolve11(127);
|
|
3311
4223
|
});
|
|
3312
|
-
child.once("close",
|
|
4224
|
+
child.once("close", resolve11);
|
|
3313
4225
|
});
|
|
3314
4226
|
if (code !== 0) {
|
|
3315
|
-
console.error(
|
|
4227
|
+
console.error(pc21.red(`Update failed (exit ${code}).`));
|
|
3316
4228
|
process.exitCode = code ?? 1;
|
|
3317
4229
|
return;
|
|
3318
4230
|
}
|
|
3319
|
-
console.log(
|
|
4231
|
+
console.log(pc21.green("botapp-cli updated. Run `bot --version` to confirm."));
|
|
3320
4232
|
});
|
|
3321
4233
|
function detectPackageManager() {
|
|
3322
4234
|
const argv = process.argv[1] ?? "";
|
|
3323
4235
|
let real = "";
|
|
3324
4236
|
try {
|
|
3325
|
-
real = argv ?
|
|
4237
|
+
real = argv ? realpathSync2(argv) : "";
|
|
3326
4238
|
} catch {
|
|
3327
4239
|
}
|
|
3328
4240
|
const candidates = [argv, real].filter(Boolean);
|
|
@@ -3350,9 +4262,755 @@ function updateCommandFor(manager) {
|
|
|
3350
4262
|
}
|
|
3351
4263
|
}
|
|
3352
4264
|
|
|
4265
|
+
// src/commands/simulate.ts
|
|
4266
|
+
import { Command as Command21 } from "commander";
|
|
4267
|
+
import { resolve as resolve8, join as join11 } from "path";
|
|
4268
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
4269
|
+
import { spawn as spawn8 } from "child_process";
|
|
4270
|
+
import pc22 from "picocolors";
|
|
4271
|
+
var simulateCommand = new Command21("simulate").description("Dev-tunnel a local app to a botapp server (per-user shadow)").argument("[path]", "Path to the app directory", ".").option("-s, --server <url>", "botapp server URL (default: active profile / $BOTAPP_SERVER)").option("-t, --token <token>", "auth token (default: active profile / $BOTAPP_TOKEN)").option("--entry <file>", "override the manifest entry path").option("--lifetime <minutes>", "dev-session token lifetime in minutes", "480").action(async (appPath, opts) => {
|
|
4272
|
+
const absPath = resolve8(appPath);
|
|
4273
|
+
if (!existsSync13(absPath)) {
|
|
4274
|
+
console.error(pc22.red(`Path not found: ${absPath}`));
|
|
4275
|
+
process.exitCode = 1;
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
const manifestPath = join11(absPath, "botapp.app.json");
|
|
4279
|
+
if (!existsSync13(manifestPath)) {
|
|
4280
|
+
console.error(pc22.red(`No botapp.app.json found in ${absPath}`));
|
|
4281
|
+
process.exitCode = 1;
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
4285
|
+
if (!manifest.name) {
|
|
4286
|
+
console.error(pc22.red('manifest missing "name"'));
|
|
4287
|
+
process.exitCode = 1;
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
const httpServer = resolveServerUrl(opts.server);
|
|
4291
|
+
const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
|
|
4292
|
+
const token = resolveToken(opts.token);
|
|
4293
|
+
if (!token) {
|
|
4294
|
+
console.error(pc22.red("not logged in: run `bot login` or pass --token"));
|
|
4295
|
+
process.exitCode = 1;
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
|
|
4299
|
+
console.log(pc22.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
|
|
4300
|
+
const devToken = await issueDevToken({
|
|
4301
|
+
serverUrl: httpServer,
|
|
4302
|
+
token,
|
|
4303
|
+
appName: manifest.name,
|
|
4304
|
+
lifetimeMs
|
|
4305
|
+
});
|
|
4306
|
+
console.log(pc22.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
|
|
4307
|
+
const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
|
|
4308
|
+
if (!existsSync13(entryPath)) {
|
|
4309
|
+
console.error(pc22.red(`entry not found: ${entryPath}`));
|
|
4310
|
+
console.error(pc22.dim(" run `pnpm build` (or your build script) first."));
|
|
4311
|
+
process.exitCode = 1;
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
console.log(pc22.dim(`spawning ${entryPath} ...`));
|
|
4315
|
+
console.log(pc22.dim(` BOTAPP_SERVER=${wsServer}`));
|
|
4316
|
+
console.log(pc22.dim(` BOTAPP_APP_NAME=${manifest.name}`));
|
|
4317
|
+
console.log(
|
|
4318
|
+
pc22.cyan(
|
|
4319
|
+
`
|
|
4320
|
+
When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
|
|
4321
|
+
`
|
|
4322
|
+
)
|
|
4323
|
+
);
|
|
4324
|
+
const child = spawn8("node", [entryPath], {
|
|
4325
|
+
cwd: absPath,
|
|
4326
|
+
env: {
|
|
4327
|
+
...process.env,
|
|
4328
|
+
BOTAPP_SERVER: wsServer,
|
|
4329
|
+
BOTAPP_APP_TOKEN: devToken,
|
|
4330
|
+
BOTAPP_APP_NAME: manifest.name,
|
|
4331
|
+
BOTAPP_DATA_DIR: join11(absPath, ".botapp-sim")
|
|
4332
|
+
},
|
|
4333
|
+
stdio: "inherit"
|
|
4334
|
+
});
|
|
4335
|
+
const stop = (signal) => {
|
|
4336
|
+
console.log(pc22.dim(`
|
|
4337
|
+
stopping (${signal})...`));
|
|
4338
|
+
if (!child.killed) child.kill(signal);
|
|
4339
|
+
};
|
|
4340
|
+
process.on("SIGINT", () => stop("SIGINT"));
|
|
4341
|
+
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
4342
|
+
child.on("exit", (code, sig) => {
|
|
4343
|
+
const reason = sig ? `signal ${sig}` : `exit ${code}`;
|
|
4344
|
+
console.log(pc22.dim(`child exited (${reason})`));
|
|
4345
|
+
process.exit(typeof code === "number" ? code : 0);
|
|
4346
|
+
});
|
|
4347
|
+
});
|
|
4348
|
+
async function issueDevToken(opts) {
|
|
4349
|
+
const res = await fetch(`${opts.serverUrl}/api/dev/token`, {
|
|
4350
|
+
method: "POST",
|
|
4351
|
+
headers: authHeaders(opts.token),
|
|
4352
|
+
body: JSON.stringify({ appName: opts.appName, lifetimeMs: opts.lifetimeMs })
|
|
4353
|
+
});
|
|
4354
|
+
if (!res.ok) {
|
|
4355
|
+
const text = await res.text().catch(() => "");
|
|
4356
|
+
throw new Error(`dev-token request failed (${res.status}): ${text}`);
|
|
4357
|
+
}
|
|
4358
|
+
const data = await res.json();
|
|
4359
|
+
if (!data.token) {
|
|
4360
|
+
throw new Error(`dev-token response missing token: ${JSON.stringify(data)}`);
|
|
4361
|
+
}
|
|
4362
|
+
return data.token;
|
|
4363
|
+
}
|
|
4364
|
+
function resolveEntry(appDir, entry) {
|
|
4365
|
+
const candidates = [
|
|
4366
|
+
entry,
|
|
4367
|
+
"dist/api.js",
|
|
4368
|
+
"dist/api/index.js",
|
|
4369
|
+
"dist/index.js",
|
|
4370
|
+
"api/index.ts",
|
|
4371
|
+
"api/index.js"
|
|
4372
|
+
].filter(Boolean);
|
|
4373
|
+
for (const c of candidates) {
|
|
4374
|
+
const full = resolve8(appDir, c);
|
|
4375
|
+
if (existsSync13(full)) return full;
|
|
4376
|
+
}
|
|
4377
|
+
return resolve8(appDir, candidates[0] ?? "dist/api.js");
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// src/commands/init.ts
|
|
4381
|
+
import { Command as Command22 } from "commander";
|
|
4382
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4383
|
+
import { resolve as resolve9, join as join12 } from "path";
|
|
4384
|
+
import pc23 from "picocolors";
|
|
4385
|
+
var initCommand = new Command22("init").description("Scaffold a new hosted-tier botapp app project").argument("<name>", "App name (kebab-case; used in URL paths and manifest)").option("-d, --dir <path>", "Output directory (default: ./<name>)").option("--headless", "No frontend \u2014 backend-only app (skip src/, tailwind, etc.)").option("--description <text>", "Short description for AI agents to read").option("-f, --force", "Overwrite existing directory contents").action(async (name, opts) => {
|
|
4386
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
4387
|
+
console.error(pc23.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
|
|
4388
|
+
console.error(pc23.dim(" e.g. my-app, todo-tracker, gomoku-2"));
|
|
4389
|
+
process.exitCode = 1;
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
const targetDir = resolve9(opts.dir ?? `./${name}`);
|
|
4393
|
+
if (existsSync14(targetDir) && !opts.force) {
|
|
4394
|
+
const stat = (() => {
|
|
4395
|
+
try {
|
|
4396
|
+
return __require("fs").readdirSync(targetDir);
|
|
4397
|
+
} catch {
|
|
4398
|
+
return [];
|
|
4399
|
+
}
|
|
4400
|
+
})();
|
|
4401
|
+
if (Array.isArray(stat) && stat.length > 0) {
|
|
4402
|
+
console.error(pc23.red(`Target directory exists and is not empty: ${targetDir}`));
|
|
4403
|
+
console.error(pc23.dim(" pass --force to overwrite, or pick a different --dir"));
|
|
4404
|
+
process.exitCode = 1;
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
const ctx = {
|
|
4409
|
+
name,
|
|
4410
|
+
description: opts.description ?? `${name} \u2014 a botapp app`,
|
|
4411
|
+
headless: !!opts.headless
|
|
4412
|
+
};
|
|
4413
|
+
mkdirSync6(targetDir, { recursive: true });
|
|
4414
|
+
const files = ctx.headless ? headlessFiles(ctx) : fullFiles(ctx);
|
|
4415
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
4416
|
+
const full = join12(targetDir, rel);
|
|
4417
|
+
mkdirSync6(dirname2(full), { recursive: true });
|
|
4418
|
+
writeFileSync4(full, content);
|
|
4419
|
+
}
|
|
4420
|
+
console.log(pc23.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc23.cyan(targetDir));
|
|
4421
|
+
console.log();
|
|
4422
|
+
console.log("Next steps:");
|
|
4423
|
+
console.log(pc23.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
|
|
4424
|
+
console.log(pc23.dim(" pnpm install # or npm install"));
|
|
4425
|
+
if (!ctx.headless) console.log(pc23.dim(" pnpm build # builds api/ + frontend (dist/)"));
|
|
4426
|
+
else console.log(pc23.dim(" pnpm build # builds api/ to dist/api.js"));
|
|
4427
|
+
console.log(pc23.dim(` bot login --server <your-botapp-server>`));
|
|
4428
|
+
console.log(pc23.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
|
|
4429
|
+
console.log();
|
|
4430
|
+
console.log(
|
|
4431
|
+
pc23.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
|
|
4432
|
+
);
|
|
4433
|
+
});
|
|
4434
|
+
function fullFiles(ctx) {
|
|
4435
|
+
return {
|
|
4436
|
+
"botapp.app.json": manifestJson(ctx),
|
|
4437
|
+
"package.json": packageJson(
|
|
4438
|
+
ctx,
|
|
4439
|
+
/*headless*/
|
|
4440
|
+
false
|
|
4441
|
+
),
|
|
4442
|
+
"tsconfig.json": tsconfigJson(false),
|
|
4443
|
+
"tsconfig.api.json": tsconfigApiJson(),
|
|
4444
|
+
"tsconfig.frontend.json": tsconfigFrontendJson(),
|
|
4445
|
+
"tsup.api.config.ts": tsupApiConfig(),
|
|
4446
|
+
"vite.config.ts": viteConfig(ctx),
|
|
4447
|
+
"index.html": indexHtml(ctx),
|
|
4448
|
+
"api/index.ts": apiEntryTs(ctx, false),
|
|
4449
|
+
"src/main.tsx": srcMainTsx(ctx),
|
|
4450
|
+
"src/App.tsx": srcAppTsx(ctx),
|
|
4451
|
+
"src/lib/api.ts": srcApiTs(),
|
|
4452
|
+
"contracts/types.ts": contractsTs(ctx),
|
|
4453
|
+
".gitignore": gitignore(),
|
|
4454
|
+
"README.md": readme(ctx, false)
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
function headlessFiles(ctx) {
|
|
4458
|
+
return {
|
|
4459
|
+
"botapp.app.json": manifestJson(ctx),
|
|
4460
|
+
"package.json": packageJson(
|
|
4461
|
+
ctx,
|
|
4462
|
+
/*headless*/
|
|
4463
|
+
true
|
|
4464
|
+
),
|
|
4465
|
+
"tsconfig.json": tsconfigJson(true),
|
|
4466
|
+
"tsup.api.config.ts": tsupApiConfig(),
|
|
4467
|
+
"api/index.ts": apiEntryTs(ctx, true),
|
|
4468
|
+
"contracts/types.ts": contractsTs(ctx),
|
|
4469
|
+
".gitignore": gitignore(),
|
|
4470
|
+
"README.md": readme(ctx, true)
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
function manifestJson(ctx) {
|
|
4474
|
+
const m = {
|
|
4475
|
+
name: ctx.name,
|
|
4476
|
+
version: "0.1.0",
|
|
4477
|
+
description: ctx.description,
|
|
4478
|
+
entry: "./dist/api.js",
|
|
4479
|
+
tier: "user",
|
|
4480
|
+
visibility: "private"
|
|
4481
|
+
};
|
|
4482
|
+
if (!ctx.headless) {
|
|
4483
|
+
m.hasFrontend = true;
|
|
4484
|
+
}
|
|
4485
|
+
return JSON.stringify(m, null, 2) + "\n";
|
|
4486
|
+
}
|
|
4487
|
+
function packageJson(ctx, headless) {
|
|
4488
|
+
const scripts = {
|
|
4489
|
+
build: headless ? "tsup --config tsup.api.config.ts" : "tsup --config tsup.api.config.ts && vite build",
|
|
4490
|
+
"build:api": "tsup --config tsup.api.config.ts",
|
|
4491
|
+
dev: headless ? "tsup --config tsup.api.config.ts --watch" : "tsup --config tsup.api.config.ts --watch & vite",
|
|
4492
|
+
typecheck: "tsc --noEmit"
|
|
4493
|
+
};
|
|
4494
|
+
const deps = {
|
|
4495
|
+
"botapp-sdk": "^0.1.0",
|
|
4496
|
+
ws: "^8.18.0"
|
|
4497
|
+
};
|
|
4498
|
+
const devDeps = {
|
|
4499
|
+
tsup: "^8.4.0",
|
|
4500
|
+
typescript: "^5.8.0",
|
|
4501
|
+
"@types/node": "^22.0.0",
|
|
4502
|
+
"@types/ws": "^8.5.0"
|
|
4503
|
+
};
|
|
4504
|
+
if (!headless) {
|
|
4505
|
+
Object.assign(deps, {
|
|
4506
|
+
react: "^19.0.0",
|
|
4507
|
+
"react-dom": "^19.0.0"
|
|
4508
|
+
});
|
|
4509
|
+
Object.assign(devDeps, {
|
|
4510
|
+
vite: "^7.0.0",
|
|
4511
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
4512
|
+
"@types/react": "^19.0.0",
|
|
4513
|
+
"@types/react-dom": "^19.0.0"
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
return JSON.stringify(
|
|
4517
|
+
{
|
|
4518
|
+
name: ctx.name,
|
|
4519
|
+
version: "0.1.0",
|
|
4520
|
+
description: ctx.description,
|
|
4521
|
+
type: "module",
|
|
4522
|
+
private: true,
|
|
4523
|
+
scripts,
|
|
4524
|
+
dependencies: deps,
|
|
4525
|
+
devDependencies: devDeps
|
|
4526
|
+
},
|
|
4527
|
+
null,
|
|
4528
|
+
2
|
|
4529
|
+
) + "\n";
|
|
4530
|
+
}
|
|
4531
|
+
function tsconfigJson(headless) {
|
|
4532
|
+
if (headless) {
|
|
4533
|
+
return JSON.stringify(
|
|
4534
|
+
{
|
|
4535
|
+
compilerOptions: {
|
|
4536
|
+
target: "ES2022",
|
|
4537
|
+
module: "ESNext",
|
|
4538
|
+
moduleResolution: "bundler",
|
|
4539
|
+
esModuleInterop: true,
|
|
4540
|
+
strict: true,
|
|
4541
|
+
skipLibCheck: true,
|
|
4542
|
+
noEmit: true
|
|
4543
|
+
},
|
|
4544
|
+
include: ["api/**/*.ts", "contracts/**/*.ts"]
|
|
4545
|
+
},
|
|
4546
|
+
null,
|
|
4547
|
+
2
|
|
4548
|
+
) + "\n";
|
|
4549
|
+
}
|
|
4550
|
+
return JSON.stringify(
|
|
4551
|
+
{
|
|
4552
|
+
files: [],
|
|
4553
|
+
references: [
|
|
4554
|
+
{ path: "./tsconfig.api.json" },
|
|
4555
|
+
{ path: "./tsconfig.frontend.json" }
|
|
4556
|
+
]
|
|
4557
|
+
},
|
|
4558
|
+
null,
|
|
4559
|
+
2
|
|
4560
|
+
) + "\n";
|
|
4561
|
+
}
|
|
4562
|
+
function tsconfigApiJson() {
|
|
4563
|
+
return JSON.stringify(
|
|
4564
|
+
{
|
|
4565
|
+
compilerOptions: {
|
|
4566
|
+
target: "ES2022",
|
|
4567
|
+
module: "ESNext",
|
|
4568
|
+
moduleResolution: "bundler",
|
|
4569
|
+
esModuleInterop: true,
|
|
4570
|
+
strict: true,
|
|
4571
|
+
skipLibCheck: true,
|
|
4572
|
+
noEmit: true
|
|
4573
|
+
},
|
|
4574
|
+
include: ["api/**/*.ts", "contracts/**/*.ts"]
|
|
4575
|
+
},
|
|
4576
|
+
null,
|
|
4577
|
+
2
|
|
4578
|
+
) + "\n";
|
|
4579
|
+
}
|
|
4580
|
+
function tsconfigFrontendJson() {
|
|
4581
|
+
return JSON.stringify(
|
|
4582
|
+
{
|
|
4583
|
+
compilerOptions: {
|
|
4584
|
+
target: "ES2022",
|
|
4585
|
+
module: "ESNext",
|
|
4586
|
+
moduleResolution: "bundler",
|
|
4587
|
+
esModuleInterop: true,
|
|
4588
|
+
strict: true,
|
|
4589
|
+
skipLibCheck: true,
|
|
4590
|
+
jsx: "react-jsx",
|
|
4591
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
4592
|
+
noEmit: true
|
|
4593
|
+
},
|
|
4594
|
+
include: ["src/**/*.ts", "src/**/*.tsx", "contracts/**/*.ts"]
|
|
4595
|
+
},
|
|
4596
|
+
null,
|
|
4597
|
+
2
|
|
4598
|
+
) + "\n";
|
|
4599
|
+
}
|
|
4600
|
+
function tsupApiConfig() {
|
|
4601
|
+
return `import { defineConfig } from 'tsup'
|
|
4602
|
+
|
|
4603
|
+
export default defineConfig({
|
|
4604
|
+
entry: { api: 'api/index.ts' },
|
|
4605
|
+
format: ['esm'],
|
|
4606
|
+
outDir: 'dist',
|
|
4607
|
+
clean: true,
|
|
4608
|
+
sourcemap: true,
|
|
4609
|
+
noExternal: [],
|
|
4610
|
+
})
|
|
4611
|
+
`;
|
|
4612
|
+
}
|
|
4613
|
+
function viteConfig(ctx) {
|
|
4614
|
+
return `import { defineConfig } from 'vite'
|
|
4615
|
+
import react from '@vitejs/plugin-react'
|
|
4616
|
+
|
|
4617
|
+
export default defineConfig({
|
|
4618
|
+
plugins: [react()],
|
|
4619
|
+
base: '/apps/${ctx.name}/',
|
|
4620
|
+
build: {
|
|
4621
|
+
outDir: 'dist/public',
|
|
4622
|
+
emptyOutDir: true,
|
|
4623
|
+
},
|
|
4624
|
+
})
|
|
4625
|
+
`;
|
|
4626
|
+
}
|
|
4627
|
+
function indexHtml(ctx) {
|
|
4628
|
+
return `<!doctype html>
|
|
4629
|
+
<html lang="en">
|
|
4630
|
+
<head>
|
|
4631
|
+
<meta charset="UTF-8" />
|
|
4632
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4633
|
+
<title>${escapeHtml(ctx.description)}</title>
|
|
4634
|
+
</head>
|
|
4635
|
+
<body>
|
|
4636
|
+
<div id="root"></div>
|
|
4637
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
4638
|
+
</body>
|
|
4639
|
+
</html>
|
|
4640
|
+
`;
|
|
4641
|
+
}
|
|
4642
|
+
function apiEntryTs(ctx, headless) {
|
|
4643
|
+
const widget = headless ? "" : `
|
|
4644
|
+
ctx.registerWidget({
|
|
4645
|
+
refresh: { intervalMs: 30_000 },
|
|
4646
|
+
render: async ({ state }) => {
|
|
4647
|
+
const count = (await state.get('count')) ?? 0
|
|
4648
|
+
return {
|
|
4649
|
+
html: \`<div class="card"><div class="label">${ctx.name.toUpperCase()}</div><div class="value">\${count}</div></div>\`,
|
|
4650
|
+
css: \`.card { padding: 20px; font-family: sans-serif; } .value { font-size: 32px; font-weight: 700; }\`,
|
|
4651
|
+
}
|
|
4652
|
+
},
|
|
4653
|
+
})
|
|
4654
|
+
|
|
4655
|
+
ctx.serveStatic('./dist/public')
|
|
4656
|
+
`;
|
|
4657
|
+
return `import { BotApp } from 'botapp-sdk'
|
|
4658
|
+
|
|
4659
|
+
const app = new BotApp({
|
|
4660
|
+
name: '${ctx.name}',
|
|
4661
|
+
version: '0.1.0',
|
|
4662
|
+
description: '${ctx.description.replace(/'/g, "\\'")}',
|
|
4663
|
+
async setup(ctx) {
|
|
4664
|
+
ctx.registerCommand('hello', {
|
|
4665
|
+
description: 'Greet the caller',
|
|
4666
|
+
params: {
|
|
4667
|
+
name: { type: 'string', required: false, default: 'world', description: 'Who to greet' },
|
|
4668
|
+
},
|
|
4669
|
+
handler: async ({ name }, cmdCtx) => {
|
|
4670
|
+
const count = ((await cmdCtx.state.get('count')) as number | null) ?? 0
|
|
4671
|
+
await cmdCtx.state.set('count', count + 1)
|
|
4672
|
+
ctx.invalidateWidget?.()
|
|
4673
|
+
return \`Hello, \${name}! (called \${count + 1}x by \${cmdCtx.agent.id})\`
|
|
4674
|
+
},
|
|
4675
|
+
})
|
|
4676
|
+
${widget} },
|
|
4677
|
+
})
|
|
4678
|
+
|
|
4679
|
+
await app.start()
|
|
4680
|
+
`;
|
|
4681
|
+
}
|
|
4682
|
+
function srcMainTsx(_ctx) {
|
|
4683
|
+
return `import { StrictMode } from 'react'
|
|
4684
|
+
import { createRoot } from 'react-dom/client'
|
|
4685
|
+
import { App } from './App'
|
|
4686
|
+
|
|
4687
|
+
createRoot(document.getElementById('root')!).render(
|
|
4688
|
+
<StrictMode>
|
|
4689
|
+
<App />
|
|
4690
|
+
</StrictMode>,
|
|
4691
|
+
)
|
|
4692
|
+
`;
|
|
4693
|
+
}
|
|
4694
|
+
function srcAppTsx(ctx) {
|
|
4695
|
+
return `import { useEffect, useState } from 'react'
|
|
4696
|
+
import { callCommand } from './lib/api'
|
|
4697
|
+
|
|
4698
|
+
export function App() {
|
|
4699
|
+
const [count, setCount] = useState<number | null>(null)
|
|
4700
|
+
const [error, setError] = useState<string | null>(null)
|
|
4701
|
+
useEffect(() => {
|
|
4702
|
+
callCommand('hello', { name: 'browser' })
|
|
4703
|
+
.then((res) => {
|
|
4704
|
+
const m = /\\(called (\\d+)x/.exec(String(res))
|
|
4705
|
+
if (m) setCount(Number(m[1]))
|
|
4706
|
+
})
|
|
4707
|
+
.catch((e: Error) => setError(e.message))
|
|
4708
|
+
}, [])
|
|
4709
|
+
return (
|
|
4710
|
+
<main style={{ fontFamily: 'sans-serif', padding: 24 }}>
|
|
4711
|
+
<h1>${ctx.name}</h1>
|
|
4712
|
+
{error ? <pre style={{ color: 'crimson' }}>{error}</pre> : <p>Calls so far: {count ?? '...'}</p>}
|
|
4713
|
+
</main>
|
|
4714
|
+
)
|
|
4715
|
+
}
|
|
4716
|
+
`;
|
|
4717
|
+
}
|
|
4718
|
+
function srcApiTs() {
|
|
4719
|
+
return `// Tiny client for calling app routes/commands from the browser.
|
|
4720
|
+
// Routes resolve as /apps/<name>/* on the platform; the platform forwards
|
|
4721
|
+
// each request to your app's WebSocket session.
|
|
4722
|
+
|
|
4723
|
+
const APP_BASE = ((): string => {
|
|
4724
|
+
const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
|
|
4725
|
+
return m ? m[0] : '/'
|
|
4726
|
+
})()
|
|
4727
|
+
|
|
4728
|
+
export async function callCommand(name: string, params: Record<string, unknown> = {}) {
|
|
4729
|
+
const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
|
|
4730
|
+
method: 'POST',
|
|
4731
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4732
|
+
body: JSON.stringify(params),
|
|
4733
|
+
})
|
|
4734
|
+
if (!r.ok) throw new Error(await r.text())
|
|
4735
|
+
return r.json()
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
export async function callAction(name: string, params: Record<string, unknown> = {}) {
|
|
4739
|
+
const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
|
|
4740
|
+
method: 'POST',
|
|
4741
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4742
|
+
body: JSON.stringify(params),
|
|
4743
|
+
})
|
|
4744
|
+
if (!r.ok) throw new Error(await r.text())
|
|
4745
|
+
return r.json()
|
|
4746
|
+
}
|
|
4747
|
+
`;
|
|
4748
|
+
}
|
|
4749
|
+
function contractsTs(ctx) {
|
|
4750
|
+
return `// Types shared between api/ (backend) and src/ (frontend).
|
|
4751
|
+
// Importing from one side picks up changes on the other immediately.
|
|
4752
|
+
|
|
4753
|
+
export interface ${pascal(ctx.name)}State {
|
|
4754
|
+
count: number
|
|
4755
|
+
}
|
|
4756
|
+
`;
|
|
4757
|
+
}
|
|
4758
|
+
function gitignore() {
|
|
4759
|
+
return `node_modules/
|
|
4760
|
+
dist/
|
|
4761
|
+
.botapp-sim/
|
|
4762
|
+
*.log
|
|
4763
|
+
.DS_Store
|
|
4764
|
+
.env*.local
|
|
4765
|
+
`;
|
|
4766
|
+
}
|
|
4767
|
+
function readme(ctx, headless) {
|
|
4768
|
+
const surfaces = headless ? "- Backend only (no frontend)" : "- React + Vite frontend (built to `dist/public/`)\n- Dashboard widget (declared in `api/index.ts`)";
|
|
4769
|
+
return `# ${ctx.name}
|
|
4770
|
+
|
|
4771
|
+
${ctx.description}
|
|
4772
|
+
|
|
4773
|
+
## Surfaces
|
|
4774
|
+
|
|
4775
|
+
${surfaces}
|
|
4776
|
+
- One agent-facing command: \`hello\`
|
|
4777
|
+
|
|
4778
|
+
## Develop
|
|
4779
|
+
|
|
4780
|
+
\`\`\`bash
|
|
4781
|
+
pnpm install
|
|
4782
|
+
pnpm build # api \u2192 dist/api.js${headless ? "" : ", frontend \u2192 dist/public/"}
|
|
4783
|
+
bot login --server <your-botapp-server>
|
|
4784
|
+
bot simulate # dev-tunnel; hot-reload as you build
|
|
4785
|
+
\`\`\`
|
|
4786
|
+
|
|
4787
|
+
The simulator opens a per-user shadow of this app on the server. Your
|
|
4788
|
+
real dashboard at the server's URL routes the app to *your* local
|
|
4789
|
+
process; nobody else sees it.
|
|
4790
|
+
|
|
4791
|
+
## Publish
|
|
4792
|
+
|
|
4793
|
+
\`\`\`bash
|
|
4794
|
+
bot publish # private, only you see it
|
|
4795
|
+
bot publish --public # requests admin review for public visibility
|
|
4796
|
+
\`\`\`
|
|
4797
|
+
`;
|
|
4798
|
+
}
|
|
4799
|
+
function escapeHtml(s) {
|
|
4800
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4801
|
+
}
|
|
4802
|
+
function pascal(s) {
|
|
4803
|
+
return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
4804
|
+
}
|
|
4805
|
+
function dirname2(p) {
|
|
4806
|
+
const i = p.lastIndexOf("/");
|
|
4807
|
+
return i < 0 ? "." : p.slice(0, i);
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
// src/commands/publish.ts
|
|
4811
|
+
import { Command as Command23 } from "commander";
|
|
4812
|
+
import { resolve as resolve10, join as join13, relative as relative2 } from "path";
|
|
4813
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
|
|
4814
|
+
import { createGzip } from "zlib";
|
|
4815
|
+
import { spawn as spawn9 } from "child_process";
|
|
4816
|
+
import pc24 from "picocolors";
|
|
4817
|
+
var publishCommand = new Command23("publish").description("Build, pack, and upload an app to a botapp server").argument("[path]", "App directory", ".").option("-s, --server <url>", "Server URL (default: active profile / $BOTAPP_SERVER)").option("-t, --token <token>", "Auth token (default: active profile / $BOTAPP_TOKEN)").option("--public", "Request public visibility (queues admin review). Default: private.").option("--no-build", "Skip running `pnpm build` / `npm run build`").option("--bundle-dir <dir>", "Directory to bundle into tar.gz (default: dist/public if exists, else dist)").action(async (appPath, opts) => {
|
|
4818
|
+
const absPath = resolve10(appPath);
|
|
4819
|
+
const manifestPath = join13(absPath, "botapp.app.json");
|
|
4820
|
+
if (!existsSync15(manifestPath)) {
|
|
4821
|
+
console.error(pc24.red(`No botapp.app.json found in ${absPath}`));
|
|
4822
|
+
process.exitCode = 1;
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
4826
|
+
if (!manifest.name) {
|
|
4827
|
+
console.error(pc24.red('manifest missing "name"'));
|
|
4828
|
+
process.exitCode = 1;
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
4831
|
+
const server = resolveServerUrl(opts.server);
|
|
4832
|
+
const token = resolveToken(opts.token);
|
|
4833
|
+
if (!token) {
|
|
4834
|
+
console.error(pc24.red("not logged in: run `bot login` or pass --token"));
|
|
4835
|
+
process.exitCode = 1;
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
if (opts.build !== false) {
|
|
4839
|
+
console.log(pc24.dim("building app..."));
|
|
4840
|
+
const ok = await runBuild(absPath);
|
|
4841
|
+
if (!ok) {
|
|
4842
|
+
console.error(pc24.red("build failed; aborting"));
|
|
4843
|
+
process.exitCode = 1;
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
|
|
4848
|
+
let bundleB64;
|
|
4849
|
+
if (bundleDir && existsSync15(bundleDir)) {
|
|
4850
|
+
console.log(pc24.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
|
|
4851
|
+
const bytes = await packDirToTarGz(bundleDir);
|
|
4852
|
+
bundleB64 = bytes.toString("base64");
|
|
4853
|
+
console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
|
|
4854
|
+
} else {
|
|
4855
|
+
console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
|
|
4856
|
+
}
|
|
4857
|
+
console.log(pc24.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
|
|
4858
|
+
const res = await fetch(`${server}/api/apps/upload`, {
|
|
4859
|
+
method: "POST",
|
|
4860
|
+
headers: authHeaders(token),
|
|
4861
|
+
body: JSON.stringify({
|
|
4862
|
+
manifest,
|
|
4863
|
+
bundleB64,
|
|
4864
|
+
visibility: opts.public ? "public" : "private"
|
|
4865
|
+
})
|
|
4866
|
+
});
|
|
4867
|
+
if (!res.ok) {
|
|
4868
|
+
const body = await res.text().catch(() => "");
|
|
4869
|
+
console.error(pc24.red(`upload failed (${res.status}): ${body}`));
|
|
4870
|
+
process.exitCode = 1;
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
const data = await res.json();
|
|
4874
|
+
if (!data.ok) {
|
|
4875
|
+
console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
|
|
4876
|
+
process.exitCode = 1;
|
|
4877
|
+
return;
|
|
4878
|
+
}
|
|
4879
|
+
console.log(pc24.green("\u2713"), data.message ?? "uploaded");
|
|
4880
|
+
if (data.install) {
|
|
4881
|
+
console.log(pc24.dim(` id: ${data.install.id}`));
|
|
4882
|
+
console.log(pc24.dim(` version: ${data.install.version}`));
|
|
4883
|
+
console.log(pc24.dim(` visibility: ${data.install.visibility}`));
|
|
4884
|
+
if (data.install.reviewStatus !== "none") {
|
|
4885
|
+
console.log(pc24.dim(` review: ${data.install.reviewStatus}`));
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
});
|
|
4889
|
+
function runBuild(cwd) {
|
|
4890
|
+
const pkgManager = existsSync15(join13(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
4891
|
+
const args = pkgManager === "pnpm" ? ["build"] : ["run", "build"];
|
|
4892
|
+
return new Promise((resolveP) => {
|
|
4893
|
+
const child = spawn9(pkgManager, args, { cwd, stdio: "inherit" });
|
|
4894
|
+
child.on("exit", (code) => resolveP(code === 0));
|
|
4895
|
+
child.on("error", () => resolveP(false));
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
function pickBundleDir(appDir) {
|
|
4899
|
+
const distPublic = join13(appDir, "dist", "public");
|
|
4900
|
+
if (existsSync15(distPublic)) return distPublic;
|
|
4901
|
+
const dist = join13(appDir, "dist");
|
|
4902
|
+
if (existsSync15(dist)) return dist;
|
|
4903
|
+
return null;
|
|
4904
|
+
}
|
|
4905
|
+
async function packDirToTarGz(rootDir) {
|
|
4906
|
+
const entries = [];
|
|
4907
|
+
walk(rootDir, "", entries);
|
|
4908
|
+
const blocks = [];
|
|
4909
|
+
for (const e of entries) {
|
|
4910
|
+
blocks.push(tarHeader(e.name, e.size));
|
|
4911
|
+
blocks.push(e.data);
|
|
4912
|
+
const pad = (512 - e.size % 512) % 512;
|
|
4913
|
+
if (pad) blocks.push(Buffer.alloc(pad));
|
|
4914
|
+
}
|
|
4915
|
+
blocks.push(Buffer.alloc(1024));
|
|
4916
|
+
const tar = Buffer.concat(blocks);
|
|
4917
|
+
return await gzip(tar);
|
|
4918
|
+
}
|
|
4919
|
+
function walk(root, prefix, out) {
|
|
4920
|
+
for (const entry of readdirSync2(root)) {
|
|
4921
|
+
const full = join13(root, entry);
|
|
4922
|
+
const st = statSync4(full);
|
|
4923
|
+
const rel = (prefix ? `${prefix}/` : "") + entry;
|
|
4924
|
+
if (st.isDirectory()) {
|
|
4925
|
+
walk(full, rel, out);
|
|
4926
|
+
} else if (st.isFile()) {
|
|
4927
|
+
const data = readFileSync10(full);
|
|
4928
|
+
out.push({ name: rel, size: data.length, data });
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
function tarHeader(name, size) {
|
|
4933
|
+
const h = Buffer.alloc(512);
|
|
4934
|
+
h.write(name.length > 100 ? name.slice(name.length - 100) : name, 0, 100);
|
|
4935
|
+
h.write("0000644", 100, 7);
|
|
4936
|
+
h.write("0000000", 108, 7);
|
|
4937
|
+
h.write("0000000", 116, 7);
|
|
4938
|
+
h.write(size.toString(8).padStart(11, "0"), 124, 11);
|
|
4939
|
+
h.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0"), 136, 11);
|
|
4940
|
+
h.write(" ", 148, 8);
|
|
4941
|
+
h.write("0", 156, 1);
|
|
4942
|
+
h.write("ustar ", 257, 8);
|
|
4943
|
+
let cksum = 0;
|
|
4944
|
+
for (const b of h) cksum += b;
|
|
4945
|
+
h.write(cksum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
|
|
4946
|
+
return h;
|
|
4947
|
+
}
|
|
4948
|
+
function gzip(input2) {
|
|
4949
|
+
return new Promise((resolveP, rejectP) => {
|
|
4950
|
+
const gz = createGzip();
|
|
4951
|
+
const chunks = [];
|
|
4952
|
+
gz.on("data", (c) => chunks.push(c));
|
|
4953
|
+
gz.on("end", () => resolveP(Buffer.concat(chunks)));
|
|
4954
|
+
gz.on("error", rejectP);
|
|
4955
|
+
gz.end(input2);
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
// src/commands/review.ts
|
|
4960
|
+
import { Command as Command24 } from "commander";
|
|
4961
|
+
import pc25 from "picocolors";
|
|
4962
|
+
var reviewCommand = new Command24("review").description("Admin: review queue for public-visibility uploads");
|
|
4963
|
+
reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
|
|
4964
|
+
const server = resolveServerUrl(opts.server);
|
|
4965
|
+
const token = resolveToken(opts.token);
|
|
4966
|
+
if (!token) return die("not logged in: run `bot login` or pass --token");
|
|
4967
|
+
const res = await fetch(`${server}/api/admin/review-queue`, { headers: authHeaders(token) });
|
|
4968
|
+
if (!res.ok) {
|
|
4969
|
+
const body = await res.text().catch(() => "");
|
|
4970
|
+
return die(`request failed (${res.status}): ${body}`);
|
|
4971
|
+
}
|
|
4972
|
+
const data = await res.json();
|
|
4973
|
+
if (!data.pending?.length) {
|
|
4974
|
+
console.log(pc25.dim("queue empty"));
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
for (const i of data.pending) {
|
|
4978
|
+
console.log(pc25.bold(i.id), pc25.cyan(i.appName), pc25.dim(`v${i.version}`));
|
|
4979
|
+
console.log(pc25.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
|
|
4980
|
+
console.log(pc25.dim(` uploaded: ${i.uploadedAt}`));
|
|
4981
|
+
const desc = i.manifest?.description;
|
|
4982
|
+
if (desc) console.log(pc25.dim(` description: ${desc}`));
|
|
4983
|
+
console.log();
|
|
4984
|
+
}
|
|
4985
|
+
});
|
|
4986
|
+
reviewCommand.command("approve <id>").description("Approve a pending public install").option("--notes <text>", "Optional reviewer notes").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (id, opts) => decide(id, "approve", opts));
|
|
4987
|
+
reviewCommand.command("reject <id>").description("Reject a pending public install").requiredOption("--notes <text>", "Reason for rejection (required)").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (id, opts) => decide(id, "reject", opts));
|
|
4988
|
+
async function decide(id, decision, opts) {
|
|
4989
|
+
const server = resolveServerUrl(opts.server);
|
|
4990
|
+
const token = resolveToken(opts.token);
|
|
4991
|
+
if (!token) return die("not logged in: run `bot login` or pass --token");
|
|
4992
|
+
const res = await fetch(`${server}/api/admin/review/${encodeURIComponent(id)}`, {
|
|
4993
|
+
method: "POST",
|
|
4994
|
+
headers: authHeaders(token),
|
|
4995
|
+
body: JSON.stringify({ decision, notes: opts.notes })
|
|
4996
|
+
});
|
|
4997
|
+
if (!res.ok) {
|
|
4998
|
+
const body = await res.text().catch(() => "");
|
|
4999
|
+
return die(`request failed (${res.status}): ${body}`);
|
|
5000
|
+
}
|
|
5001
|
+
const data = await res.json();
|
|
5002
|
+
if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
|
|
5003
|
+
console.log(pc25.green("\u2713"), `${decision}d`, data.install?.appName, pc25.dim(`(${data.install?.id})`));
|
|
5004
|
+
if (data.install?.reviewNotes) console.log(pc25.dim(` notes: ${data.install.reviewNotes}`));
|
|
5005
|
+
}
|
|
5006
|
+
function die(msg) {
|
|
5007
|
+
console.error(pc25.red(msg));
|
|
5008
|
+
process.exitCode = 1;
|
|
5009
|
+
}
|
|
5010
|
+
|
|
3353
5011
|
// src/index.ts
|
|
3354
|
-
var version = "0.2.
|
|
3355
|
-
var program = new
|
|
5012
|
+
var version = "0.2.7";
|
|
5013
|
+
var program = new Command25().name("bot").description("botapp CLI \u2014 operate apps from the command line").version(version).enablePositionalOptions(true).option("--json", "Output as JSON").option("-s, --server <url>", "Server URL override").option("-t, --token <token>", "Auth token override").option("-v, --verbose", "Verbose output");
|
|
3356
5014
|
program.addCommand(launchCommand);
|
|
3357
5015
|
program.addCommand(runCommand);
|
|
3358
5016
|
program.addCommand(appsCommand);
|
|
@@ -3363,10 +5021,15 @@ program.addCommand(loginCommand);
|
|
|
3363
5021
|
program.addCommand(agentCommand);
|
|
3364
5022
|
program.addCommand(pairingCommand);
|
|
3365
5023
|
program.addCommand(daemonCommand);
|
|
5024
|
+
program.addCommand(doctorCommand);
|
|
5025
|
+
program.addCommand(initCommand);
|
|
3366
5026
|
program.addCommand(installCommand);
|
|
3367
5027
|
program.addCommand(uninstallCommand);
|
|
3368
5028
|
program.addCommand(reloadCommand);
|
|
3369
5029
|
program.addCommand(configCommand);
|
|
5030
|
+
program.addCommand(simulateCommand);
|
|
5031
|
+
program.addCommand(publishCommand);
|
|
5032
|
+
program.addCommand(reviewCommand);
|
|
3370
5033
|
program.addCommand(serverCommand);
|
|
3371
5034
|
program.addCommand(updateCommand);
|
|
3372
5035
|
program.addCommand(devCommand, { hidden: true });
|
|
@@ -3410,29 +5073,29 @@ To discover what params a command accepts:
|
|
|
3410
5073
|
program.on("command:*", (operands) => {
|
|
3411
5074
|
const first = operands[0];
|
|
3412
5075
|
const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
|
|
3413
|
-
const topLevelHint = `Run ${
|
|
5076
|
+
const topLevelHint = `Run ${pc26.cyan("bot --help")} for the list of top-level commands.`;
|
|
3414
5077
|
const argv = process.argv.slice(2);
|
|
3415
5078
|
const firstIdx = argv.indexOf(first);
|
|
3416
5079
|
const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
|
|
3417
5080
|
if (tail.length > 0) {
|
|
3418
5081
|
const suggested = `bot run ${first} ${tail.join(" ")}`;
|
|
3419
5082
|
console.error(
|
|
3420
|
-
|
|
5083
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
3421
5084
|
|
|
3422
|
-
App commands go through ${
|
|
5085
|
+
App commands go through ${pc26.bold("bot run")}. Did you mean:
|
|
3423
5086
|
|
|
3424
|
-
${
|
|
5087
|
+
${pc26.cyan(suggested)}
|
|
3425
5088
|
|
|
3426
5089
|
${topLevelHint}`
|
|
3427
5090
|
);
|
|
3428
5091
|
process.exit(1);
|
|
3429
5092
|
}
|
|
3430
5093
|
console.error(
|
|
3431
|
-
|
|
5094
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
3432
5095
|
|
|
3433
5096
|
If '${first}' is an app name, invoke one of its commands with:
|
|
3434
|
-
${
|
|
3435
|
-
${
|
|
5097
|
+
${pc26.cyan(`bot run ${first} <command> [--key value ...]`)}
|
|
5098
|
+
${pc26.cyan("bot apps --json")} ${pc26.dim("(to see what commands exist)")}
|
|
3436
5099
|
|
|
3437
5100
|
Top-level commands: ${known.join(", ")}
|
|
3438
5101
|
${topLevelHint}`
|