@gitterm/sdk 0.0.3 → 0.0.4
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 +105 -9
- package/dist/client.d.ts +19 -3
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +149 -18
- package/dist/transport.d.ts +2 -0
- package/dist/types.d.ts +184 -20
- package/dist/workspace-client.d.ts +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,13 +72,27 @@ self-hosted.
|
|
|
72
72
|
import { createGittermClient } from "@gitterm/sdk";
|
|
73
73
|
|
|
74
74
|
const client = createGittermClient({
|
|
75
|
-
serverUrl: "https://api.gitterm.dev",
|
|
76
75
|
token: process.env.GITTERM_API_TOKEN,
|
|
77
76
|
});
|
|
78
77
|
|
|
79
78
|
const { workspaces } = await client.workspaces.list();
|
|
80
79
|
```
|
|
81
80
|
|
|
81
|
+
The SDK deliberately exposes two clients. `createGittermClient()` uses a user API token and
|
|
82
|
+
can manage the user's workspaces. `createGittermWorkspaceClient()` uses the scoped identity
|
|
83
|
+
injected into a GitTerm workspace and can inspect only that workspace and its ports:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createGittermWorkspaceClient } from "@gitterm/sdk";
|
|
87
|
+
|
|
88
|
+
const workspace = createGittermWorkspaceClient();
|
|
89
|
+
const self = await workspace.self.get();
|
|
90
|
+
const preview = await workspace.ports.open(3000, { name: "app" });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The workspace client never reads the CLI's saved account login and has no create, list,
|
|
94
|
+
pause, restart, or terminate operations.
|
|
95
|
+
|
|
82
96
|
### With the CLI's saved login
|
|
83
97
|
|
|
84
98
|
```ts
|
|
@@ -98,18 +112,100 @@ client.workspaces.ensureRunning(workspaceId, options?);
|
|
|
98
112
|
client.workspaces.pause(workspaceId);
|
|
99
113
|
client.workspaces.restart(workspaceId);
|
|
100
114
|
client.workspaces.terminate(workspaceId);
|
|
101
|
-
client.workspaces.
|
|
102
|
-
|
|
103
|
-
agent, provider, persistent, // provider is optional
|
|
115
|
+
client.workspaces.create({
|
|
116
|
+
repo: "https://github.com/acme/product",
|
|
104
117
|
});
|
|
105
118
|
client.catalog.agentTypes();
|
|
106
119
|
client.catalog.cloudProviders();
|
|
107
|
-
client.catalog.
|
|
120
|
+
client.catalog.workspaceOptions();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The server defaults the agent to `opencode`, selects the user's preferred provider,
|
|
124
|
+
uses that provider's default machine profile, and applies the provider's persistence policy.
|
|
125
|
+
Override only the placement decisions your integration cares about:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
await client.workspaces.create({
|
|
129
|
+
repo: "https://github.com/acme/product",
|
|
130
|
+
agent: "opencode",
|
|
131
|
+
setupCommands: ["npm install", "npm run generate"],
|
|
132
|
+
opencode: {
|
|
133
|
+
skills: [
|
|
134
|
+
{
|
|
135
|
+
name: "release-demo",
|
|
136
|
+
content: `---
|
|
137
|
+
name: release-demo
|
|
138
|
+
description: Record and publish a product release demo.
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
Follow the repository's release-demo workflow.`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
plugins: ["@acme/opencode-browser@1.2.3"],
|
|
145
|
+
},
|
|
146
|
+
provider: {
|
|
147
|
+
type: "exedev",
|
|
148
|
+
machine: { type: "profile", key: "content-rendering" },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Setup commands run in order from the checked-out repository after the agent server is
|
|
154
|
+
ready. They do not delay workspace creation or stop the agent if they fail. Provider and
|
|
155
|
+
agent defaults configured by an administrator run first. Use
|
|
156
|
+
`client.workspaces.setupStatus(workspaceId)` or `waitForSetup(workspaceId)` to inspect them.
|
|
157
|
+
GitTerm persists the reported state and bounded log; a recovery copy also lives in the
|
|
158
|
+
repository's git-excluded `.gitterm/setup/` directory.
|
|
159
|
+
|
|
160
|
+
`provider` is a discriminated union, so TypeScript only offers `region` for providers
|
|
161
|
+
where GitTerm supports caller-selected placement. Machine keys are configured by admins
|
|
162
|
+
and returned by `client.catalog.workspaceOptions()`; raw CPU, memory, credentials, and
|
|
163
|
+
provider account configuration are never supplied by SDK callers.
|
|
164
|
+
|
|
165
|
+
This makes release automation a normal workspace task: create an OpenCode workspace,
|
|
166
|
+
run UI review or browser capture tools in the sandbox, upload the resulting media, update
|
|
167
|
+
the changelog in the checked-out repository, then terminate the workspace. Use an
|
|
168
|
+
`idempotencyKey` based on the release SHA when the workflow may be retried.
|
|
169
|
+
|
|
170
|
+
### Agent runs
|
|
171
|
+
|
|
172
|
+
Runs use durable GitTerm IDs backed by the workspace's native OpenCode session. Reusing an
|
|
173
|
+
idempotency key with the same input returns the original run, and terminal results remain
|
|
174
|
+
available after the workspace is paused. Completion means the native session became idle;
|
|
175
|
+
it does not claim that a pull request, upload, or other product outcome succeeded.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const { workspace } = await client.workspaces.create({
|
|
179
|
+
repo: "https://github.com/acme/product",
|
|
180
|
+
setupCommands: ["npm install", "npm run db:seed"],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const run = await client.runs.create({
|
|
184
|
+
workspaceId: workspace.id,
|
|
185
|
+
idempotencyKey: "onboarding-v2",
|
|
186
|
+
waitForSetup: true,
|
|
187
|
+
prompt: "Record the new onboarding flow and open a pull request adding it to the changelog.",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const completed = await client.runs.wait(workspace.id, run.id);
|
|
191
|
+
const messages = await client.runs.messages(workspace.id, run.id);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Runs are isolated by default and can execute in parallel. To preserve conversational context,
|
|
195
|
+
continue a terminal run; continued runs sharing context must remain sequential:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const next = await client.runs.create({
|
|
199
|
+
workspaceId: workspace.id,
|
|
200
|
+
idempotencyKey: "onboarding-tests-v1",
|
|
201
|
+
prompt: "Now add tests for that change.",
|
|
202
|
+
context: { type: "continue", runId: completed.id },
|
|
203
|
+
});
|
|
108
204
|
```
|
|
109
205
|
|
|
110
|
-
`
|
|
111
|
-
|
|
112
|
-
|
|
206
|
+
Use `client.runs.cancel(workspaceId, runId)` to abort the current run. GitTerm keeps the
|
|
207
|
+
underlying OpenCode session private. For native session control, use
|
|
208
|
+
`workspaces.getRuntimeAccess()` and connect with the official OpenCode SDK.
|
|
113
209
|
|
|
114
210
|
### Errors
|
|
115
211
|
|
|
@@ -130,7 +226,7 @@ try {
|
|
|
130
226
|
Workspace lifecycle failures are also exposed as `WorkspaceLifecycleError`, with stable
|
|
131
227
|
`WORKSPACE_TERMINATED`, `WORKSPACE_NON_RECOVERABLE`, `WORKSPACE_START_TIMEOUT`, and
|
|
132
228
|
`WORKSPACE_RESTART_FAILED` codes. General codes are
|
|
133
|
-
`NOT_LOGGED_IN`, `UNAUTHORIZED`, `NOT_FOUND`, `FORBIDDEN`, `BAD_REQUEST`,
|
|
229
|
+
`NOT_LOGGED_IN`, `UNAUTHORIZED`, `NOT_FOUND`, `FORBIDDEN`, `BAD_REQUEST`, `CONFLICT`,
|
|
134
230
|
`SERVER_ERROR`, and `NETWORK`.
|
|
135
231
|
|
|
136
232
|
The package ships self-contained declarations from `dist`; TypeScript consumers do not
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentType, AuthStatus, CloudProvider, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult,
|
|
1
|
+
import type { AgentType, AgentRun, AgentRunCreateInput, AgentRunMessage, AuthStatus, CloudProvider, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceCatalog, WorkspaceSetupStatus } from "./types.js";
|
|
2
2
|
export type GittermClientOptions = {
|
|
3
3
|
serverUrl?: string;
|
|
4
4
|
token?: string;
|
|
@@ -22,7 +22,23 @@ export type GittermClient = {
|
|
|
22
22
|
restart(workspaceId: string): Promise<WorkspaceRestartResult>;
|
|
23
23
|
terminate(workspaceId: string): Promise<WorkspaceTerminateResult>;
|
|
24
24
|
create(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
|
|
25
|
-
|
|
25
|
+
setupStatus(workspaceId: string): Promise<WorkspaceSetupStatus>;
|
|
26
|
+
waitForSetup(workspaceId: string, options?: {
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
}): Promise<WorkspaceSetupStatus>;
|
|
30
|
+
};
|
|
31
|
+
runs: {
|
|
32
|
+
create(input: AgentRunCreateInput): Promise<AgentRun>;
|
|
33
|
+
get(workspaceId: string, runId: string): Promise<AgentRun>;
|
|
34
|
+
messages(workspaceId: string, runId: string): Promise<AgentRunMessage[]>;
|
|
35
|
+
cancel(workspaceId: string, runId: string): Promise<{
|
|
36
|
+
cancelled: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
wait(workspaceId: string, runId: string, options?: {
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
pollIntervalMs?: number;
|
|
41
|
+
}): Promise<AgentRun>;
|
|
26
42
|
};
|
|
27
43
|
catalog: {
|
|
28
44
|
agentTypes(input?: {
|
|
@@ -34,7 +50,7 @@ export type GittermClient = {
|
|
|
34
50
|
sandboxOnly?: boolean;
|
|
35
51
|
nonSandboxOnly?: boolean;
|
|
36
52
|
}): Promise<CloudProvider[]>;
|
|
37
|
-
|
|
53
|
+
workspaceOptions(): Promise<WorkspaceCatalog>;
|
|
38
54
|
};
|
|
39
55
|
};
|
|
40
56
|
export declare function createGittermClient(options?: GittermClientOptions): GittermClient;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type GittermErrorCode = "NOT_LOGGED_IN" | "UNAUTHORIZED" | "NOT_FOUND" | "FORBIDDEN" | "BAD_REQUEST" | "SERVER_ERROR" | "NETWORK" | WorkspaceLifecycleErrorCode;
|
|
1
|
+
export type GittermErrorCode = "NOT_LOGGED_IN" | "UNAUTHORIZED" | "NOT_FOUND" | "FORBIDDEN" | "BAD_REQUEST" | "CONFLICT" | "SERVER_ERROR" | "NETWORK" | WorkspaceLifecycleErrorCode;
|
|
2
2
|
export type WorkspaceLifecycleErrorCode = "WORKSPACE_TERMINATED" | "WORKSPACE_NON_RECOVERABLE" | "WORKSPACE_START_TIMEOUT" | "WORKSPACE_RESTART_FAILED";
|
|
3
3
|
export declare class GittermError extends Error {
|
|
4
4
|
readonly code: GittermErrorCode;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export type { CliConfig } from "./config.js";
|
|
|
4
4
|
export { loginWithDeviceCode } from "./device-login.js";
|
|
5
5
|
export type { DeviceCodeInfo, LoginWithDeviceCodeOptions } from "./device-login.js";
|
|
6
6
|
export { GittermError, WorkspaceLifecycleError } from "./errors.js";
|
|
7
|
+
export { createGittermWorkspaceClient, getWorkspaceEnvironment, type GittermWorkspaceClient, type WorkspaceClientOptions, type WorkspaceEnvironment, type WorkspacePort, type WorkspaceSelf, } from "./workspace-client.js";
|
|
7
8
|
export type { GittermErrorCode, WorkspaceLifecycleErrorCode } from "./errors.js";
|
|
8
|
-
export type { AgentType, AuthStatus, CloudProvider,
|
|
9
|
+
export type { AgentRun, AgentRunCreateInput, AgentRunMessage, AgentRunStatus, AgentType, AgentKey, AuthStatus, BuiltInAgentKey, CloudProvider, ProviderKey, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceCatalog, WorkspaceEnsureRunningResult, WorkspaceHostingType, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspaceStatus, WorkspaceSetupStatus, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceProviderSelection, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,36 @@ class WorkspaceLifecycleError extends GittermError {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// src/transport.ts
|
|
73
|
+
var LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
74
|
+
function normalizeServerUrl(value) {
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = new URL(value);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(`Invalid GitTerm server URL: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
if (url.username || url.password) {
|
|
82
|
+
throw new Error("GitTerm server URL must not contain credentials");
|
|
83
|
+
}
|
|
84
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) {
|
|
85
|
+
throw new Error("GitTerm server URL must use HTTPS (HTTP is allowed only for loopback)");
|
|
86
|
+
}
|
|
87
|
+
url.hash = "";
|
|
88
|
+
url.search = "";
|
|
89
|
+
return url.toString().replace(/\/$/, "");
|
|
90
|
+
}
|
|
91
|
+
function createNoRedirectFetch(fetchImpl = fetch) {
|
|
92
|
+
return async (input, init) => {
|
|
93
|
+
const response = await fetchImpl(input, { ...init, redirect: "manual" });
|
|
94
|
+
if (response.status >= 300 && response.status < 400) {
|
|
95
|
+
const location = response.headers.get("location");
|
|
96
|
+
throw new Error(location ? `GitTerm server redirects are not allowed: ${location}` : "GitTerm server redirects are not allowed");
|
|
97
|
+
}
|
|
98
|
+
return response;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
// src/client.ts
|
|
73
103
|
function envValue(name) {
|
|
74
104
|
const value = typeof process !== "undefined" ? process.env[name] : undefined;
|
|
@@ -76,12 +106,12 @@ function envValue(name) {
|
|
|
76
106
|
}
|
|
77
107
|
function resolveCredentials(options) {
|
|
78
108
|
const config = !options.serverUrl || !options.token ? loadConfigSync(options.configPath) : null;
|
|
79
|
-
const serverUrl = options.serverUrl ?? envValue("GITTERM_SERVER_URL") ?? config?.serverUrl;
|
|
109
|
+
const serverUrl = options.serverUrl ?? envValue("GITTERM_SERVER_URL") ?? config?.serverUrl ?? DEFAULT_GITTERM_SERVER_URL;
|
|
80
110
|
const token = options.token ?? envValue("GITTERM_API_TOKEN") ?? config?.token;
|
|
81
111
|
if (!serverUrl || !token) {
|
|
82
112
|
throw new GittermError("NOT_LOGGED_IN", "Not logged in. Run: gitterm login");
|
|
83
113
|
}
|
|
84
|
-
return { serverUrl, token };
|
|
114
|
+
return { serverUrl: normalizeServerUrl(serverUrl), token };
|
|
85
115
|
}
|
|
86
116
|
function toTrpcUrl(serverUrl) {
|
|
87
117
|
return new URL("/trpc", serverUrl).toString();
|
|
@@ -161,6 +191,8 @@ function mapTrpcCode(code) {
|
|
|
161
191
|
return "FORBIDDEN";
|
|
162
192
|
case "BAD_REQUEST":
|
|
163
193
|
return "BAD_REQUEST";
|
|
194
|
+
case "CONFLICT":
|
|
195
|
+
return "CONFLICT";
|
|
164
196
|
default:
|
|
165
197
|
return "SERVER_ERROR";
|
|
166
198
|
}
|
|
@@ -208,22 +240,13 @@ function createGittermClient(options = {}) {
|
|
|
208
240
|
links: [
|
|
209
241
|
httpBatchLink({
|
|
210
242
|
url: toTrpcUrl(credentials.serverUrl),
|
|
211
|
-
fetch: options.fetch,
|
|
243
|
+
fetch: createNoRedirectFetch(options.fetch),
|
|
212
244
|
headers: () => ({ authorization: `Bearer ${credentials.token}` })
|
|
213
245
|
})
|
|
214
246
|
]
|
|
215
247
|
});
|
|
216
248
|
const run = (operation) => runWithServer(credentials.serverUrl, operation);
|
|
217
|
-
const
|
|
218
|
-
const { agent, provider, regionId, ...workspaceInput } = input;
|
|
219
|
-
const defaults = await trpc.workspace.resolveSandboxDefaults.query({ agent, provider });
|
|
220
|
-
const apiInput = {
|
|
221
|
-
...workspaceInput,
|
|
222
|
-
agentTypeId: defaults.agentTypeId,
|
|
223
|
-
cloudProviderId: defaults.cloudProviderId,
|
|
224
|
-
regionId: regionId ?? defaults.regionId
|
|
225
|
-
};
|
|
226
|
-
const result = await trpc.workspace.createWorkspace.mutate(apiInput);
|
|
249
|
+
const normalizeCreateResult = (result) => {
|
|
227
250
|
const workspace = normalizeWorkspace(result.workspace);
|
|
228
251
|
if (!workspace)
|
|
229
252
|
throw new GittermError("SERVER_ERROR", "Workspace creation failed");
|
|
@@ -241,7 +264,30 @@ function createGittermClient(options = {}) {
|
|
|
241
264
|
providerKey: null
|
|
242
265
|
};
|
|
243
266
|
return { workspace, runtime };
|
|
267
|
+
};
|
|
268
|
+
const createWorkspace = (input) => run(async () => {
|
|
269
|
+
const result = await trpc.workspace.createWorkspace.mutate(input);
|
|
270
|
+
return normalizeCreateResult(result);
|
|
244
271
|
});
|
|
272
|
+
const waitForWorkspaceSetup = async (workspaceId, waitOptions) => {
|
|
273
|
+
const timeoutMs = waitOptions?.timeoutMs ?? 10 * 60000;
|
|
274
|
+
const pollIntervalMs = waitOptions?.pollIntervalMs ?? 2000;
|
|
275
|
+
const deadline = Date.now() + timeoutMs;
|
|
276
|
+
while (true) {
|
|
277
|
+
const result = await trpc.workspace.getSetupStatus.query({ workspaceId });
|
|
278
|
+
if (result.status === "not_requested" || result.status === "succeeded")
|
|
279
|
+
return result;
|
|
280
|
+
if (result.status === "failed") {
|
|
281
|
+
const log = result.log?.trim();
|
|
282
|
+
throw new GittermError("BAD_REQUEST", `Workspace setup failed${result.exitCode === null ? "" : ` with exit code ${result.exitCode}`}${log ? `
|
|
283
|
+
${log}` : ""}`);
|
|
284
|
+
}
|
|
285
|
+
if (Date.now() >= deadline) {
|
|
286
|
+
throw new GittermError("NETWORK", `Timed out waiting for workspace ${workspaceId} setup`);
|
|
287
|
+
}
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
245
291
|
return {
|
|
246
292
|
serverUrl: credentials.serverUrl,
|
|
247
293
|
auth: {
|
|
@@ -306,7 +352,28 @@ function createGittermClient(options = {}) {
|
|
|
306
352
|
};
|
|
307
353
|
}),
|
|
308
354
|
create: createWorkspace,
|
|
309
|
-
|
|
355
|
+
setupStatus: (workspaceId) => run(async () => trpc.workspace.getSetupStatus.query({ workspaceId })),
|
|
356
|
+
waitForSetup: (workspaceId, waitOptions) => run(() => waitForWorkspaceSetup(workspaceId, waitOptions))
|
|
357
|
+
},
|
|
358
|
+
runs: {
|
|
359
|
+
create: (input) => run(async () => trpc.run.create.mutate(input)),
|
|
360
|
+
get: (workspaceId, runId) => run(async () => trpc.run.get.query({ workspaceId, runId })),
|
|
361
|
+
messages: (workspaceId, runId) => run(async () => trpc.run.messages.query({ workspaceId, runId })),
|
|
362
|
+
cancel: (workspaceId, runId) => run(async () => trpc.run.cancel.mutate({ workspaceId, runId })),
|
|
363
|
+
wait: (workspaceId, runId, waitOptions) => run(async () => {
|
|
364
|
+
const timeoutMs = waitOptions?.timeoutMs ?? 30 * 60000;
|
|
365
|
+
const pollIntervalMs = waitOptions?.pollIntervalMs ?? 2000;
|
|
366
|
+
const deadline = Date.now() + timeoutMs;
|
|
367
|
+
while (true) {
|
|
368
|
+
const result = await trpc.run.get.query({ workspaceId, runId });
|
|
369
|
+
if (result.status !== "pending" && result.status !== "running" && result.status !== "retrying")
|
|
370
|
+
return result;
|
|
371
|
+
if (Date.now() >= deadline) {
|
|
372
|
+
throw new GittermError("NETWORK", `Timed out waiting for run ${runId}`);
|
|
373
|
+
}
|
|
374
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
375
|
+
}
|
|
376
|
+
})
|
|
310
377
|
},
|
|
311
378
|
catalog: {
|
|
312
379
|
agentTypes: (input) => run(async () => {
|
|
@@ -317,7 +384,7 @@ function createGittermClient(options = {}) {
|
|
|
317
384
|
const result = await trpc.workspace.listCloudProviders.query(input);
|
|
318
385
|
return result.cloudProviders;
|
|
319
386
|
}),
|
|
320
|
-
|
|
387
|
+
workspaceOptions: () => run(async () => trpc.workspace.getWorkspaceCatalog.query())
|
|
321
388
|
}
|
|
322
389
|
};
|
|
323
390
|
}
|
|
@@ -326,8 +393,9 @@ function sleep(ms) {
|
|
|
326
393
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
327
394
|
}
|
|
328
395
|
async function loginWithDeviceCode(serverUrl, options = {}) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
396
|
+
const normalizedServerUrl = normalizeServerUrl(serverUrl);
|
|
397
|
+
const fetchImpl = createNoRedirectFetch(options.fetch);
|
|
398
|
+
const codeRes = await fetchImpl(new URL("/api/device/code", normalizedServerUrl), {
|
|
331
399
|
method: "POST",
|
|
332
400
|
headers: { "content-type": "application/json" },
|
|
333
401
|
body: JSON.stringify({ clientName: options.clientName ?? "gitterm" })
|
|
@@ -343,7 +411,7 @@ async function loginWithDeviceCode(serverUrl, options = {}) {
|
|
|
343
411
|
});
|
|
344
412
|
const deadline = Date.now() + codeJson.expiresInSeconds * 1000;
|
|
345
413
|
while (Date.now() < deadline) {
|
|
346
|
-
const tokenRes = await fetchImpl(new URL("/api/device/token",
|
|
414
|
+
const tokenRes = await fetchImpl(new URL("/api/device/token", normalizedServerUrl), {
|
|
347
415
|
method: "POST",
|
|
348
416
|
headers: { "content-type": "application/json" },
|
|
349
417
|
body: JSON.stringify({ deviceCode: codeJson.deviceCode })
|
|
@@ -360,13 +428,76 @@ async function loginWithDeviceCode(serverUrl, options = {}) {
|
|
|
360
428
|
}
|
|
361
429
|
throw new Error("Device code expired; try again.");
|
|
362
430
|
}
|
|
431
|
+
// src/workspace-client.ts
|
|
432
|
+
import { TRPCClientError as TRPCClientError2, createTRPCClient as createTRPCClient2, httpBatchLink as httpBatchLink2 } from "@trpc/client";
|
|
433
|
+
function getWorkspaceEnvironment(environment) {
|
|
434
|
+
environment ??= typeof process === "undefined" ? {} : process.env;
|
|
435
|
+
const serverUrl = environment.WORKSPACE_API_URL;
|
|
436
|
+
const token = environment.WORKSPACE_AUTH_TOKEN;
|
|
437
|
+
const workspaceId = environment.WORKSPACE_ID;
|
|
438
|
+
const workspaceEnvironmentPresent = Boolean(serverUrl || token || workspaceId);
|
|
439
|
+
if (!workspaceEnvironmentPresent)
|
|
440
|
+
return null;
|
|
441
|
+
if (!serverUrl || !token || !workspaceId) {
|
|
442
|
+
throw new GittermError("UNAUTHORIZED", "Incomplete GitTerm workspace environment: WORKSPACE_API_URL, WORKSPACE_AUTH_TOKEN, and WORKSPACE_ID are required");
|
|
443
|
+
}
|
|
444
|
+
return { serverUrl: normalizeServerUrl(serverUrl), token, workspaceId };
|
|
445
|
+
}
|
|
446
|
+
function errorCode(code) {
|
|
447
|
+
if (code === "UNAUTHORIZED" || code === "NOT_FOUND" || code === "FORBIDDEN")
|
|
448
|
+
return code;
|
|
449
|
+
if (code === "BAD_REQUEST")
|
|
450
|
+
return code;
|
|
451
|
+
return "SERVER_ERROR";
|
|
452
|
+
}
|
|
453
|
+
function createGittermWorkspaceClient(options = {}) {
|
|
454
|
+
const detected = options.serverUrl && options.token && options.workspaceId ? null : getWorkspaceEnvironment();
|
|
455
|
+
const rawServerUrl = options.serverUrl ?? detected?.serverUrl;
|
|
456
|
+
const token = options.token ?? detected?.token;
|
|
457
|
+
const workspaceId = options.workspaceId ?? detected?.workspaceId;
|
|
458
|
+
if (!rawServerUrl || !token || !workspaceId) {
|
|
459
|
+
throw new GittermError("UNAUTHORIZED", "This command must run inside a GitTerm workspace");
|
|
460
|
+
}
|
|
461
|
+
const serverUrl = normalizeServerUrl(rawServerUrl);
|
|
462
|
+
const trpc = createTRPCClient2({
|
|
463
|
+
links: [
|
|
464
|
+
httpBatchLink2({
|
|
465
|
+
url: new URL("/trpc", serverUrl).toString(),
|
|
466
|
+
fetch: createNoRedirectFetch(options.fetch),
|
|
467
|
+
headers: () => ({ authorization: `Bearer ${token}` })
|
|
468
|
+
})
|
|
469
|
+
]
|
|
470
|
+
});
|
|
471
|
+
async function run(operation) {
|
|
472
|
+
try {
|
|
473
|
+
return await operation();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error instanceof TRPCClientError2) {
|
|
476
|
+
throw new GittermError(errorCode(error.data?.code), error.message, { cause: error });
|
|
477
|
+
}
|
|
478
|
+
throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
workspaceId,
|
|
483
|
+
serverUrl,
|
|
484
|
+
self: { get: () => run(() => trpc.workspaceOps.getSelf.query()) },
|
|
485
|
+
ports: {
|
|
486
|
+
list: () => run(() => trpc.workspaceOps.listPorts.query()),
|
|
487
|
+
open: (port, input) => run(() => trpc.workspaceOps.openPort.mutate({ port, ...input })),
|
|
488
|
+
close: (port) => run(() => trpc.workspaceOps.closePort.mutate({ port }))
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
363
492
|
export {
|
|
364
493
|
saveConfig,
|
|
365
494
|
loginWithDeviceCode,
|
|
366
495
|
loadConfigSync,
|
|
367
496
|
loadConfig,
|
|
497
|
+
getWorkspaceEnvironment,
|
|
368
498
|
getConfigPath,
|
|
369
499
|
deleteConfig,
|
|
500
|
+
createGittermWorkspaceClient,
|
|
370
501
|
createGittermClient,
|
|
371
502
|
WorkspaceLifecycleError,
|
|
372
503
|
GittermError,
|
package/dist/types.d.ts
CHANGED
|
@@ -72,23 +72,110 @@ export type WorkspaceListResult = {
|
|
|
72
72
|
hasMore: boolean;
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
|
+
export type ProviderKey = "railway" | "aws" | "e2b" | "daytona" | "cloudflare" | "vercel" | "ascii" | "exedev";
|
|
76
|
+
type ProviderSelectionBase = {
|
|
77
|
+
/** Select a specific provider installation. Usually omitted. */
|
|
78
|
+
providerId?: string;
|
|
79
|
+
/** An admin-defined profile or flexible resources within the provider's allowed limits. */
|
|
80
|
+
machine?: {
|
|
81
|
+
type: "profile";
|
|
82
|
+
key: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
type FlexibleMachine<T> = {
|
|
86
|
+
type: "profile";
|
|
87
|
+
key: string;
|
|
88
|
+
} | {
|
|
89
|
+
type: "custom";
|
|
90
|
+
resources: T;
|
|
91
|
+
};
|
|
92
|
+
type AwsResources = {
|
|
93
|
+
cpu?: number;
|
|
94
|
+
memory?: number;
|
|
95
|
+
ephemeralStorageGiB?: number;
|
|
96
|
+
architecture?: "X86_64" | "ARM64";
|
|
97
|
+
};
|
|
98
|
+
type DaytonaResources = {
|
|
99
|
+
resources?: {
|
|
100
|
+
cpu?: number;
|
|
101
|
+
memory?: number;
|
|
102
|
+
disk?: number;
|
|
103
|
+
};
|
|
104
|
+
editorResources?: {
|
|
105
|
+
cpu?: number;
|
|
106
|
+
memory?: number;
|
|
107
|
+
disk?: number;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
type VercelResources = {
|
|
111
|
+
vcpus?: number;
|
|
112
|
+
};
|
|
113
|
+
type ExeDevResources = {
|
|
114
|
+
cpu?: number;
|
|
115
|
+
memory?: string;
|
|
116
|
+
disk?: string;
|
|
117
|
+
};
|
|
118
|
+
export type WorkspaceProviderSelection = {
|
|
119
|
+
type: "railway";
|
|
120
|
+
providerId?: string;
|
|
121
|
+
region?: string;
|
|
122
|
+
} | ({
|
|
123
|
+
type: "aws";
|
|
124
|
+
region?: string;
|
|
125
|
+
} & Omit<ProviderSelectionBase, "machine"> & {
|
|
126
|
+
machine?: FlexibleMachine<AwsResources>;
|
|
127
|
+
}) | ({
|
|
128
|
+
type: "daytona";
|
|
129
|
+
} & Omit<ProviderSelectionBase, "machine"> & {
|
|
130
|
+
machine?: FlexibleMachine<DaytonaResources>;
|
|
131
|
+
}) | ({
|
|
132
|
+
type: "vercel";
|
|
133
|
+
} & Omit<ProviderSelectionBase, "machine"> & {
|
|
134
|
+
machine?: FlexibleMachine<VercelResources>;
|
|
135
|
+
}) | ({
|
|
136
|
+
type: "exedev";
|
|
137
|
+
} & Omit<ProviderSelectionBase, "machine"> & {
|
|
138
|
+
machine?: FlexibleMachine<ExeDevResources>;
|
|
139
|
+
}) | ({
|
|
140
|
+
type: "e2b" | "ascii";
|
|
141
|
+
} & ProviderSelectionBase) | {
|
|
142
|
+
type: "cloudflare";
|
|
143
|
+
providerId?: string;
|
|
144
|
+
};
|
|
145
|
+
export type BuiltInAgentKey = "opencode-ttyd" | "opencode" | "t3code";
|
|
146
|
+
export type AgentKey = BuiltInAgentKey | (string & {});
|
|
75
147
|
export type WorkspaceCreateInput = {
|
|
76
148
|
idempotencyKey?: string;
|
|
77
149
|
name?: string;
|
|
78
|
-
repo
|
|
150
|
+
repo: string;
|
|
79
151
|
branch?: string;
|
|
80
152
|
baseCommit?: string;
|
|
81
153
|
checkoutRef?: string;
|
|
82
154
|
subdomain?: string;
|
|
83
|
-
/**
|
|
84
|
-
agent
|
|
85
|
-
/**
|
|
86
|
-
provider?:
|
|
87
|
-
regionId?: string;
|
|
155
|
+
/** Stable agent key. Defaults to `opencode`. */
|
|
156
|
+
agent?: AgentKey;
|
|
157
|
+
/** Provider intent. Defaults to the user's or deployment's preferred provider. */
|
|
158
|
+
provider?: WorkspaceProviderSelection;
|
|
88
159
|
gitIntegrationId?: string;
|
|
89
|
-
|
|
160
|
+
/** Defaults from the selected provider. */
|
|
161
|
+
persistent?: boolean;
|
|
90
162
|
workspaceProfile?: "standard" | "ssh-enabled";
|
|
91
163
|
modelCredentialIds?: string[];
|
|
164
|
+
/**
|
|
165
|
+
* Ordered commands launched in the repository after the agent server starts.
|
|
166
|
+
* They do not block workspace readiness; inspect ~/.gitterm/setup for status
|
|
167
|
+
* and logs through workspaces.setupStatus()/waitForSetup().
|
|
168
|
+
*/
|
|
169
|
+
setupCommands?: string[];
|
|
170
|
+
/** OpenCode capabilities materialized only in this workspace. */
|
|
171
|
+
opencode?: {
|
|
172
|
+
skills?: Array<{
|
|
173
|
+
name: string;
|
|
174
|
+
content: string;
|
|
175
|
+
}>;
|
|
176
|
+
/** NPM package specs or plugin paths accepted by OpenCode. Pin versions for repeatable runs. */
|
|
177
|
+
plugins?: string[];
|
|
178
|
+
};
|
|
92
179
|
};
|
|
93
180
|
export type WorkspaceRestartResult = {
|
|
94
181
|
status: WorkspaceStatus;
|
|
@@ -104,8 +191,59 @@ export type WorkspaceEnsureRunningResult = {
|
|
|
104
191
|
workspace: Workspace;
|
|
105
192
|
runtime: WorkspaceRuntimeAccess;
|
|
106
193
|
};
|
|
194
|
+
export type AgentRunStatus = "pending" | "running" | "retrying" | "completed" | "failed" | "cancelled";
|
|
195
|
+
export type AgentRun = {
|
|
196
|
+
id: string;
|
|
197
|
+
workspaceId: string;
|
|
198
|
+
title: string;
|
|
199
|
+
status: AgentRunStatus;
|
|
200
|
+
error: string | null;
|
|
201
|
+
finalText: string | null;
|
|
202
|
+
context: {
|
|
203
|
+
type: "isolated";
|
|
204
|
+
} | {
|
|
205
|
+
type: "continued";
|
|
206
|
+
runId: string;
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
export type AgentRunCreateInput = {
|
|
210
|
+
workspaceId: string;
|
|
211
|
+
/** Stable key used to return the same run when a request is retried. */
|
|
212
|
+
idempotencyKey: string;
|
|
213
|
+
prompt: string;
|
|
214
|
+
title?: string;
|
|
215
|
+
agent?: string;
|
|
216
|
+
/** OpenCode model in provider/model format. */
|
|
217
|
+
model?: string;
|
|
218
|
+
/** Start with fresh context (default), or continue a terminal run's context. */
|
|
219
|
+
context?: {
|
|
220
|
+
type: "isolated";
|
|
221
|
+
} | {
|
|
222
|
+
type: "continue";
|
|
223
|
+
runId: string;
|
|
224
|
+
};
|
|
225
|
+
/** Wait for workspace setup commands before submitting the prompt. */
|
|
226
|
+
waitForSetup?: boolean;
|
|
227
|
+
setupTimeoutMs?: number;
|
|
228
|
+
};
|
|
229
|
+
export type AgentRunMessage = {
|
|
230
|
+
id: string;
|
|
231
|
+
role: "user" | "assistant";
|
|
232
|
+
createdAt: string;
|
|
233
|
+
completedAt: string | null;
|
|
234
|
+
text: string;
|
|
235
|
+
error: string | null;
|
|
236
|
+
};
|
|
237
|
+
export type WorkspaceSetupStatus = {
|
|
238
|
+
status: "not_requested" | "waiting" | "running" | "succeeded" | "failed";
|
|
239
|
+
exitCode: number | null;
|
|
240
|
+
startedAt: string | null;
|
|
241
|
+
finishedAt: string | null;
|
|
242
|
+
log: string | null;
|
|
243
|
+
};
|
|
107
244
|
export type AgentType = {
|
|
108
245
|
id: string;
|
|
246
|
+
key: string;
|
|
109
247
|
name: string;
|
|
110
248
|
description: string | null;
|
|
111
249
|
serverOnly: boolean;
|
|
@@ -116,18 +254,44 @@ export type AgentType = {
|
|
|
116
254
|
export type CloudProvider = {
|
|
117
255
|
id: string;
|
|
118
256
|
name: string;
|
|
119
|
-
providerKey: string;
|
|
120
|
-
regions?: Array<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
agentTypeId: string;
|
|
127
|
-
cloudProviderId: string;
|
|
128
|
-
regionId?: string;
|
|
257
|
+
providerKey: ProviderKey | string;
|
|
258
|
+
regions?: Array<{
|
|
259
|
+
id: string;
|
|
260
|
+
name: string;
|
|
261
|
+
location: string;
|
|
262
|
+
externalRegionIdentifier: string;
|
|
263
|
+
}>;
|
|
129
264
|
};
|
|
130
|
-
export type
|
|
131
|
-
|
|
132
|
-
|
|
265
|
+
export type WorkspaceCatalog = {
|
|
266
|
+
agents: Array<{
|
|
267
|
+
id: string;
|
|
268
|
+
key: string;
|
|
269
|
+
name: string;
|
|
270
|
+
description: string | null;
|
|
271
|
+
serverOnly: boolean;
|
|
272
|
+
}>;
|
|
273
|
+
providers: Array<{
|
|
274
|
+
id: string;
|
|
275
|
+
type: ProviderKey;
|
|
276
|
+
name: string;
|
|
277
|
+
isDefault: boolean;
|
|
278
|
+
persistence: "required" | "optional" | "unsupported";
|
|
279
|
+
regionSelection: "none" | "user" | "admin";
|
|
280
|
+
regions: Array<{
|
|
281
|
+
id: string;
|
|
282
|
+
key: string;
|
|
283
|
+
name: string;
|
|
284
|
+
location: string;
|
|
285
|
+
}>;
|
|
286
|
+
machines: Array<{
|
|
287
|
+
id: string;
|
|
288
|
+
key: string;
|
|
289
|
+
name: string;
|
|
290
|
+
description: string | null;
|
|
291
|
+
isDefault: boolean;
|
|
292
|
+
}>;
|
|
293
|
+
agentKeys: string[];
|
|
294
|
+
ssh: boolean;
|
|
295
|
+
}>;
|
|
133
296
|
};
|
|
297
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type WorkspaceEnvironment = {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
};
|
|
6
|
+
export type WorkspaceSelf = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
status: "pending" | "running" | "paused" | "terminated";
|
|
10
|
+
repositoryUrl: string | null;
|
|
11
|
+
repositoryBranch: string | null;
|
|
12
|
+
baseCommit: string | null;
|
|
13
|
+
checkoutRef: string | null;
|
|
14
|
+
providerKey: string | null;
|
|
15
|
+
url: string | null;
|
|
16
|
+
ports: WorkspacePort[];
|
|
17
|
+
};
|
|
18
|
+
export type WorkspacePort = {
|
|
19
|
+
port: number;
|
|
20
|
+
name: string | null;
|
|
21
|
+
url: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type WorkspaceClientOptions = Partial<WorkspaceEnvironment> & {
|
|
24
|
+
fetch?: typeof globalThis.fetch;
|
|
25
|
+
};
|
|
26
|
+
export type GittermWorkspaceClient = {
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
serverUrl: string;
|
|
29
|
+
self: {
|
|
30
|
+
get(): Promise<WorkspaceSelf>;
|
|
31
|
+
};
|
|
32
|
+
ports: {
|
|
33
|
+
list(): Promise<WorkspacePort[]>;
|
|
34
|
+
open(port: number, options?: {
|
|
35
|
+
name?: string;
|
|
36
|
+
}): Promise<WorkspacePort>;
|
|
37
|
+
close(port: number): Promise<{
|
|
38
|
+
port: number;
|
|
39
|
+
closed: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export declare function getWorkspaceEnvironment(environment?: Record<string, string | undefined>): WorkspaceEnvironment | null;
|
|
44
|
+
export declare function createGittermWorkspaceClient(options?: WorkspaceClientOptions): GittermWorkspaceClient;
|