@burdenoff/vibe-agent 1.0.0
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/.env.example +8 -0
- package/LICENSE +22 -0
- package/README.md +290 -0
- package/dist/app.d.ts +15 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +445 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1043 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/schema.d.ts +145 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +536 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/ModuleAuth.d.ts +61 -0
- package/dist/middleware/ModuleAuth.d.ts.map +1 -0
- package/dist/middleware/ModuleAuth.js +220 -0
- package/dist/middleware/ModuleAuth.js.map +1 -0
- package/dist/middleware/auth.d.ts +3 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +11 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/migrations/remove-notes-prompts.d.ts +13 -0
- package/dist/migrations/remove-notes-prompts.d.ts.map +1 -0
- package/dist/migrations/remove-notes-prompts.js +148 -0
- package/dist/migrations/remove-notes-prompts.js.map +1 -0
- package/dist/routes/bookmarks.d.ts +3 -0
- package/dist/routes/bookmarks.d.ts.map +1 -0
- package/dist/routes/bookmarks.js +186 -0
- package/dist/routes/bookmarks.js.map +1 -0
- package/dist/routes/config.d.ts +3 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +108 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/files.d.ts +3 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +471 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/git.d.ts +3 -0
- package/dist/routes/git.d.ts.map +1 -0
- package/dist/routes/git.js +498 -0
- package/dist/routes/git.js.map +1 -0
- package/dist/routes/moduleRegistry.d.ts +41 -0
- package/dist/routes/moduleRegistry.d.ts.map +1 -0
- package/dist/routes/moduleRegistry.js +356 -0
- package/dist/routes/moduleRegistry.js.map +1 -0
- package/dist/routes/notifications.d.ts +3 -0
- package/dist/routes/notifications.d.ts.map +1 -0
- package/dist/routes/notifications.js +250 -0
- package/dist/routes/notifications.js.map +1 -0
- package/dist/routes/port-forward.d.ts +3 -0
- package/dist/routes/port-forward.d.ts.map +1 -0
- package/dist/routes/port-forward.js +205 -0
- package/dist/routes/port-forward.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +442 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/ssh.d.ts +3 -0
- package/dist/routes/ssh.d.ts.map +1 -0
- package/dist/routes/ssh.js +192 -0
- package/dist/routes/ssh.js.map +1 -0
- package/dist/routes/tasks.d.ts +3 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +183 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/routes/tmux.d.ts +3 -0
- package/dist/routes/tmux.d.ts.map +1 -0
- package/dist/routes/tmux.js +1191 -0
- package/dist/routes/tmux.js.map +1 -0
- package/dist/routes/tunnel.d.ts +25 -0
- package/dist/routes/tunnel.d.ts.map +1 -0
- package/dist/routes/tunnel.js +449 -0
- package/dist/routes/tunnel.js.map +1 -0
- package/dist/services/ModulePermissions.d.ts +100 -0
- package/dist/services/ModulePermissions.d.ts.map +1 -0
- package/dist/services/ModulePermissions.js +312 -0
- package/dist/services/ModulePermissions.js.map +1 -0
- package/dist/services/ModuleRegistryService.d.ts +152 -0
- package/dist/services/ModuleRegistryService.d.ts.map +1 -0
- package/dist/services/ModuleRegistryService.js +522 -0
- package/dist/services/ModuleRegistryService.js.map +1 -0
- package/dist/services/agent.service.d.ts +19 -0
- package/dist/services/agent.service.d.ts.map +1 -0
- package/dist/services/agent.service.js +88 -0
- package/dist/services/agent.service.js.map +1 -0
- package/dist/services/bootstrap.d.ts +22 -0
- package/dist/services/bootstrap.d.ts.map +1 -0
- package/dist/services/bootstrap.js +206 -0
- package/dist/services/bootstrap.js.map +1 -0
- package/dist/services/service-manager.d.ts +50 -0
- package/dist/services/service-manager.d.ts.map +1 -0
- package/dist/services/service-manager.js +382 -0
- package/dist/services/service-manager.js.map +1 -0
- package/package.json +107 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { AgentService } from './services/agent.service.js';
|
|
5
|
+
import { ServiceManager } from './services/service-manager.js';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
// Read package.json for version
|
|
12
|
+
let packageVersion = '1.0.0';
|
|
13
|
+
try {
|
|
14
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
16
|
+
packageVersion = packageJson.version;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// fallback
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_AGENT_URL = 'http://localhost:3005';
|
|
22
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
|
+
async function agentFetch(agentUrl, path, options = {}) {
|
|
24
|
+
const apiKey = process.env.AGENT_API_KEY;
|
|
25
|
+
const headers = {
|
|
26
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
27
|
+
...(apiKey ? { 'x-agent-api-key': apiKey } : {}),
|
|
28
|
+
...(options.headers ?? {}),
|
|
29
|
+
};
|
|
30
|
+
const res = await fetch(`${agentUrl}${path}`, { ...options, headers });
|
|
31
|
+
const data = await res.json().catch(() => ({}));
|
|
32
|
+
return { ok: res.ok, status: res.status, data };
|
|
33
|
+
}
|
|
34
|
+
function fail(msg) {
|
|
35
|
+
console.error(`\x1b[31mError:\x1b[0m ${msg}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
function formatTable(rows) {
|
|
39
|
+
if (rows.length === 0) {
|
|
40
|
+
console.log(' (none)');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.table(rows);
|
|
44
|
+
}
|
|
45
|
+
function timeAgo(dateStr) {
|
|
46
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
47
|
+
const s = Math.floor(diff / 1000);
|
|
48
|
+
if (s < 60)
|
|
49
|
+
return `${s}s ago`;
|
|
50
|
+
const m = Math.floor(s / 60);
|
|
51
|
+
if (m < 60)
|
|
52
|
+
return `${m}m ago`;
|
|
53
|
+
const h = Math.floor(m / 60);
|
|
54
|
+
if (h < 24)
|
|
55
|
+
return `${h}h ago`;
|
|
56
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
57
|
+
}
|
|
58
|
+
// ─── Program ──────────────────────────────────────────────────────────────────
|
|
59
|
+
const program = new Command();
|
|
60
|
+
const agentService = new AgentService();
|
|
61
|
+
const serviceManager = new ServiceManager();
|
|
62
|
+
program
|
|
63
|
+
.name('vibe')
|
|
64
|
+
.description('VibeControls Agent CLI — Remote development environment management')
|
|
65
|
+
.version(packageVersion, '-v, --version');
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Core Commands
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
69
|
+
program
|
|
70
|
+
.command('start')
|
|
71
|
+
.description('Start the agent server')
|
|
72
|
+
.option('-p, --port <port>', 'Port to run on', '3005')
|
|
73
|
+
.option('-n, --name <name>', 'Instance name', 'default')
|
|
74
|
+
.option('-d, --daemon', 'Run as background daemon', false)
|
|
75
|
+
.option('--db-path <path>', 'SQLite database path', './vibecontrols-agent.db')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
try {
|
|
78
|
+
const config = {
|
|
79
|
+
port: parseInt(options.port),
|
|
80
|
+
name: options.name,
|
|
81
|
+
daemon: options.daemon,
|
|
82
|
+
dbPath: options.dbPath,
|
|
83
|
+
};
|
|
84
|
+
if (options.daemon) {
|
|
85
|
+
await serviceManager.startDaemon(config);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await agentService.start(config);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
fail(`Failed to start agent: ${error instanceof Error ? error.message : error}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command('stop')
|
|
97
|
+
.description('Stop a running agent instance')
|
|
98
|
+
.option('-n, --name <name>', 'Instance name', 'default')
|
|
99
|
+
.option('--all', 'Stop all instances', false)
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
try {
|
|
102
|
+
if (options.all) {
|
|
103
|
+
await serviceManager.stopAll();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await serviceManager.stop(options.name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
fail(`Failed to stop: ${error instanceof Error ? error.message : error}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
program
|
|
114
|
+
.command('restart')
|
|
115
|
+
.description('Restart an agent instance')
|
|
116
|
+
.option('-n, --name <name>', 'Instance name', 'default')
|
|
117
|
+
.option('-p, --port <port>', 'Port', '3005')
|
|
118
|
+
.option('--db-path <path>', 'Database path', './vibecontrols-agent.db')
|
|
119
|
+
.action(async (options) => {
|
|
120
|
+
try {
|
|
121
|
+
await serviceManager.restart(options.name, {
|
|
122
|
+
port: parseInt(options.port),
|
|
123
|
+
name: options.name,
|
|
124
|
+
daemon: true,
|
|
125
|
+
dbPath: options.dbPath,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
fail(`Failed to restart: ${error instanceof Error ? error.message : error}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
program
|
|
133
|
+
.command('status')
|
|
134
|
+
.description('Show status of agent instances')
|
|
135
|
+
.option('-n, --name <name>', 'Specific instance name')
|
|
136
|
+
.action(async (options) => {
|
|
137
|
+
try {
|
|
138
|
+
if (options.name) {
|
|
139
|
+
const status = await serviceManager.getStatus(options.name);
|
|
140
|
+
if (!status) {
|
|
141
|
+
console.log(`No agent instance named '${options.name}' found.`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`\n Agent: ${status.name}`);
|
|
145
|
+
console.log(` Status: ${status.status === 'running' ? '\x1b[32m● running\x1b[0m' : '\x1b[31m○ stopped\x1b[0m'}`);
|
|
146
|
+
console.log(` PID: ${status.pid}`);
|
|
147
|
+
console.log(` Port: ${status.port}`);
|
|
148
|
+
console.log(` Started: ${status.startTime}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const all = await serviceManager.getStatusAll();
|
|
153
|
+
if (all.length === 0) {
|
|
154
|
+
console.log('No agent instances registered.');
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
formatTable(all.map(s => ({
|
|
158
|
+
Name: s.name,
|
|
159
|
+
Status: s.status,
|
|
160
|
+
PID: s.pid,
|
|
161
|
+
Port: s.port,
|
|
162
|
+
Started: s.startTime,
|
|
163
|
+
})));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
fail(`Failed to get status: ${error instanceof Error ? error.message : error}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
program
|
|
172
|
+
.command('list')
|
|
173
|
+
.description('List all agent instances')
|
|
174
|
+
.action(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const instances = await serviceManager.listInstances();
|
|
177
|
+
if (instances.length === 0) {
|
|
178
|
+
console.log('No agent instances found.');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
formatTable(instances.map(i => ({
|
|
182
|
+
Name: i.name,
|
|
183
|
+
Status: i.status,
|
|
184
|
+
PID: i.pid,
|
|
185
|
+
Port: i.port,
|
|
186
|
+
Started: i.startTime,
|
|
187
|
+
})));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
fail(`Failed to list: ${error instanceof Error ? error.message : error}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
program
|
|
195
|
+
.command('kill')
|
|
196
|
+
.description('Force kill an agent instance')
|
|
197
|
+
.option('-n, --name <name>', 'Instance name', 'default')
|
|
198
|
+
.option('--all', 'Kill all instances', false)
|
|
199
|
+
.action(async (options) => {
|
|
200
|
+
try {
|
|
201
|
+
if (options.all) {
|
|
202
|
+
await serviceManager.killAll();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
await serviceManager.kill(options.name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
fail(`Failed to kill: ${error instanceof Error ? error.message : error}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
program
|
|
213
|
+
.command('logs')
|
|
214
|
+
.description('Show logs for an agent instance')
|
|
215
|
+
.option('-n, --name <name>', 'Instance name', 'default')
|
|
216
|
+
.option('-f, --follow', 'Follow log output', false)
|
|
217
|
+
.option('--tail <lines>', 'Number of lines', '100')
|
|
218
|
+
.action(async (options) => {
|
|
219
|
+
try {
|
|
220
|
+
await serviceManager.showLogs(options.name, {
|
|
221
|
+
follow: options.follow,
|
|
222
|
+
tail: parseInt(options.tail),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
fail(`Failed to show logs: ${error instanceof Error ? error.message : error}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// ─── Key ──────────────────────────────────────────────────────────────────────
|
|
230
|
+
program
|
|
231
|
+
.command('key')
|
|
232
|
+
.description('Display the current agent API key')
|
|
233
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
234
|
+
.action(async (options) => {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch(`${options.agentUrl}/api/agent-api-key`);
|
|
237
|
+
if (!res.ok)
|
|
238
|
+
fail(`Agent returned ${res.status}`);
|
|
239
|
+
const data = await res.json();
|
|
240
|
+
console.log(`\n \x1b[1mAPI Key:\x1b[0m ${data.apiKey}`);
|
|
241
|
+
console.log(` Generated: ${data.generatedAt}`);
|
|
242
|
+
console.log(` Note: ${data.note}\n`);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
fail(`Cannot reach agent at ${options.agentUrl}: ${error instanceof Error ? error.message : error}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// ─── Health ───────────────────────────────────────────────────────────────────
|
|
249
|
+
program
|
|
250
|
+
.command('health')
|
|
251
|
+
.description('Check health of the agent')
|
|
252
|
+
.option('-n, --name <name>', 'Instance name (uses local registry)')
|
|
253
|
+
.option('--agent-url <url>', 'Agent URL (direct)', DEFAULT_AGENT_URL)
|
|
254
|
+
.action(async (options) => {
|
|
255
|
+
try {
|
|
256
|
+
if (options.name) {
|
|
257
|
+
const h = await serviceManager.checkHealth(options.name);
|
|
258
|
+
console.log(JSON.stringify(h, null, 2));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const res = await fetch(`${options.agentUrl}/health`);
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
fail(`Health check failed: ${res.status}`);
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
console.log(`\n Status: \x1b[32m${data.status}\x1b[0m`);
|
|
266
|
+
console.log(` Version: ${data.version}`);
|
|
267
|
+
console.log(` Uptime: ${Math.floor(data.uptime / 60)}m ${Math.floor(data.uptime % 60)}s`);
|
|
268
|
+
if (data.tunnelUrl)
|
|
269
|
+
console.log(` Tunnel: ${data.tunnelUrl}`);
|
|
270
|
+
console.log();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
fail(`Health check failed: ${error instanceof Error ? error.message : error}`);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// ─── Version (detailed) ──────────────────────────────────────────────────────
|
|
278
|
+
program
|
|
279
|
+
.command('info')
|
|
280
|
+
.description('Show detailed version and system information')
|
|
281
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
282
|
+
.action(async (options) => {
|
|
283
|
+
console.log(`\n \x1b[1mVibeControls Agent\x1b[0m v${packageVersion}`);
|
|
284
|
+
console.log(` Node.js: ${process.version}`);
|
|
285
|
+
console.log(` Platform: ${process.platform}`);
|
|
286
|
+
console.log(` Architecture: ${process.arch}`);
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(`${options.agentUrl}/api/version`);
|
|
289
|
+
if (res.ok) {
|
|
290
|
+
const data = await res.json();
|
|
291
|
+
console.log(` Agent Port: ${data.port || 'N/A'}`);
|
|
292
|
+
console.log(` Uptime: ${data.uptime ? `${Math.floor(data.uptime / 60)}m` : 'N/A'}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
console.log(` Agent: not reachable at ${options.agentUrl}`);
|
|
297
|
+
}
|
|
298
|
+
console.log();
|
|
299
|
+
});
|
|
300
|
+
// ─── Setup ────────────────────────────────────────────────────────────────────
|
|
301
|
+
program
|
|
302
|
+
.command('setup')
|
|
303
|
+
.description('Install or verify system dependencies (tmux, ttyd, cloudflared)')
|
|
304
|
+
.option('--check', 'Only check without installing', false)
|
|
305
|
+
.action(async (options) => {
|
|
306
|
+
try {
|
|
307
|
+
const { bootstrap, checkDependencies } = await import('./services/bootstrap.js');
|
|
308
|
+
if (options.check) {
|
|
309
|
+
console.log('\n Checking dependencies...\n');
|
|
310
|
+
const deps = checkDependencies();
|
|
311
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
312
|
+
const icon = info.available ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
313
|
+
console.log(` ${icon} ${name.padEnd(14)} ${info.available ? info.version : 'not installed'}`);
|
|
314
|
+
}
|
|
315
|
+
const missing = Object.entries(deps).filter(([, v]) => !v.available);
|
|
316
|
+
if (missing.length > 0) {
|
|
317
|
+
console.log(`\n Missing: ${missing.map(([k]) => k).join(', ')}`);
|
|
318
|
+
console.log(' Run `vibe setup` to install them.\n');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log('\n All dependencies are installed.\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
console.log('\n Installing system dependencies...\n');
|
|
327
|
+
const results = await bootstrap({ verbose: true });
|
|
328
|
+
const failed = results.filter(r => r.status === 'failed');
|
|
329
|
+
if (failed.length > 0) {
|
|
330
|
+
console.log(`\n ${failed.length} tool(s) failed: ${failed.map(f => f.tool).join(', ')}`);
|
|
331
|
+
console.log(' You may need to install manually or use sudo.\n');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.log('\n All dependencies installed successfully.\n');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
fail(`Setup failed: ${error instanceof Error ? error.message : error}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
344
|
+
program
|
|
345
|
+
.command('config')
|
|
346
|
+
.description('Manage agent configuration')
|
|
347
|
+
.option('--set <key=value>', 'Set a configuration value')
|
|
348
|
+
.option('--get <key>', 'Get a configuration value')
|
|
349
|
+
.option('--list', 'List all configuration')
|
|
350
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
351
|
+
.action(async (options) => {
|
|
352
|
+
try {
|
|
353
|
+
if (options.set) {
|
|
354
|
+
const eq = options.set.indexOf('=');
|
|
355
|
+
if (eq === -1)
|
|
356
|
+
fail('Use --set key=value format');
|
|
357
|
+
const key = options.set.substring(0, eq);
|
|
358
|
+
const value = options.set.substring(eq + 1);
|
|
359
|
+
const { ok } = await agentFetch(options.agentUrl, `/api/config/${key}`, {
|
|
360
|
+
method: 'PUT',
|
|
361
|
+
body: JSON.stringify({ value }),
|
|
362
|
+
});
|
|
363
|
+
if (ok)
|
|
364
|
+
console.log(` Set: ${key} = ${value}`);
|
|
365
|
+
else
|
|
366
|
+
fail(`Failed to set config key '${key}'`);
|
|
367
|
+
}
|
|
368
|
+
else if (options.get) {
|
|
369
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/config/${options.get}`);
|
|
370
|
+
if (ok && data.value !== undefined) {
|
|
371
|
+
console.log(` ${options.get} = ${data.value}`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.log(` Key '${options.get}' not found`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (options.list) {
|
|
378
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/config/');
|
|
379
|
+
if (ok) {
|
|
380
|
+
const entries = Object.entries(data);
|
|
381
|
+
if (entries.length === 0) {
|
|
382
|
+
console.log(' No configuration set.');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
for (const [k, v] of entries) {
|
|
386
|
+
console.log(` ${k} = ${v}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
fail('Failed to list config');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log('Use --set key=value, --get <key>, or --list');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
fail(`Config failed: ${error instanceof Error ? error.message : error}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
// Tunnel Commands
|
|
404
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
405
|
+
const tunnelCmd = program
|
|
406
|
+
.command('tunnel')
|
|
407
|
+
.description('Manage cloudflared tunnels');
|
|
408
|
+
tunnelCmd
|
|
409
|
+
.command('list')
|
|
410
|
+
.description('List all tunnels')
|
|
411
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
412
|
+
.action(async (options) => {
|
|
413
|
+
try {
|
|
414
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel');
|
|
415
|
+
if (!ok)
|
|
416
|
+
fail('Failed to list tunnels');
|
|
417
|
+
if (data.tunnels.length === 0) {
|
|
418
|
+
console.log(' No tunnels found.');
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
formatTable(data.tunnels.map(t => ({
|
|
422
|
+
ID: t.id.substring(0, 12) + '...',
|
|
423
|
+
Port: t.localPort,
|
|
424
|
+
'Public URL': t.publicUrl || '(none)',
|
|
425
|
+
Status: t.status,
|
|
426
|
+
PID: t.pid || 'N/A',
|
|
427
|
+
})));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
tunnelCmd
|
|
435
|
+
.command('start')
|
|
436
|
+
.description('Start a tunnel for a local port')
|
|
437
|
+
.requiredOption('-p, --port <port>', 'Local port to expose')
|
|
438
|
+
.option('-s, --subdomain <subdomain>', 'Preferred subdomain')
|
|
439
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
440
|
+
.action(async (options) => {
|
|
441
|
+
try {
|
|
442
|
+
console.log(` Starting tunnel for port ${options.port}...`);
|
|
443
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel/start', {
|
|
444
|
+
method: 'POST',
|
|
445
|
+
body: JSON.stringify({
|
|
446
|
+
localPort: parseInt(options.port),
|
|
447
|
+
subdomain: options.subdomain,
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
if (!ok)
|
|
451
|
+
fail(data.error || `Agent returned error`);
|
|
452
|
+
console.log(`\n \x1b[32mTunnel started:\x1b[0m`);
|
|
453
|
+
console.log(` ID: ${data.id}`);
|
|
454
|
+
console.log(` Local Port: ${data.localPort}`);
|
|
455
|
+
console.log(` Public URL: ${data.publicUrl || '(pending...)'}`);
|
|
456
|
+
console.log(` PID: ${data.pid || 'N/A'}`);
|
|
457
|
+
console.log(` Status: ${data.status}\n`);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
tunnelCmd
|
|
464
|
+
.command('stop')
|
|
465
|
+
.description('Stop a running tunnel')
|
|
466
|
+
.requiredOption('-i, --id <id>', 'Tunnel ID')
|
|
467
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
468
|
+
.action(async (options) => {
|
|
469
|
+
try {
|
|
470
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tunnel/${options.id}/stop`, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
});
|
|
473
|
+
if (!ok)
|
|
474
|
+
fail(data.error || 'Failed to stop tunnel');
|
|
475
|
+
console.log(' Tunnel stopped.');
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
tunnelCmd
|
|
482
|
+
.command('delete')
|
|
483
|
+
.description('Delete a tunnel')
|
|
484
|
+
.requiredOption('-i, --id <id>', 'Tunnel ID')
|
|
485
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
486
|
+
.action(async (options) => {
|
|
487
|
+
try {
|
|
488
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tunnel/${options.id}`, {
|
|
489
|
+
method: 'DELETE',
|
|
490
|
+
});
|
|
491
|
+
if (!ok)
|
|
492
|
+
fail(data.error || 'Failed to delete tunnel');
|
|
493
|
+
console.log(' Tunnel deleted.');
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
tunnelCmd
|
|
500
|
+
.command('status')
|
|
501
|
+
.description('Get tunnel overview')
|
|
502
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
503
|
+
.action(async (options) => {
|
|
504
|
+
try {
|
|
505
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel/status');
|
|
506
|
+
if (!ok)
|
|
507
|
+
fail('Failed to get tunnel status');
|
|
508
|
+
console.log(`\n Total: ${data.total}`);
|
|
509
|
+
console.log(` Active: \x1b[32m${data.active}\x1b[0m`);
|
|
510
|
+
console.log(` Inactive: ${data.inactive}`);
|
|
511
|
+
console.log(` Errored: \x1b[31m${data.errored}\x1b[0m\n`);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
// Agent tunnel (the main agent tunnel, not per-port tunnels)
|
|
518
|
+
tunnelCmd
|
|
519
|
+
.command('agent')
|
|
520
|
+
.description('Show/manage the main agent cloudflared tunnel')
|
|
521
|
+
.option('--start', 'Start the agent tunnel')
|
|
522
|
+
.option('--stop', 'Stop the agent tunnel')
|
|
523
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
524
|
+
.action(async (options) => {
|
|
525
|
+
try {
|
|
526
|
+
if (options.start) {
|
|
527
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/agent-tunnel/start', { method: 'POST' });
|
|
528
|
+
if (!ok)
|
|
529
|
+
fail('Failed to start agent tunnel');
|
|
530
|
+
console.log(` Agent tunnel started: ${data.tunnelUrl}`);
|
|
531
|
+
}
|
|
532
|
+
else if (options.stop) {
|
|
533
|
+
await agentFetch(options.agentUrl, '/api/agent-tunnel/stop', { method: 'POST' });
|
|
534
|
+
console.log(' Agent tunnel stopped.');
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/agent-tunnel');
|
|
538
|
+
if (!ok)
|
|
539
|
+
fail('Failed to get agent tunnel');
|
|
540
|
+
console.log(`\n Tunnel URL: ${data.tunnelUrl || '(not running)'}`);
|
|
541
|
+
console.log(` Status: ${data.status || 'unknown'}\n`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
549
|
+
// Session Commands (tmux)
|
|
550
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
551
|
+
const sessionCmd = program
|
|
552
|
+
.command('session')
|
|
553
|
+
.description('Manage tmux terminal sessions');
|
|
554
|
+
sessionCmd
|
|
555
|
+
.command('list')
|
|
556
|
+
.description('List all tmux sessions')
|
|
557
|
+
.option('--system', 'Include system (unmanaged) sessions')
|
|
558
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
559
|
+
.action(async (options) => {
|
|
560
|
+
try {
|
|
561
|
+
const path = options.system ? '/api/tmux/system' : '/api/tmux/';
|
|
562
|
+
const { ok, data } = await agentFetch(options.agentUrl, path);
|
|
563
|
+
if (!ok)
|
|
564
|
+
fail('Failed to list sessions');
|
|
565
|
+
const sessions = Array.isArray(data) ? data : (data.sessions ?? []);
|
|
566
|
+
if (sessions.length === 0) {
|
|
567
|
+
console.log(' No sessions found.');
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
formatTable(sessions.map(s => ({
|
|
571
|
+
ID: (s.id || s.sessionId || '').substring(0, 12),
|
|
572
|
+
Name: s.sessionName || s.name || 'N/A',
|
|
573
|
+
Status: s.status || 'unknown',
|
|
574
|
+
Port: s.ttydPort || 'N/A',
|
|
575
|
+
Project: s.projectId || 'N/A',
|
|
576
|
+
})));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
sessionCmd
|
|
584
|
+
.command('create')
|
|
585
|
+
.description('Create a new tmux session')
|
|
586
|
+
.requiredOption('--name <name>', 'Session name')
|
|
587
|
+
.option('--project <id>', 'Project ID', 'default')
|
|
588
|
+
.option('--command <cmd>', 'Initial command')
|
|
589
|
+
.option('--cwd <dir>', 'Working directory')
|
|
590
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
591
|
+
.action(async (options) => {
|
|
592
|
+
try {
|
|
593
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/tmux/create', {
|
|
594
|
+
method: 'POST',
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
sessionId: `cli-${Date.now()}`,
|
|
597
|
+
sessionName: options.name,
|
|
598
|
+
projectId: options.project,
|
|
599
|
+
command: options.command,
|
|
600
|
+
startDirectory: options.cwd,
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
if (!ok)
|
|
604
|
+
fail(data.error || 'Failed to create session');
|
|
605
|
+
console.log(` Session created: ${data.sessionName || options.name}`);
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
sessionCmd
|
|
612
|
+
.command('kill')
|
|
613
|
+
.description('Kill a tmux session')
|
|
614
|
+
.requiredOption('-i, --id <id>', 'Session ID')
|
|
615
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
616
|
+
.action(async (options) => {
|
|
617
|
+
try {
|
|
618
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}`, {
|
|
619
|
+
method: 'DELETE',
|
|
620
|
+
});
|
|
621
|
+
if (!ok)
|
|
622
|
+
fail(data.error || 'Failed to kill session');
|
|
623
|
+
console.log(' Session killed.');
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
sessionCmd
|
|
630
|
+
.command('exec')
|
|
631
|
+
.description('Execute a command in a tmux session')
|
|
632
|
+
.requiredOption('-i, --id <id>', 'Session ID')
|
|
633
|
+
.requiredOption('-c, --command <cmd>', 'Command to execute')
|
|
634
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
635
|
+
.action(async (options) => {
|
|
636
|
+
try {
|
|
637
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}/command`, {
|
|
638
|
+
method: 'POST',
|
|
639
|
+
body: JSON.stringify({ command: options.command }),
|
|
640
|
+
});
|
|
641
|
+
if (!ok)
|
|
642
|
+
fail(data.error || 'Failed to execute command');
|
|
643
|
+
console.log(' Command sent.');
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
sessionCmd
|
|
650
|
+
.command('capture')
|
|
651
|
+
.description('Capture output from a tmux session')
|
|
652
|
+
.requiredOption('-i, --id <id>', 'Session ID')
|
|
653
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
654
|
+
.action(async (options) => {
|
|
655
|
+
try {
|
|
656
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}/capture`);
|
|
657
|
+
if (!ok)
|
|
658
|
+
fail('Failed to capture output');
|
|
659
|
+
console.log(data.output || '(empty)');
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
666
|
+
// SSH Commands
|
|
667
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
668
|
+
const sshCmd = program
|
|
669
|
+
.command('ssh')
|
|
670
|
+
.description('Manage SSH connections');
|
|
671
|
+
sshCmd
|
|
672
|
+
.command('list')
|
|
673
|
+
.description('List saved SSH connections')
|
|
674
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
675
|
+
.action(async (options) => {
|
|
676
|
+
try {
|
|
677
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/connections');
|
|
678
|
+
if (!ok)
|
|
679
|
+
fail('Failed to list SSH connections');
|
|
680
|
+
const conns = Array.isArray(data) ? data : (data.connections ?? []);
|
|
681
|
+
if (conns.length === 0) {
|
|
682
|
+
console.log(' No SSH connections saved.');
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
formatTable(conns.map(c => ({
|
|
686
|
+
ID: (c.id || '').substring(0, 12),
|
|
687
|
+
Name: c.serverName,
|
|
688
|
+
Host: c.host,
|
|
689
|
+
Port: c.port,
|
|
690
|
+
User: c.username,
|
|
691
|
+
})));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
sshCmd
|
|
699
|
+
.command('add')
|
|
700
|
+
.description('Add an SSH connection')
|
|
701
|
+
.requiredOption('--name <name>', 'Server name')
|
|
702
|
+
.requiredOption('--host <host>', 'Hostname or IP')
|
|
703
|
+
.requiredOption('--user <user>', 'Username')
|
|
704
|
+
.option('--port <port>', 'Port', '22')
|
|
705
|
+
.option('--key <path>', 'Private key path')
|
|
706
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
707
|
+
.action(async (options) => {
|
|
708
|
+
try {
|
|
709
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/connections', {
|
|
710
|
+
method: 'POST',
|
|
711
|
+
body: JSON.stringify({
|
|
712
|
+
serverName: options.name,
|
|
713
|
+
host: options.host,
|
|
714
|
+
port: parseInt(options.port),
|
|
715
|
+
username: options.user,
|
|
716
|
+
privateKeyPath: options.key,
|
|
717
|
+
}),
|
|
718
|
+
});
|
|
719
|
+
if (!ok)
|
|
720
|
+
fail(data.error || 'Failed to add connection');
|
|
721
|
+
console.log(` SSH connection '${options.name}' added.`);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
sshCmd
|
|
728
|
+
.command('remove')
|
|
729
|
+
.description('Remove an SSH connection')
|
|
730
|
+
.requiredOption('-i, --id <id>', 'Connection ID')
|
|
731
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
732
|
+
.action(async (options) => {
|
|
733
|
+
try {
|
|
734
|
+
const { ok } = await agentFetch(options.agentUrl, `/api/ssh/connections/${options.id}`, {
|
|
735
|
+
method: 'DELETE',
|
|
736
|
+
});
|
|
737
|
+
if (!ok)
|
|
738
|
+
fail('Failed to remove connection');
|
|
739
|
+
console.log(' SSH connection removed.');
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
sshCmd
|
|
746
|
+
.command('test')
|
|
747
|
+
.description('Test an SSH connection')
|
|
748
|
+
.requiredOption('-i, --id <id>', 'Connection ID')
|
|
749
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
750
|
+
.action(async (options) => {
|
|
751
|
+
try {
|
|
752
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/ssh/test/${options.id}`, { method: 'POST' });
|
|
753
|
+
if (!ok)
|
|
754
|
+
fail(data.error || 'Connection test failed');
|
|
755
|
+
console.log(' \x1b[32mConnection successful.\x1b[0m');
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
sshCmd
|
|
762
|
+
.command('exec')
|
|
763
|
+
.description('Execute a command on a remote server')
|
|
764
|
+
.requiredOption('-i, --id <id>', 'Connection ID')
|
|
765
|
+
.requiredOption('-c, --command <cmd>', 'Command to execute')
|
|
766
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
767
|
+
.action(async (options) => {
|
|
768
|
+
try {
|
|
769
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/execute', {
|
|
770
|
+
method: 'POST',
|
|
771
|
+
body: JSON.stringify({
|
|
772
|
+
connectionId: options.id,
|
|
773
|
+
command: options.command,
|
|
774
|
+
}),
|
|
775
|
+
});
|
|
776
|
+
if (!ok)
|
|
777
|
+
fail(data.error || 'Execution failed');
|
|
778
|
+
if (data.output)
|
|
779
|
+
console.log(data.output);
|
|
780
|
+
if (data.error)
|
|
781
|
+
console.error(data.error);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
788
|
+
// Port Forward Commands
|
|
789
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
790
|
+
const forwardCmd = program
|
|
791
|
+
.command('forward')
|
|
792
|
+
.description('Manage SSH port forwards');
|
|
793
|
+
forwardCmd
|
|
794
|
+
.command('list')
|
|
795
|
+
.description('List all port forwards')
|
|
796
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
797
|
+
.action(async (options) => {
|
|
798
|
+
try {
|
|
799
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/port-forward/');
|
|
800
|
+
if (!ok)
|
|
801
|
+
fail('Failed to list forwards');
|
|
802
|
+
const fwds = Array.isArray(data) ? data : (data.forwards ?? []);
|
|
803
|
+
if (fwds.length === 0) {
|
|
804
|
+
console.log(' No port forwards found.');
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
formatTable(fwds.map(f => ({
|
|
808
|
+
ID: (f.id || '').substring(0, 12),
|
|
809
|
+
Local: f.localPort,
|
|
810
|
+
Remote: `${f.remoteHost}:${f.remotePort}`,
|
|
811
|
+
Server: f.serverName,
|
|
812
|
+
Status: f.status,
|
|
813
|
+
})));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
forwardCmd
|
|
821
|
+
.command('create')
|
|
822
|
+
.description('Create a port forward rule')
|
|
823
|
+
.requiredOption('--local <port>', 'Local port')
|
|
824
|
+
.requiredOption('--remote-host <host>', 'Remote host')
|
|
825
|
+
.requiredOption('--remote-port <port>', 'Remote port')
|
|
826
|
+
.requiredOption('--server <name>', 'SSH server name')
|
|
827
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
828
|
+
.action(async (options) => {
|
|
829
|
+
try {
|
|
830
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/port-forward/', {
|
|
831
|
+
method: 'POST',
|
|
832
|
+
body: JSON.stringify({
|
|
833
|
+
localPort: parseInt(options.local),
|
|
834
|
+
remoteHost: options.remoteHost,
|
|
835
|
+
remotePort: parseInt(options.remotePort),
|
|
836
|
+
serverName: options.server,
|
|
837
|
+
}),
|
|
838
|
+
});
|
|
839
|
+
if (!ok)
|
|
840
|
+
fail(data.error || 'Failed to create forward');
|
|
841
|
+
console.log(' Port forward created.');
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
forwardCmd
|
|
848
|
+
.command('start')
|
|
849
|
+
.description('Start a port forward')
|
|
850
|
+
.requiredOption('-i, --id <id>', 'Forward ID')
|
|
851
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
852
|
+
.action(async (options) => {
|
|
853
|
+
try {
|
|
854
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}/start`, {
|
|
855
|
+
method: 'POST',
|
|
856
|
+
});
|
|
857
|
+
if (!ok)
|
|
858
|
+
fail(data.error || 'Failed to start forward');
|
|
859
|
+
console.log(' Port forward started.');
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
forwardCmd
|
|
866
|
+
.command('stop')
|
|
867
|
+
.description('Stop a port forward')
|
|
868
|
+
.requiredOption('-i, --id <id>', 'Forward ID')
|
|
869
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
870
|
+
.action(async (options) => {
|
|
871
|
+
try {
|
|
872
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}/stop`, {
|
|
873
|
+
method: 'POST',
|
|
874
|
+
});
|
|
875
|
+
if (!ok)
|
|
876
|
+
fail(data.error || 'Failed to stop forward');
|
|
877
|
+
console.log(' Port forward stopped.');
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
forwardCmd
|
|
884
|
+
.command('delete')
|
|
885
|
+
.description('Delete a port forward')
|
|
886
|
+
.requiredOption('-i, --id <id>', 'Forward ID')
|
|
887
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
888
|
+
.action(async (options) => {
|
|
889
|
+
try {
|
|
890
|
+
const { ok } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}`, {
|
|
891
|
+
method: 'DELETE',
|
|
892
|
+
});
|
|
893
|
+
if (!ok)
|
|
894
|
+
fail('Failed to delete forward');
|
|
895
|
+
console.log(' Port forward deleted.');
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
902
|
+
// Task Commands
|
|
903
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
904
|
+
const taskCmd = program
|
|
905
|
+
.command('task')
|
|
906
|
+
.description('Manage background tasks');
|
|
907
|
+
taskCmd
|
|
908
|
+
.command('list')
|
|
909
|
+
.description('List tasks')
|
|
910
|
+
.option('--status <status>', 'Filter by status (pending, running, completed, failed)')
|
|
911
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
912
|
+
.action(async (options) => {
|
|
913
|
+
try {
|
|
914
|
+
const query = options.status ? `?status=${options.status}` : '';
|
|
915
|
+
const { ok, data } = await agentFetch(options.agentUrl, `/api/tasks/${query}`);
|
|
916
|
+
if (!ok)
|
|
917
|
+
fail('Failed to list tasks');
|
|
918
|
+
const tasks = Array.isArray(data) ? data : [];
|
|
919
|
+
if (tasks.length === 0) {
|
|
920
|
+
console.log(' No tasks found.');
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
formatTable(tasks.map(t => ({
|
|
924
|
+
ID: (t.id || '').substring(0, 12),
|
|
925
|
+
Type: t.type,
|
|
926
|
+
Status: t.status,
|
|
927
|
+
Created: t.createdAt ? timeAgo(t.createdAt) : 'N/A',
|
|
928
|
+
})));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
taskCmd
|
|
936
|
+
.command('run')
|
|
937
|
+
.description('Run a command as a background task')
|
|
938
|
+
.requiredOption('-c, --command <cmd>', 'Command to execute')
|
|
939
|
+
.option('--cwd <dir>', 'Working directory')
|
|
940
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
941
|
+
.action(async (options) => {
|
|
942
|
+
try {
|
|
943
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/tasks/', {
|
|
944
|
+
method: 'POST',
|
|
945
|
+
body: JSON.stringify({
|
|
946
|
+
type: 'command',
|
|
947
|
+
payload: JSON.stringify({
|
|
948
|
+
command: options.command,
|
|
949
|
+
cwd: options.cwd,
|
|
950
|
+
}),
|
|
951
|
+
}),
|
|
952
|
+
});
|
|
953
|
+
if (!ok)
|
|
954
|
+
fail(data.error || 'Failed to create task');
|
|
955
|
+
console.log(` Task created: ${data.id}`);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
962
|
+
// Notification Commands
|
|
963
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
964
|
+
const notifyCmd = program
|
|
965
|
+
.command('notify')
|
|
966
|
+
.description('Manage notifications');
|
|
967
|
+
notifyCmd
|
|
968
|
+
.command('list')
|
|
969
|
+
.description('List notifications')
|
|
970
|
+
.option('--unread', 'Show only unread')
|
|
971
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
972
|
+
.action(async (options) => {
|
|
973
|
+
try {
|
|
974
|
+
const path = options.unread ? '/api/notifications/unread' : '/api/notifications/';
|
|
975
|
+
const { ok, data } = await agentFetch(options.agentUrl, path);
|
|
976
|
+
if (!ok)
|
|
977
|
+
fail('Failed to list notifications');
|
|
978
|
+
const notifications = Array.isArray(data) ? data : (data.notifications ?? []);
|
|
979
|
+
if (notifications.length === 0) {
|
|
980
|
+
console.log(' No notifications.');
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
for (const n of notifications) {
|
|
984
|
+
const icon = n.type === 'error' ? '\x1b[31m●\x1b[0m' :
|
|
985
|
+
n.type === 'warning' ? '\x1b[33m●\x1b[0m' :
|
|
986
|
+
n.type === 'success' ? '\x1b[32m●\x1b[0m' : '\x1b[34m●\x1b[0m';
|
|
987
|
+
const unread = n.status === 'unread' ? ' [NEW]' : '';
|
|
988
|
+
console.log(` ${icon} ${n.title}${unread}`);
|
|
989
|
+
console.log(` ${n.message}`);
|
|
990
|
+
if (n.createdAt)
|
|
991
|
+
console.log(` ${timeAgo(n.createdAt)}`);
|
|
992
|
+
console.log();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
notifyCmd
|
|
1001
|
+
.command('read-all')
|
|
1002
|
+
.description('Mark all notifications as read')
|
|
1003
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
1004
|
+
.action(async (options) => {
|
|
1005
|
+
try {
|
|
1006
|
+
const { ok } = await agentFetch(options.agentUrl, '/api/notifications/read-all', {
|
|
1007
|
+
method: 'PUT',
|
|
1008
|
+
});
|
|
1009
|
+
if (!ok)
|
|
1010
|
+
fail('Failed to mark as read');
|
|
1011
|
+
console.log(' All notifications marked as read.');
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1018
|
+
// System Info
|
|
1019
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1020
|
+
program
|
|
1021
|
+
.command('system')
|
|
1022
|
+
.description('Show system information from the agent')
|
|
1023
|
+
.option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
|
|
1024
|
+
.action(async (options) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const { ok, data } = await agentFetch(options.agentUrl, '/api/config/system/info');
|
|
1027
|
+
if (!ok)
|
|
1028
|
+
fail('Failed to get system info');
|
|
1029
|
+
console.log(`\n Hostname: ${data.hostname}`);
|
|
1030
|
+
console.log(` Platform: ${data.platform}`);
|
|
1031
|
+
console.log(` Architecture: ${data.arch}`);
|
|
1032
|
+
console.log(` CPUs: ${data.cpuCount}`);
|
|
1033
|
+
console.log(` Memory: ${data.totalMemory}`);
|
|
1034
|
+
console.log(` Uptime: ${data.uptime}`);
|
|
1035
|
+
console.log(` Environment: ${data.environment}\n`);
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
fail(`Failed: ${error instanceof Error ? error.message : error}`);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1042
|
+
program.parse();
|
|
1043
|
+
//# sourceMappingURL=cli.js.map
|