@document-run/cli 0.1.0

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.
Files changed (2) hide show
  1. package/dist/index.js +1928 -0
  2. package/package.json +26 -0
package/dist/index.js ADDED
@@ -0,0 +1,1928 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/settings.ts
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var SETTINGS_DIR = join(homedir(), ".documentrun");
8
+ var SETTINGS_PATH = join(SETTINGS_DIR, "settings.json");
9
+ var DEFAULT_API_URL = "https://api.document.run";
10
+ function loadSettings() {
11
+ if (!existsSync(SETTINGS_PATH))
12
+ return null;
13
+ try {
14
+ const raw = readFileSync(SETTINGS_PATH, "utf-8");
15
+ const parsed = JSON.parse(raw);
16
+ if (!parsed.token)
17
+ return null;
18
+ return parsed;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function saveSettings(settings) {
24
+ mkdirSync(SETTINGS_DIR, { recursive: true });
25
+ writeFileSync(SETTINGS_PATH, JSON.stringify({ ...settings, apiUrl: settings.apiUrl || DEFAULT_API_URL }, null, 2));
26
+ }
27
+ function isLoggedIn() {
28
+ const settings = loadSettings();
29
+ return settings !== null && !!settings.token;
30
+ }
31
+ function clearSettings() {
32
+ if (existsSync(SETTINGS_PATH)) {
33
+ unlinkSync(SETTINGS_PATH);
34
+ }
35
+ }
36
+
37
+ // src/local/app.ts
38
+ import { Command } from "commander";
39
+
40
+ // src/local/deploy.ts
41
+ import chalk2 from "chalk";
42
+
43
+ // src/config.ts
44
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
45
+ import { resolve, dirname } from "path";
46
+ import { parse as parseYaml } from "yaml";
47
+
48
+ // ../core/src/utils/slugify.ts
49
+ function slugify(input) {
50
+ return input.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/[\s]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
51
+ }
52
+ // ../core/src/utils/tokens.ts
53
+ function generateUuid() {
54
+ return crypto.randomUUID();
55
+ }
56
+ function generateAccessToken() {
57
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
58
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
59
+ }
60
+ function generateDbPassword() {
61
+ const bytes = crypto.getRandomValues(new Uint8Array(24));
62
+ return btoa(String.fromCharCode(...bytes));
63
+ }
64
+ // ../core/src/utils/naming.ts
65
+ var PREFIX = "documentrun";
66
+ var APP_PATH = "/home/daytona/app";
67
+ var SNAPSHOT_NAME = `${PREFIX}-dind`;
68
+ function sandboxLabel(slug) {
69
+ return `${PREFIX}-${slug}`;
70
+ }
71
+ function composeProject(slug) {
72
+ return `${PREFIX}-${slug}`;
73
+ }
74
+ function tunnelName(slug) {
75
+ return `${PREFIX}-${slug}`;
76
+ }
77
+ function workerName(slug) {
78
+ return `${PREFIX}-${slug}`;
79
+ }
80
+ function sandboxHostname(slug, zone) {
81
+ return `${slug}.${zone}`;
82
+ }
83
+ function serviceHostname(serviceName, slug, zone) {
84
+ return `${serviceName}-${slug}.${zone}`;
85
+ }
86
+ function serviceFqdn(serviceName, index, slug, zone) {
87
+ return index === 0 ? sandboxHostname(slug, zone) : serviceHostname(serviceName, slug, zone);
88
+ }
89
+ function workerRoute(slug, zone) {
90
+ return `${slug}.${zone}/*`;
91
+ }
92
+ function workerServiceRoute(slug, zone) {
93
+ return `*-${slug}.${zone}/*`;
94
+ }
95
+ function isSandboxDnsRecord(name, slug, zone) {
96
+ return name === sandboxHostname(slug, zone) || name.endsWith(`-${slug}.${zone}`);
97
+ }
98
+ function sandboxUrl(slug, zone) {
99
+ return `https://${sandboxHostname(slug, zone)}`;
100
+ }
101
+ function wakeUrl(slug, apiUrl) {
102
+ return `${apiUrl}/v1/sandboxes/${slug}/wake`;
103
+ }
104
+ // src/config.ts
105
+ function loadConfig(configPath = "documentrun.yaml", dir) {
106
+ const base = dir ? resolve(dir) : process.cwd();
107
+ const fullPath = resolve(base, configPath);
108
+ if (!existsSync2(fullPath)) {
109
+ throw new Error(`Config file not found: ${fullPath}`);
110
+ }
111
+ const raw = readFileSync2(fullPath, "utf-8");
112
+ const parsed = parseYaml(raw);
113
+ const envFileRelative = parsed.env_file || ".env";
114
+ const envFilePath = resolve(dirname(fullPath), envFileRelative);
115
+ const fileEnv = loadEnvFile(envFilePath);
116
+ const env = { ...fileEnv, ...process.env };
117
+ const resolved = resolveVars(raw, env);
118
+ const config2 = parseYaml(resolved);
119
+ validateRequired(config2);
120
+ const services = buildServices(config2.services);
121
+ const envVars = Object.entries(fileEnv).map(([key, value]) => ({
122
+ key,
123
+ value,
124
+ environment: "shared"
125
+ }));
126
+ const deploy = {
127
+ repo: {
128
+ name: config2.repo.name,
129
+ cloneUrl: config2.repo.clone_url,
130
+ branch: config2.repo.branch || "main",
131
+ private: config2.repo.private ?? false
132
+ },
133
+ sandbox: buildSandbox(config2),
134
+ services,
135
+ envVars,
136
+ githubToken: config2.github.token,
137
+ cloudflare: {
138
+ apiToken: config2.cloudflare.api_token,
139
+ accountId: config2.cloudflare.account_id,
140
+ zoneId: config2.cloudflare.zone_id,
141
+ zone: config2.cloudflare.zone
142
+ },
143
+ apiUrl: "https://api.document.run"
144
+ };
145
+ const daytona = {
146
+ apiUrl: config2.daytona.api_url,
147
+ apiKey: config2.daytona.api_key
148
+ };
149
+ if (config2.daytona.target != null)
150
+ daytona.target = config2.daytona.target;
151
+ return { deploy, daytona };
152
+ }
153
+ function buildSandbox(config2) {
154
+ const sandbox = {
155
+ id: generateUuid(),
156
+ slug: slugify(`${config2.repo.name}-${config2.repo.branch || "main"}`),
157
+ accessToken: generateAccessToken()
158
+ };
159
+ if (config2.sandbox?.cpu != null)
160
+ sandbox.cpu = config2.sandbox.cpu;
161
+ if (config2.sandbox?.memory != null)
162
+ sandbox.memory = config2.sandbox.memory;
163
+ if (config2.sandbox?.auto_stop_interval != null)
164
+ sandbox.autoStopInterval = config2.sandbox.auto_stop_interval;
165
+ return sandbox;
166
+ }
167
+ function buildServices(raw) {
168
+ return raw.map((s) => {
169
+ const name = slugify(s.label);
170
+ const isDb = !!s.image && /postgres|mysql|mariadb|mongo|redis/i.test(s.image);
171
+ const svc = {
172
+ name,
173
+ label: s.label,
174
+ port: s.port,
175
+ expose: s.expose ?? false,
176
+ setupCommands: s.setup_commands || [],
177
+ volumes: s.volumes || []
178
+ };
179
+ if (s.image != null)
180
+ svc.image = s.image;
181
+ if (s.command != null)
182
+ svc.command = s.command;
183
+ if (s.base_image != null)
184
+ svc.baseImage = s.base_image;
185
+ if (isDb && s.image?.includes("postgres")) {
186
+ svc.dbUser = "app";
187
+ svc.dbPassword = generateDbPassword();
188
+ svc.dbName = "app";
189
+ }
190
+ if (isDb && s.image?.includes("mysql")) {
191
+ svc.dbUser = "app";
192
+ svc.dbPassword = generateDbPassword();
193
+ svc.dbName = "app";
194
+ }
195
+ return svc;
196
+ });
197
+ }
198
+ function resolveVars(input, env) {
199
+ const pattern = /\$\{([^}]+)\}/g;
200
+ return input.replace(pattern, (match, varName) => {
201
+ const value = env[varName];
202
+ if (value === undefined) {
203
+ throw new Error(`Unresolved variable: \${${varName}}`);
204
+ }
205
+ return value;
206
+ });
207
+ }
208
+ function validateRequired(config2) {
209
+ const missing = [];
210
+ if (!config2.repo?.name)
211
+ missing.push("repo.name");
212
+ if (!config2.repo?.clone_url)
213
+ missing.push("repo.clone_url");
214
+ if (!config2.cloudflare?.api_token)
215
+ missing.push("cloudflare.api_token");
216
+ if (!config2.cloudflare?.account_id)
217
+ missing.push("cloudflare.account_id");
218
+ if (!config2.cloudflare?.zone_id)
219
+ missing.push("cloudflare.zone_id");
220
+ if (!config2.cloudflare?.zone)
221
+ missing.push("cloudflare.zone");
222
+ if (!config2.daytona?.api_url)
223
+ missing.push("daytona.api_url");
224
+ if (!config2.daytona?.api_key)
225
+ missing.push("daytona.api_key");
226
+ if (!config2.github?.token)
227
+ missing.push("github.token");
228
+ if (missing.length > 0) {
229
+ throw new Error(`Missing required config fields: ${missing.join(", ")}`);
230
+ }
231
+ }
232
+ function loadEnvFile(path) {
233
+ if (!existsSync2(path))
234
+ return {};
235
+ const content = readFileSync2(path, "utf-8");
236
+ const env = {};
237
+ for (const line of content.split(`
238
+ `)) {
239
+ const trimmed = line.trim();
240
+ if (!trimmed || trimmed.startsWith("#"))
241
+ continue;
242
+ const eqIndex = trimmed.indexOf("=");
243
+ if (eqIndex === -1)
244
+ continue;
245
+ const key = trimmed.slice(0, eqIndex).trim();
246
+ let value = trimmed.slice(eqIndex + 1).trim();
247
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
248
+ value = value.slice(1, -1);
249
+ }
250
+ env[key] = value;
251
+ }
252
+ return env;
253
+ }
254
+
255
+ // src/reporter.ts
256
+ import ora from "ora";
257
+ import chalk from "chalk";
258
+ function createTerminalReporter() {
259
+ let spinner = ora();
260
+ return {
261
+ stepStart: async (name) => {
262
+ spinner = ora(name).start();
263
+ },
264
+ stepComplete: async (name, output) => {
265
+ spinner.succeed(`${name}: ${output}`);
266
+ },
267
+ stepFail: async (name, error) => {
268
+ spinner.fail(chalk.red(`${name}: ${error}`));
269
+ },
270
+ stateChange: async (state) => {
271
+ console.log(chalk.blue(`→ ${state}`));
272
+ }
273
+ };
274
+ }
275
+
276
+ // ../core/src/services/daytona-types.ts
277
+ var DEFAULT_SNAPSHOT = {
278
+ name: SNAPSHOT_NAME,
279
+ image: "docker:28.3.3-dind",
280
+ cpuCount: 2,
281
+ memoryMB: 4096,
282
+ diskGB: 8,
283
+ entrypoint: "dockerd-entrypoint.sh"
284
+ };
285
+
286
+ // ../core/src/services/daytona.ts
287
+ class DaytonaClient {
288
+ baseUrl;
289
+ apiKey;
290
+ target;
291
+ constructor(baseUrl, apiKey, target) {
292
+ this.baseUrl = baseUrl;
293
+ this.apiKey = apiKey;
294
+ this.target = target;
295
+ }
296
+ async request(path, opts = {}) {
297
+ const maxRetries = 3;
298
+ const retryableStatuses = [429, 502, 503];
299
+ let lastErr;
300
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
301
+ try {
302
+ const res = await fetch(`${this.baseUrl}${path}`, {
303
+ ...opts,
304
+ headers: {
305
+ authorization: `Bearer ${this.apiKey}`,
306
+ "content-type": "application/json",
307
+ ...opts.headers
308
+ }
309
+ });
310
+ if (!res.ok) {
311
+ if (attempt < maxRetries && retryableStatuses.includes(res.status)) {
312
+ await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
313
+ continue;
314
+ }
315
+ const body = await res.text().catch(() => "");
316
+ throw new Error(`Daytona ${opts.method || "GET"} ${path}: ${res.status} ${body}`);
317
+ }
318
+ const text = await res.text();
319
+ if (!text)
320
+ return null;
321
+ return JSON.parse(text);
322
+ } catch (err) {
323
+ if (attempt < maxRetries && err instanceof TypeError) {
324
+ lastErr = err;
325
+ await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
326
+ continue;
327
+ }
328
+ throw err;
329
+ }
330
+ }
331
+ throw lastErr ?? new Error(`Daytona ${opts.method || "GET"} ${path}: max retries exceeded`);
332
+ }
333
+ async getSnapshot(name) {
334
+ return this.request(`/snapshots/${name}`);
335
+ }
336
+ async createSnapshot(config2) {
337
+ return this.request("/snapshots", {
338
+ method: "POST",
339
+ body: JSON.stringify({
340
+ name: config2.name,
341
+ image: config2.image,
342
+ cpu: config2.cpuCount,
343
+ memory: config2.memoryMB,
344
+ disk: config2.diskGB,
345
+ entrypoint: config2.entrypoint,
346
+ ...this.target ? { regionId: this.target } : {}
347
+ })
348
+ });
349
+ }
350
+ async ensureSnapshot() {
351
+ try {
352
+ const snap = await this.getSnapshot(DEFAULT_SNAPSHOT.name);
353
+ if (snap && snap.state === "active")
354
+ return;
355
+ } catch {}
356
+ await this.createSnapshot(DEFAULT_SNAPSHOT);
357
+ }
358
+ async createSandbox(opts) {
359
+ const body = {
360
+ snapshot: DEFAULT_SNAPSHOT.name,
361
+ labels: { app: opts.label },
362
+ autoStopInterval: opts.autoStopInterval ?? 5,
363
+ public: false
364
+ };
365
+ if (this.target)
366
+ body.target = this.target;
367
+ if (opts.cpu != null)
368
+ body.cpu = opts.cpu;
369
+ if (opts.memory != null)
370
+ body.memory = opts.memory;
371
+ if (opts.disk != null)
372
+ body.disk = opts.disk;
373
+ return this.request("/sandbox", {
374
+ method: "POST",
375
+ body: JSON.stringify(body)
376
+ });
377
+ }
378
+ async getSandbox(id) {
379
+ return this.request(`/sandbox/${id}`);
380
+ }
381
+ async startSandbox(id) {
382
+ return this.request(`/sandbox/${id}/start`, { method: "POST", body: "{}" });
383
+ }
384
+ async stopSandbox(id) {
385
+ return this.request(`/sandbox/${id}/stop`, { method: "POST", body: "{}" });
386
+ }
387
+ async deleteSandbox(id) {
388
+ return this.request(`/sandbox/${id}`, { method: "DELETE" });
389
+ }
390
+ async listSandboxes(labels) {
391
+ const params = new URLSearchParams({ labels: JSON.stringify(labels) });
392
+ return this.request(`/sandbox?${params}`);
393
+ }
394
+ async exec(sandboxId, command, timeout = 60000) {
395
+ return this.request(`/toolbox/${sandboxId}/toolbox/process/execute`, {
396
+ method: "POST",
397
+ body: JSON.stringify({ command, timeout })
398
+ });
399
+ }
400
+ async gitClone(sandboxId, url, path, branch, username, password) {
401
+ return this.request(`/toolbox/${sandboxId}/toolbox/git/clone`, {
402
+ method: "POST",
403
+ body: JSON.stringify({ url, path, branch, username, password })
404
+ });
405
+ }
406
+ async uploadFile(sandboxId, filePath, content) {
407
+ const formData = new FormData;
408
+ formData.append("file", new Blob([content]), "file");
409
+ const res = await fetch(`${this.baseUrl}/toolbox/${sandboxId}/toolbox/files/upload?path=${encodeURIComponent(filePath)}`, {
410
+ method: "POST",
411
+ headers: { authorization: `Bearer ${this.apiKey}` },
412
+ body: formData
413
+ });
414
+ if (!res.ok)
415
+ throw new Error(`Daytona upload ${filePath}: ${res.status}`);
416
+ }
417
+ }
418
+
419
+ // ../core/src/services/cloudflare.ts
420
+ var API = "https://api.cloudflare.com/client/v4";
421
+
422
+ class CloudflareClient {
423
+ config;
424
+ constructor(config2) {
425
+ this.config = config2;
426
+ }
427
+ async request(path, opts = {}) {
428
+ const res = await fetch(`${API}${path}`, {
429
+ ...opts,
430
+ headers: {
431
+ authorization: `Bearer ${this.config.apiToken}`,
432
+ "content-type": "application/json",
433
+ ...opts.headers
434
+ }
435
+ });
436
+ const body = await res.json();
437
+ if (!body.success) {
438
+ const err = new Error(`Cloudflare API ${opts.method || "GET"} ${path}: ${JSON.stringify(body.errors)}`);
439
+ err.status = res.status;
440
+ throw err;
441
+ }
442
+ return body.result;
443
+ }
444
+ async listTunnels(name) {
445
+ return this.request(`/accounts/${this.config.accountId}/cfd_tunnel?name=${encodeURIComponent(name)}&is_deleted=false`);
446
+ }
447
+ async getTunnelToken(tunnelId) {
448
+ return this.request(`/accounts/${this.config.accountId}/cfd_tunnel/${tunnelId}/token`);
449
+ }
450
+ async createTunnel(name) {
451
+ const existing = await this.listTunnels(name);
452
+ const first = existing[0];
453
+ if (first) {
454
+ const token = await this.getTunnelToken(first.id);
455
+ return { id: first.id, token };
456
+ }
457
+ const secret = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))));
458
+ const result = await this.request(`/accounts/${this.config.accountId}/cfd_tunnel`, {
459
+ method: "POST",
460
+ body: JSON.stringify({ name, tunnel_secret: secret })
461
+ });
462
+ return { id: result.id, token: result.token };
463
+ }
464
+ async cleanTunnelConnections(tunnelId) {
465
+ await this.request(`/accounts/${this.config.accountId}/cfd_tunnel/${tunnelId}/connections`, {
466
+ method: "DELETE"
467
+ }).catch((e) => {
468
+ if (e.status !== 404)
469
+ throw e;
470
+ });
471
+ }
472
+ async deleteTunnel(tunnelId) {
473
+ await this.cleanTunnelConnections(tunnelId);
474
+ await this.request(`/accounts/${this.config.accountId}/cfd_tunnel/${tunnelId}`, {
475
+ method: "DELETE",
476
+ body: JSON.stringify({})
477
+ }).catch((e) => {
478
+ if (e.status !== 404)
479
+ throw e;
480
+ });
481
+ }
482
+ async updateTunnelConfig(tunnelId, ingress) {
483
+ await this.request(`/accounts/${this.config.accountId}/cfd_tunnel/${tunnelId}/configurations`, {
484
+ method: "PUT",
485
+ body: JSON.stringify({ config: { ingress } })
486
+ });
487
+ }
488
+ async createDnsCname(hostname, tunnelId) {
489
+ const existing = await this.listDnsRecords(hostname);
490
+ const cname = existing.find((r) => r.type === "CNAME");
491
+ if (cname)
492
+ return cname.id;
493
+ const result = await this.request(`/zones/${this.config.zoneId}/dns_records`, {
494
+ method: "POST",
495
+ body: JSON.stringify({
496
+ type: "CNAME",
497
+ name: hostname,
498
+ content: `${tunnelId}.cfargotunnel.com`,
499
+ proxied: true
500
+ })
501
+ });
502
+ return result.id;
503
+ }
504
+ async deleteDnsRecord(recordId) {
505
+ await this.request(`/zones/${this.config.zoneId}/dns_records/${recordId}`, {
506
+ method: "DELETE"
507
+ }).catch((e) => {
508
+ if (e.status !== 404)
509
+ throw e;
510
+ });
511
+ }
512
+ async listDnsRecords(name) {
513
+ const params = name ? `?name=${encodeURIComponent(name)}` : "";
514
+ return this.request(`/zones/${this.config.zoneId}/dns_records${params}`);
515
+ }
516
+ async deployWorker(name, script, bindings) {
517
+ const metadata = JSON.stringify({
518
+ main_module: "worker.js",
519
+ bindings: bindings.map((b) => ({ ...b })),
520
+ compatibility_date: "2025-04-04",
521
+ compatibility_flags: ["nodejs_compat"]
522
+ });
523
+ const form = new FormData;
524
+ form.append("worker.js", new Blob([script], { type: "application/javascript+module" }), "worker.js");
525
+ form.append("metadata", new Blob([metadata], { type: "application/json" }), "metadata.json");
526
+ const res = await fetch(`${API}/accounts/${this.config.accountId}/workers/scripts/${name}`, {
527
+ method: "PUT",
528
+ headers: { authorization: `Bearer ${this.config.apiToken}` },
529
+ body: form
530
+ });
531
+ if (!res.ok) {
532
+ const body = await res.text();
533
+ throw new Error(`Cloudflare deploy worker ${name}: ${res.status} ${body}`);
534
+ }
535
+ }
536
+ async getWorkerBindings(name) {
537
+ try {
538
+ const result = await this.request(`/accounts/${this.config.accountId}/workers/scripts/${name}/settings`);
539
+ return result.bindings || [];
540
+ } catch (e) {
541
+ if (e.status === 404)
542
+ return [];
543
+ throw e;
544
+ }
545
+ }
546
+ async deleteWorker(name) {
547
+ await this.request(`/accounts/${this.config.accountId}/workers/scripts/${name}`, {
548
+ method: "DELETE"
549
+ }).catch((e) => {
550
+ if (e.status !== 404)
551
+ throw e;
552
+ });
553
+ }
554
+ async listWorkerRoutes() {
555
+ return this.request(`/zones/${this.config.zoneId}/workers/routes`);
556
+ }
557
+ async setWorkerRoute(pattern, scriptName) {
558
+ const existing = await this.listWorkerRoutes();
559
+ const match = existing.find((r) => r.pattern === pattern);
560
+ if (match)
561
+ return match.id;
562
+ const result = await this.request(`/zones/${this.config.zoneId}/workers/routes`, {
563
+ method: "POST",
564
+ body: JSON.stringify({ pattern, script: scriptName })
565
+ });
566
+ return result.id;
567
+ }
568
+ async deleteWorkerRoute(routeId) {
569
+ await this.request(`/zones/${this.config.zoneId}/workers/routes/${routeId}`, {
570
+ method: "DELETE"
571
+ }).catch((e) => {
572
+ if (e.status !== 404)
573
+ throw e;
574
+ });
575
+ }
576
+ }
577
+
578
+ // ../core/src/provisioner/create-sandbox.ts
579
+ async function createSandbox(ctx) {
580
+ const label = sandboxLabel(ctx.config.sandbox.slug);
581
+ await ctx.daytona.ensureSnapshot();
582
+ if (ctx.config.sandbox.daytonaSandboxId) {
583
+ try {
584
+ const existing = await ctx.daytona.getSandbox(ctx.config.sandbox.daytonaSandboxId);
585
+ if (existing) {
586
+ if (existing.state === "stopped") {
587
+ await ctx.daytona.startSandbox(existing.id);
588
+ }
589
+ return { success: true, output: `Reusing sandbox ${existing.id} (${existing.state})` };
590
+ }
591
+ } catch {}
592
+ }
593
+ const found = await ctx.daytona.listSandboxes({ app: label });
594
+ const first = found[0];
595
+ if (first) {
596
+ ctx.config.sandbox.daytonaSandboxId = first.id;
597
+ if (first.state === "stopped") {
598
+ await ctx.daytona.startSandbox(first.id);
599
+ }
600
+ return { success: true, output: `Recovered sandbox ${first.id} by label (${first.state})` };
601
+ }
602
+ const opts = { label };
603
+ if (ctx.config.sandbox.cpu != null)
604
+ opts.cpu = ctx.config.sandbox.cpu;
605
+ if (ctx.config.sandbox.memory != null)
606
+ opts.memory = ctx.config.sandbox.memory;
607
+ if (ctx.config.sandbox.autoStopInterval != null)
608
+ opts.autoStopInterval = ctx.config.sandbox.autoStopInterval;
609
+ const created = await ctx.daytona.createSandbox(opts);
610
+ ctx.config.sandbox.daytonaSandboxId = created.id;
611
+ return { success: true, output: `Created sandbox ${created.id}` };
612
+ }
613
+
614
+ // ../core/src/provisioner/wait-dockerd.ts
615
+ var POLL_INTERVAL = 2000;
616
+ var TIMEOUT = 120000;
617
+ async function waitDockerd(ctx) {
618
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
619
+ const start = Date.now();
620
+ while (Date.now() - start < TIMEOUT) {
621
+ try {
622
+ const result = await ctx.daytona.exec(sandboxId, "sh -c 'docker info > /dev/null 2>&1 && echo ready'", 1e4);
623
+ if (result.result?.includes("ready"))
624
+ return { success: true, output: "Docker daemon ready" };
625
+ } catch {}
626
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
627
+ }
628
+ return { success: false, error: `Docker daemon not ready after ${TIMEOUT / 1000}s` };
629
+ }
630
+
631
+ // ../core/src/provisioner/clone-repo.ts
632
+ function shellEscape(value) {
633
+ return "'" + value.replace(/'/g, "'\\''") + "'";
634
+ }
635
+ async function cloneRepo(ctx) {
636
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
637
+ const { cloneUrl, branch } = ctx.config.repo;
638
+ const check = await ctx.daytona.exec(sandboxId, `sh -c ${shellEscape(`test -d ${APP_PATH}/.git && echo exists || echo missing`)}`);
639
+ if (check.result?.includes("exists")) {
640
+ const authedUrl = cloneUrl.replace("https://", `https://x-access-token:${ctx.config.githubToken}@`);
641
+ const result = await ctx.daytona.exec(sandboxId, `sh -c ${shellEscape(`cd ${APP_PATH} && git remote set-url origin ${authedUrl} && git fetch origin && git reset --hard origin/${branch}`)}`, 120000);
642
+ if (result.exitCode !== 0)
643
+ return { success: false, error: `Git fetch failed: ${result.result}` };
644
+ return { success: true, output: `Fetched + reset to origin/${branch}` };
645
+ }
646
+ await ctx.daytona.gitClone(sandboxId, cloneUrl, APP_PATH, branch, "x-access-token", ctx.config.githubToken);
647
+ return { success: true, output: `Cloned ${ctx.config.repo.name} at ${branch}` };
648
+ }
649
+
650
+ // ../core/src/provisioner/install-claude.ts
651
+ var TIMEOUT2 = 300000;
652
+ async function installClaude(ctx) {
653
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
654
+ const result = await ctx.daytona.exec(sandboxId, "sh -c 'which claude >/dev/null 2>&1 || (apk add --no-cache nodejs npm && npm install -g @anthropic-ai/claude-code)'", TIMEOUT2);
655
+ if (result.exitCode !== 0)
656
+ return { success: false, error: `Install failed: ${result.result}` };
657
+ return { success: true, output: result.result?.includes("already") ? "Claude CLI already installed" : "Installed Node.js + Claude CLI" };
658
+ }
659
+
660
+ // ../core/src/provisioner/create-tunnel.ts
661
+ async function createTunnel(ctx) {
662
+ const { config: config2, cloudflare: cf } = ctx;
663
+ const slug = config2.sandbox.slug;
664
+ const zone = config2.cloudflare.zone;
665
+ const tunnel = await cf.createTunnel(tunnelName(slug));
666
+ config2.sandbox.tunnelId = tunnel.id;
667
+ config2.sandbox.tunnelToken = tunnel.token;
668
+ const exposedServices = config2.services.filter((s) => s.expose && s.port);
669
+ const ingress = [];
670
+ for (let i = 0;i < exposedServices.length; i++) {
671
+ const svc = exposedServices[i];
672
+ ingress.push({ hostname: serviceFqdn(svc.name, i, slug, zone), service: `http://${svc.name}:${svc.port}` });
673
+ }
674
+ if (!exposedServices.length) {
675
+ ingress.push({ hostname: sandboxHostname(slug, zone), service: "http_status:502" });
676
+ }
677
+ ingress.push({ service: "http_status:404" });
678
+ await cf.updateTunnelConfig(tunnel.id, ingress);
679
+ const hostnames = ingress.filter((r) => ("hostname" in r)).map((r) => r.hostname);
680
+ for (const hostname of hostnames) {
681
+ await cf.createDnsCname(hostname, tunnel.id);
682
+ }
683
+ return {
684
+ success: true,
685
+ output: `Tunnel ${tunnel.id} with ${hostnames.length} hostname(s): ${hostnames.join(", ")}`
686
+ };
687
+ }
688
+
689
+ // ../core/src/services/compose-generator.ts
690
+ function generateDockerfile(svc) {
691
+ const base = svc.baseImage || "ubuntu:24.04";
692
+ const setupCommands = Array.isArray(svc.setupCommands) ? svc.setupCommands : JSON.parse(svc.setupCommands || "[]");
693
+ const systemPattern = /^(apt-get|apt |apk |yum |dnf |pacman )/;
694
+ const systemCmds = [];
695
+ const appCmds = [];
696
+ for (const cmd of setupCommands) {
697
+ if (systemPattern.test(cmd.trim())) {
698
+ systemCmds.push(cmd);
699
+ } else {
700
+ appCmds.push(cmd);
701
+ }
702
+ }
703
+ const lines = [`FROM ${base}`, "WORKDIR /app"];
704
+ for (const cmd of systemCmds) {
705
+ lines.push(`RUN ${cmd}`);
706
+ }
707
+ return { dockerfile: lines.join(`
708
+ `) + `
709
+ `, appCommands: appCmds };
710
+ }
711
+ var DB_ENV = {
712
+ postgres: (svc) => ({
713
+ POSTGRES_USER: svc.dbUser || "postgres",
714
+ POSTGRES_PASSWORD: svc.dbPassword || "",
715
+ POSTGRES_DB: svc.dbName || "app"
716
+ }),
717
+ mysql: (svc) => ({
718
+ MYSQL_USER: svc.dbUser || "app",
719
+ MYSQL_PASSWORD: svc.dbPassword || "",
720
+ MYSQL_DATABASE: svc.dbName || "app",
721
+ MYSQL_ROOT_PASSWORD: svc.dbPassword || ""
722
+ }),
723
+ mariadb: (svc) => ({
724
+ MYSQL_USER: svc.dbUser || "app",
725
+ MYSQL_PASSWORD: svc.dbPassword || "",
726
+ MYSQL_DATABASE: svc.dbName || "app",
727
+ MYSQL_ROOT_PASSWORD: svc.dbPassword || ""
728
+ })
729
+ };
730
+ function isImageService(svc) {
731
+ return !!svc.image;
732
+ }
733
+ function dbEnvForImage(svc) {
734
+ if (!svc.image)
735
+ return {};
736
+ const match = Object.keys(DB_ENV).find((k) => svc.image.startsWith(k));
737
+ const factory = match ? DB_ENV[match] : undefined;
738
+ return factory ? factory(svc) : {};
739
+ }
740
+ function volumeName(path) {
741
+ return path.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
742
+ }
743
+ function yamlQuote(val) {
744
+ if (!val || /[:{}\[\],&#*?|>!%@`'"]/.test(val) || /^(true|false|yes|no|null|~)$/i.test(val)) {
745
+ return `"${val.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
746
+ }
747
+ return val;
748
+ }
749
+ function generateComposeWithFiles(services, envVars, tunnelToken) {
750
+ const lines = [];
751
+ const imageServices = services.filter(isImageService);
752
+ const commandServices = services.filter((s) => !isImageService(s) && s.command);
753
+ const allVolumes = [];
754
+ const dockerfiles = [];
755
+ lines.push("services:");
756
+ for (const svc of imageServices) {
757
+ lines.push(` ${svc.name}:`);
758
+ lines.push(` image: ${svc.image}`);
759
+ const env = dbEnvForImage(svc);
760
+ for (const ev of envVars) {
761
+ if ((ev.environment === "shared" || ev.environment === "sandbox") && env[ev.key] !== undefined) {
762
+ env[ev.key] = ev.value;
763
+ }
764
+ }
765
+ if (Object.keys(env).length) {
766
+ lines.push(" environment:");
767
+ for (const [k, v] of Object.entries(env)) {
768
+ lines.push(` ${k}: ${yamlQuote(v)}`);
769
+ }
770
+ }
771
+ if (svc.port) {
772
+ lines.push(" ports:");
773
+ lines.push(` - "${svc.port}:${svc.port}"`);
774
+ }
775
+ lines.push(" restart: unless-stopped");
776
+ lines.push("");
777
+ }
778
+ for (const svc of commandServices) {
779
+ const setupCmds = Array.isArray(svc.setupCommands) ? svc.setupCommands : JSON.parse(svc.setupCommands || "[]");
780
+ const hasSetup = setupCmds.length > 0;
781
+ lines.push(` ${svc.name}:`);
782
+ if (hasSetup) {
783
+ const { dockerfile, appCommands } = generateDockerfile(svc);
784
+ const dockerfileName = `Dockerfile.${svc.name}`;
785
+ dockerfiles.push({ path: dockerfileName, content: dockerfile, appCommands });
786
+ lines.push(` build:`);
787
+ lines.push(` context: .`);
788
+ lines.push(` dockerfile: ${dockerfileName}`);
789
+ } else if (svc.baseImage) {
790
+ lines.push(` image: ${svc.baseImage}`);
791
+ }
792
+ lines.push(` command: ${yamlQuote(svc.command)}`);
793
+ lines.push(" working_dir: /app");
794
+ lines.push(" stdin_open: true");
795
+ lines.push(" tty: true");
796
+ const svcVolumes = Array.isArray(svc.volumes) ? svc.volumes : JSON.parse(svc.volumes || "[]");
797
+ lines.push(" volumes:");
798
+ lines.push(" - .:/app");
799
+ for (const v of svcVolumes) {
800
+ const vn = volumeName(v);
801
+ lines.push(` - ${vn}:${v}`);
802
+ allVolumes.push(vn);
803
+ }
804
+ const env = {};
805
+ for (const imgSvc of imageServices) {
806
+ const dbEnv = dbEnvForImage(imgSvc);
807
+ for (const ev of envVars) {
808
+ if ((ev.environment === "shared" || ev.environment === "sandbox") && dbEnv[ev.key] !== undefined) {
809
+ dbEnv[ev.key] = ev.value;
810
+ }
811
+ }
812
+ if (dbEnv.POSTGRES_USER) {
813
+ env.DATABASE_URL = `postgres://${dbEnv.POSTGRES_USER}:${dbEnv.POSTGRES_PASSWORD}@${imgSvc.name}:5432/${dbEnv.POSTGRES_DB}`;
814
+ env.POSTGRES_HOST = imgSvc.name;
815
+ Object.assign(env, dbEnv);
816
+ }
817
+ if (dbEnv.MYSQL_USER) {
818
+ env.DATABASE_URL = `mysql://${dbEnv.MYSQL_USER}:${dbEnv.MYSQL_PASSWORD}@${imgSvc.name}:3306/${dbEnv.MYSQL_DATABASE}`;
819
+ env.MYSQL_HOST = imgSvc.name;
820
+ Object.assign(env, dbEnv);
821
+ }
822
+ }
823
+ for (const ev of envVars) {
824
+ if (ev.environment === "shared" || ev.environment === "sandbox") {
825
+ env[ev.key] = ev.value;
826
+ }
827
+ }
828
+ if (Object.keys(env).length) {
829
+ lines.push(" environment:");
830
+ for (const [k, v] of Object.entries(env)) {
831
+ lines.push(` ${k}: ${yamlQuote(v)}`);
832
+ }
833
+ }
834
+ if (svc.port && svc.expose) {
835
+ lines.push(" ports:");
836
+ lines.push(` - "${svc.port}:${svc.port}"`);
837
+ }
838
+ if (imageServices.length) {
839
+ lines.push(" depends_on:");
840
+ for (const dep of imageServices) {
841
+ lines.push(` - ${dep.name}`);
842
+ }
843
+ }
844
+ lines.push(" restart: unless-stopped");
845
+ lines.push("");
846
+ }
847
+ if (tunnelToken) {
848
+ lines.push(" tunnel:");
849
+ lines.push(" image: cloudflare/cloudflared:latest");
850
+ lines.push(` command: tunnel --protocol http2 run --token ${tunnelToken}`);
851
+ lines.push(" restart: unless-stopped");
852
+ lines.push("");
853
+ }
854
+ if (allVolumes.length) {
855
+ lines.push("volumes:");
856
+ for (const v of [...new Set(allVolumes)]) {
857
+ lines.push(` ${v}:`);
858
+ }
859
+ }
860
+ return { compose: lines.join(`
861
+ `) + `
862
+ `, dockerfiles };
863
+ }
864
+
865
+ // ../core/src/provisioner/generate-compose.ts
866
+ async function generateComposeStep(ctx) {
867
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
868
+ const { compose, dockerfiles } = generateComposeWithFiles(ctx.config.services, ctx.config.envVars, ctx.config.sandbox.tunnelToken ?? undefined);
869
+ await ctx.daytona.uploadFile(sandboxId, `${APP_PATH}/docker-compose.yml`, compose);
870
+ for (const df of dockerfiles) {
871
+ await ctx.daytona.uploadFile(sandboxId, `${APP_PATH}/${df.path}`, df.content);
872
+ }
873
+ return { success: true, output: `Uploaded docker-compose.yml + ${dockerfiles.length} Dockerfile(s)` };
874
+ }
875
+
876
+ // ../core/src/provisioner/run-setup.ts
877
+ function shellEscape2(value) {
878
+ return "'" + value.replace(/'/g, "'\\''") + "'";
879
+ }
880
+ async function runSetup(ctx) {
881
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
882
+ const compose = `docker compose -f ${APP_PATH}/docker-compose.yml`;
883
+ const output = [];
884
+ await ctx.daytona.exec(sandboxId, `${compose} pull --quiet --ignore-buildable`, 120000);
885
+ const commandServices = ctx.config.services.filter((s) => !s.image && s.command);
886
+ const setupInfo = commandServices.filter((svc) => {
887
+ const cmds = Array.isArray(svc.setupCommands) ? svc.setupCommands : JSON.parse(svc.setupCommands || "[]");
888
+ return cmds.length > 0;
889
+ }).map((svc) => {
890
+ const { appCommands } = generateDockerfile(svc);
891
+ return { name: svc.name, appCommands };
892
+ });
893
+ if (setupInfo.length) {
894
+ const result = await ctx.daytona.exec(sandboxId, `${compose} build`, 600000);
895
+ if (result.exitCode !== 0)
896
+ return { success: false, error: `Build failed: ${result.result}` };
897
+ output.push("Built service images");
898
+ }
899
+ const imageServices = ctx.config.services.filter((s) => s.image);
900
+ if (imageServices.length) {
901
+ const names = imageServices.map((s) => s.name).join(" ");
902
+ const result = await ctx.daytona.exec(sandboxId, `${compose} up -d ${names}`, 120000);
903
+ if (result.exitCode !== 0)
904
+ return { success: false, error: `Failed to start image services: ${result.result}` };
905
+ output.push(`Started ${names}`);
906
+ await new Promise((r) => setTimeout(r, 5000));
907
+ }
908
+ for (const { name, appCommands } of setupInfo) {
909
+ for (const cmd of appCommands) {
910
+ const result = await ctx.daytona.exec(sandboxId, `${compose} run --rm ${name} sh -c ${shellEscape2(cmd)}`, 300000);
911
+ if (result.exitCode !== 0)
912
+ return { success: false, error: `Setup command failed for ${name}: ${cmd}
913
+ ${result.result}` };
914
+ output.push(`${name}: ${cmd}`);
915
+ }
916
+ }
917
+ return { success: true, output: output.join(`
918
+ `) || "No setup needed" };
919
+ }
920
+
921
+ // ../core/src/provisioner/compose-up.ts
922
+ async function composeUp(ctx) {
923
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
924
+ const result = await ctx.daytona.exec(sandboxId, `docker compose -f ${APP_PATH}/docker-compose.yml up -d --remove-orphans`, 120000);
925
+ if (result.exitCode !== 0)
926
+ return { success: false, error: `compose up failed: ${result.result}` };
927
+ return { success: true, output: result.result };
928
+ }
929
+
930
+ // ../core/src/services/cloudflare-types.ts
931
+ function buildWorkerScript() {
932
+ return `
933
+ function parseCookies(h) {
934
+ const c = {};
935
+ if (!h) return c;
936
+ h.split(";").forEach(s => {
937
+ const [n, ...r] = s.trim().split("=");
938
+ if (n) c[n] = r.join("=");
939
+ });
940
+ return c;
941
+ }
942
+
943
+ function wakingPage() {
944
+ return new Response(\`<!DOCTYPE html>
945
+ <html><head>
946
+ <meta charset="utf-8"><meta http-equiv="refresh" content="5">
947
+ <title>Waking up...</title>
948
+ <style>body{font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#fafafa;color:#333}</style>
949
+ </head><body><p>Waking up sandbox&hellip;</p></body></html>\`,
950
+ { status: 503, headers: { "Content-Type": "text/html", "Retry-After": "5" } });
951
+ }
952
+
953
+ export default {
954
+ async fetch(request, env) {
955
+ const url = new URL(request.url);
956
+ const cookies = parseCookies(request.headers.get("Cookie") || "");
957
+ const token = url.searchParams.get("token") || cookies["documentrun_token"];
958
+
959
+ if (!token || token !== env.ACCESS_TOKEN) {
960
+ return new Response("Not Found", { status: 404 });
961
+ }
962
+
963
+ // Token via query param — set cookie and redirect (strips token from URL)
964
+ if (url.searchParams.get("token") && request.headers.get("Upgrade") !== "websocket") {
965
+ url.searchParams.delete("token");
966
+ return new Response(null, {
967
+ status: 302,
968
+ headers: {
969
+ "Location": url.toString(),
970
+ "Set-Cookie": "documentrun_token=" + token + "; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400",
971
+ "Cache-Control": "no-store",
972
+ },
973
+ });
974
+ }
975
+
976
+ let res;
977
+ try {
978
+ res = await fetch(request);
979
+ } catch (_e) {
980
+ if (env.WAKE_URL) {
981
+ try { await fetch(env.WAKE_URL, { method: "POST", headers: { "Authorization": "Bearer " + env.ACCESS_TOKEN } }); } catch (_e2) {}
982
+ return wakingPage();
983
+ }
984
+ return new Response("Service Unavailable", { status: 503 });
985
+ }
986
+
987
+ if (env.WAKE_URL && (res.status === 530 || res.status === 502 || res.status === 503)) {
988
+ try { await fetch(env.WAKE_URL, { method: "POST", headers: { "Authorization": "Bearer " + env.ACCESS_TOKEN } }); } catch (_e) {}
989
+ return wakingPage();
990
+ }
991
+
992
+ if (res.ok && res.headers.get("content-type")?.includes("text/html") && env.CONSOLE_SCRIPT_URL) {
993
+ const config = JSON.stringify({
994
+ sandboxId: env.SANDBOX_ID,
995
+ apiUrl: env.CONSOLE_API_URL,
996
+ });
997
+ return new HTMLRewriter()
998
+ .on("head", {
999
+ element(el) {
1000
+ el.append(
1001
+ \`<script>window.DOCUMENTRUN_CONSOLE_CONFIG = \${config};</script>\` +
1002
+ \`<script type="module" src="\${env.CONSOLE_SCRIPT_URL}"></script>\`,
1003
+ { html: true }
1004
+ );
1005
+ },
1006
+ })
1007
+ .transform(res);
1008
+ }
1009
+
1010
+ return res;
1011
+ }
1012
+ };`;
1013
+ }
1014
+
1015
+ // ../core/src/provisioner/deploy-worker.ts
1016
+ async function deployWorker(ctx) {
1017
+ const { config: config2, cloudflare: cf } = ctx;
1018
+ const slug = config2.sandbox.slug;
1019
+ const zone = config2.cloudflare.zone;
1020
+ const name = workerName(slug);
1021
+ const existingBindings = await cf.getWorkerBindings(name);
1022
+ const existingToken = existingBindings.find((b) => b.name === "ACCESS_TOKEN")?.text;
1023
+ const accessToken = existingToken || config2.sandbox.accessToken;
1024
+ const script = buildWorkerScript();
1025
+ const bindings = [
1026
+ { type: "plain_text", name: "ACCESS_TOKEN", text: accessToken },
1027
+ { type: "plain_text", name: "WAKE_URL", text: wakeUrl(slug, config2.apiUrl) },
1028
+ { type: "plain_text", name: "SANDBOX_ID", text: config2.sandbox.id }
1029
+ ];
1030
+ if (config2.console) {
1031
+ bindings.push({ type: "plain_text", name: "CONSOLE_SCRIPT_URL", text: config2.console.scriptUrl });
1032
+ bindings.push({ type: "plain_text", name: "CONSOLE_API_URL", text: config2.console.apiUrl });
1033
+ }
1034
+ await cf.deployWorker(name, script, bindings);
1035
+ await cf.setWorkerRoute(workerRoute(slug, zone), name);
1036
+ const exposedServices = config2.services.filter((s) => s.expose && s.port);
1037
+ if (exposedServices.length > 1) {
1038
+ await cf.setWorkerRoute(workerServiceRoute(slug, zone), name);
1039
+ }
1040
+ return { success: true, output: `Worker ${name} deployed with ${bindings.length} bindings` };
1041
+ }
1042
+
1043
+ // ../core/src/provisioner/index.ts
1044
+ var PROVISION_STEPS = [
1045
+ { name: "create_sandbox", run: createSandbox },
1046
+ { name: "wait_dockerd", run: waitDockerd },
1047
+ { name: "clone_repo", run: cloneRepo },
1048
+ { name: "install_claude", run: installClaude },
1049
+ { name: "create_tunnel", run: createTunnel },
1050
+ { name: "generate_compose", run: generateComposeStep },
1051
+ { name: "run_setup", run: runSetup },
1052
+ { name: "compose_up", run: composeUp },
1053
+ { name: "deploy_worker", run: deployWorker }
1054
+ ];
1055
+ async function runStep(ctx, step, reporter) {
1056
+ await reporter.stepStart(step.name);
1057
+ let result;
1058
+ try {
1059
+ result = await step.run(ctx);
1060
+ } catch (err) {
1061
+ result = { success: false, error: err instanceof Error ? err.message : String(err) };
1062
+ }
1063
+ if (result.success) {
1064
+ await reporter.stepComplete(step.name, result.output || "ok");
1065
+ } else {
1066
+ await reporter.stepFail(step.name, result.error || `Step ${step.name} failed`);
1067
+ throw new Error(result.error || `Step ${step.name} failed`);
1068
+ }
1069
+ }
1070
+ async function provision(ctx, reporter) {
1071
+ await reporter.stateChange("provisioning");
1072
+ for (const step of PROVISION_STEPS) {
1073
+ await runStep(ctx, step, reporter);
1074
+ }
1075
+ await reporter.stateChange("running");
1076
+ }
1077
+ async function wake(ctx, reporter) {
1078
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
1079
+ if (!sandboxId)
1080
+ throw new Error("No Daytona sandbox ID");
1081
+ await reporter.stateChange("starting");
1082
+ try {
1083
+ const remote = await ctx.daytona.getSandbox(sandboxId);
1084
+ const state = String(remote.state).toLowerCase();
1085
+ if (state === "stopped") {
1086
+ await ctx.daytona.startSandbox(sandboxId);
1087
+ } else if (state === "stopping") {
1088
+ await pollUntilState(ctx, "stopped");
1089
+ await ctx.daytona.startSandbox(sandboxId);
1090
+ } else if (state === "starting") {
1091
+ await pollUntilState(ctx, "started");
1092
+ } else if (state !== "started") {
1093
+ throw new Error(`Cannot wake sandbox in state: ${remote.state}`);
1094
+ }
1095
+ await requireDockerd(ctx);
1096
+ await reporter.stateChange("running");
1097
+ } catch (err) {
1098
+ await reporter.stateChange("failed");
1099
+ throw err;
1100
+ }
1101
+ }
1102
+ async function stop(ctx, reporter) {
1103
+ const sandboxId = ctx.config.sandbox.daytonaSandboxId;
1104
+ if (!sandboxId)
1105
+ return;
1106
+ await reporter.stateChange("stopping");
1107
+ await ctx.daytona.stopSandbox(sandboxId);
1108
+ const remote = await ctx.daytona.getSandbox(sandboxId);
1109
+ await reporter.stateChange(remote.state === "stopped" ? "stopped" : "stopping");
1110
+ }
1111
+ async function deprovision(ctx, reporter) {
1112
+ const { config: config2, cloudflare: cf } = ctx;
1113
+ const slug = config2.sandbox.slug;
1114
+ const zone = config2.cloudflare.zone;
1115
+ const wName = workerName(slug);
1116
+ const routes = await cf.listWorkerRoutes();
1117
+ for (const r of routes) {
1118
+ if (r.script === wName) {
1119
+ await cf.deleteWorkerRoute(r.id);
1120
+ }
1121
+ }
1122
+ await cf.deleteWorker(wName);
1123
+ const allRecords = await cf.listDnsRecords();
1124
+ for (const r of allRecords) {
1125
+ if (isSandboxDnsRecord(r.name, slug, zone)) {
1126
+ await cf.deleteDnsRecord(r.id);
1127
+ }
1128
+ }
1129
+ const tunnels = await cf.listTunnels(tunnelName(slug));
1130
+ for (const t of tunnels) {
1131
+ await cf.deleteTunnel(t.id);
1132
+ }
1133
+ const label = sandboxLabel(slug);
1134
+ const sandboxes = await ctx.daytona.listSandboxes({ app: label });
1135
+ for (const sb of sandboxes) {
1136
+ await ctx.daytona.deleteSandbox(sb.id);
1137
+ }
1138
+ await reporter.stateChange("destroyed");
1139
+ }
1140
+ async function requireDockerd(ctx) {
1141
+ const result = await waitDockerd(ctx);
1142
+ if (!result.success) {
1143
+ throw new Error(result.error || "Docker daemon not ready");
1144
+ }
1145
+ }
1146
+ async function pollUntilState(ctx, target, timeout = 120000) {
1147
+ const start = Date.now();
1148
+ while (Date.now() - start < timeout) {
1149
+ const remote = await ctx.daytona.getSandbox(ctx.config.sandbox.daytonaSandboxId);
1150
+ if (remote.state === target)
1151
+ return;
1152
+ await new Promise((r) => setTimeout(r, 2000));
1153
+ }
1154
+ throw new Error(`Sandbox did not reach ${target} within ${timeout / 1000}s`);
1155
+ }
1156
+
1157
+ // src/local/deploy.ts
1158
+ function deployCommand(program) {
1159
+ program.command("deploy").description("Provision a sandbox from local config").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1160
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1161
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1162
+ const cloudflare = new CloudflareClient(deploy.cloudflare);
1163
+ const reporter = createTerminalReporter();
1164
+ await provision({ config: deploy, daytona, cloudflare }, reporter);
1165
+ console.log(chalk2.green(`
1166
+ ✓ Sandbox ready: ${sandboxUrl(deploy.sandbox.slug, deploy.cloudflare.zone)}`));
1167
+ });
1168
+ }
1169
+
1170
+ // src/local/destroy.ts
1171
+ import chalk3 from "chalk";
1172
+ function destroyCommand(program) {
1173
+ program.command("destroy").description("Destroy a provisioned sandbox").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1174
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1175
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1176
+ const cloudflare = new CloudflareClient(deploy.cloudflare);
1177
+ const reporter = createTerminalReporter();
1178
+ await deprovision({ config: deploy, daytona, cloudflare }, reporter);
1179
+ console.log(chalk3.green(`
1180
+ ✓ Sandbox destroyed`));
1181
+ });
1182
+ }
1183
+
1184
+ // src/local/status.ts
1185
+ import chalk4 from "chalk";
1186
+ function statusCommand(program) {
1187
+ program.command("status").description("Check sandbox status").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1188
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1189
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1190
+ const label = sandboxLabel(deploy.sandbox.slug);
1191
+ const sandboxes = await daytona.listSandboxes({ label });
1192
+ if (sandboxes.length === 0) {
1193
+ console.log(chalk4.yellow("No sandbox deployed"));
1194
+ return;
1195
+ }
1196
+ const sandbox = sandboxes[0];
1197
+ if (!sandbox) {
1198
+ console.log(chalk4.yellow("No sandbox deployed"));
1199
+ return;
1200
+ }
1201
+ const stateColor = sandbox.state === "started" ? chalk4.green : sandbox.state === "stopped" ? chalk4.yellow : chalk4.red;
1202
+ console.log(`Repo: ${chalk4.bold(deploy.repo.name)}`);
1203
+ console.log(`State: ${stateColor(sandbox.state)}`);
1204
+ console.log(`Sandbox: ${sandbox.id}`);
1205
+ console.log(`URL: ${sandboxUrl(deploy.sandbox.slug, deploy.cloudflare.zone)}`);
1206
+ });
1207
+ }
1208
+
1209
+ // src/local/logs.ts
1210
+ import chalk5 from "chalk";
1211
+ function logsCommand(program) {
1212
+ program.command("logs").description("Stream sandbox logs").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").option("-f, --follow", "Follow log output").option("-n, --tail <lines>", "Number of lines to show", "100").action(async (opts) => {
1213
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1214
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1215
+ const label = sandboxLabel(deploy.sandbox.slug);
1216
+ const sandboxes = await daytona.listSandboxes({ label });
1217
+ if (sandboxes.length === 0) {
1218
+ console.log(chalk5.yellow("No sandbox deployed"));
1219
+ return;
1220
+ }
1221
+ const sandbox = sandboxes[0];
1222
+ if (!sandbox) {
1223
+ console.log(chalk5.yellow("No sandbox deployed"));
1224
+ return;
1225
+ }
1226
+ const project = composeProject(deploy.sandbox.slug);
1227
+ const followFlag = opts.follow ? "-f" : "";
1228
+ const result = await daytona.exec(sandbox.id, `cd ${APP_PATH} && docker compose -p ${project} logs --tail ${opts.tail} ${followFlag}`, opts.follow ? 300000 : 30000);
1229
+ if (result.result) {
1230
+ process.stdout.write(result.result);
1231
+ }
1232
+ if (result.exitCode !== 0) {
1233
+ process.exit(result.exitCode);
1234
+ }
1235
+ });
1236
+ }
1237
+
1238
+ // src/local/exec.ts
1239
+ import chalk6 from "chalk";
1240
+ function execCommand(program) {
1241
+ program.command("exec <command...>").description("Run a command in the sandbox").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (args, opts) => {
1242
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1243
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1244
+ const label = sandboxLabel(deploy.sandbox.slug);
1245
+ const sandboxes = await daytona.listSandboxes({ app: label });
1246
+ if (sandboxes.length === 0) {
1247
+ console.error(chalk6.red("No sandbox found"));
1248
+ process.exit(1);
1249
+ }
1250
+ const sandbox = sandboxes[0];
1251
+ if (!sandbox) {
1252
+ console.error(chalk6.red("No sandbox found"));
1253
+ process.exit(1);
1254
+ }
1255
+ const cmd = args.join(" ");
1256
+ const result = await daytona.exec(sandbox.id, cmd, 300000);
1257
+ if (result.result) {
1258
+ process.stdout.write(result.result);
1259
+ }
1260
+ process.exit(result.exitCode);
1261
+ });
1262
+ }
1263
+
1264
+ // src/local/wake.ts
1265
+ import chalk7 from "chalk";
1266
+ function wakeCommand(program) {
1267
+ program.command("wake").description("Wake a stopped sandbox").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1268
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1269
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1270
+ const cloudflare = new CloudflareClient(deploy.cloudflare);
1271
+ const label = sandboxLabel(deploy.sandbox.slug);
1272
+ const sandboxes = await daytona.listSandboxes({ app: label });
1273
+ if (sandboxes.length === 0) {
1274
+ console.log(chalk7.yellow("No sandbox found"));
1275
+ return;
1276
+ }
1277
+ const sandbox = sandboxes[0];
1278
+ if (!sandbox) {
1279
+ console.log(chalk7.yellow("No sandbox found"));
1280
+ return;
1281
+ }
1282
+ deploy.sandbox.daytonaSandboxId = sandbox.id;
1283
+ const reporter = createTerminalReporter();
1284
+ await wake({ config: deploy, daytona, cloudflare }, reporter);
1285
+ console.log(chalk7.green(`
1286
+ ✓ Sandbox awake`));
1287
+ });
1288
+ }
1289
+
1290
+ // src/local/stop.ts
1291
+ import chalk8 from "chalk";
1292
+ function stopCommand(program) {
1293
+ program.command("stop").description("Stop a running sandbox").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1294
+ const { deploy, daytona: daytonaCfg } = loadConfig(opts.config, opts.dir);
1295
+ const daytona = new DaytonaClient(daytonaCfg.apiUrl, daytonaCfg.apiKey, daytonaCfg.target);
1296
+ const cloudflare = new CloudflareClient(deploy.cloudflare);
1297
+ const label = sandboxLabel(deploy.sandbox.slug);
1298
+ const sandboxes = await daytona.listSandboxes({ app: label });
1299
+ if (sandboxes.length === 0) {
1300
+ console.log(chalk8.yellow("No sandbox found"));
1301
+ return;
1302
+ }
1303
+ const sandbox = sandboxes[0];
1304
+ if (!sandbox) {
1305
+ console.log(chalk8.yellow("No sandbox found"));
1306
+ return;
1307
+ }
1308
+ deploy.sandbox.daytonaSandboxId = sandbox.id;
1309
+ const reporter = createTerminalReporter();
1310
+ await stop({ config: deploy, daytona, cloudflare }, reporter);
1311
+ console.log(chalk8.green(`
1312
+ ✓ Sandbox stopped`));
1313
+ });
1314
+ }
1315
+
1316
+ // src/local/url.ts
1317
+ import chalk9 from "chalk";
1318
+ function urlCommand(program) {
1319
+ program.command("url").description("Print sandbox URL with access token").option("--config <path>", "Config file path", "documentrun.yaml").option("--dir <path>", "Base directory for config and env files").action(async (opts) => {
1320
+ const { deploy } = loadConfig(opts.config, opts.dir);
1321
+ const cf = new CloudflareClient(deploy.cloudflare);
1322
+ const name = workerName(deploy.sandbox.slug);
1323
+ const bindings = await cf.getWorkerBindings(name);
1324
+ const token = bindings.find((b) => b.name === "ACCESS_TOKEN")?.text;
1325
+ if (!token) {
1326
+ console.error(chalk9.yellow("No sandbox deployed. Run `dr deploy` first."));
1327
+ process.exit(1);
1328
+ }
1329
+ console.log(`${sandboxUrl(deploy.sandbox.slug, deploy.cloudflare.zone)}?token=${token}`);
1330
+ });
1331
+ }
1332
+
1333
+ // src/cloud/logs.ts
1334
+ import chalk10 from "chalk";
1335
+ var WORKER_NAME = "document-run-api-production";
1336
+ function workerLogsCommand(program) {
1337
+ program.command("worker-logs").description("Stream live API worker logs").action(async () => {
1338
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
1339
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
1340
+ if (!accountId || !apiToken) {
1341
+ console.error(chalk10.red("Missing CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_API_TOKEN in environment."));
1342
+ console.error(chalk10.dim("Run via: ./bin/dev bun packages/cli/src/index.ts worker-logs"));
1343
+ process.exit(1);
1344
+ }
1345
+ const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${WORKER_NAME}/tails`, {
1346
+ method: "POST",
1347
+ headers: { authorization: `Bearer ${apiToken}`, "content-type": "application/json" },
1348
+ body: "{}"
1349
+ });
1350
+ const data = await res.json();
1351
+ if (!data.success || !data.result) {
1352
+ console.error(chalk10.red("Failed to create tail session"));
1353
+ process.exit(1);
1354
+ }
1355
+ console.log(chalk10.blue(`Streaming logs from ${WORKER_NAME}...`));
1356
+ console.log(chalk10.dim(`Expires: ${data.result.expires_at}`));
1357
+ console.log();
1358
+ const ws = new WebSocket(data.result.url);
1359
+ ws.onmessage = (e) => {
1360
+ try {
1361
+ const event = JSON.parse(e.data);
1362
+ const req = event.event?.request;
1363
+ const resp = event.event?.response;
1364
+ if (req?.method && req?.url) {
1365
+ const path = new URL(req.url).pathname;
1366
+ const status = resp?.status || "—";
1367
+ const statusColor = (resp?.status || 0) >= 400 ? chalk10.red : chalk10.green;
1368
+ console.log(`${chalk10.bold(req.method)} ${path} ${statusColor(String(status))}`);
1369
+ }
1370
+ for (const log of event.logs || []) {
1371
+ if (log.message?.length) {
1372
+ console.log(chalk10.dim(` ${log.message.join(" ")}`));
1373
+ }
1374
+ }
1375
+ for (const ex of event.exceptions || []) {
1376
+ console.log(chalk10.red(` ${ex.name}: ${ex.message}`));
1377
+ }
1378
+ } catch {}
1379
+ };
1380
+ ws.onclose = () => {
1381
+ console.log(chalk10.yellow(`
1382
+ Log stream closed.`));
1383
+ process.exit(0);
1384
+ };
1385
+ ws.onerror = () => {
1386
+ console.error(chalk10.red("WebSocket error"));
1387
+ process.exit(1);
1388
+ };
1389
+ await new Promise(() => {});
1390
+ });
1391
+ }
1392
+
1393
+ // src/local/app.ts
1394
+ function createLocalApp() {
1395
+ const program = new Command;
1396
+ program.name("dr").description("Document Run CLI (local mode)").version("0.0.1");
1397
+ deployCommand(program);
1398
+ destroyCommand(program);
1399
+ statusCommand(program);
1400
+ logsCommand(program);
1401
+ execCommand(program);
1402
+ wakeCommand(program);
1403
+ stopCommand(program);
1404
+ urlCommand(program);
1405
+ workerLogsCommand(program);
1406
+ return program;
1407
+ }
1408
+
1409
+ // src/cloud/app.ts
1410
+ import { Command as Command2 } from "commander";
1411
+
1412
+ // src/cloud/auth.ts
1413
+ import chalk11 from "chalk";
1414
+
1415
+ // src/cloud/client.ts
1416
+ class ApiClient {
1417
+ baseUrl;
1418
+ token;
1419
+ workspaceId;
1420
+ constructor(baseUrl, token, workspaceId) {
1421
+ this.baseUrl = baseUrl;
1422
+ this.token = token;
1423
+ this.workspaceId = workspaceId;
1424
+ }
1425
+ async request(method, path, body) {
1426
+ const headers = {
1427
+ "content-type": "application/json",
1428
+ authorization: `Bearer ${this.token}`
1429
+ };
1430
+ if (this.workspaceId)
1431
+ headers["x-workspace-id"] = this.workspaceId;
1432
+ const res = await fetch(`${this.baseUrl}${path}`, {
1433
+ method,
1434
+ headers,
1435
+ body: body ? JSON.stringify(body) : undefined
1436
+ });
1437
+ if (!res.ok) {
1438
+ const text = await res.text().catch(() => "");
1439
+ throw new Error(`API ${method} ${path}: ${res.status} ${text}`);
1440
+ }
1441
+ if (res.status === 204)
1442
+ return;
1443
+ return res.json();
1444
+ }
1445
+ async get(path) {
1446
+ return this.request("GET", path);
1447
+ }
1448
+ async post(path, body) {
1449
+ return this.request("POST", path, body);
1450
+ }
1451
+ async put(path, body) {
1452
+ return this.request("PUT", path, body);
1453
+ }
1454
+ async del(path) {
1455
+ return this.request("DELETE", path);
1456
+ }
1457
+ streamWebSocket(path, onMessage) {
1458
+ return new Promise((resolve2, reject) => {
1459
+ const protocol = this.baseUrl.startsWith("https") ? "wss" : "ws";
1460
+ const host = this.baseUrl.replace(/^https?:\/\//, "");
1461
+ const ws = new WebSocket(`${protocol}://${host}${path}`);
1462
+ ws.onmessage = (e) => {
1463
+ try {
1464
+ const event = JSON.parse(e.data);
1465
+ onMessage(event);
1466
+ if (event.type === "state_change" && (event.state === "running" || event.state === "failed")) {
1467
+ ws.close();
1468
+ }
1469
+ } catch {}
1470
+ };
1471
+ ws.onclose = () => resolve2();
1472
+ ws.onerror = () => reject(new Error("WebSocket connection failed"));
1473
+ });
1474
+ }
1475
+ }
1476
+
1477
+ // src/cloud/auth.ts
1478
+ function loginCommand(program) {
1479
+ program.command("login").description("Log in to Document Run").option("--api-url <url>", "API URL", "https://api.document.run").action(async (opts) => {
1480
+ const apiUrl = opts.apiUrl;
1481
+ const server = Bun.serve({
1482
+ port: 0,
1483
+ async fetch(req) {
1484
+ const url = new URL(req.url);
1485
+ if (url.pathname === "/callback") {
1486
+ const token = url.searchParams.get("token");
1487
+ const workspaceId = url.searchParams.get("workspace_id");
1488
+ if (token) {
1489
+ const settings2 = { token, apiUrl };
1490
+ if (workspaceId)
1491
+ settings2.workspaceId = workspaceId;
1492
+ saveSettings(settings2);
1493
+ setTimeout(() => server.stop(), 100);
1494
+ return new Response("<html><body><h2>Logged in! You can close this tab.</h2></body></html>", {
1495
+ headers: { "content-type": "text/html" }
1496
+ });
1497
+ }
1498
+ return new Response("Missing token", { status: 400 });
1499
+ }
1500
+ return new Response("Not found", { status: 404 });
1501
+ }
1502
+ });
1503
+ const callbackUrl = `http://localhost:${server.port}/callback`;
1504
+ const webUrl = apiUrl.replace("api.", "");
1505
+ const loginUrl = `${webUrl}/auth/login?cli_redirect=${encodeURIComponent(callbackUrl)}`;
1506
+ console.log(chalk11.blue(`Opening browser to log in...`));
1507
+ console.log(chalk11.dim(loginUrl));
1508
+ const proc = Bun.spawn(["open", loginUrl]);
1509
+ await proc.exited;
1510
+ console.log(chalk11.dim("Waiting for login callback..."));
1511
+ await new Promise((resolve2) => {
1512
+ const check = setInterval(() => {
1513
+ if (loadSettings()) {
1514
+ clearInterval(check);
1515
+ resolve2();
1516
+ }
1517
+ }, 500);
1518
+ });
1519
+ const settings = loadSettings();
1520
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1521
+ try {
1522
+ const me = await client.get("/auth/me");
1523
+ console.log(chalk11.green(`
1524
+ ✓ Logged in as ${me.user.name}${me.user.email ? ` (${me.user.email})` : ""}`));
1525
+ } catch {
1526
+ console.log(chalk11.green(`
1527
+ ✓ Logged in`));
1528
+ }
1529
+ });
1530
+ }
1531
+ function logoutCommand(program) {
1532
+ program.command("logout").description("Log out of Document Run").action(() => {
1533
+ clearSettings();
1534
+ console.log(chalk11.green("✓ Logged out"));
1535
+ });
1536
+ }
1537
+ function whoamiCommand(program) {
1538
+ program.command("whoami").description("Show current user").action(async () => {
1539
+ const settings = loadSettings();
1540
+ if (!settings) {
1541
+ console.log(chalk11.yellow("Not logged in. Run `dr login` to authenticate."));
1542
+ return;
1543
+ }
1544
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1545
+ try {
1546
+ const me = await client.get("/auth/me");
1547
+ console.log(`User: ${chalk11.bold(me.user.name)}`);
1548
+ console.log(`Email: ${me.user.email}`);
1549
+ console.log(`Workspace: ${me.workspace.name}`);
1550
+ console.log(`API: ${settings.apiUrl}`);
1551
+ } catch (_err) {
1552
+ console.log(chalk11.red("Session expired. Run `dr login` to re-authenticate."));
1553
+ }
1554
+ });
1555
+ }
1556
+
1557
+ // src/cloud/deploy.ts
1558
+ import chalk12 from "chalk";
1559
+ function deployCommand2(program) {
1560
+ program.command("deploy").description("Deploy a sandbox via API").requiredOption("--repo <slug>", "Repository slug").option("--branch <branch>", "Branch to deploy", "main").action(async (opts) => {
1561
+ const settings = loadSettings();
1562
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1563
+ const reporter = createTerminalReporter();
1564
+ console.log(chalk12.blue("Creating sandbox..."));
1565
+ const { sandbox } = await client.post(`/repos/${opts.repo}/sandboxes`, {
1566
+ branch: opts.branch
1567
+ });
1568
+ console.log(chalk12.dim(`Sandbox: ${sandbox.slug}`));
1569
+ await client.streamWebSocket(`/ws/sandboxes/${sandbox.slug}`, (event) => {
1570
+ switch (event.type) {
1571
+ case "step_start":
1572
+ reporter.stepStart(event.name);
1573
+ break;
1574
+ case "step_complete":
1575
+ reporter.stepComplete(event.name, event.output || "ok");
1576
+ break;
1577
+ case "step_fail":
1578
+ reporter.stepFail(event.name, event.error || "failed");
1579
+ break;
1580
+ case "state_change":
1581
+ reporter.stateChange(event.state);
1582
+ break;
1583
+ }
1584
+ });
1585
+ console.log(chalk12.green(`
1586
+ ✓ Sandbox deployed: ${sandbox.slug}`));
1587
+ });
1588
+ }
1589
+
1590
+ // src/cloud/repos.ts
1591
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
1592
+ import { resolve as resolve2, dirname as dirname2 } from "path";
1593
+ import { parse as parseYaml2 } from "yaml";
1594
+ import chalk13 from "chalk";
1595
+ function loadEnvFile2(path) {
1596
+ if (!existsSync3(path))
1597
+ return [];
1598
+ const content = readFileSync3(path, "utf-8");
1599
+ const vars = [];
1600
+ for (const line of content.split(`
1601
+ `)) {
1602
+ const trimmed = line.trim();
1603
+ if (!trimmed || trimmed.startsWith("#"))
1604
+ continue;
1605
+ const eqIndex = trimmed.indexOf("=");
1606
+ if (eqIndex === -1)
1607
+ continue;
1608
+ const key = trimmed.slice(0, eqIndex).trim();
1609
+ let value = trimmed.slice(eqIndex + 1).trim();
1610
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1611
+ value = value.slice(1, -1);
1612
+ }
1613
+ vars.push({ key, value });
1614
+ }
1615
+ return vars;
1616
+ }
1617
+ function reposCommand(program) {
1618
+ const repos = program.command("repos").description("Manage repositories");
1619
+ repos.command("list").description("List repositories").action(async () => {
1620
+ const settings = loadSettings();
1621
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1622
+ const { repos: list } = await client.get("/repos");
1623
+ if (list.length === 0) {
1624
+ console.log(chalk13.yellow("No repositories"));
1625
+ return;
1626
+ }
1627
+ for (const repo of list) {
1628
+ console.log(` ${chalk13.bold(repo.name)} ${chalk13.dim(repo.slug)}`);
1629
+ }
1630
+ });
1631
+ repos.command("init").description("Initialize a repository (from URL or template)").option("--url <url>", "GitHub repository URL").option("--branch <branch>", "Branch (default: repo default)").option("--template <path>", "Template file (documentrun.yaml)").action(async (opts) => {
1632
+ if (!opts.url && !opts.template) {
1633
+ console.error(chalk13.red("Provide --url or --template"));
1634
+ process.exit(1);
1635
+ }
1636
+ const settings = loadSettings();
1637
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1638
+ if (opts.template) {
1639
+ const fullPath = resolve2(opts.template);
1640
+ if (!existsSync3(fullPath)) {
1641
+ console.error(chalk13.red(`File not found: ${fullPath}`));
1642
+ process.exit(1);
1643
+ }
1644
+ const raw = readFileSync3(fullPath, "utf-8");
1645
+ const config2 = parseYaml2(raw);
1646
+ const envFilePath = resolve2(dirname2(fullPath), config2.env_file || ".env");
1647
+ const envVars = loadEnvFile2(envFilePath);
1648
+ const { repo } = await client.post("/templates/import", {
1649
+ template: config2,
1650
+ envVars
1651
+ });
1652
+ console.log(chalk13.green(`✓ Initialized from template: ${repo.name} (${repo.id})`));
1653
+ } else {
1654
+ const match = opts.url.match(/github\.com\/([^/]+\/[^/]+)/);
1655
+ if (!match) {
1656
+ console.error(chalk13.red("Invalid GitHub URL"));
1657
+ process.exit(1);
1658
+ }
1659
+ const name = match[1].replace(/\.git$/, "");
1660
+ const cloneUrl = `https://github.com/${name}.git`;
1661
+ const { repo } = await client.post("/repos", {
1662
+ name,
1663
+ cloneUrl,
1664
+ branch: opts.branch || "main"
1665
+ });
1666
+ console.log(chalk13.green(`✓ Initialized: ${repo.name} (${repo.id})`));
1667
+ }
1668
+ });
1669
+ repos.command("delete").description("Delete a repository").argument("<id>", "Repository ID").action(async (id) => {
1670
+ const settings = loadSettings();
1671
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1672
+ await client.del(`/repos/${id}`);
1673
+ console.log(chalk13.green(`✓ Deleted repo: ${id}`));
1674
+ });
1675
+ }
1676
+
1677
+ // src/cloud/sandboxes.ts
1678
+ import chalk14 from "chalk";
1679
+ var STATE_COLORS = {
1680
+ running: chalk14.green,
1681
+ failed: chalk14.red,
1682
+ stopped: chalk14.yellow,
1683
+ stopping: chalk14.yellow,
1684
+ starting: chalk14.blue,
1685
+ provisioning: chalk14.blue,
1686
+ pending: chalk14.dim
1687
+ };
1688
+ function sandboxesCommand(program) {
1689
+ const sb = program.command("sandboxes").description("Manage sandboxes");
1690
+ sb.command("list <repo>").description("List sandboxes for a repository").action(async (repo) => {
1691
+ const settings = loadSettings();
1692
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1693
+ const { sandboxes } = await client.get(`/repos/${repo}/sandboxes`);
1694
+ if (sandboxes.length === 0) {
1695
+ console.log(chalk14.yellow("No sandboxes"));
1696
+ return;
1697
+ }
1698
+ for (const s of sandboxes) {
1699
+ const color = STATE_COLORS[s.state] || chalk14.dim;
1700
+ console.log(` ${chalk14.bold(s.slug)} ${color(s.state)} ${chalk14.dim(s.branch)}`);
1701
+ }
1702
+ });
1703
+ sb.command("status <slug>").description("Show sandbox status (live from Daytona)").action(async (slug) => {
1704
+ const settings = loadSettings();
1705
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1706
+ const { sandbox } = await client.get(`/sandboxes/${slug}`);
1707
+ const color = STATE_COLORS[sandbox.state] || chalk14.dim;
1708
+ console.log(`Slug: ${chalk14.bold(sandbox.slug)}`);
1709
+ console.log(`Branch: ${sandbox.branch}`);
1710
+ console.log(`State: ${color(sandbox.state)}`);
1711
+ if (sandbox.lastError)
1712
+ console.log(`Error: ${chalk14.red(sandbox.lastError)}`);
1713
+ });
1714
+ sb.command("url <slug>").description("Print sandbox URL with access token").action(async (slug) => {
1715
+ const settings = loadSettings();
1716
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1717
+ const { sandbox } = await client.get(`/sandboxes/${slug}`);
1718
+ if (!sandbox.accessToken) {
1719
+ console.error(chalk14.red("No access token — sandbox not deployed"));
1720
+ process.exit(1);
1721
+ }
1722
+ console.log(`${sandboxUrl(sandbox.slug, "document.run")}?token=${sandbox.accessToken}`);
1723
+ });
1724
+ sb.command("exec <slug> <command...>").description("Run a command inside the sandbox").action(async (slug, args) => {
1725
+ const settings = loadSettings();
1726
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1727
+ const cmd = args.join(" ");
1728
+ const { output, exitCode } = await client.post(`/sandboxes/${slug}/exec`, { command: cmd });
1729
+ if (output)
1730
+ process.stdout.write(output);
1731
+ process.exit(exitCode);
1732
+ });
1733
+ sb.command("logs <slug>").description("Show docker compose logs").option("-n, --tail <lines>", "Number of lines", "100").action(async (slug, opts) => {
1734
+ const settings = loadSettings();
1735
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1736
+ const { output } = await client.get(`/sandboxes/${slug}/logs?tail=${opts.tail}`);
1737
+ if (output)
1738
+ process.stdout.write(output);
1739
+ });
1740
+ sb.command("stop <slug>").description("Stop a sandbox").action(async (slug) => {
1741
+ const settings = loadSettings();
1742
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1743
+ await client.post(`/sandboxes/${slug}/stop`);
1744
+ console.log(chalk14.green(`✓ Stopping: ${slug}`));
1745
+ });
1746
+ sb.command("wake <slug>").description("Wake a sandbox").action(async (slug) => {
1747
+ const settings = loadSettings();
1748
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1749
+ await client.post(`/sandboxes/${slug}/wake`);
1750
+ console.log(chalk14.green(`✓ Waking: ${slug}`));
1751
+ });
1752
+ sb.command("destroy <slug>").description("Destroy a sandbox").action(async (slug) => {
1753
+ const settings = loadSettings();
1754
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1755
+ await client.del(`/sandboxes/${slug}`);
1756
+ console.log(chalk14.green(`✓ Destroying: ${slug}`));
1757
+ });
1758
+ }
1759
+
1760
+ // src/cloud/github.ts
1761
+ import chalk15 from "chalk";
1762
+ function githubCommand(program) {
1763
+ const github = program.command("github").description("Browse GitHub repositories");
1764
+ github.command("repos").description("List your GitHub repositories").action(async () => {
1765
+ const settings = loadSettings();
1766
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1767
+ const { repos } = await client.get("/github/repos");
1768
+ if (repos.length === 0) {
1769
+ console.log(chalk15.yellow("No repositories found"));
1770
+ return;
1771
+ }
1772
+ for (const repo of repos) {
1773
+ const badge = repo.private ? chalk15.dim(" [private]") : "";
1774
+ const stars = repo.stargazers_count > 0 ? chalk15.dim(` ★ ${repo.stargazers_count}`) : "";
1775
+ console.log(` ${repo.full_name}${badge}${stars} ${chalk15.dim(repo.default_branch)}`);
1776
+ }
1777
+ });
1778
+ github.command("branches <repo>").description("List branches for a repository (owner/repo)").action(async (repo) => {
1779
+ const settings = loadSettings();
1780
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1781
+ const [owner, name] = repo.split("/");
1782
+ if (!owner || !name) {
1783
+ console.error(chalk15.red("Usage: dr github branches owner/repo"));
1784
+ process.exit(1);
1785
+ }
1786
+ const { branches } = await client.get(`/github/repos/${owner}/${name}/branches`);
1787
+ for (const b of branches) {
1788
+ console.log(` ${b.name}`);
1789
+ }
1790
+ });
1791
+ }
1792
+
1793
+ // src/cloud/services.ts
1794
+ import chalk16 from "chalk";
1795
+ function servicesCommand(program) {
1796
+ const svc = program.command("services").description("Manage services");
1797
+ svc.command("list <repo>").description("List services for a repository").action(async (repo) => {
1798
+ const settings = loadSettings();
1799
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1800
+ const { services } = await client.get(`/repos/${repo}/services`);
1801
+ if (services.length === 0) {
1802
+ console.log(chalk16.yellow("No services"));
1803
+ return;
1804
+ }
1805
+ for (const s of services) {
1806
+ const type = s.image ? s.image : `${s.baseImage || ""} → ${s.command || ""}`;
1807
+ const port = s.port ? `:${s.port}` : "";
1808
+ const expose = s.expose ? chalk16.blue(" expose") : "";
1809
+ console.log(` ${chalk16.bold(s.label)} ${chalk16.dim(`(${s.name})`)} ${type}${port}${expose}`);
1810
+ }
1811
+ });
1812
+ svc.command("add <repo>").description("Add a service").requiredOption("--label <label>", "Service label").option("--image <image>", "Docker image (for databases)").option("--command <cmd>", "Run command (for app services)").option("--base-image <image>", "Base image (with --command)").option("--port <port>", "Port number", "0").option("--expose", "Expose publicly", false).option("--setup-commands <cmds>", "Setup commands (comma-separated)").action(async (repo, opts) => {
1813
+ const settings = loadSettings();
1814
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1815
+ const body = {
1816
+ label: opts.label,
1817
+ port: parseInt(opts.port, 10)
1818
+ };
1819
+ if (opts.image)
1820
+ body.image = opts.image;
1821
+ if (opts.command)
1822
+ body.command = opts.command;
1823
+ if (opts.baseImage)
1824
+ body.baseImage = opts.baseImage;
1825
+ if (opts.expose)
1826
+ body.expose = true;
1827
+ if (opts.setupCommands)
1828
+ body.setupCommands = opts.setupCommands.split(",").map((s) => s.trim());
1829
+ const { service } = await client.post(`/repos/${repo}/services`, body);
1830
+ console.log(chalk16.green(`✓ Created service: ${service.name}`));
1831
+ });
1832
+ svc.command("delete <repo> <name>").description("Delete a service").action(async (repo, name) => {
1833
+ const settings = loadSettings();
1834
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1835
+ await client.del(`/repos/${repo}/services/${name}`);
1836
+ console.log(chalk16.green(`✓ Deleted service: ${name}`));
1837
+ });
1838
+ }
1839
+
1840
+ // src/cloud/env.ts
1841
+ import chalk17 from "chalk";
1842
+ function envCommand(program) {
1843
+ const env = program.command("env").description("Manage environment variables");
1844
+ env.command("list <repo>").description("List environment variables").action(async (repo) => {
1845
+ const settings = loadSettings();
1846
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1847
+ const { env_vars } = await client.get(`/repos/${repo}/env-vars`);
1848
+ if (env_vars.length === 0) {
1849
+ console.log(chalk17.yellow("No environment variables"));
1850
+ return;
1851
+ }
1852
+ for (const ev of env_vars) {
1853
+ const val = ev.secure ? chalk17.dim("••••••") : ev.value;
1854
+ console.log(` ${chalk17.bold(ev.key)}=${val} ${chalk17.dim(ev.environment)}`);
1855
+ }
1856
+ });
1857
+ env.command("set <repo>").description("Set an environment variable (upserts)").requiredOption("--key <key>", "Variable name").requiredOption("--value <value>", "Variable value").option("--environment <env>", "Environment (shared/sandbox/production)", "shared").option("--secure", "Mark as secure (mask in outputs)", false).action(async (repo, opts) => {
1858
+ const settings = loadSettings();
1859
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1860
+ await client.post(`/repos/${repo}/env-vars`, {
1861
+ key: opts.key,
1862
+ value: opts.value,
1863
+ environment: opts.environment,
1864
+ secure: opts.secure
1865
+ });
1866
+ console.log(chalk17.green(`✓ Set: ${opts.key}`));
1867
+ });
1868
+ env.command("delete <repo> <key>").description("Delete an environment variable").action(async (repo, key) => {
1869
+ const settings = loadSettings();
1870
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1871
+ await client.del(`/repos/${repo}/env-vars/${key}`);
1872
+ console.log(chalk17.green(`✓ Deleted: ${key}`));
1873
+ });
1874
+ }
1875
+
1876
+ // src/cloud/issues.ts
1877
+ import chalk18 from "chalk";
1878
+ function issuesCommand(program) {
1879
+ const issues = program.command("issues").description("Manage issues");
1880
+ issues.command("list <repo>").description("List issues").action(async (repo) => {
1881
+ const settings = loadSettings();
1882
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1883
+ const { issues: list } = await client.get(`/repos/${repo}/issues`);
1884
+ if (list.length === 0) {
1885
+ console.log(chalk18.yellow("No issues synced"));
1886
+ return;
1887
+ }
1888
+ for (const issue of list) {
1889
+ const stateColor = issue.state === "open" ? chalk18.green : chalk18.dim;
1890
+ const kind = issue.kind === "pull" ? chalk18.blue("PR") : "";
1891
+ const author = issue.authorLogin ? chalk18.dim(` by ${issue.authorLogin}`) : "";
1892
+ console.log(` ${chalk18.dim(`#${issue.number}`)} ${issue.title} ${stateColor(issue.state)} ${kind}${author}`);
1893
+ }
1894
+ });
1895
+ issues.command("sync <repo>").description("Sync issues from GitHub").action(async (repo) => {
1896
+ const settings = loadSettings();
1897
+ const client = new ApiClient(settings.apiUrl, settings.token, settings.workspaceId);
1898
+ await client.post(`/repos/${repo}/sync-issues`);
1899
+ console.log(chalk18.green("✓ Syncing issues..."));
1900
+ });
1901
+ }
1902
+
1903
+ // src/cloud/app.ts
1904
+ function createCloudApp() {
1905
+ const program = new Command2;
1906
+ program.name("dr").description("Document Run CLI (cloud mode)").version("0.0.1");
1907
+ loginCommand(program);
1908
+ logoutCommand(program);
1909
+ whoamiCommand(program);
1910
+ deployCommand2(program);
1911
+ reposCommand(program);
1912
+ sandboxesCommand(program);
1913
+ workerLogsCommand(program);
1914
+ githubCommand(program);
1915
+ servicesCommand(program);
1916
+ envCommand(program);
1917
+ issuesCommand(program);
1918
+ return program;
1919
+ }
1920
+
1921
+ // src/index.ts
1922
+ var args = process.argv.slice(2);
1923
+ var cloudCommands = ["login", "logout", "whoami"];
1924
+ if (isLoggedIn() || args[0] && cloudCommands.includes(args[0])) {
1925
+ createCloudApp().parse();
1926
+ } else {
1927
+ createLocalApp().parse();
1928
+ }