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