botapp-cli 0.2.3 → 0.2.5

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/index.js CHANGED
@@ -1,6 +1,13 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/index.ts
2
- import { Command as Command20 } from "commander";
3
- import pc21 from "picocolors";
9
+ import { Command as Command24 } from "commander";
10
+ import pc25 from "picocolors";
4
11
 
5
12
  // src/commands/server.ts
6
13
  import { Command } from "commander";
@@ -151,11 +158,11 @@ function findServerEntry() {
151
158
  }
152
159
 
153
160
  // src/commands/daemon.ts
154
- import { spawn as spawn2 } from "child_process";
155
- import { randomUUID } from "crypto";
156
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
157
- import { homedir as homedir3 } from "os";
158
- import { join as join3, resolve as resolve3 } from "path";
161
+ import { spawn as spawn3 } from "child_process";
162
+ import { randomUUID as randomUUID2 } from "crypto";
163
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
164
+ import { homedir as homedir5 } from "os";
165
+ import { join as join5, resolve as resolve4 } from "path";
159
166
  import { createInterface as createInterface2 } from "readline";
160
167
  import { Command as Command3 } from "commander";
161
168
  import { WebSocket } from "ws";
@@ -483,7 +490,557 @@ async function daemonSelfRequest(server, token, path, opts) {
483
490
  return data;
484
491
  }
485
492
 
