@gricha/perry 0.0.1
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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/router.js +1017 -0
- package/dist/agent/run.js +182 -0
- package/dist/agent/static.js +58 -0
- package/dist/agent/systemd.js +229 -0
- package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
- package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
- package/dist/agent/web/index.html +14 -0
- package/dist/agent/web/vite.svg +1 -0
- package/dist/chat/handler.js +174 -0
- package/dist/chat/host-handler.js +170 -0
- package/dist/chat/host-opencode-handler.js +169 -0
- package/dist/chat/index.js +2 -0
- package/dist/chat/opencode-handler.js +177 -0
- package/dist/chat/opencode-websocket.js +95 -0
- package/dist/chat/websocket.js +100 -0
- package/dist/client/api.js +138 -0
- package/dist/client/config.js +34 -0
- package/dist/client/docker-proxy.js +103 -0
- package/dist/client/index.js +4 -0
- package/dist/client/proxy.js +96 -0
- package/dist/client/shell.js +71 -0
- package/dist/client/ws-shell.js +120 -0
- package/dist/config/loader.js +59 -0
- package/dist/docker/index.js +372 -0
- package/dist/docker/types.js +1 -0
- package/dist/index.js +475 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/metadata.js +55 -0
- package/dist/sessions/parser.js +553 -0
- package/dist/sessions/types.js +1 -0
- package/dist/shared/base-websocket.js +51 -0
- package/dist/shared/client-types.js +1 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/types.js +5 -0
- package/dist/terminal/handler.js +86 -0
- package/dist/terminal/host-handler.js +76 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/types.js +8 -0
- package/dist/terminal/websocket.js +115 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/manager.js +475 -0
- package/dist/workspace/state.js +66 -0
- package/dist/workspace/types.js +1 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pkg from '../package.json';
|
|
4
|
+
import { startAgent } from './agent/run';
|
|
5
|
+
import { installService, uninstallService, showStatus } from './agent/systemd';
|
|
6
|
+
import { createApiClient, ApiClientError } from './client/api';
|
|
7
|
+
import { loadClientConfig, getWorker, setWorker } from './client/config';
|
|
8
|
+
import { openWSShell, openDockerExec, getTerminalWSUrl, isLocalWorker } from './client/ws-shell';
|
|
9
|
+
import { getContainerName, getContainerIp } from './docker';
|
|
10
|
+
import { startProxy, parsePortForward, formatPortForwards } from './client/proxy';
|
|
11
|
+
import { startDockerProxy, parsePortForward as parseDockerPortForward, formatPortForwards as formatDockerPortForwards, } from './client/docker-proxy';
|
|
12
|
+
import { loadAgentConfig, getConfigDir, ensureConfigDir } from './config/loader';
|
|
13
|
+
import { buildImage } from './docker';
|
|
14
|
+
import { DEFAULT_AGENT_PORT, WORKSPACE_IMAGE } from './shared/constants';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('perry')
|
|
18
|
+
.description('Distributed development environment orchestrator')
|
|
19
|
+
.version(pkg.version)
|
|
20
|
+
.action(() => {
|
|
21
|
+
program.outputHelp();
|
|
22
|
+
});
|
|
23
|
+
const agentCmd = program.command('agent').description('Manage the workspace agent daemon');
|
|
24
|
+
agentCmd
|
|
25
|
+
.command('run')
|
|
26
|
+
.description('Start the agent daemon')
|
|
27
|
+
.option('-p, --port <port>', 'Port to listen on', parseInt)
|
|
28
|
+
.option('-c, --config-dir <dir>', 'Configuration directory')
|
|
29
|
+
.option('--no-host-access', 'Disable direct host machine access')
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
await startAgent({
|
|
32
|
+
port: options.port,
|
|
33
|
+
configDir: options.configDir,
|
|
34
|
+
noHostAccess: options.hostAccess === false,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
agentCmd
|
|
38
|
+
.command('install')
|
|
39
|
+
.description('Install agent as systemd user service')
|
|
40
|
+
.option('-p, --port <port>', 'Port to listen on', parseInt)
|
|
41
|
+
.option('-c, --config-dir <dir>', 'Configuration directory')
|
|
42
|
+
.option('--no-host-access', 'Disable direct host machine access')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
await installService({
|
|
45
|
+
port: options.port,
|
|
46
|
+
configDir: options.configDir,
|
|
47
|
+
noHostAccess: options.hostAccess === false,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
agentCmd
|
|
51
|
+
.command('uninstall')
|
|
52
|
+
.description('Uninstall agent systemd service')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await uninstallService();
|
|
55
|
+
});
|
|
56
|
+
agentCmd
|
|
57
|
+
.command('status')
|
|
58
|
+
.description('Show agent service status')
|
|
59
|
+
.action(async () => {
|
|
60
|
+
await showStatus();
|
|
61
|
+
});
|
|
62
|
+
agentCmd
|
|
63
|
+
.command('logs')
|
|
64
|
+
.description('View agent service logs')
|
|
65
|
+
.option('-f, --follow', 'Follow log output')
|
|
66
|
+
.option('-n, --lines <lines>', 'Number of lines to show', '50')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
const { showLogs } = await import('./agent/systemd');
|
|
69
|
+
await showLogs({
|
|
70
|
+
follow: options.follow,
|
|
71
|
+
lines: parseInt(options.lines, 10),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
async function checkLocalAgent() {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`http://localhost:${DEFAULT_AGENT_PORT}/health`, {
|
|
77
|
+
signal: AbortSignal.timeout(1000),
|
|
78
|
+
});
|
|
79
|
+
return response.ok;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function getClient() {
|
|
86
|
+
let worker = await getWorker();
|
|
87
|
+
if (!worker) {
|
|
88
|
+
const localRunning = await checkLocalAgent();
|
|
89
|
+
if (localRunning) {
|
|
90
|
+
worker = `localhost:${DEFAULT_AGENT_PORT}`;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.error('No worker configured. Run: perry config worker <hostname>');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return createApiClient(worker);
|
|
98
|
+
}
|
|
99
|
+
program
|
|
100
|
+
.command('list')
|
|
101
|
+
.alias('ls')
|
|
102
|
+
.description('List all workspaces')
|
|
103
|
+
.action(async () => {
|
|
104
|
+
try {
|
|
105
|
+
const client = await getClient();
|
|
106
|
+
const workspaces = await client.listWorkspaces();
|
|
107
|
+
if (workspaces.length === 0) {
|
|
108
|
+
console.log('No workspaces found.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log('');
|
|
112
|
+
for (const ws of workspaces) {
|
|
113
|
+
const status = ws.status === 'running' ? '●' : '○';
|
|
114
|
+
console.log(` ${status} ${ws.name}`);
|
|
115
|
+
console.log(` Status: ${ws.status}`);
|
|
116
|
+
if (ws.repo) {
|
|
117
|
+
console.log(` Repo: ${ws.repo}`);
|
|
118
|
+
}
|
|
119
|
+
console.log(` SSH Port: ${ws.ports.ssh}`);
|
|
120
|
+
console.log(` Created: ${new Date(ws.created).toLocaleString()}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
handleError(err);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
program
|
|
129
|
+
.command('create <name>')
|
|
130
|
+
.description('Create a new workspace')
|
|
131
|
+
.option('--clone <url>', 'Git repository URL to clone')
|
|
132
|
+
.action(async (name, options) => {
|
|
133
|
+
try {
|
|
134
|
+
const client = await getClient();
|
|
135
|
+
console.log(`Creating workspace '${name}'...`);
|
|
136
|
+
const workspace = await client.createWorkspace({
|
|
137
|
+
name,
|
|
138
|
+
clone: options.clone,
|
|
139
|
+
});
|
|
140
|
+
console.log(`Workspace '${workspace.name}' created.`);
|
|
141
|
+
console.log(` Status: ${workspace.status}`);
|
|
142
|
+
console.log(` SSH Port: ${workspace.ports.ssh}`);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
handleError(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
program
|
|
149
|
+
.command('start <name>')
|
|
150
|
+
.description('Start a stopped workspace')
|
|
151
|
+
.action(async (name) => {
|
|
152
|
+
try {
|
|
153
|
+
const client = await getClient();
|
|
154
|
+
console.log(`Starting workspace '${name}'...`);
|
|
155
|
+
const workspace = await client.startWorkspace(name);
|
|
156
|
+
console.log(`Workspace '${workspace.name}' started.`);
|
|
157
|
+
console.log(` Status: ${workspace.status}`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
handleError(err);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
program
|
|
164
|
+
.command('stop <name>')
|
|
165
|
+
.description('Stop a running workspace')
|
|
166
|
+
.action(async (name) => {
|
|
167
|
+
try {
|
|
168
|
+
const client = await getClient();
|
|
169
|
+
console.log(`Stopping workspace '${name}'...`);
|
|
170
|
+
const workspace = await client.stopWorkspace(name);
|
|
171
|
+
console.log(`Workspace '${workspace.name}' stopped.`);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
handleError(err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
program
|
|
178
|
+
.command('delete <name>')
|
|
179
|
+
.alias('rm')
|
|
180
|
+
.description('Delete a workspace')
|
|
181
|
+
.action(async (name) => {
|
|
182
|
+
try {
|
|
183
|
+
const client = await getClient();
|
|
184
|
+
console.log(`Deleting workspace '${name}'...`);
|
|
185
|
+
await client.deleteWorkspace(name);
|
|
186
|
+
console.log(`Workspace '${name}' deleted.`);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
handleError(err);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
program
|
|
193
|
+
.command('info [name]')
|
|
194
|
+
.description('Show workspace or agent info')
|
|
195
|
+
.action(async (name) => {
|
|
196
|
+
try {
|
|
197
|
+
const client = await getClient();
|
|
198
|
+
if (name) {
|
|
199
|
+
const workspace = await client.getWorkspace(name);
|
|
200
|
+
console.log(`Workspace: ${workspace.name}`);
|
|
201
|
+
console.log(` Status: ${workspace.status}`);
|
|
202
|
+
console.log(` Container ID: ${workspace.containerId.slice(0, 12)}`);
|
|
203
|
+
if (workspace.repo) {
|
|
204
|
+
console.log(` Repo: ${workspace.repo}`);
|
|
205
|
+
}
|
|
206
|
+
console.log(` SSH Port: ${workspace.ports.ssh}`);
|
|
207
|
+
console.log(` Created: ${new Date(workspace.created).toLocaleString()}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const info = await client.info();
|
|
211
|
+
console.log(`Agent Info:`);
|
|
212
|
+
console.log(` Hostname: ${info.hostname}`);
|
|
213
|
+
console.log(` Uptime: ${formatUptime(info.uptime)}`);
|
|
214
|
+
console.log(` Workspaces: ${info.workspacesCount}`);
|
|
215
|
+
console.log(` Docker: ${info.dockerVersion}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
handleError(err);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
program
|
|
223
|
+
.command('logs <name>')
|
|
224
|
+
.description('Show workspace logs')
|
|
225
|
+
.option('-n, --tail <lines>', 'Number of lines to show', '100')
|
|
226
|
+
.action(async (name, options) => {
|
|
227
|
+
try {
|
|
228
|
+
const client = await getClient();
|
|
229
|
+
const logs = await client.getLogs(name, parseInt(options.tail, 10));
|
|
230
|
+
console.log(logs);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
handleError(err);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
program
|
|
237
|
+
.command('sync <name>')
|
|
238
|
+
.description('Sync credentials and files to a running workspace')
|
|
239
|
+
.action(async (name) => {
|
|
240
|
+
try {
|
|
241
|
+
const client = await getClient();
|
|
242
|
+
console.log(`Syncing credentials to workspace '${name}'...`);
|
|
243
|
+
await client.syncWorkspace(name);
|
|
244
|
+
console.log(`Workspace '${name}' synced.`);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
handleError(err);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
program
|
|
251
|
+
.command('shell <name>')
|
|
252
|
+
.description('Open interactive terminal to workspace')
|
|
253
|
+
.action(async (name) => {
|
|
254
|
+
try {
|
|
255
|
+
const worker = await getWorkerWithFallback();
|
|
256
|
+
const client = await getClient();
|
|
257
|
+
const workspace = await client.getWorkspace(name);
|
|
258
|
+
if (workspace.status !== 'running') {
|
|
259
|
+
console.error(`Workspace '${name}' is not running (status: ${workspace.status})`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
if (isLocalWorker(worker)) {
|
|
263
|
+
const containerName = getContainerName(name);
|
|
264
|
+
await openDockerExec({
|
|
265
|
+
containerName,
|
|
266
|
+
onError: (err) => {
|
|
267
|
+
console.error(`\nConnection error: ${err.message}`);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const wsUrl = getTerminalWSUrl(worker, name);
|
|
273
|
+
await openWSShell({
|
|
274
|
+
url: wsUrl,
|
|
275
|
+
onError: (err) => {
|
|
276
|
+
console.error(`\nConnection error: ${err.message}`);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
handleError(err);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
async function getWorkerWithFallback() {
|
|
286
|
+
let worker = await getWorker();
|
|
287
|
+
if (!worker) {
|
|
288
|
+
const localRunning = await checkLocalAgent();
|
|
289
|
+
if (localRunning) {
|
|
290
|
+
worker = `localhost:${DEFAULT_AGENT_PORT}`;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.error('No worker configured. Run: perry config worker <hostname>');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return worker;
|
|
298
|
+
}
|
|
299
|
+
program
|
|
300
|
+
.command('proxy <name> [ports...]')
|
|
301
|
+
.description('Forward ports from workspace to local machine')
|
|
302
|
+
.action(async (name, ports) => {
|
|
303
|
+
try {
|
|
304
|
+
const worker = await getWorkerWithFallback();
|
|
305
|
+
const client = await getClient();
|
|
306
|
+
const workspace = await client.getWorkspace(name);
|
|
307
|
+
if (workspace.status !== 'running') {
|
|
308
|
+
console.error(`Workspace '${name}' is not running (status: ${workspace.status})`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
if (ports.length === 0) {
|
|
312
|
+
console.log(`Workspace '${name}' is running.`);
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log('Usage: perry proxy <name> <port> [<port>...]');
|
|
315
|
+
console.log(' Examples:');
|
|
316
|
+
console.log(' perry proxy alpha 3000 # Forward port 3000');
|
|
317
|
+
console.log(' perry proxy alpha 8080:3000 # Forward local 8080 to remote 3000');
|
|
318
|
+
console.log(' perry proxy alpha 3000 5173 # Forward multiple ports');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (isLocalWorker(worker)) {
|
|
322
|
+
const containerName = getContainerName(name);
|
|
323
|
+
const containerIp = await getContainerIp(containerName);
|
|
324
|
+
if (!containerIp) {
|
|
325
|
+
console.error(`Could not get IP for container '${containerName}'`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
const forwards = ports.map(parseDockerPortForward);
|
|
329
|
+
console.log(`Forwarding ports: ${formatDockerPortForwards(forwards)}`);
|
|
330
|
+
console.log(`Container IP: ${containerIp}`);
|
|
331
|
+
console.log('Press Ctrl+C to stop.');
|
|
332
|
+
console.log('');
|
|
333
|
+
const cleanup = await startDockerProxy({
|
|
334
|
+
containerIp,
|
|
335
|
+
forwards,
|
|
336
|
+
onConnect: (port) => {
|
|
337
|
+
console.log(`Listening on 0.0.0.0:${port}`);
|
|
338
|
+
},
|
|
339
|
+
onError: (err) => {
|
|
340
|
+
console.error(`Proxy error: ${err.message}`);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const handleSignal = () => {
|
|
344
|
+
console.log('\nStopping proxy...');
|
|
345
|
+
cleanup();
|
|
346
|
+
process.exit(0);
|
|
347
|
+
};
|
|
348
|
+
process.on('SIGINT', handleSignal);
|
|
349
|
+
process.on('SIGTERM', handleSignal);
|
|
350
|
+
await new Promise(() => { });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const forwards = ports.map(parsePortForward);
|
|
354
|
+
console.log(`Forwarding ports: ${formatPortForwards(forwards)}`);
|
|
355
|
+
console.log('Press Ctrl+C to stop.');
|
|
356
|
+
console.log('');
|
|
357
|
+
await startProxy({
|
|
358
|
+
worker,
|
|
359
|
+
sshPort: workspace.ports.ssh,
|
|
360
|
+
forwards,
|
|
361
|
+
onConnect: () => {
|
|
362
|
+
console.log('Connected. Ports are now forwarded.');
|
|
363
|
+
},
|
|
364
|
+
onDisconnect: (code) => {
|
|
365
|
+
console.log(`\nDisconnected (exit code: ${code})`);
|
|
366
|
+
},
|
|
367
|
+
onError: (err) => {
|
|
368
|
+
console.error(`\nConnection error: ${err.message}`);
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
handleError(err);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
const configCmd = program.command('config').description('Manage configuration');
|
|
378
|
+
configCmd
|
|
379
|
+
.command('show')
|
|
380
|
+
.description('Show current configuration')
|
|
381
|
+
.action(async () => {
|
|
382
|
+
const clientConfig = await loadClientConfig();
|
|
383
|
+
const configDir = getConfigDir();
|
|
384
|
+
console.log('Client Configuration:');
|
|
385
|
+
console.log(` Config Dir: ${configDir}`);
|
|
386
|
+
console.log(` Worker: ${clientConfig?.worker || '(not set)'}`);
|
|
387
|
+
});
|
|
388
|
+
configCmd
|
|
389
|
+
.command('worker [hostname]')
|
|
390
|
+
.description('Get or set the worker hostname')
|
|
391
|
+
.action(async (hostname) => {
|
|
392
|
+
if (hostname) {
|
|
393
|
+
await setWorker(hostname);
|
|
394
|
+
console.log(`Worker set to: ${hostname}`);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const worker = await getWorker();
|
|
398
|
+
if (worker) {
|
|
399
|
+
console.log(worker);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log('No worker configured.');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
configCmd
|
|
407
|
+
.command('agent')
|
|
408
|
+
.description('Show agent configuration')
|
|
409
|
+
.action(async () => {
|
|
410
|
+
const configDir = getConfigDir();
|
|
411
|
+
await ensureConfigDir(configDir);
|
|
412
|
+
const config = await loadAgentConfig(configDir);
|
|
413
|
+
console.log('Agent Configuration:');
|
|
414
|
+
console.log(` Port: ${config.port}`);
|
|
415
|
+
console.log(` Environment Variables: ${Object.keys(config.credentials.env).length}`);
|
|
416
|
+
for (const key of Object.keys(config.credentials.env)) {
|
|
417
|
+
console.log(` - ${key}`);
|
|
418
|
+
}
|
|
419
|
+
console.log(` Credential Files: ${Object.keys(config.credentials.files).length}`);
|
|
420
|
+
for (const [dest, src] of Object.entries(config.credentials.files)) {
|
|
421
|
+
console.log(` - ${dest} <- ${src}`);
|
|
422
|
+
}
|
|
423
|
+
if (config.scripts.post_start) {
|
|
424
|
+
console.log(` Post-start Script: ${config.scripts.post_start}`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
program
|
|
428
|
+
.command('build')
|
|
429
|
+
.description('Build the workspace Docker image')
|
|
430
|
+
.option('--no-cache', 'Build without cache')
|
|
431
|
+
.action(async (options) => {
|
|
432
|
+
const buildContext = './perry';
|
|
433
|
+
console.log(`Building workspace image ${WORKSPACE_IMAGE}...`);
|
|
434
|
+
try {
|
|
435
|
+
await buildImage(WORKSPACE_IMAGE, buildContext, {
|
|
436
|
+
noCache: options.noCache === false ? false : !options.cache,
|
|
437
|
+
});
|
|
438
|
+
console.log('Build complete.');
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
console.error('Build failed:', err instanceof Error ? err.message : err);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
function handleError(err) {
|
|
446
|
+
if (err instanceof ApiClientError) {
|
|
447
|
+
console.error(`Error: ${err.message}`);
|
|
448
|
+
if (err.code === 'CONNECTION_FAILED') {
|
|
449
|
+
console.error('Make sure the agent is running and accessible.');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (err instanceof Error) {
|
|
453
|
+
console.error(`Error: ${err.message}`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.error('An unknown error occurred');
|
|
457
|
+
}
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
function formatUptime(seconds) {
|
|
461
|
+
const days = Math.floor(seconds / 86400);
|
|
462
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
463
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
464
|
+
const parts = [];
|
|
465
|
+
if (days > 0)
|
|
466
|
+
parts.push(`${days}d`);
|
|
467
|
+
if (hours > 0)
|
|
468
|
+
parts.push(`${hours}h`);
|
|
469
|
+
if (minutes > 0)
|
|
470
|
+
parts.push(`${minutes}m`);
|
|
471
|
+
if (parts.length === 0)
|
|
472
|
+
parts.push(`${seconds}s`);
|
|
473
|
+
return parts.join(' ');
|
|
474
|
+
}
|
|
475
|
+
program.parse();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
function getStorePath(stateDir) {
|
|
4
|
+
return join(stateDir, 'session-names.json');
|
|
5
|
+
}
|
|
6
|
+
function makeKey(workspaceName, sessionId) {
|
|
7
|
+
return `${workspaceName}:${sessionId}`;
|
|
8
|
+
}
|
|
9
|
+
async function loadStore(stateDir) {
|
|
10
|
+
const storePath = getStorePath(stateDir);
|
|
11
|
+
try {
|
|
12
|
+
const content = await readFile(storePath, 'utf-8');
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { version: 1, names: {} };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function saveStore(stateDir, store) {
|
|
20
|
+
const storePath = getStorePath(stateDir);
|
|
21
|
+
await mkdir(dirname(storePath), { recursive: true });
|
|
22
|
+
await writeFile(storePath, JSON.stringify(store, null, 2));
|
|
23
|
+
}
|
|
24
|
+
export async function getSessionName(stateDir, workspaceName, sessionId) {
|
|
25
|
+
const store = await loadStore(stateDir);
|
|
26
|
+
const key = makeKey(workspaceName, sessionId);
|
|
27
|
+
return store.names[key]?.customName ?? null;
|
|
28
|
+
}
|
|
29
|
+
export async function setSessionName(stateDir, workspaceName, sessionId, customName) {
|
|
30
|
+
const store = await loadStore(stateDir);
|
|
31
|
+
const key = makeKey(workspaceName, sessionId);
|
|
32
|
+
store.names[key] = {
|
|
33
|
+
workspaceName,
|
|
34
|
+
sessionId,
|
|
35
|
+
customName,
|
|
36
|
+
updatedAt: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
await saveStore(stateDir, store);
|
|
39
|
+
}
|
|
40
|
+
export async function deleteSessionName(stateDir, workspaceName, sessionId) {
|
|
41
|
+
const store = await loadStore(stateDir);
|
|
42
|
+
const key = makeKey(workspaceName, sessionId);
|
|
43
|
+
delete store.names[key];
|
|
44
|
+
await saveStore(stateDir, store);
|
|
45
|
+
}
|
|
46
|
+
export async function getSessionNamesForWorkspace(stateDir, workspaceName) {
|
|
47
|
+
const store = await loadStore(stateDir);
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [_key, record] of Object.entries(store.names)) {
|
|
50
|
+
if (record.workspaceName === workspaceName) {
|
|
51
|
+
result[record.sessionId] = record.customName;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|