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