agent-relay 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trajectories/agent-relay-322-324.md +17 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
- package/.trajectories/consolidate-settings-panel.md +24 -0
- package/.trajectories/gh-cli-user-token.md +26 -0
- package/.trajectories/index.json +155 -1
- package/deploy/workspace/codex.config.toml +15 -0
- package/deploy/workspace/entrypoint.sh +167 -7
- package/deploy/workspace/git-credential-relay +17 -2
- package/dist/bridge/spawner.d.ts +7 -0
- package/dist/bridge/spawner.js +40 -9
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.js +210 -168
- package/dist/cloud/api/admin.d.ts +8 -0
- package/dist/cloud/api/admin.js +212 -0
- package/dist/cloud/api/auth.js +8 -0
- package/dist/cloud/api/billing.d.ts +0 -10
- package/dist/cloud/api/billing.js +248 -58
- package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
- package/dist/cloud/api/codex-auth-helper.js +215 -8
- package/dist/cloud/api/coordinators.js +402 -0
- package/dist/cloud/api/daemons.js +15 -11
- package/dist/cloud/api/git.js +104 -17
- package/dist/cloud/api/github-app.js +42 -8
- package/dist/cloud/api/nango-auth.js +297 -16
- package/dist/cloud/api/onboarding.js +97 -33
- package/dist/cloud/api/providers.js +12 -16
- package/dist/cloud/api/repos.js +200 -124
- package/dist/cloud/api/test-helpers.js +40 -0
- package/dist/cloud/api/usage.js +13 -0
- package/dist/cloud/api/webhooks.js +1 -1
- package/dist/cloud/api/workspaces.d.ts +18 -0
- package/dist/cloud/api/workspaces.js +945 -15
- package/dist/cloud/config.d.ts +8 -0
- package/dist/cloud/config.js +15 -0
- package/dist/cloud/db/drizzle.d.ts +5 -2
- package/dist/cloud/db/drizzle.js +27 -20
- package/dist/cloud/db/schema.d.ts +19 -51
- package/dist/cloud/db/schema.js +5 -4
- package/dist/cloud/index.d.ts +0 -1
- package/dist/cloud/index.js +0 -1
- package/dist/cloud/provisioner/index.d.ts +93 -1
- package/dist/cloud/provisioner/index.js +608 -63
- package/dist/cloud/server.js +156 -16
- package/dist/cloud/services/compute-enforcement.d.ts +57 -0
- package/dist/cloud/services/compute-enforcement.js +175 -0
- package/dist/cloud/services/index.d.ts +2 -0
- package/dist/cloud/services/index.js +4 -0
- package/dist/cloud/services/intro-expiration.d.ts +55 -0
- package/dist/cloud/services/intro-expiration.js +211 -0
- package/dist/cloud/services/nango.d.ts +14 -0
- package/dist/cloud/services/nango.js +74 -14
- package/dist/cloud/services/ssh-security.d.ts +31 -0
- package/dist/cloud/services/ssh-security.js +63 -0
- package/dist/continuity/manager.d.ts +5 -0
- package/dist/continuity/manager.js +56 -2
- package/dist/daemon/api.d.ts +2 -0
- package/dist/daemon/api.js +214 -5
- package/dist/daemon/cli-auth.d.ts +13 -1
- package/dist/daemon/cli-auth.js +166 -47
- package/dist/daemon/connection.d.ts +7 -1
- package/dist/daemon/connection.js +15 -0
- package/dist/daemon/orchestrator.d.ts +2 -0
- package/dist/daemon/orchestrator.js +26 -0
- package/dist/daemon/repo-manager.d.ts +116 -0
- package/dist/daemon/repo-manager.js +384 -0
- package/dist/daemon/router.d.ts +60 -1
- package/dist/daemon/router.js +281 -20
- package/dist/daemon/user-directory.d.ts +111 -0
- package/dist/daemon/user-directory.js +233 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/{page-3fdfa60e53f2810d.js → page-8553743baca53a00.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-c617745b81344f4f.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-dc786c183425c2ac.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
- package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +1 -0
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
- package/dist/dashboard/out/_next/static/sDcbGRTYLcpPvyTs_rsNb/_ssgManifest.js +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +3 -3
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +3 -3
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +2 -2
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +3 -3
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +3 -3
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +3 -3
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +3 -3
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +2 -2
- package/dist/dashboard-server/server.js +316 -12
- package/dist/dashboard-server/user-bridge.d.ts +103 -0
- package/dist/dashboard-server/user-bridge.js +189 -0
- package/dist/protocol/channels.d.ts +205 -0
- package/dist/protocol/channels.js +154 -0
- package/dist/protocol/types.d.ts +13 -1
- package/dist/resiliency/provider-context.js +2 -0
- package/dist/shared/cli-auth-config.d.ts +19 -0
- package/dist/shared/cli-auth-config.js +58 -2
- package/dist/utils/agent-config.js +1 -1
- package/dist/wrapper/auth-detection.d.ts +49 -0
- package/dist/wrapper/auth-detection.js +192 -0
- package/dist/wrapper/base-wrapper.d.ts +153 -0
- package/dist/wrapper/base-wrapper.js +393 -0
- package/dist/wrapper/client.d.ts +7 -1
- package/dist/wrapper/client.js +3 -0
- package/dist/wrapper/index.d.ts +1 -0
- package/dist/wrapper/index.js +4 -3
- package/dist/wrapper/pty-wrapper.d.ts +62 -84
- package/dist/wrapper/pty-wrapper.js +154 -180
- package/dist/wrapper/tmux-wrapper.d.ts +41 -66
- package/dist/wrapper/tmux-wrapper.js +90 -134
- package/package.json +4 -2
- package/scripts/postinstall.js +11 -155
- package/scripts/test-interactive-terminal.sh +248 -0
- package/dist/cloud/vault/index.d.ts +0 -76
- package/dist/cloud/vault/index.js +0 -219
- package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
- package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
- package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
- /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → sDcbGRTYLcpPvyTs_rsNb}/_buildManifest.js +0 -0
|
@@ -23,11 +23,14 @@ export class Connection {
|
|
|
23
23
|
config;
|
|
24
24
|
_state = 'CONNECTING';
|
|
25
25
|
_agentName;
|
|
26
|
+
_entityType;
|
|
26
27
|
_cli;
|
|
27
28
|
_program;
|
|
28
29
|
_model;
|
|
29
30
|
_task;
|
|
30
31
|
_workingDirectory;
|
|
32
|
+
_displayName;
|
|
33
|
+
_avatarUrl;
|
|
31
34
|
_sessionId;
|
|
32
35
|
_resumeToken;
|
|
33
36
|
_isResumed = false;
|
|
@@ -58,6 +61,9 @@ export class Connection {
|
|
|
58
61
|
get agentName() {
|
|
59
62
|
return this._agentName;
|
|
60
63
|
}
|
|
64
|
+
get entityType() {
|
|
65
|
+
return this._entityType;
|
|
66
|
+
}
|
|
61
67
|
get cli() {
|
|
62
68
|
return this._cli;
|
|
63
69
|
}
|
|
@@ -73,6 +79,12 @@ export class Connection {
|
|
|
73
79
|
get workingDirectory() {
|
|
74
80
|
return this._workingDirectory;
|
|
75
81
|
}
|
|
82
|
+
get displayName() {
|
|
83
|
+
return this._displayName;
|
|
84
|
+
}
|
|
85
|
+
get avatarUrl() {
|
|
86
|
+
return this._avatarUrl;
|
|
87
|
+
}
|
|
76
88
|
get sessionId() {
|
|
77
89
|
return this._sessionId;
|
|
78
90
|
}
|
|
@@ -133,11 +145,14 @@ export class Connection {
|
|
|
133
145
|
return;
|
|
134
146
|
}
|
|
135
147
|
this._agentName = envelope.payload.agent;
|
|
148
|
+
this._entityType = envelope.payload.entityType;
|
|
136
149
|
this._cli = envelope.payload.cli;
|
|
137
150
|
this._program = envelope.payload.program;
|
|
138
151
|
this._model = envelope.payload.model;
|
|
139
152
|
this._task = envelope.payload.task;
|
|
140
153
|
this._workingDirectory = envelope.payload.workingDirectory;
|
|
154
|
+
this._displayName = envelope.payload.displayName;
|
|
155
|
+
this._avatarUrl = envelope.payload.avatarUrl;
|
|
141
156
|
// Check for session resume
|
|
142
157
|
const resumeToken = envelope.payload.session?.resume_token;
|
|
143
158
|
if (resumeToken) {
|
|
@@ -26,6 +26,8 @@ export declare class Orchestrator extends EventEmitter {
|
|
|
26
26
|
private sessions;
|
|
27
27
|
private supervisor;
|
|
28
28
|
private workspacesFile;
|
|
29
|
+
private clientAlive;
|
|
30
|
+
private pingInterval?;
|
|
29
31
|
constructor(config?: Partial<OrchestratorConfig>);
|
|
30
32
|
/**
|
|
31
33
|
* Start the orchestrator
|
|
@@ -39,6 +39,9 @@ export class Orchestrator extends EventEmitter {
|
|
|
39
39
|
contextPersistence: { enabled: true, autoInjectOnRestart: true },
|
|
40
40
|
});
|
|
41
41
|
workspacesFile;
|
|
42
|
+
// Track alive status for ping/pong keepalive
|
|
43
|
+
clientAlive = new WeakMap();
|
|
44
|
+
pingInterval;
|
|
42
45
|
constructor(config = {}) {
|
|
43
46
|
super();
|
|
44
47
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -73,6 +76,18 @@ export class Orchestrator extends EventEmitter {
|
|
|
73
76
|
// Setup WebSocket
|
|
74
77
|
this.wss = new WebSocketServer({ server: this.server });
|
|
75
78
|
this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
|
|
79
|
+
// Setup ping/pong keepalive (30 second interval)
|
|
80
|
+
this.pingInterval = setInterval(() => {
|
|
81
|
+
this.wss?.clients.forEach((ws) => {
|
|
82
|
+
if (this.clientAlive.get(ws) === false) {
|
|
83
|
+
logger.info('WebSocket client unresponsive, closing');
|
|
84
|
+
ws.terminate();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.clientAlive.set(ws, false);
|
|
88
|
+
ws.ping();
|
|
89
|
+
});
|
|
90
|
+
}, 30000);
|
|
76
91
|
return new Promise((resolve) => {
|
|
77
92
|
this.server.listen(this.config.port, this.config.host, () => {
|
|
78
93
|
logger.info('Orchestrator started', {
|
|
@@ -87,6 +102,11 @@ export class Orchestrator extends EventEmitter {
|
|
|
87
102
|
*/
|
|
88
103
|
async stop() {
|
|
89
104
|
logger.info('Stopping orchestrator');
|
|
105
|
+
// Clear ping interval
|
|
106
|
+
if (this.pingInterval) {
|
|
107
|
+
clearInterval(this.pingInterval);
|
|
108
|
+
this.pingInterval = undefined;
|
|
109
|
+
}
|
|
90
110
|
// Stop all workspace daemons
|
|
91
111
|
for (const [id] of this.workspaces) {
|
|
92
112
|
await this.stopWorkspaceDaemon(id);
|
|
@@ -501,6 +521,12 @@ export class Orchestrator extends EventEmitter {
|
|
|
501
521
|
*/
|
|
502
522
|
handleWebSocket(ws, _req) {
|
|
503
523
|
logger.info('WebSocket client connected');
|
|
524
|
+
// Mark client as alive for ping/pong keepalive
|
|
525
|
+
this.clientAlive.set(ws, true);
|
|
526
|
+
// Handle pong responses
|
|
527
|
+
ws.on('pong', () => {
|
|
528
|
+
this.clientAlive.set(ws, true);
|
|
529
|
+
});
|
|
504
530
|
const session = {
|
|
505
531
|
userId: 'anonymous',
|
|
506
532
|
githubUsername: 'anonymous',
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Repository Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages repository cloning, updating, and removal for workspace containers.
|
|
5
|
+
* Uses a file-based tracking system (repos.json) to persist state across restarts.
|
|
6
|
+
*
|
|
7
|
+
* This replaces the static REPOSITORIES env var approach, allowing dynamic
|
|
8
|
+
* repo management without workspace restart.
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
export interface RepoInfo {
|
|
12
|
+
/** Full GitHub repo name (e.g., "owner/repo") */
|
|
13
|
+
fullName: string;
|
|
14
|
+
/** Local directory name */
|
|
15
|
+
localName: string;
|
|
16
|
+
/** Absolute path to the cloned repo */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Current status */
|
|
19
|
+
status: 'cloned' | 'cloning' | 'error' | 'removed';
|
|
20
|
+
/** Last sync timestamp */
|
|
21
|
+
lastSynced?: string;
|
|
22
|
+
/** Default branch */
|
|
23
|
+
defaultBranch?: string;
|
|
24
|
+
/** Error message if status is 'error' */
|
|
25
|
+
error?: string;
|
|
26
|
+
/** When the repo was added */
|
|
27
|
+
addedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ReposConfig {
|
|
30
|
+
version: number;
|
|
31
|
+
workspaceDir: string;
|
|
32
|
+
repos: Record<string, RepoInfo>;
|
|
33
|
+
lastUpdated: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SyncResult {
|
|
36
|
+
success: boolean;
|
|
37
|
+
repo: string;
|
|
38
|
+
action: 'cloned' | 'updated' | 'already_synced' | 'error';
|
|
39
|
+
path?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RepoManagerConfig {
|
|
43
|
+
workspaceDir: string;
|
|
44
|
+
configFile?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare class RepoManager extends EventEmitter {
|
|
47
|
+
private workspaceDir;
|
|
48
|
+
private configPath;
|
|
49
|
+
private config;
|
|
50
|
+
constructor(options: RepoManagerConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Load or initialize the repos config file
|
|
53
|
+
*/
|
|
54
|
+
private loadConfig;
|
|
55
|
+
/**
|
|
56
|
+
* Save the config to disk
|
|
57
|
+
*/
|
|
58
|
+
private saveConfig;
|
|
59
|
+
/**
|
|
60
|
+
* Get all tracked repos
|
|
61
|
+
*/
|
|
62
|
+
getRepos(): RepoInfo[];
|
|
63
|
+
/**
|
|
64
|
+
* Get a specific repo by full name
|
|
65
|
+
*/
|
|
66
|
+
getRepo(fullName: string): RepoInfo | null;
|
|
67
|
+
/**
|
|
68
|
+
* Sync a repository (clone if new, pull if exists)
|
|
69
|
+
*/
|
|
70
|
+
syncRepo(fullName: string): Promise<SyncResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Remove a repository
|
|
73
|
+
*/
|
|
74
|
+
removeRepo(fullName: string, deleteFiles?: boolean): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Sync multiple repos (e.g., from initial REPOSITORIES env var)
|
|
77
|
+
*/
|
|
78
|
+
syncRepos(fullNames: string[]): Promise<SyncResult[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Initialize from REPOSITORIES env var (backward compatibility)
|
|
81
|
+
*/
|
|
82
|
+
initFromEnv(): Promise<SyncResult[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Scan workspace directory for existing repos and register them
|
|
85
|
+
* This handles repos that were cloned by entrypoint.sh before daemon started
|
|
86
|
+
*/
|
|
87
|
+
scanExistingRepos(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Clone a repository
|
|
90
|
+
*/
|
|
91
|
+
private gitClone;
|
|
92
|
+
/**
|
|
93
|
+
* Pull updates for a repository
|
|
94
|
+
*/
|
|
95
|
+
private gitPull;
|
|
96
|
+
/**
|
|
97
|
+
* Get the default branch of a repo
|
|
98
|
+
*/
|
|
99
|
+
private getDefaultBranch;
|
|
100
|
+
/**
|
|
101
|
+
* Mark directory as safe for git (prevents "dubious ownership" errors)
|
|
102
|
+
*/
|
|
103
|
+
private markSafeDirectory;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get or create the repo manager instance
|
|
107
|
+
*/
|
|
108
|
+
export declare function getRepoManager(workspaceDir?: string): RepoManager;
|
|
109
|
+
/**
|
|
110
|
+
* Initialize repo manager (call at startup)
|
|
111
|
+
*
|
|
112
|
+
* 1. Scans workspace for existing repos (handles entrypoint.sh clones)
|
|
113
|
+
* 2. Syncs any repos from REPOSITORIES env var that aren't already cloned
|
|
114
|
+
*/
|
|
115
|
+
export declare function initRepoManager(workspaceDir?: string): Promise<RepoManager>;
|
|
116
|
+
//# sourceMappingURL=repo-manager.d.ts.map
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Repository Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages repository cloning, updating, and removal for workspace containers.
|
|
5
|
+
* Uses a file-based tracking system (repos.json) to persist state across restarts.
|
|
6
|
+
*
|
|
7
|
+
* This replaces the static REPOSITORIES env var approach, allowing dynamic
|
|
8
|
+
* repo management without workspace restart.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { execSync, spawn } from 'child_process';
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { createLogger } from '../resiliency/logger.js';
|
|
15
|
+
const logger = createLogger('repo-manager');
|
|
16
|
+
const DEFAULT_CONFIG_FILE = 'repos.json';
|
|
17
|
+
export class RepoManager extends EventEmitter {
|
|
18
|
+
workspaceDir;
|
|
19
|
+
configPath;
|
|
20
|
+
config;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super();
|
|
23
|
+
this.workspaceDir = options.workspaceDir;
|
|
24
|
+
this.configPath = path.join(this.workspaceDir, options.configFile || DEFAULT_CONFIG_FILE);
|
|
25
|
+
this.config = this.loadConfig();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Load or initialize the repos config file
|
|
29
|
+
*/
|
|
30
|
+
loadConfig() {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(this.configPath)) {
|
|
33
|
+
const data = fs.readFileSync(this.configPath, 'utf-8');
|
|
34
|
+
const config = JSON.parse(data);
|
|
35
|
+
logger.info('Loaded repo config', { repoCount: Object.keys(config.repos).length });
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
logger.warn('Failed to load repo config, starting fresh', { error: String(err) });
|
|
41
|
+
}
|
|
42
|
+
// Initialize new config
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
workspaceDir: this.workspaceDir,
|
|
46
|
+
repos: {},
|
|
47
|
+
lastUpdated: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Save the config to disk
|
|
52
|
+
*/
|
|
53
|
+
saveConfig() {
|
|
54
|
+
this.config.lastUpdated = new Date().toISOString();
|
|
55
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get all tracked repos
|
|
59
|
+
*/
|
|
60
|
+
getRepos() {
|
|
61
|
+
return Object.values(this.config.repos).filter(r => r.status !== 'removed');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get a specific repo by full name
|
|
65
|
+
*/
|
|
66
|
+
getRepo(fullName) {
|
|
67
|
+
const key = fullName.toLowerCase();
|
|
68
|
+
return this.config.repos[key] || null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sync a repository (clone if new, pull if exists)
|
|
72
|
+
*/
|
|
73
|
+
async syncRepo(fullName) {
|
|
74
|
+
const key = fullName.toLowerCase();
|
|
75
|
+
const localName = path.basename(fullName);
|
|
76
|
+
const repoPath = path.join(this.workspaceDir, localName);
|
|
77
|
+
logger.info('Syncing repo', { fullName, repoPath });
|
|
78
|
+
// Update status to cloning
|
|
79
|
+
this.config.repos[key] = {
|
|
80
|
+
fullName,
|
|
81
|
+
localName,
|
|
82
|
+
path: repoPath,
|
|
83
|
+
status: 'cloning',
|
|
84
|
+
addedAt: this.config.repos[key]?.addedAt || new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
this.saveConfig();
|
|
87
|
+
this.emit('repo:syncing', { fullName });
|
|
88
|
+
try {
|
|
89
|
+
const gitDir = path.join(repoPath, '.git');
|
|
90
|
+
const exists = fs.existsSync(gitDir);
|
|
91
|
+
if (exists) {
|
|
92
|
+
// Pull existing repo
|
|
93
|
+
await this.gitPull(repoPath, fullName);
|
|
94
|
+
this.config.repos[key] = {
|
|
95
|
+
...this.config.repos[key],
|
|
96
|
+
status: 'cloned',
|
|
97
|
+
lastSynced: new Date().toISOString(),
|
|
98
|
+
defaultBranch: this.getDefaultBranch(repoPath),
|
|
99
|
+
error: undefined,
|
|
100
|
+
};
|
|
101
|
+
this.saveConfig();
|
|
102
|
+
this.emit('repo:synced', { fullName, action: 'updated' });
|
|
103
|
+
logger.info('Repo updated', { fullName });
|
|
104
|
+
return { success: true, repo: fullName, action: 'updated', path: repoPath };
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Clone new repo
|
|
108
|
+
await this.gitClone(fullName, repoPath);
|
|
109
|
+
this.config.repos[key] = {
|
|
110
|
+
...this.config.repos[key],
|
|
111
|
+
status: 'cloned',
|
|
112
|
+
lastSynced: new Date().toISOString(),
|
|
113
|
+
defaultBranch: this.getDefaultBranch(repoPath),
|
|
114
|
+
error: undefined,
|
|
115
|
+
};
|
|
116
|
+
this.saveConfig();
|
|
117
|
+
// Mark directory as safe for git
|
|
118
|
+
this.markSafeDirectory(repoPath);
|
|
119
|
+
this.emit('repo:synced', { fullName, action: 'cloned' });
|
|
120
|
+
logger.info('Repo cloned', { fullName });
|
|
121
|
+
return { success: true, repo: fullName, action: 'cloned', path: repoPath };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
126
|
+
this.config.repos[key] = {
|
|
127
|
+
...this.config.repos[key],
|
|
128
|
+
status: 'error',
|
|
129
|
+
error: errorMsg,
|
|
130
|
+
};
|
|
131
|
+
this.saveConfig();
|
|
132
|
+
this.emit('repo:error', { fullName, error: errorMsg });
|
|
133
|
+
logger.error('Repo sync failed', { fullName, error: errorMsg });
|
|
134
|
+
return { success: false, repo: fullName, action: 'error', error: errorMsg };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Remove a repository
|
|
139
|
+
*/
|
|
140
|
+
async removeRepo(fullName, deleteFiles = false) {
|
|
141
|
+
const key = fullName.toLowerCase();
|
|
142
|
+
const repo = this.config.repos[key];
|
|
143
|
+
if (!repo) {
|
|
144
|
+
logger.warn('Repo not found for removal', { fullName });
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
logger.info('Removing repo', { fullName, deleteFiles });
|
|
148
|
+
if (deleteFiles && fs.existsSync(repo.path)) {
|
|
149
|
+
try {
|
|
150
|
+
fs.rmSync(repo.path, { recursive: true, force: true });
|
|
151
|
+
logger.info('Deleted repo files', { fullName, path: repo.path });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
logger.error('Failed to delete repo files', { fullName, error: String(err) });
|
|
155
|
+
// Continue anyway - mark as removed in config
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Mark as removed (or delete from config entirely)
|
|
159
|
+
if (deleteFiles) {
|
|
160
|
+
delete this.config.repos[key];
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.config.repos[key] = {
|
|
164
|
+
...repo,
|
|
165
|
+
status: 'removed',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
this.saveConfig();
|
|
169
|
+
this.emit('repo:removed', { fullName });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sync multiple repos (e.g., from initial REPOSITORIES env var)
|
|
174
|
+
*/
|
|
175
|
+
async syncRepos(fullNames) {
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const fullName of fullNames) {
|
|
178
|
+
if (!fullName.trim())
|
|
179
|
+
continue;
|
|
180
|
+
const result = await this.syncRepo(fullName.trim());
|
|
181
|
+
results.push(result);
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Initialize from REPOSITORIES env var (backward compatibility)
|
|
187
|
+
*/
|
|
188
|
+
async initFromEnv() {
|
|
189
|
+
const repoList = process.env.REPOSITORIES || '';
|
|
190
|
+
if (!repoList.trim()) {
|
|
191
|
+
logger.info('No REPOSITORIES env var set, skipping initial sync');
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const repos = repoList.split(',').map(r => r.trim()).filter(Boolean);
|
|
195
|
+
logger.info('Initializing repos from env', { count: repos.length });
|
|
196
|
+
return this.syncRepos(repos);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Scan workspace directory for existing repos and register them
|
|
200
|
+
* This handles repos that were cloned by entrypoint.sh before daemon started
|
|
201
|
+
*/
|
|
202
|
+
scanExistingRepos() {
|
|
203
|
+
try {
|
|
204
|
+
const entries = fs.readdirSync(this.workspaceDir, { withFileTypes: true });
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (!entry.isDirectory())
|
|
207
|
+
continue;
|
|
208
|
+
if (entry.name === DEFAULT_CONFIG_FILE || entry.name.startsWith('.'))
|
|
209
|
+
continue;
|
|
210
|
+
const repoPath = path.join(this.workspaceDir, entry.name);
|
|
211
|
+
const gitDir = path.join(repoPath, '.git');
|
|
212
|
+
if (!fs.existsSync(gitDir))
|
|
213
|
+
continue;
|
|
214
|
+
// Try to get the remote URL to determine full repo name
|
|
215
|
+
let fullName = entry.name; // Default to directory name
|
|
216
|
+
try {
|
|
217
|
+
const remoteUrl = execSync('git config --get remote.origin.url', {
|
|
218
|
+
cwd: repoPath,
|
|
219
|
+
encoding: 'utf-8',
|
|
220
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
221
|
+
}).trim();
|
|
222
|
+
// Parse GitHub URL: https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
223
|
+
const match = remoteUrl.match(/github\.com[/:]([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?$/);
|
|
224
|
+
if (match) {
|
|
225
|
+
fullName = `${match[1]}/${match[2]}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Couldn't get remote, use directory name
|
|
230
|
+
}
|
|
231
|
+
const key = fullName.toLowerCase();
|
|
232
|
+
// Only register if not already tracked
|
|
233
|
+
if (!this.config.repos[key]) {
|
|
234
|
+
this.config.repos[key] = {
|
|
235
|
+
fullName,
|
|
236
|
+
localName: entry.name,
|
|
237
|
+
path: repoPath,
|
|
238
|
+
status: 'cloned',
|
|
239
|
+
lastSynced: new Date().toISOString(),
|
|
240
|
+
defaultBranch: this.getDefaultBranch(repoPath),
|
|
241
|
+
addedAt: new Date().toISOString(),
|
|
242
|
+
};
|
|
243
|
+
logger.info('Registered existing repo', { fullName, path: repoPath });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.saveConfig();
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
logger.warn('Failed to scan for existing repos', { error: String(err) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Clone a repository
|
|
254
|
+
*/
|
|
255
|
+
gitClone(fullName, targetPath) {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const url = `https://github.com/${fullName}.git`;
|
|
258
|
+
logger.info('Cloning', { url, targetPath });
|
|
259
|
+
const proc = spawn('git', ['clone', url, targetPath], {
|
|
260
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
261
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
262
|
+
});
|
|
263
|
+
let stderr = '';
|
|
264
|
+
proc.stderr?.on('data', (data) => {
|
|
265
|
+
stderr += data.toString();
|
|
266
|
+
});
|
|
267
|
+
proc.on('close', (code) => {
|
|
268
|
+
if (code === 0) {
|
|
269
|
+
resolve();
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
reject(new Error(`git clone failed (code ${code}): ${stderr}`));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
proc.on('error', (err) => {
|
|
276
|
+
reject(err);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Pull updates for a repository
|
|
282
|
+
*/
|
|
283
|
+
gitPull(repoPath, fullName) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
logger.info('Pulling', { repoPath });
|
|
286
|
+
// First update remote URL in case it changed
|
|
287
|
+
try {
|
|
288
|
+
const url = `https://github.com/${fullName}.git`;
|
|
289
|
+
execSync(`git remote set-url origin "${url}"`, {
|
|
290
|
+
cwd: repoPath,
|
|
291
|
+
stdio: 'ignore',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Ignore - remote might not exist yet
|
|
296
|
+
}
|
|
297
|
+
// Fetch and pull
|
|
298
|
+
const proc = spawn('git', ['pull', '--ff-only'], {
|
|
299
|
+
cwd: repoPath,
|
|
300
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
301
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
302
|
+
});
|
|
303
|
+
let stderr = '';
|
|
304
|
+
proc.stderr?.on('data', (data) => {
|
|
305
|
+
stderr += data.toString();
|
|
306
|
+
});
|
|
307
|
+
proc.on('close', (code) => {
|
|
308
|
+
if (code === 0) {
|
|
309
|
+
resolve();
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Try fetch --all as fallback (handles diverged branches better)
|
|
313
|
+
try {
|
|
314
|
+
execSync('git fetch --all --prune', { cwd: repoPath, stdio: 'ignore' });
|
|
315
|
+
resolve();
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
reject(new Error(`git pull failed (code ${code}): ${stderr}`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
proc.on('error', (err) => {
|
|
323
|
+
reject(err);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get the default branch of a repo
|
|
329
|
+
*/
|
|
330
|
+
getDefaultBranch(repoPath) {
|
|
331
|
+
try {
|
|
332
|
+
const result = execSync('git symbolic-ref --short HEAD', {
|
|
333
|
+
cwd: repoPath,
|
|
334
|
+
encoding: 'utf-8',
|
|
335
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
336
|
+
});
|
|
337
|
+
return result.trim();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return 'main';
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Mark directory as safe for git (prevents "dubious ownership" errors)
|
|
345
|
+
*/
|
|
346
|
+
markSafeDirectory(repoPath) {
|
|
347
|
+
try {
|
|
348
|
+
execSync(`git config --global --add safe.directory "${repoPath}"`, {
|
|
349
|
+
stdio: 'ignore',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Ignore errors
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Singleton instance
|
|
358
|
+
let repoManagerInstance = null;
|
|
359
|
+
/**
|
|
360
|
+
* Get or create the repo manager instance
|
|
361
|
+
*/
|
|
362
|
+
export function getRepoManager(workspaceDir) {
|
|
363
|
+
if (!repoManagerInstance) {
|
|
364
|
+
const dir = workspaceDir || process.env.WORKSPACE_DIR || '/workspace';
|
|
365
|
+
repoManagerInstance = new RepoManager({ workspaceDir: dir });
|
|
366
|
+
}
|
|
367
|
+
return repoManagerInstance;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Initialize repo manager (call at startup)
|
|
371
|
+
*
|
|
372
|
+
* 1. Scans workspace for existing repos (handles entrypoint.sh clones)
|
|
373
|
+
* 2. Syncs any repos from REPOSITORIES env var that aren't already cloned
|
|
374
|
+
*/
|
|
375
|
+
export async function initRepoManager(workspaceDir) {
|
|
376
|
+
const manager = getRepoManager(workspaceDir);
|
|
377
|
+
// First, scan for repos already cloned by entrypoint.sh
|
|
378
|
+
manager.scanExistingRepos();
|
|
379
|
+
// Then sync any additional repos from env var
|
|
380
|
+
// (syncRepos skips repos that are already cloned and up-to-date)
|
|
381
|
+
await manager.initFromEnv();
|
|
382
|
+
return manager;
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=repo-manager.js.map
|