-
-
Notifications
You must be signed in to change notification settings - Fork 81
Expand file tree
/
Copy pathcodinitBackend.ts
More file actions
187 lines (164 loc) · 6.55 KB
/
Copy pathcodinitBackend.ts
File metadata and controls
187 lines (164 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { Mutex } from 'async-mutex';
import { existsSync, mkdirSync, writeFileSync, chmodSync, openSync } from 'fs';
import path from 'path';
import os from 'os';
import JSZip from 'jszip';
import portfinder from 'portfinder';
import { ChildProcess, spawn } from 'child_process';
import type { CodinitProject } from 'codinit-agent/types.js';
import { cleanCodinitOutput } from 'codinit-agent/utils/shell.js';
import { execFile } from './utils.js';
import { logger } from 'codinit-agent/utils/logger.js';
import { wrapTraced } from 'braintrust';
const instance_name = 'carnitas';
const instance_secret = '4361726e697461732c206c69746572616c6c79206d65616e696e6720226c6974';
const admin_key =
'0135d8598650f8f5cb0f30c34ec2e2bb62793bc28717c8eb6fb577996d50be5f4281b59181095065c5d0f86a2c31ddbe9b597ec62b47ded69782cd';
const codinitRelease = fetch('https://api.github.com/repos/get-codinit/codinit-backend/releases')
.then((r) => r.json() as Promise<any[]>)
.then((releases) => releases.find((release: any) => release.prerelease === false));
const downloadBinaryMutex = new Mutex();
const portMutex = new Mutex();
export interface CodinitBackend {
port: number;
siteProxyPort: number;
process: ChildProcess;
project: CodinitProject;
}
export async function withCodinitBackend<T>(backendDir: string, fn: (backend: CodinitBackend) => Promise<T>): Promise<T> {
const storageDir = path.join(backendDir, 'codinit_local_storage');
mkdirSync(storageDir, { recursive: true });
const sqlitePath = path.join(backendDir, 'codinit_local_backend.sqlite3');
const codinitBinary = await downloadCodinitBinary();
const { port, siteProxyPort, process } = await portMutex.runExclusive(async () => {
const port = await portfinder.getPortPromise();
// NB: `port` is currently unused, but we want `portFinder` to pick something else.
const siteProxyPort = await portfinder.getPortPromise({ port: port + 1 });
const args = [
'--port',
port.toString(),
'--site-proxy-port',
siteProxyPort.toString(),
'--instance-name',
instance_name,
'--instance-secret',
instance_secret,
'--local-storage',
storageDir,
sqlitePath,
];
const process = spawn(codinitBinary, args, {
cwd: backendDir,
stdio: [
null,
openSync(path.join(backendDir, 'backend.stdout.log'), 'w'),
openSync(path.join(backendDir, 'backend.stderr.log'), 'w'),
],
});
await healthcheck(port);
if (process.exitCode !== null) {
throw new Error(`Codinit backend exited with code ${process.exitCode}`);
}
return { port, siteProxyPort, process };
});
try {
const project = {
deploymentUrl: `http://localhost:${port}`,
deploymentName: instance_name,
projectSlug: 'codinit',
teamSlug: 'codinit',
token: admin_key,
};
return await fn({ port, siteProxyPort, process, project });
} finally {
process.kill();
}
}
export const deploy = wrapTraced(async function deploy(repoDir: string, backend: CodinitBackend) {
const args = ['codinit', 'dev', '--once', '--admin-key', admin_key, '--url', backend.project.deploymentUrl];
const { stdout, stderr } = await execFile('npx', args, { cwd: repoDir });
return cleanCodinitOutput(stdout.toString() + stderr.toString());
});
export const runTypecheck = wrapTraced(async function runTypecheck(repoDir: string) {
const args = ['tsc', '--noEmit', '--project', 'tsconfig.app.json'];
const { stdout, stderr } = await execFile('npx', args, { cwd: repoDir });
return cleanCodinitOutput(stdout.toString() + stderr.toString());
});
export const npmInstall = wrapTraced(async function npmInstall(repoDir: string, packages: string[]) {
const args = ['npm', 'install', ...packages];
const { stdout, stderr } = await execFile('npx', args, { cwd: repoDir });
return cleanCodinitOutput(stdout.toString() + stderr.toString());
});
const downloadCodinitBinary = wrapTraced(async function downloadCodinitBinary() {
const latest = await codinitRelease;
const version = latest['tag_name'];
const arch = ({ x64: 'x86_64', arm64: 'aarch64' } as Record<string, string>)[os.arch()];
if (!arch) {
throw new Error(`Unsupported architecture: ${os.arch()}`);
}
const tripleOs = {
darwin: 'apple-darwin',
linux: 'unknown-linux-gnu',
win32: 'pc-windows-msvc',
}[os.platform() as string];
if (!tripleOs) {
throw new Error(`Unsupported platform: ${os.platform()}`);
}
const targetPattern = `codinit-local-backend-${arch}-${tripleOs}`;
const matchingAsset = latest['assets'].find((asset: any) => asset['name'].includes(targetPattern));
if (!matchingAsset) {
throw new Error(`Could not find matching asset for ${targetPattern}`);
}
const binaryDir = path.join(os.homedir(), '.codinit-evals', 'releases');
mkdirSync(binaryDir, { recursive: true });
// Include version in binary name
const binaryName = `codinit-local-backend-${version}${os.platform() === 'win32' ? '.exe' : ''}`;
const binaryPath = path.join(binaryDir, binaryName);
return await downloadBinaryMutex.runExclusive(async () => {
if (existsSync(binaryPath)) {
return binaryPath;
}
logger.info('Latest release:', version);
const url = matchingAsset['browser_download_url'];
logger.info('Downloading:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download: ${response.statusText}`);
}
const zipBuffer = await response.arrayBuffer();
const zip = await JSZip.loadAsync(zipBuffer);
// Extract the binary
const extractedBinary = await zip.file('codinit-local-backend')?.async('nodebuffer');
if (!extractedBinary) {
throw new Error('Could not find binary in zip file');
}
// Write the binary to disk
mkdirSync(path.dirname(binaryPath), { recursive: true });
writeFileSync(binaryPath, extractedBinary);
// Make the binary executable on Unix systems
if (os.platform() !== 'win32') {
chmodSync(binaryPath, 0o755);
}
logger.info('Extracted binary to:', binaryPath);
return binaryPath;
});
});
const healthcheck = wrapTraced(async function healthcheck(port: number) {
const deadline = Date.now() + 10000;
let numAttempts = 0;
while (true) {
try {
const response = await fetch(`http://localhost:${port}/version`);
if (response.ok) {
return true;
}
} catch (e) {
const remaining = deadline - Date.now();
if (remaining < 0) {
throw e;
}
await new Promise((resolve) => setTimeout(resolve, Math.min(0.1 * 2 ** numAttempts, remaining)));
numAttempts++;
}
}
});