@openclaw-cloud/agent-controller 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/commands/install.js +3 -0
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/config-file.d.ts +9 -0
  4. package/dist/config-file.js +47 -0
  5. package/dist/config-file.js.map +1 -0
  6. package/dist/connection.d.ts +1 -0
  7. package/dist/connection.js +27 -13
  8. package/dist/connection.js.map +1 -1
  9. package/dist/handlers/backup.js +7 -2
  10. package/dist/handlers/backup.js.map +1 -1
  11. package/dist/handlers/knowledge-sync.d.ts +2 -0
  12. package/dist/handlers/knowledge-sync.js +51 -0
  13. package/dist/handlers/knowledge-sync.js.map +1 -0
  14. package/dist/heartbeat.d.ts +1 -0
  15. package/dist/heartbeat.js +30 -0
  16. package/dist/heartbeat.js.map +1 -1
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/types.d.ts +2 -1
  20. package/package.json +6 -1
  21. package/.claude/cc-notify.sh +0 -32
  22. package/.claude/settings.json +0 -31
  23. package/.husky/pre-commit +0 -1
  24. package/BIZPLAN.md +0 -530
  25. package/CLAUDE.md +0 -172
  26. package/Dockerfile +0 -9
  27. package/__tests__/api.test.ts +0 -183
  28. package/__tests__/backup.test.ts +0 -145
  29. package/__tests__/board-handler.test.ts +0 -323
  30. package/__tests__/chat.test.ts +0 -191
  31. package/__tests__/config.test.ts +0 -100
  32. package/__tests__/connection.test.ts +0 -289
  33. package/__tests__/file-delete.test.ts +0 -90
  34. package/__tests__/file-write.test.ts +0 -119
  35. package/__tests__/gateway-adapter.test.ts +0 -366
  36. package/__tests__/gateway-client.test.ts +0 -272
  37. package/__tests__/handlers.test.ts +0 -150
  38. package/__tests__/heartbeat.test.ts +0 -124
  39. package/__tests__/onboarding.test.ts +0 -55
  40. package/__tests__/package-install.test.ts +0 -109
  41. package/__tests__/pair.test.ts +0 -60
  42. package/__tests__/self-update.test.ts +0 -123
  43. package/__tests__/stop.test.ts +0 -38
  44. package/jest.config.ts +0 -16
  45. package/src/api.ts +0 -62
  46. package/src/commands/install.ts +0 -68
  47. package/src/commands/self-update.ts +0 -43
  48. package/src/commands/uninstall.ts +0 -19
  49. package/src/config-file.ts +0 -56
  50. package/src/connection.ts +0 -203
  51. package/src/debug.ts +0 -11
  52. package/src/handlers/backup.ts +0 -101
  53. package/src/handlers/board-handler.ts +0 -155
  54. package/src/handlers/chat.ts +0 -79
  55. package/src/handlers/config.ts +0 -48
  56. package/src/handlers/deploy.ts +0 -32
  57. package/src/handlers/exec.ts +0 -32
  58. package/src/handlers/file-delete.ts +0 -46
  59. package/src/handlers/file-write.ts +0 -65
  60. package/src/handlers/knowledge-sync.ts +0 -53
  61. package/src/handlers/onboarding.ts +0 -19
  62. package/src/handlers/package-install.ts +0 -69
  63. package/src/handlers/pair.ts +0 -26
  64. package/src/handlers/restart.ts +0 -19
  65. package/src/handlers/stop.ts +0 -17
  66. package/src/heartbeat.ts +0 -110
  67. package/src/index.ts +0 -97
  68. package/src/openclaw/gateway-adapter.ts +0 -129
  69. package/src/openclaw/gateway-client.ts +0 -131
  70. package/src/openclaw/index.ts +0 -17
  71. package/src/openclaw/types.ts +0 -41
  72. package/src/platform/linux.ts +0 -108
  73. package/src/platform/macos.ts +0 -122
  74. package/src/platform/windows.ts +0 -92
  75. package/src/types.ts +0 -94
  76. package/tsconfig.json +0 -18
