@suzuke/agend 0.0.1 → 1.0.2
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/README.md +78 -0
- package/README.zh-TW.md +79 -0
- package/dist/access-path.d.ts +7 -0
- package/dist/access-path.js +12 -0
- package/dist/access-path.js.map +1 -0
- package/dist/backend/claude-code.d.ts +13 -0
- package/dist/backend/claude-code.js +114 -0
- package/dist/backend/claude-code.js.map +1 -0
- package/dist/backend/codex.d.ts +10 -0
- package/dist/backend/codex.js +58 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +2 -0
- package/dist/backend/factory.js +19 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/gemini-cli.d.ts +10 -0
- package/dist/backend/gemini-cli.js +68 -0
- package/dist/backend/gemini-cli.js.map +1 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +6 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/opencode.d.ts +10 -0
- package/dist/backend/opencode.js +63 -0
- package/dist/backend/opencode.js.map +1 -0
- package/dist/backend/types.d.ts +26 -0
- package/dist/backend/types.js +2 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/channel/access-manager.d.ts +18 -0
- package/dist/channel/access-manager.js +149 -0
- package/dist/channel/access-manager.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +45 -0
- package/dist/channel/adapters/discord.js +366 -0
- package/dist/channel/adapters/discord.js.map +1 -0
- package/dist/channel/adapters/telegram.d.ts +58 -0
- package/dist/channel/adapters/telegram.js +569 -0
- package/dist/channel/adapters/telegram.js.map +1 -0
- package/dist/channel/attachment-handler.d.ts +15 -0
- package/dist/channel/attachment-handler.js +55 -0
- package/dist/channel/attachment-handler.js.map +1 -0
- package/dist/channel/factory.d.ts +12 -0
- package/dist/channel/factory.js +38 -0
- package/dist/channel/factory.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +26 -0
- package/dist/channel/ipc-bridge.js +170 -0
- package/dist/channel/ipc-bridge.js.map +1 -0
- package/dist/channel/mcp-server.d.ts +10 -0
- package/dist/channel/mcp-server.js +196 -0
- package/dist/channel/mcp-server.js.map +1 -0
- package/dist/channel/mcp-tools.d.ts +909 -0
- package/dist/channel/mcp-tools.js +346 -0
- package/dist/channel/mcp-tools.js.map +1 -0
- package/dist/channel/message-bus.d.ts +17 -0
- package/dist/channel/message-bus.js +86 -0
- package/dist/channel/message-bus.js.map +1 -0
- package/dist/channel/message-queue.d.ts +39 -0
- package/dist/channel/message-queue.js +248 -0
- package/dist/channel/message-queue.js.map +1 -0
- package/dist/channel/tool-router.d.ts +6 -0
- package/dist/channel/tool-router.js +69 -0
- package/dist/channel/tool-router.js.map +1 -0
- package/dist/channel/tool-tracker.d.ts +13 -0
- package/dist/channel/tool-tracker.js +58 -0
- package/dist/channel/tool-tracker.js.map +1 -0
- package/dist/channel/types.d.ts +116 -0
- package/dist/channel/types.js +2 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +782 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/context-guardian.d.ts +29 -0
- package/dist/context-guardian.js +123 -0
- package/dist/context-guardian.js.map +1 -0
- package/dist/cost-guard.d.ts +21 -0
- package/dist/cost-guard.js +113 -0
- package/dist/cost-guard.js.map +1 -0
- package/dist/daemon-entry.d.ts +1 -0
- package/dist/daemon-entry.js +29 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/daemon.d.ts +88 -0
- package/dist/daemon.js +821 -0
- package/dist/daemon.js.map +1 -0
- package/dist/daily-summary.d.ts +13 -0
- package/dist/daily-summary.js +55 -0
- package/dist/daily-summary.js.map +1 -0
- package/dist/event-log.d.ts +22 -0
- package/dist/event-log.js +66 -0
- package/dist/event-log.js.map +1 -0
- package/dist/export-import.d.ts +2 -0
- package/dist/export-import.js +110 -0
- package/dist/export-import.js.map +1 -0
- package/dist/fleet-context.d.ts +36 -0
- package/dist/fleet-context.js +4 -0
- package/dist/fleet-context.js.map +1 -0
- package/dist/fleet-manager.d.ts +115 -0
- package/dist/fleet-manager.js +1739 -0
- package/dist/fleet-manager.js.map +1 -0
- package/dist/fleet-system-prompt.d.ts +11 -0
- package/dist/fleet-system-prompt.js +60 -0
- package/dist/fleet-system-prompt.js.map +1 -0
- package/dist/hang-detector.d.ts +16 -0
- package/dist/hang-detector.js +53 -0
- package/dist/hang-detector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +63 -0
- package/dist/logger.js.map +1 -0
- package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
- package/dist/scheduler/db.d.ts +16 -0
- package/dist/scheduler/db.js +132 -0
- package/dist/scheduler/db.js.map +1 -0
- package/dist/scheduler/db.test.d.ts +1 -0
- package/dist/scheduler/db.test.js +92 -0
- package/dist/scheduler/db.test.js.map +1 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +4 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +25 -0
- package/dist/scheduler/scheduler.js +119 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/scheduler.test.d.ts +1 -0
- package/dist/scheduler/scheduler.test.js +119 -0
- package/dist/scheduler/scheduler.test.js.map +1 -0
- package/dist/scheduler/types.d.ts +47 -0
- package/dist/scheduler/types.js +7 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service-installer.d.ts +14 -0
- package/dist/service-installer.js +91 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/setup-wizard.d.ts +14 -0
- package/dist/setup-wizard.js +517 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/stt.d.ts +10 -0
- package/dist/stt.js +33 -0
- package/dist/stt.js.map +1 -0
- package/dist/tmux-manager.d.ts +22 -0
- package/dist/tmux-manager.js +132 -0
- package/dist/tmux-manager.js.map +1 -0
- package/dist/topic-commands.d.ts +22 -0
- package/dist/topic-commands.js +176 -0
- package/dist/topic-commands.js.map +1 -0
- package/dist/transcript-monitor.d.ts +21 -0
- package/dist/transcript-monitor.js +149 -0
- package/dist/transcript-monitor.js.map +1 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook-emitter.d.ts +15 -0
- package/dist/webhook-emitter.js +41 -0
- package/dist/webhook-emitter.js.map +1 -0
- package/package.json +58 -4
- package/templates/launchd.plist.ejs +29 -0
- package/templates/systemd.service.ejs +15 -0
- package/index.js +0 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { SchedulerDb } from "./scheduler/db.js";
|
|
5
|
+
import { Cron } from "croner";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, rmSync, } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const DATA_DIR = join(homedir(), ".agend");
|
|
13
|
+
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
14
|
+
const program = new Command();
|
|
15
|
+
// Read version from package.json at build time
|
|
16
|
+
const pkgVersion = (() => {
|
|
17
|
+
try {
|
|
18
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
19
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
program
|
|
26
|
+
.name("agend")
|
|
27
|
+
.description("Claude Channel Daemon")
|
|
28
|
+
.version(pkgVersion);
|
|
29
|
+
function signalFleetReload() {
|
|
30
|
+
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
31
|
+
try {
|
|
32
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
33
|
+
process.kill(pid, "SIGHUP");
|
|
34
|
+
console.log("Fleet manager notified to reload schedules.");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
console.log("Fleet manager not running. Schedules will be loaded on next start.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// === Fleet commands ===
|
|
41
|
+
const fleet = program.command("fleet").description("Fleet management");
|
|
42
|
+
fleet
|
|
43
|
+
.command("start")
|
|
44
|
+
.description("Start fleet or specific instance")
|
|
45
|
+
.argument("[instance]", "Specific instance to start")
|
|
46
|
+
.action(async (instance) => {
|
|
47
|
+
const { FleetManager } = await import("./fleet-manager.js");
|
|
48
|
+
const fm = new FleetManager(DATA_DIR);
|
|
49
|
+
if (instance) {
|
|
50
|
+
const config = fm.loadConfig(FLEET_CONFIG_PATH);
|
|
51
|
+
const inst = config.instances[instance];
|
|
52
|
+
if (!inst) {
|
|
53
|
+
console.error(`Instance "${instance}" not found in fleet config`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const topicMode = config.channel?.mode === "topic";
|
|
57
|
+
await fm.startInstance(instance, inst, topicMode);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
await fm.startAll(FLEET_CONFIG_PATH);
|
|
61
|
+
}
|
|
62
|
+
console.log("Fleet started");
|
|
63
|
+
// Keep process alive + clean shutdown on Ctrl+C
|
|
64
|
+
const shutdown = async () => {
|
|
65
|
+
console.log("\nStopping fleet...");
|
|
66
|
+
await fm.stopAll();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
};
|
|
69
|
+
process.on("SIGINT", shutdown);
|
|
70
|
+
process.on("SIGTERM", shutdown);
|
|
71
|
+
process.on("uncaughtException", async (err) => {
|
|
72
|
+
console.error("Uncaught exception:", err);
|
|
73
|
+
await fm.stopAll().catch(() => { });
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
process.on("unhandledRejection", async (err) => {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
// 409 = another bot poller exists — adapter handles retry, don't crash
|
|
79
|
+
if (msg.includes("409") && msg.includes("getUpdates")) {
|
|
80
|
+
console.error("Bot polling conflict (409) — retrying...");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.error("Unhandled rejection:", err);
|
|
84
|
+
await fm.stopAll().catch(() => { });
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
fleet
|
|
89
|
+
.command("stop")
|
|
90
|
+
.description("Stop fleet or specific instance")
|
|
91
|
+
.argument("[instance]", "Specific instance to stop")
|
|
92
|
+
.action(async (instance) => {
|
|
93
|
+
if (instance) {
|
|
94
|
+
const { FleetManager } = await import("./fleet-manager.js");
|
|
95
|
+
const fm = new FleetManager(DATA_DIR);
|
|
96
|
+
await fm.stopInstance(instance);
|
|
97
|
+
console.log("Stopped");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
101
|
+
if (!existsSync(pidPath)) {
|
|
102
|
+
console.error("Fleet is not running (no PID file found)");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, "SIGTERM");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
console.error("Failed to send SIGTERM (process may have already exited)");
|
|
111
|
+
try {
|
|
112
|
+
unlinkSync(pidPath);
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
// Wait for process exit (up to 10 seconds)
|
|
118
|
+
const deadline = Date.now() + 10_000;
|
|
119
|
+
while (Date.now() < deadline) {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
await new Promise(r => setTimeout(r, 200));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Process exited
|
|
126
|
+
console.log("Fleet stopped");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.warn("Warning: fleet process still running after 10s");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
fleet
|
|
134
|
+
.command("restart")
|
|
135
|
+
.description("Graceful restart: wait for instances to idle, then restart")
|
|
136
|
+
.option("--reload", "Full process restart to load new code")
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
139
|
+
if (!existsSync(pidPath)) {
|
|
140
|
+
console.error("Fleet is not running (no PID file found)");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
144
|
+
if (opts.reload) {
|
|
145
|
+
// Check if managed by launchd — if so, just signal and let launchd restart
|
|
146
|
+
let managedByLaunchd = false;
|
|
147
|
+
try {
|
|
148
|
+
const ppid = parseInt(execSync(`ps -o ppid= -p ${pid}`).toString().trim(), 10);
|
|
149
|
+
managedByLaunchd = ppid === 1;
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
try {
|
|
153
|
+
process.kill(pid, "SIGUSR1");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
console.error("Failed to send reload signal (process may have exited)");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (managedByLaunchd) {
|
|
160
|
+
console.log("Fleet is managed by launchd — sent reload signal.");
|
|
161
|
+
console.log("launchd will automatically restart with new code.");
|
|
162
|
+
// Wait briefly for old process to exit, then confirm new one started
|
|
163
|
+
const deadline = Date.now() + 6 * 60 * 1000;
|
|
164
|
+
while (Date.now() < deadline) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(pid, 0);
|
|
167
|
+
await new Promise(r => setTimeout(r, 500));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Wait for launchd to start new process
|
|
174
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
175
|
+
if (existsSync(pidPath)) {
|
|
176
|
+
const newPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
177
|
+
if (newPid !== pid) {
|
|
178
|
+
console.log(`New fleet process started (PID ${newPid})`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
console.log("Full restart signal sent — waiting for fleet to stop...");
|
|
184
|
+
// Wait for old process to exit (up to 6 minutes: 5 min idle wait + buffer)
|
|
185
|
+
const deadline = Date.now() + 6 * 60 * 1000;
|
|
186
|
+
while (Date.now() < deadline) {
|
|
187
|
+
try {
|
|
188
|
+
process.kill(pid, 0); // check if alive
|
|
189
|
+
await new Promise(r => setTimeout(r, 500));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
break; // process exited
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Verify it actually exited
|
|
196
|
+
try {
|
|
197
|
+
process.kill(pid, 0);
|
|
198
|
+
console.error("Old fleet process still running after timeout — aborting");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Good, it exited
|
|
203
|
+
}
|
|
204
|
+
console.log("Old fleet stopped. Starting with new code...");
|
|
205
|
+
// Start new fleet in this process (new Node.js process = new code loaded)
|
|
206
|
+
const { FleetManager } = await import("./fleet-manager.js");
|
|
207
|
+
const fm = new FleetManager(DATA_DIR);
|
|
208
|
+
await fm.startAll(FLEET_CONFIG_PATH);
|
|
209
|
+
console.log("Fleet restarted with new code");
|
|
210
|
+
// Keep process alive (same as fleet start)
|
|
211
|
+
const shutdown = async () => {
|
|
212
|
+
console.log("\nStopping fleet...");
|
|
213
|
+
await fm.stopAll();
|
|
214
|
+
process.exit(0);
|
|
215
|
+
};
|
|
216
|
+
process.on("SIGINT", shutdown);
|
|
217
|
+
process.on("SIGTERM", shutdown);
|
|
218
|
+
process.on("uncaughtException", async (err) => {
|
|
219
|
+
console.error("Uncaught exception:", err);
|
|
220
|
+
await fm.stopAll().catch(() => { });
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
|
223
|
+
process.on("unhandledRejection", async (err) => {
|
|
224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
225
|
+
if (msg.includes("409") && msg.includes("getUpdates")) {
|
|
226
|
+
console.error("Bot polling conflict (409) — retrying...");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.error("Unhandled rejection:", err);
|
|
230
|
+
await fm.stopAll().catch(() => { });
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Instance-only restart (existing behavior)
|
|
236
|
+
try {
|
|
237
|
+
process.kill(pid, "SIGUSR2");
|
|
238
|
+
console.log("Graceful restart signal sent — fleet will restart when all instances are idle");
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
console.error("Failed to send restart signal (process may have exited)");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
fleet
|
|
247
|
+
.command("status")
|
|
248
|
+
.description("Show fleet status")
|
|
249
|
+
.action(async () => {
|
|
250
|
+
const { FleetManager } = await import("./fleet-manager.js");
|
|
251
|
+
const fm = new FleetManager(DATA_DIR);
|
|
252
|
+
const config = fm.loadConfig(FLEET_CONFIG_PATH);
|
|
253
|
+
const names = Object.keys(config.instances);
|
|
254
|
+
const nameWidth = Math.max(20, ...names.map(n => n.length + 2));
|
|
255
|
+
console.log("Instance".padEnd(nameWidth) + "Status".padEnd(10) + "Context".padEnd(10) + "Cost".padEnd(10) + "Topic");
|
|
256
|
+
console.log("\u2500".repeat(nameWidth + 40));
|
|
257
|
+
for (const [name, inst] of Object.entries(config.instances)) {
|
|
258
|
+
const status = fm.getInstanceStatus(name);
|
|
259
|
+
const topic = inst.topic_id ? `#${inst.topic_id}` : "(DM)";
|
|
260
|
+
// Read statusline.json for context usage and cost
|
|
261
|
+
let contextStr = "-";
|
|
262
|
+
let costStr = "-";
|
|
263
|
+
const statusFile = join(DATA_DIR, "instances", name, "statusline.json");
|
|
264
|
+
try {
|
|
265
|
+
if (existsSync(statusFile)) {
|
|
266
|
+
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
267
|
+
if (data.context_window?.used_percentage != null) {
|
|
268
|
+
contextStr = `${Math.round(data.context_window.used_percentage)}%`;
|
|
269
|
+
}
|
|
270
|
+
if (data.cost?.total_cost_usd != null) {
|
|
271
|
+
costStr = `$${data.cost.total_cost_usd.toFixed(2)}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch { /* ignore read errors */ }
|
|
276
|
+
console.log(name.padEnd(nameWidth) +
|
|
277
|
+
status.padEnd(10) +
|
|
278
|
+
contextStr.padEnd(10) +
|
|
279
|
+
costStr.padEnd(10) +
|
|
280
|
+
topic);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
fleet
|
|
284
|
+
.command("logs")
|
|
285
|
+
.description("Show instance logs")
|
|
286
|
+
.argument("<instance>", "Instance name")
|
|
287
|
+
.option("-n, --lines <count>", "Number of lines to show", "50")
|
|
288
|
+
.action((instance, opts) => {
|
|
289
|
+
const logPath = join(DATA_DIR, "instances", instance, "daemon.log");
|
|
290
|
+
if (!existsSync(logPath)) {
|
|
291
|
+
console.error(`No logs found for instance "${instance}"`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const content = readFileSync(logPath, "utf-8");
|
|
295
|
+
const lines = content.trim().split("\n");
|
|
296
|
+
const n = parseInt(opts.lines, 10);
|
|
297
|
+
console.log(lines.slice(-n).join("\n"));
|
|
298
|
+
});
|
|
299
|
+
fleet
|
|
300
|
+
.command("history")
|
|
301
|
+
.description("Show fleet event history")
|
|
302
|
+
.option("--instance <name>", "Filter by instance name")
|
|
303
|
+
.option("--type <type>", "Filter by event type")
|
|
304
|
+
.option("--since <date>", "Filter events since date (ISO format)")
|
|
305
|
+
.option("--limit <n>", "Number of events to show", "50")
|
|
306
|
+
.option("--json", "Output as JSON")
|
|
307
|
+
.action(async (opts) => {
|
|
308
|
+
const { EventLog } = await import("./event-log.js");
|
|
309
|
+
const evLog = new EventLog(join(DATA_DIR, "events.db"));
|
|
310
|
+
try {
|
|
311
|
+
const rows = evLog.query({
|
|
312
|
+
instance: opts.instance,
|
|
313
|
+
type: opts.type,
|
|
314
|
+
since: opts.since,
|
|
315
|
+
limit: parseInt(opts.limit, 10),
|
|
316
|
+
});
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (rows.length === 0) {
|
|
322
|
+
console.log("No events found.");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const instWidth = Math.max(20, ...rows.map(r => r.instance_name.length + 2));
|
|
326
|
+
console.log("Time".padEnd(22) + "Instance".padEnd(instWidth) + "Type".padEnd(25) + "Payload");
|
|
327
|
+
console.log("\u2500".repeat(22 + instWidth + 25 + 23));
|
|
328
|
+
for (const r of rows) {
|
|
329
|
+
const payloadStr = r.payload != null ? JSON.stringify(r.payload) : "";
|
|
330
|
+
console.log(r.created_at.padEnd(22) +
|
|
331
|
+
r.instance_name.padEnd(instWidth) +
|
|
332
|
+
r.event_type.padEnd(25) +
|
|
333
|
+
payloadStr);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
evLog.close();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
fleet
|
|
341
|
+
.command("cleanup")
|
|
342
|
+
.description("Remove orphaned instance directories not in fleet.yaml")
|
|
343
|
+
.option("--dry-run", "List orphans without deleting")
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
const { FleetManager } = await import("./fleet-manager.js");
|
|
346
|
+
const fm = new FleetManager(DATA_DIR);
|
|
347
|
+
const config = fm.loadConfig(FLEET_CONFIG_PATH);
|
|
348
|
+
const configuredNames = new Set(Object.keys(config.instances));
|
|
349
|
+
const instancesDir = join(DATA_DIR, "instances");
|
|
350
|
+
if (!existsSync(instancesDir)) {
|
|
351
|
+
console.log("No instances directory.");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const dirs = readdirSync(instancesDir).filter(d => !configuredNames.has(d));
|
|
355
|
+
if (dirs.length === 0) {
|
|
356
|
+
console.log("No orphaned directories.");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
console.log(`Found ${dirs.length} orphaned instance directories:`);
|
|
360
|
+
for (const d of dirs)
|
|
361
|
+
console.log(` ${d}`);
|
|
362
|
+
if (opts.dryRun)
|
|
363
|
+
return;
|
|
364
|
+
for (const d of dirs) {
|
|
365
|
+
rmSync(join(instancesDir, d), { recursive: true, force: true });
|
|
366
|
+
console.log(` Removed: ${d}`);
|
|
367
|
+
}
|
|
368
|
+
console.log(`Cleaned up ${dirs.length} directories.`);
|
|
369
|
+
// Clean stale files from active instances
|
|
370
|
+
const staleFiles = ["memory.db", "sandbox-bash"];
|
|
371
|
+
let staleCount = 0;
|
|
372
|
+
for (const name of configuredNames) {
|
|
373
|
+
const instDir = join(instancesDir, name);
|
|
374
|
+
for (const f of staleFiles) {
|
|
375
|
+
const p = join(instDir, f);
|
|
376
|
+
if (existsSync(p)) {
|
|
377
|
+
if (!opts.dryRun)
|
|
378
|
+
rmSync(p, { force: true });
|
|
379
|
+
staleCount++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (staleCount > 0)
|
|
384
|
+
console.log(`Removed ${staleCount} stale files (memory.db, sandbox-bash).`);
|
|
385
|
+
});
|
|
386
|
+
// === Topic commands ===
|
|
387
|
+
const topic = program.command("topic").description("Topic binding management");
|
|
388
|
+
topic
|
|
389
|
+
.command("list")
|
|
390
|
+
.description("List topic bindings")
|
|
391
|
+
.action(async () => {
|
|
392
|
+
const { loadFleetConfig } = await import("./config.js");
|
|
393
|
+
const config = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
394
|
+
let found = false;
|
|
395
|
+
for (const [name, inst] of Object.entries(config.instances)) {
|
|
396
|
+
if (inst.topic_id != null) {
|
|
397
|
+
console.log(`${name} \u2192 topic #${inst.topic_id}`);
|
|
398
|
+
found = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!found) {
|
|
402
|
+
console.log("No topic bindings configured");
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
topic
|
|
406
|
+
.command("bind")
|
|
407
|
+
.description("Bind an instance to a topic")
|
|
408
|
+
.argument("<instance>", "Instance name")
|
|
409
|
+
.argument("<topic-id>", "Topic ID")
|
|
410
|
+
.action(async (instance, topicId) => {
|
|
411
|
+
const { loadFleetConfig } = await import("./config.js");
|
|
412
|
+
const yaml = await import("js-yaml");
|
|
413
|
+
const config = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
414
|
+
if (!config.instances[instance]) {
|
|
415
|
+
console.error(`Instance "${instance}" not found in fleet config`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
config.instances[instance].topic_id = parseInt(topicId, 10);
|
|
419
|
+
writeFileSync(FLEET_CONFIG_PATH, yaml.dump(config));
|
|
420
|
+
console.log(`Bound ${instance} \u2192 topic #${topicId}`);
|
|
421
|
+
});
|
|
422
|
+
topic
|
|
423
|
+
.command("unbind")
|
|
424
|
+
.description("Unbind an instance from its topic")
|
|
425
|
+
.argument("<instance>", "Instance name")
|
|
426
|
+
.action(async (instance) => {
|
|
427
|
+
const { loadFleetConfig } = await import("./config.js");
|
|
428
|
+
const yaml = await import("js-yaml");
|
|
429
|
+
const config = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
430
|
+
if (!config.instances[instance]) {
|
|
431
|
+
console.error(`Instance "${instance}" not found in fleet config`);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
delete config.instances[instance].topic_id;
|
|
435
|
+
writeFileSync(FLEET_CONFIG_PATH, yaml.dump(config));
|
|
436
|
+
console.log(`Unbound ${instance} from topic`);
|
|
437
|
+
});
|
|
438
|
+
// === Access commands ===
|
|
439
|
+
const access = program
|
|
440
|
+
.command("access")
|
|
441
|
+
.description("Access control for instances");
|
|
442
|
+
async function resolveAccessPath(instance) {
|
|
443
|
+
const { loadFleetConfig } = await import("./config.js");
|
|
444
|
+
const { resolveAccessPathFromConfig } = await import("./access-path.js");
|
|
445
|
+
const config = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
446
|
+
const inst = config.instances[instance];
|
|
447
|
+
return resolveAccessPathFromConfig(DATA_DIR, instance, config.channel);
|
|
448
|
+
}
|
|
449
|
+
access
|
|
450
|
+
.command("lock")
|
|
451
|
+
.description("Lock instance access")
|
|
452
|
+
.argument("<instance>", "Instance name")
|
|
453
|
+
.action(async (instance) => {
|
|
454
|
+
const { AccessManager } = await import("./channel/access-manager.js");
|
|
455
|
+
const instanceDir = join(DATA_DIR, "instances", instance);
|
|
456
|
+
if (!existsSync(instanceDir)) {
|
|
457
|
+
console.error(`Instance "${instance}" not found`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
const statePath = await resolveAccessPath(instance);
|
|
461
|
+
const am = new AccessManager({ mode: "locked", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
|
|
462
|
+
am.setMode("locked");
|
|
463
|
+
console.log(`${instance}: locked`);
|
|
464
|
+
});
|
|
465
|
+
access
|
|
466
|
+
.command("unlock")
|
|
467
|
+
.description("Unlock instance access")
|
|
468
|
+
.argument("<instance>", "Instance name")
|
|
469
|
+
.action(async (instance) => {
|
|
470
|
+
const { AccessManager } = await import("./channel/access-manager.js");
|
|
471
|
+
const instanceDir = join(DATA_DIR, "instances", instance);
|
|
472
|
+
if (!existsSync(instanceDir)) {
|
|
473
|
+
console.error(`Instance "${instance}" not found`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const statePath = await resolveAccessPath(instance);
|
|
477
|
+
const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
|
|
478
|
+
am.setMode("pairing");
|
|
479
|
+
console.log(`${instance}: unlocked`);
|
|
480
|
+
});
|
|
481
|
+
access
|
|
482
|
+
.command("list")
|
|
483
|
+
.description("List allowed users for an instance")
|
|
484
|
+
.argument("<instance>", "Instance name")
|
|
485
|
+
.action(async (instance) => {
|
|
486
|
+
const { AccessManager } = await import("./channel/access-manager.js");
|
|
487
|
+
const instanceDir = join(DATA_DIR, "instances", instance);
|
|
488
|
+
if (!existsSync(instanceDir)) {
|
|
489
|
+
console.error(`Instance "${instance}" not found`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
const statePath = await resolveAccessPath(instance);
|
|
493
|
+
const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
|
|
494
|
+
const users = am.getAllowedUsers();
|
|
495
|
+
if (users.length === 0) {
|
|
496
|
+
console.log(`${instance}: no allowed users`);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
console.log(`${instance} allowed users:`);
|
|
500
|
+
for (const uid of users) {
|
|
501
|
+
console.log(` - ${uid}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
access
|
|
506
|
+
.command("remove")
|
|
507
|
+
.description("Remove a user from allowed list")
|
|
508
|
+
.argument("<instance>", "Instance name")
|
|
509
|
+
.argument("<user-id>", "User ID to remove")
|
|
510
|
+
.action(async (instance, userId) => {
|
|
511
|
+
const { AccessManager } = await import("./channel/access-manager.js");
|
|
512
|
+
const instanceDir = join(DATA_DIR, "instances", instance);
|
|
513
|
+
if (!existsSync(instanceDir)) {
|
|
514
|
+
console.error(`Instance "${instance}" not found`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
const statePath = await resolveAccessPath(instance);
|
|
518
|
+
const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
|
|
519
|
+
am.removeUser(parseInt(userId, 10));
|
|
520
|
+
console.log(`${instance}: removed user ${userId}`);
|
|
521
|
+
});
|
|
522
|
+
access
|
|
523
|
+
.command("pair")
|
|
524
|
+
.description("Generate a pairing code for a user")
|
|
525
|
+
.argument("<instance>", "Instance name")
|
|
526
|
+
.argument("<user-id>", "Telegram user ID requesting pairing")
|
|
527
|
+
.action(async (instance, userId) => {
|
|
528
|
+
const { AccessManager } = await import("./channel/access-manager.js");
|
|
529
|
+
const instanceDir = join(DATA_DIR, "instances", instance);
|
|
530
|
+
if (!existsSync(instanceDir)) {
|
|
531
|
+
console.error(`Instance "${instance}" not found`);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
const statePath = await resolveAccessPath(instance);
|
|
535
|
+
const am = new AccessManager({ mode: "pairing", allowed_users: [], max_pending_codes: 5, code_expiry_minutes: 10 }, statePath);
|
|
536
|
+
const code = am.generateCode(parseInt(userId, 10));
|
|
537
|
+
console.log(`${instance}: pairing code = ${code}`);
|
|
538
|
+
console.log("Share this code with the user. It expires in 10 minutes.");
|
|
539
|
+
});
|
|
540
|
+
// === Install/Uninstall ===
|
|
541
|
+
program
|
|
542
|
+
.command("install")
|
|
543
|
+
.description("Install as system service")
|
|
544
|
+
.option("--activate", "Stop manual fleet and load the service immediately")
|
|
545
|
+
.action(async (opts) => {
|
|
546
|
+
const { installService, activateService, detectPlatform } = await import("./service-installer.js");
|
|
547
|
+
const execPath = process.argv[1];
|
|
548
|
+
const svcPath = installService({
|
|
549
|
+
label: "com.agend.fleet",
|
|
550
|
+
execPath,
|
|
551
|
+
path: process.env.PATH,
|
|
552
|
+
workingDirectory: DATA_DIR,
|
|
553
|
+
logPath: join(DATA_DIR, "fleet.log"),
|
|
554
|
+
});
|
|
555
|
+
console.log(`Service installed at: ${svcPath}`);
|
|
556
|
+
if (opts.activate) {
|
|
557
|
+
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
558
|
+
activateService(svcPath, pidPath);
|
|
559
|
+
console.log("Service activated.");
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
const plat = detectPlatform();
|
|
563
|
+
if (plat === "macos") {
|
|
564
|
+
console.log(`Run: launchctl load ${svcPath}`);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
console.log(`Run: systemctl --user enable --now com.agend.fleet`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
program
|
|
572
|
+
.command("uninstall")
|
|
573
|
+
.description("Remove system service")
|
|
574
|
+
.action(async () => {
|
|
575
|
+
const { uninstallService } = await import("./service-installer.js");
|
|
576
|
+
const removed = uninstallService("com.agend.fleet");
|
|
577
|
+
if (removed) {
|
|
578
|
+
console.log("Service uninstalled");
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.log("No service found to uninstall");
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
program
|
|
585
|
+
.command("init")
|
|
586
|
+
.description("Interactive setup wizard")
|
|
587
|
+
.action(async () => {
|
|
588
|
+
const { runSetupWizard } = await import("./setup-wizard.js");
|
|
589
|
+
await runSetupWizard();
|
|
590
|
+
});
|
|
591
|
+
// === Schedule commands ===
|
|
592
|
+
const schedule = program.command("schedule").description("Manage scheduled tasks");
|
|
593
|
+
schedule
|
|
594
|
+
.command("list")
|
|
595
|
+
.description("List all schedules")
|
|
596
|
+
.option("--target <instance>", "Filter by target instance")
|
|
597
|
+
.option("--json", "Output as JSON")
|
|
598
|
+
.action((opts) => {
|
|
599
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
600
|
+
try {
|
|
601
|
+
const schedules = db.list(opts.target);
|
|
602
|
+
if (opts.json) {
|
|
603
|
+
console.log(JSON.stringify(schedules, null, 2));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (schedules.length === 0) {
|
|
607
|
+
console.log("No schedules found.");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
console.log("ID\t\t\t\t\tLabel\t\t\tCron\t\tTarget\tEnabled\tLast Status");
|
|
611
|
+
for (const s of schedules) {
|
|
612
|
+
console.log(`${s.id}\t${s.label ?? "-"}\t${s.cron}\t${s.target}\t${s.enabled ? "✅" : "❌"}\t${s.last_status ?? "-"}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
db.close();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
schedule
|
|
620
|
+
.command("add")
|
|
621
|
+
.description("Add a new schedule")
|
|
622
|
+
.requiredOption("--cron <expr>", "Cron expression")
|
|
623
|
+
.requiredOption("--target <instance>", "Target instance")
|
|
624
|
+
.requiredOption("--message <text>", "Message to send on trigger")
|
|
625
|
+
.option("--label <text>", "Human-readable name")
|
|
626
|
+
.option("--timezone <tz>", "IANA timezone", "Asia/Taipei")
|
|
627
|
+
.action((opts) => {
|
|
628
|
+
// Validate cron expression
|
|
629
|
+
try {
|
|
630
|
+
new Cron(opts.cron, { timezone: opts.timezone });
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
console.error(`Invalid cron expression: ${err.message}`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
637
|
+
try {
|
|
638
|
+
const s = db.create({
|
|
639
|
+
cron: opts.cron,
|
|
640
|
+
message: opts.message,
|
|
641
|
+
source: opts.target,
|
|
642
|
+
target: opts.target,
|
|
643
|
+
reply_chat_id: "",
|
|
644
|
+
reply_thread_id: null,
|
|
645
|
+
label: opts.label,
|
|
646
|
+
timezone: opts.timezone,
|
|
647
|
+
});
|
|
648
|
+
console.log(`Created schedule ${s.id}`);
|
|
649
|
+
signalFleetReload();
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
db.close();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
schedule
|
|
656
|
+
.command("update")
|
|
657
|
+
.description("Update an existing schedule")
|
|
658
|
+
.argument("<id>", "Schedule ID")
|
|
659
|
+
.option("--cron <expr>", "New cron expression")
|
|
660
|
+
.option("--message <text>", "New message")
|
|
661
|
+
.option("--target <instance>", "New target instance")
|
|
662
|
+
.option("--label <text>", "New label")
|
|
663
|
+
.option("--timezone <tz>", "New timezone")
|
|
664
|
+
.option("--enabled <bool>", "Enable/disable (true/false)")
|
|
665
|
+
.action((id, opts) => {
|
|
666
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
667
|
+
try {
|
|
668
|
+
const params = {};
|
|
669
|
+
if (opts.cron)
|
|
670
|
+
params.cron = opts.cron;
|
|
671
|
+
if (opts.message)
|
|
672
|
+
params.message = opts.message;
|
|
673
|
+
if (opts.target)
|
|
674
|
+
params.target = opts.target;
|
|
675
|
+
if (opts.label)
|
|
676
|
+
params.label = opts.label;
|
|
677
|
+
if (opts.timezone)
|
|
678
|
+
params.timezone = opts.timezone;
|
|
679
|
+
if (opts.enabled !== undefined)
|
|
680
|
+
params.enabled = opts.enabled === "true";
|
|
681
|
+
db.update(id, params);
|
|
682
|
+
console.log(`Updated schedule ${id}`);
|
|
683
|
+
signalFleetReload();
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
db.close();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
schedule
|
|
690
|
+
.command("delete")
|
|
691
|
+
.description("Delete a schedule")
|
|
692
|
+
.argument("<id>", "Schedule ID")
|
|
693
|
+
.action((id) => {
|
|
694
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
695
|
+
try {
|
|
696
|
+
db.delete(id);
|
|
697
|
+
console.log(`Deleted schedule ${id}`);
|
|
698
|
+
signalFleetReload();
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
db.close();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
schedule
|
|
705
|
+
.command("enable")
|
|
706
|
+
.description("Enable a schedule")
|
|
707
|
+
.argument("<id>", "Schedule ID")
|
|
708
|
+
.action((id) => {
|
|
709
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
710
|
+
try {
|
|
711
|
+
db.update(id, { enabled: true });
|
|
712
|
+
console.log(`Enabled schedule ${id}`);
|
|
713
|
+
signalFleetReload();
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
db.close();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
schedule
|
|
720
|
+
.command("disable")
|
|
721
|
+
.description("Disable a schedule")
|
|
722
|
+
.argument("<id>", "Schedule ID")
|
|
723
|
+
.action((id) => {
|
|
724
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
725
|
+
try {
|
|
726
|
+
db.update(id, { enabled: false });
|
|
727
|
+
console.log(`Disabled schedule ${id}`);
|
|
728
|
+
signalFleetReload();
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
db.close();
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
schedule
|
|
735
|
+
.command("history")
|
|
736
|
+
.description("Show schedule run history")
|
|
737
|
+
.argument("<id>", "Schedule ID")
|
|
738
|
+
.option("--limit <n>", "Number of runs to show", "20")
|
|
739
|
+
.action((id, opts) => {
|
|
740
|
+
const db = new SchedulerDb(join(DATA_DIR, "scheduler.db"));
|
|
741
|
+
try {
|
|
742
|
+
const runs = db.getRuns(id, parseInt(opts.limit, 10));
|
|
743
|
+
if (runs.length === 0) {
|
|
744
|
+
console.log("No runs found.");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
console.log("Time\t\t\tStatus\t\t\tDetail");
|
|
748
|
+
for (const r of runs) {
|
|
749
|
+
console.log(`${r.triggered_at}\t${r.status}\t${r.detail ?? ""}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
db.close();
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
schedule
|
|
757
|
+
.command("trigger")
|
|
758
|
+
.description("Manually trigger a schedule")
|
|
759
|
+
.argument("<id>", "Schedule ID")
|
|
760
|
+
.action((id) => {
|
|
761
|
+
console.log("Manual trigger requires fleet manager running. Use the Telegram interface instead.");
|
|
762
|
+
});
|
|
763
|
+
// === Export / Import ===
|
|
764
|
+
program
|
|
765
|
+
.command("export")
|
|
766
|
+
.description("Export configuration for migration to another device")
|
|
767
|
+
.argument("[output]", "Output file path")
|
|
768
|
+
.option("--full", "Include all instance data (not just config)")
|
|
769
|
+
.action(async (output, opts) => {
|
|
770
|
+
const { exportConfig } = await import("./export-import.js");
|
|
771
|
+
await exportConfig(DATA_DIR, output, opts?.full ?? false);
|
|
772
|
+
});
|
|
773
|
+
program
|
|
774
|
+
.command("import")
|
|
775
|
+
.description("Import configuration from an export file")
|
|
776
|
+
.argument("<file>", "Path to export tarball")
|
|
777
|
+
.action(async (file) => {
|
|
778
|
+
const { importConfig } = await import("./export-import.js");
|
|
779
|
+
await importConfig(DATA_DIR, file);
|
|
780
|
+
});
|
|
781
|
+
program.parse();
|
|
782
|
+
//# sourceMappingURL=cli.js.map
|