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/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 Command20 } from "commander";
5
- import pc21 from "picocolors";
10
+ import { Command as Command24 } from "commander";
11
+ import pc25 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 spawn2 } from "child_process";
157
- import { randomUUID } from "crypto";
158
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
159
- import { homedir as homedir3 } from "os";
160
- import { join as join3, resolve as resolve3 } from "path";
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";
@@ -473,7 +479,557 @@ async function daemonSelfRequest(server, token, path, opts) {
473
479
  return data;
474
480
  }
475
481
 
482
+ // src/rpc/registry.ts
483
+ var RpcRegistry = class {
484
+ handlers = /* @__PURE__ */ new Map();
485
+ register(op, handler) {
486
+ if (this.handlers.has(op)) {
487
+ throw new Error(`RPC op already registered: ${op}`);
488
+ }
489
+ this.handlers.set(op, handler);
490
+ }
491
+ get(op) {
492
+ return this.handlers.get(op);
493
+ }
494
+ has(op) {
495
+ return this.handlers.has(op);
496
+ }
497
+ };
498
+ var InternalRpcContext = class {
499
+ constructor(appName, rpcId, ws) {
500
+ this.appName = appName;
501
+ this.rpcId = rpcId;
502
+ this.ws = ws;
503
+ }
504
+ appName;
505
+ rpcId;
506
+ ws;
507
+ cancelled = false;
508
+ inputCallback = null;
509
+ cancelCallback = null;
510
+ pushChunk(payload) {
511
+ sendJson(this.ws, {
512
+ type: "daemon_rpc_stream",
513
+ rpcId: this.rpcId,
514
+ payload
515
+ });
516
+ }
517
+ isCancelled() {
518
+ return this.cancelled;
519
+ }
520
+ onInput(callback) {
521
+ this.inputCallback = callback;
522
+ }
523
+ onCancel(callback) {
524
+ this.cancelCallback = callback;
525
+ }
526
+ cancel() {
527
+ if (this.cancelled) return;
528
+ this.cancelled = true;
529
+ const cb = this.cancelCallback;
530
+ this.cancelCallback = null;
531
+ if (cb) {
532
+ try {
533
+ cb();
534
+ } catch {
535
+ }
536
+ }
537
+ }
538
+ receiveInput(payload) {
539
+ this.inputCallback?.(payload);
540
+ }
541
+ };
542
+ var RpcDispatcher = class {
543
+ constructor(registry, ws) {
544
+ this.registry = registry;
545
+ this.ws = ws;
546
+ }
547
+ registry;
548
+ ws;
549
+ inflight = /* @__PURE__ */ new Map();
550
+ /** Handle a `daemon_rpc_request` frame. */
551
+ async dispatchRequest(frame) {
552
+ const handler = this.registry.get(frame.op);
553
+ if (!handler) {
554
+ this.sendResponse(frame.rpcId, {
555
+ ok: false,
556
+ error: `unknown daemon RPC op: ${frame.op}`
557
+ });
558
+ return;
559
+ }
560
+ const ctx = new InternalRpcContext(
561
+ frame.appName ?? "unknown",
562
+ frame.rpcId,
563
+ this.ws
564
+ );
565
+ this.inflight.set(frame.rpcId, { ctx });
566
+ try {
567
+ const result = await handler(frame.params ?? {}, ctx);
568
+ if (this.inflight.has(frame.rpcId)) {
569
+ this.sendResponse(frame.rpcId, { ok: true, result });
570
+ }
571
+ } catch (e) {
572
+ if (this.inflight.has(frame.rpcId)) {
573
+ this.sendResponse(frame.rpcId, {
574
+ ok: false,
575
+ error: e?.message ?? String(e)
576
+ });
577
+ }
578
+ } finally {
579
+ this.inflight.delete(frame.rpcId);
580
+ }
581
+ }
582
+ /** Handle a `daemon_rpc_input` frame. */
583
+ dispatchInput(frame) {
584
+ const call = this.inflight.get(frame.rpcId);
585
+ if (!call) return;
586
+ call.ctx.receiveInput(frame.payload);
587
+ }
588
+ /** Handle a `daemon_rpc_cancel` frame. */
589
+ dispatchCancel(frame) {
590
+ const call = this.inflight.get(frame.rpcId);
591
+ if (!call) return;
592
+ call.ctx.cancel();
593
+ }
594
+ /** Cancel everything (called on socket close). */
595
+ shutdown() {
596
+ for (const [, call] of this.inflight) {
597
+ call.ctx.cancel();
598
+ }
599
+ this.inflight.clear();
600
+ }
601
+ sendResponse(rpcId, body) {
602
+ sendJson(this.ws, { type: "daemon_rpc_response", rpcId, ...body });
603
+ }
604
+ };
605
+ function sendJson(ws, frame) {
606
+ if (ws.readyState !== ws.OPEN) return;
607
+ ws.send(JSON.stringify(frame));
608
+ }
609
+
610
+ // src/rpc/file-handlers.ts
611
+ import { Buffer as Buffer2 } from "buffer";
612
+ import {
613
+ promises as fsp,
614
+ realpathSync,
615
+ statSync
616
+ } from "fs";
617
+ import { homedir as homedir3 } from "os";
618
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
619
+ function registerFileHandlers(registry) {
620
+ registry.register("file.tree", fileTree);
621
+ registry.register("file.read", fileRead);
622
+ registry.register("file.write", fileWrite);
623
+ registry.register("file.stat", fileStat);
624
+ registry.register("file.mkdir", fileMkdir);
625
+ registry.register("file.delete", fileDelete);
626
+ registry.register("file.rename", fileRename);
627
+ registry.register("fs.browse", fsBrowse);
628
+ registry.register("fs.home", fsHome);
629
+ registry.register("fs.mkdir", fsMkdir);
630
+ }
631
+ function expandHome(p) {
632
+ if (p === "~") return homedir3();
633
+ if (p.startsWith("~/")) return join3(homedir3(), p.slice(2));
634
+ return p;
635
+ }
636
+ function resolveRoot(root) {
637
+ if (typeof root !== "string" || !root.trim()) {
638
+ throw new Error("file.*: `root` is required");
639
+ }
640
+ const expanded = expandHome(root);
641
+ if (!isAbsolute(expanded)) {
642
+ throw new Error(`file.*: \`root\` must be absolute (got "${root}")`);
643
+ }
644
+ try {
645
+ return realpathSync(expanded);
646
+ } catch {
647
+ return resolve3(expanded);
648
+ }
649
+ }
650
+ function jailedPath(root, sub) {
651
+ const canonicalRoot = resolveRoot(root);
652
+ const candidate = sub == null || sub === "" ? canonicalRoot : isAbsolute(sub) ? sub : resolve3(canonicalRoot, sub);
653
+ let realBase = candidate;
654
+ let tail = "";
655
+ while (true) {
656
+ try {
657
+ realBase = realpathSync(realBase);
658
+ break;
659
+ } catch {
660
+ const parent = dirname(realBase);
661
+ if (parent === realBase) {
662
+ realBase = candidate;
663
+ tail = "";
664
+ break;
665
+ }
666
+ tail = tail ? join3(realBase.slice(parent.length + 1), tail) : realBase.slice(parent.length + 1);
667
+ realBase = parent;
668
+ }
669
+ }
670
+ const final = tail ? join3(realBase, tail) : realBase;
671
+ const rel = relative(canonicalRoot, final);
672
+ if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
673
+ throw new Error(`file.*: path "${sub ?? ""}" escapes workspace root "${root}"`);
674
+ }
675
+ return final;
676
+ }
677
+ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
678
+ "node_modules",
679
+ ".git",
680
+ ".next",
681
+ ".turbo",
682
+ "dist",
683
+ "build",
684
+ ".venv",
685
+ "__pycache__",
686
+ ".DS_Store"
687
+ ]);
688
+ async function fileTree(params) {
689
+ const root = resolveRoot(params.root);
690
+ const start = jailedPath(params.root, params.path);
691
+ const depth = Math.max(0, params.depth ?? 1);
692
+ const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...params.ignore ?? []]);
693
+ const out = [];
694
+ async function walk2(absDir, level) {
695
+ let entries;
696
+ try {
697
+ entries = await fsp.readdir(absDir, {
698
+ withFileTypes: true,
699
+ encoding: "utf8"
700
+ });
701
+ } catch (e) {
702
+ if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return;
703
+ throw e;
704
+ }
705
+ entries.sort((a, b) => {
706
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
707
+ return a.name.localeCompare(b.name);
708
+ });
709
+ for (const entry of entries) {
710
+ if (ignore.has(entry.name)) continue;
711
+ const abs = join3(absDir, entry.name);
712
+ const rel = relative(root, abs);
713
+ let stat = null;
714
+ try {
715
+ stat = await fsp.stat(abs);
716
+ } catch {
717
+ continue;
718
+ }
719
+ const node = {
720
+ name: entry.name,
721
+ path: rel,
722
+ isDir: entry.isDirectory(),
723
+ size: entry.isFile() ? stat.size : void 0,
724
+ mtimeMs: stat.mtimeMs
725
+ };
726
+ out.push(node);
727
+ if (entry.isDirectory() && level < depth) {
728
+ await walk2(abs, level + 1);
729
+ }
730
+ }
731
+ }
732
+ await walk2(start, 1);
733
+ return out;
734
+ }
735
+ async function fileRead(params) {
736
+ const abs = jailedPath(params.root, params.path);
737
+ const maxBytes = params.maxBytes ?? 5 * 1024 * 1024;
738
+ const stat = await fsp.stat(abs);
739
+ if (!stat.isFile()) {
740
+ throw new Error(`file.read: not a file: ${params.path}`);
741
+ }
742
+ if (stat.size > maxBytes) {
743
+ throw new Error(
744
+ `file.read: file too large (${stat.size} bytes > ${maxBytes}). Pass maxBytes to override or chunk the read.`
745
+ );
746
+ }
747
+ const buf = await fsp.readFile(abs);
748
+ const encoding = params.encoding ?? "utf8";
749
+ return {
750
+ path: params.path,
751
+ content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
752
+ encoding,
753
+ size: stat.size,
754
+ mtimeMs: stat.mtimeMs
755
+ };
756
+ }
757
+ async function fileWrite(params) {
758
+ const abs = jailedPath(params.root, params.path);
759
+ if (params.createDirs !== false) {
760
+ await fsp.mkdir(dirname(abs), { recursive: true });
761
+ }
762
+ const buf = (params.encoding ?? "utf8") === "base64" ? Buffer2.from(params.content, "base64") : Buffer2.from(params.content, "utf8");
763
+ await fsp.writeFile(abs, buf);
764
+ const stat = await fsp.stat(abs);
765
+ return { path: params.path, size: stat.size, mtimeMs: stat.mtimeMs };
766
+ }
767
+ async function fileStat(params) {
768
+ let abs;
769
+ try {
770
+ abs = params.path == null || params.path === "" ? expandHome(params.root) : jailedPath(params.root, params.path);
771
+ } catch (e) {
772
+ if (/escapes workspace root/.test(e?.message ?? "")) throw e;
773
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
774
+ }
775
+ try {
776
+ const stat = statSync(abs);
777
+ return {
778
+ exists: true,
779
+ isFile: stat.isFile(),
780
+ isDir: stat.isDirectory(),
781
+ size: stat.size,
782
+ mtimeMs: stat.mtimeMs
783
+ };
784
+ } catch {
785
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
786
+ }
787
+ }
788
+ async function fileMkdir(params) {
789
+ const abs = jailedPath(params.root, params.path);
790
+ await fsp.mkdir(abs, { recursive: true });
791
+ return { path: params.path };
792
+ }
793
+ async function fileDelete(params) {
794
+ const abs = jailedPath(params.root, params.path);
795
+ await fsp.rm(abs, { recursive: !!params.recursive, force: false });
796
+ return { path: params.path };
797
+ }
798
+ async function fileRename(params) {
799
+ const fromAbs = jailedPath(params.root, params.from);
800
+ const toAbs = jailedPath(params.root, params.to);
801
+ await fsp.rename(fromAbs, toAbs);
802
+ return { from: params.from, to: params.to };
803
+ }
804
+ async function fsBrowse(params) {
805
+ const expanded = expandHome(params.path ?? "~");
806
+ if (!isAbsolute(expanded)) {
807
+ throw new Error(`fs.browse: path must be absolute (got "${params.path}")`);
808
+ }
809
+ let resolved;
810
+ try {
811
+ resolved = realpathSync(expanded);
812
+ } catch {
813
+ resolved = resolve3(expanded);
814
+ }
815
+ let raw;
816
+ try {
817
+ raw = await fsp.readdir(resolved, {
818
+ withFileTypes: true,
819
+ encoding: "utf8"
820
+ });
821
+ } catch (e) {
822
+ throw new Error(`fs.browse: cannot read ${resolved}: ${e?.code ?? e?.message ?? e}`);
823
+ }
824
+ const entries = [];
825
+ for (const entry of raw) {
826
+ if (!params.showHidden && entry.name.startsWith(".")) continue;
827
+ const isDir = entry.isDirectory();
828
+ if (!isDir && !params.includeFiles) continue;
829
+ entries.push({
830
+ name: entry.name,
831
+ path: join3(resolved, entry.name),
832
+ isDir
833
+ });
834
+ }
835
+ entries.sort((a, b) => {
836
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
837
+ return a.name.localeCompare(b.name);
838
+ });
839
+ const parent = dirname(resolved);
840
+ return {
841
+ path: resolved,
842
+ parent: parent === resolved ? null : parent,
843
+ entries
844
+ };
845
+ }
846
+ async function fsHome() {
847
+ return { home: homedir3(), cwd: process.cwd() };
848
+ }
849
+ async function fsMkdir(params) {
850
+ const expanded = expandHome(params.path ?? "");
851
+ if (!expanded || !isAbsolute(expanded)) {
852
+ throw new Error(`fs.mkdir: path must be absolute (got "${params.path}")`);
853
+ }
854
+ await fsp.mkdir(expanded, { recursive: true });
855
+ return { path: expanded };
856
+ }
857
+
858
+ // src/rpc/pty-handlers.ts
859
+ import { spawn as spawn2 } from "child_process";
860
+ import { randomUUID } from "crypto";
861
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
862
+ import { homedir as homedir4, platform } from "os";
863
+ import { join as join4 } from "path";
864
+ import { isAbsolute as isAbsolute2 } from "path";
865
+ var handles = /* @__PURE__ */ new Map();
866
+ function registerPtyHandlers(registry) {
867
+ registry.register("pty.spawn", ptySpawn);
868
+ registry.register("pty.write", ptyWrite);
869
+ registry.register("pty.resize", ptyResize);
870
+ registry.register("pty.kill", ptyKill);
871
+ }
872
+ async function ptySpawn(params, ctx) {
873
+ if (!params.cwd || typeof params.cwd !== "string" || !isAbsolute2(params.cwd)) {
874
+ throw new Error("pty.spawn: `cwd` is required and must be absolute");
875
+ }
876
+ let cwdStat;
877
+ try {
878
+ cwdStat = statSync2(params.cwd);
879
+ } catch {
880
+ throw new Error(`pty.spawn: cwd does not exist: ${params.cwd}`);
881
+ }
882
+ if (!cwdStat.isDirectory()) {
883
+ throw new Error(`pty.spawn: cwd is not a directory: ${params.cwd}`);
884
+ }
885
+ const command = params.command || defaultShell();
886
+ if (!existsSync4(command)) {
887
+ throw new Error(
888
+ `pty.spawn: shell binary not found: ${command} (set $SHELL in the daemon's environment, or pass an explicit \`command\`)`
889
+ );
890
+ }
891
+ const args = params.args ?? defaultShellArgs();
892
+ const env = { TERM: "xterm-256color" };
893
+ for (const [k, v] of Object.entries(process.env)) {
894
+ if (typeof v === "string") env[k] = v;
895
+ }
896
+ for (const [k, v] of Object.entries(params.env ?? {})) {
897
+ env[k] = v;
898
+ }
899
+ const cols = params.cols ?? 80;
900
+ const rows = params.rows ?? 24;
901
+ const handle = await openPty({ command, args, cwd: params.cwd, env, cols, rows, ctx });
902
+ handles.set(handle.handle.ptyId, handle.handle);
903
+ ctx.onInput((payload) => {
904
+ if (typeof payload === "string") handle.handle.write(payload);
905
+ else if (payload && typeof payload.data === "string") {
906
+ handle.handle.write(payload.data);
907
+ }
908
+ });
909
+ ctx.onCancel(() => {
910
+ handle.handle.kill("SIGTERM");
911
+ setTimeout(() => {
912
+ try {
913
+ handle.handle.kill("SIGKILL");
914
+ } catch {
915
+ }
916
+ }, 5e3).unref();
917
+ });
918
+ ctx.pushChunk({ kind: "mode", mode: handle.mode, ptyId: handle.handle.ptyId });
919
+ const exitCode = await handle.exited;
920
+ handles.delete(handle.handle.ptyId);
921
+ return { ptyId: handle.handle.ptyId, exitCode, mode: handle.mode };
922
+ }
923
+ async function ptyWrite(params) {
924
+ const handle = handles.get(params.ptyId);
925
+ if (!handle) throw new Error(`pty.write: unknown ptyId ${params.ptyId}`);
926
+ handle.write(params.data);
927
+ return { ok: true };
928
+ }
929
+ async function ptyResize(params) {
930
+ const handle = handles.get(params.ptyId);
931
+ if (!handle) throw new Error(`pty.resize: unknown ptyId ${params.ptyId}`);
932
+ handle.resize(params.cols, params.rows);
933
+ return { ok: true };
934
+ }
935
+ async function ptyKill(params) {
936
+ const handle = handles.get(params.ptyId);
937
+ if (!handle) throw new Error(`pty.kill: unknown ptyId ${params.ptyId}`);
938
+ handle.kill(params.signal ?? "SIGTERM");
939
+ return { ok: true };
940
+ }
941
+ async function openPty(opts) {
942
+ const nodePty = await loadNodePty();
943
+ if (nodePty) {
944
+ const proc2 = nodePty.spawn(opts.command, opts.args, {
945
+ name: "xterm-256color",
946
+ cols: opts.cols,
947
+ rows: opts.rows,
948
+ cwd: opts.cwd,
949
+ env: opts.env
950
+ });
951
+ const ptyId2 = randomUUID();
952
+ proc2.onData((data) => {
953
+ opts.ctx.pushChunk({ kind: "data", data });
954
+ });
955
+ const exited2 = new Promise((resolve11) => {
956
+ proc2.onExit(({ exitCode }) => {
957
+ resolve11(exitCode);
958
+ });
959
+ });
960
+ return {
961
+ mode: "pty",
962
+ exited: exited2,
963
+ handle: {
964
+ ptyId: ptyId2,
965
+ resize: (cols, rows) => proc2.resize(cols, rows),
966
+ write: (data) => proc2.write(data),
967
+ kill: (signal) => proc2.kill(signal)
968
+ }
969
+ };
970
+ }
971
+ const proc = spawn2(
972
+ opts.command,
973
+ opts.args,
974
+ {
975
+ cwd: opts.cwd,
976
+ env: opts.env,
977
+ stdio: ["pipe", "pipe", "pipe"]
978
+ }
979
+ );
980
+ const ptyId = randomUUID();
981
+ proc.stdout.on("data", (chunk) => {
982
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
983
+ });
984
+ proc.stderr.on("data", (chunk) => {
985
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
986
+ });
987
+ const exited = new Promise((resolve11) => {
988
+ proc.on("close", (code) => resolve11(code));
989
+ });
990
+ return {
991
+ mode: "pipe",
992
+ exited,
993
+ handle: {
994
+ ptyId,
995
+ resize: () => {
996
+ },
997
+ write: (data) => {
998
+ proc.stdin.write(data);
999
+ },
1000
+ kill: (signal) => {
1001
+ proc.kill(signal ?? "SIGTERM");
1002
+ }
1003
+ }
1004
+ };
1005
+ }
1006
+ var nodePtyCache = void 0;
1007
+ async function loadNodePty() {
1008
+ if (nodePtyCache !== void 0) return nodePtyCache;
1009
+ try {
1010
+ const moduleName = "node-pty";
1011
+ nodePtyCache = await import(moduleName);
1012
+ return nodePtyCache;
1013
+ } catch {
1014
+ nodePtyCache = null;
1015
+ return null;
1016
+ }
1017
+ }
1018
+ function defaultShell() {
1019
+ if (platform() === "win32") {
1020
+ return process.env.COMSPEC || "cmd.exe";
1021
+ }
1022
+ return process.env.SHELL || "/bin/bash";
1023
+ }
1024
+ function defaultShellArgs() {
1025
+ if (platform() === "win32") return [];
1026
+ return ["-l", "-i"];
1027
+ }
1028
+
476
1029
  // src/commands/daemon.ts
