@kortix/sandbox 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ app.get('/kortix/health', async (c) => {
28
28
  version = data.version || '0.0.0'
29
29
  }
30
30
  } catch {}
31
- return c.json({ status: 'ok', version })
31
+ return c.json({ status: 'ok', version, build: '0.4.4-update-test' })
32
32
  })
33
33
 
34
34
  // Update check — /kortix/update and /kortix/update/status
@@ -1,12 +1,4 @@
1
1
  import { Hono } from 'hono';
2
- import { config } from '../config';
3
-
4
- // ─── Types ──────────────────────────────────────────────────────────────────
5
-
6
- interface LocalVersion {
7
- version: string;
8
- updatedAt: string;
9
- }
10
2
 
11
3
  // ─── Constants ──────────────────────────────────────────────────────────────
12
4
 
@@ -32,35 +24,17 @@ let updateInProgress = false;
32
24
 
33
25
  // ─── Helpers ────────────────────────────────────────────────────────────────
34
26
 
35
- async function readLocalVersion(): Promise<LocalVersion> {
27
+ async function readLocalVersion(): Promise<string> {
36
28
  try {
37
29
  const file = Bun.file(VERSION_FILE);
38
30
  if (await file.exists()) {
39
- return await file.json();
31
+ const data = await file.json();
32
+ return data.version || '0.0.0';
40
33
  }
41
34
  } catch (e) {
42
35
  console.error('[Update] Failed to read version file:', e);
43
36
  }
44
- return { version: '0.0.0', updatedAt: '' };
45
- }
46
-
47
- async function fetchLatestVersion(): Promise<string | null> {
48
- const url = `${config.KORTIX_API_URL}/v1/sandbox/version`;
49
- try {
50
- const res = await fetch(url, {
51
- headers: { 'Accept': 'application/json' },
52
- signal: AbortSignal.timeout(10_000),
53
- });
54
- if (!res.ok) {
55
- console.error(`[Update] Version fetch failed: ${res.status}`);
56
- return null;
57
- }
58
- const data = await res.json() as { version: string };
59
- return data.version;
60
- } catch (e) {
61
- console.error('[Update] Failed to fetch version:', e);
62
- return null;
63
- }
37
+ return '0.0.0';
64
38
  }
65
39
 
66
40
  async function run(cmd: string): Promise<{ ok: boolean; output: string }> {
@@ -116,62 +90,45 @@ async function performUpdate(targetVersion: string): Promise<{
116
90
 
117
91
  const updateRouter = new Hono();
118
92
 
119
- /**
120
- * GET /kortix/update/status
121
- *
122
- * Returns current sandbox version + latest available version.
123
- * Frontend uses this to decide whether to show "Update available".
124
- * Read-only — does NOT trigger any update.
125
- */
126
- updateRouter.get('/status', async (c) => {
127
- const local = await readLocalVersion();
128
- const latest = await fetchLatestVersion();
129
-
130
- return c.json({
131
- currentVersion: local.version,
132
- latestVersion: latest || 'unknown',
133
- updateAvailable: latest ? local.version !== latest : false,
134
- updatedAt: local.updatedAt,
135
- updateInProgress,
136
- });
137
- });
138
-
139
93
  /**
140
94
  * POST /kortix/update
141
95
  *
142
- * User-triggered update. Fetches latest version, installs the package,
143
- * restarts services. Only runs when explicitly called.
96
+ * User-triggered update. Frontend passes the target version.
97
+ * Installs the package, restarts services.
98
+ * Only runs when explicitly called.
99
+ *
100
+ * Body: { "version": "0.4.3" }
144
101
  */
145
102
  updateRouter.post('/', async (c) => {
146
103
  if (updateInProgress) {
147
104
  return c.json({ error: 'Update already in progress' }, 409);
148
105
  }
149
106
 
107
+ const body = await c.req.json().catch(() => ({}));
108
+ const targetVersion = body.version;
109
+
110
+ if (!targetVersion || typeof targetVersion !== 'string') {
111
+ return c.json({ error: 'Missing "version" in request body' }, 400);
112
+ }
113
+
150
114
  updateInProgress = true;
151
115
  try {
152
- const local = await readLocalVersion();
153
- const latestVersion = await fetchLatestVersion();
154
-
155
- if (!latestVersion) {
156
- return c.json({ error: 'Could not reach version service' }, 502);
157
- }
116
+ const currentVersion = await readLocalVersion();
158
117
 
159
- if (local.version === latestVersion) {
118
+ if (currentVersion === targetVersion) {
160
119
  return c.json({
161
120
  upToDate: true,
162
- currentVersion: local.version,
163
- latestVersion,
121
+ currentVersion,
164
122
  });
165
123
  }
166
124
 
167
- console.log(`[Update] User triggered: ${local.version} -> ${latestVersion}`);
168
- const update = await performUpdate(latestVersion);
125
+ console.log(`[Update] User triggered: ${currentVersion} -> ${targetVersion}`);
126
+ const update = await performUpdate(targetVersion);
169
127
 
170
128
  return c.json({
171
129
  success: update.success,
172
- previousVersion: local.version,
173
- currentVersion: update.success ? latestVersion : local.version,
174
- latestVersion,
130
+ previousVersion: currentVersion,
131
+ currentVersion: update.success ? targetVersion : currentVersion,
175
132
  output: update.output,
176
133
  });
177
134
  } catch (e) {
@@ -9,8 +9,8 @@ The Kortix Cron service runs outside the sandbox as a platform service. It manag
9
9
 
10
10
  ## Architecture
11
11
 
12
- - **Service**: `kortix-cron` (Bun + Hono) running at port 8011
13
- - **Database**: `kortix_cron` schema in Supabase PostgreSQL (Drizzle ORM)
12
+ - **Service**: `kortix-api` (Bun + Hono) running at port 8008
13
+ - **Database**: `kortix` schema in Supabase PostgreSQL (Drizzle ORM)
14
14
  - **Scheduler**: Polling loop (1s tick) that checks for due triggers
15
15
  - **Executor**: Calls OpenCode API inside sandbox to create sessions and send prompts
16
16
 
@@ -44,7 +44,7 @@ The Kortix Cron service runs outside the sandbox as a platform service. It manag
44
44
 
45
45
  ## API Reference
46
46
 
47
- Base URL: `http://localhost:8011` (local) or the deployed service URL.
47
+ Base URL: `http://localhost:8008` (local) or the deployed service URL.
48
48
 
49
49
  All `/v1/*` endpoints require `Authorization: Bearer <supabase-jwt>`.
50
50
 
@@ -52,7 +52,7 @@ All `/v1/*` endpoints require `Authorization: Bearer <supabase-jwt>`.
52
52
 
53
53
  ```bash
54
54
  # Register a sandbox
55
- curl -X POST http://localhost:8011/v1/sandboxes \
55
+ curl -X POST http://localhost:8008/v1/sandboxes \
56
56
  -H "Authorization: Bearer $TOKEN" \
57
57
  -H "Content-Type: application/json" \
58
58
  -d '{
@@ -62,11 +62,11 @@ curl -X POST http://localhost:8011/v1/sandboxes \
62
62
  }'
63
63
 
64
64
  # List sandboxes
65
- curl http://localhost:8011/v1/sandboxes \
65
+ curl http://localhost:8008/v1/sandboxes \
66
66
  -H "Authorization: Bearer $TOKEN"
67
67
 
68
68
  # Check sandbox health
69
- curl -X POST http://localhost:8011/v1/sandboxes/{id}/health \
69
+ curl -X POST http://localhost:8008/v1/sandboxes/{id}/health \
70
70
  -H "Authorization: Bearer $TOKEN"
71
71
  ```
72
72
 
@@ -74,7 +74,7 @@ curl -X POST http://localhost:8011/v1/sandboxes/{id}/health \
74
74
 
75
75
  ```bash
76
76
  # Create a trigger - daily report at 9 AM UTC
77
- curl -X POST http://localhost:8011/v1/triggers \
77
+ curl -X POST http://localhost:8008/v1/triggers \
78
78
  -H "Authorization: Bearer $TOKEN" \
79
79
  -H "Content-Type: application/json" \
80
80
  -d '{
@@ -88,7 +88,7 @@ curl -X POST http://localhost:8011/v1/triggers \
88
88
  }'
89
89
 
90
90
  # Create a trigger - every 5 minutes health check
91
- curl -X POST http://localhost:8011/v1/triggers \
91
+ curl -X POST http://localhost:8008/v1/triggers \
92
92
  -H "Authorization: Bearer $TOKEN" \
93
93
  -H "Content-Type: application/json" \
94
94
  -d '{
@@ -101,23 +101,23 @@ curl -X POST http://localhost:8011/v1/triggers \
101
101
  }'
102
102
 
103
103
  # List triggers
104
- curl http://localhost:8011/v1/triggers \
104
+ curl http://localhost:8008/v1/triggers \
105
105
  -H "Authorization: Bearer $TOKEN"
106
106
 
107
107
  # Pause a trigger
108
- curl -X POST http://localhost:8011/v1/triggers/{id}/pause \
108
+ curl -X POST http://localhost:8008/v1/triggers/{id}/pause \
109
109
  -H "Authorization: Bearer $TOKEN"
110
110
 
111
111
  # Resume a trigger
112
- curl -X POST http://localhost:8011/v1/triggers/{id}/resume \
112
+ curl -X POST http://localhost:8008/v1/triggers/{id}/resume \
113
113
  -H "Authorization: Bearer $TOKEN"
114
114
 
115
115
  # Fire a trigger immediately (manual run)
116
- curl -X POST http://localhost:8011/v1/triggers/{id}/run \
116
+ curl -X POST http://localhost:8008/v1/triggers/{id}/run \
117
117
  -H "Authorization: Bearer $TOKEN"
118
118
 
119
119
  # Delete a trigger
120
- curl -X DELETE http://localhost:8011/v1/triggers/{id} \
120
+ curl -X DELETE http://localhost:8008/v1/triggers/{id} \
121
121
  -H "Authorization: Bearer $TOKEN"
122
122
  ```
123
123
 
@@ -125,30 +125,30 @@ curl -X DELETE http://localhost:8011/v1/triggers/{id} \
125
125
 
126
126
  ```bash
127
127
  # List all executions
128
- curl "http://localhost:8011/v1/executions?limit=20" \
128
+ curl "http://localhost:8008/v1/executions?limit=20" \
129
129
  -H "Authorization: Bearer $TOKEN"
130
130
 
131
131
  # Filter by status
132
- curl "http://localhost:8011/v1/executions?status=failed" \
132
+ curl "http://localhost:8008/v1/executions?status=failed" \
133
133
  -H "Authorization: Bearer $TOKEN"
134
134
 
135
135
  # Filter by date range
136
- curl "http://localhost:8011/v1/executions?since=2026-02-01T00:00:00Z&until=2026-02-11T00:00:00Z" \
136
+ curl "http://localhost:8008/v1/executions?since=2026-02-01T00:00:00Z&until=2026-02-11T00:00:00Z" \
137
137
  -H "Authorization: Bearer $TOKEN"
138
138
 
139
139
  # Executions for a specific trigger
140
- curl http://localhost:8011/v1/executions/by-trigger/{triggerId} \
140
+ curl http://localhost:8008/v1/executions/by-trigger/{triggerId} \
141
141
  -H "Authorization: Bearer $TOKEN"
142
142
 
143
143
  # Get execution details
144
- curl http://localhost:8011/v1/executions/{id} \
144
+ curl http://localhost:8008/v1/executions/{id} \
145
145
  -H "Authorization: Bearer $TOKEN"
146
146
  ```
147
147
 
148
148
  ### Health Check (no auth)
149
149
 
150
150
  ```bash
151
- curl http://localhost:8011/health
151
+ curl http://localhost:8008/health
152
152
  # Returns: { status, service, timestamp, scheduler: { running, tickCount, lastTick } }
153
153
  ```
154
154
 
@@ -0,0 +1,438 @@
1
+ ---
2
+ name: kortix-deploy
3
+ description: "Deploy any web app, API, or static site to Freestyle.sh with a live preview URL. Supports Next.js, Vite, static sites, Express/Hono APIs, raw code snippets, Git repos, local files, and tar URLs. Auto-detects frameworks, builds, and deploys with instant SSL on *.style.dev subdomains or custom domains. Triggers on: 'deploy this', 'deploy my app', 'make this live', 'publish this', 'host this', 'get me a preview URL', 'deploy to freestyle', 'ship this', 'deploy to production', '1-click deploy', 'preview URL', 'put this online', any request to deploy, host, or publish a web application, site, or API to the internet."
4
+ ---
5
+
6
+ # Deploy to Freestyle.sh
7
+
8
+ One-command deploys to live preview URLs via [Freestyle Serverless Deployments](https://docs.freestyle.sh/v2/serverless/deployments). Write a small deploy script, run it, hand back the URL.
9
+
10
+ ## About Freestyle Serverless Deployments
11
+
12
+ Freestyle is an API-first serverless platform built for **programmatic deployment at scale** (not dashboard-clicking like Vercel/Netlify). Deployments run **Node.js only** with automatic scaling, wildcard subdomains, and framework detection.
13
+
14
+ **Key capabilities:**
15
+ - **Sub-second deploys** for non-build deployments (no containers, cached deps)
16
+ - **4 source types**: Git repo, inline code, local files (`readFiles`), tar URL
17
+ - **Auto-detects** Next.js, Vite, Expo — TypeScript works out of the box
18
+ - **Free `*.style.dev` subdomains** with instant SSL, custom domains with wildcard certs
19
+ - **WebSocket support** — timeout is per last TCP packet, not HTTP request
20
+ - **Cached modules** — never upload `node_modules`, just your lockfile
21
+
22
+ **When NOT to use Deployments (use Freestyle VMs instead):**
23
+ - Non-Node workloads (Python, Ruby, Go) — VMs are full Linux environments
24
+ - One-shot code execution (no HTTP server needed) — use Serverless Runs
25
+ - Low-level system access (SSH, systemd, filesystem persistence)
26
+ - Browser automation (scraping, testing)
27
+
28
+ ## Hard-Won Deployment Lessons
29
+
30
+ These are critical gotchas discovered through real e2e testing:
31
+
32
+ 1. **SDK version**: Use `freestyle-sandboxes@latest` (NOT `@beta`). The `@beta` tag (0.1.3) has a different API path (`freestyle.edge.deployments`) that doesn't match current docs. Latest uses `freestyle.serverless.deployments`.
33
+ 2. **`readFiles` import**: `import { readFiles } from "freestyle-sandboxes"` — it is exported from the main package. NOT from `freestyle-sandboxes/utils` (that subpath doesn't exist).
34
+ 3. **Runtime is Node.js, NOT Deno**: `Deno.serve()` and Hono's `app.fire()` do NOT work. Always use Express `app.listen(3000)` or Hono with `@hono/node-server` and `serve({ fetch: app.fetch, port: 3000 })`.
35
+ 4. **Static sites MUST have a Node.js server entrypoint**: Setting `entrypointPath` to an HTML file (e.g., `index.html`) will serve that page but CSS/JS/image sub-assets will NOT load. You MUST bundle an Express static file server.
36
+ 5. **Deploy scripts must be `.mjs`**: The SDK uses ESM exports. Write `.mjs` files and run with `node`, not `.ts` with `npx tsx`.
37
+ 6. **Cold starts**: First request may 503 for ~10-15 seconds after deploy completes. This is normal. Subsequent requests are instant.
38
+ 7. **Port 3000**: All servers must listen on port 3000. This is the port Freestyle routes to.
39
+ 8. **`nodeModules` field**: Only needed for `code` deploys. For `files`/`git` deploys, include your lockfile and Freestyle installs deps automatically.
40
+ 9. **`envVars` are runtime-only**: NOT available at build time. Use `build.envVars` for build-time env vars.
41
+ 10. **Never upload `node_modules`**: `readFiles` auto-excludes it. Freestyle installs from your lockfile.
42
+
43
+ ## Prerequisites
44
+
45
+ 1. **API Key**: `FREESTYLE_API_KEY` must be set. Check with `env | grep FREESTYLE_API_KEY`. If missing, set via kortix-secrets or ask the user for their key from [admin.freestyle.sh](https://admin.freestyle.sh).
46
+
47
+ 2. **SDK**: `freestyle-sandboxes` must be installed (already pre-installed in sandbox). If missing:
48
+ ```bash
49
+ npm i freestyle-sandboxes
50
+ ```
51
+
52
+ ## Workflow
53
+
54
+ 1. **Detect** project type (see Detection below)
55
+ 2. **Pick** the right starter template
56
+ 3. **Customize** — fill in project-specific values (repo URL, domain slug, env vars, etc.)
57
+ 4. **Write** the deploy script as `.mjs` file (e.g., `/tmp/deploy-freestyle.mjs`)
58
+ 5. **Run** it: `FREESTYLE_API_KEY=... node /tmp/deploy-freestyle.mjs`
59
+ 6. **Report** the live URL to the user and show it
60
+
61
+ **CRITICAL**: Deploy scripts must be `.mjs` files (ESM). The SDK uses ES module exports. Run with `node`, NOT `npx tsx`.
62
+
63
+ ### Domain Naming
64
+
65
+ Generate a descriptive `*.style.dev` subdomain:
66
+ - Use project name or directory name as base
67
+ - Append short random suffix to avoid collisions: `my-app-x7k2.style.dev`
68
+ - Keep it lowercase, alphanumeric + hyphens only
69
+ - Generate with: `` `${slug}-${crypto.randomUUID().slice(0, 4)}.style.dev` ``
70
+
71
+ ## Project Detection
72
+
73
+ Check these files to determine the framework. If ambiguous, ask the user.
74
+
75
+ | File present | Framework | Deploy strategy |
76
+ |---|---|---|
77
+ | `next.config.{js,mjs,ts}` | Next.js | Git + `build: true` or local files via readFiles |
78
+ | `vite.config.{js,ts,mjs}` | Vite | Git + `build: true` or local files via readFiles |
79
+ | Only `.html`/`.css`/`.js` files | Static | Express static server + readFiles |
80
+ | `package.json` with `express`/`hono`/`fastify` dep | Node.js API | Git or local files, no build |
81
+ | `.git` with GitHub remote | Any | Prefer git deploy with repo URL |
82
+ | User provides a code snippet | Code snippet | Inline `code` + `nodeModules` |
83
+ | User provides a URL to `.tar.gz` | Tar | `tarUrl` source |
84
+
85
+ ### Next.js Pre-Flight
86
+
87
+ Before deploying Next.js, verify `next.config` has:
88
+ ```javascript
89
+ output: "standalone"
90
+ images: { unoptimized: true }
91
+ ```
92
+ If missing, add them automatically and inform the user.
93
+
94
+ ## Starter Templates
95
+
96
+ Each template is a complete, runnable `.mjs` deploy script. Copy the appropriate one, fill in the `CUSTOMIZE` values, write to `/tmp/deploy-freestyle.mjs`, and run with `FREESTYLE_API_KEY=... node /tmp/deploy-freestyle.mjs`.
97
+
98
+ **Import pattern** (same for all templates):
99
+ ```javascript
100
+ import { freestyle, readFiles } from "freestyle-sandboxes"; // readFiles only when deploying local files
101
+ ```
102
+
103
+ > **NOTE**: `readFiles` is exported from `freestyle-sandboxes` directly. NOT from `freestyle-sandboxes/utils`.
104
+
105
+ ---
106
+
107
+ ### 1. Git Repo Deploy (any framework)
108
+
109
+ The most common path. Works for any project with a Git remote. Freestyle auto-detects Next.js, Vite, Expo.
110
+
111
+ ```javascript
112
+ import { freestyle } from "freestyle-sandboxes";
113
+
114
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
115
+ repo: "https://github.com/USER/REPO", // CUSTOMIZE: repo URL
116
+ // branch: "main", // CUSTOMIZE: optional
117
+ // rootPath: "./apps/web", // CUSTOMIZE: optional, for monorepos
118
+ domains: ["SLUG.style.dev"], // CUSTOMIZE: unique subdomain
119
+ build: true, // set false if no build needed
120
+ // envVars: { KEY: "value" }, // CUSTOMIZE: optional runtime env vars
121
+ });
122
+
123
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
124
+ console.log("Deployment ID:", deployment.deploymentId);
125
+ ```
126
+
127
+ ---
128
+
129
+ ### 2. Next.js (from Git)
130
+
131
+ ```javascript
132
+ import { freestyle } from "freestyle-sandboxes";
133
+
134
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
135
+ repo: "https://github.com/USER/REPO", // CUSTOMIZE
136
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
137
+ build: true, // auto-detects Next.js
138
+ // envVars: { DATABASE_URL: "..." }, // CUSTOMIZE: optional runtime env vars
139
+ // build: { // use this form for build-time env vars
140
+ // command: "npm run build",
141
+ // envVars: { NEXT_PUBLIC_API_URL: "https://api.example.com" },
142
+ // },
143
+ });
144
+
145
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
146
+ console.log("Deployment ID:", deployment.deploymentId);
147
+ ```
148
+
149
+ ---
150
+
151
+ ### 3. Next.js (from local files)
152
+
153
+ Build locally, copy standalone artifacts, upload with `readFiles`.
154
+
155
+ ```javascript
156
+ import { freestyle, readFiles } from "freestyle-sandboxes";
157
+ import { execSync } from "child_process";
158
+ import { cpSync } from "fs";
159
+
160
+ // Build
161
+ execSync("npm run build", { stdio: "inherit", cwd: "PROJECT_DIR" }); // CUSTOMIZE
162
+
163
+ // Prepare standalone artifacts
164
+ cpSync("PROJECT_DIR/public", "PROJECT_DIR/.next/standalone/public", { recursive: true });
165
+ cpSync("PROJECT_DIR/.next/static", "PROJECT_DIR/.next/standalone/.next/static", { recursive: true });
166
+ cpSync("PROJECT_DIR/package-lock.json", "PROJECT_DIR/.next/standalone/package-lock.json"); // CUSTOMIZE: use your lockfile
167
+
168
+ const files = await readFiles("PROJECT_DIR/.next/standalone"); // CUSTOMIZE
169
+
170
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
171
+ files,
172
+ entrypointPath: "server.js",
173
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
174
+ });
175
+
176
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
177
+ console.log("Deployment ID:", deployment.deploymentId);
178
+ ```
179
+
180
+ ---
181
+
182
+ ### 4. Vite (from Git)
183
+
184
+ ```javascript
185
+ import { freestyle } from "freestyle-sandboxes";
186
+
187
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
188
+ repo: "https://github.com/USER/REPO", // CUSTOMIZE
189
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
190
+ build: true, // auto-detects Vite
191
+ // envVars: { VITE_API_URL: "..." }, // CUSTOMIZE: optional
192
+ });
193
+
194
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
195
+ console.log("Deployment ID:", deployment.deploymentId);
196
+ ```
197
+
198
+ ---
199
+
200
+ ### 5. Vite / Static SPA (from local files)
201
+
202
+ Build locally, add an Express static server, deploy the dist. **This pattern works for any pre-built SPA (React, Vue, Svelte, etc.) or static site.**
203
+
204
+ ```javascript
205
+ import { freestyle, readFiles } from "freestyle-sandboxes";
206
+ import { execSync } from "child_process";
207
+ import { writeFileSync } from "fs";
208
+
209
+ // Build (skip if already built or pure static)
210
+ execSync("npm run build", { stdio: "inherit", cwd: "PROJECT_DIR" }); // CUSTOMIZE
211
+
212
+ const distDir = "PROJECT_DIR/dist"; // CUSTOMIZE: build output directory
213
+
214
+ // Write Express static server into dist
215
+ writeFileSync(`${distDir}/server.js`, `
216
+ import express from 'express';
217
+ import { fileURLToPath } from 'url';
218
+ import { dirname, join } from 'path';
219
+
220
+ const __dirname = dirname(fileURLToPath(import.meta.url));
221
+ const app = express();
222
+
223
+ app.use(express.static(__dirname));
224
+ app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html')));
225
+
226
+ app.listen(3000, () => console.log('Server running on port 3000'));
227
+ `);
228
+
229
+ writeFileSync(`${distDir}/package.json`, JSON.stringify({
230
+ name: "deploy", type: "module", dependencies: { express: "^4.18.2" }
231
+ }));
232
+
233
+ const files = await readFiles(distDir);
234
+
235
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
236
+ files,
237
+ entrypointPath: "server.js",
238
+ nodeModules: { express: "^4.18.2" },
239
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
240
+ });
241
+
242
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
243
+ console.log("Deployment ID:", deployment.deploymentId);
244
+ ```
245
+
246
+ ---
247
+
248
+ ### 6. Static Site (HTML/CSS/JS files)
249
+
250
+ For plain HTML/CSS/JS with no build step. **Must include an Express server** — Freestyle needs a Node.js entrypoint.
251
+
252
+ ```javascript
253
+ import { freestyle, readFiles } from "freestyle-sandboxes";
254
+ import { writeFileSync } from "fs";
255
+
256
+ const siteDir = "PROJECT_DIR"; // CUSTOMIZE: directory with HTML/CSS/JS
257
+
258
+ // Write Express static server
259
+ writeFileSync(`${siteDir}/server.js`, `
260
+ import express from 'express';
261
+ import { fileURLToPath } from 'url';
262
+ import { dirname, join } from 'path';
263
+
264
+ const __dirname = dirname(fileURLToPath(import.meta.url));
265
+ const app = express();
266
+
267
+ app.use(express.static(__dirname));
268
+ app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html')));
269
+
270
+ app.listen(3000, () => console.log('Static server on port 3000'));
271
+ `);
272
+
273
+ const files = await readFiles(siteDir);
274
+
275
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
276
+ files,
277
+ entrypointPath: "server.js",
278
+ nodeModules: { express: "^4.18.2" },
279
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
280
+ });
281
+
282
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
283
+ console.log("Deployment ID:", deployment.deploymentId);
284
+ ```
285
+
286
+ ---
287
+
288
+ ### 7. Code Snippet Deploy (Express)
289
+
290
+ For quick API servers or demos. **Use Express with `app.listen(3000)`** — this is the proven pattern.
291
+
292
+ ```javascript
293
+ import { freestyle } from "freestyle-sandboxes";
294
+
295
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
296
+ code: `
297
+ import express from 'express';
298
+ const app = express();
299
+
300
+ app.get('/', (req, res) => {
301
+ res.json({ status: 'ok', time: new Date().toISOString() });
302
+ });
303
+
304
+ app.listen(3000, () => console.log('Running on port 3000'));
305
+ `,
306
+ nodeModules: {
307
+ express: "^4.18.2", // CUSTOMIZE: dependencies
308
+ },
309
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
310
+ // envVars: { KEY: "value" }, // CUSTOMIZE: optional
311
+ });
312
+
313
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
314
+ console.log("Deployment ID:", deployment.deploymentId);
315
+ ```
316
+
317
+ **Hono variant** (also works — must use `@hono/node-server`):
318
+ ```javascript
319
+ code: `
320
+ import { Hono } from "hono";
321
+ import { serve } from "@hono/node-server";
322
+ const app = new Hono();
323
+ app.get("/", (c) => c.json({ status: "ok" }));
324
+ serve({ fetch: app.fetch, port: 3000 });
325
+ `,
326
+ nodeModules: { hono: "4.11.1", "@hono/node-server": "^1.13.8" },
327
+ ```
328
+
329
+ > **WARNING**: Do NOT use `app.fire()` or `Deno.serve()` — these do not work in the Freestyle runtime. Always use `app.listen(3000)` (Express) or `serve({ fetch: app.fetch, port: 3000 })` (Hono).
330
+
331
+ ---
332
+
333
+ ### 8. Tar URL Deploy
334
+
335
+ For deploying from a remote archive (S3, GCS signed URL, etc).
336
+
337
+ ```javascript
338
+ import { freestyle } from "freestyle-sandboxes";
339
+
340
+ const { deployment, domains } = await freestyle.serverless.deployments.create({
341
+ tarUrl: "https://s3.example.com/signed-url/app.tar.gz", // CUSTOMIZE
342
+ domains: ["SLUG.style.dev"], // CUSTOMIZE
343
+ build: true, // CUSTOMIZE: set false if pre-built
344
+ // entrypointPath: "server.js", // CUSTOMIZE: if needed
345
+ // envVars: { KEY: "value" }, // CUSTOMIZE: optional
346
+ });
347
+
348
+ console.log("Live at:", domains.map(d => `https://${d}`).join(", "));
349
+ console.log("Deployment ID:", deployment.deploymentId);
350
+ ```
351
+
352
+ ## Custom Domains
353
+
354
+ If the user wants to deploy to their own domain instead of `*.style.dev`:
355
+
356
+ ### 1. Verify domain ownership
357
+
358
+ ```javascript
359
+ const { record, instructions } = await freestyle.domains.verifications.create({
360
+ domain: "example.com",
361
+ });
362
+ // Tell user: Add TXT record _freestyle_custom_hostname.example.com → record.value
363
+ ```
364
+
365
+ ### 2. Complete verification (after user adds DNS record)
366
+
367
+ ```javascript
368
+ await freestyle.domains.verifications.complete({ domain: "example.com" });
369
+ ```
370
+
371
+ ### 3. Configure DNS
372
+
373
+ Tell the user to add an A record pointing to `35.235.84.134`:
374
+ - **APEX** (`example.com`): `A @ 35.235.84.134`
375
+ - **Subdomain** (`app.example.com`): `A app 35.235.84.134`
376
+ - **Wildcard** (`*.example.com`): `A * 35.235.84.134`
377
+
378
+ ### 4. Deploy
379
+
380
+ ```javascript
381
+ domains: ["example.com"] // use the verified domain in the deploy call
382
+ ```
383
+
384
+ ## API Reference (Quick)
385
+
386
+ Full details in `references/freestyle-api.md`. Key options for `freestyle.serverless.deployments.create()`:
387
+
388
+ **Sources** (exactly one required):
389
+ - `repo: "https://github.com/user/repo"` + optional `branch`, `rootPath`
390
+ - `code: "..."` + `nodeModules: { pkg: "version" }`
391
+ - `files` (from `readFiles(dir)`) + `entrypointPath: "server.js"`
392
+ - `tarUrl: "https://..."`
393
+
394
+ **Options:**
395
+ - `domains: ["slug.style.dev"]` — required, free `*.style.dev` or verified custom domain
396
+ - `build: true` or `build: { command, outDir, envVars }` — triggers framework build
397
+ - `entrypointPath: "server.js"` — main file (auto-detected for Next.js/Vite)
398
+ - `envVars: { KEY: "value" }` — runtime env vars (NOT build-time)
399
+ - `nodeModules: { express: "^4.18.2" }` — only for `code` deploys
400
+ - `timeoutMs: 60000` — idle timeout before scale-down (per last TCP packet)
401
+ - `networkPermissions: [{ action, domain, behavior }]` — outbound network ACL
402
+ - `headers: [{ source, headers: [{ key, value }] }]` — custom response headers
403
+ - `redirects: [{ source, destination, permanent }]` — URL redirects
404
+ - `waitForRollout: true` — wait until fully serving traffic
405
+
406
+ **Return value:**
407
+ ```javascript
408
+ const { deployment, domains } = await freestyle.serverless.deployments.create({...});
409
+ // deployment.deploymentId — unique ID
410
+ // domains — string[] of live URLs
411
+ ```
412
+
413
+ ## Post-Deploy
414
+
415
+ After a successful deployment:
416
+
417
+ 1. **Show the live URL** prominently — `https://SLUG.style.dev`
418
+ 2. **Show deployment ID** — for reference and debugging
419
+ 3. **Open in browser** if the kortix-browser skill is available:
420
+ ```bash
421
+ agent-browser --session preview-deploy open https://SLUG.style.dev
422
+ ```
423
+ 4. **Clean up** — remove `/tmp/deploy-freestyle.mjs`
424
+
425
+ ## Troubleshooting
426
+
427
+ | Problem | Fix |
428
+ |---|---|
429
+ | `FREESTYLE_API_KEY` not set | Set via kortix-secrets or `export FREESTYLE_API_KEY=...` |
430
+ | `Cannot find package` | Run deploy script from project root where `freestyle-sandboxes` is installed, or use absolute path to node_modules |
431
+ | Build fails for Next.js | Ensure `output: "standalone"` and `images: { unoptimized: true }` in next.config |
432
+ | Module not found on Freestyle | Include lockfile in source. Freestyle installs deps from it. Never upload `node_modules`. |
433
+ | 404 on SPA routes / static assets not loading | You MUST use an Express static server entrypoint. Setting `entrypointPath` to an HTML file does NOT work for sub-assets. |
434
+ | `app.fire()` / `Deno.serve()` fails | Freestyle runs Node.js, not Deno. Use `app.listen(3000)` or `@hono/node-server` `serve()`. |
435
+ | 503 on first request | Cold start — wait 10-15 seconds after deploy for the instance to warm up. |
436
+ | Domain not working | Check DNS: `dig yourdomain.com` should show `35.235.84.134` |
437
+ | Deploy takes too long | Use `await: false` to return immediately, then poll with `freestyle.serverless.deployments.get()` |
438
+ | Subdomain taken | Pick a different `*.style.dev` slug — add more random chars |
@@ -0,0 +1,279 @@
1
+ # Freestyle Serverless Deployments - Full API Reference
2
+
3
+ Complete reference for `freestyle.serverless.deployments.create()`.
4
+
5
+ ## Import
6
+
7
+ ```javascript
8
+ import { freestyle, readFiles } from "freestyle-sandboxes"; // readFiles for local file deploys
9
+ ```
10
+
11
+ Requires `FREESTYLE_API_KEY` environment variable.
12
+
13
+ ## Source Types (exactly one required)
14
+
15
+ ### Git Repository
16
+
17
+ ```typescript
18
+ {
19
+ repo: "https://github.com/user/repo", // public URL, authenticated URL, or freestyle repo ID
20
+ branch: "main", // optional, defaults to default branch
21
+ rootPath: "./apps/web", // optional, for monorepos
22
+ }
23
+ ```
24
+
25
+ Private repos: use `https://user:token@github.com/user/repo.git` or a Freestyle Git repo ID.
26
+
27
+ ### Inline Code
28
+
29
+ ```typescript
30
+ {
31
+ code: `
32
+ import express from 'express';
33
+ const app = express();
34
+ app.get('/', (req, res) => res.send('Hello'));
35
+ app.listen(3000);
36
+ `,
37
+ nodeModules: { // required with code deploys
38
+ express: "^4.18.2",
39
+ },
40
+ }
41
+ ```
42
+
43
+ ### Local Files (readFiles)
44
+
45
+ ```javascript
46
+ import { readFiles } from "freestyle-sandboxes"; // NOT from /utils
47
+
48
+ const files = await readFiles("./dist"); // reads dir, excludes node_modules, base64-encodes
49
+ {
50
+ files,
51
+ entrypointPath: "server.js", // required for file deploys - MUST be a Node.js server, not an HTML file
52
+ }
53
+ ```
54
+
55
+ `readFiles` automatically excludes `node_modules/` and handles binary files. The `entrypointPath` MUST point to a Node.js server file (e.g., Express/Hono) — pointing it at an HTML file will not serve sub-assets.
56
+
57
+ ### Tar URL
58
+
59
+ ```typescript
60
+ {
61
+ tarUrl: "https://s3.example.com/signed-url/app.tar.gz",
62
+ }
63
+ ```
64
+
65
+ Useful when source is hosted externally (S3, GCS, etc).
66
+
67
+ ## Options
68
+
69
+ ### domains (required)
70
+
71
+ ```typescript
72
+ domains: ["my-app.style.dev"] // free *.style.dev subdomain
73
+ domains: ["app.style.dev", "app.yourdomain.com"] // multiple domains
74
+ ```
75
+
76
+ Any unclaimed `*.style.dev` subdomain works instantly with SSL. Custom domains need verification first.
77
+
78
+ ### build
79
+
80
+ ```typescript
81
+ build: true // auto-detect framework, run build
82
+ build: {
83
+ command: "npm run build", // custom build command
84
+ outDir: "dist", // build output directory
85
+ envVars: { // build-time env vars (NOT available at runtime)
86
+ NEXT_PUBLIC_API_URL: "https://api.example.com",
87
+ NODE_ENV: "production",
88
+ },
89
+ }
90
+ ```
91
+
92
+ Default: `false` (deploy files as-is). Set `true` for Next.js, Vite, and other frameworks requiring a build step. Freestyle auto-detects Next.js, Vite, and Expo.
93
+
94
+ ### entrypoint
95
+
96
+ ```typescript
97
+ entrypoint: "server.js" // main file of your application
98
+ ```
99
+
100
+ Auto-detected for Next.js, Vite, Expo. Specify manually for custom setups.
101
+
102
+ ### envVars
103
+
104
+ ```typescript
105
+ envVars: {
106
+ API_KEY: "secret-value", // available at RUNTIME only
107
+ DATABASE_URL: "postgres://...",
108
+ }
109
+ ```
110
+
111
+ NOT available at build time. For build-time vars, use `build.envVars`. Env vars are tied to deployments; to change them, create a new deployment.
112
+
113
+ ### nodeModules
114
+
115
+ ```typescript
116
+ nodeModules: {
117
+ express: "^4.18.2",
118
+ cors: "^2.8.5",
119
+ }
120
+ ```
121
+
122
+ Only needed for `code` deploys. For git/file deploys, include your lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, or `bun.lock`) and Freestyle installs from it.
123
+
124
+ ### timeoutMs
125
+
126
+ ```typescript
127
+ timeoutMs: 60000 // milliseconds, idle timeout before scale-down
128
+ ```
129
+
130
+ Timeout is from last TCP packet (not HTTP request), so WebSockets stay alive as long as you ping faster than the timeout.
131
+
132
+ ### networkPermissions
133
+
134
+ ```typescript
135
+ networkPermissions: [
136
+ { action: "allow", domain: "api.stripe.com", behavior: "exact" },
137
+ { action: "allow", domain: ".*\\.amazonaws\\.com", behavior: "regex" },
138
+ ]
139
+ ```
140
+
141
+ ### await
142
+
143
+ ```typescript
144
+ await: false // return immediately with deploymentId for polling
145
+ ```
146
+
147
+ Default: `true` (waits for deployment to build and propagate). Set `false` to return immediately.
148
+
149
+ ```typescript
150
+ // Polling pattern
151
+ const { deploymentId } = await freestyle.serverless.deployments.create({
152
+ ...,
153
+ await: false,
154
+ });
155
+ const status = await freestyle.serverless.deployments.get({ deploymentId });
156
+ ```
157
+
158
+ ### waitForRollout
159
+
160
+ ```typescript
161
+ waitForRollout: true // wait until deployment is fully serving traffic
162
+ ```
163
+
164
+ ### staticOnly
165
+
166
+ ```typescript
167
+ staticOnly: true,
168
+ publicDir: "dist", // directory with static files
169
+ cleanUrls: true, // /about.html becomes /about
170
+ ```
171
+
172
+ Serves files directly without a server entrypoint. For pure static sites.
173
+
174
+ ### headers
175
+
176
+ ```typescript
177
+ headers: [
178
+ {
179
+ source: "^/assets/.*$",
180
+ headers: [{ key: "Cache-Control", value: "max-age=31536000, immutable" }],
181
+ },
182
+ ]
183
+ ```
184
+
185
+ ### redirects
186
+
187
+ ```typescript
188
+ redirects: [
189
+ { source: "^/old-page$", destination: "/new-page", permanent: true },
190
+ ]
191
+ ```
192
+
193
+ ## Return Value
194
+
195
+ ```typescript
196
+ const { deployment, domains } = await freestyle.serverless.deployments.create({...});
197
+
198
+ deployment.deploymentId // string - unique deployment ID
199
+ domains // string[] - live URLs
200
+
201
+ // With await: false
202
+ const { deploymentId } = await freestyle.serverless.deployments.create({..., await: false});
203
+ ```
204
+
205
+ ## Custom Domains
206
+
207
+ ### Step 1: Verify ownership
208
+
209
+ ```typescript
210
+ const { record, instructions } = await freestyle.domains.verifications.create({
211
+ domain: "example.com",
212
+ });
213
+ // Add TXT record: _freestyle_custom_hostname.example.com → <verification-code>
214
+ ```
215
+
216
+ ### Step 2: Complete verification
217
+
218
+ ```typescript
219
+ await freestyle.domains.verifications.complete({ domain: "example.com" });
220
+ ```
221
+
222
+ ### Step 3: Configure DNS
223
+
224
+ Point domain to Freestyle:
225
+ - **APEX** (`example.com`): A record → `35.235.84.134`
226
+ - **Subdomain** (`app.example.com`): A record for `app` → `35.235.84.134`
227
+ - **Wildcard** (`*.example.com`): A record for `*` → `35.235.84.134`
228
+
229
+ ### Step 4: Deploy
230
+
231
+ ```typescript
232
+ await freestyle.serverless.deployments.create({
233
+ ...,
234
+ domains: ["example.com"],
235
+ });
236
+ ```
237
+
238
+ ## Framework Notes
239
+
240
+ ### Next.js
241
+
242
+ Requires in `next.config.mjs`:
243
+ ```javascript
244
+ const nextConfig = {
245
+ output: "standalone", // required
246
+ images: { unoptimized: true }, // required (no Sharp on Freestyle)
247
+ };
248
+ export default nextConfig;
249
+ ```
250
+
251
+ For local file deploys, after `npm run build`:
252
+ ```bash
253
+ cp -r public .next/standalone/public
254
+ cp -r .next/static .next/standalone/.next/static
255
+ cp package-lock.json .next/standalone/package-lock.json
256
+ ```
257
+ Then `readFiles(".next/standalone")` with `entrypointPath: "server.js"`.
258
+
259
+ ### Vite
260
+
261
+ Auto-detected when deploying from git with `build: true`. For local file deploys, use an Express static server:
262
+
263
+ ```javascript
264
+ import express from 'express';
265
+ import { fileURLToPath } from 'url';
266
+ import { dirname, join } from 'path';
267
+
268
+ const __dirname = dirname(fileURLToPath(import.meta.url));
269
+ const app = express();
270
+ app.use(express.static(__dirname));
271
+ app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html'))); // SPA fallback
272
+ app.listen(3000);
273
+ ```
274
+
275
+ > **WARNING**: Do NOT use `Deno.serve()` or Hono's `app.fire()`. Freestyle runs Node.js. Use `app.listen(3000)` (Express) or `serve({ fetch: app.fetch, port: 3000 })` from `@hono/node-server`.
276
+
277
+ ### Static Sites
278
+
279
+ Static sites MUST have an Express/Hono Node.js server entrypoint. Use the same Express pattern as Vite above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kortix/sandbox",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Kortix sandbox runtime — kortix-master, opencode config/agents/skills, and dependencies",
5
5
  "private": false,
6
6
  "scripts": {