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,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple workspace daemons and provides a unified API for the dashboard.
|
|
5
|
+
* This is the top-level service that runs by default, handling workspace switching
|
|
6
|
+
* and agent management across all connected repositories.
|
|
7
|
+
*/
|
|
8
|
+
import * as http from 'http';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
13
|
+
import { createLogger } from '../resiliency/logger.js';
|
|
14
|
+
import { metrics } from '../resiliency/metrics.js';
|
|
15
|
+
import { getSupervisor } from '../resiliency/supervisor.js';
|
|
16
|
+
import { Daemon } from './server.js';
|
|
17
|
+
import { AgentSpawner } from '../bridge/spawner.js';
|
|
18
|
+
import { getProjectPaths } from '../utils/project-namespace.js';
|
|
19
|
+
const logger = createLogger('orchestrator');
|
|
20
|
+
function generateId() {
|
|
21
|
+
return Math.random().toString(36).substring(2, 15);
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_CONFIG = {
|
|
24
|
+
port: 3456,
|
|
25
|
+
host: 'localhost',
|
|
26
|
+
dataDir: path.join(process.env.HOME || '', '.agent-relay', 'orchestrator'),
|
|
27
|
+
autoStartDaemons: true,
|
|
28
|
+
};
|
|
29
|
+
export class Orchestrator extends EventEmitter {
|
|
30
|
+
config;
|
|
31
|
+
workspaces = new Map();
|
|
32
|
+
activeWorkspaceId;
|
|
33
|
+
server;
|
|
34
|
+
wss;
|
|
35
|
+
sessions = new Map();
|
|
36
|
+
supervisor = getSupervisor({
|
|
37
|
+
autoRestart: true,
|
|
38
|
+
maxRestarts: 5,
|
|
39
|
+
contextPersistence: { enabled: true, autoInjectOnRestart: true },
|
|
40
|
+
});
|
|
41
|
+
workspacesFile;
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
super();
|
|
44
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
45
|
+
this.workspacesFile = path.join(this.config.dataDir, 'workspaces.json');
|
|
46
|
+
// Ensure data directory exists
|
|
47
|
+
if (!fs.existsSync(this.config.dataDir)) {
|
|
48
|
+
fs.mkdirSync(this.config.dataDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
// Load existing workspaces
|
|
51
|
+
this.loadWorkspaces();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start the orchestrator
|
|
55
|
+
*/
|
|
56
|
+
async start() {
|
|
57
|
+
logger.info('Starting orchestrator', {
|
|
58
|
+
port: this.config.port,
|
|
59
|
+
host: this.config.host,
|
|
60
|
+
});
|
|
61
|
+
// Start supervisor
|
|
62
|
+
this.supervisor.start();
|
|
63
|
+
// Auto-start daemons for workspaces
|
|
64
|
+
if (this.config.autoStartDaemons) {
|
|
65
|
+
for (const [id, workspace] of this.workspaces) {
|
|
66
|
+
if (fs.existsSync(workspace.path)) {
|
|
67
|
+
await this.startWorkspaceDaemon(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Start HTTP server
|
|
72
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
73
|
+
// Setup WebSocket
|
|
74
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
75
|
+
this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
78
|
+
logger.info('Orchestrator started', {
|
|
79
|
+
url: `http://${this.config.host}:${this.config.port}`,
|
|
80
|
+
});
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Stop the orchestrator
|
|
87
|
+
*/
|
|
88
|
+
async stop() {
|
|
89
|
+
logger.info('Stopping orchestrator');
|
|
90
|
+
// Stop all workspace daemons
|
|
91
|
+
for (const [id] of this.workspaces) {
|
|
92
|
+
await this.stopWorkspaceDaemon(id);
|
|
93
|
+
}
|
|
94
|
+
// Stop supervisor
|
|
95
|
+
this.supervisor.stop();
|
|
96
|
+
// Close WebSocket connections
|
|
97
|
+
if (this.wss) {
|
|
98
|
+
for (const ws of this.wss.clients) {
|
|
99
|
+
ws.close();
|
|
100
|
+
}
|
|
101
|
+
this.wss.close();
|
|
102
|
+
}
|
|
103
|
+
// Close HTTP server
|
|
104
|
+
if (this.server) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
this.server.close(() => {
|
|
107
|
+
logger.info('Orchestrator stopped');
|
|
108
|
+
resolve();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// === Workspace Management ===
|
|
114
|
+
/**
|
|
115
|
+
* Add a workspace
|
|
116
|
+
*/
|
|
117
|
+
addWorkspace(request) {
|
|
118
|
+
const resolvedPath = this.resolvePath(request.path);
|
|
119
|
+
// Check if already exists
|
|
120
|
+
const existing = this.findWorkspaceByPath(resolvedPath);
|
|
121
|
+
if (existing) {
|
|
122
|
+
return existing;
|
|
123
|
+
}
|
|
124
|
+
// Validate path exists
|
|
125
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
126
|
+
throw new Error(`Path does not exist: ${resolvedPath}`);
|
|
127
|
+
}
|
|
128
|
+
const workspace = {
|
|
129
|
+
id: generateId(),
|
|
130
|
+
name: request.name || path.basename(resolvedPath),
|
|
131
|
+
path: resolvedPath,
|
|
132
|
+
status: 'inactive',
|
|
133
|
+
provider: request.provider || this.detectProvider(resolvedPath),
|
|
134
|
+
createdAt: new Date(),
|
|
135
|
+
lastActiveAt: new Date(),
|
|
136
|
+
...this.getGitInfo(resolvedPath),
|
|
137
|
+
};
|
|
138
|
+
this.workspaces.set(workspace.id, workspace);
|
|
139
|
+
this.saveWorkspaces();
|
|
140
|
+
logger.info('Workspace added', { id: workspace.id, name: workspace.name });
|
|
141
|
+
this.broadcastEvent({
|
|
142
|
+
type: 'workspace:added',
|
|
143
|
+
workspaceId: workspace.id,
|
|
144
|
+
data: this.toPublicWorkspace(workspace),
|
|
145
|
+
timestamp: new Date(),
|
|
146
|
+
});
|
|
147
|
+
// Auto-start daemon
|
|
148
|
+
if (this.config.autoStartDaemons) {
|
|
149
|
+
this.startWorkspaceDaemon(workspace.id).catch((err) => {
|
|
150
|
+
logger.error('Failed to start workspace daemon', { id: workspace.id, error: String(err) });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return this.toPublicWorkspace(workspace);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Remove a workspace
|
|
157
|
+
*/
|
|
158
|
+
async removeWorkspace(workspaceId) {
|
|
159
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
160
|
+
if (!workspace)
|
|
161
|
+
return false;
|
|
162
|
+
// Stop daemon if running
|
|
163
|
+
await this.stopWorkspaceDaemon(workspaceId);
|
|
164
|
+
// Clear active if this was active
|
|
165
|
+
if (this.activeWorkspaceId === workspaceId) {
|
|
166
|
+
this.activeWorkspaceId = undefined;
|
|
167
|
+
}
|
|
168
|
+
this.workspaces.delete(workspaceId);
|
|
169
|
+
this.saveWorkspaces();
|
|
170
|
+
logger.info('Workspace removed', { id: workspaceId });
|
|
171
|
+
this.broadcastEvent({
|
|
172
|
+
type: 'workspace:removed',
|
|
173
|
+
workspaceId,
|
|
174
|
+
data: { id: workspaceId },
|
|
175
|
+
timestamp: new Date(),
|
|
176
|
+
});
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Switch to a workspace
|
|
181
|
+
*/
|
|
182
|
+
async switchWorkspace(workspaceId) {
|
|
183
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
184
|
+
if (!workspace) {
|
|
185
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
186
|
+
}
|
|
187
|
+
const previousId = this.activeWorkspaceId;
|
|
188
|
+
// Update status
|
|
189
|
+
if (previousId && previousId !== workspaceId) {
|
|
190
|
+
const prev = this.workspaces.get(previousId);
|
|
191
|
+
if (prev) {
|
|
192
|
+
prev.status = 'inactive';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
workspace.status = 'active';
|
|
196
|
+
workspace.lastActiveAt = new Date();
|
|
197
|
+
this.activeWorkspaceId = workspaceId;
|
|
198
|
+
// Ensure daemon is running
|
|
199
|
+
if (!workspace.daemon?.isRunning) {
|
|
200
|
+
await this.startWorkspaceDaemon(workspaceId);
|
|
201
|
+
}
|
|
202
|
+
this.saveWorkspaces();
|
|
203
|
+
logger.info('Switched workspace', { id: workspaceId, name: workspace.name });
|
|
204
|
+
this.broadcastEvent({
|
|
205
|
+
type: 'workspace:switched',
|
|
206
|
+
workspaceId,
|
|
207
|
+
data: { previousId, currentId: workspaceId },
|
|
208
|
+
timestamp: new Date(),
|
|
209
|
+
});
|
|
210
|
+
return this.toPublicWorkspace(workspace);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get all workspaces
|
|
214
|
+
*/
|
|
215
|
+
getWorkspaces() {
|
|
216
|
+
return Array.from(this.workspaces.values()).map((w) => this.toPublicWorkspace(w));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get workspace by ID
|
|
220
|
+
*/
|
|
221
|
+
getWorkspace(workspaceId) {
|
|
222
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
223
|
+
return workspace ? this.toPublicWorkspace(workspace) : undefined;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get active workspace
|
|
227
|
+
*/
|
|
228
|
+
getActiveWorkspace() {
|
|
229
|
+
if (!this.activeWorkspaceId)
|
|
230
|
+
return undefined;
|
|
231
|
+
return this.getWorkspace(this.activeWorkspaceId);
|
|
232
|
+
}
|
|
233
|
+
// === Agent Management ===
|
|
234
|
+
/**
|
|
235
|
+
* Spawn an agent in a workspace
|
|
236
|
+
*/
|
|
237
|
+
async spawnAgent(workspaceId, request) {
|
|
238
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
239
|
+
if (!workspace) {
|
|
240
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
241
|
+
}
|
|
242
|
+
// Ensure daemon is running
|
|
243
|
+
if (!workspace.daemon?.isRunning) {
|
|
244
|
+
await this.startWorkspaceDaemon(workspaceId);
|
|
245
|
+
}
|
|
246
|
+
// Ensure spawner exists
|
|
247
|
+
if (!workspace.spawner) {
|
|
248
|
+
workspace.spawner = new AgentSpawner(workspace.path);
|
|
249
|
+
}
|
|
250
|
+
const result = await workspace.spawner.spawn({
|
|
251
|
+
name: request.name,
|
|
252
|
+
cli: this.getCliForProvider(request.provider || workspace.provider),
|
|
253
|
+
task: request.task || '',
|
|
254
|
+
});
|
|
255
|
+
if (!result.success) {
|
|
256
|
+
throw new Error(result.error || 'Failed to spawn agent');
|
|
257
|
+
}
|
|
258
|
+
const agent = {
|
|
259
|
+
id: generateId(),
|
|
260
|
+
name: request.name,
|
|
261
|
+
workspaceId,
|
|
262
|
+
provider: request.provider || workspace.provider,
|
|
263
|
+
status: 'running',
|
|
264
|
+
pid: result.pid,
|
|
265
|
+
task: request.task,
|
|
266
|
+
spawnedAt: new Date(),
|
|
267
|
+
restartCount: 0,
|
|
268
|
+
};
|
|
269
|
+
logger.info('Agent spawned', { id: agent.id, name: agent.name, workspaceId });
|
|
270
|
+
this.broadcastEvent({
|
|
271
|
+
type: 'agent:spawned',
|
|
272
|
+
workspaceId,
|
|
273
|
+
agentId: agent.id,
|
|
274
|
+
data: agent,
|
|
275
|
+
timestamp: new Date(),
|
|
276
|
+
});
|
|
277
|
+
return agent;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Stop an agent
|
|
281
|
+
*/
|
|
282
|
+
async stopAgent(workspaceId, agentName) {
|
|
283
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
284
|
+
if (!workspace?.spawner)
|
|
285
|
+
return false;
|
|
286
|
+
const released = await workspace.spawner.release(agentName);
|
|
287
|
+
if (released) {
|
|
288
|
+
this.broadcastEvent({
|
|
289
|
+
type: 'agent:stopped',
|
|
290
|
+
workspaceId,
|
|
291
|
+
data: { name: agentName },
|
|
292
|
+
timestamp: new Date(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return released;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get agents in a workspace
|
|
299
|
+
*/
|
|
300
|
+
getAgents(workspaceId) {
|
|
301
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
302
|
+
if (!workspace?.spawner)
|
|
303
|
+
return [];
|
|
304
|
+
return workspace.spawner.getActiveWorkers().map((w) => ({
|
|
305
|
+
id: w.name,
|
|
306
|
+
name: w.name,
|
|
307
|
+
workspaceId,
|
|
308
|
+
provider: this.detectProviderFromCli(w.cli),
|
|
309
|
+
status: 'running',
|
|
310
|
+
pid: w.pid,
|
|
311
|
+
task: w.task,
|
|
312
|
+
spawnedAt: new Date(w.spawnedAt),
|
|
313
|
+
restartCount: 0,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
// === Private Methods ===
|
|
317
|
+
/**
|
|
318
|
+
* Start daemon for a workspace
|
|
319
|
+
*/
|
|
320
|
+
async startWorkspaceDaemon(workspaceId) {
|
|
321
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
322
|
+
if (!workspace)
|
|
323
|
+
return;
|
|
324
|
+
if (workspace.daemon?.isRunning)
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
const paths = getProjectPaths(workspace.path);
|
|
328
|
+
workspace.daemon = new Daemon({
|
|
329
|
+
socketPath: paths.socketPath,
|
|
330
|
+
teamDir: paths.teamDir,
|
|
331
|
+
});
|
|
332
|
+
await workspace.daemon.start();
|
|
333
|
+
workspace.status = 'active';
|
|
334
|
+
// Create spawner
|
|
335
|
+
workspace.spawner = new AgentSpawner(workspace.path);
|
|
336
|
+
logger.info('Workspace daemon started', { id: workspaceId, socket: paths.socketPath });
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
workspace.status = 'error';
|
|
340
|
+
logger.error('Failed to start workspace daemon', { id: workspaceId, error: String(err) });
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Stop daemon for a workspace
|
|
346
|
+
*/
|
|
347
|
+
async stopWorkspaceDaemon(workspaceId) {
|
|
348
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
349
|
+
if (!workspace)
|
|
350
|
+
return;
|
|
351
|
+
// Release all agents first
|
|
352
|
+
if (workspace.spawner) {
|
|
353
|
+
await workspace.spawner.releaseAll();
|
|
354
|
+
}
|
|
355
|
+
// Stop daemon
|
|
356
|
+
if (workspace.daemon) {
|
|
357
|
+
await workspace.daemon.stop();
|
|
358
|
+
workspace.daemon = undefined;
|
|
359
|
+
}
|
|
360
|
+
workspace.spawner = undefined;
|
|
361
|
+
workspace.status = 'inactive';
|
|
362
|
+
logger.info('Workspace daemon stopped', { id: workspaceId });
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Handle HTTP request
|
|
366
|
+
*/
|
|
367
|
+
async handleRequest(req, res) {
|
|
368
|
+
// CORS
|
|
369
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
370
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
371
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
372
|
+
if (req.method === 'OPTIONS') {
|
|
373
|
+
res.writeHead(204);
|
|
374
|
+
res.end();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
378
|
+
const pathname = url.pathname;
|
|
379
|
+
const method = req.method || 'GET';
|
|
380
|
+
try {
|
|
381
|
+
let response;
|
|
382
|
+
// Health check
|
|
383
|
+
if (pathname === '/' && method === 'GET') {
|
|
384
|
+
response = { status: 200, body: { status: 'ok', version: '1.0.0' } };
|
|
385
|
+
}
|
|
386
|
+
// Metrics
|
|
387
|
+
else if (pathname === '/metrics' && method === 'GET') {
|
|
388
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
389
|
+
res.writeHead(200);
|
|
390
|
+
res.end(metrics.toPrometheus());
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// List workspaces
|
|
394
|
+
else if (pathname === '/workspaces' && method === 'GET') {
|
|
395
|
+
response = {
|
|
396
|
+
status: 200,
|
|
397
|
+
body: {
|
|
398
|
+
workspaces: this.getWorkspaces(),
|
|
399
|
+
activeWorkspaceId: this.activeWorkspaceId,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// Add workspace
|
|
404
|
+
else if (pathname === '/workspaces' && method === 'POST') {
|
|
405
|
+
const body = await this.parseBody(req);
|
|
406
|
+
const workspace = this.addWorkspace(body);
|
|
407
|
+
response = { status: 201, body: workspace };
|
|
408
|
+
}
|
|
409
|
+
// Get workspace
|
|
410
|
+
else if (pathname.match(/^\/workspaces\/[^/]+$/) && method === 'GET') {
|
|
411
|
+
const id = pathname.split('/')[2];
|
|
412
|
+
const workspace = this.getWorkspace(id);
|
|
413
|
+
response = workspace
|
|
414
|
+
? { status: 200, body: workspace }
|
|
415
|
+
: { status: 404, body: { error: 'Not found' } };
|
|
416
|
+
}
|
|
417
|
+
// Delete workspace
|
|
418
|
+
else if (pathname.match(/^\/workspaces\/[^/]+$/) && method === 'DELETE') {
|
|
419
|
+
const id = pathname.split('/')[2];
|
|
420
|
+
const removed = await this.removeWorkspace(id);
|
|
421
|
+
response = removed
|
|
422
|
+
? { status: 204, body: null }
|
|
423
|
+
: { status: 404, body: { error: 'Not found' } };
|
|
424
|
+
}
|
|
425
|
+
// Switch workspace
|
|
426
|
+
else if (pathname.match(/^\/workspaces\/[^/]+\/switch$/) && method === 'POST') {
|
|
427
|
+
const id = pathname.split('/')[2];
|
|
428
|
+
const workspace = await this.switchWorkspace(id);
|
|
429
|
+
response = { status: 200, body: workspace };
|
|
430
|
+
}
|
|
431
|
+
// List agents in workspace
|
|
432
|
+
else if (pathname.match(/^\/workspaces\/[^/]+\/agents$/) && method === 'GET') {
|
|
433
|
+
const id = pathname.split('/')[2];
|
|
434
|
+
const agents = this.getAgents(id);
|
|
435
|
+
response = { status: 200, body: { agents, workspaceId: id } };
|
|
436
|
+
}
|
|
437
|
+
// Spawn agent
|
|
438
|
+
else if (pathname.match(/^\/workspaces\/[^/]+\/agents$/) && method === 'POST') {
|
|
439
|
+
const id = pathname.split('/')[2];
|
|
440
|
+
const body = await this.parseBody(req);
|
|
441
|
+
const agent = await this.spawnAgent(id, body);
|
|
442
|
+
response = { status: 201, body: agent };
|
|
443
|
+
}
|
|
444
|
+
// Stop agent
|
|
445
|
+
else if (pathname.match(/^\/workspaces\/[^/]+\/agents\/[^/]+$/) && method === 'DELETE') {
|
|
446
|
+
const parts = pathname.split('/');
|
|
447
|
+
const workspaceId = parts[2];
|
|
448
|
+
const agentName = parts[4];
|
|
449
|
+
const stopped = await this.stopAgent(workspaceId, agentName);
|
|
450
|
+
response = stopped
|
|
451
|
+
? { status: 204, body: null }
|
|
452
|
+
: { status: 404, body: { error: 'Not found' } };
|
|
453
|
+
}
|
|
454
|
+
// Not found
|
|
455
|
+
else {
|
|
456
|
+
response = { status: 404, body: { error: 'Not found' } };
|
|
457
|
+
}
|
|
458
|
+
res.setHeader('Content-Type', 'application/json');
|
|
459
|
+
res.writeHead(response.status);
|
|
460
|
+
res.end(response.body ? JSON.stringify(response.body) : '');
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
logger.error('Request error', { error: String(err) });
|
|
464
|
+
res.setHeader('Content-Type', 'application/json');
|
|
465
|
+
res.writeHead(500);
|
|
466
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Handle WebSocket connection
|
|
471
|
+
*/
|
|
472
|
+
handleWebSocket(ws, _req) {
|
|
473
|
+
logger.info('WebSocket client connected');
|
|
474
|
+
const session = {
|
|
475
|
+
userId: 'anonymous',
|
|
476
|
+
githubUsername: 'anonymous',
|
|
477
|
+
connectedAt: new Date(),
|
|
478
|
+
activeWorkspaceId: this.activeWorkspaceId,
|
|
479
|
+
};
|
|
480
|
+
this.sessions.set(ws, session);
|
|
481
|
+
// Send initial state
|
|
482
|
+
this.sendToClient(ws, {
|
|
483
|
+
type: 'init',
|
|
484
|
+
data: {
|
|
485
|
+
workspaces: this.getWorkspaces(),
|
|
486
|
+
activeWorkspaceId: this.activeWorkspaceId,
|
|
487
|
+
agents: this.activeWorkspaceId ? this.getAgents(this.activeWorkspaceId) : [],
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
ws.on('message', (data) => {
|
|
491
|
+
try {
|
|
492
|
+
const msg = JSON.parse(data.toString());
|
|
493
|
+
this.handleWebSocketMessage(ws, session, msg);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
logger.error('WebSocket message error', { error: String(err) });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
ws.on('close', () => {
|
|
500
|
+
this.sessions.delete(ws);
|
|
501
|
+
logger.info('WebSocket client disconnected');
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Handle WebSocket message
|
|
506
|
+
*/
|
|
507
|
+
handleWebSocketMessage(ws, session, msg) {
|
|
508
|
+
switch (msg.type) {
|
|
509
|
+
case 'switch_workspace':
|
|
510
|
+
if (typeof msg.data === 'string') {
|
|
511
|
+
this.switchWorkspace(msg.data)
|
|
512
|
+
.then((workspace) => {
|
|
513
|
+
session.activeWorkspaceId = workspace.id;
|
|
514
|
+
})
|
|
515
|
+
.catch((err) => {
|
|
516
|
+
this.sendToClient(ws, { type: 'error', data: String(err) });
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
case 'ping':
|
|
521
|
+
this.sendToClient(ws, { type: 'pong' });
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Send to WebSocket client
|
|
527
|
+
*/
|
|
528
|
+
sendToClient(ws, msg) {
|
|
529
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
530
|
+
ws.send(JSON.stringify(msg));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Broadcast event to all clients
|
|
535
|
+
*/
|
|
536
|
+
broadcastEvent(event) {
|
|
537
|
+
if (!this.wss)
|
|
538
|
+
return;
|
|
539
|
+
const msg = JSON.stringify({ type: 'event', data: event });
|
|
540
|
+
for (const ws of this.wss.clients) {
|
|
541
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
542
|
+
ws.send(msg);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Parse request body
|
|
548
|
+
*/
|
|
549
|
+
parseBody(req) {
|
|
550
|
+
return new Promise((resolve, reject) => {
|
|
551
|
+
let data = '';
|
|
552
|
+
req.on('data', (chunk) => (data += chunk));
|
|
553
|
+
req.on('end', () => {
|
|
554
|
+
try {
|
|
555
|
+
resolve(data ? JSON.parse(data) : {});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
reject(new Error('Invalid JSON'));
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Load workspaces from disk
|
|
565
|
+
*/
|
|
566
|
+
loadWorkspaces() {
|
|
567
|
+
if (!fs.existsSync(this.workspacesFile))
|
|
568
|
+
return;
|
|
569
|
+
try {
|
|
570
|
+
const data = JSON.parse(fs.readFileSync(this.workspacesFile, 'utf8'));
|
|
571
|
+
for (const w of data.workspaces || []) {
|
|
572
|
+
this.workspaces.set(w.id, {
|
|
573
|
+
...w,
|
|
574
|
+
createdAt: new Date(w.createdAt),
|
|
575
|
+
lastActiveAt: new Date(w.lastActiveAt),
|
|
576
|
+
status: 'inactive',
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
this.activeWorkspaceId = data.activeWorkspaceId;
|
|
580
|
+
logger.info('Loaded workspaces', { count: this.workspaces.size });
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
logger.error('Failed to load workspaces', { error: String(err) });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Save workspaces to disk
|
|
588
|
+
*/
|
|
589
|
+
saveWorkspaces() {
|
|
590
|
+
try {
|
|
591
|
+
const data = {
|
|
592
|
+
workspaces: Array.from(this.workspaces.values()).map((w) => this.toPublicWorkspace(w)),
|
|
593
|
+
activeWorkspaceId: this.activeWorkspaceId,
|
|
594
|
+
};
|
|
595
|
+
fs.writeFileSync(this.workspacesFile, JSON.stringify(data, null, 2));
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
logger.error('Failed to save workspaces', { error: String(err) });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Find workspace by path
|
|
603
|
+
*/
|
|
604
|
+
findWorkspaceByPath(path) {
|
|
605
|
+
const resolved = this.resolvePath(path);
|
|
606
|
+
const workspace = Array.from(this.workspaces.values()).find((w) => w.path === resolved);
|
|
607
|
+
return workspace ? this.toPublicWorkspace(workspace) : undefined;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Resolve path
|
|
611
|
+
*/
|
|
612
|
+
resolvePath(p) {
|
|
613
|
+
if (p.startsWith('~')) {
|
|
614
|
+
p = path.join(process.env.HOME || '', p.slice(1));
|
|
615
|
+
}
|
|
616
|
+
return path.resolve(p);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Detect provider from workspace
|
|
620
|
+
*/
|
|
621
|
+
detectProvider(workspacePath) {
|
|
622
|
+
if (fs.existsSync(path.join(workspacePath, 'CLAUDE.md')) ||
|
|
623
|
+
fs.existsSync(path.join(workspacePath, '.claude'))) {
|
|
624
|
+
return 'claude';
|
|
625
|
+
}
|
|
626
|
+
if (fs.existsSync(path.join(workspacePath, '.codex'))) {
|
|
627
|
+
return 'codex';
|
|
628
|
+
}
|
|
629
|
+
if (fs.existsSync(path.join(workspacePath, '.gemini'))) {
|
|
630
|
+
return 'gemini';
|
|
631
|
+
}
|
|
632
|
+
return 'generic';
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Detect provider from CLI command
|
|
636
|
+
*/
|
|
637
|
+
detectProviderFromCli(cli) {
|
|
638
|
+
if (cli.includes('claude'))
|
|
639
|
+
return 'claude';
|
|
640
|
+
if (cli.includes('codex'))
|
|
641
|
+
return 'codex';
|
|
642
|
+
if (cli.includes('gemini'))
|
|
643
|
+
return 'gemini';
|
|
644
|
+
return 'generic';
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get CLI command for provider
|
|
648
|
+
*/
|
|
649
|
+
getCliForProvider(provider) {
|
|
650
|
+
switch (provider) {
|
|
651
|
+
case 'claude':
|
|
652
|
+
return 'claude';
|
|
653
|
+
case 'codex':
|
|
654
|
+
return 'codex';
|
|
655
|
+
case 'gemini':
|
|
656
|
+
return 'gemini';
|
|
657
|
+
default:
|
|
658
|
+
return 'claude';
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Get git info
|
|
663
|
+
*/
|
|
664
|
+
getGitInfo(workspacePath) {
|
|
665
|
+
try {
|
|
666
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
667
|
+
const { execSync } = require('child_process');
|
|
668
|
+
const branch = execSync('git branch --show-current', {
|
|
669
|
+
cwd: workspacePath,
|
|
670
|
+
encoding: 'utf8',
|
|
671
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
672
|
+
}).trim();
|
|
673
|
+
let remote;
|
|
674
|
+
try {
|
|
675
|
+
remote = execSync('git remote get-url origin', {
|
|
676
|
+
cwd: workspacePath,
|
|
677
|
+
encoding: 'utf8',
|
|
678
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
679
|
+
}).trim();
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
// No remote
|
|
683
|
+
}
|
|
684
|
+
return { gitRemote: remote, gitBranch: branch };
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return {};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Convert to public workspace (without internal references)
|
|
692
|
+
*/
|
|
693
|
+
toPublicWorkspace(w) {
|
|
694
|
+
return {
|
|
695
|
+
id: w.id,
|
|
696
|
+
name: w.name,
|
|
697
|
+
path: w.path,
|
|
698
|
+
status: w.status,
|
|
699
|
+
provider: w.provider,
|
|
700
|
+
createdAt: w.createdAt,
|
|
701
|
+
lastActiveAt: w.lastActiveAt,
|
|
702
|
+
cloudId: w.cloudId,
|
|
703
|
+
customDomain: w.customDomain,
|
|
704
|
+
gitRemote: w.gitRemote,
|
|
705
|
+
gitBranch: w.gitBranch,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
let orchestratorInstance;
|
|
710
|
+
/**
|
|
711
|
+
* Start the orchestrator
|
|
712
|
+
*/
|
|
713
|
+
export async function startOrchestrator(config = {}) {
|
|
714
|
+
if (orchestratorInstance) {
|
|
715
|
+
return orchestratorInstance;
|
|
716
|
+
}
|
|
717
|
+
orchestratorInstance = new Orchestrator(config);
|
|
718
|
+
await orchestratorInstance.start();
|
|
719
|
+
return orchestratorInstance;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Stop the orchestrator
|
|
723
|
+
*/
|
|
724
|
+
export async function stopOrchestrator() {
|
|
725
|
+
if (orchestratorInstance) {
|
|
726
|
+
await orchestratorInstance.stop();
|
|
727
|
+
orchestratorInstance = undefined;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get orchestrator instance
|
|
732
|
+
*/
|
|
733
|
+
export function getOrchestrator() {
|
|
734
|
+
return orchestratorInstance;
|
|
735
|
+
}
|
|
736
|
+
//# sourceMappingURL=orchestrator.js.map
|