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 +1457 -130
- package/dist/bin/bot.js.map +1 -1
- package/dist/index.js +1458 -130
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
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
|
|
3
|
-
import
|
|
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
|
|
155
|
-
import { randomUUID } from "crypto";
|
|
156
|
-
import { existsSync as
|
|
157
|
-
import { homedir as
|
|
158
|
-
import { join as
|
|
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 =
|
|
532
|
-
if (!
|
|
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 =
|
|
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((
|
|
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 =
|
|
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((
|
|
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 =
|
|
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((
|
|
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-${
|
|
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:
|
|
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 =
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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 (!
|
|
1200
|
-
return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) =>
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1977
|
+
return join5(homedir5(), ".hermes", "sessions");
|
|
1407
1978
|
}
|
|
1408
1979
|
function hermesSessionFile(sessionDir, sessionId) {
|
|
1409
|
-
return
|
|
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 (
|
|
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
|
|
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 (!
|
|
1473
|
-
return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) =>
|
|
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
|
|
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 ??
|
|
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 =
|
|
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((
|
|
1581
|
-
pending.set(id, { resolve:
|
|
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.
|
|
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
|
|
1698
|
-
if (path.startsWith("~/")) return
|
|
1699
|
-
return
|
|
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
|
|
1756
|
-
import { resolve as
|
|
1757
|
-
import { existsSync as
|
|
1758
|
-
import { homedir as
|
|
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
|
|
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((
|
|
2432
|
+
const callbackPromise = new Promise((resolve11, reject) => {
|
|
1862
2433
|
resolveCb = (p) => {
|
|
1863
2434
|
if (settled) return;
|
|
1864
2435
|
settled = true;
|
|
1865
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
|
2577
|
+
if (!raw) return resolve11(null);
|
|
2007
2578
|
try {
|
|
2008
|
-
|
|
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
|
-
|
|
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
|
|
2027
|
-
import { homedir as
|
|
2028
|
-
import { join as
|
|
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
|
|
2601
|
+
return join6(homedir6(), ".botapp", "daemon.pid");
|
|
2031
2602
|
}
|
|
2032
2603
|
function isDaemonRunningLocally(pidFile = daemonPidFile()) {
|
|
2033
|
-
if (!
|
|
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 =
|
|
2202
|
-
const pidFile =
|
|
2203
|
-
const logFile =
|
|
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 || !
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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 (
|
|
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
|
|
2544
|
-
import { existsSync as
|
|
2545
|
-
import { homedir as
|
|
2546
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
2551
|
-
if (!
|
|
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 =
|
|
2557
|
-
if (!
|
|
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 =
|
|
3142
|
+
const targetDir = join8(APPS_DIR, appName);
|
|
2572
3143
|
mkdirSync4(APPS_DIR, { recursive: true });
|
|
2573
|
-
if (
|
|
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
|
|
2593
|
-
import { homedir as
|
|
2594
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
2599
|
-
if (!
|
|
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
|
|
2612
|
-
import { existsSync as
|
|
2613
|
-
import { homedir as
|
|
2614
|
-
import { spawn as
|
|
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 =
|
|
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 =
|
|
2619
|
-
if (!
|
|
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 =
|
|
2625
|
-
if (!
|
|
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 =
|
|
3210
|
+
const targetDir = join10(APPS_DIR3, appName);
|
|
2640
3211
|
mkdirSync5(APPS_DIR3, { recursive: true });
|
|
2641
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
2689
|
-
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
|
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 =
|
|
3316
|
-
const code = await new Promise((
|
|
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
|
-
|
|
3891
|
+
resolve11(127);
|
|
3320
3892
|
});
|
|
3321
|
-
child.once("close",
|
|
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
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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(
|
|
3340
|
-
return needles.some((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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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.
|
|
3358
|
-
var program = new
|
|
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 ${
|
|
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
|
-
|
|
4751
|
+
pc25.red(`error: unknown command '${first}'`) + `
|
|
3424
4752
|
|
|
3425
|
-
App commands go through ${
|
|
4753
|
+
App commands go through ${pc25.bold("bot run")}. Did you mean:
|
|
3426
4754
|
|
|
3427
|
-
${
|
|
4755
|
+
${pc25.cyan(suggested)}
|
|
3428
4756
|
|
|
3429
4757
|
${topLevelHint}`
|
|
3430
4758
|
);
|
|
3431
4759
|
process.exit(1);
|
|
3432
4760
|
}
|
|
3433
4761
|
console.error(
|
|
3434
|
-
|
|
4762
|
+
pc25.red(`error: unknown command '${first}'`) + `
|
|
3435
4763
|
|
|
3436
4764
|
If '${first}' is an app name, invoke one of its commands with:
|
|
3437
|
-
${
|
|
3438
|
-
${
|
|
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}`
|