493
+ // src/rpc/registry.ts
494
+ var RpcRegistry = class {
495
+ handlers = /* @__PURE__ */ new Map();
496
+ register(op, handler) {
497
+ if (this.handlers.has(op)) {
498
+ throw new Error(`RPC op already registered: ${op}`);
499
+ }
500
+ this.handlers.set(op, handler);
501
+ }
502
+ get(op) {
503
+ return this.handlers.get(op);
504
+ }
505
+ has(op) {
506
+ return this.handlers.has(op);
507
+ }
508
+ };
509
+ var InternalRpcContext = class {
510
+ constructor(appName, rpcId, ws) {
511
+ this.appName = appName;
512
+ this.rpcId = rpcId;
513
+ this.ws = ws;
514
+ }
515
+ appName;
516
+ rpcId;
517
+ ws;
518
+ cancelled = false;
519
+ inputCallback = null;
520
+ cancelCallback = null;
521
+ pushChunk(payload) {
522
+ sendJson(this.ws, {
523
+ type: "daemon_rpc_stream",
524
+ rpcId: this.rpcId,
525
+ payload
526
+ });
527
+ }
528
+ isCancelled() {
529
+ return this.cancelled;
530
+ }
531
+ onInput(callback) {
532
+ this.inputCallback = callback;
533
+ }
534
+ onCancel(callback) {
535
+ this.cancelCallback = callback;
536
+ }
537
+ cancel() {
538
+ if (this.cancelled) return;
539
+ this.cancelled = true;
540
+ const cb = this.cancelCallback;
541
+ this.cancelCallback = null;
542
+ if (cb) {
543
+ try {
544
+ cb();
545
+ } catch {
546
+ }
547
+ }
548
+ }
549
+ receiveInput(payload) {
550
+ this.inputCallback?.(payload);
551
+ }
552
+ };
553
+ var RpcDispatcher = class {
554
+ constructor(registry, ws) {
555
+ this.registry = registry;
556
+ this.ws = ws;
557
+ }
558
+ registry;
559
+ ws;
560
+ inflight = /* @__PURE__ */ new Map();
561
+ /** Handle a `daemon_rpc_request` frame. */
562
+ async dispatchRequest(frame) {
563
+ const handler = this.registry.get(frame.op);
564
+ if (!handler) {
565
+ this.sendResponse(frame.rpcId, {
566
+ ok: false,
567
+ error: `unknown daemon RPC op: ${frame.op}`
568
+ });
569
+ return;
570
+ }
571
+ const ctx = new InternalRpcContext(
572
+ frame.appName ?? "unknown",
573
+ frame.rpcId,
574
+ this.ws
575
+ );
576
+ this.inflight.set(frame.rpcId, { ctx });
577
+ try {
578
+ const result = await handler(frame.params ?? {}, ctx);
579
+ if (this.inflight.has(frame.rpcId)) {
580
+ this.sendResponse(frame.rpcId, { ok: true, result });
581
+ }
582
+ } catch (e) {
583
+ if (this.inflight.has(frame.rpcId)) {
584
+ this.sendResponse(frame.rpcId, {
585
+ ok: false,
586
+ error: e?.message ?? String(e)
587
+ });
588
+ }
589
+ } finally {
590
+ this.inflight.delete(frame.rpcId);
591
+ }
592
+ }
593
+ /** Handle a `daemon_rpc_input` frame. */
594
+ dispatchInput(frame) {
595
+ const call = this.inflight.get(frame.rpcId);
596
+ if (!call) return;
597
+ call.ctx.receiveInput(frame.payload);
598
+ }
599
+ /** Handle a `daemon_rpc_cancel` frame. */
600
+ dispatchCancel(frame) {
601
+ const call = this.inflight.get(frame.rpcId);
602
+ if (!call) return;
603
+ call.ctx.cancel();
604
+ }
605
+ /** Cancel everything (called on socket close). */
606
+ shutdown() {
607
+ for (const [, call] of this.inflight) {
608
+ call.ctx.cancel();
609
+ }
610
+ this.inflight.clear();
611
+ }
612
+ sendResponse(rpcId, body) {
613
+ sendJson(this.ws, { type: "daemon_rpc_response", rpcId, ...body });
614
+ }
615
+ };
616
+ function sendJson(ws, frame) {
617
+ if (ws.readyState !== ws.OPEN) return;
618
+ ws.send(JSON.stringify(frame));
619
+ }
620
+
621
+ // src/rpc/file-handlers.ts
622
+ import { Buffer as Buffer2 } from "buffer";
623
+ import {
624
+ promises as fsp,
625
+ realpathSync,
626
+ statSync
627
+ } from "fs";
628
+ import { homedir as homedir3 } from "os";
629
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
630
+ function registerFileHandlers(registry) {
631
+ registry.register("file.tree", fileTree);
632
+ registry.register("file.read", fileRead);
633
+ registry.register("file.write", fileWrite);
634
+ registry.register("file.stat", fileStat);
635
+ registry.register("file.mkdir", fileMkdir);
636
+ registry.register("file.delete", fileDelete);
637
+ registry.register("file.rename", fileRename);
638
+ registry.register("fs.browse", fsBrowse);
639
+ registry.register("fs.home", fsHome);
640
+ registry.register("fs.mkdir", fsMkdir);
641
+ }
642
+ function expandHome(p) {
643
+ if (p === "~") return homedir3();
644
+ if (p.startsWith("~/")) return join3(homedir3(), p.slice(2));
645
+ return p;
646
+ }
647
+ function resolveRoot(root) {
648
+ if (typeof root !== "string" || !root.trim()) {
649
+ throw new Error("file.*: `root` is required");
650
+ }
651
+ const expanded = expandHome(root);
652
+ if (!isAbsolute(expanded)) {
653
+ throw new Error(`file.*: \`root\` must be absolute (got "${root}")`);
654
+ }
655
+ try {
656
+ return realpathSync(expanded);
657
+ } catch {
658
+ return resolve3(expanded);
659
+ }
660
+ }
661
+ function jailedPath(root, sub) {
662
+ const canonicalRoot = resolveRoot(root);
663
+ const candidate = sub == null || sub === "" ? canonicalRoot : isAbsolute(sub) ? sub : resolve3(canonicalRoot, sub);
664
+ let realBase = candidate;
665
+ let tail = "";
666
+ while (true) {
667
+ try {
668
+ realBase = realpathSync(realBase);
669
+ break;
670
+ } catch {
671
+ const parent = dirname(realBase);
672
+ if (parent === realBase) {
673
+ realBase = candidate;
674
+ tail = "";
675
+ break;
676
+ }
677
+ tail = tail ? join3(realBase.slice(parent.length + 1), tail) : realBase.slice(parent.length + 1);
678
+ realBase = parent;
679
+ }
680
+ }
681
+ const final = tail ? join3(realBase, tail) : realBase;
682
+ const rel = relative(canonicalRoot, final);
683
+ if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
684
+ throw new Error(`file.*: path "${sub ?? ""}" escapes workspace root "${root}"`);
685
+ }
686
+ return final;
687
+ }
688
+ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
689
+ "node_modules",
690
+ ".git",
691
+ ".next",
692
+ ".turbo",
693
+ "dist",
694
+ "build",
695
+ ".venv",
696
+ "__pycache__",
697
+ ".DS_Store"
698
+ ]);
699
+ async function fileTree(params) {
700
+ const root = resolveRoot(params.root);
701
+ const start = jailedPath(params.root, params.path);
702
+ const depth = Math.max(0, params.depth ?? 1);
703
+ const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...params.ignore ?? []]);
704
+ const out = [];
705
+ async function walk2(absDir, level) {
706
+ let entries;
707
+ try {
708
+ entries = await fsp.readdir(absDir, {
709
+ withFileTypes: true,
710
+ encoding: "utf8"
711
+ });
712
+ } catch (e) {
713
+ if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return;
714
+ throw e;
715
+ }
716
+ entries.sort((a, b) => {
717
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
718
+ return a.name.localeCompare(b.name);
719
+ });
720
+ for (const entry of entries) {
721
+ if (ignore.has(entry.name)) continue;
722
+ const abs = join3(absDir, entry.name);
723
+ const rel = relative(root, abs);
724
+ let stat = null;
725
+ try {
726
+ stat = await fsp.stat(abs);
727
+ } catch {
728
+ continue;
729
+ }
730
+ const node = {
731
+ name: entry.name,
732
+ path: rel,
733
+ isDir: entry.isDirectory(),
734
+ size: entry.isFile() ? stat.size : void 0,
735
+ mtimeMs: stat.mtimeMs
736
+ };
737
+ out.push(node);
738
+ if (entry.isDirectory() && level < depth) {
739
+ await walk2(abs, level + 1);
740
+ }
741
+ }
742
+ }
743
+ await walk2(start, 1);
744
+ return out;
745
+ }
746
+ async function fileRead(params) {
747
+ const abs = jailedPath(params.root, params.path);
748
+ const maxBytes = params.maxBytes ?? 5 * 1024 * 1024;
749
+ const stat = await fsp.stat(abs);
750
+ if (!stat.isFile()) {
751
+ throw new Error(`file.read: not a file: ${params.path}`);
752
+ }
753
+ if (stat.size > maxBytes) {
754
+ throw new Error(
755
+ `file.read: file too large (${stat.size} bytes > ${maxBytes}). Pass maxBytes to override or chunk the read.`
756
+ );
757
+ }
758
+ const buf = await fsp.readFile(abs);
759
+ const encoding = params.encoding ?? "utf8";
760
+ return {
761
+ path: params.path,
762
+ content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
763
+ encoding,
764
+ size: stat.size,
765
+ mtimeMs: stat.mtimeMs
766
+ };
767
+ }
768
+ async function fileWrite(params) {
769
+ const abs = jailedPath(params.root, params.path);
770
+ if (params.createDirs !== false) {
771
+ await fsp.mkdir(dirname(abs), { recursive: true });
772
+ }
773
+ const buf = (params.encoding ?? "utf8") === "base64" ? Buffer2.from(params.content, "base64") : Buffer2.from(params.content, "utf8");
774
+ await fsp.writeFile(abs, buf);
775
+ const stat = await fsp.stat(abs);
776
+ return { path: params.path, size: stat.size, mtimeMs: stat.mtimeMs };
777
+ }
778
+ async function fileStat(params) {
779
+ let abs;
780
+ try {
781
+ abs = params.path == null || params.path === "" ? expandHome(params.root) : jailedPath(params.root, params.path);
782
+ } catch (e) {
783
+ if (/escapes workspace root/.test(e?.message ?? "")) throw e;
784
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
785
+ }
786
+ try {
787
+ const stat = statSync(abs);
788
+ return {
789
+ exists: true,
790
+ isFile: stat.isFile(),
791
+ isDir: stat.isDirectory(),
792
+ size: stat.size,
793
+ mtimeMs: stat.mtimeMs
794
+ };
795
+ } catch {
796
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
797
+ }
798
+ }
799
+ async function fileMkdir(params) {
800
+ const abs = jailedPath(params.root, params.path);
801
+ await fsp.mkdir(abs, { recursive: true });
802
+ return { path: params.path };
803
+ }
804
+ async function fileDelete(params) {
805
+ const abs = jailedPath(params.root, params.path);
806
+ await fsp.rm(abs, { recursive: !!params.recursive, force: false });
807
+ return { path: params.path };
808
+ }
809
+ async function fileRename(params) {
810
+ const fromAbs = jailedPath(params.root, params.from);
811
+ const toAbs = jailedPath(params.root, params.to);
812
+ await fsp.rename(fromAbs, toAbs);
813
+ return { from: params.from, to: params.to };
814
+ }
815
+ async function fsBrowse(params) {
816
+ const expanded = expandHome(params.path ?? "~");
817
+ if (!isAbsolute(expanded)) {
818
+ throw new Error(`fs.browse: path must be absolute (got "${params.path}")`);
819
+ }
820
+ let resolved;
821
+ try {
822
+ resolved = realpathSync(expanded);
823
+ } catch {
824
+ resolved = resolve3(expanded);
825
+ }
826
+ let raw;
827
+ try {
828
+ raw = await fsp.readdir(resolved, {
829
+ withFileTypes: true,
830
+ encoding: "utf8"
831
+ });
832
+ } catch (e) {
833
+ throw new Error(`fs.browse: cannot read ${resolved}: ${e?.code ?? e?.message ?? e}`);
834
+ }
835
+ const entries = [];
836
+ for (const entry of raw) {
837
+ if (!params.showHidden && entry.name.startsWith(".")) continue;
838
+ const isDir = entry.isDirectory();
839
+ if (!isDir && !params.includeFiles) continue;
840
+ entries.push({
841
+ name: entry.name,
842
+ path: join3(resolved, entry.name),
843
+ isDir
844
+ });
845
+ }
846
+ entries.sort((a, b) => {
847
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
848
+ return a.name.localeCompare(b.name);
849
+ });
850
+ const parent = dirname(resolved);
851
+ return {
852
+ path: resolved,
853
+ parent: parent === resolved ? null : parent,
854
+ entries
855
+ };
856
+ }
857
+ async function fsHome() {
858
+ return { home: homedir3(), cwd: process.cwd() };
859
+ }
860
+ async function fsMkdir(params) {
861
+ const expanded = expandHome(params.path ?? "");
862
+ if (!expanded || !isAbsolute(expanded)) {
863
+ throw new Error(`fs.mkdir: path must be absolute (got "${params.path}")`);
864
+ }
865
+ await fsp.mkdir(expanded, { recursive: true });
866
+ return { path: expanded };
867
+ }
868
+
869
+ // src/rpc/pty-handlers.ts
870
+ import { spawn as spawn2 } from "child_process";
871
+ import { randomUUID } from "crypto";
872
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
873
+ import { homedir as homedir4, platform } from "os";
874
+ import { join as join4 } from "path";
875
+ import { isAbsolute as isAbsolute2 } from "path";
876
+ var handles = /* @__PURE__ */ new Map();
877
+ function registerPtyHandlers(registry) {
878
+ registry.register("pty.spawn", ptySpawn);
879
+ registry.register("pty.write", ptyWrite);
880
+ registry.register("pty.resize", ptyResize);
881
+ registry.register("pty.kill", ptyKill);
882
+ }
883
+ async function ptySpawn(params, ctx) {
884
+ if (!params.cwd || typeof params.cwd !== "string" || !isAbsolute2(params.cwd)) {
885
+ throw new Error("pty.spawn: `cwd` is required and must be absolute");
886
+ }
887
+ let cwdStat;
888
+ try {
889
+ cwdStat = statSync2(params.cwd);
890
+ } catch {
891
+ throw new Error(`pty.spawn: cwd does not exist: ${params.cwd}`);
892
+ }
893
+ if (!cwdStat.isDirectory()) {
894
+ throw new Error(`pty.spawn: cwd is not a directory: ${params.cwd}`);
895
+ }
896
+ const command = params.command || defaultShell();
897
+ if (!existsSync4(command)) {
898
+ throw new Error(
899
+ `pty.spawn: shell binary not found: ${command} (set $SHELL in the daemon's environment, or pass an explicit \`command\`)`
900
+ );
901
+ }
902
+ const args = params.args ?? defaultShellArgs();
903
+ const env = { TERM: "xterm-256color" };
904
+ for (const [k, v] of Object.entries(process.env)) {
905
+ if (typeof v === "string") env[k] = v;
906
+ }
907
+ for (const [k, v] of Object.entries(params.env ?? {})) {
908
+ env[k] = v;
909
+ }
910
+ const cols = params.cols ?? 80;
911
+ const rows = params.rows ?? 24;
912
+ const handle = await openPty({ command, args, cwd: params.cwd, env, cols, rows, ctx });
913
+ handles.set(handle.handle.ptyId, handle.handle);
914
+ ctx.onInput((payload) => {
915
+ if (typeof payload === "string") handle.handle.write(payload);
916
+ else if (payload && typeof payload.data === "string") {
917
+ handle.handle.write(payload.data);
918
+ }
919
+ });
920
+ ctx.onCancel(() => {
921
+ handle.handle.kill("SIGTERM");
922
+ setTimeout(() => {
923
+ try {
924
+ handle.handle.kill("SIGKILL");
925
+ } catch {
926
+ }
927
+ }, 5e3).unref();
928
+ });
929
+ ctx.pushChunk({ kind: "mode", mode: handle.mode, ptyId: handle.handle.ptyId });
930
+ const exitCode = await handle.exited;
931
+ handles.delete(handle.handle.ptyId);
932
+ return { ptyId: handle.handle.ptyId, exitCode, mode: handle.mode };
933
+ }
934
+ async function ptyWrite(params) {
935
+ const handle = handles.get(params.ptyId);
936
+ if (!handle) throw new Error(`pty.write: unknown ptyId ${params.ptyId}`);
937
+ handle.write(params.data);
938
+ return { ok: true };
939
+ }
940
+ async function ptyResize(params) {
941
+ const handle = handles.get(params.ptyId);
942
+ if (!handle) throw new Error(`pty.resize: unknown ptyId ${params.ptyId}`);
943
+ handle.resize(params.cols, params.rows);
944
+ return { ok: true };
945
+ }
946
+ async function ptyKill(params) {
947
+ const handle = handles.get(params.ptyId);
948
+ if (!handle) throw new Error(`pty.kill: unknown ptyId ${params.ptyId}`);
949
+ handle.kill(params.signal ?? "SIGTERM");
950
+ return { ok: true };
951
+ }
952
+ async function openPty(opts) {
953
+ const nodePty = await loadNodePty();
954
+ if (nodePty) {
955
+ const proc2 = nodePty.spawn(opts.command, opts.args, {
956
+ name: "xterm-256color",
957
+ cols: opts.cols,
958
+ rows: opts.rows,
959
+ cwd: opts.cwd,
960
+ env: opts.env
961
+ });
962
+ const ptyId2 = randomUUID();
963
+ proc2.onData((data) => {
964
+ opts.ctx.pushChunk({ kind: "data", data });
965
+ });
966
+ const exited2 = new Promise((resolve11) => {
967
+ proc2.onExit(({ exitCode }) => {
968
+ resolve11(exitCode);
969
+ });
970
+ });
971
+ return {
972
+ mode: "pty",
973
+ exited: exited2,
974
+ handle: {
975
+ ptyId: ptyId2,
976
+ resize: (cols, rows) => proc2.resize(cols, rows),
977
+ write: (data) => proc2.write(data),
978
+ kill: (signal) => proc2.kill(signal)
979
+ }
980
+ };
981
+ }
982
+ const proc = spawn2(
983
+ opts.command,
984
+ opts.args,
985
+ {
986
+ cwd: opts.cwd,
987
+ env: opts.env,
988
+ stdio: ["pipe", "pipe", "pipe"]
989
+ }
990
+ );
991
+ const ptyId = randomUUID();
992
+ proc.stdout.on("data", (chunk) => {
993
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
994
+ });
995
+ proc.stderr.on("data", (chunk) => {
996
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
997
+ });
998
+ const exited = new Promise((resolve11) => {
999
+ proc.on("close", (code) => resolve11(code));
1000
+ });
1001
+ return {
1002
+ mode: "pipe",
1003
+ exited,
1004
+ handle: {
1005
+ ptyId,
1006
+ resize: () => {
1007
+ },
1008
+ write: (data) => {
1009
+ proc.stdin.write(data);
1010
+ },
1011
+ kill: (signal) => {
1012
+ proc.kill(signal ?? "SIGTERM");
1013
+ }
1014
+ }
1015
+ };
1016
+ }
1017
+ var nodePtyCache = void 0;
1018
+ async function loadNodePty() {
1019
+ if (nodePtyCache !== void 0) return nodePtyCache;
1020
+ try {
1021
+ const moduleName = "node-pty";
1022
+ nodePtyCache = await import(moduleName);
1023
+ return nodePtyCache;
1024
+ } catch {
1025
+ nodePtyCache = null;
1026
+ return null;
1027
+ }
1028
+ }
1029
+ function defaultShell() {
1030
+ if (platform() === "win32") {
1031
+ return process.env.COMSPEC || "cmd.exe";
1032
+ }
1033
+ return process.env.SHELL || "/bin/bash";
1034
+ }
1035
+ function defaultShellArgs() {
1036
+ if (platform() === "win32") return [];
1037
+ return ["-l", "-i"];
1038
+ }
1039
+
486
1040
  // src/commands/daemon.ts
1041
+ var rpcRegistry = new RpcRegistry();
1042
+ registerFileHandlers(rpcRegistry);
1043
+ registerPtyHandlers(rpcRegistry);
487
1044
  var daemonCommand = new Command3("daemon").description("Manage and run the local botapp daemon");
