@gitterm/sdk 0.1.0 → 0.1.2
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/README.md +125 -16
- package/dist/direct/client.d.ts +3 -1
- package/dist/direct/index.d.ts +1 -1
- package/dist/direct/index.js +264 -40
- package/dist/direct/provisioning.d.ts +5 -0
- package/dist/direct/types.d.ts +36 -2
- package/dist/index.js +12 -12
- package/dist/types.d.ts +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ Direct mode runs an agent using your cloud-provider account without a Gitterm se
|
|
|
10
10
|
|
|
11
11
|
All built-in compute providers use the same provisioning plan and workspace/run API:
|
|
12
12
|
|
|
13
|
-
| Provider | Direct prerequisite
|
|
14
|
-
| -------- |
|
|
15
|
-
| E2B | OpenCode-compatible template
|
|
16
|
-
| Daytona | Public Gitterm OpenCode server image by default
|
|
17
|
-
| Vercel | Vercel Sandbox project
|
|
18
|
-
| Ascii | Box API key
|
|
19
|
-
| exe.dev |
|
|
20
|
-
| Railway | Project/environment and public service domains
|
|
13
|
+
| Provider | Direct prerequisite | Persistent pause | Keep-alive |
|
|
14
|
+
| -------- | -------------------------------------------------------------- | ---------------- | ---------- |
|
|
15
|
+
| E2B | OpenCode-compatible template | Yes | Yes |
|
|
16
|
+
| Daytona | Public Gitterm OpenCode server image by default | Yes | Yes |
|
|
17
|
+
| Vercel | Vercel Sandbox project | Yes | Yes |
|
|
18
|
+
| Ascii | Box API key | Yes | Yes |
|
|
19
|
+
| exe.dev | Lifecycle token, or an existing VM with `ls,ssh,share,ssh-key` | Yes | No |
|
|
20
|
+
| Railway | Project/environment and public service domains | With a volume | No |
|
|
21
21
|
|
|
22
22
|
AWS remains available through `createGittermClient()` and the Gitterm control plane; it is intentionally not exposed in direct mode.
|
|
23
23
|
|
|
@@ -53,6 +53,43 @@ try {
|
|
|
53
53
|
|
|
54
54
|
Every adapter receives the same normalized plan: repository/ref and optional Git credentials, agent files, model credentials, environment, setup commands, serve command, and port. Provider-specific configuration only describes how to allocate and expose compute.
|
|
55
55
|
|
|
56
|
+
Direct setup has explicit phases. `beforeAgent` blocks workspace creation, while
|
|
57
|
+
`afterAgent` runs in the background and can be observed with `setupStatus()` or
|
|
58
|
+
`waitForSetup()`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const workspace = await direct.workspaces.create({
|
|
62
|
+
repo: "https://github.com/acme/project",
|
|
63
|
+
setup: {
|
|
64
|
+
beforeAgent: ["npm install"],
|
|
65
|
+
afterAgent: ["npm run generate"],
|
|
66
|
+
},
|
|
67
|
+
secretFiles: [
|
|
68
|
+
{
|
|
69
|
+
path: "~/.config/gcloud/service-account.json",
|
|
70
|
+
content: process.env.GCP_SERVICE_ACCOUNT_JSON!,
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await direct.workspaces.waitForSetup(workspace);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
To attach to an existing exe.dev VM without giving Gitterm ownership of that VM, pass
|
|
80
|
+
`exedev: { existingVmName: "acme-agent-machine" }` to `workspaces.create()`. Terminating
|
|
81
|
+
that workspace stops only its tracked agent process and does not remove the VM.
|
|
82
|
+
|
|
83
|
+
Trusted integration context can be appended to the generated global `AGENTS.md` without changing the model system prompt:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
await direct.workspaces.create({
|
|
87
|
+
repo: "https://github.com/acme/project",
|
|
88
|
+
additionalAgentInstructions:
|
|
89
|
+
"You are running as a Slack bot. Keep responses concise and suitable for a thread.",
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
56
93
|
### Provider authentication
|
|
57
94
|
|
|
58
95
|
Direct workspaces can start OpenCode provider authentication without shell access. Discover the provider's methods and select a headless or device-code OAuth method when OpenCode is running remotely:
|
|
@@ -187,6 +224,49 @@ const client = createGittermClient({
|
|
|
187
224
|
const { workspaces } = await client.workspaces.list();
|
|
188
225
|
```
|
|
189
226
|
|
|
227
|
+
### Managed private repositories
|
|
228
|
+
|
|
229
|
+
For renewable, short-lived repository authentication, connect the GitHub App in the GitTerm
|
|
230
|
+
dashboard and copy its **SDK integration ID** from the Integrations page:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const { workspace, runtime } = await client.workspaces.create({
|
|
234
|
+
repo: "https://github.com/acme/private-repo",
|
|
235
|
+
branch: "main",
|
|
236
|
+
gitIntegrationId: "your-dashboard-integration-id",
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Managed workspaces can also use dashboard-managed model subscriptions while accepting an
|
|
241
|
+
application-owned GitHub PAT inline:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const client = createGittermClient({
|
|
245
|
+
token: process.env.GITTERM_API_TOKEN,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const { workspace, runtime } = await client.workspaces.create({
|
|
249
|
+
repo: "https://github.com/acme/private-repo",
|
|
250
|
+
branch: "main",
|
|
251
|
+
repositoryCredentials: {
|
|
252
|
+
username: "x-access-token",
|
|
253
|
+
token: process.env.GITHUB_TOKEN!,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The username defaults to `x-access-token`. Inline repository credentials take precedence over
|
|
259
|
+
`gitIntegrationId` and authenticate repository validation, cloning, and runtime Git operations such
|
|
260
|
+
as pull and push. Without inline credentials, `gitIntegrationId` continues to use the connected
|
|
261
|
+
dashboard integration. Omitting `modelCredentialIds` and `modelCredentials` likewise continues to
|
|
262
|
+
use dashboard-managed model credentials.
|
|
263
|
+
|
|
264
|
+
GitTerm does not save inline PATs in its application database. Inline PATs must be delivered to the
|
|
265
|
+
selected compute provider and retained on the workspace machine for runtime Git operations, so
|
|
266
|
+
provider infrastructure and processes running in that workspace may be able to access them. Prefer
|
|
267
|
+
`gitIntegrationId` for durable managed workspaces and use narrowly scoped, short-lived PATs when
|
|
268
|
+
inline credentials are necessary.
|
|
269
|
+
|
|
190
270
|
The SDK deliberately exposes two clients. `createGittermClient()` uses a user API token and
|
|
191
271
|
can manage the user's workspaces. `createGittermWorkspaceClient()` uses the scoped identity
|
|
192
272
|
injected into a GitTerm workspace and can inspect only that workspace and its ports:
|
|
@@ -237,7 +317,10 @@ Override only the placement decisions your integration cares about:
|
|
|
237
317
|
await client.workspaces.create({
|
|
238
318
|
repo: "https://github.com/acme/product",
|
|
239
319
|
agent: "opencode",
|
|
240
|
-
|
|
320
|
+
setup: {
|
|
321
|
+
beforeAgent: ["npm install"],
|
|
322
|
+
afterAgent: ["npm run generate"],
|
|
323
|
+
},
|
|
241
324
|
opencode: {
|
|
242
325
|
skills: [
|
|
243
326
|
{
|
|
@@ -259,12 +342,38 @@ Follow the repository's release-demo workflow.`,
|
|
|
259
342
|
});
|
|
260
343
|
```
|
|
261
344
|
|
|
262
|
-
Setup commands run in order from the checked-out repository
|
|
263
|
-
|
|
264
|
-
agent
|
|
265
|
-
`client.workspaces.setupStatus(workspaceId)`
|
|
266
|
-
|
|
267
|
-
repository's git-excluded `.gitterm/setup/` directory.
|
|
345
|
+
Setup commands run in order from the checked-out repository. `beforeAgent` blocks agent
|
|
346
|
+
startup; when it fails, `create()` rejects with the tail of its log. `afterAgent` starts
|
|
347
|
+
after the agent is reachable and reports status independently. Provider and agent defaults
|
|
348
|
+
configured by an administrator run first. Use `client.workspaces.setupStatus(workspaceId)`
|
|
349
|
+
or `waitForSetup(workspaceId)` to inspect the `afterAgent` phase. GitTerm persists bounded
|
|
350
|
+
logs and a recovery copy in the repository's git-excluded `.gitterm/setup/` directory.
|
|
351
|
+
Setup commands can reference the checkout with `$WORKSPACE_REPO_DIR`, which is the same on
|
|
352
|
+
every provider even though the underlying path differs.
|
|
353
|
+
|
|
354
|
+
Secret files are created relative to the repository with restrictive permissions and are
|
|
355
|
+
added to `.git/info/exclude` so the agent cannot commit them. GitTerm does not retain their
|
|
356
|
+
contents; to rotate a secret, recreate the workspace. Like model credentials, they are
|
|
357
|
+
delivered to the sandbox through its launch environment, so anyone who can read the
|
|
358
|
+
provider's task or container definition can read them:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
await client.workspaces.create({
|
|
362
|
+
repo: "https://github.com/acme/product",
|
|
363
|
+
secretFiles: [
|
|
364
|
+
{
|
|
365
|
+
path: ".secrets/gcp.json",
|
|
366
|
+
content: process.env.GCP_SERVICE_ACCOUNT_JSON!,
|
|
367
|
+
mode: "0600",
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
setup: {
|
|
371
|
+
beforeAgent: [
|
|
372
|
+
'gcloud auth activate-service-account --key-file "$WORKSPACE_REPO_DIR/.secrets/gcp.json"',
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
```
|
|
268
377
|
|
|
269
378
|
`provider` is a discriminated union, so TypeScript only offers `region` for providers
|
|
270
379
|
where GitTerm supports caller-selected placement. Machine keys are configured by admins
|
|
@@ -286,7 +395,7 @@ it does not claim that a pull request, upload, or other product outcome succeede
|
|
|
286
395
|
```ts
|
|
287
396
|
const { workspace } = await client.workspaces.create({
|
|
288
397
|
repo: "https://github.com/acme/product",
|
|
289
|
-
|
|
398
|
+
setup: { afterAgent: ["npm install", "npm run db:seed"] },
|
|
290
399
|
});
|
|
291
400
|
|
|
292
401
|
const run = await client.runs.create({
|
package/dist/direct/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DirectProviderAdapter, DirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthWaitOptions, DirectModelCredential, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput } from "./types.js";
|
|
1
|
+
import type { DirectProviderAdapter, DirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthWaitOptions, DirectModelCredential, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceSetupStatus, DirectWorkspaceSetupWaitOptions } from "./types.js";
|
|
2
2
|
export type DirectGittermClientOptions = {
|
|
3
3
|
provider: DirectProviderAdapter | DirectProviderConfig;
|
|
4
4
|
};
|
|
@@ -36,6 +36,8 @@ export declare function createDirectGittermClient(options: DirectGittermClientOp
|
|
|
36
36
|
resume(workspace: DirectWorkspace): Promise<DirectWorkspace>;
|
|
37
37
|
terminate(workspace: DirectWorkspace): Promise<DirectWorkspace>;
|
|
38
38
|
keepAlive(workspace: DirectWorkspace, timeoutMs: number): Promise<void>;
|
|
39
|
+
setupStatus: (workspace: DirectWorkspace) => Promise<DirectWorkspaceSetupStatus>;
|
|
40
|
+
waitForSetup(workspace: DirectWorkspace, wait?: DirectWorkspaceSetupWaitOptions): Promise<DirectWorkspaceSetupStatus>;
|
|
39
41
|
};
|
|
40
42
|
runs: {
|
|
41
43
|
create(input: DirectRunCreateInput): Promise<DirectRun>;
|
package/dist/direct/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { createDaytonaDirectProvider } from "./daytona.js";
|
|
|
5
5
|
export { createExeDevDirectProvider } from "./exedev.js";
|
|
6
6
|
export { createRailwayDirectProvider } from "./railway.js";
|
|
7
7
|
export { createVercelDirectProvider } from "./vercel.js";
|
|
8
|
-
export type { AsciiDirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthMethod, DirectAuthPrompt, DirectAuthWaitOptions, DirectApiModelCredential, DaytonaDirectProviderConfig, DirectModelCredential, DirectOAuthModelCredential, DirectProviderAdapter, DirectProviderCapabilities, DirectProviderConfig, DirectProviderWorkspaceInput, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceLifecycle, DirectWorkspaceRuntime, E2BDirectProviderConfig, ExeDevDirectProviderConfig, RailwayDirectProviderConfig, VercelDirectProviderConfig, } from "./types.js";
|
|
8
|
+
export type { AsciiDirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthMethod, DirectAuthPrompt, DirectAuthWaitOptions, DirectApiModelCredential, DaytonaDirectProviderConfig, DirectModelCredential, DirectOAuthModelCredential, DirectProviderAdapter, DirectProviderCapabilities, DirectProviderConfig, DirectProviderWorkspaceInput, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectSecretFile, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceLifecycle, DirectWorkspaceRuntime, DirectWorkspaceSetup, DirectWorkspaceSetupStatus, DirectWorkspaceSetupWaitOptions, E2BDirectProviderConfig, ExeDevDirectProviderConfig, RailwayDirectProviderConfig, VercelDirectProviderConfig, } from "./types.js";
|
package/dist/direct/index.js
CHANGED
|
@@ -16,6 +16,8 @@ var DIRECT_E2B_TEMPLATES = {
|
|
|
16
16
|
standard: "gitterm-opencode-server",
|
|
17
17
|
large: "gitterm-opencode-server-lg"
|
|
18
18
|
};
|
|
19
|
+
var DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.";
|
|
20
|
+
var MAX_ADDITIONAL_AGENT_INSTRUCTIONS = 50000;
|
|
19
21
|
function resolveDirectImage(image) {
|
|
20
22
|
return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
|
|
21
23
|
}
|
|
@@ -46,6 +48,28 @@ function directModelAuth(credential) {
|
|
|
46
48
|
function base64(value) {
|
|
47
49
|
return Buffer.from(value).toString("base64");
|
|
48
50
|
}
|
|
51
|
+
function validateDirectFilePath(path) {
|
|
52
|
+
if (path.includes("\x00") || !path.startsWith("/") && !path.startsWith("~/") || path.split("/").some((part) => part === ".." || part === ".") || path === "/" || path === "~/") {
|
|
53
|
+
throw new Error(`Invalid secret file path: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
function validateDirectFileMode(mode = 384) {
|
|
58
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 511) {
|
|
59
|
+
throw new Error("Secret file mode must be an integer between 0000 and 0777");
|
|
60
|
+
}
|
|
61
|
+
return mode;
|
|
62
|
+
}
|
|
63
|
+
function buildDirectGittermInstructions(additional) {
|
|
64
|
+
const trimmed = additional?.trim();
|
|
65
|
+
if (trimmed && trimmed.length > MAX_ADDITIONAL_AGENT_INSTRUCTIONS) {
|
|
66
|
+
throw new Error("additionalAgentInstructions is too large");
|
|
67
|
+
}
|
|
68
|
+
return trimmed ? `${DIRECT_GITTERM_INSTRUCTIONS}
|
|
69
|
+
|
|
70
|
+
${trimmed}
|
|
71
|
+
` : DIRECT_GITTERM_INSTRUCTIONS;
|
|
72
|
+
}
|
|
49
73
|
function validateName(value, kind) {
|
|
50
74
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) || value === "." || value === "..") {
|
|
51
75
|
throw new Error(`Invalid ${kind}: ${value}`);
|
|
@@ -89,10 +113,15 @@ function buildDirectProvisioningPlan(input) {
|
|
|
89
113
|
const environmentVariables = { ...input.environmentVariables };
|
|
90
114
|
delete environmentVariables.OPENCODE_SERVER_USERNAME;
|
|
91
115
|
delete environmentVariables.GITTERM_DIRECT_PROVIDER;
|
|
116
|
+
const userPermission = input.opencode?.config?.permission && typeof input.opencode.config.permission === "object" && !Array.isArray(input.opencode.config.permission) ? input.opencode.config.permission : {};
|
|
92
117
|
const config = {
|
|
93
118
|
$schema: "https://opencode.ai/config.json",
|
|
94
119
|
...input.opencode?.config,
|
|
95
120
|
username: "Gitterm direct",
|
|
121
|
+
permission: {
|
|
122
|
+
external_directory: "allow",
|
|
123
|
+
...userPermission
|
|
124
|
+
},
|
|
96
125
|
...plugins.length ? { plugin: plugins } : {}
|
|
97
126
|
};
|
|
98
127
|
const files = [
|
|
@@ -106,13 +135,21 @@ function buildDirectProvisioningPlan(input) {
|
|
|
106
135
|
},
|
|
107
136
|
{
|
|
108
137
|
path: "~/.config/opencode/AGENTS.md",
|
|
109
|
-
contentBase64: base64(
|
|
138
|
+
contentBase64: base64(buildDirectGittermInstructions(input.additionalAgentInstructions))
|
|
110
139
|
},
|
|
111
140
|
...(input.opencode?.skills ?? []).map((skill) => ({
|
|
112
141
|
path: `~/.config/opencode/skills/${validateName(skill.name, "skill name")}/SKILL.md`,
|
|
113
142
|
contentBase64: base64(skill.content)
|
|
143
|
+
})),
|
|
144
|
+
...(input.secretFiles ?? []).map((file) => ({
|
|
145
|
+
path: validateDirectFilePath(file.path),
|
|
146
|
+
contentBase64: base64(file.content),
|
|
147
|
+
mode: validateDirectFileMode(file.mode)
|
|
114
148
|
}))
|
|
115
149
|
];
|
|
150
|
+
const duplicatePath = files.find((file, index) => files.findIndex((candidate) => candidate.path === file.path) !== index);
|
|
151
|
+
if (duplicatePath)
|
|
152
|
+
throw new Error(`Duplicate provisioned file path: ${duplicatePath.path}`);
|
|
116
153
|
return {
|
|
117
154
|
workspaceId: input.id,
|
|
118
155
|
lifecycle: input.lifecycle,
|
|
@@ -134,11 +171,18 @@ function buildDirectProvisioningPlan(input) {
|
|
|
134
171
|
command: DIRECT_OPENCODE_COMMAND,
|
|
135
172
|
port: DIRECT_OPENCODE_PORT
|
|
136
173
|
},
|
|
137
|
-
|
|
174
|
+
setup: {
|
|
175
|
+
beforeAgent: input.setup?.beforeAgent ?? [],
|
|
176
|
+
afterAgent: input.setup?.afterAgent ?? []
|
|
177
|
+
}
|
|
138
178
|
};
|
|
139
179
|
}
|
|
140
180
|
function railwayContainerEnvironment(plan) {
|
|
141
181
|
const repository = plan.repository;
|
|
182
|
+
const beforeAgent = [
|
|
183
|
+
...plan.agent.files.flatMap((file) => file.mode == null ? [] : [`chmod ${file.mode.toString(8)} ${shellPath(file.path)}`]),
|
|
184
|
+
...plan.setup.beforeAgent
|
|
185
|
+
];
|
|
142
186
|
return {
|
|
143
187
|
...plan.agent.environmentVariables,
|
|
144
188
|
...repository ? {
|
|
@@ -153,7 +197,7 @@ function railwayContainerEnvironment(plan) {
|
|
|
153
197
|
} : {}
|
|
154
198
|
} : {},
|
|
155
199
|
AGENT_FILES_BASE64: base64(JSON.stringify(plan.agent.files)),
|
|
156
|
-
...
|
|
200
|
+
...beforeAgent.length ? { WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(beforeAgent)) } : {},
|
|
157
201
|
GITTERM_DIRECT_PROVIDER: "railway"
|
|
158
202
|
};
|
|
159
203
|
}
|
|
@@ -164,6 +208,9 @@ function setupCommandScript(commands) {
|
|
|
164
208
|
function shellQuote(value) {
|
|
165
209
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
166
210
|
}
|
|
211
|
+
function shellPath(path) {
|
|
212
|
+
return path.startsWith("~/") ? `"$HOME"/${shellQuote(path.slice(2))}` : shellQuote(path);
|
|
213
|
+
}
|
|
167
214
|
function cloneRepositoryScript(repository, directory) {
|
|
168
215
|
const ref = repository.checkoutRef ?? repository.branch;
|
|
169
216
|
const commands = [
|
|
@@ -333,9 +380,12 @@ function createAsciiDirectProvider(config) {
|
|
|
333
380
|
boxId: handle.boxId,
|
|
334
381
|
fileWriteRequest: { path, content: file.contentBase64, encoding: "base64" }
|
|
335
382
|
});
|
|
383
|
+
if (file.mode != null) {
|
|
384
|
+
await runCommand(handle.boxId, `chmod ${file.mode.toString(8)} ${shellQuote(`${HOME}/${path}`)}`);
|
|
385
|
+
}
|
|
336
386
|
}
|
|
337
|
-
if (plan.
|
|
338
|
-
await runCommand(handle.boxId, setupCommandScript(plan.
|
|
387
|
+
if (plan.setup.beforeAgent.length) {
|
|
388
|
+
await runCommand(handle.boxId, setupCommandScript(plan.setup.beforeAgent), directory, 600);
|
|
339
389
|
}
|
|
340
390
|
await startRuntime(handle);
|
|
341
391
|
const runtime = {
|
|
@@ -499,10 +549,10 @@ function createDaytonaDirectProvider(config) {
|
|
|
499
549
|
for (const file of plan.agent.files) {
|
|
500
550
|
const target = file.path.replace(/^~/, home);
|
|
501
551
|
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
502
|
-
await execute(sandbox, `mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(target)}`);
|
|
552
|
+
await execute(sandbox, `mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(target)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(target)}`}`);
|
|
503
553
|
}
|
|
504
|
-
if (plan.
|
|
505
|
-
await execute(sandbox, setupCommandScript(plan.
|
|
554
|
+
if (plan.setup.beforeAgent.length) {
|
|
555
|
+
await execute(sandbox, setupCommandScript(plan.setup.beforeAgent), directory);
|
|
506
556
|
}
|
|
507
557
|
await startAgent(sandbox, handle);
|
|
508
558
|
const runtime = await runtimeFor(sandbox, handle, input.password);
|
|
@@ -625,10 +675,12 @@ function createE2BDirectProvider(config) {
|
|
|
625
675
|
for (const file of plan.agent.files) {
|
|
626
676
|
const path = file.path.replace(/^~/, "/home/user");
|
|
627
677
|
const parent = path.slice(0, path.lastIndexOf("/"));
|
|
628
|
-
await sandbox.commands.run(`mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
|
|
678
|
+
await sandbox.commands.run(`mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(path)}`}`);
|
|
629
679
|
}
|
|
630
|
-
if (plan.
|
|
631
|
-
await sandbox.commands.run(setupCommandScript(plan.
|
|
680
|
+
if (plan.setup.beforeAgent.length) {
|
|
681
|
+
await sandbox.commands.run(setupCommandScript(plan.setup.beforeAgent), {
|
|
682
|
+
cwd: directory
|
|
683
|
+
});
|
|
632
684
|
}
|
|
633
685
|
await sandbox.commands.run(plan.agent.command, {
|
|
634
686
|
cwd: directory,
|
|
@@ -697,7 +749,7 @@ function serializeHandle2(handle) {
|
|
|
697
749
|
function parseHandle3(value) {
|
|
698
750
|
try {
|
|
699
751
|
const handle = JSON.parse(value);
|
|
700
|
-
if (!handle.vmName || !handle.repoDir || !handle.serve?.command || !handle.serve.port) {
|
|
752
|
+
if (!handle.vmName || !handle.repoDir || !handle.serve?.command || !handle.serve.port || !handle.pidFile || typeof handle.owned !== "boolean") {
|
|
701
753
|
throw new Error("missing required fields");
|
|
702
754
|
}
|
|
703
755
|
return handle;
|
|
@@ -748,8 +800,9 @@ function createExeDevDirectProvider(config) {
|
|
|
748
800
|
}
|
|
749
801
|
const runVmCommand = (handle, command) => execute(`ssh ${handle.vmName} -- bash -lc ${shellQuote(command)}`);
|
|
750
802
|
async function startRuntime(handle) {
|
|
751
|
-
await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null
|
|
803
|
+
await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null & printf %s "$!" > ${shellQuote(handle.pidFile)}`);
|
|
752
804
|
}
|
|
805
|
+
const stopRuntime = (handle) => runVmCommand(handle, `if [ -f ${shellQuote(handle.pidFile)} ]; then kill "$(cat ${shellQuote(handle.pidFile)})" 2>/dev/null || true; rm -f ${shellQuote(handle.pidFile)}; fi`);
|
|
753
806
|
async function accessToken(vmName) {
|
|
754
807
|
const token = findToken(await execute(`ssh-key generate-api-key --vm=${vmName} --label=gitterm-direct --exp=never`));
|
|
755
808
|
if (!token)
|
|
@@ -787,11 +840,19 @@ function createExeDevDirectProvider(config) {
|
|
|
787
840
|
async create(input) {
|
|
788
841
|
const plan = input.provisioning;
|
|
789
842
|
const vmSuffix = input.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 20).toLowerCase();
|
|
790
|
-
const
|
|
843
|
+
const attachedVmName = input.exedev?.existingVmName.trim();
|
|
844
|
+
if (input.exedev && !attachedVmName)
|
|
845
|
+
throw new Error("exe.dev existingVmName is required");
|
|
846
|
+
if (attachedVmName && !/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(attachedVmName)) {
|
|
847
|
+
throw new Error("Invalid exe.dev existingVmName");
|
|
848
|
+
}
|
|
849
|
+
const vmName = attachedVmName ?? `gitterm-${vmSuffix}`;
|
|
791
850
|
const handle = {
|
|
792
851
|
vmName,
|
|
793
852
|
repoDir: `${HOME2}/${plan.repository?.name ?? "workspace"}`,
|
|
794
|
-
serve: { command: plan.agent.command, port: plan.agent.port }
|
|
853
|
+
serve: { command: plan.agent.command, port: plan.agent.port },
|
|
854
|
+
pidFile: `/tmp/gitterm-${vmSuffix}.pid`,
|
|
855
|
+
owned: !attachedVmName
|
|
795
856
|
};
|
|
796
857
|
const createArgs = [
|
|
797
858
|
`new --name=${vmName}`,
|
|
@@ -803,7 +864,8 @@ function createExeDevDirectProvider(config) {
|
|
|
803
864
|
config.disk ? `--disk=${shellQuote(config.disk)}` : "",
|
|
804
865
|
...Object.entries(plan.agent.environmentVariables).map(([key, value]) => `--env=${shellQuote(`${key}=${value}`)}`)
|
|
805
866
|
].filter(Boolean).join(" ");
|
|
806
|
-
|
|
867
|
+
if (handle.owned)
|
|
868
|
+
await execute(createArgs);
|
|
807
869
|
try {
|
|
808
870
|
await waitUntilRunning(vmName);
|
|
809
871
|
await runVmCommand(handle, `mkdir -p ${shellQuote(handle.repoDir)}`);
|
|
@@ -815,10 +877,10 @@ function createExeDevDirectProvider(config) {
|
|
|
815
877
|
}
|
|
816
878
|
for (const file of plan.agent.files) {
|
|
817
879
|
const path = file.path.startsWith("~/") ? `${HOME2}/${file.path.slice(2)}` : file.path;
|
|
818
|
-
await runVmCommand(handle, `mkdir -p ${shellQuote(path.slice(0, path.lastIndexOf("/")))} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
|
|
880
|
+
await runVmCommand(handle, `mkdir -p ${shellQuote(path.slice(0, path.lastIndexOf("/")))} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(path)}`}`);
|
|
819
881
|
}
|
|
820
|
-
if (plan.
|
|
821
|
-
await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && ${setupCommandScript(plan.
|
|
882
|
+
if (plan.setup.beforeAgent.length) {
|
|
883
|
+
await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && ${setupCommandScript(plan.setup.beforeAgent)}`);
|
|
822
884
|
}
|
|
823
885
|
await startRuntime(handle);
|
|
824
886
|
await execute(`share port ${vmName} ${handle.serve.port}`);
|
|
@@ -827,9 +889,15 @@ function createExeDevDirectProvider(config) {
|
|
|
827
889
|
await waitForDirectRuntime(directRuntime);
|
|
828
890
|
return { externalId: serializeHandle2(handle), runtime: directRuntime };
|
|
829
891
|
} catch (error) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
892
|
+
if (handle.owned) {
|
|
893
|
+
await execute(`rm ${vmName}`).catch(() => {
|
|
894
|
+
return;
|
|
895
|
+
});
|
|
896
|
+
} else {
|
|
897
|
+
await stopRuntime(handle).catch(() => {
|
|
898
|
+
return;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
833
901
|
throw error;
|
|
834
902
|
}
|
|
835
903
|
},
|
|
@@ -859,7 +927,12 @@ function createExeDevDirectProvider(config) {
|
|
|
859
927
|
return directRuntime;
|
|
860
928
|
},
|
|
861
929
|
async terminate(workspace) {
|
|
862
|
-
|
|
930
|
+
const handle = parseHandle3(workspace.externalId);
|
|
931
|
+
if (handle.owned) {
|
|
932
|
+
await execute(`rm ${handle.vmName}`);
|
|
933
|
+
} else {
|
|
934
|
+
await stopRuntime(handle);
|
|
935
|
+
}
|
|
863
936
|
}
|
|
864
937
|
};
|
|
865
938
|
}
|
|
@@ -908,6 +981,7 @@ var SERVICE_DEPLOY = `
|
|
|
908
981
|
`;
|
|
909
982
|
var LATEST_DEPLOYMENT = `
|
|
910
983
|
query DirectLatestDeployment($environmentId: String!, $serviceId: String!) {
|
|
984
|
+
service(id: $serviceId) { id deletedAt }
|
|
911
985
|
serviceInstance(environmentId: $environmentId, serviceId: $serviceId) {
|
|
912
986
|
latestDeployment { id status }
|
|
913
987
|
}
|
|
@@ -1000,10 +1074,31 @@ function createRailwayDirectProvider(config) {
|
|
|
1000
1074
|
throw new Error("Railway GraphQL response did not include data");
|
|
1001
1075
|
return result.data;
|
|
1002
1076
|
};
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1077
|
+
const missingService = (error) => error instanceof Error && /not found|does not exist|deleted|no service/i.test(error.message);
|
|
1078
|
+
const serviceSnapshot = async (serviceId) => {
|
|
1079
|
+
const response = await fetch(apiUrl, {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
headers: {
|
|
1082
|
+
"Content-Type": "application/json",
|
|
1083
|
+
Authorization: `Bearer ${config.apiToken}`
|
|
1084
|
+
},
|
|
1085
|
+
body: JSON.stringify({
|
|
1086
|
+
query: LATEST_DEPLOYMENT,
|
|
1087
|
+
variables: { environmentId: config.environmentId, serviceId }
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
if (!response.ok) {
|
|
1091
|
+
throw new Error(`Railway API request failed (${response.status} ${response.statusText})`);
|
|
1092
|
+
}
|
|
1093
|
+
const result = await response.json();
|
|
1094
|
+
if (result.data)
|
|
1095
|
+
return result.data;
|
|
1096
|
+
if (result.errors?.length) {
|
|
1097
|
+
throw new Error(`Railway GraphQL error: ${result.errors.map((error) => error.message).join(", ")}`);
|
|
1098
|
+
}
|
|
1099
|
+
throw new Error("Railway GraphQL response did not include data");
|
|
1006
1100
|
};
|
|
1101
|
+
const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
|
|
1007
1102
|
const waitForDeployment = async (serviceId, previousDeploymentId) => {
|
|
1008
1103
|
const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
1009
1104
|
while (Date.now() < deadline) {
|
|
@@ -1149,12 +1244,16 @@ function createRailwayDirectProvider(config) {
|
|
|
1149
1244
|
async status(workspace) {
|
|
1150
1245
|
const handle = parseHandle4(workspace.externalId);
|
|
1151
1246
|
try {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1247
|
+
const snapshot = await serviceSnapshot(handle.serviceId);
|
|
1248
|
+
if (!snapshot.service || snapshot.service.deletedAt)
|
|
1249
|
+
return "terminated";
|
|
1250
|
+
const deployment = snapshot.serviceInstance?.latestDeployment;
|
|
1251
|
+
if (!deployment)
|
|
1252
|
+
return "paused";
|
|
1253
|
+
return workspaceStatus(deployment.status);
|
|
1154
1254
|
} catch (error) {
|
|
1155
|
-
if (
|
|
1255
|
+
if (missingService(error))
|
|
1156
1256
|
return "terminated";
|
|
1157
|
-
}
|
|
1158
1257
|
throw error;
|
|
1159
1258
|
}
|
|
1160
1259
|
},
|
|
@@ -1307,11 +1406,15 @@ esac
|
|
|
1307
1406
|
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
1308
1407
|
await run(sandbox, `mkdir -p ${shellQuote(parent)}`);
|
|
1309
1408
|
await sandbox.writeFiles([
|
|
1310
|
-
{
|
|
1409
|
+
{
|
|
1410
|
+
path: target,
|
|
1411
|
+
content: Buffer.from(file.contentBase64, "base64"),
|
|
1412
|
+
...file.mode == null ? {} : { mode: file.mode }
|
|
1413
|
+
}
|
|
1311
1414
|
]);
|
|
1312
1415
|
}
|
|
1313
|
-
if (plan.
|
|
1314
|
-
await run(sandbox, setupCommandScript(plan.
|
|
1416
|
+
if (plan.setup.beforeAgent.length) {
|
|
1417
|
+
await run(sandbox, setupCommandScript(plan.setup.beforeAgent), handle.directory);
|
|
1315
1418
|
}
|
|
1316
1419
|
await startAgent(sandbox, handle);
|
|
1317
1420
|
const runtime = {
|
|
@@ -1383,6 +1486,7 @@ esac
|
|
|
1383
1486
|
}
|
|
1384
1487
|
|
|
1385
1488
|
// src/direct/client.ts
|
|
1489
|
+
var SETUP_DIR = ".gitterm/setup";
|
|
1386
1490
|
function resolveProvider(provider) {
|
|
1387
1491
|
if ("create" in provider)
|
|
1388
1492
|
return provider;
|
|
@@ -1480,6 +1584,38 @@ function modelParts(model) {
|
|
|
1480
1584
|
}
|
|
1481
1585
|
return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
|
|
1482
1586
|
}
|
|
1587
|
+
function validateEnvironmentVariables(values) {
|
|
1588
|
+
for (const key of Object.keys(values)) {
|
|
1589
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1590
|
+
throw new Error(`Invalid environment variable name: ${key}`);
|
|
1591
|
+
}
|
|
1592
|
+
if (key === "OPENCODE_SERVER_PASSWORD" || key === "OPENCODE_SERVER_USERNAME" || key === "GITTERM_DIRECT_PROVIDER") {
|
|
1593
|
+
throw new Error(`Environment variable ${key} is managed by Gitterm`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return values;
|
|
1597
|
+
}
|
|
1598
|
+
function setupRunner(commands) {
|
|
1599
|
+
const body = Buffer.from(setupCommandScript(commands)).toString("base64");
|
|
1600
|
+
return [
|
|
1601
|
+
"set -u",
|
|
1602
|
+
`SETUP_DIR=${shellQuote(SETUP_DIR)}`,
|
|
1603
|
+
'mkdir -p "$SETUP_DIR"',
|
|
1604
|
+
'if [ -d .git/info ]; then grep -qxF "/.gitterm/" .git/info/exclude 2>/dev/null || printf "/.gitterm/\\n" >> .git/info/exclude; fi',
|
|
1605
|
+
'printf "waiting\\n" > "$SETUP_DIR/state"',
|
|
1606
|
+
`printf %s ${shellQuote(body)} | base64 -d > "$SETUP_DIR/script.sh"`,
|
|
1607
|
+
'chmod 700 "$SETUP_DIR/script.sh"',
|
|
1608
|
+
'date -u +%Y-%m-%dT%H:%M:%SZ > "$SETUP_DIR/started-at"',
|
|
1609
|
+
'printf "running\\n" > "$SETUP_DIR/state"',
|
|
1610
|
+
'bash -e "$SETUP_DIR/script.sh" > "$SETUP_DIR/setup.log" 2>&1',
|
|
1611
|
+
"code=$?",
|
|
1612
|
+
'printf "%s\\n" "$code" > "$SETUP_DIR/exit-code"',
|
|
1613
|
+
'date -u +%Y-%m-%dT%H:%M:%SZ > "$SETUP_DIR/finished-at"',
|
|
1614
|
+
'if [ "$code" -eq 0 ]; then printf "succeeded\\n" > "$SETUP_DIR/state"; else printf "failed\\n" > "$SETUP_DIR/state"; fi',
|
|
1615
|
+
"exit $code"
|
|
1616
|
+
].join(`
|
|
1617
|
+
`);
|
|
1618
|
+
}
|
|
1483
1619
|
function createDirectGittermClient(options) {
|
|
1484
1620
|
const provider = resolveProvider(options.provider);
|
|
1485
1621
|
function assertWorkspace(workspace) {
|
|
@@ -1537,6 +1673,63 @@ function createDirectGittermClient(options) {
|
|
|
1537
1673
|
finalText: finalText || null
|
|
1538
1674
|
};
|
|
1539
1675
|
}
|
|
1676
|
+
async function startPty(workspace, command, title) {
|
|
1677
|
+
const result = await authClient(workspace).pty.create({
|
|
1678
|
+
command: "bash",
|
|
1679
|
+
args: ["-lc", command],
|
|
1680
|
+
cwd: workspace.runtime.directory,
|
|
1681
|
+
directory: workspace.runtime.directory,
|
|
1682
|
+
title
|
|
1683
|
+
});
|
|
1684
|
+
if (result.error || !result.data)
|
|
1685
|
+
throw new Error(errorMessage(result.error));
|
|
1686
|
+
return result.data;
|
|
1687
|
+
}
|
|
1688
|
+
async function setupFile(workspace, name) {
|
|
1689
|
+
const result = await authClient(workspace).file.read({
|
|
1690
|
+
directory: workspace.runtime.directory,
|
|
1691
|
+
path: `${SETUP_DIR}/${name}`
|
|
1692
|
+
});
|
|
1693
|
+
if (result.error || !result.data || result.data.type !== "text")
|
|
1694
|
+
return null;
|
|
1695
|
+
return result.data.content.trim();
|
|
1696
|
+
}
|
|
1697
|
+
async function getSetupStatus(workspace) {
|
|
1698
|
+
assertWorkspace(workspace);
|
|
1699
|
+
if (workspace.setup === "not_requested") {
|
|
1700
|
+
return {
|
|
1701
|
+
status: "not_requested",
|
|
1702
|
+
exitCode: null,
|
|
1703
|
+
startedAt: null,
|
|
1704
|
+
finishedAt: null,
|
|
1705
|
+
log: null
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
if (workspace.setup === "before_agent_complete") {
|
|
1709
|
+
return {
|
|
1710
|
+
status: "succeeded",
|
|
1711
|
+
exitCode: 0,
|
|
1712
|
+
startedAt: null,
|
|
1713
|
+
finishedAt: null,
|
|
1714
|
+
log: null
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
const [state, exitCode, startedAt, finishedAt, log] = await Promise.all([
|
|
1718
|
+
setupFile(workspace, "state"),
|
|
1719
|
+
setupFile(workspace, "exit-code"),
|
|
1720
|
+
setupFile(workspace, "started-at"),
|
|
1721
|
+
setupFile(workspace, "finished-at"),
|
|
1722
|
+
setupFile(workspace, "setup.log")
|
|
1723
|
+
]);
|
|
1724
|
+
const status = ["waiting", "running", "succeeded", "failed"].includes(state ?? "") ? state : "waiting";
|
|
1725
|
+
return {
|
|
1726
|
+
status,
|
|
1727
|
+
exitCode: exitCode != null && Number.isInteger(Number(exitCode)) ? Number(exitCode) : null,
|
|
1728
|
+
startedAt,
|
|
1729
|
+
finishedAt,
|
|
1730
|
+
log: log?.slice(-50000) ?? null
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1540
1733
|
return {
|
|
1541
1734
|
provider: { name: provider.name, capabilities: provider.capabilities },
|
|
1542
1735
|
auth: {
|
|
@@ -1654,17 +1847,30 @@ function createDirectGittermClient(options) {
|
|
|
1654
1847
|
}
|
|
1655
1848
|
const id = input.id ?? randomUUID();
|
|
1656
1849
|
const password = randomUUID();
|
|
1850
|
+
validateEnvironmentVariables(input.environmentVariables ?? {});
|
|
1657
1851
|
const provisioning = buildDirectProvisioningPlan({ ...input, id, lifecycle, password });
|
|
1658
1852
|
const created = await provider.create({ ...input, id, lifecycle, password, provisioning });
|
|
1659
|
-
|
|
1853
|
+
const workspace = {
|
|
1660
1854
|
id,
|
|
1661
1855
|
provider: provider.name,
|
|
1662
1856
|
externalId: created.externalId,
|
|
1663
1857
|
status: "running",
|
|
1664
1858
|
lifecycle,
|
|
1665
1859
|
runtime: created.runtime,
|
|
1860
|
+
setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
|
|
1666
1861
|
createdAt: new Date().toISOString()
|
|
1667
1862
|
};
|
|
1863
|
+
if (provisioning.setup.afterAgent.length) {
|
|
1864
|
+
try {
|
|
1865
|
+
await startPty(workspace, setupRunner(provisioning.setup.afterAgent), "Gitterm setup");
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
await provider.terminate(workspace).catch(() => {
|
|
1868
|
+
return;
|
|
1869
|
+
});
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return workspace;
|
|
1668
1874
|
},
|
|
1669
1875
|
async status(workspace) {
|
|
1670
1876
|
assertWorkspace(workspace);
|
|
@@ -1701,6 +1907,24 @@ function createDirectGittermClient(options) {
|
|
|
1701
1907
|
if (!provider.keepAlive)
|
|
1702
1908
|
throw new Error(`${provider.name} does not support keep-alive`);
|
|
1703
1909
|
await provider.keepAlive(workspace, timeoutMs);
|
|
1910
|
+
},
|
|
1911
|
+
setupStatus: getSetupStatus,
|
|
1912
|
+
async waitForSetup(workspace, wait = {}) {
|
|
1913
|
+
const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
|
|
1914
|
+
const deadline = Date.now() + timeoutMs;
|
|
1915
|
+
while (true) {
|
|
1916
|
+
const status = await getSetupStatus(workspace);
|
|
1917
|
+
if (status.status === "not_requested" || status.status === "succeeded")
|
|
1918
|
+
return status;
|
|
1919
|
+
if (status.status === "failed") {
|
|
1920
|
+
throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
|
|
1921
|
+
${status.log}` : ""}`);
|
|
1922
|
+
}
|
|
1923
|
+
if (Date.now() >= deadline) {
|
|
1924
|
+
throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
|
|
1925
|
+
}
|
|
1926
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1927
|
+
}
|
|
1704
1928
|
}
|
|
1705
1929
|
},
|
|
1706
1930
|
runs: {
|
|
@@ -1792,11 +2016,11 @@ function createDirectGittermClient(options) {
|
|
|
1792
2016
|
};
|
|
1793
2017
|
}
|
|
1794
2018
|
export {
|
|
1795
|
-
|
|
1796
|
-
createRailwayDirectProvider,
|
|
1797
|
-
createExeDevDirectProvider,
|
|
1798
|
-
createE2BDirectProvider,
|
|
1799
|
-
createDirectGittermClient,
|
|
2019
|
+
createAsciiDirectProvider,
|
|
1800
2020
|
createDaytonaDirectProvider,
|
|
1801
|
-
|
|
2021
|
+
createDirectGittermClient,
|
|
2022
|
+
createE2BDirectProvider,
|
|
2023
|
+
createExeDevDirectProvider,
|
|
2024
|
+
createRailwayDirectProvider,
|
|
2025
|
+
createVercelDirectProvider
|
|
1802
2026
|
};
|
|
@@ -6,6 +6,7 @@ export declare const DIRECT_E2B_TEMPLATES: {
|
|
|
6
6
|
readonly standard: "gitterm-opencode-server";
|
|
7
7
|
readonly large: "gitterm-opencode-server-lg";
|
|
8
8
|
};
|
|
9
|
+
export declare const DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.";
|
|
9
10
|
export declare function resolveDirectImage(image?: string): string;
|
|
10
11
|
export declare function directModelAuth(credential: DirectModelCredential): {
|
|
11
12
|
enterpriseUrl?: string | undefined;
|
|
@@ -19,6 +20,9 @@ export declare function directModelAuth(credential: DirectModelCredential): {
|
|
|
19
20
|
type: "api";
|
|
20
21
|
key: string;
|
|
21
22
|
};
|
|
23
|
+
export declare function validateDirectFilePath(path: string): string;
|
|
24
|
+
export declare function validateDirectFileMode(mode?: number): number;
|
|
25
|
+
export declare function buildDirectGittermInstructions(additional?: string): string;
|
|
22
26
|
export declare function repositoryName(url: string): string;
|
|
23
27
|
export declare function buildDirectProvisioningPlan(input: DirectWorkspaceCreateInput & {
|
|
24
28
|
id: string;
|
|
@@ -28,6 +32,7 @@ export declare function buildDirectProvisioningPlan(input: DirectWorkspaceCreate
|
|
|
28
32
|
export declare function railwayContainerEnvironment(plan: DirectProvisioningPlan): Record<string, string>;
|
|
29
33
|
export declare function setupCommandScript(commands: string[]): string;
|
|
30
34
|
export declare function shellQuote(value: string): string;
|
|
35
|
+
export declare function shellPath(path: string): string;
|
|
31
36
|
export declare function cloneRepositoryScript(repository: NonNullable<DirectProvisioningPlan["repository"]>, directory: string): string;
|
|
32
37
|
export declare function pinFloatingDockerImage(image: string): Promise<string>;
|
|
33
38
|
export declare function basicAuthHeader(password: string): string;
|
package/dist/direct/types.d.ts
CHANGED
|
@@ -108,6 +108,17 @@ export type DirectAuthWaitOptions = {
|
|
|
108
108
|
timeoutMs?: number;
|
|
109
109
|
pollIntervalMs?: number;
|
|
110
110
|
};
|
|
111
|
+
export type DirectSecretFile = {
|
|
112
|
+
/** Absolute path or a path below the workspace user's home (`~/...`). */
|
|
113
|
+
path: string;
|
|
114
|
+
content: string;
|
|
115
|
+
/** Unix permission bits. Defaults to owner read/write (0600). */
|
|
116
|
+
mode?: number;
|
|
117
|
+
};
|
|
118
|
+
export type DirectWorkspaceSetup = {
|
|
119
|
+
beforeAgent?: string[];
|
|
120
|
+
afterAgent?: string[];
|
|
121
|
+
};
|
|
111
122
|
export type DirectWorkspaceCreateInput = {
|
|
112
123
|
id?: string;
|
|
113
124
|
repo?: string;
|
|
@@ -121,7 +132,14 @@ export type DirectWorkspaceCreateInput = {
|
|
|
121
132
|
lifecycle?: DirectWorkspaceLifecycle;
|
|
122
133
|
environmentVariables?: Record<string, string>;
|
|
123
134
|
modelCredentials?: DirectModelCredential[];
|
|
124
|
-
|
|
135
|
+
setup?: DirectWorkspaceSetup;
|
|
136
|
+
secretFiles?: DirectSecretFile[];
|
|
137
|
+
/** Provider-specific attachment settings. */
|
|
138
|
+
exedev?: {
|
|
139
|
+
existingVmName: string;
|
|
140
|
+
};
|
|
141
|
+
/** Trusted integration context appended to the generated global AGENTS.md. */
|
|
142
|
+
additionalAgentInstructions?: string;
|
|
125
143
|
opencode?: {
|
|
126
144
|
config?: Record<string, unknown>;
|
|
127
145
|
plugins?: string[];
|
|
@@ -144,8 +162,20 @@ export type DirectWorkspace = {
|
|
|
144
162
|
status: DirectWorkspaceStatus;
|
|
145
163
|
lifecycle: DirectWorkspaceLifecycle;
|
|
146
164
|
runtime: DirectWorkspaceRuntime;
|
|
165
|
+
setup: "not_requested" | "before_agent_complete" | "after_agent";
|
|
147
166
|
createdAt: string;
|
|
148
167
|
};
|
|
168
|
+
export type DirectWorkspaceSetupStatus = {
|
|
169
|
+
status: "not_requested" | "waiting" | "running" | "succeeded" | "failed";
|
|
170
|
+
exitCode: number | null;
|
|
171
|
+
startedAt: string | null;
|
|
172
|
+
finishedAt: string | null;
|
|
173
|
+
log: string | null;
|
|
174
|
+
};
|
|
175
|
+
export type DirectWorkspaceSetupWaitOptions = {
|
|
176
|
+
timeoutMs?: number;
|
|
177
|
+
pollIntervalMs?: number;
|
|
178
|
+
};
|
|
149
179
|
export type DirectProviderWorkspaceInput = DirectWorkspaceCreateInput & {
|
|
150
180
|
id: string;
|
|
151
181
|
lifecycle: DirectWorkspaceLifecycle;
|
|
@@ -155,6 +185,7 @@ export type DirectProviderWorkspaceInput = DirectWorkspaceCreateInput & {
|
|
|
155
185
|
export type DirectAgentFile = {
|
|
156
186
|
path: string;
|
|
157
187
|
contentBase64: string;
|
|
188
|
+
mode?: number;
|
|
158
189
|
};
|
|
159
190
|
export type DirectProvisioningPlan = {
|
|
160
191
|
workspaceId: string;
|
|
@@ -174,7 +205,10 @@ export type DirectProvisioningPlan = {
|
|
|
174
205
|
command: string;
|
|
175
206
|
port: number;
|
|
176
207
|
};
|
|
177
|
-
|
|
208
|
+
setup: {
|
|
209
|
+
beforeAgent: string[];
|
|
210
|
+
afterAgent: string[];
|
|
211
|
+
};
|
|
178
212
|
};
|
|
179
213
|
export interface DirectProviderAdapter {
|
|
180
214
|
readonly name: string;
|
package/dist/index.js
CHANGED
|
@@ -236,7 +236,7 @@ async function runWithServer(serverUrl, operation) {
|
|
|
236
236
|
cause: error
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
|
-
throw new GittermError(code, code === "UNAUTHORIZED" ?
|
|
239
|
+
throw new GittermError(code, code === "UNAUTHORIZED" ? `Authentication failed: ${error.message}. Check that the API token is valid and has not expired.` : error.message, { cause: error });
|
|
240
240
|
}
|
|
241
241
|
throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
|
|
242
242
|
}
|
|
@@ -525,16 +525,16 @@ function createGittermWorkspaceClient(options = {}) {
|
|
|
525
525
|
};
|
|
526
526
|
}
|
|
527
527
|
export {
|
|
528
|
-
|
|
529
|
-
loginWithDeviceCode,
|
|
530
|
-
loadConfigSync,
|
|
531
|
-
loadConfig,
|
|
532
|
-
getWorkspaceEnvironment,
|
|
533
|
-
getConfigPath,
|
|
534
|
-
deleteConfig,
|
|
535
|
-
createGittermWorkspaceClient,
|
|
536
|
-
createGittermClient,
|
|
537
|
-
WorkspaceLifecycleError,
|
|
528
|
+
DEFAULT_GITTERM_SERVER_URL,
|
|
538
529
|
GittermError,
|
|
539
|
-
|
|
530
|
+
WorkspaceLifecycleError,
|
|
531
|
+
createGittermClient,
|
|
532
|
+
createGittermWorkspaceClient,
|
|
533
|
+
deleteConfig,
|
|
534
|
+
getConfigPath,
|
|
535
|
+
getWorkspaceEnvironment,
|
|
536
|
+
loadConfig,
|
|
537
|
+
loadConfigSync,
|
|
538
|
+
loginWithDeviceCode,
|
|
539
|
+
saveConfig
|
|
540
540
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -167,6 +167,11 @@ export type WorkspaceCreateInput = {
|
|
|
167
167
|
agent?: AgentKey;
|
|
168
168
|
/** Provider intent. Defaults to the user's or deployment's preferred provider. */
|
|
169
169
|
provider?: WorkspaceProviderSelection;
|
|
170
|
+
/** Inline Git credentials for repository validation, cloning, and runtime pull/push. */
|
|
171
|
+
repositoryCredentials?: {
|
|
172
|
+
username?: string;
|
|
173
|
+
token: string;
|
|
174
|
+
};
|
|
170
175
|
gitIntegrationId?: string;
|
|
171
176
|
/** Defaults from the selected provider. */
|
|
172
177
|
persistent?: boolean;
|
|
@@ -183,11 +188,22 @@ export type WorkspaceCreateInput = {
|
|
|
183
188
|
/** Ephemeral environment variables injected into this workspace only. */
|
|
184
189
|
environmentVariables?: Record<string, string>;
|
|
185
190
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* and
|
|
191
|
+
* Setup phases run in order. `beforeAgent` blocks agent startup and fails
|
|
192
|
+
* create() when it exits non-zero; `afterAgent` starts after the agent is
|
|
193
|
+
* reachable and is observable with setupStatus()/waitForSetup().
|
|
189
194
|
*/
|
|
190
|
-
|
|
195
|
+
setup?: {
|
|
196
|
+
beforeAgent?: string[];
|
|
197
|
+
afterAgent?: string[];
|
|
198
|
+
};
|
|
199
|
+
/** Secret files written relative to the repository and excluded from git. Rotate by recreating the workspace. */
|
|
200
|
+
secretFiles?: Array<{
|
|
201
|
+
path: string;
|
|
202
|
+
content: string;
|
|
203
|
+
mode?: "0400" | "0600";
|
|
204
|
+
}>;
|
|
205
|
+
/** Trusted integration context appended to the workspace's global AGENTS.md. */
|
|
206
|
+
additionalAgentInstructions?: string;
|
|
191
207
|
/** OpenCode capabilities materialized only in this workspace. */
|
|
192
208
|
opencode?: {
|
|
193
209
|
skills?: Array<{
|