package/src/api.ts DELETED
@@ -1,62 +0,0 @@
1
- import { DEBUG, debugLog } from './debug.js';
2
-
3
- const TIMEOUT = 10_000;
4
-
5
- export interface AgentApi {
6
- get(path: string): Promise<unknown>;
7
- post(path: string, body?: unknown): Promise<Response>;
8
- publishResponse(agentId: string, data: unknown): Promise<void>;
9
- publishHeartbeat(agentId: string, payload: unknown): Promise<void>;
10
- }
11
-
12
- export function createAgentApi(backendUrl: string, agentToken: string): AgentApi {
13
- async function request(method: string, path: string, body?: unknown): Promise<Response> {
14
- const controller = new AbortController();
15
- const timeout = setTimeout(() => controller.abort(), TIMEOUT);
16
- try {
17
- debugLog('api', '→', method, path, body ? JSON.stringify(body).slice(0, 500) : '');
18
- const res = await fetch(`${backendUrl}${path}`, {
19
- method,
20
- headers: {
21
- 'Content-Type': 'application/json',
22
- 'Authorization': `Bearer ${agentToken}`,
23
- },
24
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
25
- signal: controller.signal,
26
- });
27
- if (DEBUG) {
28
- const clone = res.clone();
29
- const bodyText = await clone.text().catch(() => '');
30
- debugLog('api', '←', method, path, res.status, bodyText.slice(0, 1000));
31
- }
32
- return res;
33
- } finally {
34
- clearTimeout(timeout);
35
- }
36
- }
37
-
38
- return {
39
- async get(path: string): Promise<unknown> {
40
- const res = await request('GET', path);
41
- if (!res.ok) {
42
- throw new Error(`GET ${path} failed: HTTP ${res.status}`);
43
- }
44
- return res.json();
45
- },
46
- async post(path: string, body?: unknown): Promise<Response> {
47
- return request('POST', path, body);
48
- },
49
- async publishResponse(agentId: string, data: unknown): Promise<void> {
50
- const res = await request('POST', '/api/internal/agent-response', { agentId, data });
51
- if (!res.ok) {
52
- throw new Error(`publishResponse failed: HTTP ${res.status}`);
53
- }
54
- },
55
- async publishHeartbeat(agentId: string, payload: unknown): Promise<void> {
56
- const res = await request('POST', '/api/internal/agent-heartbeat', { agentId, payload });
57
- if (!res.ok) {
58
- throw new Error(`publishHeartbeat failed: HTTP ${res.status}`);
59
- }
60
- },
61
- };
62
- }
@@ -1,68 +0,0 @@
1
- import { createInterface } from 'node:readline';
2
- import { saveEnvFile } from '../config-file.js';
3
-
4
- async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
5
- return new Promise((resolve) => {
6
- rl.question(question, (answer) => {
7
- resolve(answer.trim());
8
- });
9
- });
10
- }
11
-
12
- async function getOrPrompt(
13
- rl: ReturnType<typeof createInterface>,
14
- envKey: string,
15
- question: string,
16
- ): Promise<string> {
17
- const fromEnv = process.env[envKey];
18
- if (fromEnv) {
19
- const display = envKey.includes('TOKEN') ? '***' : fromEnv;
20
- console.log(` ${envKey}: ${display} (from environment)`);
21
- return fromEnv;
22
- }
23
- return prompt(rl, question);
24
- }
25
-
26
- export async function install(): Promise<void> {
27
- const rl = createInterface({ input: process.stdin, output: process.stdout });
28
-
29
- console.log('OpenClaw Agent Controller — Installation Wizard');
30
- console.log('================================================');
31
- console.log('');
32
-
33
- const centrifugoUrl = await getOrPrompt(rl, 'CENTRIFUGO_URL', 'CENTRIFUGO_URL (e.g. wss://ws.openclaw-cloud.io): ');
34
- const agentToken = await getOrPrompt(rl, 'AGENT_TOKEN', 'AGENT_TOKEN: ');
35
- const agentId = await getOrPrompt(rl, 'AGENT_ID', 'AGENT_ID: ');
36
- const backendInternalUrl = await getOrPrompt(rl, 'BACKEND_INTERNAL_URL', 'BACKEND_INTERNAL_URL (e.g. https://api.openclaw-cloud.io): ');
37
-
38
- rl.close();
39
-
40
- if (!centrifugoUrl || !agentToken || !agentId || !backendInternalUrl) {
41
- console.error('All fields are required. Aborting.');
42
- process.exit(1);
43
- }
44
-
45
- const config = { centrifugoUrl, agentToken, agentId, backendInternalUrl };
46
-
47
- // Save env vars for future CLI usage (backup, knowledge_sync, etc.)
48
- await saveEnvFile(config);
49
-
50
- const platform = process.platform;
51
- console.log('');
52
- console.log(`Detected platform: ${platform}`);
53
- console.log('');
54
-
55
- if (platform === 'darwin') {
56
- const { installMacOS } = await import('../platform/macos.js');
57
- await installMacOS(config);
58
- } else if (platform === 'linux') {
59
- const { installLinux } = await import('../platform/linux.js');
60
- await installLinux(config);
61
- } else if (platform === 'win32') {
62
- const { installWindows } = await import('../platform/windows.js');
63
- await installWindows(config);
64
- } else {
65
- console.error(`Unsupported platform: ${platform}`);
66
- process.exit(1);
67
- }
68
- }
@@ -1,43 +0,0 @@
1
- import { exec } from 'node:child_process';
2
-
3
- const PACKAGE_NAME = '@openclaw-cloud/agent-controller';
4
-
5
- function execAsync(cmd: string, timeout: number): Promise<string> {
6
- return new Promise((resolve, reject) => {
7
- exec(cmd, { timeout }, (err, stdout) => {
8
- if (err) reject(err);
9
- else resolve(stdout.toString().trim());
10
- });
11
- });
12
- }
13
-
14
- export async function selfUpdate(currentVersion: string): Promise<void> {
15
- console.log(`Current version: ${currentVersion}`);
16
-
17
- let latestVersion: string;
18
- try {
19
- latestVersion = await execAsync(`npm view ${PACKAGE_NAME} version`, 10_000);
20
- } catch (err) {
21
- console.error('Failed to fetch latest version:', err instanceof Error ? err.message : err);
22
- process.exit(1);
23
- return; // unreachable in production; guards test flow when process.exit is mocked
24
- }
25
-
26
- console.log(`Latest version: ${latestVersion}`);
27
-
28
- if (currentVersion === latestVersion) {
29
- console.log('Already up to date.');
30
- return;
31
- }
32
-
33
- console.log(`Updating from ${currentVersion} to ${latestVersion}...`);
34
- try {
35
- await execAsync(`npm install -g ${PACKAGE_NAME}@latest`, 120_000);
36
- console.log(`Successfully updated to ${latestVersion}. Restarting...`);
37
- process.exit(0);
38
- return; // unreachable in production; guards test flow when process.exit is mocked
39
- } catch (err) {
40
- console.error('Update failed:', err instanceof Error ? err.message : err);
41
- process.exit(1);
42
- }
43
- }
@@ -1,19 +0,0 @@
1
- export async function uninstall(): Promise<void> {
2
- const platform = process.platform;
3
- console.log(`Uninstalling Agent Controller on platform: ${platform}`);
4
- console.log('');
5
-
6
- if (platform === 'darwin') {
7
- const { uninstallMacOS } = await import('../platform/macos.js');
8
- await uninstallMacOS();
9
- } else if (platform === 'linux') {
10
- const { uninstallLinux } = await import('../platform/linux.js');
11
- await uninstallLinux();
12
- } else if (platform === 'win32') {
13
- const { uninstallWindows } = await import('../platform/windows.js');
14
- await uninstallWindows();
15
- } else {
16
- console.error(`Unsupported platform: ${platform}`);
17
- process.exit(1);
18
- }
19
- }
@@ -1,56 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- const ENV_KEYS = ['CENTRIFUGO_URL', 'AGENT_TOKEN', 'AGENT_ID', 'BACKEND_INTERNAL_URL'] as const;
6
-
7
- export function getConfigDir(): string {
8
- const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? path.join(os.homedir(), '.openclaw');
9
- return path.join(stateDir, 'agent-controller');
10
- }
11
-
12
- export function getConfigPath(): string {
13
- return path.join(getConfigDir(), 'agent.env');
14
- }
15
-
16
- export async function saveEnvFile(config: {
17
- centrifugoUrl: string;
18
- agentToken: string;
19
- agentId: string;
20
- backendInternalUrl: string;
21
- }): Promise<void> {
22
- const dir = getConfigDir();
23
- await fs.mkdir(dir, { recursive: true });
24
-
25
- const content = [
26
- `CENTRIFUGO_URL=${config.centrifugoUrl}`,
27
- `AGENT_TOKEN=${config.agentToken}`,
28
- `AGENT_ID=${config.agentId}`,
29
- `BACKEND_INTERNAL_URL=${config.backendInternalUrl}`,
30
- ].join('\n') + '\n';
31
-
32
- await fs.writeFile(getConfigPath(), content, { mode: 0o600 });
33
- console.log(` Config saved to ${getConfigPath()}`);
34
- }
35
-
36
- export async function loadEnvFile(): Promise<void> {
37
- const filePath = getConfigPath();
38
- let content: string;
39
- try {
40
- content = await fs.readFile(filePath, 'utf-8');
41
- } catch {
42
- return; // No config file — skip
43
- }
44
-
45
- for (const line of content.split('\n')) {
46
- const trimmed = line.trim();
47
- if (!trimmed || trimmed.startsWith('#')) continue;
48
- const eq = trimmed.indexOf('=');
49
- if (eq === -1) continue;
50
- const key = trimmed.slice(0, eq).trim();
51
- const value = trimmed.slice(eq + 1).trim();
52
- if (ENV_KEYS.includes(key as typeof ENV_KEYS[number]) && !process.env[key]) {
53
- process.env[key] = value;
54
- }
55
- }
56
- }
package/src/connection.ts DELETED
@@ -1,203 +0,0 @@
1
- import { debugLog } from './debug.js';
2
- import type { AgentApi } from './api.js';
3
- import { Centrifuge, Subscription } from 'centrifuge';
4
- import WebSocket from 'ws';
5
- import type { AgentCommand, AgentResponse } from './types.js';
6
- import { handleExec } from './handlers/exec.js';
7
- import { handleRestart } from './handlers/restart.js';
8
- import { handleDeploy } from './handlers/deploy.js';
9
- import { handleConfig } from './handlers/config.js';
10
- import { handlePair } from './handlers/pair.js';
11
- import { handleStop } from './handlers/stop.js';
12
- import { handleBackup } from './handlers/backup.js';
13
- import { handleChatListSessions, handleChatHistory, handleChatSend } from './handlers/chat.js';
14
- import { handlePackageInstall } from './handlers/package-install.js';
15
- import { handleFileWrite } from './handlers/file-write.js';
16
- import { handleFileDelete } from './handlers/file-delete.js';
17
- import { handleOnboardingComplete } from './handlers/onboarding.js';
18
- import { handleKnowledgeSync } from './handlers/knowledge-sync.js';
19
- import { selfUpdate } from './commands/self-update.js';
20
-
21
- export interface ConnectionOptions {
22
- url: string;
23
- token: string;
24
- agentId: string;
25
- backendUrl: string;
26
- api: AgentApi;
27
- version: string;
28
- }
29
-
30
- function buildHandlers(version: string): Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> {
31
- return {
32
- exec: handleExec,
33
- restart: handleRestart,
34
- deploy: handleDeploy,
35
- config: handleConfig,
36
- pair: handlePair,
37
- stop: handleStop,
38
- backup: handleBackup,
39
- package_install: handlePackageInstall,
40
- file_write: handleFileWrite,
41
- file_delete: handleFileDelete,
42
- onboarding_complete: handleOnboardingComplete,
43
- knowledge_sync: handleKnowledgeSync,
44
- self_update: (cmd: AgentCommand) =>
45
- selfUpdate(version)
46
- .then(() => ({ id: cmd.id, type: cmd.type, success: true, data: {} }))
47
- .catch((err: unknown) => ({
48
- id: cmd.id,
49
- type: cmd.type,
50
- success: false,
51
- error: err instanceof Error ? err.message : String(err),
52
- })),
53
- };
54
- }
55
-
56
- async function fetchCentrifugoToken(backendUrl: string, agentToken: string, agentId: string): Promise<string> {
57
- const controller = new AbortController();
58
- const timeout = setTimeout(() => controller.abort(), 5000);
59
- try {
60
- const res = await fetch(`${backendUrl}/api/internal/centrifugo-token`, {
61
- method: 'POST',
62
- headers: {
63
- 'Content-Type': 'application/json',
64
- 'Authorization': `Bearer ${agentToken}`,
65
- },
66
- body: JSON.stringify({ agentId }),
67
- signal: controller.signal,
68
- });
69
- if (!res.ok) {
70
- throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
71
- }
72
- const data = await res.json() as { token: string };
73
- return data.token;
74
- } finally {
75
- clearTimeout(timeout);
76
- }
77
- }
78
-
79
- export function createConnection(opts: ConnectionOptions): {
80
- client: Centrifuge;
81
- commandSub: Subscription;
82
- } {
83
- // Dynamic JWT: fetch fresh token on every connect/reconnect
84
- const getToken = async (): Promise<string> => {
85
- console.log('Fetching Centrifugo JWT from backend...');
86
- try {
87
- const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
88
- console.log('Got Centrifugo JWT');
89
- return token;
90
- } catch (err) {
91
- console.error('Failed to fetch Centrifugo JWT:', err instanceof Error ? err.message : err);
92
- // Retry once before giving up — centrifuge will handle reconnect backoff
93
- try {
94
- const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
95
- console.log('Got Centrifugo JWT (retry)');
96
- return token;
97
- } catch (retryErr) {
98
- console.error('JWT fetch retry failed:', retryErr instanceof Error ? retryErr.message : retryErr);
99
- throw retryErr;
100
- }
101
- }
102
- };
103
-
104
- const centrifugeOpts: Record<string, unknown> = {
105
- websocket: WebSocket,
106
- name: 'agent-controller',
107
- getToken,
108
- minReconnectDelay: 1000,
109
- maxReconnectDelay: 30000,
110
- };
111
-
112
- const client = new Centrifuge(opts.url, centrifugeOpts);
113
-
114
- const commandChannel = `agent:${opts.agentId}`;
115
- const commandSub = client.newSubscription(commandChannel);
116
- const handlers = buildHandlers(opts.version);
117
-
118
- commandSub.on('publication', async (ctx) => {
119
- console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
120
- debugLog('connection', 'publication', commandChannel, JSON.stringify(ctx.data).slice(0, 500));
121
- const command = ctx.data as AgentCommand;
122
- if (!command || !command.type) {
123
- console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
124
- return;
125
- }
126
-
127
- // Chat commands — handle before generic dispatch
128
- if (command.type === 'chat_list_sessions' || command.type === 'chat_history' || command.type === 'chat_send') {
129
- const publishFn = async (data: unknown): Promise<void> => { await opts.api.publishResponse(opts.agentId, data); };
130
- console.log(`[WS] Handling chat command: type=${command.type} id=${command.id}`);
131
- switch (command.type) {
132
- case 'chat_list_sessions':
133
- handleChatListSessions(command, publishFn).catch(console.error);
134
- break;
135
- case 'chat_history':
136
- handleChatHistory(command, publishFn).catch(console.error);
137
- break;
138
- case 'chat_send':
139
- handleChatSend(command, publishFn, opts.agentId).catch(console.error);
140
- break;
141
- }
142
- return;
143
- }
144
-
145
- const handler = handlers[command.type];
146
- if (!handler) {
147
- console.error(`[WS] Unknown command type: ${command.type}`);
148
- return;
149
- }
150
- console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
151
-
152
- try {
153
- const response = await handler(command);
154
- await opts.api.publishResponse(opts.agentId, response);
155
- } catch (err) {
156
- const errorResponse: AgentResponse = {
157
- id: command.id,
158
- type: command.type,
159
- success: false,
160
- error: err instanceof Error ? err.message : String(err),
161
- };
162
- await opts.api.publishResponse(opts.agentId, errorResponse);
163
- }
164
- });
165
-
166
- client.on('connected', (ctx) => {
167
- console.log(`Connected to Centrifugo via ${ctx.transport}`);
168
- debugLog('connection', 'connected', ctx);
169
- });
170
-
171
- client.on('disconnected', (ctx) => {
172
- console.log(`Disconnected: ${ctx.reason} (code ${ctx.code})`);
173
- debugLog('connection', 'disconnected', ctx);
174
- });
175
-
176
- client.on('connecting', (ctx) => {
177
- console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
178
- debugLog('connection', 'connecting', ctx);
179
- });
180
-
181
- commandSub.on('subscribed', (ctx) => {
182
- console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
183
- debugLog('connection', 'subscribed', commandChannel, ctx);
184
- });
185
-
186
- commandSub.on('subscribing', (ctx) => {
187
- console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
188
- debugLog('connection', 'subscribing', commandChannel, ctx);
189
- });
190
-
191
- commandSub.on('error', (ctx) => {
192
- console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
193
- });
194
-
195
- commandSub.on('unsubscribed', (ctx) => {
196
- console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
197
- });
198
-
199
- commandSub.subscribe();
200
- client.connect();
201
-
202
- return { client, commandSub };
203
- }
package/src/debug.ts DELETED
@@ -1,11 +0,0 @@
1
- export const DEBUG = !!(process.env.AC_DEBUG || process.env.DEBUG);
2
-
3
- export function debugLog(prefix: string, ...args: unknown[]): void {
4
- if (!DEBUG) return;
5
- console.log(`[debug][${prefix}]`, ...args);
6
- }
7
-
8
- export function maskToken(token: string): string {
9
- if (!token) return '';
10
- return token.slice(0, 10) + '...';
11
- }
@@ -1,101 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import fs from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import type { AgentCommand, AgentResponse } from '../types.js';
6
-
7
- function getWorkspaceDir(): string {
8
- const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? path.join(os.homedir(), '.openclaw');
9
- return path.join(stateDir, 'workspace');
10
- }
11
-
12
- const BACKUP_DIR = '/tmp';
13
- const TAR_TIMEOUT = 120_000;
14
- const UPLOAD_TIMEOUT = 120_000;
15
-
16
- export function handleBackup(command: AgentCommand): Promise<AgentResponse> {
17
- const uploadUrl = command.payload.uploadUrl as string;
18
- const token = command.payload.token as string;
19
- if (!uploadUrl) {
20
- return Promise.resolve({
21
- id: command.id,
22
- type: 'backup',
23
- success: false,
24
- error: 'Missing "uploadUrl" in payload',
25
- });
26
- }
27
-
28
- const timestamp = Date.now();
29
- const filename = `backup-${timestamp}.tar.gz`;
30
- const archivePath = path.join(BACKUP_DIR, filename);
31
-
32
- return createArchive(archivePath)
33
- .then(() => uploadArchive(archivePath, uploadUrl, token))
34
- .then(async (size) => {
35
- await cleanup(archivePath);
36
- return {
37
- id: command.id,
38
- type: 'backup' as const,
39
- success: true,
40
- data: { size, filename },
41
- };
42
- })
43
- .catch(async (err) => {
44
- await cleanup(archivePath);
45
- return {
46
- id: command.id,
47
- type: 'backup' as const,
48
- success: false,
49
- error: err instanceof Error ? err.message : String(err),
50
- };
51
- });
52
- }
53
-
54
- function createArchive(archivePath: string): Promise<void> {
55
- const workspaceDir = getWorkspaceDir();
56
- return new Promise((resolve, reject) => {
57
- exec(
58
- `tar -czf ${archivePath} -C ${path.dirname(workspaceDir)} ${path.basename(workspaceDir)}`,
59
- { timeout: TAR_TIMEOUT },
60
- (error, _stdout, stderr) => {
61
- if (error) {
62
- reject(new Error(`tar failed: ${stderr.toString() || error.message}`));
63
- } else {
64
- resolve();
65
- }
66
- },
67
- );
68
- });
69
- }
70
-
71
- async function uploadArchive(archivePath: string, uploadUrl: string, token?: string): Promise<number> {
72
- const stat = await fs.stat(archivePath);
73
- const fileBuffer = await fs.readFile(archivePath);
74
-
75
- const headers: Record<string, string> = { 'Content-Type': 'application/gzip' };
76
- if (token) {
77
- headers['Authorization'] = `Bearer ${token}`;
78
- }
79
-
80
- const response = await fetch(uploadUrl, {
81
- method: 'POST',
82
- headers,
83
- body: fileBuffer,
84
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
85
- });
86
-
87
- if (!response.ok) {
88
- const text = await response.text().catch(() => '');
89
- throw new Error(`Upload failed: ${response.status} ${response.statusText} ${text}`);
90
- }
91
-
92
- return stat.size;
93
- }
94
-
95
- async function cleanup(archivePath: string): Promise<void> {
96
- try {
97
- await fs.unlink(archivePath);
98
- } catch {
99
- // ignore cleanup errors
100
- }
101
- }