488
1045
  daemonCommand.command("run").description(
489
1046
  "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."
@@ -528,8 +1085,8 @@ function pickProfilesToRun(alias, server) {
528
1085
  return loadDaemonProfiles();
529
1086
  }
530
1087
  daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
531
- const pidFile = join3(homedir3(), ".botapp", "daemon.pid");
532
- if (!existsSync4(pidFile)) {
1088
+ const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
1089
+ if (!existsSync5(pidFile)) {
533
1090
  console.log(pc3.yellow("No background daemon PID file found."));
534
1091
  return;
535
1092
  }
@@ -657,6 +1214,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
657
1214
  return new Promise((resolveRun) => {
658
1215
  const ws = new WebSocket(wsUrl);
659
1216
  setActiveWs(ws);
1217
+ const rpcDispatcher = new RpcDispatcher(rpcRegistry, ws);
660
1218
  let opened = false;
661
1219
  let superseded = false;
662
1220
  let settled = false;
@@ -665,6 +1223,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
665
1223
  if (settled) return;
666
1224
  settled = true;
667
1225
  if (ping) clearInterval(ping);
1226
+ rpcDispatcher.shutdown();
668
1227
  resolveRun({ opened, superseded });
669
1228
  }
670
1229
  ws.on("open", () => {
@@ -676,7 +1235,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
676
1235
  }, 3e4);
677
1236
  });
678
1237
  ws.on("message", (raw) => {
679
- void handleFrame(ws, raw.toString());
1238
+ void handleFrame(ws, raw.toString(), rpcDispatcher);
680
1239
  });
681
1240
  ws.on("close", (code, reason) => {
682
1241
  if (code === 4e3) superseded = true;
@@ -692,8 +1251,20 @@ function runDaemonSocket(wsUrl, setActiveWs) {
692
1251
  });
693
1252
  });
694
1253
  }
