@gitterm/sdk 0.0.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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @gitterm/sdk
2
+
3
+ TypeScript SDK for the [GitTerm](https://gitterm.dev) API. Used by the `gitterm` CLI,
4
+ the OpenCode plugin, and any integration that needs to manage GitTerm workspaces with
5
+ a user API token.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ bun add @gitterm/sdk
11
+ # or
12
+ npm install @gitterm/sdk
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### With an explicit API token
18
+
19
+ Create a token in the GitTerm dashboard under **Settings → Account → API tokens**
20
+ (revocable, optional expiry), or obtain one via `gitterm login`.
21
+
22
+ ```ts
23
+ import { createGittermClient } from "@gitterm/sdk";
24
+
25
+ const client = createGittermClient({
26
+ serverUrl: "https://api.gitterm.dev",
27
+ token: process.env.GITTERM_API_TOKEN,
28
+ });
29
+
30
+ const { workspaces } = await client.workspaces.list();
31
+ ```
32
+
33
+ ### With the CLI's saved login
34
+
35
+ If you omit `serverUrl`/`token`, the SDK reads the config written by `gitterm login`
36
+ (`~/.config/gitterm/cli.json`), falling back to the `GITTERM_SERVER_URL` and
37
+ `GITTERM_API_TOKEN` environment variables.
38
+
39
+ ```ts
40
+ const client = createGittermClient();
41
+ const status = await client.auth.status();
42
+ ```
43
+
44
+ ### API
45
+
46
+ ```ts
47
+ client.auth.status(); // -> { userId, email, name, plan, authMethod }
48
+ client.workspaces.list(options?); // -> { workspaces, pagination }
49
+ client.workspaces.get(workspaceId);
50
+ client.workspaces.pause(workspaceId);
51
+ client.workspaces.restart(workspaceId);
52
+ client.workspaces.terminate(workspaceId);
53
+ client.workspaces.create(input); // needs agentTypeId + cloudProviderId, see catalog
54
+ client.catalog.agentTypes();
55
+ client.catalog.cloudProviders();
56
+ ```
57
+
58
+ ### Errors
59
+
60
+ Every method throws `GittermError` with a stable `code`:
61
+
62
+ ```ts
63
+ import { GittermError } from "@gitterm/sdk";
64
+
65
+ try {
66
+ await client.workspaces.get(id);
67
+ } catch (error) {
68
+ if (error instanceof GittermError && error.code === "NOT_LOGGED_IN") {
69
+ // "Not logged in. Run: gitterm login"
70
+ }
71
+ }
72
+ ```
73
+
74
+ Codes: `NOT_LOGGED_IN`, `UNAUTHORIZED`, `NOT_FOUND`, `FORBIDDEN`, `BAD_REQUEST`,
75
+ `SERVER_ERROR`, `NETWORK`.
76
+
77
+ ### Obtaining a token programmatically
78
+
79
+ The device-code flow used by `gitterm login` is exposed for integrations:
80
+
81
+ ```ts
82
+ import { loginWithDeviceCode, saveConfig, DEFAULT_GITTERM_SERVER_URL } from "@gitterm/sdk";
83
+
84
+ const { token } = await loginWithDeviceCode(DEFAULT_GITTERM_SERVER_URL, {
85
+ onCode: ({ verificationUri, userCode }) => {
86
+ console.log(`Visit ${verificationUri} and enter ${userCode}`);
87
+ },
88
+ });
89
+
90
+ await saveConfig({
91
+ serverUrl: DEFAULT_GITTERM_SERVER_URL,
92
+ token,
93
+ createdAt: Date.now(),
94
+ });
95
+ ```
96
+
97
+ Device-code logins produce the same revocable `gt_...` API token as the dashboard;
98
+ they appear in **Settings → Account → API tokens** and can be revoked there.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Hand-maintained public type surface for @gitterm/sdk.
3
+ *
4
+ * External npm consumers resolve these types instead of the TypeScript
5
+ * sources, because the sources derive types from the private @gitterm/api
6
+ * router. Keep this file in sync with src/types.ts and src/client.ts.
7
+ *
8
+ * TODO: replace with generated declarations once the AppRouter types can be
9
+ * bundled into a standalone .d.ts.
10
+ */
11
+
12
+ export const DEFAULT_GITTERM_SERVER_URL: string;
13
+
14
+ export type CliConfig = {
15
+ serverUrl: string;
16
+ token: string;
17
+ createdAt: number;
18
+ };
19
+
20
+ export type GittermErrorCode =
21
+ | "NOT_LOGGED_IN"
22
+ | "UNAUTHORIZED"
23
+ | "NOT_FOUND"
24
+ | "FORBIDDEN"
25
+ | "BAD_REQUEST"
26
+ | "SERVER_ERROR"
27
+ | "NETWORK";
28
+
29
+ export class GittermError extends Error {
30
+ readonly code: GittermErrorCode;
31
+ readonly cause?: unknown;
32
+ constructor(code: GittermErrorCode, message: string, options?: { cause?: unknown });
33
+ }
34
+
35
+ export type GittermClientOptions = {
36
+ serverUrl?: string;
37
+ token?: string;
38
+ configPath?: string;
39
+ fetch?: typeof fetch;
40
+ };
41
+
42
+ export type AuthStatus = {
43
+ loggedIn: true;
44
+ userId: string;
45
+ email: string;
46
+ name: string;
47
+ plan: string;
48
+ authMethod: "session" | "apiToken";
49
+ };
50
+
51
+ export type WorkspaceStatus = "pending" | "running" | "paused" | "terminated";
52
+ export type WorkspaceHostingType = "cloud" | "local";
53
+
54
+ export type Workspace = {
55
+ id: string;
56
+ name: string | null;
57
+ status: WorkspaceStatus;
58
+ repositoryUrl: string | null;
59
+ repositoryBranch: string | null;
60
+ baseCommit: string | null;
61
+ checkoutRef: string | null;
62
+ domain: string;
63
+ subdomain: string | null;
64
+ persistent: boolean;
65
+ hostingType: WorkspaceHostingType;
66
+ serverOnly: boolean;
67
+ workspaceProfile: string;
68
+ cloudProviderId: string;
69
+ agentType: { id: string; name: string; description: string | null } | null;
70
+ image: { id: string; name: string; imageId: string } | null;
71
+ startedAt: string | null;
72
+ stoppedAt: string | null;
73
+ terminatedAt: string | null;
74
+ lastActiveAt: string | null;
75
+ updatedAt: string | null;
76
+ };
77
+
78
+ export type WorkspaceRuntimeAccess = {
79
+ workspaceId: string;
80
+ status: WorkspaceStatus;
81
+ url: string | null;
82
+ headers?: Record<string, string>;
83
+ password?: string;
84
+ directory: string;
85
+ repo: string | null;
86
+ branch: string | null;
87
+ baseCommit: string | null;
88
+ checkoutRef: string | null;
89
+ persistent: boolean;
90
+ recoverable: boolean;
91
+ providerKey: string | null;
92
+ };
93
+
94
+ export type WorkspaceCreateResult = {
95
+ workspace: Workspace;
96
+ runtime: WorkspaceRuntimeAccess;
97
+ };
98
+
99
+ export type WorkspaceListOptions = {
100
+ limit?: number;
101
+ offset?: number;
102
+ status?: "all" | "active" | "terminated";
103
+ };
104
+
105
+ export type WorkspaceListResult = {
106
+ workspaces: Workspace[];
107
+ pagination: {
108
+ total: number;
109
+ limit: number;
110
+ offset: number;
111
+ hasMore: boolean;
112
+ };
113
+ };
114
+
115
+ export type WorkspaceCreateInput = {
116
+ name?: string;
117
+ repo?: string;
118
+ branch?: string;
119
+ baseCommit?: string;
120
+ checkoutRef?: string;
121
+ subdomain?: string;
122
+ agentTypeId: string;
123
+ cloudProviderId: string;
124
+ regionId?: string;
125
+ gitIntegrationId?: string;
126
+ persistent: boolean;
127
+ workspaceProfile?: "standard" | "ssh-enabled";
128
+ };
129
+
130
+ export type WorkspaceStopResult = { durationMinutes: number };
131
+ export type WorkspacePauseResult = WorkspaceStopResult;
132
+ export type WorkspaceRestartResult = { status: WorkspaceStatus };
133
+ export type WorkspaceTerminateResult = {
134
+ workspace: Workspace | null;
135
+ cleanupInBackground: boolean;
136
+ };
137
+ export type WorkspaceEnsureRunningResult = {
138
+ workspace: Workspace;
139
+ runtime: WorkspaceRuntimeAccess;
140
+ };
141
+
142
+ export type AgentType = {
143
+ id: string;
144
+ name: string;
145
+ description: string | null;
146
+ serverOnly: boolean;
147
+ isEnabled: boolean;
148
+ createdAt: Date | string;
149
+ updatedAt: Date | string;
150
+ };
151
+
152
+ export type CloudProvider = Record<string, unknown> & {
153
+ id: string;
154
+ name: string;
155
+ providerKey: string;
156
+ regions?: Array<Record<string, unknown>>;
157
+ };
158
+
159
+ export type GittermClient = {
160
+ serverUrl: string;
161
+ auth: {
162
+ status(): Promise<AuthStatus>;
163
+ };
164
+ workspaces: {
165
+ list(input?: WorkspaceListOptions): Promise<WorkspaceListResult>;
166
+ get(workspaceId: string): Promise<Workspace>;
167
+ getRuntimeAccess(workspaceId: string): Promise<WorkspaceRuntimeAccess>;
168
+ ensureRunning(
169
+ workspaceId: string,
170
+ options?: { timeoutMs?: number; pollIntervalMs?: number },
171
+ ): Promise<WorkspaceEnsureRunningResult>;
172
+ pause(workspaceId: string): Promise<WorkspacePauseResult>;
173
+ restart(workspaceId: string): Promise<WorkspaceRestartResult>;
174
+ terminate(workspaceId: string): Promise<WorkspaceTerminateResult>;
175
+ create(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
176
+ createSandbox(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
177
+ };
178
+ catalog: {
179
+ agentTypes(input?: { serverOnly?: boolean }): Promise<AgentType[]>;
180
+ cloudProviders(input?: {
181
+ localOnly?: boolean;
182
+ cloudOnly?: boolean;
183
+ sandboxOnly?: boolean;
184
+ nonSandboxOnly?: boolean;
185
+ }): Promise<CloudProvider[]>;
186
+ };
187
+ };
188
+
189
+ export function createGittermClient(options?: GittermClientOptions): GittermClient;
190
+ export function getConfigPath(configPath?: string): string;
191
+ export function loadConfig(configPath?: string): Promise<CliConfig | null>;
192
+ export function loadConfigSync(configPath?: string): CliConfig | null;
193
+ export function saveConfig(config: CliConfig, configPath?: string): Promise<void>;
194
+ export function deleteConfig(configPath?: string): Promise<void>;
195
+
196
+ export type DeviceCodeInfo = {
197
+ deviceCode: string;
198
+ userCode: string;
199
+ verificationUri: string;
200
+ intervalSeconds: number;
201
+ expiresInSeconds: number;
202
+ };
203
+
204
+ export type LoginWithDeviceCodeOptions = {
205
+ clientName?: string;
206
+ fetch?: typeof fetch;
207
+ onCode?: (code: Omit<DeviceCodeInfo, "deviceCode">) => void | Promise<void>;
208
+ };
209
+
210
+ export function loginWithDeviceCode(
211
+ serverUrl: string,
212
+ options?: LoginWithDeviceCodeOptions,
213
+ ): Promise<{ token: string }>;
package/dist/index.js ADDED
@@ -0,0 +1,339 @@
1
+ // src/client.ts
2
+ import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client";
3
+
4
+ // src/config.ts
5
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
6
+ import { readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ var DEFAULT_GITTERM_SERVER_URL = "https://api.gitterm.dev";
10
+ function getConfigPath(configPath) {
11
+ return configPath ?? join(homedir(), ".config", "gitterm", "cli.json");
12
+ }
13
+ async function ensureConfigDir(path) {
14
+ await mkdir(dirname(path), { recursive: true });
15
+ }
16
+ function parseConfig(text) {
17
+ const parsed = JSON.parse(text);
18
+ if (!parsed.token || !parsed.serverUrl)
19
+ return null;
20
+ return {
21
+ serverUrl: parsed.serverUrl,
22
+ token: parsed.token,
23
+ createdAt: parsed.createdAt ?? Date.now()
24
+ };
25
+ }
26
+ async function loadConfig(configPath) {
27
+ try {
28
+ const text = await readFile(getConfigPath(configPath), "utf-8");
29
+ return parseConfig(text);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function loadConfigSync(configPath) {
35
+ try {
36
+ const text = readFileSync(getConfigPath(configPath), "utf-8");
37
+ return parseConfig(text);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ async function saveConfig(config, configPath) {
43
+ const path = getConfigPath(configPath);
44
+ await ensureConfigDir(path);
45
+ await writeFile(path, JSON.stringify(config, null, 2), "utf-8");
46
+ }
47
+ async function deleteConfig(configPath) {
48
+ try {
49
+ await unlink(getConfigPath(configPath));
50
+ } catch {}
51
+ }
52
+
53
+ // src/errors.ts
54
+ class GittermError extends Error {
55
+ code;
56
+ cause;
57
+ constructor(code, message, options = {}) {
58
+ super(message);
59
+ this.name = "GittermError";
60
+ this.code = code;
61
+ this.cause = options.cause;
62
+ }
63
+ }
64
+
65
+ // src/client.ts
66
+ function envValue(name) {
67
+ const value = typeof process !== "undefined" ? process.env[name] : undefined;
68
+ return value && value.trim() ? value : undefined;
69
+ }
70
+ function resolveCredentials(options) {
71
+ const config = !options.serverUrl || !options.token ? loadConfigSync(options.configPath) : null;
72
+ const serverUrl = options.serverUrl ?? envValue("GITTERM_SERVER_URL") ?? config?.serverUrl;
73
+ const token = options.token ?? envValue("GITTERM_API_TOKEN") ?? config?.token;
74
+ if (!serverUrl || !token) {
75
+ throw new GittermError("NOT_LOGGED_IN", "Not logged in. Run: gitterm login");
76
+ }
77
+ return { serverUrl, token };
78
+ }
79
+ function toTrpcUrl(serverUrl) {
80
+ return new URL("/trpc", serverUrl).toString();
81
+ }
82
+ function toIso(value) {
83
+ if (!value)
84
+ return null;
85
+ return value instanceof Date ? value.toISOString() : value;
86
+ }
87
+ function normalizeBaseCommit(value) {
88
+ if (value == null)
89
+ return null;
90
+ const trimmed = value.trim();
91
+ if (!trimmed)
92
+ return null;
93
+ return /^[0-9a-f]{40}([0-9a-f]{24})?$/i.test(trimmed) ? trimmed.toLowerCase() : trimmed;
94
+ }
95
+ function normalizeWorkspace(workspace) {
96
+ if (!workspace)
97
+ return null;
98
+ return {
99
+ id: workspace.id,
100
+ name: workspace.name,
101
+ status: workspace.status,
102
+ repositoryUrl: workspace.repositoryUrl,
103
+ repositoryBranch: workspace.repositoryBranch,
104
+ baseCommit: normalizeBaseCommit(workspace.baseCommit ?? workspace.repositoryBaseCommit ?? null),
105
+ checkoutRef: workspace.checkoutRef ?? workspace.repositoryCheckoutRef ?? null,
106
+ domain: workspace.domain,
107
+ subdomain: workspace.subdomain,
108
+ persistent: workspace.persistent,
109
+ hostingType: workspace.hostingType,
110
+ serverOnly: workspace.serverOnly,
111
+ workspaceProfile: workspace.workspaceProfile,
112
+ cloudProviderId: workspace.cloudProviderId,
113
+ agentType: workspace.image?.agentType ? {
114
+ id: workspace.image.agentType.id,
115
+ name: workspace.image.agentType.name,
116
+ description: workspace.image.agentType.description
117
+ } : null,
118
+ image: workspace.image ? {
119
+ id: workspace.image.id,
120
+ name: workspace.image.name,
121
+ imageId: workspace.image.imageId
122
+ } : null,
123
+ startedAt: toIso(workspace.startedAt),
124
+ stoppedAt: toIso(workspace.stoppedAt),
125
+ terminatedAt: toIso(workspace.terminatedAt),
126
+ lastActiveAt: toIso(workspace.lastActiveAt),
127
+ updatedAt: toIso(workspace.updatedAt)
128
+ };
129
+ }
130
+ function normalizeRuntime(runtime) {
131
+ return {
132
+ workspaceId: runtime.workspaceId,
133
+ status: runtime.status,
134
+ url: runtime.url,
135
+ headers: runtime.headers,
136
+ password: runtime.password,
137
+ directory: runtime.directory,
138
+ repo: runtime.repo,
139
+ branch: runtime.branch,
140
+ baseCommit: normalizeBaseCommit(runtime.baseCommit),
141
+ checkoutRef: runtime.checkoutRef,
142
+ persistent: runtime.persistent,
143
+ recoverable: runtime.recoverable,
144
+ providerKey: runtime.providerKey
145
+ };
146
+ }
147
+ function mapTrpcCode(code) {
148
+ switch (code) {
149
+ case "UNAUTHORIZED":
150
+ return "UNAUTHORIZED";
151
+ case "NOT_FOUND":
152
+ return "NOT_FOUND";
153
+ case "FORBIDDEN":
154
+ return "FORBIDDEN";
155
+ case "BAD_REQUEST":
156
+ return "BAD_REQUEST";
157
+ default:
158
+ return "SERVER_ERROR";
159
+ }
160
+ }
161
+ async function runWithServer(serverUrl, operation) {
162
+ try {
163
+ return await operation();
164
+ } catch (error) {
165
+ if (error instanceof GittermError)
166
+ throw error;
167
+ if (error instanceof TRPCClientError) {
168
+ const trpcCode = error.data?.code;
169
+ if (!error.data) {
170
+ throw new GittermError("NETWORK", `Could not reach the GitTerm server at ${serverUrl}`, {
171
+ cause: error
172
+ });
173
+ }
174
+ const code = mapTrpcCode(trpcCode);
175
+ throw new GittermError(code, code === "UNAUTHORIZED" ? "Not logged in or token expired. Run: gitterm login" : error.message, { cause: error });
176
+ }
177
+ throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
178
+ }
179
+ }
180
+ function createGittermClient(options = {}) {
181
+ const credentials = resolveCredentials(options);
182
+ const trpc = createTRPCClient({
183
+ links: [
184
+ httpBatchLink({
185
+ url: toTrpcUrl(credentials.serverUrl),
186
+ fetch: options.fetch,
187
+ headers: () => ({ authorization: `Bearer ${credentials.token}` })
188
+ })
189
+ ]
190
+ });
191
+ const run = (operation) => runWithServer(credentials.serverUrl, operation);
192
+ const createWorkspace = (input) => run(async () => {
193
+ const result = await trpc.workspace.createWorkspace.mutate(input);
194
+ const workspace = normalizeWorkspace(result.workspace);
195
+ if (!workspace)
196
+ throw new GittermError("SERVER_ERROR", "Workspace creation failed");
197
+ const runtime = result.runtime ? normalizeRuntime(result.runtime) : {
198
+ workspaceId: workspace.id,
199
+ status: workspace.status,
200
+ url: null,
201
+ directory: "/workspace",
202
+ repo: workspace.repositoryUrl,
203
+ branch: workspace.repositoryBranch,
204
+ baseCommit: workspace.baseCommit,
205
+ checkoutRef: workspace.checkoutRef,
206
+ persistent: workspace.persistent,
207
+ recoverable: workspace.status !== "terminated",
208
+ providerKey: null
209
+ };
210
+ return { workspace, runtime };
211
+ });
212
+ return {
213
+ serverUrl: credentials.serverUrl,
214
+ auth: {
215
+ status: () => run(async () => {
216
+ const me = await trpc.agent.me.query();
217
+ return {
218
+ loggedIn: true,
219
+ userId: me.userId,
220
+ email: me.email,
221
+ name: me.name,
222
+ plan: me.plan ?? "free",
223
+ authMethod: me.authMethod
224
+ };
225
+ })
226
+ },
227
+ workspaces: {
228
+ list: (input) => run(async () => {
229
+ const result = await trpc.workspace.listWorkspaces.query(input);
230
+ return {
231
+ workspaces: result.workspaces.map((workspace) => normalizeWorkspace(workspace)),
232
+ pagination: result.pagination
233
+ };
234
+ }),
235
+ get: (workspaceId) => run(async () => {
236
+ const result = await trpc.workspace.getWorkspace.query({ workspaceId });
237
+ const workspace = normalizeWorkspace(result.workspace);
238
+ if (!workspace)
239
+ throw new GittermError("NOT_FOUND", "Workspace not found");
240
+ return workspace;
241
+ }),
242
+ getRuntimeAccess: (workspaceId) => run(async () => {
243
+ const result = await trpc.workspace.getRuntimeAccess.query({ workspaceId });
244
+ return normalizeRuntime(result);
245
+ }),
246
+ ensureRunning: (workspaceId, options2) => run(async () => {
247
+ const result = await trpc.workspace.ensureRunning.mutate({
248
+ workspaceId,
249
+ timeoutMs: options2?.timeoutMs,
250
+ pollIntervalMs: options2?.pollIntervalMs
251
+ });
252
+ const workspace = normalizeWorkspace(result.workspace);
253
+ if (!workspace)
254
+ throw new GittermError("SERVER_ERROR", "ensureRunning failed");
255
+ return {
256
+ workspace,
257
+ runtime: normalizeRuntime(result.runtime)
258
+ };
259
+ }),
260
+ pause: (workspaceId) => run(async () => {
261
+ const result = await trpc.workspace.pauseWorkspace.mutate({ workspaceId });
262
+ return { durationMinutes: result.durationMinutes };
263
+ }),
264
+ restart: (workspaceId) => run(async () => {
265
+ const result = await trpc.workspace.restartWorkspace.mutate({ workspaceId });
266
+ return { status: result.status };
267
+ }),
268
+ terminate: (workspaceId) => run(async () => {
269
+ const result = await trpc.workspace.deleteWorkspace.mutate({ workspaceId });
270
+ return {
271
+ workspace: normalizeWorkspace(result.workspace),
272
+ cleanupInBackground: result.cleanupInBackground
273
+ };
274
+ }),
275
+ create: createWorkspace,
276
+ createSandbox: createWorkspace
277
+ },
278
+ catalog: {
279
+ agentTypes: (input) => run(async () => {
280
+ const result = await trpc.workspace.listAgentTypes.query(input);
281
+ return result.agentTypes;
282
+ }),
283
+ cloudProviders: (input) => run(async () => {
284
+ const result = await trpc.workspace.listCloudProviders.query(input);
285
+ return result.cloudProviders;
286
+ })
287
+ }
288
+ };
289
+ }
290
+ // src/device-login.ts
291
+ function sleep(ms) {
292
+ return new Promise((resolve) => setTimeout(resolve, ms));
293
+ }
294
+ async function loginWithDeviceCode(serverUrl, options = {}) {
295
+ const fetchImpl = options.fetch ?? fetch;
296
+ const codeRes = await fetchImpl(new URL("/api/device/code", serverUrl), {
297
+ method: "POST",
298
+ headers: { "content-type": "application/json" },
299
+ body: JSON.stringify({ clientName: options.clientName ?? "gitterm" })
300
+ });
301
+ if (!codeRes.ok)
302
+ throw new Error(`Failed to start device login: ${codeRes.status}`);
303
+ const codeJson = await codeRes.json();
304
+ await options.onCode?.({
305
+ userCode: codeJson.userCode,
306
+ verificationUri: codeJson.verificationUri,
307
+ intervalSeconds: codeJson.intervalSeconds,
308
+ expiresInSeconds: codeJson.expiresInSeconds
309
+ });
310
+ const deadline = Date.now() + codeJson.expiresInSeconds * 1000;
311
+ while (Date.now() < deadline) {
312
+ const tokenRes = await fetchImpl(new URL("/api/device/token", serverUrl), {
313
+ method: "POST",
314
+ headers: { "content-type": "application/json" },
315
+ body: JSON.stringify({ deviceCode: codeJson.deviceCode })
316
+ });
317
+ if (tokenRes.ok) {
318
+ const tokenJson = await tokenRes.json();
319
+ return { token: tokenJson.accessToken };
320
+ }
321
+ if (tokenRes.status !== 428) {
322
+ const errText = await tokenRes.text().catch(() => "");
323
+ throw new Error(`Login failed: ${tokenRes.status} ${errText}`);
324
+ }
325
+ await sleep(Math.max(1, codeJson.intervalSeconds) * 1000);
326
+ }
327
+ throw new Error("Device code expired; try again.");
328
+ }
329
+ export {
330
+ saveConfig,
331
+ loginWithDeviceCode,
332
+ loadConfigSync,
333
+ loadConfig,
334
+ getConfigPath,
335
+ deleteConfig,
336
+ createGittermClient,
337
+ GittermError,
338
+ DEFAULT_GITTERM_SERVER_URL
339
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@gitterm/sdk",
3
+ "version": "0.0.1",
4
+ "files": [
5
+ "dist",
6
+ "src/index.d.ts"
7
+ ],
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "bun": "./src/index.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external @trpc/client && cp src/index.d.ts dist/index.d.ts",
22
+ "prepublishOnly": "bun run check-types && bun run build",
23
+ "check-types": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@trpc/client": "^11.8.1"
27
+ },
28
+ "devDependencies": {
29
+ "@gitterm/api": "workspace:*",
30
+ "@gitterm/config": "workspace:*",
31
+ "@trpc/server": "catalog:",
32
+ "@types/bun": "^1.2.6",
33
+ "typescript": "^5.8.2"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Hand-maintained public type surface for @gitterm/sdk.
3
+ *
4
+ * External npm consumers resolve these types instead of the TypeScript
5
+ * sources, because the sources derive types from the private @gitterm/api
6
+ * router. Keep this file in sync with src/types.ts and src/client.ts.
7
+ *
8
+ * TODO: replace with generated declarations once the AppRouter types can be
9
+ * bundled into a standalone .d.ts.
10
+ */
11
+
12
+ export const DEFAULT_GITTERM_SERVER_URL: string;
13
+
14
+ export type CliConfig = {
15
+ serverUrl: string;
16
+ token: string;
17
+ createdAt: number;
18
+ };
19
+
20
+ export type GittermErrorCode =
21
+ | "NOT_LOGGED_IN"
22
+ | "UNAUTHORIZED"
23
+ | "NOT_FOUND"
24
+ | "FORBIDDEN"
25
+ | "BAD_REQUEST"
26
+ | "SERVER_ERROR"
27
+ | "NETWORK";
28
+
29
+ export class GittermError extends Error {
30
+ readonly code: GittermErrorCode;
31
+ readonly cause?: unknown;
32
+ constructor(code: GittermErrorCode, message: string, options?: { cause?: unknown });
33
+ }
34
+
35
+ export type GittermClientOptions = {
36
+ serverUrl?: string;
37
+ token?: string;
38
+ configPath?: string;
39
+ fetch?: typeof fetch;
40
+ };
41
+
42
+ export type AuthStatus = {
43
+ loggedIn: true;
44
+ userId: string;
45
+ email: string;
46
+ name: string;
47
+ plan: string;
48
+ authMethod: "session" | "apiToken";
49
+ };
50
+
51
+ export type WorkspaceStatus = "pending" | "running" | "paused" | "terminated";
52
+ export type WorkspaceHostingType = "cloud" | "local";
53
+
54
+ export type Workspace = {
55
+ id: string;
56
+ name: string | null;
57
+ status: WorkspaceStatus;
58
+ repositoryUrl: string | null;
59
+ repositoryBranch: string | null;
60
+ baseCommit: string | null;
61
+ checkoutRef: string | null;
62
+ domain: string;
63
+ subdomain: string | null;
64
+ persistent: boolean;
65
+ hostingType: WorkspaceHostingType;
66
+ serverOnly: boolean;
67
+ workspaceProfile: string;
68
+ cloudProviderId: string;
69
+ agentType: { id: string; name: string; description: string | null } | null;
70
+ image: { id: string; name: string; imageId: string } | null;
71
+ startedAt: string | null;
72
+ stoppedAt: string | null;
73
+ terminatedAt: string | null;
74
+ lastActiveAt: string | null;
75
+ updatedAt: string | null;
76
+ };
77
+
78
+ export type WorkspaceRuntimeAccess = {
79
+ workspaceId: string;
80
+ status: WorkspaceStatus;
81
+ url: string | null;
82
+ headers?: Record<string, string>;
83
+ password?: string;
84
+ directory: string;
85
+ repo: string | null;
86
+ branch: string | null;
87
+ baseCommit: string | null;
88
+ checkoutRef: string | null;
89
+ persistent: boolean;
90
+ recoverable: boolean;
91
+ providerKey: string | null;
92
+ };
93
+
94
+ export type WorkspaceCreateResult = {
95
+ workspace: Workspace;
96
+ runtime: WorkspaceRuntimeAccess;
97
+ };
98
+
99
+ export type WorkspaceListOptions = {
100
+ limit?: number;
101
+ offset?: number;
102
+ status?: "all" | "active" | "terminated";
103
+ };
104
+
105
+ export type WorkspaceListResult = {
106
+ workspaces: Workspace[];
107
+ pagination: {
108
+ total: number;
109
+ limit: number;
110
+ offset: number;
111
+ hasMore: boolean;
112
+ };
113
+ };
114
+
115
+ export type WorkspaceCreateInput = {
116
+ name?: string;
117
+ repo?: string;
118
+ branch?: string;
119
+ baseCommit?: string;
120
+ checkoutRef?: string;
121
+ subdomain?: string;
122
+ agentTypeId: string;
123
+ cloudProviderId: string;
124
+ regionId?: string;
125
+ gitIntegrationId?: string;
126
+ persistent: boolean;
127
+ workspaceProfile?: "standard" | "ssh-enabled";
128
+ };
129
+
130
+ export type WorkspaceStopResult = { durationMinutes: number };
131
+ export type WorkspacePauseResult = WorkspaceStopResult;
132
+ export type WorkspaceRestartResult = { status: WorkspaceStatus };
133
+ export type WorkspaceTerminateResult = {
134
+ workspace: Workspace | null;
135
+ cleanupInBackground: boolean;
136
+ };
137
+ export type WorkspaceEnsureRunningResult = {
138
+ workspace: Workspace;
139
+ runtime: WorkspaceRuntimeAccess;
140
+ };
141
+
142
+ export type AgentType = {
143
+ id: string;
144
+ name: string;
145
+ description: string | null;
146
+ serverOnly: boolean;
147
+ isEnabled: boolean;
148
+ createdAt: Date | string;
149
+ updatedAt: Date | string;
150
+ };
151
+
152
+ export type CloudProvider = Record<string, unknown> & {
153
+ id: string;
154
+ name: string;
155
+ providerKey: string;
156
+ regions?: Array<Record<string, unknown>>;
157
+ };
158
+
159
+ export type GittermClient = {
160
+ serverUrl: string;
161
+ auth: {
162
+ status(): Promise<AuthStatus>;
163
+ };
164
+ workspaces: {
165
+ list(input?: WorkspaceListOptions): Promise<WorkspaceListResult>;
166
+ get(workspaceId: string): Promise<Workspace>;
167
+ getRuntimeAccess(workspaceId: string): Promise<WorkspaceRuntimeAccess>;
168
+ ensureRunning(
169
+ workspaceId: string,
170
+ options?: { timeoutMs?: number; pollIntervalMs?: number },
171
+ ): Promise<WorkspaceEnsureRunningResult>;
172
+ pause(workspaceId: string): Promise<WorkspacePauseResult>;
173
+ restart(workspaceId: string): Promise<WorkspaceRestartResult>;
174
+ terminate(workspaceId: string): Promise<WorkspaceTerminateResult>;
175
+ create(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
176
+ createSandbox(input: WorkspaceCreateInput): Promise<WorkspaceCreateResult>;
177
+ };
178
+ catalog: {
179
+ agentTypes(input?: { serverOnly?: boolean }): Promise<AgentType[]>;
180
+ cloudProviders(input?: {
181
+ localOnly?: boolean;
182
+ cloudOnly?: boolean;
183
+ sandboxOnly?: boolean;
184
+ nonSandboxOnly?: boolean;
185
+ }): Promise<CloudProvider[]>;
186
+ };
187
+ };
188
+
189
+ export function createGittermClient(options?: GittermClientOptions): GittermClient;
190
+ export function getConfigPath(configPath?: string): string;
191
+ export function loadConfig(configPath?: string): Promise<CliConfig | null>;
192
+ export function loadConfigSync(configPath?: string): CliConfig | null;
193
+ export function saveConfig(config: CliConfig, configPath?: string): Promise<void>;
194
+ export function deleteConfig(configPath?: string): Promise<void>;
195
+
196
+ export type DeviceCodeInfo = {
197
+ deviceCode: string;
198
+ userCode: string;
199
+ verificationUri: string;
200
+ intervalSeconds: number;
201
+ expiresInSeconds: number;
202
+ };
203
+
204
+ export type LoginWithDeviceCodeOptions = {
205
+ clientName?: string;
206
+ fetch?: typeof fetch;
207
+ onCode?: (code: Omit<DeviceCodeInfo, "deviceCode">) => void | Promise<void>;
208
+ };
209
+
210
+ export function loginWithDeviceCode(
211
+ serverUrl: string,
212
+ options?: LoginWithDeviceCodeOptions,
213
+ ): Promise<{ token: string }>;