1030
+ var rpcRegistry = new RpcRegistry();
1031
+ registerFileHandlers(rpcRegistry);
1032
+ registerPtyHandlers(rpcRegistry);
477
1033
  var daemonCommand = new Command3("daemon").description("Manage and run the local botapp daemon");
478
1034
  daemonCommand.command("run").description(
479
1035
  "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."
@@ -518,8 +1074,8 @@ function pickProfilesToRun(alias, server) {
518
1074
  return loadDaemonProfiles();
519
1075
  }
520
1076
  daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
521
- const pidFile = join3(homedir3(), ".botapp", "daemon.pid");
522
- if (!existsSync4(pidFile)) {
1077
+ const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
1078
+ if (!existsSync5(pidFile)) {
523
1079
  console.log(pc3.yellow("No background daemon PID file found."));
524
1080
  return;
525
1081
  }
@@ -647,6 +1203,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
647
1203
  return new Promise((resolveRun) => {
648
1204
  const ws = new WebSocket(wsUrl);
649
1205
  setActiveWs(ws);
1206
+ const rpcDispatcher = new RpcDispatcher(rpcRegistry, ws);
650
1207
  let opened = false;
651
1208
  let superseded = false;
652
1209
  let settled = false;
@@ -655,6 +1212,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
655
1212
  if (settled) return;
656
1213
  settled = true;
657
1214
  if (ping) clearInterval(ping);
1215
+ rpcDispatcher.shutdown();
658
1216
  resolveRun({ opened, superseded });
659
1217
  }
660
1218
  ws.on("open", () => {
@@ -666,7 +1224,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
666
1224
  }, 3e4);
667
1225
  });
668
1226
  ws.on("message", (raw) => {
669
- void handleFrame(ws, raw.toString());
1227
+ void handleFrame(ws, raw.toString(), rpcDispatcher);
670
1228
  });
671
1229
  ws.on("close", (code, reason) => {
672
1230
  if (code === 4e3) superseded = true;
@@ -682,8 +1240,20 @@ function runDaemonSocket(wsUrl, setActiveWs) {
682
1240
  });
683
1241
  });
