agent-relay 1.0.21 → 1.1.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/dist/bridge/shadow-cli.d.ts +17 -0
- package/dist/bridge/shadow-cli.d.ts.map +1 -0
- package/dist/bridge/shadow-cli.js +75 -0
- package/dist/bridge/shadow-cli.js.map +1 -0
- package/dist/bridge/shadow-config.d.ts +87 -0
- package/dist/bridge/shadow-config.d.ts.map +1 -0
- package/dist/bridge/shadow-config.js +134 -0
- package/dist/bridge/shadow-config.js.map +1 -0
- package/dist/bridge/spawner.d.ts +15 -1
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +164 -4
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +55 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +796 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +19 -0
- package/dist/cloud/api/auth.d.ts.map +1 -0
- package/dist/cloud/api/auth.js +216 -0
- package/dist/cloud/api/auth.js.map +1 -0
- package/dist/cloud/api/billing.d.ts +17 -0
- package/dist/cloud/api/billing.d.ts.map +1 -0
- package/dist/cloud/api/billing.js +353 -0
- package/dist/cloud/api/billing.js.map +1 -0
- package/dist/cloud/api/coordinators.d.ts +8 -0
- package/dist/cloud/api/coordinators.d.ts.map +1 -0
- package/dist/cloud/api/coordinators.js +347 -0
- package/dist/cloud/api/coordinators.js.map +1 -0
- package/dist/cloud/api/daemons.d.ts +12 -0
- package/dist/cloud/api/daemons.d.ts.map +1 -0
- package/dist/cloud/api/daemons.js +320 -0
- package/dist/cloud/api/daemons.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +36 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
- package/dist/cloud/api/middleware/planLimits.js +164 -0
- package/dist/cloud/api/middleware/planLimits.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +8 -0
- package/dist/cloud/api/onboarding.d.ts.map +1 -0
- package/dist/cloud/api/onboarding.js +407 -0
- package/dist/cloud/api/onboarding.js.map +1 -0
- package/dist/cloud/api/providers.d.ts +7 -0
- package/dist/cloud/api/providers.d.ts.map +1 -0
- package/dist/cloud/api/providers.js +435 -0
- package/dist/cloud/api/providers.js.map +1 -0
- package/dist/cloud/api/repos.d.ts +7 -0
- package/dist/cloud/api/repos.d.ts.map +1 -0
- package/dist/cloud/api/repos.js +314 -0
- package/dist/cloud/api/repos.js.map +1 -0
- package/dist/cloud/api/teams.d.ts +7 -0
- package/dist/cloud/api/teams.d.ts.map +1 -0
- package/dist/cloud/api/teams.js +279 -0
- package/dist/cloud/api/teams.js.map +1 -0
- package/dist/cloud/api/usage.d.ts +7 -0
- package/dist/cloud/api/usage.d.ts.map +1 -0
- package/dist/cloud/api/usage.js +98 -0
- package/dist/cloud/api/usage.js.map +1 -0
- package/dist/cloud/api/workspaces.d.ts +7 -0
- package/dist/cloud/api/workspaces.d.ts.map +1 -0
- package/dist/cloud/api/workspaces.js +510 -0
- package/dist/cloud/api/workspaces.js.map +1 -0
- package/dist/cloud/billing/index.d.ts +9 -0
- package/dist/cloud/billing/index.d.ts.map +1 -0
- package/dist/cloud/billing/index.js +9 -0
- package/dist/cloud/billing/index.js.map +1 -0
- package/dist/cloud/billing/plans.d.ts +39 -0
- package/dist/cloud/billing/plans.d.ts.map +1 -0
- package/dist/cloud/billing/plans.js +232 -0
- package/dist/cloud/billing/plans.js.map +1 -0
- package/dist/cloud/billing/service.d.ts +80 -0
- package/dist/cloud/billing/service.d.ts.map +1 -0
- package/dist/cloud/billing/service.js +388 -0
- package/dist/cloud/billing/service.js.map +1 -0
- package/dist/cloud/billing/types.d.ts +135 -0
- package/dist/cloud/billing/types.d.ts.map +1 -0
- package/dist/cloud/billing/types.js +7 -0
- package/dist/cloud/billing/types.js.map +1 -0
- package/dist/cloud/config.d.ts +59 -0
- package/dist/cloud/config.d.ts.map +1 -0
- package/dist/cloud/config.js +83 -0
- package/dist/cloud/config.js.map +1 -0
- package/dist/cloud/db/drizzle.d.ts +132 -0
- package/dist/cloud/db/drizzle.d.ts.map +1 -0
- package/dist/cloud/db/drizzle.js +613 -0
- package/dist/cloud/db/drizzle.js.map +1 -0
- package/dist/cloud/db/index.d.ts +30 -0
- package/dist/cloud/db/index.d.ts.map +1 -0
- package/dist/cloud/db/index.js +44 -0
- package/dist/cloud/db/index.js.map +1 -0
- package/dist/cloud/db/schema.d.ts +1792 -0
- package/dist/cloud/db/schema.d.ts.map +1 -0
- package/dist/cloud/db/schema.js +234 -0
- package/dist/cloud/db/schema.js.map +1 -0
- package/dist/cloud/index.d.ts +11 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +37 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/provisioner/index.d.ts +51 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -0
- package/dist/cloud/provisioner/index.js +676 -0
- package/dist/cloud/provisioner/index.js.map +1 -0
- package/dist/cloud/server.d.ts +16 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +190 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/services/coordinator.d.ts +62 -0
- package/dist/cloud/services/coordinator.d.ts.map +1 -0
- package/dist/cloud/services/coordinator.js +389 -0
- package/dist/cloud/services/coordinator.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +110 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -0
- package/dist/cloud/services/planLimits.js +254 -0
- package/dist/cloud/services/planLimits.js.map +1 -0
- package/dist/cloud/vault/index.d.ts +76 -0
- package/dist/cloud/vault/index.d.ts.map +1 -0
- package/dist/cloud/vault/index.js +219 -0
- package/dist/cloud/vault/index.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +87 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -0
- package/dist/daemon/agent-manager.js +412 -0
- package/dist/daemon/agent-manager.js.map +1 -0
- package/dist/daemon/agent-registry.d.ts +2 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +3 -0
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +69 -0
- package/dist/daemon/api.d.ts.map +1 -0
- package/dist/daemon/api.js +425 -0
- package/dist/daemon/api.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts +101 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -0
- package/dist/daemon/cloud-sync.js +261 -0
- package/dist/daemon/cloud-sync.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +6 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts +155 -0
- package/dist/daemon/orchestrator.d.ts.map +1 -0
- package/dist/daemon/orchestrator.js +736 -0
- package/dist/daemon/orchestrator.js.map +1 -0
- package/dist/daemon/router.d.ts +24 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +71 -1
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +37 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +191 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/types.d.ts +127 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +6 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/workspace-manager.d.ts +75 -0
- package/dist/daemon/workspace-manager.d.ts.map +1 -0
- package/dist/daemon/workspace-manager.js +289 -0
- package/dist/daemon/workspace-manager.js.map +1 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-b6edd4dde8d08194.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-c2f423b9c9f4591b.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{webpack-c81f7fd28659d64f.js → webpack-a5acc2831d094776.js} +1 -1
- package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +1 -0
- package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app.html +14 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -515
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts.map +1 -1
- package/dist/dashboard-server/metrics.js +3 -2
- package/dist/dashboard-server/metrics.js.map +1 -1
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +1279 -56
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/protocol/types.d.ts +10 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/resiliency/context-persistence.d.ts +140 -0
- package/dist/resiliency/context-persistence.d.ts.map +1 -0
- package/dist/resiliency/context-persistence.js +397 -0
- package/dist/resiliency/context-persistence.js.map +1 -0
- package/dist/resiliency/health-monitor.d.ts +97 -0
- package/dist/resiliency/health-monitor.d.ts.map +1 -0
- package/dist/resiliency/health-monitor.js +291 -0
- package/dist/resiliency/health-monitor.js.map +1 -0
- package/dist/resiliency/index.d.ts +63 -0
- package/dist/resiliency/index.d.ts.map +1 -0
- package/dist/resiliency/index.js +63 -0
- package/dist/resiliency/index.js.map +1 -0
- package/dist/resiliency/logger.d.ts +114 -0
- package/dist/resiliency/logger.d.ts.map +1 -0
- package/dist/resiliency/logger.js +250 -0
- package/dist/resiliency/logger.js.map +1 -0
- package/dist/resiliency/metrics.d.ts +115 -0
- package/dist/resiliency/metrics.d.ts.map +1 -0
- package/dist/resiliency/metrics.js +239 -0
- package/dist/resiliency/metrics.js.map +1 -0
- package/dist/resiliency/provider-context.d.ts +100 -0
- package/dist/resiliency/provider-context.d.ts.map +1 -0
- package/dist/resiliency/provider-context.js +360 -0
- package/dist/resiliency/provider-context.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +109 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -0
- package/dist/resiliency/supervisor.js +337 -0
- package/dist/resiliency/supervisor.js.map +1 -0
- package/dist/storage/adapter.d.ts +2 -0
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +12 -2
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +18 -14
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/wrapper/client.d.ts +16 -1
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +32 -1
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +3 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +121 -18
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +28 -1
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +166 -30
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +5 -0
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +58 -18
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +652 -0
- package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
- package/docs/TESTING_PRESENCE_FEATURES.md +327 -0
- package/docs/agent-relay-snippet.md +107 -4
- package/docs/guides/CLOUD.md +236 -0
- package/docs/guides/LOCAL.md +535 -0
- package/docs/guides/SELF-HOSTED.md +494 -0
- package/docs/proposals/shadow-as-subagent.md +765 -0
- package/docs/proposals/slack-bot-integration.md +1457 -0
- package/package.json +33 -4
- package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-49055e5d2b5e34ec.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
- package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
- /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b2cd8d6485aacf2b.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-8f3f26864ce515e5.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-0b990dbb71d72a98.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Relay Cloud - Workspace Provisioner
|
|
3
|
+
*
|
|
4
|
+
* One-click provisioning for compute resources (Fly.io, Railway, Docker).
|
|
5
|
+
*/
|
|
6
|
+
import { getConfig } from '../config.js';
|
|
7
|
+
import { db } from '../db/index.js';
|
|
8
|
+
import { vault } from '../vault/index.js';
|
|
9
|
+
const WORKSPACE_PORT = 3888;
|
|
10
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
11
|
+
async function loadCredentialToken(userId, provider) {
|
|
12
|
+
try {
|
|
13
|
+
const cred = await vault.getCredential(userId, provider);
|
|
14
|
+
if (cred?.accessToken) {
|
|
15
|
+
return cred.accessToken;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn(`Failed to decrypt ${provider} credential from vault; trying raw storage fallback`, error);
|
|
20
|
+
const raw = await db.credentials.findByUserAndProvider(userId, provider);
|
|
21
|
+
return raw?.accessToken ?? null;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async function wait(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
async function fetchWithRetry(url, options = {}) {
|
|
29
|
+
const retries = options.retries ?? 2;
|
|
30
|
+
let attempt = 0;
|
|
31
|
+
while (attempt <= retries) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
if (!response.ok && response.status >= 500 && attempt < retries) {
|
|
38
|
+
attempt += 1;
|
|
39
|
+
await wait(500 * attempt);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (attempt >= retries) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
attempt += 1;
|
|
50
|
+
await wait(500 * attempt);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error('fetchWithRetry exhausted retries');
|
|
54
|
+
}
|
|
55
|
+
async function softHealthCheck(url) {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetchWithRetry(`${url.replace(/\/$/, '')}/health`, { method: 'GET', retries: 1 });
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
console.warn(`[health] Non-200 from ${url}/health: ${res.status}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.warn(`[health] Failed to reach ${url}/health`, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fly.io provisioner
|
|
68
|
+
*/
|
|
69
|
+
class FlyProvisioner {
|
|
70
|
+
apiToken;
|
|
71
|
+
org;
|
|
72
|
+
region;
|
|
73
|
+
workspaceDomain;
|
|
74
|
+
constructor() {
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
if (!config.compute.fly) {
|
|
77
|
+
throw new Error('Fly.io configuration missing');
|
|
78
|
+
}
|
|
79
|
+
this.apiToken = config.compute.fly.apiToken;
|
|
80
|
+
this.org = config.compute.fly.org;
|
|
81
|
+
this.region = config.compute.fly.region || 'sjc';
|
|
82
|
+
this.workspaceDomain = config.compute.fly.workspaceDomain;
|
|
83
|
+
}
|
|
84
|
+
async provision(workspace, credentials) {
|
|
85
|
+
const appName = `ar-${workspace.id.substring(0, 8)}`;
|
|
86
|
+
// Create Fly app
|
|
87
|
+
const createResponse = await fetchWithRetry('https://api.machines.dev/v1/apps', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
app_name: appName,
|
|
95
|
+
org_slug: this.org,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
// Set secrets (credentials)
|
|
99
|
+
const secrets = {};
|
|
100
|
+
for (const [provider, token] of credentials) {
|
|
101
|
+
secrets[`${provider.toUpperCase()}_TOKEN`] = token;
|
|
102
|
+
}
|
|
103
|
+
await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/secrets`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(secrets),
|
|
110
|
+
});
|
|
111
|
+
// If custom workspace domain is configured, add certificate
|
|
112
|
+
const customHostname = this.workspaceDomain
|
|
113
|
+
? `${appName}.${this.workspaceDomain}`
|
|
114
|
+
: null;
|
|
115
|
+
if (customHostname) {
|
|
116
|
+
await this.allocateCertificate(appName, customHostname);
|
|
117
|
+
}
|
|
118
|
+
// Create machine with auto-stop/start for cost optimization
|
|
119
|
+
const machineResponse = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
region: this.region,
|
|
127
|
+
config: {
|
|
128
|
+
image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
|
|
129
|
+
env: {
|
|
130
|
+
WORKSPACE_ID: workspace.id,
|
|
131
|
+
SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
|
|
132
|
+
MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
|
|
133
|
+
REPOSITORIES: (workspace.config.repositories ?? []).join(','),
|
|
134
|
+
PROVIDERS: (workspace.config.providers ?? []).join(','),
|
|
135
|
+
PORT: String(WORKSPACE_PORT),
|
|
136
|
+
AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
|
|
137
|
+
},
|
|
138
|
+
services: [
|
|
139
|
+
{
|
|
140
|
+
ports: [
|
|
141
|
+
{ port: 443, handlers: ['tls', 'http'] },
|
|
142
|
+
{ port: 80, handlers: ['http'] },
|
|
143
|
+
],
|
|
144
|
+
protocol: 'tcp',
|
|
145
|
+
internal_port: WORKSPACE_PORT,
|
|
146
|
+
// Auto-stop after 5 minutes of inactivity
|
|
147
|
+
auto_stop_machines: true,
|
|
148
|
+
auto_start_machines: true,
|
|
149
|
+
min_machines_running: 0,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
guest: {
|
|
153
|
+
cpu_kind: 'shared',
|
|
154
|
+
cpus: 1,
|
|
155
|
+
memory_mb: 512,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
if (!machineResponse.ok) {
|
|
161
|
+
const error = await machineResponse.text();
|
|
162
|
+
throw new Error(`Failed to create Fly machine: ${error}`);
|
|
163
|
+
}
|
|
164
|
+
const machine = (await machineResponse.json());
|
|
165
|
+
// Return custom domain URL if configured, otherwise default fly.dev
|
|
166
|
+
const publicUrl = customHostname
|
|
167
|
+
? `https://${customHostname}`
|
|
168
|
+
: `https://${appName}.fly.dev`;
|
|
169
|
+
await softHealthCheck(publicUrl);
|
|
170
|
+
return {
|
|
171
|
+
computeId: machine.id,
|
|
172
|
+
publicUrl,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Allocate SSL certificate for custom domain
|
|
177
|
+
*/
|
|
178
|
+
async allocateCertificate(appName, hostname) {
|
|
179
|
+
const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/certificates`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify({ hostname }),
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const error = await response.text();
|
|
189
|
+
// Don't fail if cert already exists
|
|
190
|
+
if (!error.includes('already exists')) {
|
|
191
|
+
throw new Error(`Failed to allocate certificate for ${hostname}: ${error}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async deprovision(workspace) {
|
|
196
|
+
const appName = `ar-${workspace.id.substring(0, 8)}`;
|
|
197
|
+
await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}`, {
|
|
198
|
+
method: 'DELETE',
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async getStatus(workspace) {
|
|
205
|
+
if (!workspace.computeId)
|
|
206
|
+
return 'error';
|
|
207
|
+
const appName = `ar-${workspace.id.substring(0, 8)}`;
|
|
208
|
+
const response = await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}`, {
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok)
|
|
214
|
+
return 'error';
|
|
215
|
+
const machine = await response.json();
|
|
216
|
+
switch (machine.state) {
|
|
217
|
+
case 'started':
|
|
218
|
+
return 'running';
|
|
219
|
+
case 'stopped':
|
|
220
|
+
return 'stopped';
|
|
221
|
+
case 'created':
|
|
222
|
+
case 'starting':
|
|
223
|
+
return 'provisioning';
|
|
224
|
+
default:
|
|
225
|
+
return 'error';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async restart(workspace) {
|
|
229
|
+
if (!workspace.computeId)
|
|
230
|
+
return;
|
|
231
|
+
const appName = `ar-${workspace.id.substring(0, 8)}`;
|
|
232
|
+
await fetchWithRetry(`https://api.machines.dev/v1/apps/${appName}/machines/${workspace.computeId}/restart`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Railway provisioner
|
|
242
|
+
*/
|
|
243
|
+
class RailwayProvisioner {
|
|
244
|
+
apiToken;
|
|
245
|
+
constructor() {
|
|
246
|
+
const config = getConfig();
|
|
247
|
+
if (!config.compute.railway) {
|
|
248
|
+
throw new Error('Railway configuration missing');
|
|
249
|
+
}
|
|
250
|
+
this.apiToken = config.compute.railway.apiToken;
|
|
251
|
+
}
|
|
252
|
+
async provision(workspace, credentials) {
|
|
253
|
+
// Create project
|
|
254
|
+
const projectResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
258
|
+
'Content-Type': 'application/json',
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
query: `
|
|
262
|
+
mutation CreateProject($input: ProjectCreateInput!) {
|
|
263
|
+
projectCreate(input: $input) {
|
|
264
|
+
id
|
|
265
|
+
name
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
`,
|
|
269
|
+
variables: {
|
|
270
|
+
input: {
|
|
271
|
+
name: `agent-relay-${workspace.id.substring(0, 8)}`,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
const projectData = await projectResponse.json();
|
|
277
|
+
const projectId = projectData.data.projectCreate.id;
|
|
278
|
+
// Deploy service
|
|
279
|
+
const serviceResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: {
|
|
282
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
283
|
+
'Content-Type': 'application/json',
|
|
284
|
+
},
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
query: `
|
|
287
|
+
mutation CreateService($input: ServiceCreateInput!) {
|
|
288
|
+
serviceCreate(input: $input) {
|
|
289
|
+
id
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
`,
|
|
293
|
+
variables: {
|
|
294
|
+
input: {
|
|
295
|
+
projectId,
|
|
296
|
+
name: 'workspace',
|
|
297
|
+
source: {
|
|
298
|
+
image: 'ghcr.io/khaliqgant/agent-relay-workspace:latest',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
const serviceData = await serviceResponse.json();
|
|
305
|
+
const serviceId = serviceData.data.serviceCreate.id;
|
|
306
|
+
// Set environment variables
|
|
307
|
+
const envVars = {
|
|
308
|
+
WORKSPACE_ID: workspace.id,
|
|
309
|
+
SUPERVISOR_ENABLED: String(workspace.config.supervisorEnabled ?? false),
|
|
310
|
+
MAX_AGENTS: String(workspace.config.maxAgents ?? 10),
|
|
311
|
+
REPOSITORIES: (workspace.config.repositories ?? []).join(','),
|
|
312
|
+
PROVIDERS: (workspace.config.providers ?? []).join(','),
|
|
313
|
+
PORT: String(WORKSPACE_PORT),
|
|
314
|
+
AGENT_RELAY_DASHBOARD_PORT: String(WORKSPACE_PORT),
|
|
315
|
+
};
|
|
316
|
+
for (const [provider, token] of credentials) {
|
|
317
|
+
envVars[`${provider.toUpperCase()}_TOKEN`] = token;
|
|
318
|
+
}
|
|
319
|
+
await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: {
|
|
322
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
323
|
+
'Content-Type': 'application/json',
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
query: `
|
|
327
|
+
mutation SetVariables($input: VariableCollectionUpsertInput!) {
|
|
328
|
+
variableCollectionUpsert(input: $input)
|
|
329
|
+
}
|
|
330
|
+
`,
|
|
331
|
+
variables: {
|
|
332
|
+
input: {
|
|
333
|
+
projectId,
|
|
334
|
+
serviceId,
|
|
335
|
+
variables: envVars,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
// Generate domain
|
|
341
|
+
const domainResponse = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: {
|
|
344
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
345
|
+
'Content-Type': 'application/json',
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
query: `
|
|
349
|
+
mutation CreateDomain($input: ServiceDomainCreateInput!) {
|
|
350
|
+
serviceDomainCreate(input: $input) {
|
|
351
|
+
domain
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
`,
|
|
355
|
+
variables: {
|
|
356
|
+
input: {
|
|
357
|
+
serviceId,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
const domainData = await domainResponse.json();
|
|
363
|
+
const domain = domainData.data.serviceDomainCreate.domain;
|
|
364
|
+
await softHealthCheck(`https://${domain}`);
|
|
365
|
+
return {
|
|
366
|
+
computeId: projectId,
|
|
367
|
+
publicUrl: `https://${domain}`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async deprovision(workspace) {
|
|
371
|
+
if (!workspace.computeId)
|
|
372
|
+
return;
|
|
373
|
+
await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
headers: {
|
|
376
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
},
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
query: `
|
|
381
|
+
mutation DeleteProject($id: String!) {
|
|
382
|
+
projectDelete(id: $id)
|
|
383
|
+
}
|
|
384
|
+
`,
|
|
385
|
+
variables: {
|
|
386
|
+
id: workspace.computeId,
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async getStatus(workspace) {
|
|
392
|
+
if (!workspace.computeId)
|
|
393
|
+
return 'error';
|
|
394
|
+
const response = await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: {
|
|
397
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
query: `
|
|
402
|
+
query GetProject($id: String!) {
|
|
403
|
+
project(id: $id) {
|
|
404
|
+
deployments {
|
|
405
|
+
edges {
|
|
406
|
+
node {
|
|
407
|
+
status
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`,
|
|
414
|
+
variables: {
|
|
415
|
+
id: workspace.computeId,
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
const data = await response.json();
|
|
420
|
+
const deployments = data.data?.project?.deployments?.edges;
|
|
421
|
+
if (!deployments || deployments.length === 0)
|
|
422
|
+
return 'provisioning';
|
|
423
|
+
const latestStatus = deployments[0].node.status;
|
|
424
|
+
switch (latestStatus) {
|
|
425
|
+
case 'SUCCESS':
|
|
426
|
+
return 'running';
|
|
427
|
+
case 'BUILDING':
|
|
428
|
+
case 'DEPLOYING':
|
|
429
|
+
return 'provisioning';
|
|
430
|
+
case 'CRASHED':
|
|
431
|
+
case 'FAILED':
|
|
432
|
+
return 'error';
|
|
433
|
+
default:
|
|
434
|
+
return 'stopped';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async restart(workspace) {
|
|
438
|
+
// Railway doesn't have a direct restart - redeploy instead
|
|
439
|
+
if (!workspace.computeId)
|
|
440
|
+
return;
|
|
441
|
+
await fetchWithRetry('https://backboard.railway.app/graphql/v2', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: {
|
|
444
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
445
|
+
'Content-Type': 'application/json',
|
|
446
|
+
},
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
query: `
|
|
449
|
+
mutation RedeployService($input: DeploymentTriggerInput!) {
|
|
450
|
+
deploymentTrigger(input: $input)
|
|
451
|
+
}
|
|
452
|
+
`,
|
|
453
|
+
variables: {
|
|
454
|
+
input: {
|
|
455
|
+
projectId: workspace.computeId,
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
}),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Local Docker provisioner (for development/self-hosted)
|
|
464
|
+
*/
|
|
465
|
+
class DockerProvisioner {
|
|
466
|
+
async provision(workspace, credentials) {
|
|
467
|
+
const containerName = `ar-${workspace.id.substring(0, 8)}`;
|
|
468
|
+
// Build environment variables
|
|
469
|
+
const envArgs = [
|
|
470
|
+
`-e WORKSPACE_ID=${workspace.id}`,
|
|
471
|
+
`-e SUPERVISOR_ENABLED=${workspace.config.supervisorEnabled ?? false}`,
|
|
472
|
+
`-e MAX_AGENTS=${workspace.config.maxAgents ?? 10}`,
|
|
473
|
+
`-e REPOSITORIES=${(workspace.config.repositories ?? []).join(',')}`,
|
|
474
|
+
`-e PROVIDERS=${(workspace.config.providers ?? []).join(',')}`,
|
|
475
|
+
`-e PORT=${WORKSPACE_PORT}`,
|
|
476
|
+
`-e AGENT_RELAY_DASHBOARD_PORT=${WORKSPACE_PORT}`,
|
|
477
|
+
];
|
|
478
|
+
for (const [provider, token] of credentials) {
|
|
479
|
+
envArgs.push(`-e ${provider.toUpperCase()}_TOKEN=${token}`);
|
|
480
|
+
}
|
|
481
|
+
// Run container
|
|
482
|
+
const { execSync } = await import('child_process');
|
|
483
|
+
const hostPort = 3000 + Math.floor(Math.random() * 1000);
|
|
484
|
+
try {
|
|
485
|
+
execSync(`docker run -d --name ${containerName} -p ${hostPort}:${WORKSPACE_PORT} ${envArgs.join(' ')} ghcr.io/khaliqgant/agent-relay-workspace:latest`, { stdio: 'pipe' });
|
|
486
|
+
return {
|
|
487
|
+
computeId: containerName,
|
|
488
|
+
publicUrl: `http://localhost:${hostPort}`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
throw new Error(`Failed to start Docker container: ${error}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async deprovision(workspace) {
|
|
496
|
+
if (!workspace.computeId)
|
|
497
|
+
return;
|
|
498
|
+
const { execSync } = await import('child_process');
|
|
499
|
+
try {
|
|
500
|
+
execSync(`docker rm -f ${workspace.computeId}`, { stdio: 'pipe' });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Container may already be removed
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async getStatus(workspace) {
|
|
507
|
+
if (!workspace.computeId)
|
|
508
|
+
return 'error';
|
|
509
|
+
const { execSync } = await import('child_process');
|
|
510
|
+
try {
|
|
511
|
+
const result = execSync(`docker inspect -f '{{.State.Status}}' ${workspace.computeId}`, { stdio: 'pipe' }).toString().trim();
|
|
512
|
+
switch (result) {
|
|
513
|
+
case 'running':
|
|
514
|
+
return 'running';
|
|
515
|
+
case 'exited':
|
|
516
|
+
case 'dead':
|
|
517
|
+
return 'stopped';
|
|
518
|
+
case 'created':
|
|
519
|
+
case 'restarting':
|
|
520
|
+
return 'provisioning';
|
|
521
|
+
default:
|
|
522
|
+
return 'error';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return 'error';
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async restart(workspace) {
|
|
530
|
+
if (!workspace.computeId)
|
|
531
|
+
return;
|
|
532
|
+
const { execSync } = await import('child_process');
|
|
533
|
+
try {
|
|
534
|
+
execSync(`docker restart ${workspace.computeId}`, { stdio: 'pipe' });
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
throw new Error(`Failed to restart container: ${error}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Main Workspace Provisioner
|
|
543
|
+
*/
|
|
544
|
+
export class WorkspaceProvisioner {
|
|
545
|
+
provisioner;
|
|
546
|
+
constructor() {
|
|
547
|
+
const config = getConfig();
|
|
548
|
+
switch (config.compute.provider) {
|
|
549
|
+
case 'fly':
|
|
550
|
+
this.provisioner = new FlyProvisioner();
|
|
551
|
+
break;
|
|
552
|
+
case 'railway':
|
|
553
|
+
this.provisioner = new RailwayProvisioner();
|
|
554
|
+
break;
|
|
555
|
+
case 'docker':
|
|
556
|
+
default:
|
|
557
|
+
this.provisioner = new DockerProvisioner();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Provision a new workspace (one-click)
|
|
562
|
+
*/
|
|
563
|
+
async provision(config) {
|
|
564
|
+
// Create workspace record
|
|
565
|
+
const workspace = await db.workspaces.create({
|
|
566
|
+
userId: config.userId,
|
|
567
|
+
name: config.name,
|
|
568
|
+
computeProvider: getConfig().compute.provider,
|
|
569
|
+
config: {
|
|
570
|
+
providers: config.providers,
|
|
571
|
+
repositories: config.repositories,
|
|
572
|
+
supervisorEnabled: config.supervisorEnabled ?? true,
|
|
573
|
+
maxAgents: config.maxAgents ?? 10,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
// Get credentials
|
|
577
|
+
const credentials = new Map();
|
|
578
|
+
for (const provider of config.providers) {
|
|
579
|
+
const token = await loadCredentialToken(config.userId, provider);
|
|
580
|
+
if (token) {
|
|
581
|
+
credentials.set(provider, token);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// GitHub token is required for cloning repositories
|
|
585
|
+
if (config.repositories.length > 0) {
|
|
586
|
+
const githubToken = await loadCredentialToken(config.userId, 'github');
|
|
587
|
+
if (githubToken) {
|
|
588
|
+
credentials.set('github', githubToken);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
console.warn(`No GitHub token found for user ${config.userId}; repository cloning may fail.`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Provision compute
|
|
595
|
+
try {
|
|
596
|
+
const { computeId, publicUrl } = await this.provisioner.provision(workspace, credentials);
|
|
597
|
+
await db.workspaces.updateStatus(workspace.id, 'running', {
|
|
598
|
+
computeId,
|
|
599
|
+
publicUrl,
|
|
600
|
+
});
|
|
601
|
+
return {
|
|
602
|
+
workspaceId: workspace.id,
|
|
603
|
+
status: 'running',
|
|
604
|
+
publicUrl,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
609
|
+
await db.workspaces.updateStatus(workspace.id, 'error', {
|
|
610
|
+
errorMessage,
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
workspaceId: workspace.id,
|
|
614
|
+
status: 'error',
|
|
615
|
+
error: errorMessage,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Deprovision a workspace
|
|
621
|
+
*/
|
|
622
|
+
async deprovision(workspaceId) {
|
|
623
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
624
|
+
if (!workspace) {
|
|
625
|
+
throw new Error('Workspace not found');
|
|
626
|
+
}
|
|
627
|
+
await this.provisioner.deprovision(workspace);
|
|
628
|
+
await db.workspaces.delete(workspaceId);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get workspace status
|
|
632
|
+
*/
|
|
633
|
+
async getStatus(workspaceId) {
|
|
634
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
635
|
+
if (!workspace) {
|
|
636
|
+
throw new Error('Workspace not found');
|
|
637
|
+
}
|
|
638
|
+
const status = await this.provisioner.getStatus(workspace);
|
|
639
|
+
// Update database if status changed
|
|
640
|
+
if (status !== workspace.status) {
|
|
641
|
+
await db.workspaces.updateStatus(workspaceId, status);
|
|
642
|
+
}
|
|
643
|
+
return status;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Restart a workspace
|
|
647
|
+
*/
|
|
648
|
+
async restart(workspaceId) {
|
|
649
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
650
|
+
if (!workspace) {
|
|
651
|
+
throw new Error('Workspace not found');
|
|
652
|
+
}
|
|
653
|
+
await this.provisioner.restart(workspace);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Stop a workspace
|
|
657
|
+
*/
|
|
658
|
+
async stop(workspaceId) {
|
|
659
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
660
|
+
if (!workspace) {
|
|
661
|
+
throw new Error('Workspace not found');
|
|
662
|
+
}
|
|
663
|
+
// For now, just deprovision to stop
|
|
664
|
+
await this.provisioner.deprovision(workspace);
|
|
665
|
+
await db.workspaces.updateStatus(workspaceId, 'stopped');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Singleton instance
|
|
669
|
+
let _provisioner = null;
|
|
670
|
+
export function getProvisioner() {
|
|
671
|
+
if (!_provisioner) {
|
|
672
|
+
_provisioner = new WorkspaceProvisioner();
|
|
673
|
+
}
|
|
674
|
+
return _provisioner;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=index.js.map
|