@solcreek/cli 0.3.1 → 0.3.3

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.
@@ -0,0 +1,22 @@
1
+ export declare const devCommand: import("citty").CommandDef<{
2
+ port: {
3
+ type: "string";
4
+ description: string;
5
+ default: string;
6
+ };
7
+ reset: {
8
+ type: "boolean";
9
+ description: string;
10
+ };
11
+ json: {
12
+ type: "boolean";
13
+ description: string;
14
+ default: boolean;
15
+ };
16
+ yes: {
17
+ type: "boolean";
18
+ description: string;
19
+ default: boolean;
20
+ };
21
+ }>;
22
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { defineCommand } from "citty";
2
+ import { consola } from "consola";
3
+ import { resolveConfig, formatDetectionSummary } from "@solcreek/sdk";
4
+ import { globalArgs } from "../utils/output.js";
5
+ import { DevServer } from "../dev/server.js";
6
+ export const devCommand = defineCommand({
7
+ meta: {
8
+ name: "dev",
9
+ description: "Start local development server",
10
+ },
11
+ args: {
12
+ ...globalArgs,
13
+ port: {
14
+ type: "string",
15
+ description: "Port number (default: 3000)",
16
+ default: "3000",
17
+ },
18
+ reset: {
19
+ type: "boolean",
20
+ description: "Clear local data before starting",
21
+ },
22
+ },
23
+ async run({ args }) {
24
+ const cwd = process.cwd();
25
+ let config;
26
+ try {
27
+ config = resolveConfig(cwd);
28
+ }
29
+ catch (e) {
30
+ consola.error(e.message);
31
+ process.exit(1);
32
+ }
33
+ const port = parseInt(args.port, 10);
34
+ if (isNaN(port) || port < 0 || port > 65535) {
35
+ consola.error(`Invalid port: ${args.port}`);
36
+ process.exit(1);
37
+ }
38
+ consola.info(`Detected: ${formatDetectionSummary(config)}`);
39
+ const server = new DevServer({
40
+ cwd,
41
+ port,
42
+ config,
43
+ reset: !!args.reset,
44
+ });
45
+ // Graceful shutdown
46
+ const shutdown = async () => {
47
+ consola.info("Shutting down...");
48
+ await server.stop();
49
+ process.exit(0);
50
+ };
51
+ process.on("SIGINT", shutdown);
52
+ process.on("SIGTERM", shutdown);
53
+ try {
54
+ await server.start();
55
+ }
56
+ catch (e) {
57
+ consola.error(`Failed to start dev server: ${e.message}`);
58
+ await server.stop();
59
+ process.exit(1);
60
+ }
61
+ },
62
+ });
63
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1,26 @@
1
+ import { type Server, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { type LocalRealtimeServer } from "./local-realtime.js";
3
+ type ConnectServer = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
4
+ export interface DevProxyOptions {
5
+ /** User-facing port. */
6
+ port: number;
7
+ /** Miniflare worker URL (e.g. "http://127.0.0.1:8787"). Null if no worker. */
8
+ workerUrl: string | null;
9
+ /** Vite middleware. Null if no Vite (worker-only project). */
10
+ viteMiddleware: ConnectServer | null;
11
+ /** Local realtime server instance. */
12
+ realtimeServer: LocalRealtimeServer;
13
+ /** Project slug. */
14
+ projectSlug: string;
15
+ }
16
+ export declare class DevProxy {
17
+ private server;
18
+ private options;
19
+ constructor(options: DevProxyOptions);
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ /** Get the underlying HTTP server (for Vite HMR upgrade). */
23
+ getServer(): Server | null;
24
+ }
25
+ export {};
26
+ //# sourceMappingURL=dev-proxy.d.ts.map
@@ -0,0 +1,134 @@
1
+ // Dev proxy server for `creek dev`.
2
+ //
3
+ // Single user-facing HTTP server that routes:
4
+ // /__creek/config → local config response
5
+ // /{slug}/**/broadcast → realtime server (HTTP)
6
+ // /{slug}/**/ws → realtime server (WebSocket upgrade)
7
+ // /api/*, /__creek/* → worker (Miniflare)
8
+ // everything else → Vite (HMR) or worker (if no Vite)
9
+ import { createServer, request as httpRequest, } from "node:http";
10
+ import { parseRealtimePath, } from "./local-realtime.js";
11
+ export class DevProxy {
12
+ server = null;
13
+ options;
14
+ constructor(options) {
15
+ this.options = options;
16
+ }
17
+ async start() {
18
+ const { port, workerUrl, viteMiddleware, realtimeServer, projectSlug } = this.options;
19
+ this.server = createServer((req, res) => {
20
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
21
+ // 1. /__creek/config → local config
22
+ if (url.pathname === "/__creek/config" && req.method === "GET") {
23
+ res.writeHead(200, { "Content-Type": "application/json" });
24
+ res.end(JSON.stringify({
25
+ realtimeUrl: `http://localhost:${port}`,
26
+ projectSlug,
27
+ wsToken: null,
28
+ }));
29
+ return;
30
+ }
31
+ // 2. Realtime broadcast: POST /{slug}/**/broadcast
32
+ const route = parseRealtimePath(url.pathname);
33
+ if (route && route.action === "/broadcast" && req.method === "POST") {
34
+ realtimeServer.handleBroadcast(req, res);
35
+ return;
36
+ }
37
+ // 3. Realtime status: GET /{slug}/**/status
38
+ if (route && route.action === "/status" && req.method === "GET") {
39
+ realtimeServer.handleStatus(req, res);
40
+ return;
41
+ }
42
+ // 4. API/worker routes → proxy to Miniflare
43
+ if (workerUrl && isWorkerRoute(url.pathname)) {
44
+ proxyToWorker(req, res, workerUrl);
45
+ return;
46
+ }
47
+ // 5. Everything else → Vite middleware or worker fallback
48
+ if (viteMiddleware) {
49
+ viteMiddleware(req, res, () => {
50
+ // If Vite doesn't handle it and we have a worker, try worker
51
+ if (workerUrl) {
52
+ proxyToWorker(req, res, workerUrl);
53
+ }
54
+ else {
55
+ res.writeHead(404);
56
+ res.end("Not Found");
57
+ }
58
+ });
59
+ return;
60
+ }
61
+ // No Vite — all traffic to worker
62
+ if (workerUrl) {
63
+ proxyToWorker(req, res, workerUrl);
64
+ return;
65
+ }
66
+ res.writeHead(404);
67
+ res.end("Not Found");
68
+ });
69
+ // WebSocket upgrade routing
70
+ this.server.on("upgrade", (req, socket, head) => {
71
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
72
+ const route = parseRealtimePath(url.pathname);
73
+ // Realtime WebSocket: /{slug}/**/ws
74
+ if (route && route.action === "/ws") {
75
+ realtimeServer.handleUpgrade(req, socket, head);
76
+ return;
77
+ }
78
+ // Vite HMR WebSocket — pass through to Vite middleware
79
+ if (viteMiddleware) {
80
+ // Vite's middleware handles WebSocket upgrades via the http server's
81
+ // upgrade event. We need to let Vite handle it. Vite listens on the
82
+ // same server's upgrade event, so we should NOT destroy the socket.
83
+ // The Vite server is in middleware mode and registered its own
84
+ // upgrade handler already. We just don't interfere.
85
+ return;
86
+ }
87
+ socket.destroy();
88
+ });
89
+ return new Promise((resolve, reject) => {
90
+ this.server.on("error", reject);
91
+ this.server.listen(port, "127.0.0.1", () => resolve());
92
+ });
93
+ }
94
+ async stop() {
95
+ return new Promise((resolve) => {
96
+ if (this.server) {
97
+ this.server.close(() => resolve());
98
+ this.server = null;
99
+ }
100
+ else {
101
+ resolve();
102
+ }
103
+ });
104
+ }
105
+ /** Get the underlying HTTP server (for Vite HMR upgrade). */
106
+ getServer() {
107
+ return this.server;
108
+ }
109
+ }
110
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
111
+ function isWorkerRoute(pathname) {
112
+ return (pathname.startsWith("/api/") ||
113
+ pathname.startsWith("/__creek/") ||
114
+ pathname === "/__creek");
115
+ }
116
+ function proxyToWorker(req, res, workerUrl) {
117
+ const { hostname, port } = new URL(workerUrl);
118
+ const proxyReq = httpRequest({
119
+ hostname,
120
+ port,
121
+ path: req.url,
122
+ method: req.method,
123
+ headers: req.headers,
124
+ }, (proxyRes) => {
125
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
126
+ proxyRes.pipe(res);
127
+ });
128
+ proxyReq.on("error", () => {
129
+ res.writeHead(502);
130
+ res.end("Worker unavailable");
131
+ });
132
+ req.pipe(proxyReq);
133
+ }
134
+ //# sourceMappingURL=dev-proxy.js.map
@@ -0,0 +1,40 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { WebSocket } from "ws";
3
+ export interface RealtimeRoute {
4
+ slug: string;
5
+ roomId: string | null;
6
+ action: string;
7
+ }
8
+ export declare function parseRealtimePath(pathname: string): RealtimeRoute | null;
9
+ export declare function getDoName(route: RealtimeRoute): string;
10
+ export declare class LocalRealtimeServer {
11
+ private httpServer;
12
+ private wss;
13
+ private rooms;
14
+ private port;
15
+ constructor(options?: {
16
+ port?: number;
17
+ });
18
+ start(): Promise<{
19
+ port: number;
20
+ }>;
21
+ stop(): Promise<void>;
22
+ /** Broadcast a message to all connected clients in a room. */
23
+ broadcast(roomKey: string, message: object): void;
24
+ /** Get the number of connected clients in a room. */
25
+ getRoomCount(roomKey: string): number;
26
+ /** Get the port the server is listening on. */
27
+ getPort(): number;
28
+ /** @internal — inject a mock WebSocket into a room for testing. */
29
+ _testAddSocket(roomKey: string, ws: WebSocket): void;
30
+ /** Handle WebSocket upgrade from an external HTTP server. */
31
+ handleUpgrade(req: IncomingMessage, socket: import("node:net").Socket, head: Buffer): void;
32
+ /** Handle broadcast POST from an external HTTP server. */
33
+ handleBroadcast(req: IncomingMessage, res: ServerResponse): void;
34
+ /** Handle status GET from an external HTTP server. */
35
+ handleStatus(req: IncomingMessage, res: ServerResponse): void;
36
+ private addToRoom;
37
+ private broadcastPeers;
38
+ private handleHttp;
39
+ }
40
+ //# sourceMappingURL=local-realtime.d.ts.map
@@ -0,0 +1,242 @@
1
+ // Local realtime server for `creek dev`.
2
+ //
3
+ // Replaces the production Durable Object realtime service (rt.creek.dev)
4
+ // with a lightweight Node.js WebSocket server. Same URL routing and
5
+ // message format — client code works unchanged.
6
+ import { createServer, } from "node:http";
7
+ import { WebSocketServer, WebSocket } from "ws";
8
+ const VALID_ACTIONS = new Set(["/broadcast", "/ws", "/status"]);
9
+ export function parseRealtimePath(pathname) {
10
+ const parts = pathname.split("/").filter(Boolean);
11
+ if (parts.length < 2)
12
+ return null;
13
+ const slug = parts[0];
14
+ // Room-scoped: /{slug}/rooms/{roomId}/{action}
15
+ if (parts[1] === "rooms") {
16
+ if (parts.length < 4)
17
+ return null;
18
+ const roomId = parts[2];
19
+ const action = "/" + parts[3];
20
+ if (!VALID_ACTIONS.has(action))
21
+ return null;
22
+ if (!roomId)
23
+ return null;
24
+ return { slug, roomId, action };
25
+ }
26
+ // Project-wide (legacy): /{slug}/{action}
27
+ const action = "/" + parts.slice(1).join("/");
28
+ if (!VALID_ACTIONS.has(action))
29
+ return null;
30
+ return { slug, roomId: null, action };
31
+ }
32
+ export function getDoName(route) {
33
+ return route.roomId ? `${route.slug}:${route.roomId}` : route.slug;
34
+ }
35
+ // ─── LocalRealtimeServer ──────────────────────────────────────────────────────
36
+ export class LocalRealtimeServer {
37
+ httpServer = null;
38
+ wss = null;
39
+ rooms = new Map();
40
+ port;
41
+ constructor(options) {
42
+ this.port = options?.port ?? 0; // 0 = OS auto-assign
43
+ }
44
+ async start() {
45
+ this.httpServer = createServer((req, res) => this.handleHttp(req, res));
46
+ this.wss = new WebSocketServer({ noServer: true });
47
+ this.httpServer.on("upgrade", (req, socket, head) => {
48
+ const url = new URL(req.url ?? "/", "http://localhost");
49
+ const route = parseRealtimePath(url.pathname);
50
+ if (!route || route.action !== "/ws") {
51
+ socket.destroy();
52
+ return;
53
+ }
54
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
55
+ const roomKey = getDoName(route);
56
+ this.addToRoom(roomKey, ws);
57
+ });
58
+ });
59
+ return new Promise((resolve, reject) => {
60
+ this.httpServer.on("error", reject);
61
+ this.httpServer.listen(this.port, "127.0.0.1", () => {
62
+ const addr = this.httpServer.address();
63
+ if (typeof addr === "object" && addr) {
64
+ this.port = addr.port;
65
+ }
66
+ resolve({ port: this.port });
67
+ });
68
+ });
69
+ }
70
+ async stop() {
71
+ // Close all WebSocket connections
72
+ for (const [, room] of this.rooms) {
73
+ for (const ws of room) {
74
+ ws.close(1001, "Server shutting down");
75
+ }
76
+ }
77
+ this.rooms.clear();
78
+ // Close WebSocket server
79
+ if (this.wss) {
80
+ this.wss.close();
81
+ this.wss = null;
82
+ }
83
+ // Close HTTP server
84
+ return new Promise((resolve) => {
85
+ if (this.httpServer) {
86
+ this.httpServer.close(() => resolve());
87
+ this.httpServer = null;
88
+ }
89
+ else {
90
+ resolve();
91
+ }
92
+ });
93
+ }
94
+ /** Broadcast a message to all connected clients in a room. */
95
+ broadcast(roomKey, message) {
96
+ const room = this.rooms.get(roomKey);
97
+ if (!room)
98
+ return;
99
+ const data = JSON.stringify(message);
100
+ for (const ws of room) {
101
+ if (ws.readyState === WebSocket.OPEN) {
102
+ ws.send(data);
103
+ }
104
+ }
105
+ }
106
+ /** Get the number of connected clients in a room. */
107
+ getRoomCount(roomKey) {
108
+ return this.rooms.get(roomKey)?.size ?? 0;
109
+ }
110
+ /** Get the port the server is listening on. */
111
+ getPort() {
112
+ return this.port;
113
+ }
114
+ /** @internal — inject a mock WebSocket into a room for testing. */
115
+ _testAddSocket(roomKey, ws) {
116
+ let room = this.rooms.get(roomKey);
117
+ if (!room) {
118
+ room = new Set();
119
+ this.rooms.set(roomKey, room);
120
+ }
121
+ room.add(ws);
122
+ }
123
+ // ─── Public handlers (for DevProxy integration) ────────────────────────────
124
+ /** Handle WebSocket upgrade from an external HTTP server. */
125
+ handleUpgrade(req, socket, head) {
126
+ const url = new URL(req.url ?? "/", "http://localhost");
127
+ const route = parseRealtimePath(url.pathname);
128
+ if (!route || route.action !== "/ws") {
129
+ socket.destroy();
130
+ return;
131
+ }
132
+ if (!this.wss) {
133
+ // Lazy-init WSS if server not started standalone
134
+ this.wss = new WebSocketServer({ noServer: true });
135
+ }
136
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
137
+ const roomKey = getDoName(route);
138
+ this.addToRoom(roomKey, ws);
139
+ });
140
+ }
141
+ /** Handle broadcast POST from an external HTTP server. */
142
+ handleBroadcast(req, res) {
143
+ const url = new URL(req.url ?? "/", "http://localhost");
144
+ const route = parseRealtimePath(url.pathname);
145
+ if (!route || route.action !== "/broadcast" || req.method !== "POST") {
146
+ res.writeHead(404, { "Content-Type": "application/json" });
147
+ res.end(JSON.stringify({ error: "Not Found" }));
148
+ return;
149
+ }
150
+ let body = "";
151
+ req.on("data", (chunk) => {
152
+ body += chunk.toString();
153
+ });
154
+ req.on("end", () => {
155
+ let event;
156
+ try {
157
+ event = JSON.parse(body);
158
+ }
159
+ catch {
160
+ res.writeHead(400, { "Content-Type": "application/json" });
161
+ res.end(JSON.stringify({ error: "invalid JSON" }));
162
+ return;
163
+ }
164
+ const roomKey = getDoName(route);
165
+ this.broadcast(roomKey, {
166
+ type: "db_changed",
167
+ table: event.table ?? "unknown",
168
+ operation: event.operation ?? "UNKNOWN",
169
+ });
170
+ const count = this.rooms.get(roomKey)?.size ?? 0;
171
+ res.writeHead(200, { "Content-Type": "application/json" });
172
+ res.end(JSON.stringify({ ok: true, clients: count }));
173
+ });
174
+ }
175
+ /** Handle status GET from an external HTTP server. */
176
+ handleStatus(req, res) {
177
+ const url = new URL(req.url ?? "/", "http://localhost");
178
+ const route = parseRealtimePath(url.pathname);
179
+ if (!route) {
180
+ res.writeHead(404, { "Content-Type": "application/json" });
181
+ res.end(JSON.stringify({ error: "Not Found" }));
182
+ return;
183
+ }
184
+ const roomKey = getDoName(route);
185
+ const count = this.rooms.get(roomKey)?.size ?? 0;
186
+ res.writeHead(200, { "Content-Type": "application/json" });
187
+ res.end(JSON.stringify({ clients: count }));
188
+ }
189
+ // ─── Internal ───────────────────────────────────────────────────────────────
190
+ addToRoom(roomKey, ws) {
191
+ let room = this.rooms.get(roomKey);
192
+ if (!room) {
193
+ room = new Set();
194
+ this.rooms.set(roomKey, room);
195
+ }
196
+ room.add(ws);
197
+ this.broadcastPeers(roomKey);
198
+ ws.on("close", () => {
199
+ room.delete(ws);
200
+ if (room.size === 0)
201
+ this.rooms.delete(roomKey);
202
+ this.broadcastPeers(roomKey);
203
+ });
204
+ ws.on("error", () => {
205
+ room.delete(ws);
206
+ if (room.size === 0)
207
+ this.rooms.delete(roomKey);
208
+ this.broadcastPeers(roomKey);
209
+ });
210
+ }
211
+ broadcastPeers(roomKey) {
212
+ const count = this.rooms.get(roomKey)?.size ?? 0;
213
+ this.broadcast(roomKey, { type: "peers", count });
214
+ }
215
+ handleHttp(req, res) {
216
+ const url = new URL(req.url ?? "/", "http://localhost");
217
+ // Root health check
218
+ if (url.pathname === "/" || url.pathname === "") {
219
+ res.writeHead(200, { "Content-Type": "application/json" });
220
+ res.end(JSON.stringify({ service: "creek-realtime-local", status: "ok" }));
221
+ return;
222
+ }
223
+ const route = parseRealtimePath(url.pathname);
224
+ if (!route) {
225
+ res.writeHead(404, { "Content-Type": "application/json" });
226
+ res.end(JSON.stringify({ error: "Not Found" }));
227
+ return;
228
+ }
229
+ // Delegate to public handlers
230
+ if (route.action === "/broadcast" && req.method === "POST") {
231
+ this.handleBroadcast(req, res);
232
+ return;
233
+ }
234
+ if (route.action === "/status" && req.method === "GET") {
235
+ this.handleStatus(req, res);
236
+ return;
237
+ }
238
+ res.writeHead(404, { "Content-Type": "application/json" });
239
+ res.end(JSON.stringify({ error: "Not Found" }));
240
+ }
241
+ }
242
+ //# sourceMappingURL=local-realtime.js.map
@@ -0,0 +1,5 @@
1
+ /** Check if a port is available on localhost. */
2
+ export declare function isPortAvailable(port: number): Promise<boolean>;
3
+ /** Find an available port, starting from the preferred port. */
4
+ export declare function findAvailablePort(preferred?: number): Promise<number>;
5
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,34 @@
1
+ // Port allocation utilities for `creek dev`.
2
+ import { createServer } from "node:net";
3
+ /** Check if a port is available on localhost. */
4
+ export function isPortAvailable(port) {
5
+ return new Promise((resolve) => {
6
+ const server = createServer();
7
+ server.once("error", () => resolve(false));
8
+ server.listen(port, "127.0.0.1", () => {
9
+ server.close(() => resolve(true));
10
+ });
11
+ });
12
+ }
13
+ /** Find an available port, starting from the preferred port. */
14
+ export async function findAvailablePort(preferred = 3000) {
15
+ if (await isPortAvailable(preferred))
16
+ return preferred;
17
+ // Try a few ports around the preferred one
18
+ for (let offset = 1; offset <= 10; offset++) {
19
+ const port = preferred + offset;
20
+ if (await isPortAvailable(port))
21
+ return port;
22
+ }
23
+ // Fall back to OS-assigned port
24
+ return new Promise((resolve, reject) => {
25
+ const server = createServer();
26
+ server.once("error", reject);
27
+ server.listen(0, "127.0.0.1", () => {
28
+ const addr = server.address();
29
+ const port = typeof addr === "object" && addr ? addr.port : 0;
30
+ server.close(() => resolve(port));
31
+ });
32
+ });
33
+ }
34
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,18 @@
1
+ import type { ResolvedConfig } from "@solcreek/sdk";
2
+ export interface DevServerOptions {
3
+ cwd: string;
4
+ port: number;
5
+ config: ResolvedConfig;
6
+ reset: boolean;
7
+ }
8
+ export declare class DevServer {
9
+ private options;
10
+ private realtimeServer;
11
+ private workerRunner;
12
+ private viteBridge;
13
+ private proxy;
14
+ constructor(options: DevServerOptions);
15
+ start(): Promise<void>;
16
+ stop(): Promise<void>;
17
+ }
18
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,143 @@
1
+ // DevServer orchestrator for `creek dev`.
2
+ //
3
+ // Manages the lifecycle of all subsystems:
4
+ // 1. LocalRealtimeServer — WebSocket broadcast
5
+ // 2. WorkerRunner — Miniflare with D1/KV/R2
6
+ // 3. ViteBridge — Client-side HMR
7
+ // 4. DevProxy — User-facing HTTP server
8
+ import { existsSync, rmSync, mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { consola } from "consola";
11
+ import { LocalRealtimeServer } from "./local-realtime.js";
12
+ import { WorkerRunner } from "./worker-runner.js";
13
+ import { ViteBridge } from "./vite-bridge.js";
14
+ import { DevProxy } from "./dev-proxy.js";
15
+ import { findAvailablePort } from "./ports.js";
16
+ export class DevServer {
17
+ options;
18
+ realtimeServer = null;
19
+ workerRunner = null;
20
+ viteBridge = null;
21
+ proxy = null;
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ async start() {
26
+ const { cwd, port, config, reset } = this.options;
27
+ const persistDir = join(cwd, ".creek", "dev");
28
+ const startTime = Date.now();
29
+ // 1. Handle --reset
30
+ if (reset && existsSync(persistDir)) {
31
+ rmSync(persistDir, { recursive: true, force: true });
32
+ consola.info("Cleared local data");
33
+ }
34
+ // Create persistence directory
35
+ mkdirSync(persistDir, { recursive: true });
36
+ // 2. Start realtime server
37
+ this.realtimeServer = new LocalRealtimeServer({ port: 0 });
38
+ await this.realtimeServer.start();
39
+ // 3. Start worker (if project has a worker entry)
40
+ let workerUrl = null;
41
+ if (config.workerEntry) {
42
+ this.workerRunner = new WorkerRunner({
43
+ entryPoint: config.workerEntry,
44
+ cwd,
45
+ bindings: config.bindings,
46
+ persistDir,
47
+ realtimeUrl: `http://127.0.0.1:${this.realtimeServer.getPort()}`,
48
+ projectSlug: config.projectName,
49
+ vars: config.vars,
50
+ onRebuild: (ms) => {
51
+ consola.info(`Worker rebuilt in ${ms}ms`);
52
+ },
53
+ });
54
+ const { port: workerPort } = await this.workerRunner.start();
55
+ workerUrl = `http://127.0.0.1:${workerPort}`;
56
+ }
57
+ // 4. Start Vite (if project has a frontend framework)
58
+ let viteMiddleware = null;
59
+ const hasFramework = config.framework !== null;
60
+ if (hasFramework) {
61
+ this.viteBridge = new ViteBridge({ cwd });
62
+ try {
63
+ await this.viteBridge.start();
64
+ viteMiddleware = this.viteBridge.middlewares;
65
+ }
66
+ catch (e) {
67
+ if (e.message?.includes("Vite is not installed")) {
68
+ consola.warn(e.message);
69
+ this.viteBridge = null;
70
+ }
71
+ else {
72
+ throw e;
73
+ }
74
+ }
75
+ }
76
+ // 5. Resolve port (auto-find if occupied)
77
+ const actualPort = await findAvailablePort(port);
78
+ if (actualPort !== port) {
79
+ consola.warn(`Port ${port} is in use, using ${actualPort} instead`);
80
+ }
81
+ // 6. Start proxy
82
+ this.proxy = new DevProxy({
83
+ port: actualPort,
84
+ workerUrl,
85
+ viteMiddleware,
86
+ realtimeServer: this.realtimeServer,
87
+ projectSlug: config.projectName,
88
+ });
89
+ await this.proxy.start();
90
+ // 7. Wire Vite's HMR to the proxy's HTTP server
91
+ if (this.viteBridge && this.proxy.getServer()) {
92
+ const httpServer = this.proxy.getServer();
93
+ // Vite's middleware mode needs to handle upgrade events
94
+ // from our proxy server for HMR WebSocket
95
+ const viteWss = this.viteBridge.viteServer?.ws;
96
+ if (viteWss) {
97
+ httpServer.on("upgrade", (req, socket, head) => {
98
+ // Only handle Vite's HMR paths
99
+ if (req.url?.startsWith("/__vite") ||
100
+ req.url?.startsWith("/@vite")) {
101
+ viteWss.handleUpgrade(req, socket, head);
102
+ }
103
+ });
104
+ }
105
+ }
106
+ // 8. Print status
107
+ const elapsed = Date.now() - startTime;
108
+ const bindings = config.bindings
109
+ .filter((b) => ["d1", "kv", "r2", "ai"].includes(b.type))
110
+ .map((b) => b.type.toUpperCase());
111
+ const bindingStr = bindings.length > 0 ? ` (${bindings.join(", ")})` : "";
112
+ console.log("");
113
+ consola.success(`creek dev\n`);
114
+ consola.info(`App: http://localhost:${actualPort}`);
115
+ if (config.workerEntry) {
116
+ consola.info(`Worker: ${config.workerEntry}${bindingStr}`);
117
+ }
118
+ consola.info(`Realtime: ws://localhost:${actualPort}`);
119
+ consola.info(`Data: .creek/dev/`);
120
+ console.log("");
121
+ consola.success(`Ready in ${elapsed}ms`);
122
+ }
123
+ async stop() {
124
+ // Stop in reverse order
125
+ if (this.proxy) {
126
+ await this.proxy.stop();
127
+ this.proxy = null;
128
+ }
129
+ if (this.viteBridge) {
130
+ await this.viteBridge.stop();
131
+ this.viteBridge = null;
132
+ }
133
+ if (this.workerRunner) {
134
+ await this.workerRunner.stop();
135
+ this.workerRunner = null;
136
+ }
137
+ if (this.realtimeServer) {
138
+ await this.realtimeServer.stop();
139
+ this.realtimeServer = null;
140
+ }
141
+ }
142
+ }
143
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,15 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ type ConnectMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
3
+ export declare class ViteBridge {
4
+ private viteServer;
5
+ private cwd;
6
+ constructor(options: {
7
+ cwd: string;
8
+ });
9
+ start(): Promise<void>;
10
+ stop(): Promise<void>;
11
+ /** Vite's Connect middleware stack — plug into the proxy server. */
12
+ get middlewares(): ConnectMiddleware;
13
+ }
14
+ export {};
15
+ //# sourceMappingURL=vite-bridge.d.ts.map
@@ -0,0 +1,42 @@
1
+ // Vite dev server integration for `creek dev`.
2
+ //
3
+ // Uses Vite's middleware mode — no separate HTTP server.
4
+ // HMR WebSocket is handled through Vite's own middleware.
5
+ export class ViteBridge {
6
+ viteServer = null; // ViteDevServer — dynamically imported
7
+ cwd;
8
+ constructor(options) {
9
+ this.cwd = options.cwd;
10
+ }
11
+ async start() {
12
+ let vite;
13
+ try {
14
+ vite = await import("vite");
15
+ }
16
+ catch {
17
+ throw new Error("[creek dev] Vite is not installed. Run `npm install -D vite` to enable client-side HMR.");
18
+ }
19
+ this.viteServer = await vite.createServer({
20
+ root: this.cwd,
21
+ server: {
22
+ middlewareMode: true,
23
+ hmr: true,
24
+ },
25
+ appType: "spa",
26
+ });
27
+ }
28
+ async stop() {
29
+ if (this.viteServer) {
30
+ await this.viteServer.close();
31
+ this.viteServer = null;
32
+ }
33
+ }
34
+ /** Vite's Connect middleware stack — plug into the proxy server. */
35
+ get middlewares() {
36
+ if (!this.viteServer) {
37
+ throw new Error("ViteBridge not started");
38
+ }
39
+ return this.viteServer.middlewares;
40
+ }
41
+ }
42
+ //# sourceMappingURL=vite-bridge.js.map
@@ -0,0 +1,61 @@
1
+ import type { BindingDeclaration } from "@solcreek/sdk";
2
+ export interface WorkerRunnerOptions {
3
+ /** User's worker entry point (e.g. "worker/index.ts"). */
4
+ entryPoint: string;
5
+ /** Project root directory. */
6
+ cwd: string;
7
+ /** Declared bindings from creek.toml / wrangler config. */
8
+ bindings: BindingDeclaration[];
9
+ /** Persistence directory for D1/KV/R2 data. */
10
+ persistDir: string;
11
+ /** Local realtime server URL (e.g. "http://localhost:8788"). */
12
+ realtimeUrl: string;
13
+ /** Project slug (e.g. "realtime-todos"). */
14
+ projectSlug: string;
15
+ /** User-defined environment variables from config. */
16
+ vars?: Record<string, string>;
17
+ /** Whether the project has client assets (Worker + SPA hybrid). */
18
+ hasClientAssets?: boolean;
19
+ /** Callback when worker is rebuilt. */
20
+ onRebuild?: (durationMs: number) => void;
21
+ /**
22
+ * Additional node module resolution paths for esbuild.
23
+ * @internal — used by tests to resolve `creek` from monorepo.
24
+ */
25
+ nodePaths?: string[];
26
+ }
27
+ export declare class WorkerRunner {
28
+ private mf;
29
+ private esbuildCtx;
30
+ private options;
31
+ private mfOptions;
32
+ private port;
33
+ constructor(options: WorkerRunnerOptions);
34
+ start(): Promise<{
35
+ port: number;
36
+ }>;
37
+ stop(): Promise<void>;
38
+ /** Get the URL the worker is running on. */
39
+ getUrl(): string;
40
+ /** Get the port the worker is running on. */
41
+ getPort(): number;
42
+ /** Dispatch a fetch request to the worker (via Miniflare). */
43
+ dispatchFetch(input: string, init?: RequestInit): Promise<Response>;
44
+ private buildMiniflareOptions;
45
+ private get esbuildOptions();
46
+ private bundle;
47
+ private startWatching;
48
+ }
49
+ /**
50
+ * Build Miniflare options from bindings — extracted for testing.
51
+ * @internal
52
+ */
53
+ export declare function buildMiniflareBindingOptions(bindings: BindingDeclaration[]): {
54
+ hasD1: boolean;
55
+ hasKV: boolean;
56
+ hasR2: boolean;
57
+ d1BindingName: string;
58
+ kvBindingName: string;
59
+ r2BindingName: string;
60
+ };
61
+ //# sourceMappingURL=worker-runner.d.ts.map
@@ -0,0 +1,203 @@
1
+ // Worker execution for `creek dev` — bundles user code and runs it via Miniflare.
2
+ //
3
+ // Uses esbuild watch mode for hot reload and Miniflare for D1/KV/R2 simulation.
4
+ import { writeFileSync, mkdirSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { context } from "esbuild";
7
+ import { Miniflare } from "miniflare";
8
+ import { generateWorkerWrapper } from "../utils/worker-bundle.js";
9
+ const NODE_BUILTINS = [
10
+ "node:async_hooks",
11
+ "node:stream",
12
+ "node:stream/web",
13
+ "node:buffer",
14
+ "node:util",
15
+ "node:events",
16
+ "node:crypto",
17
+ "node:path",
18
+ "node:url",
19
+ "node:string_decoder",
20
+ "node:diagnostics_channel",
21
+ "node:process",
22
+ "node:fs",
23
+ "node:os",
24
+ "node:child_process",
25
+ "node:http",
26
+ "node:https",
27
+ "node:net",
28
+ "node:tls",
29
+ "node:zlib",
30
+ "node:perf_hooks",
31
+ "node:worker_threads",
32
+ ];
33
+ export class WorkerRunner {
34
+ mf = null;
35
+ esbuildCtx = null;
36
+ options;
37
+ mfOptions = {};
38
+ port = 0;
39
+ constructor(options) {
40
+ this.options = options;
41
+ }
42
+ async start() {
43
+ const { cwd, entryPoint, persistDir } = this.options;
44
+ // 1. Generate worker wrapper
45
+ const wrapperDir = join(cwd, ".creek", "dev");
46
+ mkdirSync(wrapperDir, { recursive: true });
47
+ const entryAbsolute = resolve(cwd, entryPoint);
48
+ const wrapper = generateWorkerWrapper(entryAbsolute, wrapperDir, {
49
+ hasClientAssets: this.options.hasClientAssets,
50
+ });
51
+ const wrapperPath = join(wrapperDir, "__worker_entry.js");
52
+ writeFileSync(wrapperPath, wrapper);
53
+ // 2. Initial bundle with esbuild
54
+ const bundledScript = await this.bundle(wrapperPath);
55
+ // 3. Start Miniflare
56
+ this.mfOptions = this.buildMiniflareOptions(bundledScript, persistDir);
57
+ this.mf = new Miniflare(this.mfOptions);
58
+ // Wait for Miniflare to be ready and get the port
59
+ const readyUrl = await this.mf.ready;
60
+ // mf.ready returns a URL object
61
+ this.port = readyUrl.port ? parseInt(String(readyUrl.port), 10) : 8787;
62
+ // 4. Start esbuild watch mode for hot reload
63
+ await this.startWatching(wrapperPath);
64
+ return { port: this.port };
65
+ }
66
+ async stop() {
67
+ if (this.esbuildCtx) {
68
+ await this.esbuildCtx.dispose();
69
+ this.esbuildCtx = null;
70
+ }
71
+ if (this.mf) {
72
+ await this.mf.dispose();
73
+ this.mf = null;
74
+ }
75
+ }
76
+ /** Get the URL the worker is running on. */
77
+ getUrl() {
78
+ return `http://127.0.0.1:${this.port}`;
79
+ }
80
+ /** Get the port the worker is running on. */
81
+ getPort() {
82
+ return this.port;
83
+ }
84
+ /** Dispatch a fetch request to the worker (via Miniflare). */
85
+ async dispatchFetch(input, init) {
86
+ if (!this.mf)
87
+ throw new Error("WorkerRunner not started");
88
+ return this.mf.dispatchFetch(input, init);
89
+ }
90
+ // ─── Internal ───────────────────────────────────────────────────────────────
91
+ buildMiniflareOptions(script, persistDir) {
92
+ const { bindings, projectSlug, realtimeUrl, vars } = this.options;
93
+ const hasD1 = bindings.some((b) => b.type === "d1");
94
+ const hasKV = bindings.some((b) => b.type === "kv");
95
+ const hasR2 = bindings.some((b) => b.type === "r2");
96
+ const d1BindingName = bindings.find((b) => b.type === "d1")?.name ?? "DB";
97
+ const kvBindingName = bindings.find((b) => b.type === "kv")?.name ?? "KV";
98
+ const r2BindingName = bindings.find((b) => b.type === "r2")?.name ?? "STORAGE";
99
+ const opts = {
100
+ modules: true,
101
+ script,
102
+ compatibilityDate: "2024-12-01",
103
+ compatibilityFlags: ["nodejs_compat"],
104
+ // Environment variables
105
+ bindings: {
106
+ CREEK_PROJECT_SLUG: projectSlug,
107
+ CREEK_REALTIME_URL: realtimeUrl,
108
+ // CREEK_REALTIME_SECRET intentionally omitted (dev mode = no auth)
109
+ ...vars,
110
+ },
111
+ };
112
+ // D1 — SQLite-backed
113
+ if (hasD1) {
114
+ opts.d1Databases = { [d1BindingName]: "creek-dev-db" };
115
+ opts.d1Persist = persistDir ? join(persistDir, "d1") : false;
116
+ }
117
+ // KV
118
+ if (hasKV) {
119
+ opts.kvNamespaces = { [kvBindingName]: "creek-dev-kv" };
120
+ opts.kvPersist = persistDir ? join(persistDir, "kv") : false;
121
+ }
122
+ // R2
123
+ if (hasR2) {
124
+ opts.r2Buckets = { [r2BindingName]: "creek-dev-r2" };
125
+ opts.r2Persist = persistDir ? join(persistDir, "r2") : false;
126
+ }
127
+ return opts;
128
+ }
129
+ get esbuildOptions() {
130
+ return {
131
+ absWorkingDir: this.options.cwd,
132
+ bundle: true,
133
+ format: "esm",
134
+ platform: "neutral",
135
+ target: "es2022",
136
+ write: false,
137
+ minify: false,
138
+ external: NODE_BUILTINS,
139
+ conditions: ["workerd", "worker", "import"],
140
+ mainFields: ["module", "main"],
141
+ logLevel: "warning",
142
+ ...(this.options.nodePaths?.length
143
+ ? { nodePaths: this.options.nodePaths }
144
+ : {}),
145
+ };
146
+ }
147
+ async bundle(entryPoint) {
148
+ const result = await import("esbuild").then((esbuild) => esbuild.build({
149
+ entryPoints: [entryPoint],
150
+ ...this.esbuildOptions,
151
+ }));
152
+ if (result.errors.length > 0) {
153
+ throw new Error(`esbuild: ${result.errors.map((e) => e.text).join(", ")}`);
154
+ }
155
+ return result.outputFiles[0].text;
156
+ }
157
+ async startWatching(entryPoint) {
158
+ this.esbuildCtx = await context({
159
+ entryPoints: [entryPoint],
160
+ ...this.esbuildOptions,
161
+ plugins: [
162
+ {
163
+ name: "creek-hot-reload",
164
+ setup: (build) => {
165
+ build.onEnd(async (result) => {
166
+ if (result.errors.length > 0 || !result.outputFiles?.length) {
167
+ return;
168
+ }
169
+ const start = Date.now();
170
+ const newScript = result.outputFiles[0].text;
171
+ try {
172
+ await this.mf?.setOptions({
173
+ ...this.mfOptions,
174
+ script: newScript,
175
+ });
176
+ this.options.onRebuild?.(Date.now() - start);
177
+ }
178
+ catch {
179
+ // Reload failed — keep running with old version
180
+ }
181
+ });
182
+ },
183
+ },
184
+ ],
185
+ });
186
+ await this.esbuildCtx.watch();
187
+ }
188
+ }
189
+ /**
190
+ * Build Miniflare options from bindings — extracted for testing.
191
+ * @internal
192
+ */
193
+ export function buildMiniflareBindingOptions(bindings) {
194
+ return {
195
+ hasD1: bindings.some((b) => b.type === "d1"),
196
+ hasKV: bindings.some((b) => b.type === "kv"),
197
+ hasR2: bindings.some((b) => b.type === "r2"),
198
+ d1BindingName: bindings.find((b) => b.type === "d1")?.name ?? "DB",
199
+ kvBindingName: bindings.find((b) => b.type === "kv")?.name ?? "KV",
200
+ r2BindingName: bindings.find((b) => b.type === "r2")?.name ?? "STORAGE",
201
+ };
202
+ }
203
+ //# sourceMappingURL=worker-runner.js.map
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
+ import { createRequire } from "node:module";
6
7
  import { loginCommand } from "./commands/login.js";
7
8
  import { whoamiCommand } from "./commands/whoami.js";
8
9
  import { initCommand } from "./commands/init.js";
@@ -13,15 +14,28 @@ import { domainsCommand } from "./commands/domains.js";
13
14
  import { projectsCommand } from "./commands/projects.js";
14
15
  import { deploymentsCommand } from "./commands/deployments.js";
15
16
  import { statusCommand } from "./commands/status.js";
17
+ import { devCommand } from "./commands/dev.js";
16
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
19
+ const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
20
+ // Read version from the "creek" facade package (what users install),
21
+ // falling back to CLI's own version if not available.
22
+ let version = cliPkg.version;
23
+ try {
24
+ const require = createRequire(import.meta.url);
25
+ const facadePkg = require("creek/package.json");
26
+ version = facadePkg.version;
27
+ }
28
+ catch {
29
+ // Running outside facade (e.g. workspace dev) — use CLI version
30
+ }
18
31
  const main = defineCommand({
19
32
  meta: {
20
33
  name: "creek",
21
- version: pkg.version,
34
+ version,
22
35
  description: "Deploy full-stack apps to the edge",
23
36
  },
24
37
  subCommands: {
38
+ dev: devCommand,
25
39
  deploy: deployCommand,
26
40
  status: statusCommand,
27
41
  projects: projectsCommand,
@@ -23,7 +23,7 @@ export function generateWorkerWrapper(entryPoint, wrapperDir, options) {
23
23
  // - Fall back to the worker handler for API routes
24
24
  // - SPA fallback: serve index.html for extensionless paths
25
25
  if (hasAssets) {
26
- return `import { _setEnv, _setCtx, generateWsToken } from "creek";
26
+ return `import { _runRequest, generateWsToken } from "creek";
27
27
  import userModule from "${importPath}";
28
28
 
29
29
  const handler = userModule.default ?? userModule;
@@ -39,81 +39,80 @@ function hasExtension(pathname) {
39
39
 
40
40
  export default {
41
41
  async fetch(request, env, ctx) {
42
- _setEnv(env);
43
- _setCtx(ctx);
44
- const url = new URL(request.url);
42
+ return _runRequest(env, ctx, async () => {
43
+ const url = new URL(request.url);
45
44
 
46
- // /__creek/config — auto-discovery for CreekRoom WebSocket
47
- if (url.pathname === "/__creek/config" && request.method === "GET") {
48
- const wsToken = await generateWsToken();
49
- return new Response(JSON.stringify({
50
- realtimeUrl: env.CREEK_REALTIME_URL || null,
51
- projectSlug: env.CREEK_PROJECT_SLUG || null,
52
- wsToken,
53
- }), { headers: { "Content-Type": "application/json" } });
54
- }
55
-
56
- // API routes → always go to the worker handler
57
- if (isApiPath(url.pathname)) {
58
- if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
59
- if (typeof handler === "function") return handler(request, env, ctx);
60
- }
45
+ // /__creek/config — auto-discovery for CreekRoom WebSocket
46
+ if (url.pathname === "/__creek/config" && request.method === "GET") {
47
+ const wsToken = await generateWsToken();
48
+ return new Response(JSON.stringify({
49
+ realtimeUrl: env.CREEK_REALTIME_URL || null,
50
+ projectSlug: env.CREEK_PROJECT_SLUG || null,
51
+ wsToken,
52
+ }), { headers: { "Content-Type": "application/json" } });
53
+ }
61
54
 
62
- // Static assetstry WfP Static Assets API
63
- if (env.ASSETS) {
64
- try {
65
- const assetResponse = await env.ASSETS.fetch(request);
66
- if (assetResponse.status !== 404) return assetResponse;
67
- } catch {}
55
+ // API routesalways go to the worker handler
56
+ if (isApiPath(url.pathname)) {
57
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
58
+ if (typeof handler === "function") return handler(request, env, ctx);
59
+ }
68
60
 
69
- // SPA fallback: extensionless paths index.html
70
- if (!hasExtension(url.pathname)) {
61
+ // Static assets try WfP Static Assets API
62
+ if (env.ASSETS) {
71
63
  try {
72
- const indexReq = new Request(new URL("/index.html", request.url), request);
73
- const indexResponse = await env.ASSETS.fetch(indexReq);
74
- if (indexResponse.status !== 404) {
75
- return new Response(indexResponse.body, {
76
- status: 200,
77
- headers: indexResponse.headers,
78
- });
79
- }
64
+ const assetResponse = await env.ASSETS.fetch(request);
65
+ if (assetResponse.status !== 404) return assetResponse;
80
66
  } catch {}
67
+
68
+ // SPA fallback: extensionless paths → index.html
69
+ if (!hasExtension(url.pathname)) {
70
+ try {
71
+ const indexReq = new Request(new URL("/index.html", request.url), request);
72
+ const indexResponse = await env.ASSETS.fetch(indexReq);
73
+ if (indexResponse.status !== 404) {
74
+ return new Response(indexResponse.body, {
75
+ status: 200,
76
+ headers: indexResponse.headers,
77
+ });
78
+ }
79
+ } catch {}
80
+ }
81
81
  }
82
- }
83
82
 
84
- // Fallback: pass to worker handler
85
- if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
86
- if (typeof handler === "function") return handler(request, env, ctx);
87
- return new Response("Not Found", { status: 404 });
83
+ // Fallback: pass to worker handler
84
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
85
+ if (typeof handler === "function") return handler(request, env, ctx);
86
+ return new Response("Not Found", { status: 404 });
87
+ });
88
88
  },
89
89
  };
90
90
  `;
91
91
  }
92
92
  // Pure Worker (no client assets)
93
- return `import { _setEnv, _setCtx, generateWsToken } from "creek";
93
+ return `import { _runRequest, generateWsToken } from "creek";
94
94
  import userModule from "${importPath}";
95
95
 
96
96
  const handler = userModule.default ?? userModule;
97
97
 
98
98
  export default {
99
99
  async fetch(request, env, ctx) {
100
- _setEnv(env);
101
- _setCtx(ctx);
102
-
103
- // /__creek/config auto-discovery for CreekRoom WebSocket
104
- const url = new URL(request.url);
105
- if (url.pathname === "/__creek/config" && request.method === "GET") {
106
- const wsToken = await generateWsToken();
107
- return new Response(JSON.stringify({
108
- realtimeUrl: env.CREEK_REALTIME_URL || null,
109
- projectSlug: env.CREEK_PROJECT_SLUG || null,
110
- wsToken,
111
- }), { headers: { "Content-Type": "application/json" } });
112
- }
100
+ return _runRequest(env, ctx, async () => {
101
+ // /__creek/config — auto-discovery for CreekRoom WebSocket
102
+ const url = new URL(request.url);
103
+ if (url.pathname === "/__creek/config" && request.method === "GET") {
104
+ const wsToken = await generateWsToken();
105
+ return new Response(JSON.stringify({
106
+ realtimeUrl: env.CREEK_REALTIME_URL || null,
107
+ projectSlug: env.CREEK_PROJECT_SLUG || null,
108
+ wsToken,
109
+ }), { headers: { "Content-Type": "application/json" } });
110
+ }
113
111
 
114
- if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
115
- if (typeof handler === "function") return handler(request, env, ctx);
116
- throw new Error("[creek] Worker must export default a fetch handler, Hono app, or { fetch() } object.");
112
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
113
+ if (typeof handler === "function") return handler(request, env, ctx);
114
+ throw new Error("[creek] Worker must export default a fetch handler, Hono app, or { fetch() } object.");
115
+ });
117
116
  },
118
117
  };
119
118
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,12 @@
10
10
  "!dist/**/*.map",
11
11
  "LICENSE"
12
12
  ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "typecheck": "tsc --noEmit",
17
+ "clean": "rm -rf dist"
18
+ },
13
19
  "keywords": [
14
20
  "creek",
15
21
  "cli",
@@ -26,11 +32,13 @@
26
32
  "directory": "packages/cli"
27
33
  },
28
34
  "dependencies": {
35
+ "@solcreek/sdk": "workspace:*",
29
36
  "citty": "^0.1.6",
30
37
  "consola": "^3.4.2",
31
38
  "esbuild": "^0.25.0",
39
+ "miniflare": "^4.20260317.3",
32
40
  "smol-toml": "^1.3.1",
33
- "@solcreek/sdk": "0.1.0-alpha.3"
41
+ "ws": "^8.20.0"
34
42
  },
35
43
  "optionalDependencies": {
36
44
  "@solcreek/adapter-creek": "*"
@@ -40,16 +48,12 @@
40
48
  "@testing-library/react": "^16.3.2",
41
49
  "@types/node": "^22.19.15",
42
50
  "@types/react-dom": "^19",
51
+ "@types/ws": "^8.18.1",
43
52
  "hono": "^4.7.4",
44
53
  "jsdom": "^29.0.1",
45
54
  "react": "^19.2.4",
46
55
  "react-dom": "^19.2.4",
47
- "typescript": "^5.8.2"
48
- },
49
- "scripts": {
50
- "build": "tsc",
51
- "dev": "tsc --watch",
52
- "typecheck": "tsc --noEmit",
53
- "clean": "rm -rf dist"
56
+ "typescript": "^5.8.2",
57
+ "vite": "^6.3.5"
54
58
  }
55
- }
59
+ }