684
1242
  }
685
- async function handleFrame(ws, raw) {
1243
+ async function handleFrame(ws, raw, rpcDispatcher) {
686
1244
  const frame = JSON.parse(raw);
1245
+ if (frame.type === "daemon_rpc_request") {
1246
+ void rpcDispatcher.dispatchRequest(frame);
1247
+ return;
1248
+ }
1249
+ if (frame.type === "daemon_rpc_input") {
1250
+ rpcDispatcher.dispatchInput(frame);
1251
+ return;
1252
+ }
1253
+ if (frame.type === "daemon_rpc_cancel") {
1254
+ rpcDispatcher.dispatchCancel(frame);
1255
+ return;
1256
+ }
687
1257
  if (frame.type === "daemon_job") {
688
1258
  const job = frame.job;
689
1259
  console.log(pc3.blue(`Running ${job.agent.name} job ${job.id}`));
@@ -739,7 +1309,7 @@ async function runAgentJob(job, update) {
739
1309
  return runAcpAgent(job);
740
1310
  }
741
1311
  async function runShellAgent(job) {
742
- const child = spawn2(job.agent.command, [...job.agent.args, job.query], {
1312
+ const child = spawn3(job.agent.command, [...job.agent.args, job.query], {
743
1313
  cwd: job.agent.cwd ?? process.cwd(),
744
1314
  env: { ...process.env, ...job.agent.env ?? {} }
745
1315
  });
@@ -751,7 +1321,7 @@ async function runShellAgent(job) {
751
1321
  child.stderr.on("data", (chunk) => {
752
1322
  stderr += chunk.toString();
753
1323
  });
754
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1324
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
755
1325
  if (code !== 0) {
756
1326
  throw new Error(stderr.trim() || `Agent exited with code ${code}`);
757
1327
  }
@@ -779,7 +1349,7 @@ async function runCodexAgent(job, update) {
779
1349
  args.push("--dangerously-bypass-approvals-and-sandbox");
780
1350
  }
781
1351
  args.push(job.query);
782
- const child = spawn2(job.agent.command, args, {
1352
+ const child = spawn3(job.agent.command, args, {
783
1353
  cwd: job.agent.cwd ?? process.cwd(),
784
1354
  env: { ...process.env, ...job.agent.env ?? {} },
785
1355
  stdio: ["ignore", "pipe", "pipe"]
@@ -829,7 +1399,7 @@ async function runCodexAgent(job, update) {
829
1399
  }
830
1400
  const rl = createInterface2({ input: child.stdout });
831
1401
  rl.on("line", processLine);
832
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1402
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
833
1403
  if (code !== 0) {
834
1404
  throw new Error(stderr.trim() || `Codex exited with code ${code}`);
835
1405
  }
@@ -859,7 +1429,7 @@ async function runClaudeCodeAgent(job, update) {
859
1429
  if (resume && !args.includes("--resume")) {
860
1430
  args.push("--resume", resume);
861
1431
  }
862
- const child = spawn2(job.agent.command, args, {
1432
+ const child = spawn3(job.agent.command, args, {
863
1433
  cwd: job.agent.cwd ?? process.cwd(),
864
1434
  env: { ...process.env, ...job.agent.env ?? {} },
865
1435
  stdio: ["pipe", "pipe", "pipe"]
@@ -950,7 +1520,7 @@ async function runClaudeCodeAgent(job, update) {
950
1520
  }
951
1521
  const rl = createInterface2({ input: child.stdout });
952
1522
  rl.on("line", processLine);
953
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1523
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
954
1524
  if (code !== 0) {
955
1525
  throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
956
1526
  }
@@ -982,7 +1552,7 @@ async function runOpenClawAgent(job, update) {
982
1552
  }
983
1553
  let requestedSessionId = getFlagValue(args, "--session-id");
984
1554
  if (!requestedSessionId) {
985
- requestedSessionId = job.resumeSessionId || `botapp-${randomUUID()}`;
1555
+ requestedSessionId = job.resumeSessionId || `botapp-${randomUUID2()}`;
986
1556
  args.push("--session-id", requestedSessionId);
987
1557
  }
988
1558
  if (!hasFlag(args, "--verbose")) {
@@ -992,7 +1562,7 @@ async function runOpenClawAgent(job, update) {
992
1562
  const state = {
993
1563
  startedAtMs: Date.now(),
994
1564
  sessionDir,
995
- sessionStorePath: join3(sessionDir, "sessions.json"),
1565
+ sessionStorePath: join5(sessionDir, "sessions.json"),
996
1566
  requestedSessionId,
997
1567
  sessionKey: null,
998
1568
  sessionId: null,
@@ -1017,7 +1587,7 @@ async function runOpenClawAgent(job, update) {
1017
1587
  const toolCalls = /* @__PURE__ */ new Map();
1018
1588
  let stderr = "";
1019
1589
  let stopPolling = false;
1020
- const child = spawn2(job.agent.command, args, {
1590
+ const child = spawn3(job.agent.command, args, {
1021
1591
  cwd: job.agent.cwd ?? process.cwd(),
1022
1592
  env,
1023
1593
  stdio: ["ignore", "pipe", "pipe"]
@@ -1084,7 +1654,7 @@ function resolveOpenClawSessionDir(args, env) {
1084
1654
  const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
1085
1655
  if (configured) return expandPath(configured);
1086
1656
  const agentName = resolveOpenClawAgentName(args);
1087
- return join3(homedir3(), ".openclaw", "agents", agentName, "sessions");
1657
+ return join5(homedir5(), ".openclaw", "agents", agentName, "sessions");
1088
1658
  }
1089
1659
  function resolveOpenClawAgentName(args) {
1090
1660
  return getFlagValue(args, "--agent") ?? "main";
@@ -1093,7 +1663,7 @@ function snapshotOpenClawSessionOffsets(sessionDir) {
1093
1663
  const offsets = /* @__PURE__ */ new Map();
1094
1664
  for (const file of listOpenClawSessionFiles(sessionDir)) {
1095
1665
  try {
1096
- offsets.set(file, statSync(file).size);
1666
+ offsets.set(file, statSync3(file).size);
1097
1667
  } catch {
1098
1668
  }
1099
1669
  }
@@ -1169,7 +1739,7 @@ function selectOpenClawSessionFile(state) {
1169
1739
  const files = listOpenClawSessionFiles(state.sessionDir);
1170
1740
  if (files.length === 0) return null;
1171
1741
  if (state.sessionId) {
1172
- const exact = join3(state.sessionDir, `${state.sessionId}.jsonl`);
1742
+ const exact = join5(state.sessionDir, `${state.sessionId}.jsonl`);
1173
1743
  if (files.includes(exact)) return exact;
1174
1744
  const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
1175
1745
  if (matching.length > 0) return newestFile(matching);
@@ -1177,7 +1747,7 @@ function selectOpenClawSessionFile(state) {
1177
1747
  if (files.length === 1) return files[0];
1178
1748
  const recent = files.filter((file) => {
1179
1749
  try {
1180
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
1750
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1181
1751
  } catch {
1182
1752
  return false;
1183
1753
  }
@@ -1186,10 +1756,10 @@ function selectOpenClawSessionFile(state) {
1186
1756
  }
1187
1757
  function listOpenClawSessionFiles(sessionDir) {
1188
1758
  try {
1189
- if (!existsSync4(sessionDir)) return [];
1190
- return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join3(sessionDir, name)).filter((file) => {
1759
+ if (!existsSync5(sessionDir)) return [];
1760
+ return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join5(sessionDir, name)).filter((file) => {
1191
1761
  try {
1192
- return statSync(file).isFile();
1762
+ return statSync3(file).isFile();
1193
1763
  } catch {
1194
1764
  return false;
1195
1765
  }
@@ -1202,7 +1772,7 @@ function newestFile(files) {
1202
1772
  if (files.length === 0) return null;
1203
1773
  return files.reduce((selected, file) => {
1204
1774
  try {
1205
- return statSync(file).mtimeMs > statSync(selected).mtimeMs ? file : selected;
1775
+ return statSync3(file).mtimeMs > statSync3(selected).mtimeMs ? file : selected;
1206
1776
  } catch {
1207
1777
  return selected;
1208
1778
  }
@@ -1326,7 +1896,7 @@ async function runHermesAgent(job, update) {
1326
1896
  let stderr = "";
1327
1897
  let stdoutText = "";
1328
1898
  let stopPolling = false;
1329
- const child = spawn2(job.agent.command, args, {
1899
+ const child = spawn3(job.agent.command, args, {
1330
1900
  cwd: job.agent.cwd ?? process.cwd(),
1331
1901
  env,
1332
1902
  stdio: ["ignore", "pipe", "pipe"]
@@ -1393,10 +1963,10 @@ async function runHermesAgent(job, update) {
1393
1963
  function resolveHermesSessionDir(env) {
1394
1964
  const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
1395
1965
  if (configured) return expandPath(configured);
1396
- return join3(homedir3(), ".hermes", "sessions");
1966
+ return join5(homedir5(), ".hermes", "sessions");
1397
1967
  }
1398
1968
  function hermesSessionFile(sessionDir, sessionId) {
1399
- return join3(sessionDir, `session_${sessionId}.json`);
1969
+ return join5(sessionDir, `session_${sessionId}.json`);
1400
1970
  }
1401
1971
  function parseHermesSessionId(text) {
1402
1972
  const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
@@ -1444,13 +2014,13 @@ function readHermesSessionUpdates(state, result, toolCalls, update, final = fals
1444
2014
  function selectHermesSessionFile(state) {
1445
2015
  if (state.resumeSessionId) {
1446
2016
  const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
1447
- if (existsSync4(exact)) return exact;
2017
+ if (existsSync5(exact)) return exact;
1448
2018
  }
1449
2019
  const files = listHermesSessionFiles(state.sessionDir);
1450
2020
  if (files.length === 0) return null;
1451
2021
  const recent = files.filter((file) => {
1452
2022
  try {
1453
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
2023
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1454
2024
  } catch {
1455
2025
  return false;
1456
2026
  }
@@ -1459,10 +2029,10 @@ function selectHermesSessionFile(state) {
1459
2029
  }
1460
2030
  function listHermesSessionFiles(sessionDir) {
1461
2031
  try {
1462
- if (!existsSync4(sessionDir)) return [];
1463
- return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join3(sessionDir, name)).filter((file) => {
2032
+ if (!existsSync5(sessionDir)) return [];
2033
+ return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join5(sessionDir, name)).filter((file) => {
1464
2034
  try {
1465
- return statSync(file).isFile();
2035
+ return statSync3(file).isFile();
1466
2036
  } catch {
1467
2037
  return false;
1468
2038
  }
@@ -1482,7 +2052,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1482
2052
  }
1483
2053
  if (Array.isArray(message.tool_calls)) {
1484
2054
  for (const call of message.tool_calls) {
1485
- const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID());
2055
+ const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID2());
1486
2056
  const name = String(call?.function?.name ?? call?.name ?? "tool");
1487
2057
  const next = {
1488
2058
  ...toolCalls.get(id),
@@ -1511,7 +2081,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1511
2081
  }
1512
2082
  }
1513
2083
  async function runAcpAgent(job) {
1514
- const child = spawn2(job.agent.command, job.agent.args, {
2084
+ const child = spawn3(job.agent.command, job.agent.args, {
1515
2085
  cwd: job.agent.cwd ?? process.cwd(),
1516
2086
  env: { ...process.env, ...job.agent.env ?? {} },
1517
2087
  stdio: ["pipe", "pipe", "pipe"]
@@ -1567,8 +2137,8 @@ Invalid ACP stdout: ${line}`;
1567
2137
  function request2(method, params) {
1568
2138
  const id = nextId++;
1569
2139
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
1570
- return new Promise((resolve7, reject) => {
1571
- pending.set(id, { resolve: resolve7, reject });
2140
+ return new Promise((resolve11, reject) => {
2141
+ pending.set(id, { resolve: resolve11, reject });
1572
2142
  });
1573
2143
  }
1574
2144
  child.on("exit", (code) => {
@@ -1592,7 +2162,7 @@ Invalid ACP stdout: ${line}`;
1592
2162
  clientInfo: {
1593
2163
  name: "botapp-daemon",
1594
2164
  title: "botapp daemon",
1595
- version: "0.2.3"
2165
+ version: "0.2.5"
1596
2166
  }
1597
2167
  });
1598
2168
  const session = await request2("session/new", {
@@ -1684,9 +2254,9 @@ function hasAnyFlag(args, flags) {
1684
2254
  return flags.some((flag) => hasFlag(args, flag));
1685
2255
  }
1686
2256
  function expandPath(path) {
1687
- if (path === "~") return homedir3();
1688
- if (path.startsWith("~/")) return join3(homedir3(), path.slice(2));
1689
- return resolve3(path);
2257
+ if (path === "~") return homedir5();
2258
+ if (path.startsWith("~/")) return join5(homedir5(), path.slice(2));
2259
+ return resolve4(path);
1690
2260
  }
1691
2261
  function envNumber(env, name, fallback) {
1692
2262
  const value = env[name];
@@ -1742,10 +2312,10 @@ async function waitForChild(child, timeoutMs, label) {
1742
2312
 
1743
2313
  // src/commands/launch.ts
1744
2314
  import { Command as Command4 } from "commander";
1745
- import { spawn as spawn4 } from "child_process";
1746
- import { resolve as resolve4, join as join5 } from "path";
1747
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
1748
- import { homedir as homedir5 } from "os";
2315
+ import { spawn as spawn5 } from "child_process";
2316
+ import { resolve as resolve5, join as join7 } from "path";
2317
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
2318
+ import { homedir as homedir7 } from "os";
1749
2319
  import { createInterface as createInterface3 } from "readline";
1750
2320
  import { hostname } from "os";
1751
2321
  import pc5 from "picocolors";
@@ -1753,7 +2323,7 @@ import pc5 from "picocolors";
1753
2323
  // src/auth/browser-auth.ts
1754
2324
  import { createServer } from "http";
1755
2325
  import { randomBytes } from "crypto";
1756
- import { spawn as spawn3 } from "child_process";
2326
+ import { spawn as spawn4 } from "child_process";
1757
2327
  import pc4 from "picocolors";
1758
2328
  var OK_HTML = `<!doctype html>
1759
2329
  <meta charset="utf-8">
@@ -1848,11 +2418,11 @@ async function startLoopback(expectedState) {
1848
2418
  let rejectCb = () => {
1849
2419
  };
1850
2420
  let settled = false;
1851
- const callbackPromise = new Promise((resolve7, reject) => {
2421
+ const callbackPromise = new Promise((resolve11, reject) => {
1852
2422
  resolveCb = (p) => {
1853
2423
  if (settled) return;
1854
2424
  settled = true;
1855
- resolve7(p);
2425
+ resolve11(p);
1856
2426
  };
1857
2427
  rejectCb = (e) => {
1858
2428
  if (settled) return;
@@ -1863,11 +2433,11 @@ async function startLoopback(expectedState) {
1863
2433
  const server = createServer((req, res) => {
1864
2434
  void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
1865
2435
  });
1866
- await new Promise((resolve7, reject) => {
2436
+ await new Promise((resolve11, reject) => {
1867
2437
  server.once("error", reject);
1868
2438
  server.listen(0, "127.0.0.1", () => {
1869
2439
  server.removeListener("error", reject);
1870
- resolve7();
2440
+ resolve11();
1871
2441
  });
1872
2442
  });
1873
2443
  const address = server.address();
@@ -1979,7 +2549,7 @@ function asString(v) {
1979
2549
  return typeof v === "string" ? v : void 0;
1980
2550
  }
1981
2551
  function readJsonBody(req) {
1982
- return new Promise((resolve7, reject) => {
2552
+ return new Promise((resolve11, reject) => {
1983
2553
  const chunks = [];
1984
2554
  let total = 0;
1985
2555
  req.on("data", (c) => {
@@ -1993,9 +2563,9 @@ function readJsonBody(req) {
1993
2563
  });
1994
2564
  req.on("end", () => {
1995
2565
  const raw = Buffer.concat(chunks).toString("utf-8");
1996
- if (!raw) return resolve7(null);
2566
+ if (!raw) return resolve11(null);
1997
2567
  try {
1998
- resolve7(JSON.parse(raw));
2568
+ resolve11(JSON.parse(raw));
1999
2569
  } catch (e) {
2000
2570
  reject(e);
2001
2571
  }
@@ -2007,20 +2577,20 @@ function openUrl(url) {
2007
2577
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2008
2578
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
2009
2579
  try {
2010
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
2580
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
2011
2581
  } catch {
2012
2582
  }
2013
2583
  }
2014
2584
 
2015
2585
  // src/commands/daemon-supervisor.ts
2016
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync } from "fs";
2017
- import { homedir as homedir4 } from "os";
2018
- import { join as join4 } from "path";
2586
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
2587
+ import { homedir as homedir6 } from "os";
2588
+ import { join as join6 } from "path";
2019
2589
  function daemonPidFile() {
2020
- return join4(homedir4(), ".botapp", "daemon.pid");
2590
+ return join6(homedir6(), ".botapp", "daemon.pid");
2021
2591
  }
2022
2592
  function isDaemonRunningLocally(pidFile = daemonPidFile()) {
2023
- if (!existsSync5(pidFile)) return false;
2593
+ if (!existsSync6(pidFile)) return false;
2024
2594
  try {
2025
2595
  const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
2026
2596
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -2188,20 +2758,20 @@ async function autoStartDaemon(opts, serverUrl, daemonId) {
2188
2758
  Next: run \`bot daemon run\` to bring this machine online.`));
2189
2759
  return;
2190
2760
  }
2191
- const dir = join5(homedir5(), ".botapp");
2192
- const pidFile = join5(dir, "daemon.pid");
2193
- const logFile = join5(dir, "daemon.log");
2761
+ const dir = join7(homedir7(), ".botapp");
2762
+ const pidFile = join7(dir, "daemon.pid");
2763
+ const logFile = join7(dir, "daemon.log");
2194
2764
  mkdirSync3(dir, { recursive: true });
2195
2765
  if (isDaemonRunningLocally(pidFile)) {
2196
2766
  stopExistingDaemon(pidFile);
2197
2767
  }
2198
2768
  const botBin = process.argv[1];
2199
- if (!botBin || !existsSync6(botBin)) {
2769
+ if (!botBin || !existsSync7(botBin)) {
2200
2770
  console.log(pc5.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
2201
2771
  return;
2202
2772
  }
2203
2773
  const logFd = openSync(logFile, "a");
2204
- const child = spawn4(process.execPath, [botBin, "daemon", "run"], {
2774
+ const child = spawn5(process.execPath, [botBin, "daemon", "run"], {
2205
2775
  stdio: ["ignore", logFd, logFd],
2206
2776
  detached: true
2207
2777
  });
@@ -2306,7 +2876,7 @@ Or run a local server from source:
2306
2876
  return false;
2307
2877
  }
2308
2878
  console.log(pc5.blue("Starting local server..."));
2309
- const child = spawn4("node", ["--import", "tsx", serverEntry], {
2879
+ const child = spawn5("node", ["--import", "tsx", serverEntry], {
2310
2880
  env: { ...process.env, PORT: opts.port },
2311
2881
  stdio: opts.background ? "ignore" : "inherit",
2312
2882
  detached: opts.background
@@ -2323,12 +2893,12 @@ Or run a local server from source:
2323
2893
  }
2324
2894
  function findServerEntry2() {
2325
2895
  const candidates = [
2326
- resolve4(process.cwd(), "packages/server/src/index.ts"),
2327
- resolve4(process.cwd(), "../server/src/index.ts"),
2328
- resolve4(process.cwd(), "../../packages/server/src/index.ts")
2896
+ resolve5(process.cwd(), "packages/server/src/index.ts"),
2897
+ resolve5(process.cwd(), "../server/src/index.ts"),
2898
+ resolve5(process.cwd(), "../../packages/server/src/index.ts")
2329
2899
  ];
2330
2900
  for (const c of candidates) {
2331
- if (existsSync6(c)) return c;
2901
+ if (existsSync7(c)) return c;
2332
2902
  }
2333
2903
  return null;
2334
2904
  }
@@ -2530,21 +3100,21 @@ var loginCommand = new Command7("login").description("Login to a botapp server")
2530
3100
 
2531
3101
  // src/commands/install.ts
2532
3102
  import { Command as Command8 } from "commander";
2533
- import { resolve as resolve5, basename } from "path";
2534
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
2535
- import { homedir as homedir6 } from "os";
2536
- import { join as join6 } from "path";
3103
+ import { resolve as resolve6, basename } from "path";
3104
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
3105
+ import { homedir as homedir8 } from "os";
3106
+ import { join as join8 } from "path";
2537
3107
  import pc9 from "picocolors";
2538
- var APPS_DIR = join6(homedir6(), ".botapp", "apps");
3108
+ var APPS_DIR = join8(homedir8(), ".botapp", "apps");
2539
3109
  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 = resolve5(appPath);
2541
- if (!existsSync7(absPath)) {
3110
+ const absPath = resolve6(appPath);
3111
+ if (!existsSync8(absPath)) {
2542
3112
  console.error(pc9.red(`Path not found: ${absPath}`));
2543
3113
  process.exitCode = 1;
2544
3114
  return;
2545
3115
  }
2546
- const manifestPath = join6(absPath, "botapp.app.json");
2547
- if (!existsSync7(manifestPath)) {
3116
+ const manifestPath = join8(absPath, "botapp.app.json");
3117
+ if (!existsSync8(manifestPath)) {
2548
3118
  console.error(pc9.red(`No botapp.app.json found in ${absPath}`));
2549
3119
  process.exitCode = 1;
2550
3120
  return;
@@ -2558,9 +3128,9 @@ var installCommand = new Command8("install").description("Install an app from a
2558
3128
  return;
2559
3129
  }
2560
3130
  const appName = manifest.name || basename(absPath);
2561
- const targetDir = join6(APPS_DIR, appName);
3131
+ const targetDir = join8(APPS_DIR, appName);
2562
3132
  mkdirSync4(APPS_DIR, { recursive: true });
2563
- if (existsSync7(targetDir)) {
3133
+ if (existsSync8(targetDir)) {
2564
3134
  console.log(pc9.yellow(`App "${appName}" is already installed. Reinstalling...`));
2565
3135
  const { rmSync: rmSync2 } = await import("fs");
2566
3136
  rmSync2(targetDir, { recursive: true, force: true });
@@ -2579,14 +3149,14 @@ var installCommand = new Command8("install").description("Install an app from a
2579
3149
 
2580
3150
  // src/commands/uninstall.ts
2581
3151
  import { Command as Command9 } from "commander";
2582
- import { existsSync as existsSync8, rmSync } from "fs";
2583
- import { homedir as homedir7 } from "os";
2584
- import { join as join7 } from "path";
3152
+ import { existsSync as existsSync9, rmSync } from "fs";
3153
+ import { homedir as homedir9 } from "os";
3154
+ import { join as join9 } from "path";
2585
3155
  import pc10 from "picocolors";
2586
- var APPS_DIR2 = join7(homedir7(), ".botapp", "apps");
3156
+ var APPS_DIR2 = join9(homedir9(), ".botapp", "apps");
2587
3157
  var uninstallCommand = new Command9("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
2588
- const targetDir = join7(APPS_DIR2, name);
2589
- if (!existsSync8(targetDir)) {
3158
+ const targetDir = join9(APPS_DIR2, name);
3159
+ if (!existsSync9(targetDir)) {
2590
3160
  console.error(pc10.red(`App "${name}" is not installed`));
2591
3161
  process.exitCode = 1;
2592
3162
  return;
@@ -2598,21 +3168,21 @@ var uninstallCommand = new Command9("uninstall").description("Uninstall an app")
2598
3168
 
2599
3169
  // src/commands/dev.ts
2600
3170
  import { Command as Command10 } from "commander";
2601
- import { resolve as resolve6, basename as basename2, join as join8 } from "path";
2602
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
2603
- import { homedir as homedir8 } from "os";
2604
- import { spawn as spawn5 } from "child_process";
3171
+ import { resolve as resolve7, basename as basename2, join as join10 } from "path";
3172
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
3173
+ import { homedir as homedir10 } from "os";
3174
+ import { spawn as spawn6 } from "child_process";
2605
3175
  import pc11 from "picocolors";
2606
- var APPS_DIR3 = join8(homedir8(), ".botapp", "apps");
3176
+ var APPS_DIR3 = join10(homedir10(), ".botapp", "apps");
2607
3177
  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 = resolve6(appPath);
2609
- if (!existsSync9(absPath)) {
3178
+ const absPath = resolve7(appPath);
3179
+ if (!existsSync10(absPath)) {
2610
3180
  console.error(pc11.red(`Path not found: ${absPath}`));
2611
3181
  process.exitCode = 1;
2612
3182
  return;
2613
3183
  }
2614
- const manifestPath = join8(absPath, "botapp.app.json");
2615
- if (!existsSync9(manifestPath)) {
3184
+ const manifestPath = join10(absPath, "botapp.app.json");
3185
+ if (!existsSync10(manifestPath)) {
2616
3186
  console.error(pc11.red(`No botapp.app.json found in ${absPath}`));
2617
3187
  process.exitCode = 1;
2618
3188
  return;
@@ -2626,9 +3196,9 @@ var devCommand = new Command10("dev").description("Start development mode for an
2626
3196
  return;
2627
3197
  }
2628
3198
  const appName = manifest.name || basename2(absPath);
2629
- const targetDir = join8(APPS_DIR3, appName);
3199
+ const targetDir = join10(APPS_DIR3, appName);
2630
3200
  mkdirSync5(APPS_DIR3, { recursive: true });
2631
- if (!existsSync9(targetDir)) {
3201
+ if (!existsSync10(targetDir)) {
2632
3202
  symlinkSync2(absPath, targetDir, "dir");
2633
3203
  console.log(pc11.blue(`Linked ${pc11.bold(appName)} \u2192 ${pc11.dim(absPath)}`));
2634
3204
  } else {
@@ -2661,7 +3231,7 @@ Start the server separately, then restart it to load the app:
2661
3231
  return;
2662
3232
  }
2663
3233
  console.log(pc11.blue("Starting botapp server..."));
2664
- const child = spawn5("node", ["--import", "tsx", serverEntry], {
3234
+ const child = spawn6("node", ["--import", "tsx", serverEntry], {
2665
3235
  env: { ...process.env, PORT: opts.port },
2666
3236
  stdio: "inherit"
2667
3237
  });
@@ -2675,11 +3245,11 @@ Start the server separately, then restart it to load the app:
2675
3245
  });
2676
3246
  function findServerEntry3() {
2677
3247
  const candidates = [
2678
- resolve6(process.cwd(), "packages/server/src/index.ts"),
2679
- resolve6(process.cwd(), "../server/src/index.ts")
3248
+ resolve7(process.cwd(), "packages/server/src/index.ts"),
3249
+ resolve7(process.cwd(), "../server/src/index.ts")
2680
3250
  ];
2681
3251
  for (const c of candidates) {
2682
- if (existsSync9(c)) return c;
3252
+ if (existsSync10(c)) return c;
2683
3253
  }
2684
3254
  return null;
2685
3255
  }
@@ -2913,13 +3483,13 @@ All agents:`);
2913
3483
 
2914
3484
  // src/commands/register.ts
2915
3485
  import { Command as Command14 } from "commander";
2916
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3486
+ import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2917
3487
  import pc15 from "picocolors";
2918
3488
  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
3489
  const globalOpts = cmd.parent?.opts() ?? {};
2920
3490
  const serverUrl = resolveServerUrl(globalOpts.server);
2921
3491
  const token = resolveToken(globalOpts.token);
2922
- if (!existsSync10(manifestPath)) {
3492
+ if (!existsSync11(manifestPath)) {
2923
3493
  console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
2924
3494
  process.exitCode = 1;
2925
3495
  return;
@@ -2956,13 +3526,13 @@ var registerCommand = new Command14("register").description("Register an externa
2956
3526
 
2957
3527
  // src/commands/wrap.ts
2958
3528
  import { Command as Command15 } from "commander";
2959
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
3529
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
2960
3530
  import pc16 from "picocolors";
2961
3531
  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
3532
  const globalOpts = cmd.parent?.opts() ?? {};
2963
3533
  const serverUrl = resolveServerUrl(globalOpts.server);
2964
3534
  const token = resolveToken(globalOpts.token);
2965
- if (!existsSync11(manifestPath)) {
3535
+ if (!existsSync12(manifestPath)) {
2966
3536
  console.error(pc16.red(`Error: Manifest not found: ${manifestPath}`));
2967
3537
  process.exitCode = 1;
2968
3538
  return;
@@ -3263,7 +3833,8 @@ async function obtainPairingToken(opts) {
3263
3833
  }
3264
3834
 
3265
3835
  // src/commands/update.ts
3266
- import { spawn as spawn6 } from "child_process";
3836
+ import { spawn as spawn7 } from "child_process";
3837
+ import { realpathSync as realpathSync2 } from "fs";
3267
3838
  import { Command as Command19 } from "commander";
3268
3839
  import pc20 from "picocolors";
3269
3840
  var PACKAGE_NAME = "botapp-cli";
@@ -3302,13 +3873,13 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3302
3873
  const { command, args } = updateCommandFor(manager);
3303
3874
  console.log(pc20.dim(`$ ${command} ${args.join(" ")}`));
3304
3875
  if (opts.dryRun) return;
3305
- const child = spawn6(command, args, { stdio: "inherit" });
3306
- const code = await new Promise((resolve7) => {
3876
+ const child = spawn7(command, args, { stdio: "inherit" });
3877
+ const code = await new Promise((resolve11) => {
3307
3878
  child.once("error", (err) => {
3308
3879
  console.error(pc20.red(`Failed to spawn ${command}: ${err.message}`));
3309
- resolve7(127);
3880
+ resolve11(127);
3310
3881
  });
3311
- child.once("close", resolve7);
3882
+ child.once("close", resolve11);
3312
3883
  });
3313
3884
  if (code !== 0) {
3314
3885
  console.error(pc20.red(`Update failed (exit ${code}).`));
@@ -3318,16 +3889,22 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3318
3889
  console.log(pc20.green("botapp-cli updated. Run `bot --version` to confirm."));
3319
3890
  });
3320
3891
  function detectPackageManager() {
3321
- const bin = process.argv[1] ?? "";
3322
- if (matchAny(bin, ["/_npx/", "\\_npx\\"])) return "npx";
3323
- if (matchAny(bin, ["/pnpm/", "\\pnpm\\", "/.pnpm/"])) return "pnpm";
3324
- if (matchAny(bin, ["/.yarn/", "/yarn/global/"])) return "yarn";
3325
- if (matchAny(bin, ["/Cellar/", "/homebrew/", "\\Cellar\\"])) return "brew";
3326
- if (matchAny(bin, ["/lib/node_modules/", "\\node_modules\\"]) || bin.includes("npm")) return "npm";
3892
+ const argv = process.argv[1] ?? "";
3893
+ let real = "";
3894
+ try {
3895
+ real = argv ? realpathSync2(argv) : "";
3896
+ } catch {
3897
+ }
3898
+ const candidates = [argv, real].filter(Boolean);
3899
+ if (matchAny(candidates, ["/_npx/", "\\_npx\\"])) return "npx";
3900
+ if (matchAny(candidates, ["/pnpm/", "\\pnpm\\", "/.pnpm/"])) return "pnpm";
3901
+ if (matchAny(candidates, ["/.yarn/", "/yarn/global/"])) return "yarn";
3902
+ if (matchAny(candidates, ["/Cellar/", "/homebrew/", "\\Cellar\\"])) return "brew";
3903
+ if (matchAny(candidates, ["/lib/node_modules/", "\\node_modules\\"]) || candidates.some((c) => c.includes("npm"))) return "npm";
3327
3904
  return null;
3328
3905
  }
3329
- function matchAny(haystack, needles) {
3330
- return needles.some((n) => haystack.includes(n));
3906
+ function matchAny(haystacks, needles) {
3907
+ return haystacks.some((h) => needles.some((n) => h.includes(n)));
3331
3908
  }
3332
3909
  function updateCommandFor(manager) {
3333
3910
  switch (manager) {
@@ -3343,9 +3920,755 @@ function updateCommandFor(manager) {
3343
3920
  }
3344
3921
  }
3345
3922
 
3923
+ // src/commands/simulate.ts
3924
+ import { Command as Command20 } from "commander";
3925
+ import { resolve as resolve8, join as join11 } from "path";
3926
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
3927
+ import { spawn as spawn8 } from "child_process";
3928
+ import pc21 from "picocolors";
3929
+ 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) => {
3930
+ const absPath = resolve8(appPath);
3931
+ if (!existsSync13(absPath)) {
3932
+ console.error(pc21.red(`Path not found: ${absPath}`));
3933
+ process.exitCode = 1;
3934
+ return;
3935
+ }
3936
+ const manifestPath = join11(absPath, "botapp.app.json");
3937
+ if (!existsSync13(manifestPath)) {
3938
+ console.error(pc21.red(`No botapp.app.json found in ${absPath}`));
3939
+ process.exitCode = 1;
3940
+ return;
3941
+ }
3942
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
3943
+ if (!manifest.name) {
3944
+ console.error(pc21.red('manifest missing "name"'));
3945
+ process.exitCode = 1;
3946
+ return;
3947
+ }
3948
+ const httpServer = resolveServerUrl(opts.server);
3949
+ const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
3950
+ const token = resolveToken(opts.token);
3951
+ if (!token) {
3952
+ console.error(pc21.red("not logged in: run `bot login` or pass --token"));
3953
+ process.exitCode = 1;
3954
+ return;
3955
+ }
3956
+ const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
3957
+ console.log(pc21.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
3958
+ const devToken = await issueDevToken({
3959
+ serverUrl: httpServer,
3960
+ token,
3961
+ appName: manifest.name,
3962
+ lifetimeMs
3963
+ });
3964
+ console.log(pc21.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
3965
+ const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
3966
+ if (!existsSync13(entryPath)) {
3967
+ console.error(pc21.red(`entry not found: ${entryPath}`));
3968
+ console.error(pc21.dim(" run `pnpm build` (or your build script) first."));
3969
+ process.exitCode = 1;
3970
+ return;
3971
+ }
3972
+ console.log(pc21.dim(`spawning ${entryPath} ...`));
3973
+ console.log(pc21.dim(` BOTAPP_SERVER=${wsServer}`));
3974
+ console.log(pc21.dim(` BOTAPP_APP_NAME=${manifest.name}`));
3975
+ console.log(
3976
+ pc21.cyan(
3977
+ `
3978
+ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
3979
+ `
3980
+ )
3981
+ );
3982
+ const child = spawn8("node", [entryPath], {
3983
+ cwd: absPath,
3984
+ env: {
3985
+ ...process.env,
3986
+ BOTAPP_SERVER: wsServer,
3987
+ BOTAPP_APP_TOKEN: devToken,
3988
+ BOTAPP_APP_NAME: manifest.name,
3989
+ BOTAPP_DATA_DIR: join11(absPath, ".botapp-sim")
3990
+ },
3991
+ stdio: "inherit"
3992
+ });
3993
+ const stop = (signal) => {
3994
+ console.log(pc21.dim(`
3995
+ stopping (${signal})...`));
3996
+ if (!child.killed) child.kill(signal);
3997
+ };
3998
+ process.on("SIGINT", () => stop("SIGINT"));
3999
+ process.on("SIGTERM", () => stop("SIGTERM"));
4000
+ child.on("exit", (code, sig) => {
4001
+ const reason = sig ? `signal ${sig}` : `exit ${code}`;
4002
+ console.log(pc21.dim(`child exited (${reason})`));
4003
+ process.exit(typeof code === "number" ? code : 0);
4004
+ });
4005
+ });
4006
+ async function issueDevToken(opts) {
4007
+ const res = await fetch(`${opts.serverUrl}/api/dev/token`, {
4008
+ method: "POST",
4009
+ headers: authHeaders(opts.token),
4010
+ body: JSON.stringify({ appName: opts.appName, lifetimeMs: opts.lifetimeMs })
4011
+ });
4012
+ if (!res.ok) {
4013
+ const text = await res.text().catch(() => "");
4014
+ throw new Error(`dev-token request failed (${res.status}): ${text}`);
4015
+ }
4016
+ const data = await res.json();
4017
+ if (!data.token) {
4018
+ throw new Error(`dev-token response missing token: ${JSON.stringify(data)}`);
4019
+ }
4020
+ return data.token;
4021
+ }
4022
+ function resolveEntry(appDir, entry) {
4023
+ const candidates = [
4024
+ entry,
4025
+ "dist/api.js",
4026
+ "dist/api/index.js",
4027
+ "dist/index.js",
4028
+ "api/index.ts",
4029
+ "api/index.js"
4030
+ ].filter(Boolean);
4031
+ for (const c of candidates) {
4032
+ const full = resolve8(appDir, c);
4033
+ if (existsSync13(full)) return full;
4034
+ }
4035
+ return resolve8(appDir, candidates[0] ?? "dist/api.js");
4036
+ }
4037
+
4038
+ // src/commands/init.ts
4039
+ import { Command as Command21 } from "commander";
4040
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
4041
+ import { resolve as resolve9, join as join12 } from "path";
4042
+ import pc22 from "picocolors";
4043
+ 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) => {
4044
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
4045
+ console.error(pc22.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
4046
+ console.error(pc22.dim(" e.g. my-app, todo-tracker, gomoku-2"));
4047
+ process.exitCode = 1;
4048
+ return;
4049
+ }
4050
+ const targetDir = resolve9(opts.dir ?? `./${name}`);
4051
+ if (existsSync14(targetDir) && !opts.force) {
4052
+ const stat = (() => {
4053
+ try {
4054
+ return __require("fs").readdirSync(targetDir);
4055
+ } catch {
4056
+ return [];
4057
+ }
4058
+ })();
4059
+ if (Array.isArray(stat) && stat.length > 0) {
4060
+ console.error(pc22.red(`Target directory exists and is not empty: ${targetDir}`));
4061
+ console.error(pc22.dim(" pass --force to overwrite, or pick a different --dir"));
4062
+ process.exitCode = 1;
4063
+ return;
4064
+ }
4065
+ }
4066
+ const ctx = {
4067
+ name,
4068
+ description: opts.description ?? `${name} \u2014 a botapp app`,
4069
+ headless: !!opts.headless
4070
+ };
4071
+ mkdirSync6(targetDir, { recursive: true });
4072
+ const files = ctx.headless ? headlessFiles(ctx) : fullFiles(ctx);
4073
+ for (const [rel, content] of Object.entries(files)) {
4074
+ const full = join12(targetDir, rel);
4075
+ mkdirSync6(dirname2(full), { recursive: true });
4076
+ writeFileSync4(full, content);
4077
+ }
4078
+ console.log(pc22.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc22.cyan(targetDir));
4079
+ console.log();
4080
+ console.log("Next steps:");
4081
+ console.log(pc22.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
4082
+ console.log(pc22.dim(" pnpm install # or npm install"));
4083
+ if (!ctx.headless) console.log(pc22.dim(" pnpm build # builds api/ + frontend (dist/)"));
4084
+ else console.log(pc22.dim(" pnpm build # builds api/ to dist/api.js"));
4085
+ console.log(pc22.dim(` bot login --server <your-botapp-server>`));
4086
+ console.log(pc22.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
4087
+ console.log();
4088
+ console.log(
4089
+ pc22.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
4090
+ );
4091
+ });
4092
+ function fullFiles(ctx) {
4093
+ return {
4094
+ "botapp.app.json": manifestJson(ctx),
4095
+ "package.json": packageJson(
4096
+ ctx,
4097
+ /*headless*/
4098
+ false
4099
+ ),
4100
+ "tsconfig.json": tsconfigJson(false),
4101
+ "tsconfig.api.json": tsconfigApiJson(),
4102
+ "tsconfig.frontend.json": tsconfigFrontendJson(),
4103
+ "tsup.api.config.ts": tsupApiConfig(),
4104
+ "vite.config.ts": viteConfig(ctx),
4105
+ "index.html": indexHtml(ctx),
4106
+ "api/index.ts": apiEntryTs(ctx, false),
4107
+ "src/main.tsx": srcMainTsx(ctx),
4108
+ "src/App.tsx": srcAppTsx(ctx),
4109
+ "src/lib/api.ts": srcApiTs(),
4110
+ "contracts/types.ts": contractsTs(ctx),
4111
+ ".gitignore": gitignore(),
4112
+ "README.md": readme(ctx, false)
4113
+ };
4114
+ }
4115
+ function headlessFiles(ctx) {
4116
+ return {
4117
+ "botapp.app.json": manifestJson(ctx),
4118
+ "package.json": packageJson(
4119
+ ctx,
4120
+ /*headless*/
4121
+ true
4122
+ ),
4123
+ "tsconfig.json": tsconfigJson(true),
4124
+ "tsup.api.config.ts": tsupApiConfig(),
4125
+ "api/index.ts": apiEntryTs(ctx, true),
4126
+ "contracts/types.ts": contractsTs(ctx),
4127
+ ".gitignore": gitignore(),
4128
+ "README.md": readme(ctx, true)
4129
+ };
4130
+ }
4131
+ function manifestJson(ctx) {
4132
+ const m = {
4133
+ name: ctx.name,
4134
+ version: "0.1.0",
4135
+ description: ctx.description,
4136
+ entry: "./dist/api.js",
4137
+ tier: "user",
4138
+ visibility: "private"
4139
+ };
4140
+ if (!ctx.headless) {
4141
+ m.hasFrontend = true;
4142
+ }
4143
+ return JSON.stringify(m, null, 2) + "\n";
4144
+ }
4145
+ function packageJson(ctx, headless) {
4146
+ const scripts = {
4147
+ build: headless ? "tsup --config tsup.api.config.ts" : "tsup --config tsup.api.config.ts && vite build",
4148
+ "build:api": "tsup --config tsup.api.config.ts",
4149
+ dev: headless ? "tsup --config tsup.api.config.ts --watch" : "tsup --config tsup.api.config.ts --watch & vite",
4150
+ typecheck: "tsc --noEmit"
4151
+ };
4152
+ const deps = {
4153
+ "botapp-sdk": "^0.1.0",
4154
+ ws: "^8.18.0"
4155
+ };
4156
+ const devDeps = {
4157
+ tsup: "^8.4.0",
4158
+ typescript: "^5.8.0",
4159
+ "@types/node": "^22.0.0",
4160
+ "@types/ws": "^8.5.0"
4161
+ };
4162
+ if (!headless) {
4163
+ Object.assign(deps, {
4164
+ react: "^19.0.0",
4165
+ "react-dom": "^19.0.0"
4166
+ });
4167
+ Object.assign(devDeps, {
4168
+ vite: "^7.0.0",
4169
+ "@vitejs/plugin-react": "^5.0.0",
4170
+ "@types/react": "^19.0.0",
4171
+ "@types/react-dom": "^19.0.0"
4172
+ });
4173
+ }
4174
+ return JSON.stringify(
4175
+ {
4176
+ name: ctx.name,
4177
+ version: "0.1.0",
4178
+ description: ctx.description,
4179
+ type: "module",
4180
+ private: true,
4181
+ scripts,
4182
+ dependencies: deps,
4183
+ devDependencies: devDeps
4184
+ },
4185
+ null,
4186
+ 2
4187
+ ) + "\n";
4188
+ }
4189
+ function tsconfigJson(headless) {
4190
+ if (headless) {
4191
+ return JSON.stringify(
4192
+ {
4193
+ compilerOptions: {
4194
+ target: "ES2022",
4195
+ module: "ESNext",
4196
+ moduleResolution: "bundler",
4197
+ esModuleInterop: true,
4198
+ strict: true,
4199
+ skipLibCheck: true,
4200
+ noEmit: true
4201
+ },
4202
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4203
+ },
4204
+ null,
4205
+ 2
4206
+ ) + "\n";
4207
+ }
4208
+ return JSON.stringify(
4209
+ {
4210
+ files: [],
4211
+ references: [
4212
+ { path: "./tsconfig.api.json" },
4213
+ { path: "./tsconfig.frontend.json" }
4214
+ ]
4215
+ },
4216
+ null,
4217
+ 2
4218
+ ) + "\n";
4219
+ }
4220
+ function tsconfigApiJson() {
4221
+ return JSON.stringify(
4222
+ {
4223
+ compilerOptions: {
4224
+ target: "ES2022",
4225
+ module: "ESNext",
4226
+ moduleResolution: "bundler",
4227
+ esModuleInterop: true,
4228
+ strict: true,
4229
+ skipLibCheck: true,
4230
+ noEmit: true
4231
+ },
4232
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4233
+ },
4234
+ null,
4235
+ 2
4236
+ ) + "\n";
4237
+ }
4238
+ function tsconfigFrontendJson() {
4239
+ return JSON.stringify(
4240
+ {
4241
+ compilerOptions: {
4242
+ target: "ES2022",
4243
+ module: "ESNext",
4244
+ moduleResolution: "bundler",
4245
+ esModuleInterop: true,
4246
+ strict: true,
4247
+ skipLibCheck: true,
4248
+ jsx: "react-jsx",
4249
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
4250
+ noEmit: true
4251
+ },
4252
+ include: ["src/**/*.ts", "src/**/*.tsx", "contracts/**/*.ts"]
4253
+ },
4254
+ null,
4255
+ 2
4256
+ ) + "\n";
4257
+ }
4258
+ function tsupApiConfig() {
4259
+ return `import { defineConfig } from 'tsup'
4260
+
4261
+ export default defineConfig({
4262
+ entry: { api: 'api/index.ts' },
4263
+ format: ['esm'],
4264
+ outDir: 'dist',
4265
+ clean: true,
4266
+ sourcemap: true,
4267
+ noExternal: [],
4268
+ })
4269
+ `;
4270
+ }
4271
+ function viteConfig(ctx) {
4272
+ return `import { defineConfig } from 'vite'
4273
+ import react from '@vitejs/plugin-react'
4274
+
4275
+ export default defineConfig({
4276
+ plugins: [react()],
4277
+ base: '/apps/${ctx.name}/',
4278
+ build: {
4279
+ outDir: 'dist/public',
4280
+ emptyOutDir: true,
4281
+ },
4282
+ })
4283
+ `;
4284
+ }
4285
+ function indexHtml(ctx) {
4286
+ return `<!doctype html>
4287
+ <html lang="en">
4288
+ <head>
4289
+ <meta charset="UTF-8" />
4290
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4291
+ <title>${escapeHtml(ctx.description)}</title>
4292
+ </head>
4293
+ <body>
4294
+ <div id="root"></div>
4295
+ <script type="module" src="/src/main.tsx"></script>
4296
+ </body>
4297
+ </html>
4298
+ `;
4299
+ }
4300
+ function apiEntryTs(ctx, headless) {
4301
+ const widget = headless ? "" : `
4302
+ ctx.registerWidget({
4303
+ refresh: { intervalMs: 30_000 },
4304
+ render: async ({ state }) => {
4305
+ const count = (await state.get('count')) ?? 0
4306
+ return {
4307
+ html: \`<div class="card"><div class="label">${ctx.name.toUpperCase()}</div><div class="value">\${count}</div></div>\`,
4308
+ css: \`.card { padding: 20px; font-family: sans-serif; } .value { font-size: 32px; font-weight: 700; }\`,
4309
+ }
4310
+ },
4311
+ })
4312
+
4313
+ ctx.serveStatic('./dist/public')
4314
+ `;
4315
+ return `import { BotApp } from 'botapp-sdk'
4316
+
4317
+ const app = new BotApp({
4318
+ name: '${ctx.name}',
4319
+ version: '0.1.0',
4320
+ description: '${ctx.description.replace(/'/g, "\\'")}',
4321
+ async setup(ctx) {
4322
+ ctx.registerCommand('hello', {
4323
+ description: 'Greet the caller',
4324
+ params: {
4325
+ name: { type: 'string', required: false, default: 'world', description: 'Who to greet' },
4326
+ },
4327
+ handler: async ({ name }, cmdCtx) => {
4328
+ const count = ((await cmdCtx.state.get('count')) as number | null) ?? 0
4329
+ await cmdCtx.state.set('count', count + 1)
4330
+ ctx.invalidateWidget?.()
4331
+ return \`Hello, \${name}! (called \${count + 1}x by \${cmdCtx.agent.id})\`
4332
+ },
4333
+ })
4334
+ ${widget} },
4335
+ })
4336
+
4337
+ await app.start()
4338
+ `;
4339
+ }
4340
+ function srcMainTsx(_ctx) {
4341
+ return `import { StrictMode } from 'react'
4342
+ import { createRoot } from 'react-dom/client'
4343
+ import { App } from './App'
4344
+
4345
+ createRoot(document.getElementById('root')!).render(
4346
+ <StrictMode>
4347
+ <App />
4348
+ </StrictMode>,
4349
+ )
4350
+ `;
4351
+ }
4352
+ function srcAppTsx(ctx) {
4353
+ return `import { useEffect, useState } from 'react'
4354
+ import { callCommand } from './lib/api'
4355
+
4356
+ export function App() {
4357
+ const [count, setCount] = useState<number | null>(null)
4358
+ const [error, setError] = useState<string | null>(null)
4359
+ useEffect(() => {
4360
+ callCommand('hello', { name: 'browser' })
4361
+ .then((res) => {
4362
+ const m = /\\(called (\\d+)x/.exec(String(res))
4363
+ if (m) setCount(Number(m[1]))
4364
+ })
4365
+ .catch((e: Error) => setError(e.message))
4366
+ }, [])
4367
+ return (
4368
+ <main style={{ fontFamily: 'sans-serif', padding: 24 }}>
4369
+ <h1>${ctx.name}</h1>
4370
+ {error ? <pre style={{ color: 'crimson' }}>{error}</pre> : <p>Calls so far: {count ?? '...'}</p>}
4371
+ </main>
4372
+ )
4373
+ }
4374
+ `;
4375
+ }
4376
+ function srcApiTs() {
4377
+ return `// Tiny client for calling app routes/commands from the browser.
4378
+ // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4379
+ // each request to your app's WebSocket session.
4380
+
4381
+ const APP_BASE = ((): string => {
4382
+ const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
4383
+ return m ? m[0] : '/'
4384
+ })()
4385
+
4386
+ export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4387
+ const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
4388
+ method: 'POST',
4389
+ headers: { 'Content-Type': 'application/json' },
4390
+ body: JSON.stringify(params),
4391
+ })
4392
+ if (!r.ok) throw new Error(await r.text())
4393
+ return r.json()
4394
+ }
4395
+
4396
+ export async function callAction(name: string, params: Record<string, unknown> = {}) {
4397
+ const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
4398
+ method: 'POST',
4399
+ headers: { 'Content-Type': 'application/json' },
4400
+ body: JSON.stringify(params),
4401
+ })
4402
+ if (!r.ok) throw new Error(await r.text())
4403
+ return r.json()
4404
+ }
4405
+ `;
4406
+ }
4407
+ function contractsTs(ctx) {
4408
+ return `// Types shared between api/ (backend) and src/ (frontend).
4409
+ // Importing from one side picks up changes on the other immediately.
4410
+
4411
+ export interface ${pascal(ctx.name)}State {
4412
+ count: number
4413
+ }
4414
+ `;
4415
+ }
4416
+ function gitignore() {
4417
+ return `node_modules/
4418
+ dist/
4419
+ .botapp-sim/
4420
+ *.log
4421
+ .DS_Store
4422
+ .env*.local
4423
+ `;
4424
+ }
4425
+ function readme(ctx, headless) {
4426
+ const surfaces = headless ? "- Backend only (no frontend)" : "- React + Vite frontend (built to `dist/public/`)\n- Dashboard widget (declared in `api/index.ts`)";
4427
+ return `# ${ctx.name}
4428
+
4429
+ ${ctx.description}
4430
+
4431
+ ## Surfaces
4432
+
4433
+ ${surfaces}
4434
+ - One agent-facing command: \`hello\`
4435
+
4436
+ ## Develop
4437
+
4438
+ \`\`\`bash
4439
+ pnpm install
4440
+ pnpm build # api \u2192 dist/api.js${headless ? "" : ", frontend \u2192 dist/public/"}
4441
+ bot login --server <your-botapp-server>
4442
+ bot simulate # dev-tunnel; hot-reload as you build
4443
+ \`\`\`
4444
+
4445
+ The simulator opens a per-user shadow of this app on the server. Your
4446
+ real dashboard at the server's URL routes the app to *your* local
4447
+ process; nobody else sees it.
4448
+
4449
+ ## Publish
4450
+
4451
+ \`\`\`bash
4452
+ bot publish # private, only you see it
4453
+ bot publish --public # requests admin review for public visibility
4454
+ \`\`\`
4455
+ `;
4456
+ }
4457
+ function escapeHtml(s) {
4458
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4459
+ }
4460
+ function pascal(s) {
4461
+ return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
4462
+ }
4463
+ function dirname2(p) {
4464
+ const i = p.lastIndexOf("/");
4465
+ return i < 0 ? "." : p.slice(0, i);
4466
+ }
4467
+
4468
+ // src/commands/publish.ts
4469
+ import { Command as Command22 } from "commander";
4470
+ import { resolve as resolve10, join as join13, relative as relative2 } from "path";
4471
+ import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
4472
+ import { createGzip } from "zlib";
4473
+ import { spawn as spawn9 } from "child_process";
4474
+ import pc23 from "picocolors";
4475
+ 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) => {
4476
+ const absPath = resolve10(appPath);
4477
+ const manifestPath = join13(absPath, "botapp.app.json");
4478
+ if (!existsSync15(manifestPath)) {
4479
+ console.error(pc23.red(`No botapp.app.json found in ${absPath}`));
4480
+ process.exitCode = 1;
4481
+ return;
4482
+ }
4483
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
4484
+ if (!manifest.name) {
4485
+ console.error(pc23.red('manifest missing "name"'));
4486
+ process.exitCode = 1;
4487
+ return;
4488
+ }
4489
+ const server = resolveServerUrl(opts.server);
4490
+ const token = resolveToken(opts.token);
4491
+ if (!token) {
4492
+ console.error(pc23.red("not logged in: run `bot login` or pass --token"));
4493
+ process.exitCode = 1;
4494
+ return;
4495
+ }
4496
+ if (opts.build !== false) {
4497
+ console.log(pc23.dim("building app..."));
4498
+ const ok = await runBuild(absPath);
4499
+ if (!ok) {
4500
+ console.error(pc23.red("build failed; aborting"));
4501
+ process.exitCode = 1;
4502
+ return;
4503
+ }
4504
+ }
4505
+ const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
4506
+ let bundleB64;
4507
+ if (bundleDir && existsSync15(bundleDir)) {
4508
+ console.log(pc23.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
4509
+ const bytes = await packDirToTarGz(bundleDir);
4510
+ bundleB64 = bytes.toString("base64");
4511
+ console.log(pc23.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4512
+ } else {
4513
+ console.log(pc23.dim("no frontend bundle to upload (headless app or no dist/public)"));
4514
+ }
4515
+ console.log(pc23.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
4516
+ const res = await fetch(`${server}/api/apps/upload`, {
4517
+ method: "POST",
4518
+ headers: authHeaders(token),
4519
+ body: JSON.stringify({
4520
+ manifest,
4521
+ bundleB64,
4522
+ visibility: opts.public ? "public" : "private"
4523
+ })
4524
+ });
4525
+ if (!res.ok) {
4526
+ const body = await res.text().catch(() => "");
4527
+ console.error(pc23.red(`upload failed (${res.status}): ${body}`));
4528
+ process.exitCode = 1;
4529
+ return;
4530
+ }
4531
+ const data = await res.json();
4532
+ if (!data.ok) {
4533
+ console.error(pc23.red(`upload rejected: ${data.error ?? "unknown"}`));
4534
+ process.exitCode = 1;
4535
+ return;
4536
+ }
4537
+ console.log(pc23.green("\u2713"), data.message ?? "uploaded");
4538
+ if (data.install) {
4539
+ console.log(pc23.dim(` id: ${data.install.id}`));
4540
+ console.log(pc23.dim(` version: ${data.install.version}`));
4541
+ console.log(pc23.dim(` visibility: ${data.install.visibility}`));
4542
+ if (data.install.reviewStatus !== "none") {
4543
+ console.log(pc23.dim(` review: ${data.install.reviewStatus}`));
4544
+ }
4545
+ }
4546
+ });
4547
+ function runBuild(cwd) {
4548
+ const pkgManager = existsSync15(join13(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
4549
+ const args = pkgManager === "pnpm" ? ["build"] : ["run", "build"];
4550
+ return new Promise((resolveP) => {
4551
+ const child = spawn9(pkgManager, args, { cwd, stdio: "inherit" });
4552
+ child.on("exit", (code) => resolveP(code === 0));
4553
+ child.on("error", () => resolveP(false));
4554
+ });
4555
+ }
4556
+ function pickBundleDir(appDir) {
4557
+ const distPublic = join13(appDir, "dist", "public");
4558
+ if (existsSync15(distPublic)) return distPublic;
4559
+ const dist = join13(appDir, "dist");
4560
+ if (existsSync15(dist)) return dist;
4561
+ return null;
4562
+ }
4563
+ async function packDirToTarGz(rootDir) {
4564
+ const entries = [];
4565
+ walk(rootDir, "", entries);
4566
+ const blocks = [];
4567
+ for (const e of entries) {
4568
+ blocks.push(tarHeader(e.name, e.size));
4569
+ blocks.push(e.data);
4570
+ const pad = (512 - e.size % 512) % 512;
4571
+ if (pad) blocks.push(Buffer.alloc(pad));
4572
+ }
4573
+ blocks.push(Buffer.alloc(1024));
4574
+ const tar = Buffer.concat(blocks);
4575
+ return await gzip(tar);
4576
+ }
4577
+ function walk(root, prefix, out) {
4578
+ for (const entry of readdirSync2(root)) {
4579
+ const full = join13(root, entry);
4580
+ const st = statSync4(full);
4581
+ const rel = (prefix ? `${prefix}/` : "") + entry;
4582
+ if (st.isDirectory()) {
4583
+ walk(full, rel, out);
4584
+ } else if (st.isFile()) {
4585
+ const data = readFileSync10(full);
4586
+ out.push({ name: rel, size: data.length, data });
4587
+ }
4588
+ }
4589
+ }
4590
+ function tarHeader(name, size) {
4591
+ const h = Buffer.alloc(512);
4592
+ h.write(name.length > 100 ? name.slice(name.length - 100) : name, 0, 100);
4593
+ h.write("0000644", 100, 7);
4594
+ h.write("0000000", 108, 7);
4595
+ h.write("0000000", 116, 7);
4596
+ h.write(size.toString(8).padStart(11, "0"), 124, 11);
4597
+ h.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0"), 136, 11);
4598
+ h.write(" ", 148, 8);
4599
+ h.write("0", 156, 1);
4600
+ h.write("ustar ", 257, 8);
4601
+ let cksum = 0;
4602
+ for (const b of h) cksum += b;
4603
+ h.write(cksum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
4604
+ return h;
4605
+ }
4606
+ function gzip(input2) {
4607
+ return new Promise((resolveP, rejectP) => {
4608
+ const gz = createGzip();
4609
+ const chunks = [];
4610
+ gz.on("data", (c) => chunks.push(c));
4611
+ gz.on("end", () => resolveP(Buffer.concat(chunks)));
4612
+ gz.on("error", rejectP);
4613
+ gz.end(input2);
4614
+ });
4615
+ }
4616
+
4617
+ // src/commands/review.ts
4618
+ import { Command as Command23 } from "commander";
4619
+ import pc24 from "picocolors";
4620
+ var reviewCommand = new Command23("review").description("Admin: review queue for public-visibility uploads");
4621
+ reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
4622
+ const server = resolveServerUrl(opts.server);
4623
+ const token = resolveToken(opts.token);
4624
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4625
+ const res = await fetch(`${server}/api/admin/review-queue`, { headers: authHeaders(token) });
4626
+ if (!res.ok) {
4627
+ const body = await res.text().catch(() => "");
4628
+ return die(`request failed (${res.status}): ${body}`);
4629
+ }
4630
+ const data = await res.json();
4631
+ if (!data.pending?.length) {
4632
+ console.log(pc24.dim("queue empty"));
4633
+ return;
4634
+ }
4635
+ for (const i of data.pending) {
4636
+ console.log(pc24.bold(i.id), pc24.cyan(i.appName), pc24.dim(`v${i.version}`));
4637
+ console.log(pc24.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4638
+ console.log(pc24.dim(` uploaded: ${i.uploadedAt}`));
4639
+ const desc = i.manifest?.description;
4640
+ if (desc) console.log(pc24.dim(` description: ${desc}`));
4641
+ console.log();
4642
+ }
4643
+ });
4644
+ 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));
4645
+ 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));
4646
+ async function decide(id, decision, opts) {
4647
+ const server = resolveServerUrl(opts.server);
4648
+ const token = resolveToken(opts.token);
4649
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4650
+ const res = await fetch(`${server}/api/admin/review/${encodeURIComponent(id)}`, {
4651
+ method: "POST",
4652
+ headers: authHeaders(token),
4653
+ body: JSON.stringify({ decision, notes: opts.notes })
4654
+ });
4655
+ if (!res.ok) {
4656
+ const body = await res.text().catch(() => "");
4657
+ return die(`request failed (${res.status}): ${body}`);
4658
+ }
4659
+ const data = await res.json();
4660
+ if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
4661
+ console.log(pc24.green("\u2713"), `${decision}d`, data.install?.appName, pc24.dim(`(${data.install?.id})`));
4662
+ if (data.install?.reviewNotes) console.log(pc24.dim(` notes: ${data.install.reviewNotes}`));
4663
+ }
4664
+ function die(msg) {
4665
+ console.error(pc24.red(msg));
4666
+ process.exitCode = 1;
4667
+ }
4668
+
3346
4669
  // src/index.ts
3347
- var version = "0.2.3";
3348
- 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");
4670
+ var version = "0.2.5";
4671
+ 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");
3349
4672
  program.addCommand(launchCommand);
3350
4673
  program.addCommand(runCommand);
3351
4674
  program.addCommand(appsCommand);
@@ -3356,10 +4679,14 @@ program.addCommand(loginCommand);
3356
4679
  program.addCommand(agentCommand);
3357
4680
  program.addCommand(pairingCommand);
3358
4681
  program.addCommand(daemonCommand);
4682
+ program.addCommand(initCommand);
3359
4683
  program.addCommand(installCommand);
3360
4684
  program.addCommand(uninstallCommand);
3361
4685
  program.addCommand(reloadCommand);
3362
4686
  program.addCommand(configCommand);
4687
+ program.addCommand(simulateCommand);
4688
+ program.addCommand(publishCommand);
4689
+ program.addCommand(reviewCommand);
3363
4690
  program.addCommand(serverCommand);
3364
4691
  program.addCommand(updateCommand);
3365
4692
  program.addCommand(devCommand, { hidden: true });
@@ -3403,29 +4730,29 @@ To discover what params a command accepts:
3403
4730
  program.on("command:*", (operands) => {
3404
4731
  const first = operands[0];
3405
4732
  const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
3406
- const topLevelHint = `Run ${pc21.cyan("bot --help")} for the list of top-level commands.`;
4733
+ const topLevelHint = `Run ${pc25.cyan("bot --help")} for the list of top-level commands.`;
3407
4734
  const argv = process.argv.slice(2);
3408
4735
  const firstIdx = argv.indexOf(first);
3409
4736
  const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
3410
4737
  if (tail.length > 0) {
3411
4738
  const suggested = `bot run ${first} ${tail.join(" ")}`;
3412
4739
  console.error(
3413
- pc21.red(`error: unknown command '${first}'`) + `
4740
+ pc25.red(`error: unknown command '${first}'`) + `
3414
4741
 
3415
- App commands go through ${pc21.bold("bot run")}. Did you mean:
4742
+ App commands go through ${pc25.bold("bot run")}. Did you mean:
3416
4743
 
3417
- ${pc21.cyan(suggested)}
4744
+ ${pc25.cyan(suggested)}
3418
4745
 
3419
4746
  ${topLevelHint}`
3420
4747
  );
3421
4748
  process.exit(1);
3422
4749
  }
3423
4750
  console.error(
3424
- pc21.red(`error: unknown command '${first}'`) + `
4751
+ pc25.red(`error: unknown command '${first}'`) + `
3425
4752
 
3426
4753
  If '${first}' is an app name, invoke one of its commands with:
3427
- ${pc21.cyan(`bot run ${first} <command> [--key value ...]`)}
3428
- ${pc21.cyan("bot apps --json")} ${pc21.dim("(to see what commands exist)")}
4754
+ ${pc25.cyan(`bot run ${first} <command> [--key value ...]`)}
4755
+ ${pc25.cyan("bot apps --json")} ${pc25.dim("(to see what commands exist)")}
3429
4756
 
3430
4757
  Top-level commands: ${known.join(", ")}
3431
4758
  ${topLevelHint}`