695
- async function handleFrame(ws, raw) {
1254
+ async function handleFrame(ws, raw, rpcDispatcher) {
696
1255
  const frame = JSON.parse(raw);
1256
+ if (frame.type === "daemon_rpc_request") {
1257
+ void rpcDispatcher.dispatchRequest(frame);
1258
+ return;
1259
+ }
1260
+ if (frame.type === "daemon_rpc_input") {
1261
+ rpcDispatcher.dispatchInput(frame);
1262
+ return;
1263
+ }
1264
+ if (frame.type === "daemon_rpc_cancel") {
1265
+ rpcDispatcher.dispatchCancel(frame);
1266
+ return;
1267
+ }
697
1268
  if (frame.type === "daemon_job") {
698
1269
  const job = frame.job;
699
1270
  console.log(pc3.blue(`Running ${job.agent.name} job ${job.id}`));
@@ -749,7 +1320,7 @@ async function runAgentJob(job, update) {
749
1320
  return runAcpAgent(job);
750
1321
  }
751
1322
  async function runShellAgent(job) {
752
- const child = spawn2(job.agent.command, [...job.agent.args, job.query], {
1323
+ const child = spawn3(job.agent.command, [...job.agent.args, job.query], {
753
1324
  cwd: job.agent.cwd ?? process.cwd(),
754
1325
  env: { ...process.env, ...job.agent.env ?? {} }
755
1326
  });
@@ -761,7 +1332,7 @@ async function runShellAgent(job) {
761
1332
  child.stderr.on("data", (chunk) => {
762
1333
  stderr += chunk.toString();
763
1334
  });
764
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1335
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
765
1336
  if (code !== 0) {
766
1337
  throw new Error(stderr.trim() || `Agent exited with code ${code}`);
767
1338
  }
@@ -789,7 +1360,7 @@ async function runCodexAgent(job, update) {
789
1360
  args.push("--dangerously-bypass-approvals-and-sandbox");
790
1361
  }
791
1362
  args.push(job.query);
792
- const child = spawn2(job.agent.command, args, {
1363
+ const child = spawn3(job.agent.command, args, {
793
1364
  cwd: job.agent.cwd ?? process.cwd(),
794
1365
  env: { ...process.env, ...job.agent.env ?? {} },
795
1366
  stdio: ["ignore", "pipe", "pipe"]
@@ -839,7 +1410,7 @@ async function runCodexAgent(job, update) {
839
1410
  }
840
1411
  const rl = createInterface2({ input: child.stdout });
841
1412
  rl.on("line", processLine);
842
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1413
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
843
1414
  if (code !== 0) {
844
1415
  throw new Error(stderr.trim() || `Codex exited with code ${code}`);
845
1416
  }
@@ -869,7 +1440,7 @@ async function runClaudeCodeAgent(job, update) {
869
1440
  if (resume && !args.includes("--resume")) {
870
1441
  args.push("--resume", resume);
871
1442
  }
872
- const child = spawn2(job.agent.command, args, {
1443
+ const child = spawn3(job.agent.command, args, {
873
1444
  cwd: job.agent.cwd ?? process.cwd(),
874
1445
  env: { ...process.env, ...job.agent.env ?? {} },
875
1446
  stdio: ["pipe", "pipe", "pipe"]
@@ -960,7 +1531,7 @@ async function runClaudeCodeAgent(job, update) {
960
1531
  }
961
1532
  const rl = createInterface2({ input: child.stdout });
962
1533
  rl.on("line", processLine);
963
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1534
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
964
1535
  if (code !== 0) {
965
1536
  throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
966
1537
  }
@@ -992,7 +1563,7 @@ async function runOpenClawAgent(job, update) {
992
1563
  }
993
1564
  let requestedSessionId = getFlagValue(args, "--session-id");
994
1565
  if (!requestedSessionId) {
995
- requestedSessionId = job.resumeSessionId || `botapp-${randomUUID()}`;
1566
+ requestedSessionId = job.resumeSessionId || `botapp-${randomUUID2()}`;
996
1567
  args.push("--session-id", requestedSessionId);
997
1568
  }
998
1569
  if (!hasFlag(args, "--verbose")) {
@@ -1002,7 +1573,7 @@ async function runOpenClawAgent(job, update) {
1002
1573
  const state = {
1003
1574
  startedAtMs: Date.now(),
1004
1575
  sessionDir,
1005
- sessionStorePath: join3(sessionDir, "sessions.json"),
1576
+ sessionStorePath: join5(sessionDir, "sessions.json"),
1006
1577
  requestedSessionId,
1007
1578
  sessionKey: null,
1008
1579
  sessionId: null,
@@ -1027,7 +1598,7 @@ async function runOpenClawAgent(job, update) {
1027
1598
  const toolCalls = /* @__PURE__ */ new Map();
1028
1599
  let stderr = "";
1029
1600
  let stopPolling = false;
1030
- const child = spawn2(job.agent.command, args, {
1601
+ const child = spawn3(job.agent.command, args, {
1031
1602
  cwd: job.agent.cwd ?? process.cwd(),
1032
1603
  env,
1033
1604
  stdio: ["ignore", "pipe", "pipe"]
@@ -1094,7 +1665,7 @@ function resolveOpenClawSessionDir(args, env) {
1094
1665
  const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
1095
1666
  if (configured) return expandPath(configured);
1096
1667
  const agentName = resolveOpenClawAgentName(args);
1097
- return join3(homedir3(), ".openclaw", "agents", agentName, "sessions");
1668
+ return join5(homedir5(), ".openclaw", "agents", agentName, "sessions");
1098
1669
  }
1099
1670
  function resolveOpenClawAgentName(args) {
1100
1671
  return getFlagValue(args, "--agent") ?? "main";
@@ -1103,7 +1674,7 @@ function snapshotOpenClawSessionOffsets(sessionDir) {
1103
1674
  const offsets = /* @__PURE__ */ new Map();
1104
1675
  for (const file of listOpenClawSessionFiles(sessionDir)) {
1105
1676
  try {
1106
- offsets.set(file, statSync(file).size);
1677
+ offsets.set(file, statSync3(file).size);
1107
1678
  } catch {
1108
1679
  }
1109
1680
  }
@@ -1179,7 +1750,7 @@ function selectOpenClawSessionFile(state) {
1179
1750
  const files = listOpenClawSessionFiles(state.sessionDir);
1180
1751
  if (files.length === 0) return null;
1181
1752
  if (state.sessionId) {
1182
- const exact = join3(state.sessionDir, `${state.sessionId}.jsonl`);
1753
+ const exact = join5(state.sessionDir, `${state.sessionId}.jsonl`);
1183
1754
  if (files.includes(exact)) return exact;
1184
1755
  const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
1185
1756
  if (matching.length > 0) return newestFile(matching);
@@ -1187,7 +1758,7 @@ function selectOpenClawSessionFile(state) {
1187
1758
  if (files.length === 1) return files[0];
1188
1759
  const recent = files.filter((file) => {
1189
1760
  try {
1190
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
1761
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1191
1762
  } catch {
1192
1763
  return false;
1193
1764
  }
@@ -1196,10 +1767,10 @@ function selectOpenClawSessionFile(state) {
1196
1767
  }
1197
1768
  function listOpenClawSessionFiles(sessionDir) {
1198
1769
  try {
1199
- if (!existsSync4(sessionDir)) return [];
1200
- return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join3(sessionDir, name)).filter((file) => {
1770
+ if (!existsSync5(sessionDir)) return [];
1771
+ return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join5(sessionDir, name)).filter((file) => {
1201
1772
  try {
1202
- return statSync(file).isFile();
1773
+ return statSync3(file).isFile();
1203
1774
  } catch {
1204
1775
  return false;
1205
1776
  }
@@ -1212,7 +1783,7 @@ function newestFile(files) {
1212
1783
  if (files.length === 0) return null;
1213
1784
  return files.reduce((selected, file) => {
1214
1785
  try {
1215
- return statSync(file).mtimeMs > statSync(selected).mtimeMs ? file : selected;
1786
+ return statSync3(file).mtimeMs > statSync3(selected).mtimeMs ? file : selected;
1216
1787
  } catch {
1217
1788
  return selected;
1218
1789
  }
@@ -1336,7 +1907,7 @@ async function runHermesAgent(job, update) {
1336
1907
  let stderr = "";
1337
1908
  let stdoutText = "";
1338
1909
  let stopPolling = false;
1339
- const child = spawn2(job.agent.command, args, {
1910
+ const child = spawn3(job.agent.command, args, {
1340
1911
  cwd: job.agent.cwd ?? process.cwd(),
1341
1912
  env,
1342
1913
  stdio: ["ignore", "pipe", "pipe"]
@@ -1403,10 +1974,10 @@ async function runHermesAgent(job, update) {
1403
1974
  function resolveHermesSessionDir(env) {
1404
1975
  const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
1405
1976
  if (configured) return expandPath(configured);
1406
- return join3(homedir3(), ".hermes", "sessions");
1977
+ return join5(homedir5(), ".hermes", "sessions");
1407
1978
  }
1408
1979
  function hermesSessionFile(sessionDir, sessionId) {
1409
- return join3(sessionDir, `session_${sessionId}.json`);
1980
+ return join5(sessionDir, `session_${sessionId}.json`);
1410
1981
  }
1411
1982
  function parseHermesSessionId(text) {
1412
1983
  const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
@@ -1454,13 +2025,13 @@ function readHermesSessionUpdates(state, result, toolCalls, update, final = fals
1454
2025
  function selectHermesSessionFile(state) {
1455
2026
  if (state.resumeSessionId) {
1456
2027
  const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
1457
- if (existsSync4(exact)) return exact;
2028
+ if (existsSync5(exact)) return exact;
1458
2029
  }
1459
2030
  const files = listHermesSessionFiles(state.sessionDir);
1460
2031
  if (files.length === 0) return null;
1461
2032
  const recent = files.filter((file) => {
1462
2033
  try {
1463
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
2034
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1464
2035
  } catch {
1465
2036
  return false;
1466
2037
  }
@@ -1469,10 +2040,10 @@ function selectHermesSessionFile(state) {
1469
2040
  }
1470
2041
  function listHermesSessionFiles(sessionDir) {
1471
2042
  try {
1472
- if (!existsSync4(sessionDir)) return [];
1473
- return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join3(sessionDir, name)).filter((file) => {
2043
+ if (!existsSync5(sessionDir)) return [];
2044
+ return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join5(sessionDir, name)).filter((file) => {
1474
2045
  try {
1475
- return statSync(file).isFile();
2046
+ return statSync3(file).isFile();
1476
2047
  } catch {
1477
2048
  return false;
1478
2049
  }
@@ -1492,7 +2063,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1492
2063
  }
1493
2064
  if (Array.isArray(message.tool_calls)) {
1494
2065
  for (const call of message.tool_calls) {
1495
- const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID());
2066
+ const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID2());
1496
2067
  const name = String(call?.function?.name ?? call?.name ?? "tool");
1497
2068
  const next = {
1498
2069
  ...toolCalls.get(id),
@@ -1521,7 +2092,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1521
2092
  }
1522
2093
  }
1523
2094
  async function runAcpAgent(job) {
1524
- const child = spawn2(job.agent.command, job.agent.args, {
2095
+ const child = spawn3(job.agent.command, job.agent.args, {
1525
2096
  cwd: job.agent.cwd ?? process.cwd(),
1526
2097
  env: { ...process.env, ...job.agent.env ?? {} },
1527
2098
  stdio: ["pipe", "pipe", "pipe"]
@@ -1577,8 +2148,8 @@ Invalid ACP stdout: ${line}`;
1577
2148
  function request2(method, params) {
1578
2149
  const id = nextId++;
1579
2150
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
1580
- return new Promise((resolve7, reject) => {
1581
- pending.set(id, { resolve: resolve7, reject });
2151
+ return new Promise((resolve11, reject) => {
2152
+ pending.set(id, { resolve: resolve11, reject });
1582
2153
  });
1583
2154
  }
1584
2155
  child.on("exit", (code) => {
@@ -1602,7 +2173,7 @@ Invalid ACP stdout: ${line}`;
1602
2173
  clientInfo: {
1603
2174
  name: "botapp-daemon",
1604
2175
  title: "botapp daemon",
1605
- version: "0.2.3"
2176
+ version: "0.2.5"
1606
2177
  }
1607
2178
  });
1608
2179
  const session = await request2("session/new", {
@@ -1694,9 +2265,9 @@ function hasAnyFlag(args, flags) {
1694
2265
  return flags.some((flag) => hasFlag(args, flag));
1695
2266
  }
1696
2267
  function expandPath(path) {
1697
- if (path === "~") return homedir3();
1698
- if (path.startsWith("~/")) return join3(homedir3(), path.slice(2));
1699
- return resolve3(path);
2268
+ if (path === "~") return homedir5();
2269
+ if (path.startsWith("~/")) return join5(homedir5(), path.slice(2));
2270
+ return resolve4(path);
1700
2271
  }
1701
2272
  function envNumber(env, name, fallback) {
1702
2273
  const value = env[name];
@@ -1752,10 +2323,10 @@ async function waitForChild(child, timeoutMs, label) {
1752
2323
 
1753
2324
  // src/commands/launch.ts
1754
2325
  import { Command as Command4 } from "commander";
1755
- import { spawn as spawn4 } from "child_process";
1756
- import { resolve as resolve4, join as join5 } from "path";
1757
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
1758
- import { homedir as homedir5 } from "os";
2326
+ import { spawn as spawn5 } from "child_process";
2327
+ import { resolve as resolve5, join as join7 } from "path";
2328
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
2329
+ import { homedir as homedir7 } from "os";
1759
2330
  import { createInterface as createInterface3 } from "readline";
1760
2331
  import { hostname } from "os";
1761
2332
  import pc5 from "picocolors";
@@ -1763,7 +2334,7 @@ import pc5 from "picocolors";
1763
2334
  // src/auth/browser-auth.ts
1764
2335
  import { createServer } from "http";
1765
2336
  import { randomBytes } from "crypto";
1766
- import { spawn as spawn3 } from "child_process";
2337
+ import { spawn as spawn4 } from "child_process";
1767
2338
  import pc4 from "picocolors";
1768
2339
  var OK_HTML = `<!doctype html>
1769
2340
  <meta charset="utf-8">
@@ -1858,11 +2429,11 @@ async function startLoopback(expectedState) {
1858
2429
  let rejectCb = () => {
1859
2430
  };
1860
2431
  let settled = false;
1861
- const callbackPromise = new Promise((resolve7, reject) => {
2432
+ const callbackPromise = new Promise((resolve11, reject) => {
1862
2433
  resolveCb = (p) => {
1863
2434
  if (settled) return;
1864
2435
  settled = true;
1865
- resolve7(p);
2436
+ resolve11(p);
1866
2437
  };
1867
2438
  rejectCb = (e) => {
1868
2439
  if (settled) return;
@@ -1873,11 +2444,11 @@ async function startLoopback(expectedState) {
1873
2444
  const server = createServer((req, res) => {
1874
2445
  void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
1875
2446
  });
1876
- await new Promise((resolve7, reject) => {
2447
+ await new Promise((resolve11, reject) => {
1877
2448
  server.once("error", reject);
1878
2449
  server.listen(0, "127.0.0.1", () => {
1879
2450
  server.removeListener("error", reject);
1880
- resolve7();
2451
+ resolve11();
1881
2452
  });
1882
2453
  });
1883
2454
  const address = server.address();
@@ -1989,7 +2560,7 @@ function asString(v) {
1989
2560
  return typeof v === "string" ? v : void 0;
1990
2561
  }
1991
2562
  function readJsonBody(req) {
1992
- return new Promise((resolve7, reject) => {
2563
+ return new Promise((resolve11, reject) => {
1993
2564
  const chunks = [];
1994
2565
  let total = 0;
1995
2566
  req.on("data", (c) => {
@@ -2003,9 +2574,9 @@ function readJsonBody(req) {
2003
2574
  });
2004
2575
  req.on("end", () => {
2005
2576
  const raw = Buffer.concat(chunks).toString("utf-8");
2006
- if (!raw) return resolve7(null);
2577
+ if (!raw) return resolve11(null);
2007
2578
  try {
2008
- resolve7(JSON.parse(raw));
2579
+ resolve11(JSON.parse(raw));
2009
2580
  } catch (e) {
2010
2581
  reject(e);
2011
2582
  }
@@ -2017,20 +2588,20 @@ function openUrl(url) {
2017
2588
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2018
2589
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
2019
2590
  try {
2020
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
2591
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
2021
2592
  } catch {
2022
2593
  }
2023
2594
  }
2024
2595
 
2025
2596
  // src/commands/daemon-supervisor.ts
2026
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync } from "fs";
2027
- import { homedir as homedir4 } from "os";
2028
- import { join as join4 } from "path";
2597
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
2598
+ import { homedir as homedir6 } from "os";
2599
+ import { join as join6 } from "path";
2029
2600
  function daemonPidFile() {
2030
- return join4(homedir4(), ".botapp", "daemon.pid");
2601
+ return join6(homedir6(), ".botapp", "daemon.pid");
2031
2602
  }
2032
2603
  function isDaemonRunningLocally(pidFile = daemonPidFile()) {
2033
- if (!existsSync5(pidFile)) return false;
2604
+ if (!existsSync6(pidFile)) return false;
2034
2605
  try {
2035
2606
  const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
2036
2607
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -2198,20 +2769,20 @@ async function autoStartDaemon(opts, serverUrl, daemonId) {
2198
2769
  Next: run \`bot daemon run\` to bring this machine online.`));
2199
2770
  return;
2200
2771
  }
2201
- const dir = join5(homedir5(), ".botapp");
2202
- const pidFile = join5(dir, "daemon.pid");
2203
- const logFile = join5(dir, "daemon.log");
2772
+ const dir = join7(homedir7(), ".botapp");
2773
+ const pidFile = join7(dir, "daemon.pid");
2774
+ const logFile = join7(dir, "daemon.log");
2204
2775
  mkdirSync3(dir, { recursive: true });
2205
2776
  if (isDaemonRunningLocally(pidFile)) {
2206
2777
  stopExistingDaemon(pidFile);
2207
2778
  }
2208
2779
  const botBin = process.argv[1];
2209
- if (!botBin || !existsSync6(botBin)) {
2780
+ if (!botBin || !existsSync7(botBin)) {
2210
2781
  console.log(pc5.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
2211
2782
  return;
2212
2783
  }
2213
2784
  const logFd = openSync(logFile, "a");
2214
- const child = spawn4(process.execPath, [botBin, "daemon", "run"], {
2785
+ const child = spawn5(process.execPath, [botBin, "daemon", "run"], {
2215
2786
  stdio: ["ignore", logFd, logFd],
2216
2787
  detached: true
2217
2788
  });
@@ -2316,7 +2887,7 @@ Or run a local server from source:
2316
2887
  return false;
2317
2888
  }
2318
2889
  console.log(pc5.blue("Starting local server..."));
2319
- const child = spawn4("node", ["--import", "tsx", serverEntry], {
2890
+ const child = spawn5("node", ["--import", "tsx", serverEntry], {
2320
2891
  env: { ...process.env, PORT: opts.port },
2321
2892
  stdio: opts.background ? "ignore" : "inherit",
2322
2893
  detached: opts.background
@@ -2333,12 +2904,12 @@ Or run a local server from source:
2333
2904
  }
2334
2905
  function findServerEntry2() {
2335
2906
  const candidates = [
2336
- resolve4(process.cwd(), "packages/server/src/index.ts"),
2337
- resolve4(process.cwd(), "../server/src/index.ts"),
2338
- resolve4(process.cwd(), "../../packages/server/src/index.ts")
2907
+ resolve5(process.cwd(), "packages/server/src/index.ts"),
2908
+ resolve5(process.cwd(), "../server/src/index.ts"),
2909
+ resolve5(process.cwd(), "../../packages/server/src/index.ts")
2339
2910
  ];
2340
2911
  for (const c of candidates) {
2341
- if (existsSync6(c)) return c;
2912
+ if (existsSync7(c)) return c;
2342
2913
  }
2343
2914
  return null;
2344
2915
  }
@@ -2540,21 +3111,21 @@ var loginCommand = new Command7("login").description("Login to a botapp server")
2540
3111
 
2541
3112
  // src/commands/install.ts
2542
3113
  import { Command as Command8 } from "commander";
2543
- import { resolve as resolve5, basename } from "path";
2544
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
2545
- import { homedir as homedir6 } from "os";
2546
- import { join as join6 } from "path";
3114
+ import { resolve as resolve6, basename } from "path";
3115
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
3116
+ import { homedir as homedir8 } from "os";
3117
+ import { join as join8 } from "path";
2547
3118
  import pc9 from "picocolors";
2548
- var APPS_DIR = join6(homedir6(), ".botapp", "apps");
3119
+ var APPS_DIR = join8(homedir8(), ".botapp", "apps");
2549
3120
  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) => {
2550
- const absPath = resolve5(appPath);
2551
- if (!existsSync7(absPath)) {
3121
+ const absPath = resolve6(appPath);
3122
+ if (!existsSync8(absPath)) {
2552
3123
  console.error(pc9.red(`Path not found: ${absPath}`));
2553
3124
  process.exitCode = 1;
2554
3125
  return;
2555
3126
  }
2556
- const manifestPath = join6(absPath, "botapp.app.json");
2557
- if (!existsSync7(manifestPath)) {
3127
+ const manifestPath = join8(absPath, "botapp.app.json");
3128
+ if (!existsSync8(manifestPath)) {
2558
3129
  console.error(pc9.red(`No botapp.app.json found in ${absPath}`));
2559
3130
  process.exitCode = 1;
2560
3131
  return;
@@ -2568,9 +3139,9 @@ var installCommand = new Command8("install").description("Install an app from a
2568
3139
  return;
2569
3140
  }
2570
3141
  const appName = manifest.name || basename(absPath);
2571
- const targetDir = join6(APPS_DIR, appName);
3142
+ const targetDir = join8(APPS_DIR, appName);
2572
3143
  mkdirSync4(APPS_DIR, { recursive: true });
2573
- if (existsSync7(targetDir)) {
3144
+ if (existsSync8(targetDir)) {
2574
3145
  console.log(pc9.yellow(`App "${appName}" is already installed. Reinstalling...`));
2575
3146
  const { rmSync: rmSync2 } = await import("fs");
2576
3147
  rmSync2(targetDir, { recursive: true, force: true });
@@ -2589,14 +3160,14 @@ var installCommand = new Command8("install").description("Install an app from a
2589
3160
 
2590
3161
  // src/commands/uninstall.ts
2591
3162
  import { Command as Command9 } from "commander";
2592
- import { existsSync as existsSync8, rmSync } from "fs";
2593
- import { homedir as homedir7 } from "os";
2594
- import { join as join7 } from "path";
3163
+ import { existsSync as existsSync9, rmSync } from "fs";
3164
+ import { homedir as homedir9 } from "os";
3165
+ import { join as join9 } from "path";
2595
3166
  import pc10 from "picocolors";
2596
- var APPS_DIR2 = join7(homedir7(), ".botapp", "apps");
3167
+ var APPS_DIR2 = join9(homedir9(), ".botapp", "apps");
2597
3168
  var uninstallCommand = new Command9("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
2598
- const targetDir = join7(APPS_DIR2, name);
2599
- if (!existsSync8(targetDir)) {
3169
+ const targetDir = join9(APPS_DIR2, name);
3170
+ if (!existsSync9(targetDir)) {
2600
3171
  console.error(pc10.red(`App "${name}" is not installed`));
2601
3172
  process.exitCode = 1;
2602
3173
  return;
@@ -2608,21 +3179,21 @@ var uninstallCommand = new Command9("uninstall").description("Uninstall an app")
2608
3179
 
2609
3180
  // src/commands/dev.ts
2610
3181
  import { Command as Command10 } from "commander";
2611
- import { resolve as resolve6, basename as basename2, join as join8 } from "path";
2612
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
2613
- import { homedir as homedir8 } from "os";
2614
- import { spawn as spawn5 } from "child_process";
3182
+ import { resolve as resolve7, basename as basename2, join as join10 } from "path";
3183
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
3184
+ import { homedir as homedir10 } from "os";
3185
+ import { spawn as spawn6 } from "child_process";
2615
3186
  import pc11 from "picocolors";
2616
- var APPS_DIR3 = join8(homedir8(), ".botapp", "apps");
3187
+ var APPS_DIR3 = join10(homedir10(), ".botapp", "apps");
2617
3188
  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) => {
2618
- const absPath = resolve6(appPath);
2619
- if (!existsSync9(absPath)) {
3189
+ const absPath = resolve7(appPath);
3190
+ if (!existsSync10(absPath)) {
2620
3191
  console.error(pc11.red(`Path not found: ${absPath}`));
2621
3192
  process.exitCode = 1;
2622
3193
  return;
2623
3194
  }
2624
- const manifestPath = join8(absPath, "botapp.app.json");
2625
- if (!existsSync9(manifestPath)) {
3195
+ const manifestPath = join10(absPath, "botapp.app.json");
3196
+ if (!existsSync10(manifestPath)) {
2626
3197
  console.error(pc11.red(`No botapp.app.json found in ${absPath}`));
2627
3198
  process.exitCode = 1;
2628
3199
  return;
@@ -2636,9 +3207,9 @@ var devCommand = new Command10("dev").description("Start development mode for an
2636
3207
  return;
2637
3208
  }
2638
3209
  const appName = manifest.name || basename2(absPath);
2639
- const targetDir = join8(APPS_DIR3, appName);
3210
+ const targetDir = join10(APPS_DIR3, appName);
2640
3211
  mkdirSync5(APPS_DIR3, { recursive: true });
2641
- if (!existsSync9(targetDir)) {
3212
+ if (!existsSync10(targetDir)) {
2642
3213
  symlinkSync2(absPath, targetDir, "dir");
2643
3214
  console.log(pc11.blue(`Linked ${pc11.bold(appName)} \u2192 ${pc11.dim(absPath)}`));
2644
3215
  } else {
@@ -2671,7 +3242,7 @@ Start the server separately, then restart it to load the app:
2671
3242
  return;
2672
3243
  }
2673
3244
  console.log(pc11.blue("Starting botapp server..."));
2674
- const child = spawn5("node", ["--import", "tsx", serverEntry], {
3245
+ const child = spawn6("node", ["--import", "tsx", serverEntry], {
2675
3246
  env: { ...process.env, PORT: opts.port },
2676
3247
  stdio: "inherit"
2677
3248
  });
@@ -2685,11 +3256,11 @@ Start the server separately, then restart it to load the app:
2685
3256
  });
2686
3257
  function findServerEntry3() {
2687
3258
  const candidates = [
2688
- resolve6(process.cwd(), "packages/server/src/index.ts"),
2689
- resolve6(process.cwd(), "../server/src/index.ts")
3259
+ resolve7(process.cwd(), "packages/server/src/index.ts"),
3260
+ resolve7(process.cwd(), "../server/src/index.ts")
2690
3261
  ];
2691
3262
  for (const c of candidates) {
2692
- if (existsSync9(c)) return c;
3263
+ if (existsSync10(c)) return c;
2693
3264
  }
2694
3265
  return null;
2695
3266
  }
@@ -2923,13 +3494,13 @@ All agents:`);
2923
3494
 
2924
3495
  // src/commands/register.ts
2925
3496
  import { Command as Command14 } from "commander";
2926
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3497
+ import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2927
3498
  import pc15 from "picocolors";
2928
3499
  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) => {
2929
3500
  const globalOpts = cmd.parent?.opts() ?? {};
2930
3501
  const serverUrl = resolveServerUrl(globalOpts.server);
2931
3502
  const token = resolveToken(globalOpts.token);
2932
- if (!existsSync10(manifestPath)) {
3503
+ if (!existsSync11(manifestPath)) {
2933
3504
  console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
2934
3505
  process.exitCode = 1;
2935
3506
  return;
@@ -2966,13 +3537,13 @@ var registerCommand = new Command14("register").description("Register an externa
2966
3537
 
2967
3538
  // src/commands/wrap.ts
2968
3539
  import { Command as Command15 } from "commander";
2969
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
3540
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
2970
3541
  import pc16 from "picocolors";
2971
3542
  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) => {
2972
3543
  const globalOpts = cmd.parent?.opts() ?? {};
2973
3544
  const serverUrl = resolveServerUrl(globalOpts.server);
2974
3545
  const token = resolveToken(globalOpts.token);
2975
- if (!existsSync11(manifestPath)) {
3546
+ if (!existsSync12(manifestPath)) {
2976
3547
  console.error(pc16.red(`Error: Manifest not found: ${manifestPath}`));
2977
3548
  process.exitCode = 1;
2978
3549
  return;
@@ -3273,7 +3844,8 @@ async function obtainPairingToken(opts) {
3273
3844
  }
3274
3845
 
3275
3846
  // src/commands/update.ts
3276
- import { spawn as spawn6 } from "child_process";
3847
+ import { spawn as spawn7 } from "child_process";
3848
+ import { realpathSync as realpathSync2 } from "fs";
3277
3849
  import { Command as Command19 } from "commander";
3278
3850
  import pc20 from "picocolors";
3279
3851
  var PACKAGE_NAME = "botapp-cli";
@@ -3312,13 +3884,13 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3312
3884
  const { command, args } = updateCommandFor(manager);
3313
3885
  console.log(pc20.dim(`$ ${command} ${args.join(" ")}`));
3314
3886
  if (opts.dryRun) return;
3315
- const child = spawn6(command, args, { stdio: "inherit" });
3316
- const code = await new Promise((resolve7) => {
3887
+ const child = spawn7(command, args, { stdio: "inherit" });
3888
+ const code = await new Promise((resolve11) => {
3317
3889
  child.once("error", (err) => {
3318
3890
  console.error(pc20.red(`Failed to spawn ${command}: ${err.message}`));
3319
- resolve7(127);
3891
+ resolve11(127);
3320
3892
  });
3321
- child.once("close", resolve7);
3893
+ child.once("close", resolve11);
3322
3894
  });
3323
3895
  if (code !== 0) {
3324
3896
  console.error(pc20.red(`Update failed (exit ${code}).`));
@@ -3328,16 +3900,22 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3328
3900
  console.log(pc20.green("botapp-cli updated. Run `bot --version` to confirm."));
3329
3901
  });
3330
3902
  function detectPackageManager() {
3331
- const bin = process.argv[1] ?? "";
3332
- if (matchAny(bin, ["/_npx/", "\\_npx\\"])) return "npx";
3333
- if (matchAny(bin, ["/pnpm/", "\\pnpm\\", "/.pnpm/"])) return "pnpm";
3334
- if (matchAny(bin, ["/.yarn/", "/yarn/global/"])) return "yarn";
3335
- if (matchAny(bin, ["/Cellar/", "/homebrew/", "\\Cellar\\"])) return "brew";
3336
- if (matchAny(bin, ["/lib/node_modules/", "\\node_modules\\"]) || bin.includes("npm")) return "npm";
3903
+ const argv = process.argv[1] ?? "";
3904
+ let real = "";
3905
+ try {
3906
+ real = argv ? realpathSync2(argv) : "";
3907
+ } catch {
3908
+ }
3909
+ const candidates = [argv, real].filter(Boolean);
3910
+ if (matchAny(candidates, ["/_npx/", "\\_npx\\"])) return "npx";
3911
+ if (matchAny(candidates, ["/pnpm/", "\\pnpm\\", "/.pnpm/"])) return "pnpm";
3912
+ if (matchAny(candidates, ["/.yarn/", "/yarn/global/"])) return "yarn";
3913
+ if (matchAny(candidates, ["/Cellar/", "/homebrew/", "\\Cellar\\"])) return "brew";
3914
+ if (matchAny(candidates, ["/lib/node_modules/", "\\node_modules\\"]) || candidates.some((c) => c.includes("npm"))) return "npm";
3337
3915
  return null;
3338
3916
  }
3339
- function matchAny(haystack, needles) {
3340
- return needles.some((n) => haystack.includes(n));
3917
+ function matchAny(haystacks, needles) {
3918
+ return haystacks.some((h) => needles.some((n) => h.includes(n)));
3341
3919
  }
3342
3920
  function updateCommandFor(manager) {
3343
3921
  switch (manager) {
@@ -3353,9 +3931,755 @@ function updateCommandFor(manager) {
3353
3931
  }
3354
3932
  }
3355
3933
 
3934
+ // src/commands/simulate.ts
3935
+ import { Command as Command20 } from "commander";
3936
+ import { resolve as resolve8, join as join11 } from "path";
3937
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
3938
+ import { spawn as spawn8 } from "child_process";
3939
+ import pc21 from "picocolors";
3940
+ var simulateCommand = new Command20("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) => {
3941
+ const absPath = resolve8(appPath);
3942
+ if (!existsSync13(absPath)) {
3943
+ console.error(pc21.red(`Path not found: ${absPath}`));
3944
+ process.exitCode = 1;
3945
+ return;
3946
+ }
3947
+ const manifestPath = join11(absPath, "botapp.app.json");
3948
+ if (!existsSync13(manifestPath)) {
3949
+ console.error(pc21.red(`No botapp.app.json found in ${absPath}`));
3950
+ process.exitCode = 1;
3951
+ return;
3952
+ }
3953
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
3954
+ if (!manifest.name) {
3955
+ console.error(pc21.red('manifest missing "name"'));
3956
+ process.exitCode = 1;
3957
+ return;
3958
+ }
3959
+ const httpServer = resolveServerUrl(opts.server);
3960
+ const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
3961
+ const token = resolveToken(opts.token);
3962
+ if (!token) {
3963
+ console.error(pc21.red("not logged in: run `bot login` or pass --token"));
3964
+ process.exitCode = 1;
3965
+ return;
3966
+ }
3967
+ const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
3968
+ console.log(pc21.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
3969
+ const devToken = await issueDevToken({
3970
+ serverUrl: httpServer,
3971
+ token,
3972
+ appName: manifest.name,
3973
+ lifetimeMs
3974
+ });
3975
+ console.log(pc21.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
3976
+ const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
3977
+ if (!existsSync13(entryPath)) {
3978
+ console.error(pc21.red(`entry not found: ${entryPath}`));
3979
+ console.error(pc21.dim(" run `pnpm build` (or your build script) first."));
3980
+ process.exitCode = 1;
3981
+ return;
3982
+ }
3983
+ console.log(pc21.dim(`spawning ${entryPath} ...`));
3984
+ console.log(pc21.dim(` BOTAPP_SERVER=${wsServer}`));
3985
+ console.log(pc21.dim(` BOTAPP_APP_NAME=${manifest.name}`));
3986
+ console.log(
3987
+ pc21.cyan(
3988
+ `
3989
+ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
3990
+ `
3991
+ )
3992
+ );
3993
+ const child = spawn8("node", [entryPath], {
3994
+ cwd: absPath,
3995
+ env: {
3996
+ ...process.env,
3997
+ BOTAPP_SERVER: wsServer,
3998
+ BOTAPP_APP_TOKEN: devToken,
3999
+ BOTAPP_APP_NAME: manifest.name,
4000
+ BOTAPP_DATA_DIR: join11(absPath, ".botapp-sim")
4001
+ },
4002
+ stdio: "inherit"
4003
+ });
4004
+ const stop = (signal) => {
4005
+ console.log(pc21.dim(`
4006
+ stopping (${signal})...`));
4007
+ if (!child.killed) child.kill(signal);
4008
+ };
4009
+ process.on("SIGINT", () => stop("SIGINT"));
4010
+ process.on("SIGTERM", () => stop("SIGTERM"));
4011
+ child.on("exit", (code, sig) => {
4012
+ const reason = sig ? `signal ${sig}` : `exit ${code}`;
4013
+ console.log(pc21.dim(`child exited (${reason})`));
4014
+ process.exit(typeof code === "number" ? code : 0);
4015
+ });
4016
+ });
4017
+ async function issueDevToken(opts) {
4018
+ const res = await fetch(`${opts.serverUrl}/api/dev/token`, {
4019
+ method: "POST",
4020
+ headers: authHeaders(opts.token),
4021
+ body: JSON.stringify({ appName: opts.appName, lifetimeMs: opts.lifetimeMs })
4022
+ });
4023
+ if (!res.ok) {
4024
+ const text = await res.text().catch(() => "");
4025
+ throw new Error(`dev-token request failed (${res.status}): ${text}`);
4026
+ }
4027
+ const data = await res.json();
4028
+ if (!data.token) {
4029
+ throw new Error(`dev-token response missing token: ${JSON.stringify(data)}`);
4030
+ }
4031
+ return data.token;
4032
+ }
4033
+ function resolveEntry(appDir, entry) {
4034
+ const candidates = [
4035
+ entry,
4036
+ "dist/api.js",
4037
+ "dist/api/index.js",
4038
+ "dist/index.js",
4039
+ "api/index.ts",
4040
+ "api/index.js"
4041
+ ].filter(Boolean);
4042
+ for (const c of candidates) {
4043
+ const full = resolve8(appDir, c);
4044
+ if (existsSync13(full)) return full;
4045
+ }
4046
+ return resolve8(appDir, candidates[0] ?? "dist/api.js");
4047
+ }
4048
+
4049
+ // src/commands/init.ts
4050
+ import { Command as Command21 } from "commander";
4051
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
4052
+ import { resolve as resolve9, join as join12 } from "path";
4053
+ import pc22 from "picocolors";
4054
+ var initCommand = new Command21("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) => {
4055
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
4056
+ console.error(pc22.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
4057
+ console.error(pc22.dim(" e.g. my-app, todo-tracker, gomoku-2"));
4058
+ process.exitCode = 1;
4059
+ return;
4060
+ }
4061
+ const targetDir = resolve9(opts.dir ?? `./${name}`);
4062
+ if (existsSync14(targetDir) && !opts.force) {
4063
+ const stat = (() => {
4064
+ try {
4065
+ return __require("fs").readdirSync(targetDir);
4066
+ } catch {
4067
+ return [];
4068
+ }
4069
+ })();
4070
+ if (Array.isArray(stat) && stat.length > 0) {
4071
+ console.error(pc22.red(`Target directory exists and is not empty: ${targetDir}`));
4072
+ console.error(pc22.dim(" pass --force to overwrite, or pick a different --dir"));
4073
+ process.exitCode = 1;
4074
+ return;
4075
+ }
4076
+ }
4077
+ const ctx = {
4078
+ name,
4079
+ description: opts.description ?? `${name} \u2014 a botapp app`,
4080
+ headless: !!opts.headless
4081
+ };
4082
+ mkdirSync6(targetDir, { recursive: true });
4083
+ const files = ctx.headless ? headlessFiles(ctx) : fullFiles(ctx);
4084
+ for (const [rel, content] of Object.entries(files)) {
4085
+ const full = join12(targetDir, rel);
4086
+ mkdirSync6(dirname2(full), { recursive: true });
4087
+ writeFileSync4(full, content);
4088
+ }
4089
+ console.log(pc22.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc22.cyan(targetDir));
4090
+ console.log();
4091
+ console.log("Next steps:");
4092
+ console.log(pc22.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
4093
+ console.log(pc22.dim(" pnpm install # or npm install"));
4094
+ if (!ctx.headless) console.log(pc22.dim(" pnpm build # builds api/ + frontend (dist/)"));
4095
+ else console.log(pc22.dim(" pnpm build # builds api/ to dist/api.js"));
4096
+ console.log(pc22.dim(` bot login --server <your-botapp-server>`));
4097
+ console.log(pc22.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
4098
+ console.log();
4099
+ console.log(
4100
+ pc22.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
4101
+ );
4102
+ });
4103
+ function fullFiles(ctx) {
4104
+ return {
4105
+ "botapp.app.json": manifestJson(ctx),
4106
+ "package.json": packageJson(
4107
+ ctx,
4108
+ /*headless*/
4109
+ false
4110
+ ),
4111
+ "tsconfig.json": tsconfigJson(false),
4112
+ "tsconfig.api.json": tsconfigApiJson(),
4113
+ "tsconfig.frontend.json": tsconfigFrontendJson(),
4114
+ "tsup.api.config.ts": tsupApiConfig(),
4115
+ "vite.config.ts": viteConfig(ctx),
4116
+ "index.html": indexHtml(ctx),
4117
+ "api/index.ts": apiEntryTs(ctx, false),
4118
+ "src/main.tsx": srcMainTsx(ctx),
4119
+ "src/App.tsx": srcAppTsx(ctx),
4120
+ "src/lib/api.ts": srcApiTs(),
4121
+ "contracts/types.ts": contractsTs(ctx),
4122
+ ".gitignore": gitignore(),
4123
+ "README.md": readme(ctx, false)
4124
+ };
4125
+ }
4126
+ function headlessFiles(ctx) {
4127
+ return {
4128
+ "botapp.app.json": manifestJson(ctx),
4129
+ "package.json": packageJson(
4130
+ ctx,
4131
+ /*headless*/
4132
+ true
4133
+ ),
4134
+ "tsconfig.json": tsconfigJson(true),
4135
+ "tsup.api.config.ts": tsupApiConfig(),
4136
+ "api/index.ts": apiEntryTs(ctx, true),
4137
+ "contracts/types.ts": contractsTs(ctx),
4138
+ ".gitignore": gitignore(),
4139
+ "README.md": readme(ctx, true)
4140
+ };
4141
+ }
4142
+ function manifestJson(ctx) {
4143
+ const m = {
4144
+ name: ctx.name,
4145
+ version: "0.1.0",
4146
+ description: ctx.description,
4147
+ entry: "./dist/api.js",
4148
+ tier: "user",
4149
+ visibility: "private"
4150
+ };
4151
+ if (!ctx.headless) {
4152
+ m.hasFrontend = true;
4153
+ }
4154
+ return JSON.stringify(m, null, 2) + "\n";
4155
+ }
4156
+ function packageJson(ctx, headless) {
4157
+ const scripts = {
4158
+ build: headless ? "tsup --config tsup.api.config.ts" : "tsup --config tsup.api.config.ts && vite build",
4159
+ "build:api": "tsup --config tsup.api.config.ts",
4160
+ dev: headless ? "tsup --config tsup.api.config.ts --watch" : "tsup --config tsup.api.config.ts --watch & vite",
4161
+ typecheck: "tsc --noEmit"
4162
+ };
4163
+ const deps = {
4164
+ "botapp-sdk": "^0.1.0",
4165
+ ws: "^8.18.0"
4166
+ };
4167
+ const devDeps = {
4168
+ tsup: "^8.4.0",
4169
+ typescript: "^5.8.0",
4170
+ "@types/node": "^22.0.0",
4171
+ "@types/ws": "^8.5.0"
4172
+ };
4173
+ if (!headless) {
4174
+ Object.assign(deps, {
4175
+ react: "^19.0.0",
4176
+ "react-dom": "^19.0.0"
4177
+ });
4178
+ Object.assign(devDeps, {
4179
+ vite: "^7.0.0",
4180
+ "@vitejs/plugin-react": "^5.0.0",
4181
+ "@types/react": "^19.0.0",
4182
+ "@types/react-dom": "^19.0.0"
4183
+ });
4184
+ }
4185
+ return JSON.stringify(
4186
+ {
4187
+ name: ctx.name,
4188
+ version: "0.1.0",
4189
+ description: ctx.description,
4190
+ type: "module",
4191
+ private: true,
4192
+ scripts,
4193
+ dependencies: deps,
4194
+ devDependencies: devDeps
4195
+ },
4196
+ null,
4197
+ 2
4198
+ ) + "\n";
4199
+ }
4200
+ function tsconfigJson(headless) {
4201
+ if (headless) {
4202
+ return JSON.stringify(
4203
+ {
4204
+ compilerOptions: {
4205
+ target: "ES2022",
4206
+ module: "ESNext",
4207
+ moduleResolution: "bundler",
4208
+ esModuleInterop: true,
4209
+ strict: true,
4210
+ skipLibCheck: true,
4211
+ noEmit: true
4212
+ },
4213
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4214
+ },
4215
+ null,
4216
+ 2
4217
+ ) + "\n";
4218
+ }
4219
+ return JSON.stringify(
4220
+ {
4221
+ files: [],
4222
+ references: [
4223
+ { path: "./tsconfig.api.json" },
4224
+ { path: "./tsconfig.frontend.json" }
4225
+ ]
4226
+ },
4227
+ null,
4228
+ 2
4229
+ ) + "\n";
4230
+ }
4231
+ function tsconfigApiJson() {
4232
+ return JSON.stringify(
4233
+ {
4234
+ compilerOptions: {
4235
+ target: "ES2022",
4236
+ module: "ESNext",
4237
+ moduleResolution: "bundler",
4238
+ esModuleInterop: true,
4239
+ strict: true,
4240
+ skipLibCheck: true,
4241
+ noEmit: true
4242
+ },
4243
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4244
+ },
4245
+ null,
4246
+ 2
4247
+ ) + "\n";
4248
+ }
4249
+ function tsconfigFrontendJson() {
4250
+ return JSON.stringify(
4251
+ {
4252
+ compilerOptions: {
4253
+ target: "ES2022",
4254
+ module: "ESNext",
4255
+ moduleResolution: "bundler",
4256
+ esModuleInterop: true,
4257
+ strict: true,
4258
+ skipLibCheck: true,
4259
+ jsx: "react-jsx",
4260
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
4261
+ noEmit: true
4262
+ },
4263
+ include: ["src/**/*.ts", "src/**/*.tsx", "contracts/**/*.ts"]
4264
+ },
4265
+ null,
4266
+ 2
4267
+ ) + "\n";
4268
+ }
4269
+ function tsupApiConfig() {
4270
+ return `import { defineConfig } from 'tsup'
4271
+
4272
+ export default defineConfig({
4273
+ entry: { api: 'api/index.ts' },
4274
+ format: ['esm'],
4275
+ outDir: 'dist',
4276
+ clean: true,
4277
+ sourcemap: true,
4278
+ noExternal: [],
4279
+ })
4280
+ `;
4281
+ }
4282
+ function viteConfig(ctx) {
4283
+ return `import { defineConfig } from 'vite'
4284
+ import react from '@vitejs/plugin-react'
4285
+
4286
+ export default defineConfig({
4287
+ plugins: [react()],
4288
+ base: '/apps/${ctx.name}/',
4289
+ build: {
4290
+ outDir: 'dist/public',
4291
+ emptyOutDir: true,
4292
+ },
4293
+ })
4294
+ `;
4295
+ }
4296
+ function indexHtml(ctx) {
4297
+ return `<!doctype html>
4298
+ <html lang="en">
4299
+ <head>
4300
+ <meta charset="UTF-8" />
4301
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4302
+ <title>${escapeHtml(ctx.description)}</title>
4303
+ </head>
4304
+ <body>
4305
+ <div id="root"></div>
4306
+ <script type="module" src="/src/main.tsx"></script>
4307
+ </body>
4308
+ </html>
4309
+ `;
4310
+ }
4311
+ function apiEntryTs(ctx, headless) {
4312
+ const widget = headless ? "" : `
4313
+ ctx.registerWidget({
4314
+ refresh: { intervalMs: 30_000 },
4315
+ render: async ({ state }) => {
4316
+ const count = (await state.get('count')) ?? 0
4317
+ return {
4318
+ html: \`<div class="card"><div class="label">${ctx.name.toUpperCase()}</div><div class="value">\${count}</div></div>\`,
4319
+ css: \`.card { padding: 20px; font-family: sans-serif; } .value { font-size: 32px; font-weight: 700; }\`,
4320
+ }
4321
+ },
4322
+ })
4323
+
4324
+ ctx.serveStatic('./dist/public')
4325
+ `;
4326
+ return `import { BotApp } from 'botapp-sdk'
4327
+
4328
+ const app = new BotApp({
4329
+ name: '${ctx.name}',
4330
+ version: '0.1.0',
4331
+ description: '${ctx.description.replace(/'/g, "\\'")}',
4332
+ async setup(ctx) {
4333
+ ctx.registerCommand('hello', {
4334
+ description: 'Greet the caller',
4335
+ params: {
4336
+ name: { type: 'string', required: false, default: 'world', description: 'Who to greet' },
4337
+ },
4338
+ handler: async ({ name }, cmdCtx) => {
4339
+ const count = ((await cmdCtx.state.get('count')) as number | null) ?? 0
4340
+ await cmdCtx.state.set('count', count + 1)
4341
+ ctx.invalidateWidget?.()
4342
+ return \`Hello, \${name}! (called \${count + 1}x by \${cmdCtx.agent.id})\`
4343
+ },
4344
+ })
4345
+ ${widget} },
4346
+ })
4347
+
4348
+ await app.start()
4349
+ `;
4350
+ }
4351
+ function srcMainTsx(_ctx) {
4352
+ return `import { StrictMode } from 'react'
4353
+ import { createRoot } from 'react-dom/client'
4354
+ import { App } from './App'
4355
+
4356
+ createRoot(document.getElementById('root')!).render(
4357
+ <StrictMode>
4358
+ <App />
4359
+ </StrictMode>,
4360
+ )
4361
+ `;
4362
+ }
4363
+ function srcAppTsx(ctx) {
4364
+ return `import { useEffect, useState } from 'react'
4365
+ import { callCommand } from './lib/api'
4366
+
4367
+ export function App() {
4368
+ const [count, setCount] = useState<number | null>(null)
4369
+ const [error, setError] = useState<string | null>(null)
4370
+ useEffect(() => {
4371
+ callCommand('hello', { name: 'browser' })
4372
+ .then((res) => {
4373
+ const m = /\\(called (\\d+)x/.exec(String(res))
4374
+ if (m) setCount(Number(m[1]))
4375
+ })
4376
+ .catch((e: Error) => setError(e.message))
4377
+ }, [])
4378
+ return (
4379
+ <main style={{ fontFamily: 'sans-serif', padding: 24 }}>
4380
+ <h1>${ctx.name}</h1>
4381
+ {error ? <pre style={{ color: 'crimson' }}>{error}</pre> : <p>Calls so far: {count ?? '...'}</p>}
4382
+ </main>
4383
+ )
4384
+ }
4385
+ `;
4386
+ }
4387
+ function srcApiTs() {
4388
+ return `// Tiny client for calling app routes/commands from the browser.
4389
+ // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4390
+ // each request to your app's WebSocket session.
4391
+
4392
+ const APP_BASE = ((): string => {
4393
+ const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
4394
+ return m ? m[0] : '/'
4395
+ })()
4396
+
4397
+ export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4398
+ const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
4399
+ method: 'POST',
4400
+ headers: { 'Content-Type': 'application/json' },
4401
+ body: JSON.stringify(params),
4402
+ })
4403
+ if (!r.ok) throw new Error(await r.text())
4404
+ return r.json()
4405
+ }
4406
+
4407
+ export async function callAction(name: string, params: Record<string, unknown> = {}) {
4408
+ const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
4409
+ method: 'POST',
4410
+ headers: { 'Content-Type': 'application/json' },
4411
+ body: JSON.stringify(params),
4412
+ })
4413
+ if (!r.ok) throw new Error(await r.text())
4414
+ return r.json()
4415
+ }
4416
+ `;
4417
+ }
4418
+ function contractsTs(ctx) {
4419
+ return `// Types shared between api/ (backend) and src/ (frontend).
4420
+ // Importing from one side picks up changes on the other immediately.
4421
+
4422
+ export interface ${pascal(ctx.name)}State {
4423
+ count: number
4424
+ }
4425
+ `;
4426
+ }
4427
+ function gitignore() {
4428
+ return `node_modules/
4429
+ dist/
4430
+ .botapp-sim/
4431
+ *.log
4432
+ .DS_Store
4433
+ .env*.local
4434
+ `;
4435
+ }
4436
+ function readme(ctx, headless) {
4437
+ const surfaces = headless ? "- Backend only (no frontend)" : "- React + Vite frontend (built to `dist/public/`)\n- Dashboard widget (declared in `api/index.ts`)";
4438
+ return `# ${ctx.name}
4439
+
4440
+ ${ctx.description}
4441
+
4442
+ ## Surfaces
4443
+
4444
+ ${surfaces}
4445
+ - One agent-facing command: \`hello\`
4446
+
4447
+ ## Develop
4448
+
4449
+ \`\`\`bash
4450
+ pnpm install
4451
+ pnpm build # api \u2192 dist/api.js${headless ? "" : ", frontend \u2192 dist/public/"}
4452
+ bot login --server <your-botapp-server>
4453
+ bot simulate # dev-tunnel; hot-reload as you build
4454
+ \`\`\`
4455
+
4456
+ The simulator opens a per-user shadow of this app on the server. Your
4457
+ real dashboard at the server's URL routes the app to *your* local
4458
+ process; nobody else sees it.
4459
+
4460
+ ## Publish
4461
+
4462
+ \`\`\`bash
4463
+ bot publish # private, only you see it
4464
+ bot publish --public # requests admin review for public visibility
4465
+ \`\`\`
4466
+ `;
4467
+ }
4468
+ function escapeHtml(s) {
4469
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4470
+ }
4471
+ function pascal(s) {
4472
+ return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
4473
+ }
4474
+ function dirname2(p) {
4475
+ const i = p.lastIndexOf("/");
4476
+ return i < 0 ? "." : p.slice(0, i);
4477
+ }
4478
+
4479
+ // src/commands/publish.ts
4480
+ import { Command as Command22 } from "commander";
4481
+ import { resolve as resolve10, join as join13, relative as relative2 } from "path";
4482
+ import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
4483
+ import { createGzip } from "zlib";
4484
+ import { spawn as spawn9 } from "child_process";
4485
+ import pc23 from "picocolors";
4486
+ var publishCommand = new Command22("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) => {
4487
+ const absPath = resolve10(appPath);
4488
+ const manifestPath = join13(absPath, "botapp.app.json");
4489
+ if (!existsSync15(manifestPath)) {
4490
+ console.error(pc23.red(`No botapp.app.json found in ${absPath}`));
4491
+ process.exitCode = 1;
4492
+ return;
4493
+ }
4494
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
4495
+ if (!manifest.name) {
4496
+ console.error(pc23.red('manifest missing "name"'));
4497
+ process.exitCode = 1;
4498
+ return;
4499
+ }
4500
+ const server = resolveServerUrl(opts.server);
4501
+ const token = resolveToken(opts.token);
4502
+ if (!token) {
4503
+ console.error(pc23.red("not logged in: run `bot login` or pass --token"));
4504
+ process.exitCode = 1;
4505
+ return;
4506
+ }
4507
+ if (opts.build !== false) {
4508
+ console.log(pc23.dim("building app..."));
4509
+ const ok = await runBuild(absPath);
4510
+ if (!ok) {
4511
+ console.error(pc23.red("build failed; aborting"));
4512
+ process.exitCode = 1;
4513
+ return;
4514
+ }
4515
+ }
4516
+ const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
4517
+ let bundleB64;
4518
+ if (bundleDir && existsSync15(bundleDir)) {
4519
+ console.log(pc23.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
4520
+ const bytes = await packDirToTarGz(bundleDir);
4521
+ bundleB64 = bytes.toString("base64");
4522
+ console.log(pc23.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4523
+ } else {
4524
+ console.log(pc23.dim("no frontend bundle to upload (headless app or no dist/public)"));
4525
+ }
4526
+ console.log(pc23.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
4527
+ const res = await fetch(`${server}/api/apps/upload`, {
4528
+ method: "POST",
4529
+ headers: authHeaders(token),
4530
+ body: JSON.stringify({
4531
+ manifest,
4532
+ bundleB64,
4533
+ visibility: opts.public ? "public" : "private"
4534
+ })
4535
+ });
4536
+ if (!res.ok) {
4537
+ const body = await res.text().catch(() => "");
4538
+ console.error(pc23.red(`upload failed (${res.status}): ${body}`));
4539
+ process.exitCode = 1;
4540
+ return;
4541
+ }
4542
+ const data = await res.json();
4543
+ if (!data.ok) {
4544
+ console.error(pc23.red(`upload rejected: ${data.error ?? "unknown"}`));
4545
+ process.exitCode = 1;
4546
+ return;
4547
+ }
4548
+ console.log(pc23.green("\u2713"), data.message ?? "uploaded");
4549
+ if (data.install) {
4550
+ console.log(pc23.dim(` id: ${data.install.id}`));
4551
+ console.log(pc23.dim(` version: ${data.install.version}`));
4552
+ console.log(pc23.dim(` visibility: ${data.install.visibility}`));
4553
+ if (data.install.reviewStatus !== "none") {
4554
+ console.log(pc23.dim(` review: ${data.install.reviewStatus}`));
4555
+ }
4556
+ }
4557
+ });
4558
+ function runBuild(cwd) {
4559
+ const pkgManager = existsSync15(join13(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
4560
+ const args = pkgManager === "pnpm" ? ["build"] : ["run", "build"];
4561
+ return new Promise((resolveP) => {
4562
+ const child = spawn9(pkgManager, args, { cwd, stdio: "inherit" });
4563
+ child.on("exit", (code) => resolveP(code === 0));
4564
+ child.on("error", () => resolveP(false));
4565
+ });
4566
+ }
4567
+ function pickBundleDir(appDir) {
4568
+ const distPublic = join13(appDir, "dist", "public");
4569
+ if (existsSync15(distPublic)) return distPublic;
4570
+ const dist = join13(appDir, "dist");
4571
+ if (existsSync15(dist)) return dist;
4572
+ return null;
4573
+ }
4574
+ async function packDirToTarGz(rootDir) {
4575
+ const entries = [];
4576
+ walk(rootDir, "", entries);
4577
+ const blocks = [];
4578
+ for (const e of entries) {
4579
+ blocks.push(tarHeader(e.name, e.size));
4580
+ blocks.push(e.data);
4581
+ const pad = (512 - e.size % 512) % 512;
4582
+ if (pad) blocks.push(Buffer.alloc(pad));
4583
+ }
4584
+ blocks.push(Buffer.alloc(1024));
4585
+ const tar = Buffer.concat(blocks);
4586
+ return await gzip(tar);
4587
+ }
4588
+ function walk(root, prefix, out) {
4589
+ for (const entry of readdirSync2(root)) {
4590
+ const full = join13(root, entry);
4591
+ const st = statSync4(full);
4592
+ const rel = (prefix ? `${prefix}/` : "") + entry;
4593
+ if (st.isDirectory()) {
4594
+ walk(full, rel, out);
4595
+ } else if (st.isFile()) {
4596
+ const data = readFileSync10(full);
4597
+ out.push({ name: rel, size: data.length, data });
4598
+ }
4599
+ }
4600
+ }
4601
+ function tarHeader(name, size) {
4602
+ const h = Buffer.alloc(512);
4603
+ h.write(name.length > 100 ? name.slice(name.length - 100) : name, 0, 100);
4604
+ h.write("0000644", 100, 7);
4605
+ h.write("0000000", 108, 7);
4606
+ h.write("0000000", 116, 7);
4607
+ h.write(size.toString(8).padStart(11, "0"), 124, 11);
4608
+ h.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0"), 136, 11);
4609
+ h.write(" ", 148, 8);
4610
+ h.write("0", 156, 1);
4611
+ h.write("ustar ", 257, 8);
4612
+ let cksum = 0;
4613
+ for (const b of h) cksum += b;
4614
+ h.write(cksum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
4615
+ return h;
4616
+ }
4617
+ function gzip(input2) {
4618
+ return new Promise((resolveP, rejectP) => {
4619
+ const gz = createGzip();
4620
+ const chunks = [];
4621
+ gz.on("data", (c) => chunks.push(c));
4622
+ gz.on("end", () => resolveP(Buffer.concat(chunks)));
4623
+ gz.on("error", rejectP);
4624
+ gz.end(input2);
4625
+ });
4626
+ }
4627
+
4628
+ // src/commands/review.ts
4629
+ import { Command as Command23 } from "commander";
4630
+ import pc24 from "picocolors";
4631
+ var reviewCommand = new Command23("review").description("Admin: review queue for public-visibility uploads");
4632
+ reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
4633
+ const server = resolveServerUrl(opts.server);
4634
+ const token = resolveToken(opts.token);
4635
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4636
+ const res = await fetch(`${server}/api/admin/review-queue`, { headers: authHeaders(token) });
4637
+ if (!res.ok) {
4638
+ const body = await res.text().catch(() => "");
4639
+ return die(`request failed (${res.status}): ${body}`);
4640
+ }
4641
+ const data = await res.json();
4642
+ if (!data.pending?.length) {
4643
+ console.log(pc24.dim("queue empty"));
4644
+ return;
4645
+ }
4646
+ for (const i of data.pending) {
4647
+ console.log(pc24.bold(i.id), pc24.cyan(i.appName), pc24.dim(`v${i.version}`));
4648
+ console.log(pc24.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4649
+ console.log(pc24.dim(` uploaded: ${i.uploadedAt}`));
4650
+ const desc = i.manifest?.description;
4651
+ if (desc) console.log(pc24.dim(` description: ${desc}`));
4652
+ console.log();
4653
+ }
4654
+ });
4655
+ 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));
4656
+ 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));
4657
+ async function decide(id, decision, opts) {
4658
+ const server = resolveServerUrl(opts.server);
4659
+ const token = resolveToken(opts.token);
4660
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4661
+ const res = await fetch(`${server}/api/admin/review/${encodeURIComponent(id)}`, {
4662
+ method: "POST",
4663
+ headers: authHeaders(token),
4664
+ body: JSON.stringify({ decision, notes: opts.notes })
4665
+ });
4666
+ if (!res.ok) {
4667
+ const body = await res.text().catch(() => "");
4668
+ return die(`request failed (${res.status}): ${body}`);
4669
+ }
4670
+ const data = await res.json();
4671
+ if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
4672
+ console.log(pc24.green("\u2713"), `${decision}d`, data.install?.appName, pc24.dim(`(${data.install?.id})`));
4673
+ if (data.install?.reviewNotes) console.log(pc24.dim(` notes: ${data.install.reviewNotes}`));
4674
+ }
4675
+ function die(msg) {
4676
+ console.error(pc24.red(msg));
4677
+ process.exitCode = 1;
4678
+ }
4679
+
3356
4680
  // src/index.ts
3357
- var version = "0.2.3";
3358
- var program = new Command20().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");
4681
+ var version = "0.2.5";
4682
+ var program = new Command24().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");
3359
4683
  program.addCommand(launchCommand);
3360
4684
  program.addCommand(runCommand);
3361
4685
  program.addCommand(appsCommand);
@@ -3366,10 +4690,14 @@ program.addCommand(loginCommand);
3366
4690
  program.addCommand(agentCommand);
3367
4691
  program.addCommand(pairingCommand);
3368
4692
  program.addCommand(daemonCommand);
4693
+ program.addCommand(initCommand);
3369
4694
  program.addCommand(installCommand);
3370
4695
  program.addCommand(uninstallCommand);
3371
4696
  program.addCommand(reloadCommand);
3372
4697
  program.addCommand(configCommand);
4698
+ program.addCommand(simulateCommand);
4699
+ program.addCommand(publishCommand);
4700
+ program.addCommand(reviewCommand);
3373
4701
  program.addCommand(serverCommand);
3374
4702
  program.addCommand(updateCommand);
3375
4703
  program.addCommand(devCommand, { hidden: true });
@@ -3413,29 +4741,29 @@ To discover what params a command accepts:
3413
4741
  program.on("command:*", (operands) => {
3414
4742
  const first = operands[0];
3415
4743
  const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
3416
- const topLevelHint = `Run ${pc21.cyan("bot --help")} for the list of top-level commands.`;
4744
+ const topLevelHint = `Run ${pc25.cyan("bot --help")} for the list of top-level commands.`;
3417
4745
  const argv = process.argv.slice(2);
3418
4746
  const firstIdx = argv.indexOf(first);
3419
4747
  const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
3420
4748
  if (tail.length > 0) {
3421
4749
  const suggested = `bot run ${first} ${tail.join(" ")}`;
3422
4750
  console.error(
3423
- pc21.red(`error: unknown command '${first}'`) + `
4751
+ pc25.red(`error: unknown command '${first}'`) + `
3424
4752
 
3425
- App commands go through ${pc21.bold("bot run")}. Did you mean:
4753
+ App commands go through ${pc25.bold("bot run")}. Did you mean:
3426
4754
 
3427
- ${pc21.cyan(suggested)}
4755
+ ${pc25.cyan(suggested)}
3428
4756
 
3429
4757
  ${topLevelHint}`
3430
4758
  );
3431
4759
  process.exit(1);
3432
4760
  }
3433
4761
  console.error(
3434
- pc21.red(`error: unknown command '${first}'`) + `
4762
+ pc25.red(`error: unknown command '${first}'`) + `
3435
4763
 
3436
4764
  If '${first}' is an app name, invoke one of its commands with:
3437
- ${pc21.cyan(`bot run ${first} <command> [--key value ...]`)}
3438
- ${pc21.cyan("bot apps --json")} ${pc21.dim("(to see what commands exist)")}
4765
+ ${pc25.cyan(`bot run ${first} <command> [--key value ...]`)}
4766
+ ${pc25.cyan("bot apps --json")} ${pc25.dim("(to see what commands exist)")}
3439
4767
 
3440
4768
  Top-level commands: ${known.join(", ")}
3441
4769
  ${topLevelHint}`