@helipod/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.
@@ -0,0 +1,2653 @@
1
+ import {
2
+ handleHttpRequest
3
+ } from "./chunk-N5J276OX.js";
4
+ import {
5
+ loadProject
6
+ } from "./chunk-EPHSHGFU.js";
7
+
8
+ // src/load-config.ts
9
+ import { existsSync } from "fs";
10
+ import { join } from "path";
11
+ import { pathToFileURL } from "url";
12
+ var CACHE_BUST = () => `?t=${Date.now()}`;
13
+ async function loadConfig(projectDir) {
14
+ const path = ["helipod.config.ts", "helipod.config.js"].map((f) => join(projectDir, f)).find((p) => existsSync(p));
15
+ if (!path) return { components: [] };
16
+ const mod = await import(pathToFileURL(path).href + CACHE_BUST());
17
+ const cfg = mod.default ?? mod;
18
+ return { components: cfg.components ?? [], deploy: cfg.deploy, functionsDir: cfg.functionsDir };
19
+ }
20
+
21
+ // src/functions-dir.ts
22
+ import { existsSync as existsSync2 } from "fs";
23
+ import { dirname, isAbsolute, join as join2, resolve } from "path";
24
+ var DEFAULT_FUNCTIONS_DIR = "helipod";
25
+ async function resolveFunctionsDir(flagValue, cwd) {
26
+ if (flagValue !== void 0 && flagValue !== "") {
27
+ const functionsDir = isAbsolute(flagValue) ? flagValue : resolve(cwd, flagValue);
28
+ return { functionsDir, projectRoot: dirname(functionsDir) };
29
+ }
30
+ const projectRoot = resolve(cwd);
31
+ const config = await loadConfig(projectRoot);
32
+ const name = config.functionsDir ?? DEFAULT_FUNCTIONS_DIR;
33
+ return { functionsDir: isAbsolute(name) ? name : join2(projectRoot, name), projectRoot };
34
+ }
35
+ function functionsDirNotFoundMessage(functionsDir) {
36
+ return `no functions directory found at ${functionsDir}
37
+
38
+ If this is a Convex app, run \`helipod migrate\` to convert it. It renames
39
+ convex/ to helipod/ and rewrites your imports.
40
+
41
+ Otherwise create ${join2(functionsDir, "schema.ts")}, or point at an existing folder:
42
+ helipod dev --dir <path>
43
+ `;
44
+ }
45
+ function ensureFunctionsDirExists(functionsDir) {
46
+ if (existsSync2(functionsDir)) return true;
47
+ process.stderr.write(functionsDirNotFoundMessage(functionsDir));
48
+ return false;
49
+ }
50
+
51
+ // src/dev-options.ts
52
+ function resolveDevOptions(options = {}) {
53
+ return {
54
+ port: options.port ?? 3e3,
55
+ ip: options.ip ?? "127.0.0.1",
56
+ functionsDir: options.functionsDir ?? DEFAULT_FUNCTIONS_DIR,
57
+ dataPath: options.dataPath ?? ".helipod/data.db",
58
+ runtime: options.runtime ?? "auto",
59
+ webDir: options.webDir,
60
+ databaseUrl: options.databaseUrl ?? process.env.HELIPOD_DATABASE_URL,
61
+ storageBucket: options.storageBucket,
62
+ storageEndpoint: options.storageEndpoint
63
+ };
64
+ }
65
+ function detectRuntime() {
66
+ return typeof globalThis.Bun !== "undefined" ? "bun" : "node";
67
+ }
68
+
69
+ // src/load-modules.ts
70
+ import { createHash } from "crypto";
71
+ import { readdirSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
72
+ import { join as join3, resolve as resolve2, dirname as dirname2 } from "path";
73
+ import { pathToFileURL as pathToFileURL2, fileURLToPath } from "url";
74
+ import { build } from "esbuild";
75
+ var CACHE_BUST2 = () => `?t=${Date.now()}`;
76
+ function moduleKeyForFile(file) {
77
+ return file.replace(/\.(ts|js)$/, "");
78
+ }
79
+ function listFunctionModuleFiles(absDir) {
80
+ const isModule = (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".d.ts") && !f.startsWith("_") && f !== "schema.ts" && f !== "schema.js";
81
+ return readdirSync(absDir).filter(isModule);
82
+ }
83
+ function nearestNodeModulesRoot(startDir) {
84
+ let dir = resolve2(startDir);
85
+ for (; ; ) {
86
+ if (existsSync3(join3(dir, "node_modules"))) return dir;
87
+ const parent = dirname2(dir);
88
+ if (parent === dir) return void 0;
89
+ dir = parent;
90
+ }
91
+ }
92
+ function resolveCacheDir(startDir) {
93
+ const dir = nearestNodeModulesRoot(startDir) ?? // Fallback: the dir containing the CLI's own node_modules (found by walking up from this module).
94
+ (nearestNodeModulesRoot(dirname2(fileURLToPath(import.meta.url))) ?? resolve2(startDir));
95
+ const ns = createHash("sha256").update(resolve2(startDir)).digest("hex").slice(0, 16);
96
+ const cacheDir = join3(dir, "node_modules", ".cache", "helipod", ns);
97
+ mkdirSync(cacheDir, { recursive: true });
98
+ return cacheDir;
99
+ }
100
+ function extraBundleExternals() {
101
+ const raw = process.env.HELIPOD_BUNDLE_EXTERNAL;
102
+ if (!raw) return [];
103
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
104
+ }
105
+ async function bundleAndImport(file, key, cacheDir) {
106
+ const result = await build({
107
+ entryPoints: [file],
108
+ bundle: true,
109
+ external: ["@helipod/*", ...extraBundleExternals()],
110
+ format: "esm",
111
+ platform: "node",
112
+ write: false,
113
+ sourcemap: "inline",
114
+ logLevel: "silent"
115
+ });
116
+ const code = result.outputFiles[0].text;
117
+ const outFile = join3(cacheDir, `${key.replace(/[\\/]/g, "__")}.mjs`);
118
+ writeFileSync(outFile, code);
119
+ return await import(pathToFileURL2(outFile).href + CACHE_BUST2());
120
+ }
121
+ async function loadFunctionsDir(dir) {
122
+ const absDir = resolve2(dir);
123
+ const entries = listFunctionModuleFiles(absDir);
124
+ const cacheDir = resolveCacheDir(absDir);
125
+ const schemaFile = existsSync3(join3(absDir, "schema.ts")) ? "schema.ts" : "schema.js";
126
+ const schemaModule = await bundleAndImport(join3(absDir, schemaFile), "schema", cacheDir);
127
+ const modules = {};
128
+ for (const file of entries) {
129
+ const key = moduleKeyForFile(file);
130
+ modules[key] = await bundleAndImport(join3(absDir, file), key, cacheDir);
131
+ }
132
+ return { schema: schemaModule.default, modules };
133
+ }
134
+
135
+ // src/push-pipeline.ts
136
+ import { generateAll } from "@helipod/codegen";
137
+ function push(loaded, components = [], existingTableNumbers) {
138
+ const project = loadProject(loaded, components, existingTableNumbers);
139
+ const generated = generateAll({
140
+ schema: project.schemaJson,
141
+ manifest: project.manifest,
142
+ tableNumbers: project.tableNumbers,
143
+ components: components.map((c) => ({ name: c.name, contextType: c.contextType, serverExports: c.serverExports }))
144
+ });
145
+ return { project, generated };
146
+ }
147
+
148
+ // src/boot.ts
149
+ import { mkdirSync as mkdirSync2, readFileSync, accessSync, constants as fsConstants } from "fs";
150
+ import { createRequire } from "module";
151
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "path";
152
+ import { randomUUID } from "crypto";
153
+ import { NodeSqliteAdapter, BunSqliteAdapter, SqliteDocStore } from "@helipod/docstore-sqlite";
154
+ import { NodePgClient, BunSqlClient, PostgresDocStore } from "@helipod/docstore-postgres";
155
+ import {
156
+ createEmbeddedRuntime
157
+ } from "@helipod/runtime-embedded";
158
+ import { InMemoryLogSink } from "@helipod/executor";
159
+ import { AdminApi, browseTableModule, systemModules, verifyAdminKey } from "@helipod/admin";
160
+ import { shardIdList, DEFAULT_SHARD } from "@helipod/id-codec";
161
+ import {
162
+ storageContextProvider,
163
+ storageReaper,
164
+ storageModules,
165
+ storageRoutes
166
+ } from "@helipod/storage";
167
+ import { receiptsReaper } from "@helipod/receipts";
168
+
169
+ // src/blobstore-select.ts
170
+ import { join as join4 } from "path";
171
+ import { FsBlobStore } from "@helipod/blobstore-fs";
172
+ import { S3BlobStore } from "@helipod/blobstore-s3";
173
+ function isS3Config(storage) {
174
+ return Boolean(storage?.bucket);
175
+ }
176
+ function resolveStorageConfig(env, flags) {
177
+ return {
178
+ bucket: flags?.bucket ?? env.HELIPOD_STORAGE_BUCKET,
179
+ endpoint: flags?.endpoint ?? env.HELIPOD_STORAGE_ENDPOINT,
180
+ region: flags?.region ?? env.HELIPOD_STORAGE_REGION,
181
+ publicBaseUrl: flags?.publicBaseUrl ?? env.HELIPOD_STORAGE_PUBLIC_URL,
182
+ accessKeyId: flags?.accessKeyId ?? env.AWS_ACCESS_KEY_ID,
183
+ secretAccessKey: flags?.secretAccessKey ?? env.AWS_SECRET_ACCESS_KEY,
184
+ ...flags?.forcePathStyle !== void 0 ? { forcePathStyle: flags.forcePathStyle } : {}
185
+ };
186
+ }
187
+ function makeBlobStore(opts) {
188
+ const s = opts.storage;
189
+ if (isS3Config(s)) {
190
+ return new S3BlobStore({
191
+ bucket: s.bucket,
192
+ region: s.region,
193
+ endpoint: s.endpoint,
194
+ accessKeyId: s.accessKeyId,
195
+ secretAccessKey: s.secretAccessKey,
196
+ forcePathStyle: s.forcePathStyle,
197
+ publicBaseUrl: s.publicBaseUrl
198
+ });
199
+ }
200
+ return new FsBlobStore({ root: join4(opts.dataPath, "storage") });
201
+ }
202
+
203
+ // src/objectstore-select.ts
204
+ import { FsObjectStore } from "@helipod/objectstore-fs";
205
+ import { S3ObjectStore } from "@helipod/objectstore-s3";
206
+ function parseBoolParam(v) {
207
+ if (v === null) return void 0;
208
+ if (v === "true") return true;
209
+ if (v === "false") return false;
210
+ throw new Error(`helipod: invalid --object-store URL \u2014 forcePathStyle must be "true" or "false", got "${v}"`);
211
+ }
212
+ function parseS3ObjectStoreUrl(url, env = process.env) {
213
+ let u;
214
+ try {
215
+ u = new URL(url);
216
+ } catch (e) {
217
+ throw new Error(`helipod: invalid --object-store URL "${url}": ${e.message}`);
218
+ }
219
+ const bucket = decodeURIComponent(u.pathname.replace(/^\//, "").split("/")[0] ?? "");
220
+ if (!bucket) {
221
+ throw new Error(
222
+ `helipod: --object-store S3 URL "${url}" has no bucket \u2014 use s3://host/<bucket> (or s3:///<bucket> for real AWS S3 with no custom endpoint).`
223
+ );
224
+ }
225
+ const endpointProtocol = u.protocol === "s3+https:" ? "https" : "http";
226
+ const explicitEndpoint = u.searchParams.get("endpoint") ?? void 0;
227
+ const endpoint = explicitEndpoint ?? (u.hostname ? `${endpointProtocol}://${u.hostname}${u.port ? `:${u.port}` : ""}` : void 0);
228
+ const region = u.searchParams.get("region") ?? void 0;
229
+ const forcePathStyle = parseBoolParam(u.searchParams.get("forcePathStyle"));
230
+ const accessKeyId = (u.username ? decodeURIComponent(u.username) : void 0) ?? env.AWS_ACCESS_KEY_ID;
231
+ const secretAccessKey = (u.password ? decodeURIComponent(u.password) : void 0) ?? env.AWS_SECRET_ACCESS_KEY;
232
+ if (!accessKeyId || !secretAccessKey) {
233
+ throw new Error(
234
+ `helipod: --object-store S3 URL "${url}" is missing credentials \u2014 supply them in the URL (s3://accessKeyId:secretAccessKey@\u2026) or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.`
235
+ );
236
+ }
237
+ return { bucket, endpoint, region, accessKeyId, secretAccessKey, forcePathStyle };
238
+ }
239
+ function parseFsObjectStorePath(url) {
240
+ return url.startsWith("file://") ? url.slice("file://".length) : url;
241
+ }
242
+ var SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.\-]*):\/\//;
243
+ var SUPPORTED_SCHEMES = "s3://, s3+http://, s3+https://, file:// (or a bare filesystem path with no scheme)";
244
+ function resolveObjectStore(url, env = process.env) {
245
+ const trimmed = url?.trim();
246
+ if (!trimmed) return null;
247
+ const schemeMatch = trimmed.match(SCHEME_RE);
248
+ if (!schemeMatch) {
249
+ return { objectStore: new FsObjectStore({ dir: parseFsObjectStorePath(trimmed) }), kind: "fs" };
250
+ }
251
+ switch (schemeMatch[1]) {
252
+ case "s3":
253
+ case "s3+http":
254
+ case "s3+https": {
255
+ const config = parseS3ObjectStoreUrl(trimmed, env);
256
+ return { objectStore: new S3ObjectStore(config), kind: "s3" };
257
+ }
258
+ case "file":
259
+ return { objectStore: new FsObjectStore({ dir: parseFsObjectStorePath(trimmed) }), kind: "fs" };
260
+ default:
261
+ throw new Error(
262
+ `helipod: --object-store URL "${trimmed}" uses an unsupported scheme "${schemeMatch[1]}://" \u2014 supported schemes are ${SUPPORTED_SCHEMES}.`
263
+ );
264
+ }
265
+ }
266
+
267
+ // src/replica-forward.ts
268
+ function trimTrailingSlash(url) {
269
+ return url.endsWith("/") ? url.slice(0, -1) : url;
270
+ }
271
+ var ReplicaWriteForwarder = class {
272
+ constructor(writerUrl) {
273
+ this.writerUrl = writerUrl;
274
+ }
275
+ writerUrl;
276
+ /** A replica is never the writer for any shard — every mutation/action forwards. Consulted
277
+ * fresh on every call (mirrors the fleet forwarder's own "never cached" contract) — cheap
278
+ * here since it's a constant, not a live lease read. */
279
+ isLocalWriter(_shardId) {
280
+ return false;
281
+ }
282
+ async forward(kind, path, args, identity, _shardId, dedup) {
283
+ const body = {
284
+ path,
285
+ args,
286
+ kind,
287
+ forwarded: true,
288
+ ...dedup ? { clientId: dedup.clientId, seq: dedup.seq } : {}
289
+ };
290
+ let res;
291
+ try {
292
+ res = await fetch(`${trimTrailingSlash(this.writerUrl)}/api/run`, {
293
+ method: "POST",
294
+ headers: {
295
+ "content-type": "application/json",
296
+ ...identity !== null ? { authorization: `Bearer ${identity}` } : {}
297
+ },
298
+ body: JSON.stringify(body)
299
+ });
300
+ } catch (e) {
301
+ throw new Error(
302
+ `helipod: replica write-forward to writer "${this.writerUrl}" failed (unreachable) for ${kind} "${path}" \u2014 ${e instanceof Error ? e.message : String(e)}`
303
+ );
304
+ }
305
+ const text = await res.text();
306
+ let parsed = {};
307
+ try {
308
+ parsed = text ? JSON.parse(text) : {};
309
+ } catch {
310
+ parsed = {};
311
+ }
312
+ if (!res.ok || parsed.error !== void 0) {
313
+ throw new Error(
314
+ parsed.error ?? `helipod: replica write-forward to writer "${this.writerUrl}" returned HTTP ${res.status} for ${kind} "${path}"`
315
+ );
316
+ }
317
+ if (parsed.clientReplay) return { value: parsed.clientReplay.value ?? null, replay: parsed.clientReplay };
318
+ return {
319
+ value: parsed.value ?? null,
320
+ commitTs: parsed.commitTs !== void 0 ? Number(parsed.commitTs) : void 0
321
+ };
322
+ }
323
+ };
324
+
325
+ // src/boot.ts
326
+ function isPostgresUrl(s) {
327
+ return !!s && /^postgres(ql)?:\/\//.test(s);
328
+ }
329
+ function makePgClient(connectionString) {
330
+ return detectRuntime() === "bun" ? new BunSqlClient({ connectionString }) : new NodePgClient({ connectionString });
331
+ }
332
+ function makeStore(opts) {
333
+ if (isPostgresUrl(opts.databaseUrl)) {
334
+ return new PostgresDocStore(makePgClient(opts.databaseUrl));
335
+ }
336
+ mkdirSync2(dirname3(resolve3(opts.dataPath)), { recursive: true });
337
+ const adapter = detectRuntime() === "bun" ? new BunSqliteAdapter({ path: opts.dataPath }) : new NodeSqliteAdapter({ path: opts.dataPath });
338
+ return new SqliteDocStore(adapter);
339
+ }
340
+ var OBJECTSTORE_SUBSTRATE_ERR_NO_PACKAGE = "helipod: --object-store requires @helipod/objectstore-substrate \u2014 install it (bun add @helipod/objectstore-substrate).";
341
+ function isObjectStoreBootFailFast(e) {
342
+ return e instanceof Error && /^helipod: (--object-store|invalid --object-store)\b/.test(e.message);
343
+ }
344
+ async function loadObjectStoreSubstrateModule() {
345
+ try {
346
+ const specifier = "@helipod/objectstore-substrate";
347
+ return await import(specifier);
348
+ } catch {
349
+ throw new Error(OBJECTSTORE_SUBSTRATE_ERR_NO_PACKAGE);
350
+ }
351
+ }
352
+ async function acquireWithRetry(store, opts) {
353
+ const now = opts.now ?? Date.now;
354
+ const pollIntervalMs = opts.pollIntervalMs ?? 1e3;
355
+ const deadline = now() + opts.timeoutMs;
356
+ let last;
357
+ for (; ; ) {
358
+ const result = await store.acquire({ writerId: opts.writerId, leaseTtlMs: opts.leaseTtlMs, now: now() });
359
+ if (result.acquired) return;
360
+ last = { heldBy: result.heldBy, expiresAt: result.expiresAt };
361
+ if (now() >= deadline) {
362
+ throw new Error(
363
+ `helipod: --object-store shard "0" held by '${last.heldBy}' until ${new Date(last.expiresAt).toISOString()} \u2014 timed out after ${opts.timeoutMs}ms waiting for the lease to free up. If '${last.heldBy}' crashed, its lease will expire on its own and a retry will take over; otherwise stop that writer before starting this one.`
364
+ );
365
+ }
366
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
367
+ }
368
+ }
369
+ function makeLocalSqliteStore(dataPath) {
370
+ mkdirSync2(dirname3(resolve3(dataPath)), { recursive: true });
371
+ const adapter = detectRuntime() === "bun" ? new BunSqliteAdapter({ path: dataPath }) : new NodeSqliteAdapter({ path: dataPath });
372
+ return new SqliteDocStore(adapter);
373
+ }
374
+ function makeInMemorySqliteStore() {
375
+ const adapter = detectRuntime() === "bun" ? new BunSqliteAdapter({ path: ":memory:" }) : new NodeSqliteAdapter({ path: ":memory:" });
376
+ return new SqliteDocStore(adapter);
377
+ }
378
+ var RUNTIME_DEPLOYMENT_ID_GLOBAL_KEY = "fleet:deploymentId";
379
+ var DEFAULT_OBJECTSTORE_LEASE_TTL_MS = 15e3;
380
+ var DEFAULT_OBJECTSTORE_HEARTBEAT_MS = 5e3;
381
+ var DEFAULT_OBJECTSTORE_GC_MS = 6e4;
382
+ async function buildObjectStoreWriterNode(opts) {
383
+ const resolved = resolveObjectStore(opts.objectStoreUrl);
384
+ if (resolved === null) {
385
+ throw new Error(`helipod: --object-store "${opts.objectStoreUrl}" did not resolve to a store (empty/unset value?).`);
386
+ }
387
+ await resolved.objectStore.assertCasSupported();
388
+ const substrate = await loadObjectStoreSubstrateModule();
389
+ const writerId = opts.writerId ?? randomUUID();
390
+ const leaseTtlMs = opts.leaseTtlMs ?? DEFAULT_OBJECTSTORE_LEASE_TTL_MS;
391
+ const heartbeatMs = opts.heartbeatMs ?? DEFAULT_OBJECTSTORE_HEARTBEAT_MS;
392
+ const acquireTimeoutMs = opts.acquireTimeoutMs ?? leaseTtlMs + 5e3;
393
+ const gcMs = opts.gcMs ?? DEFAULT_OBJECTSTORE_GC_MS;
394
+ const desiredShards = opts.shards && opts.shards > 1 ? opts.shards : 1;
395
+ const globals = await substrate.ensureGlobals(resolved.objectStore, {
396
+ deploymentId: randomUUID(),
397
+ numShards: desiredShards
398
+ });
399
+ const numShards = globals.numShards;
400
+ if (opts.shards !== void 0 && opts.shards !== numShards) {
401
+ throw new Error(
402
+ `helipod: --shards ${opts.shards} disagrees with the bucket's persisted shard count ${numShards} \u2014 reshard the bucket (\`helipod objectstore reshard --object-store <url> --dir <dir> --shards ${opts.shards}\`) to change it, or drop --shards.`
403
+ );
404
+ }
405
+ const shardIds = numShards > 1 ? [...shardIdList(numShards)] : ["0"];
406
+ const buildLane = async (shardId, laneDataPath) => {
407
+ const local = makeLocalSqliteStore(laneDataPath);
408
+ const lane = await substrate.ObjectStoreDocStore.open({ objectStore: resolved.objectStore, shard: shardId, local });
409
+ await lane.writeGlobalIfAbsent(RUNTIME_DEPLOYMENT_ID_GLOBAL_KEY, globals.deploymentId);
410
+ await acquireWithRetry(lane, {
411
+ writerId,
412
+ leaseTtlMs,
413
+ timeoutMs: acquireTimeoutMs,
414
+ ...opts.acquirePollIntervalMs !== void 0 ? { pollIntervalMs: opts.acquirePollIntervalMs } : {}
415
+ });
416
+ const heartbeat = substrate.leaseHeartbeatDriver(lane, {
417
+ leaseTtlMs,
418
+ heartbeatMs,
419
+ onFenced: (e) => opts.onFenced?.(e)
420
+ });
421
+ const gc = substrate.gcDriver(lane, { sweepMs: gcMs });
422
+ return { lane, heartbeat, gc };
423
+ };
424
+ const built = [];
425
+ for (const shardId of shardIds) {
426
+ const laneDataPath = numShards > 1 ? `${opts.dataPath}.${shardId}` : opts.dataPath;
427
+ built.push({ shardId, ...await buildLane(shardId, laneDataPath) });
428
+ }
429
+ const drivers = built.flatMap((b) => [b.heartbeat, b.gc]);
430
+ const release = async () => {
431
+ await Promise.all(built.map((b) => b.lane.relinquish()));
432
+ };
433
+ if (numShards === 1) return { store: built[0].lane, drivers, release, numShards };
434
+ const lanes = new Map(built.map((b) => [b.shardId, b.lane]));
435
+ const store = new substrate.ShardedObjectStoreDocStore(lanes, { defaultShard: DEFAULT_SHARD });
436
+ return { store, drivers, release, numShards };
437
+ }
438
+ var REPLICA_WRITE_REJECTED_MESSAGE = "helipod: this node is a read replica (--replica) \u2014 it holds no write lease and cannot commit mutations. Send writes to the primary/writer node.";
439
+ var LEASE_OWNER_REJECTION_RE = /not the lease owner/;
440
+ function wrapReplicaWriteRejection(store) {
441
+ return new Proxy(store, {
442
+ get(target, prop, receiver) {
443
+ const value = Reflect.get(target, prop, receiver);
444
+ if (typeof value !== "function") return value;
445
+ const bound = value.bind(target);
446
+ if (prop !== "commitWrite" && prop !== "commitWriteBatch") return bound;
447
+ return async (...args) => {
448
+ try {
449
+ return await bound(...args);
450
+ } catch (e) {
451
+ if (e instanceof Error && LEASE_OWNER_REJECTION_RE.test(e.message)) {
452
+ throw new Error(REPLICA_WRITE_REJECTED_MESSAGE);
453
+ }
454
+ throw e;
455
+ }
456
+ };
457
+ }
458
+ });
459
+ }
460
+ function defaultReplicaConsumerId() {
461
+ return `replica-${randomUUID()}`;
462
+ }
463
+ var DEFAULT_OBJECTSTORE_REPLICA_POLL_MS = 1e3;
464
+ async function buildObjectStoreReplicaNode(opts) {
465
+ const resolved = resolveObjectStore(opts.objectStoreUrl);
466
+ if (resolved === null) {
467
+ throw new Error(`helipod: --object-store "${opts.objectStoreUrl}" did not resolve to a store (empty/unset value?).`);
468
+ }
469
+ await resolved.objectStore.assertCasSupported();
470
+ const substrate = await loadObjectStoreSubstrateModule();
471
+ const objectStore = resolved.objectStore;
472
+ const globals = await substrate.ensureGlobals(objectStore, { deploymentId: randomUUID(), numShards: 1 });
473
+ const numShards = globals.numShards;
474
+ const shardIds = numShards > 1 ? [...shardIdList(numShards)] : ["0"];
475
+ const lanes = [];
476
+ for (const shardId of shardIds) {
477
+ const laneDataPath = numShards > 1 ? `${opts.dataPath}.${shardId}` : opts.dataPath;
478
+ const local = makeLocalSqliteStore(laneDataPath);
479
+ const laneStore = await substrate.ObjectStoreDocStore.open({ objectStore, shard: shardId, local });
480
+ await laneStore.writeGlobalIfAbsent(RUNTIME_DEPLOYMENT_ID_GLOBAL_KEY, globals.deploymentId);
481
+ lanes.push({ shardId, store: laneStore, local });
482
+ }
483
+ const composite = numShards === 1 ? lanes[0].store : new substrate.ShardedObjectStoreDocStore(new Map(lanes.map((l) => [l.shardId, l.store])), {
484
+ defaultShard: DEFAULT_SHARD
485
+ });
486
+ const store = wrapReplicaWriteRejection(composite);
487
+ const baseConsumerId = opts.consumerId ?? defaultReplicaConsumerId();
488
+ const pollMs = opts.pollMs ?? DEFAULT_OBJECTSTORE_REPLICA_POLL_MS;
489
+ const laneConsumerId = (shardId) => numShards === 1 ? baseConsumerId : `${baseConsumerId}:${shardId}`;
490
+ return {
491
+ store,
492
+ numShards,
493
+ attachTailer: (runtime) => {
494
+ const handles = lanes.map(
495
+ (l) => substrate.startReplicaReactiveTailer({
496
+ runtime,
497
+ objectStore,
498
+ shard: l.shardId,
499
+ local: l.local,
500
+ consumerId: laneConsumerId(l.shardId),
501
+ pollMs
502
+ })
503
+ );
504
+ return async () => {
505
+ await Promise.all(handles.map((h) => h.stop()));
506
+ await Promise.all(lanes.map((l) => substrate.removeConsumer(objectStore, l.shardId, laneConsumerId(l.shardId))));
507
+ };
508
+ }
509
+ };
510
+ }
511
+ var DEFAULT_NUM_SHARDS = 8;
512
+ var NUM_SHARDS_GLOBAL_KEY = "fleet:numShards";
513
+ function parseNumShards(raw) {
514
+ if (raw === void 0 || raw.trim() === "") return void 0;
515
+ const n = Number(raw);
516
+ return Number.isFinite(n) && Number.isInteger(n) && n >= 1 ? n : void 0;
517
+ }
518
+ function groupCommitEnabled(raw) {
519
+ return /^(1|true|yes)$/i.test(raw ?? "");
520
+ }
521
+ function resolveGroupCommit(opts) {
522
+ const raw = opts.envRaw;
523
+ if (raw !== void 0 && raw !== "") return groupCommitEnabled(raw);
524
+ return isPostgresUrl(opts.databaseUrl);
525
+ }
526
+ function numShardsMismatchError(envValue, persisted) {
527
+ return new Error(
528
+ `helipod: HELIPOD_FLEET_SHARDS=${envValue} conflicts with the shard count already persisted for this deployment (${persisted}, set at first boot). The shard count is immutable after first boot \u2014 changing it live isn't supported; resharding is a planned offline tool (B5). Unset HELIPOD_FLEET_SHARDS, or set it to ${persisted} to match the existing deployment.`
529
+ );
530
+ }
531
+ async function resolveNumShards(store, envValue) {
532
+ const persistedRaw = await store.getGlobal(NUM_SHARDS_GLOBAL_KEY);
533
+ if (persistedRaw !== null) {
534
+ const persisted = Number(persistedRaw);
535
+ if (envValue !== void 0 && envValue !== persisted) throw numShardsMismatchError(envValue, persisted);
536
+ return persisted;
537
+ }
538
+ const resolved = envValue ?? DEFAULT_NUM_SHARDS;
539
+ const wrote = await store.writeGlobalIfAbsent(NUM_SHARDS_GLOBAL_KEY, String(resolved));
540
+ if (wrote) return resolved;
541
+ const raced = Number(await store.getGlobal(NUM_SHARDS_GLOBAL_KEY));
542
+ if (envValue !== void 0 && envValue !== raced) throw numShardsMismatchError(envValue, raced);
543
+ return raced;
544
+ }
545
+ function withStorageModules(map) {
546
+ return { ...map, ...storageModules };
547
+ }
548
+ function assertStorageConfigCoherent(storage) {
549
+ if (isS3Config(storage)) return;
550
+ const s3Shaped = storage?.endpoint !== void 0 || storage?.region !== void 0 || storage?.publicBaseUrl !== void 0;
551
+ if (!s3Shaped) return;
552
+ throw new Error(
553
+ "helipod: S3 storage settings (HELIPOD_STORAGE_ENDPOINT/REGION/PUBLIC_URL or --storage-endpoint/etc.) are set, but no bucket was provided \u2014 the S3 backend is selected only by HELIPOD_STORAGE_BUCKET (or --storage-bucket). Set the bucket to use S3, or unset the other storage settings to use local FS. Refusing to boot rather than silently store uploads on local disk."
554
+ );
555
+ }
556
+ function ensureStorageDirWritable(dir) {
557
+ try {
558
+ mkdirSync2(dir, { recursive: true });
559
+ accessSync(dir, fsConstants.W_OK);
560
+ } catch (e) {
561
+ throw new Error(
562
+ `helipod: file-storage directory "${dir}" is not creatable/writable \u2014 ${e instanceof Error ? e.message : String(e)}. Point --data at a writable location, or configure S3 storage (set HELIPOD_STORAGE_BUCKET).`
563
+ );
564
+ }
565
+ }
566
+ async function bootLoaded(opts) {
567
+ const { project, generated } = push(opts.loaded, opts.components);
568
+ const logSink = new InMemoryLogSink();
569
+ if (opts.objectStoreUrl !== void 0 && opts.fleet) {
570
+ throw new Error("helipod: --object-store cannot be combined with --fleet (Tier 2) \u2014 pick one write-scaling story.");
571
+ }
572
+ if (opts.replica && opts.objectStoreUrl === void 0) {
573
+ throw new Error(
574
+ "helipod: --replica requires --object-store \u2014 a replica materializes from an object-storage bucket; pass --object-store <url>."
575
+ );
576
+ }
577
+ const objectStoreWriterNode = opts.objectStoreUrl !== void 0 && !opts.replica ? await buildObjectStoreWriterNode({
578
+ objectStoreUrl: opts.objectStoreUrl,
579
+ dataPath: opts.dataPath,
580
+ onFenced: opts.objectStoreOnFenced,
581
+ leaseTtlMs: opts.objectStoreLeaseTtlMs,
582
+ heartbeatMs: opts.objectStoreHeartbeatMs,
583
+ acquireTimeoutMs: opts.objectStoreAcquireTimeoutMs,
584
+ acquirePollIntervalMs: opts.objectStoreAcquirePollIntervalMs,
585
+ writerId: opts.objectStoreWriterId,
586
+ gcMs: opts.objectStoreGcMs,
587
+ ...opts.objectStoreShards !== void 0 ? { shards: opts.objectStoreShards } : {}
588
+ }) : void 0;
589
+ const objectStoreReplicaNode = opts.objectStoreUrl !== void 0 && opts.replica ? await buildObjectStoreReplicaNode({
590
+ objectStoreUrl: opts.objectStoreUrl,
591
+ dataPath: opts.dataPath,
592
+ consumerId: opts.objectStoreReplicaConsumerId,
593
+ pollMs: opts.objectStoreReplicaPollMs
594
+ }) : void 0;
595
+ const store = opts.fleet?.store ?? objectStoreWriterNode?.store ?? objectStoreReplicaNode?.store ?? makeStore({ dataPath: opts.dataPath, databaseUrl: opts.databaseUrl });
596
+ if (objectStoreReplicaNode && opts.writerUrl !== void 0 && objectStoreReplicaNode.numShards > 1) {
597
+ throw new Error(
598
+ `helipod: --writer-url (replica write-forwarding) is not yet supported on a multi-shard bucket (this bucket has ${objectStoreReplicaNode.numShards} shards) \u2014 run the replica in reject mode (drop --writer-url) and send writes directly to the writer, or use a single-shard deployment.`
599
+ );
600
+ }
601
+ const replicaWriteForwarder = objectStoreReplicaNode && opts.writerUrl !== void 0 ? new ReplicaWriteForwarder(opts.writerUrl) : void 0;
602
+ let numShards;
603
+ if (opts.fleet) {
604
+ numShards = opts.fleet.numShards ?? 1;
605
+ } else if (objectStoreWriterNode || objectStoreReplicaNode) {
606
+ await store.setupSchema();
607
+ numShards = objectStoreWriterNode?.numShards ?? objectStoreReplicaNode?.numShards ?? 1;
608
+ } else {
609
+ await store.setupSchema();
610
+ numShards = await resolveNumShards(store, parseNumShards(process.env.HELIPOD_FLEET_SHARDS));
611
+ }
612
+ const groupCommit = opts.fleet ? opts.fleet.groupCommit ?? false : resolveGroupCommit({ envRaw: process.env.HELIPOD_GROUP_COMMIT, databaseUrl: opts.databaseUrl });
613
+ const dataDir = dirname3(resolve3(opts.dataPath));
614
+ const storageConfig = resolveStorageConfig(process.env, opts.storage);
615
+ assertStorageConfigCoherent(storageConfig);
616
+ const blobStore = makeBlobStore({ dataPath: dataDir, storage: storageConfig });
617
+ if (!isS3Config(storageConfig)) ensureStorageDirWritable(join5(dataDir, "storage"));
618
+ const runtime = await createEmbeddedRuntime({
619
+ store,
620
+ catalog: project.catalog,
621
+ logSink,
622
+ // `_storage:*` built-ins go in BOTH maps: `modules` (reached by the action facade's `invoke`
623
+ // and the reaper's `runFunction`) and `systemModules` (reached by the HTTP routes' `runSystem`,
624
+ // the same trusted path `_admin` uses). `systemModules` isn't swapped by `setModules`, so it
625
+ // persists across reload/deploy on its own; `modules` is re-applied via `withStorageModules`.
626
+ modules: withStorageModules(project.moduleMap),
627
+ systemModules: { ...systemModules(), ...storageModules },
628
+ adminModules: { "_admin:browseTable": browseTableModule },
629
+ verifyAdmin: (key) => verifyAdminKey(opts.adminKey, key),
630
+ componentNames: project.componentNames,
631
+ // Prepend the `ctx.storage` provider and the orphan-reaper driver to whatever components composed.
632
+ contextProviders: [
633
+ storageContextProvider(blobStore, {
634
+ signingKey: opts.adminKey,
635
+ ...opts.storageUploadTtlMs !== void 0 ? { uploadTtlMs: opts.storageUploadTtlMs } : {}
636
+ }),
637
+ ...project.contextProviders
638
+ ],
639
+ tableNumbers: project.tableNumbers,
640
+ bootSteps: project.bootSteps,
641
+ drivers: [
642
+ storageReaper(blobStore, opts.storageReaperSweepMs !== void 0 ? { sweepMs: opts.storageReaperSweepMs } : void 0),
643
+ // Receipted Outbox TTL reaper (verdict §(c) Retention): a timer-only bulk sweep of expired
644
+ // `client_mutations` rows. Always on (every deployment has the receipts tables); no-op work when
645
+ // no client ever wrote a receipt. Reads the SAME `store` the runtime commits to.
646
+ receiptsReaper(store),
647
+ // Tier 3 Slice 6: the lease-heartbeat + gc drivers (renews shard-0's lease on cadence; stops +
648
+ // signals via `objectStoreOnFenced` on a fence). Writer-only — empty outside the object-store
649
+ // writer path (absent entirely for a `--replica` node, Task 8.2: no heartbeat, no gc — a
650
+ // replica holds no lease and reclaims nothing; its OWN reactive-tailer wiring is started
651
+ // separately, below, once the runtime exists).
652
+ ...objectStoreWriterNode?.drivers ?? [],
653
+ ...project.drivers
654
+ ],
655
+ // Fleet (Tier 2): route writes to the lease-holder when not the writer, defer drivers until
656
+ // promotion, and (writer boot) fan out commits cross-process via pg_notify.
657
+ ...opts.fleet?.writeRouter ? { writeRouter: opts.fleet.writeRouter } : {},
658
+ // Tier 3 Slice 8 follow-on (replica write-forwarding): mutually exclusive with `opts.fleet`
659
+ // (guarded at the top of this function — `--object-store` + `--fleet` throws), so at most one
660
+ // of these two `writeRouter` spreads ever contributes a key.
661
+ ...replicaWriteForwarder ? { writeRouter: replicaWriteForwarder } : {},
662
+ ...opts.fleet?.deferDrivers ? { deferDrivers: true } : {},
663
+ ...opts.fleet?.fanoutAdapter ? { fanoutAdapter: opts.fleet.fanoutAdapter } : {},
664
+ // Fleet B3 hybrid (multi-writer): the replica-backed query path + the own-commit RYOW drain gate.
665
+ ...opts.fleet?.queryStore ? { queryStore: opts.fleet.queryStore } : {},
666
+ // Receipted Outbox (verdict §(c) placement): route the Connect handshake's classification/ack-prune
667
+ // to the authoritative PRIMARY receipts store on a sync node (whose `store` is the receipt-less
668
+ // replica) — without this the handshake spuriously resets a client. Absent → the runtime uses `store`.
669
+ ...opts.fleet?.receiptsStore ? { receiptsStore: opts.fleet.receiptsStore } : {},
670
+ ...opts.fleet?.beforeNotify ? { beforeNotify: opts.fleet.beforeNotify } : {},
671
+ // Triggers D1: the fleet stable-prefix bound for `readLog` (`min(shard_leases.frontier_ts)`).
672
+ ...opts.fleet?.stablePrefix ? { stablePrefix: opts.fleet.stablePrefix } : {},
673
+ // Receipted Outbox: fleet owns the receipts guard on the concrete Postgres store (armWriter,
674
+ // before the fence) — the runtime skips its own registration so it never lands on a sync node's
675
+ // SwitchableDocStore only to vanish on the promotion swapTo. Non-fleet → the runtime owns it.
676
+ ...opts.fleet?.externalReceiptsGuard ? { externalReceiptsGuard: true } : {},
677
+ // Shards B2a: >1 → a ShardedTransactor (per-shard parallel commits) over the store — resolved
678
+ // above (fleet: threaded in already-resolved; non-fleet: resolved+persisted just now).
679
+ numShards,
680
+ // Fleet B4 (T4): route every shard's commits through the group-commit committer loop — resolved
681
+ // above (fleet: threaded in already-resolved; non-fleet: read from HELIPOD_GROUP_COMMIT).
682
+ groupCommit,
683
+ // The wake seam (`serve --wake-url` / `--backstop-min-ms`): a host that stops the process between
684
+ // requests fires driver timers via `runtime.fireDueTimers()` instead of `setTimeout`, and can
685
+ // stretch the pure-backstop cadences so an idle app isn't cold-started every 30s. Both absent →
686
+ // today's `setTimeout` + 30s/60s behavior, unchanged.
687
+ ...opts.wakeHost ? { wakeHost: opts.wakeHost } : {},
688
+ ...opts.backstopMs ? { backstopMs: opts.backstopMs } : {}
689
+ });
690
+ const objectStoreReplicaRelease = objectStoreReplicaNode ? objectStoreReplicaNode.attachTailer(runtime) : void 0;
691
+ const storageRouteDeps = {
692
+ // The routes reach the privileged `_storage:_finalize`/`_get` built-ins via `runSystem` (trusted,
693
+ // like `_admin`), which reads `systemModules` — unaffected by any later `setModules` swap.
694
+ runMutation: async (path, args) => (await runtime.runSystem(path, args)).value,
695
+ runQuery: async (path, args) => (await runtime.runSystem(path, args)).value,
696
+ signingKey: opts.adminKey,
697
+ // When authz isn't composed (the default), leave `checkRead` undefined so `handleServe` falls
698
+ // back to the capability-token check (a private file's `getUrl` embeds a valid token). See the
699
+ // task-10 report for the authz effective-permissions bridge status.
700
+ checkRead: makeStorageCheckRead(opts.components)
701
+ };
702
+ const routes = storageRoutes(blobStore, storageRouteDeps);
703
+ const bearerOf = (request) => {
704
+ const h = request.headers.get("authorization");
705
+ const m = h ? /^Bearer\s+(.+)$/.exec(h) : null;
706
+ return m ? m[1] ?? null : null;
707
+ };
708
+ const componentRoutes = project.componentRoutes.map((r) => ({
709
+ method: r.method,
710
+ pathPrefix: r.pathPrefix,
711
+ handler: (request) => runtime.runHttpAction(r.handlerPath, request, { identity: bearerOf(request) })
712
+ }));
713
+ const adminApi = new AdminApi({
714
+ runtime,
715
+ schemaJson: project.schemaJson,
716
+ tableNumbers: project.tableNumbers,
717
+ manifest: project.manifest,
718
+ logSink,
719
+ catalog: project.catalog
720
+ });
721
+ return {
722
+ runtime,
723
+ adminApi,
724
+ project,
725
+ generated,
726
+ store,
727
+ logSink,
728
+ components: opts.components,
729
+ blobStore,
730
+ storageRoutes: routes,
731
+ componentRoutes,
732
+ // Mutually exclusive by construction (Task 8.2 guards `objectStoreUrl !== undefined && !replica`
733
+ // vs. `&& replica`) — at most one of these two spreads ever contributes a key, so `serve.ts`'s
734
+ // shutdown can call `objectStoreRelease()` generically without knowing which node type booted.
735
+ ...objectStoreWriterNode ? { objectStoreRelease: objectStoreWriterNode.release } : {},
736
+ ...objectStoreReplicaRelease ? { objectStoreRelease: objectStoreReplicaRelease } : {},
737
+ // Tier 3 Slice 8 follow-on (replica write-forwarding): threaded through so `serve.ts` can arm
738
+ // `/api/run`'s single-hop defensive guard on THIS node (`startDevServer`'s `replicaWriterUrl`).
739
+ ...replicaWriteForwarder ? { replicaWriterUrl: opts.writerUrl } : {}
740
+ };
741
+ }
742
+ function makeStorageCheckRead(_components) {
743
+ return void 0;
744
+ }
745
+ async function bootProject(opts) {
746
+ const { functionsDir, ...forwarded } = opts;
747
+ const loaded = await loadFunctionsDir(functionsDir);
748
+ const config = await loadConfig(dirname3(functionsDir));
749
+ return bootLoaded({ ...forwarded, loaded, components: config.components });
750
+ }
751
+ function loadDashboard(adminKey) {
752
+ try {
753
+ const indexPath = createRequire(import.meta.url).resolve("@helipod/dashboard/dist");
754
+ const distDir = dirname3(indexPath);
755
+ const raw = readFileSync(indexPath, "utf8");
756
+ if (adminKey === void 0) return { distDir, html: raw };
757
+ const inject = `<script>window.__ADMIN_KEY__=${JSON.stringify(adminKey).replace(/</g, "\\u003c")}</script>`;
758
+ return { distDir, html: raw.replace("</head>", `${inject}</head>`) };
759
+ } catch {
760
+ return void 0;
761
+ }
762
+ }
763
+
764
+ // src/server.ts
765
+ import { createServer } from "http";
766
+ import { readFileSync as readFileSync2, realpathSync, statSync } from "fs";
767
+ import { extname, join as join6, resolve as resolve4, sep } from "path";
768
+ var SYNC_PATH = "/api/sync";
769
+ var STORAGE_PREFIX = "/api/storage/";
770
+ var MAX_BODY_BYTES = 5 * 1024 * 1024;
771
+ function matchStorageRoute(routes, method, path) {
772
+ if (!routes || !path.startsWith(STORAGE_PREFIX)) return void 0;
773
+ return routes.find((r) => r.method === method && path.startsWith(r.pathPrefix));
774
+ }
775
+ function matchComponentRoute(routes, method, path) {
776
+ if (!routes) return void 0;
777
+ return routes.find((r) => r.method === method && path.startsWith(r.pathPrefix));
778
+ }
779
+ function hasBody(method) {
780
+ return method === "POST" || method === "PUT" || method === "PATCH";
781
+ }
782
+ var CONTENT_TYPES = {
783
+ ".html": "text/html; charset=utf-8",
784
+ ".js": "text/javascript; charset=utf-8",
785
+ ".mjs": "text/javascript; charset=utf-8",
786
+ ".css": "text/css; charset=utf-8",
787
+ ".json": "application/json",
788
+ ".svg": "image/svg+xml",
789
+ ".ico": "image/x-icon"
790
+ };
791
+ var EMBEDDED_CONTENT_TYPES = {
792
+ ".js": "application/javascript",
793
+ ".css": "text/css",
794
+ ".html": "text/html",
795
+ ".svg": "image/svg+xml",
796
+ ".woff2": "font/woff2",
797
+ ".woff": "font/woff",
798
+ ".png": "image/png",
799
+ ".ico": "image/x-icon",
800
+ ".json": "application/json",
801
+ ".map": "application/json"
802
+ };
803
+ async function serveDashboard(path, d) {
804
+ if ("distDir" in d) {
805
+ if (path === "/_dashboard" || path === "/_dashboard/" || path === "/_dashboard/index.html")
806
+ return { contentType: "text/html; charset=utf-8", body: d.html };
807
+ if (path.startsWith("/_dashboard/")) {
808
+ return resolveStatic(d.distDir, path.slice("/_dashboard".length));
809
+ }
810
+ return null;
811
+ }
812
+ if (path === "/" || path === "/index.html") return { contentType: "text/html", body: d.html };
813
+ const embeddedPath = d.assets[path] ?? (path.startsWith("/_dashboard/") ? d.assets[path.slice("/_dashboard".length)] : void 0);
814
+ if (!embeddedPath) return null;
815
+ const bun = globalThis.Bun;
816
+ if (!bun) return null;
817
+ const body = new Uint8Array(await bun.file(embeddedPath).arrayBuffer());
818
+ return { contentType: EMBEDDED_CONTENT_TYPES[extname(embeddedPath)] ?? "application/octet-stream", body };
819
+ }
820
+ function resolveStatic(webDir, urlPath) {
821
+ const rel = urlPath === "/" ? "/index.html" : urlPath;
822
+ let root;
823
+ let resolved;
824
+ try {
825
+ root = realpathSync(resolve4(webDir));
826
+ resolved = realpathSync(resolve4(join6(webDir, rel)));
827
+ } catch {
828
+ return null;
829
+ }
830
+ if (resolved !== root && !resolved.startsWith(root + sep)) return null;
831
+ if (!statSync(resolved).isFile()) return null;
832
+ return { contentType: CONTENT_TYPES[extname(resolved)] ?? "application/octet-stream", body: readFileSync2(resolved) };
833
+ }
834
+ function readBodyBytes(req) {
835
+ return new Promise((resolvePromise, reject) => {
836
+ const chunks = [];
837
+ let total = 0;
838
+ req.on("data", (c) => {
839
+ total += c.length;
840
+ if (total > MAX_BODY_BYTES) {
841
+ req.destroy();
842
+ reject(new Error("request body too large"));
843
+ return;
844
+ }
845
+ chunks.push(c);
846
+ });
847
+ req.on("end", () => resolvePromise(Buffer.concat(chunks)));
848
+ req.on("error", reject);
849
+ });
850
+ }
851
+ function readBody(req) {
852
+ return readBodyBytes(req).then((b) => b.toString("utf8"));
853
+ }
854
+ async function startNodeServer(runtime, options) {
855
+ const { WebSocketServer } = await import("ws");
856
+ let currentRoutes = options.routes ?? [];
857
+ const server = createServer((req, res) => {
858
+ void (async () => {
859
+ try {
860
+ const rawUrl = req.url ?? "/";
861
+ const url = new URL(rawUrl, "http://x");
862
+ const path = url.pathname;
863
+ const needsBody = hasBody(req.method);
864
+ const isStorageRequest = path.startsWith(STORAGE_PREFIX);
865
+ const bodyBytes = needsBody && isStorageRequest ? await readBodyBytes(req) : void 0;
866
+ const body = needsBody && !isStorageRequest ? await readBody(req) : void 0;
867
+ const query = {};
868
+ url.searchParams.forEach((val, key) => {
869
+ query[key] = val;
870
+ });
871
+ const authorization = req.headers.authorization ?? void 0;
872
+ const headers = Object.fromEntries(
873
+ Object.entries(req.headers).filter((e) => typeof e[1] === "string")
874
+ );
875
+ if ((req.method ?? "GET") === "GET" && options.dashboard) {
876
+ const dash = await serveDashboard(path, options.dashboard);
877
+ if (dash) {
878
+ res.writeHead(200, { "content-type": dash.contentType });
879
+ res.end(dash.body);
880
+ return;
881
+ }
882
+ }
883
+ const storageRoute = matchStorageRoute(options.storageRoutes, req.method ?? "GET", path);
884
+ if (storageRoute) {
885
+ const storageHeaders = new Headers(headers);
886
+ if (authorization && !storageHeaders.has("authorization")) storageHeaders.set("authorization", authorization);
887
+ const request = new Request(`http://${storageHeaders.get("host") ?? "localhost"}${rawUrl}`, {
888
+ method: req.method ?? "GET",
889
+ headers: storageHeaders,
890
+ // Raw bytes, NOT the utf8-decoded `body` string — see `readBodyBytes`'s doc comment.
891
+ // (Copied into a plain `Uint8Array<ArrayBuffer>` — `Buffer`'s `.buffer` is typed
892
+ // `ArrayBufferLike`, which doesn't structurally satisfy DOM lib's `BodyInit`.)
893
+ ...needsBody && bodyBytes !== void 0 ? { body: new Uint8Array(bodyBytes) } : {}
894
+ });
895
+ const response2 = await storageRoute.handler(request);
896
+ const outHeaders = {};
897
+ response2.headers.forEach((v, k) => {
898
+ outHeaders[k] = v;
899
+ });
900
+ res.writeHead(response2.status, outHeaders);
901
+ res.end(Buffer.from(await response2.arrayBuffer()));
902
+ return;
903
+ }
904
+ const componentRoute = matchComponentRoute(options.componentRoutes, req.method ?? "GET", path);
905
+ if (componentRoute) {
906
+ const compHeaders = new Headers(headers);
907
+ if (authorization && !compHeaders.has("authorization")) compHeaders.set("authorization", authorization);
908
+ const request = new Request(`http://${compHeaders.get("host") ?? "localhost"}${rawUrl}`, {
909
+ method: req.method ?? "GET",
910
+ headers: compHeaders,
911
+ ...needsBody && !isStorageRequest && body !== void 0 ? { body } : {}
912
+ });
913
+ const response2 = await componentRoute.handler(request);
914
+ const outHeaders = {};
915
+ response2.headers.forEach((v, k) => {
916
+ outHeaders[k] = v;
917
+ });
918
+ res.writeHead(response2.status, outHeaders);
919
+ res.end(Buffer.from(await response2.arrayBuffer()));
920
+ return;
921
+ }
922
+ const info = { functions: runtime.functionPaths(), tables: runtime.tableNames() };
923
+ const response = await handleHttpRequest(
924
+ runtime,
925
+ { method: req.method ?? "GET", path, body, query, authorization, headers },
926
+ info,
927
+ options.admin,
928
+ currentRoutes,
929
+ options.deploy,
930
+ options.fleet,
931
+ options.replicaWriterUrl
932
+ );
933
+ if (response.status === 404 && (req.method ?? "GET") === "GET" && options.webDir) {
934
+ const file = resolveStatic(options.webDir, path);
935
+ if (file) {
936
+ res.writeHead(200, { "content-type": file.contentType });
937
+ res.end(file.body);
938
+ return;
939
+ }
940
+ }
941
+ res.writeHead(response.status, response.headers);
942
+ res.end(response.body);
943
+ } catch (e) {
944
+ res.writeHead(500, { "content-type": "application/json" });
945
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
946
+ }
947
+ })();
948
+ });
949
+ const wss = new WebSocketServer({ noServer: true });
950
+ let sessionCounter = 0;
951
+ server.on("upgrade", (req, socket, head) => {
952
+ if ((req.url ?? "").split("?")[0] !== SYNC_PATH) {
953
+ socket.destroy();
954
+ return;
955
+ }
956
+ wss.handleUpgrade(req, socket, head, (ws) => {
957
+ const sessionId = `ws-${++sessionCounter}`;
958
+ const syncSocket = {
959
+ send: (data) => ws.send(data),
960
+ get bufferedAmount() {
961
+ return ws.bufferedAmount;
962
+ },
963
+ close: () => ws.close(),
964
+ ping: (onPong) => {
965
+ ws.once("pong", onPong);
966
+ ws.ping();
967
+ }
968
+ };
969
+ runtime.handler.connect(sessionId, syncSocket);
970
+ ws.on("message", (data) => void runtime.handler.handleMessage(sessionId, data.toString("utf8")));
971
+ ws.on("close", () => runtime.handler.disconnect(sessionId));
972
+ ws.on("error", () => runtime.handler.disconnect(sessionId));
973
+ });
974
+ });
975
+ await new Promise((res) => server.listen(options.port, options.ip, res));
976
+ const address = server.address();
977
+ const port = typeof address === "object" && address ? address.port : options.port;
978
+ return {
979
+ url: `http://${options.ip}:${port}`,
980
+ port,
981
+ setRoutes: (r) => {
982
+ currentRoutes = r;
983
+ },
984
+ close: async () => {
985
+ await runtime.stopDrivers();
986
+ await new Promise((res) => {
987
+ for (const c of wss.clients) c.terminate();
988
+ wss.close();
989
+ server.close(() => res());
990
+ });
991
+ }
992
+ };
993
+ }
994
+ var bunPongCallbacks = /* @__PURE__ */ new Map();
995
+ async function startBunServer(runtime, options) {
996
+ const bun = globalThis.Bun;
997
+ if (!bun) throw new Error("Bun runtime not available");
998
+ let sessionCounter = 0;
999
+ let currentRoutes = options.routes ?? [];
1000
+ const handle = bun.serve({
1001
+ port: options.port,
1002
+ hostname: options.ip,
1003
+ maxRequestBodySize: MAX_BODY_BYTES,
1004
+ async fetch(req, server) {
1005
+ const url = new URL(req.url);
1006
+ const path = url.pathname;
1007
+ if (path === SYNC_PATH) {
1008
+ const sessionId = `ws-${++sessionCounter}`;
1009
+ return server.upgrade(req, { data: { sessionId } }) ? void 0 : new Response("upgrade failed", { status: 400 });
1010
+ }
1011
+ if (req.method === "GET" && options.dashboard) {
1012
+ const dash = await serveDashboard(path, options.dashboard);
1013
+ if (dash) {
1014
+ const body2 = typeof dash.body === "string" ? dash.body : new Uint8Array(dash.body);
1015
+ return new Response(body2, { headers: { "content-type": dash.contentType } });
1016
+ }
1017
+ }
1018
+ const storageRoute = matchStorageRoute(options.storageRoutes, req.method, path);
1019
+ if (storageRoute) return await storageRoute.handler(req);
1020
+ const componentRoute = matchComponentRoute(options.componentRoutes, req.method, path);
1021
+ if (componentRoute) return await componentRoute.handler(req);
1022
+ const body = hasBody(req.method) ? await req.text() : void 0;
1023
+ const query = {};
1024
+ url.searchParams.forEach((val, key) => {
1025
+ query[key] = val;
1026
+ });
1027
+ const authorization = req.headers.get("authorization") ?? void 0;
1028
+ const headers = {};
1029
+ req.headers.forEach((v, k) => {
1030
+ headers[k] = v;
1031
+ });
1032
+ const info = { functions: runtime.functionPaths(), tables: runtime.tableNames() };
1033
+ const response = await handleHttpRequest(
1034
+ runtime,
1035
+ { method: req.method, path, body, query, authorization, headers },
1036
+ info,
1037
+ options.admin,
1038
+ currentRoutes,
1039
+ options.deploy,
1040
+ options.fleet,
1041
+ options.replicaWriterUrl
1042
+ );
1043
+ if (response.status === 404 && req.method === "GET" && options.webDir) {
1044
+ const file = resolveStatic(options.webDir, path);
1045
+ if (file) return new Response(new Uint8Array(file.body), { headers: { "content-type": file.contentType } });
1046
+ }
1047
+ return new Response(response.body, { status: response.status, headers: response.headers });
1048
+ },
1049
+ websocket: {
1050
+ open(ws) {
1051
+ const syncSocket = {
1052
+ send: (data) => void ws.send(data),
1053
+ get bufferedAmount() {
1054
+ return ws.getBufferedAmount();
1055
+ },
1056
+ close: () => ws.close(),
1057
+ ping: (onPong) => {
1058
+ bunPongCallbacks.set(ws.data.sessionId, onPong);
1059
+ ws.ping();
1060
+ }
1061
+ };
1062
+ runtime.handler.connect(ws.data.sessionId, syncSocket);
1063
+ },
1064
+ message(ws, message) {
1065
+ void runtime.handler.handleMessage(ws.data.sessionId, typeof message === "string" ? message : new TextDecoder().decode(message));
1066
+ },
1067
+ close(ws) {
1068
+ bunPongCallbacks.delete(ws.data.sessionId);
1069
+ runtime.handler.disconnect(ws.data.sessionId);
1070
+ },
1071
+ pong(ws) {
1072
+ const cb = bunPongCallbacks.get(ws.data.sessionId);
1073
+ bunPongCallbacks.delete(ws.data.sessionId);
1074
+ cb?.();
1075
+ }
1076
+ }
1077
+ });
1078
+ return {
1079
+ url: `http://${options.ip}:${handle.port}`,
1080
+ port: handle.port,
1081
+ setRoutes: (r) => {
1082
+ currentRoutes = r;
1083
+ },
1084
+ close: async () => {
1085
+ await runtime.stopDrivers();
1086
+ handle.stop(true);
1087
+ }
1088
+ };
1089
+ }
1090
+ function startDevServer(runtime, options) {
1091
+ return detectRuntime() === "bun" ? startBunServer(runtime, options) : startNodeServer(runtime, options);
1092
+ }
1093
+ var ProcessRuntimeHost = class {
1094
+ serve(runtime, options) {
1095
+ return startDevServer(runtime, options);
1096
+ }
1097
+ };
1098
+
1099
+ // src/watch.ts
1100
+ function createWatchLoop(options) {
1101
+ const debounceMs = options.debounceMs ?? 50;
1102
+ const setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1103
+ const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
1104
+ let timer = null;
1105
+ let unsubscribe = null;
1106
+ const fire = () => {
1107
+ timer = null;
1108
+ void options.onTrigger("change");
1109
+ };
1110
+ return {
1111
+ start() {
1112
+ void options.onTrigger("initial");
1113
+ unsubscribe = options.subscribe(() => {
1114
+ if (timer !== null) clearTimer(timer);
1115
+ timer = setTimer(fire, debounceMs);
1116
+ });
1117
+ },
1118
+ stop() {
1119
+ if (timer !== null) clearTimer(timer);
1120
+ unsubscribe?.();
1121
+ unsubscribe = null;
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ // src/cli.ts
1127
+ import { watch as fsWatch } from "fs";
1128
+ import { dirname as dirname9, join as join13, resolve as resolve8 } from "path";
1129
+ import { writeGenerated as writeGenerated4 } from "@helipod/codegen";
1130
+ import { generateAdminKey } from "@helipod/admin";
1131
+
1132
+ // src/serve.ts
1133
+ import { existsSync as existsSync4 } from "fs";
1134
+ import { dirname as dirname5, join as join8, resolve as resolve5 } from "path";
1135
+ import { PostgresDocStore as PostgresDocStore2 } from "@helipod/docstore-postgres";
1136
+
1137
+ // src/deploy-apply.ts
1138
+ import { createHash as createHash2 } from "crypto";
1139
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
1140
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7 } from "path";
1141
+ import { sha256Hex } from "@helipod/deploy";
1142
+
1143
+ // src/schema-diff.ts
1144
+ function fieldsOf(s, table) {
1145
+ return s.schemaJson.tables[table]?.documentType?.value ?? {};
1146
+ }
1147
+ function compatibleType(cur, next) {
1148
+ return cur.type === next.type;
1149
+ }
1150
+ function diffSchema(current, next) {
1151
+ for (const name of Object.keys(current.tableNumbers)) {
1152
+ if (!(name in next.tableNumbers)) return { ok: false, reason: `table "${name}" was removed (destructive \u2014 rename/drop not supported)` };
1153
+ if (current.tableNumbers[name] !== next.tableNumbers[name])
1154
+ return { ok: false, reason: `table "${name}" tableNumber changed ${current.tableNumbers[name]}\u2192${next.tableNumbers[name]} (destructive)` };
1155
+ const cur = fieldsOf(current, name);
1156
+ const nxt = fieldsOf(next, name);
1157
+ for (const [field, curV] of Object.entries(cur)) {
1158
+ const nxtV = nxt[field];
1159
+ if (nxtV === void 0) return { ok: false, reason: `field "${name}.${field}" was removed (destructive)` };
1160
+ if (!compatibleType(curV.fieldType, nxtV.fieldType))
1161
+ return { ok: false, reason: `field "${name}.${field}" changed type ${curV.fieldType.type}\u2192${nxtV.fieldType.type} (destructive)` };
1162
+ if (curV.optional && !nxtV.optional)
1163
+ return { ok: false, reason: `field "${name}.${field}" became required (destructive \u2014 existing rows may omit it)` };
1164
+ }
1165
+ for (const [field, nxtV] of Object.entries(nxt)) {
1166
+ if (cur[field] === void 0 && !nxtV.optional)
1167
+ return { ok: false, reason: `field "${name}.${field}" is a new required field on an existing table (destructive \u2014 existing rows lack it; make it optional)` };
1168
+ }
1169
+ }
1170
+ return { ok: true };
1171
+ }
1172
+
1173
+ // src/deploy-apply.ts
1174
+ function reconstructFiles(payload, currentModules) {
1175
+ if ("changed" in payload) {
1176
+ const files = [...payload.changed ?? []];
1177
+ for (const u of payload.unchanged ?? []) {
1178
+ const cur = currentModules.get(u.path);
1179
+ if (!cur) return { ok: false, error: `stale-base: server has no module "${u.path}"` };
1180
+ if (cur.sha !== u.sha256) return { ok: false, error: `stale-base: module "${u.path}" hash mismatch` };
1181
+ files.push({ path: u.path, code: cur.code });
1182
+ }
1183
+ return { ok: true, files };
1184
+ }
1185
+ return { ok: true, files: payload.files ?? [] };
1186
+ }
1187
+ async function applyDeploy(deps, payload) {
1188
+ const rec = reconstructFiles(payload, deps.currentModules);
1189
+ if (!rec.ok) return { ok: false, kind: "stale-base", error: rec.error };
1190
+ const files = rec.files;
1191
+ const rev = createHash2("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
1192
+ const dir = join7(deps.deployRoot, rev, "functions");
1193
+ let project;
1194
+ try {
1195
+ for (const f of files) {
1196
+ if (typeof f.path !== "string" || typeof f.code !== "string") {
1197
+ throw new Error(`invalid deploy payload: file entry must have a string "path" and "code"`);
1198
+ }
1199
+ if (isAbsolute2(f.path) || f.path.split("/").includes("..")) {
1200
+ throw new Error(`invalid deploy payload: unsafe path "${f.path}"`);
1201
+ }
1202
+ const abs = join7(dir, f.path);
1203
+ mkdirSync3(dirname4(abs), { recursive: true });
1204
+ writeFileSync2(abs, f.code);
1205
+ }
1206
+ const loaded = await loadFunctionsDir(dir);
1207
+ project = push(loaded, deps.components, deps.current().tableNumbers).project;
1208
+ } catch (e) {
1209
+ return { ok: false, kind: "load-error", error: e instanceof Error ? e.message : String(e) };
1210
+ }
1211
+ const diff = diffSchema(deps.current(), {
1212
+ schemaJson: project.schemaJson,
1213
+ tableNumbers: project.tableNumbers
1214
+ });
1215
+ if (!diff.ok) return { ok: false, kind: "schema-incompatible", error: diff.reason };
1216
+ deps.runtime.setModules(withStorageModules(project.moduleMap));
1217
+ deps.runtime.setTableNumbers(project.tableNumbers);
1218
+ deps.setRoutes(project.routes);
1219
+ deps.adminApi.setSchema(project.schemaJson, project.tableNumbers, project.manifest);
1220
+ const modules = new Map(files.map((f) => [f.path, { code: f.code, sha: sha256Hex(f.code) }]));
1221
+ return { ok: true, rev, functions: Object.keys(project.moduleMap).length, modules };
1222
+ }
1223
+
1224
+ // src/wake-host.ts
1225
+ function httpWakeHost(url) {
1226
+ return {
1227
+ armWake(atMs) {
1228
+ void fetch(url, { method: "POST", body: atMs === null ? "" : String(atMs) }).then((res) => {
1229
+ if (!res.ok) console.error(`[wake] arm at ${atMs} rejected by ${url}: ${res.status}`);
1230
+ }).catch((e) => {
1231
+ console.error(`[wake] arm at ${atMs} failed to reach ${url}:`, e);
1232
+ });
1233
+ }
1234
+ };
1235
+ }
1236
+
1237
+ // src/serve.ts
1238
+ function raceWithTimeout(promise, ms) {
1239
+ return new Promise((resolveOuter) => {
1240
+ let settled = false;
1241
+ const timer = setTimeout(() => {
1242
+ if (!settled) {
1243
+ settled = true;
1244
+ resolveOuter();
1245
+ }
1246
+ }, ms);
1247
+ promise.then(
1248
+ () => {
1249
+ if (!settled) {
1250
+ settled = true;
1251
+ clearTimeout(timer);
1252
+ resolveOuter();
1253
+ }
1254
+ },
1255
+ () => {
1256
+ if (!settled) {
1257
+ settled = true;
1258
+ clearTimeout(timer);
1259
+ resolveOuter();
1260
+ }
1261
+ }
1262
+ );
1263
+ });
1264
+ }
1265
+ var OBJECTSTORE_RELINQUISH_TIMEOUT_MS = 2e3;
1266
+ var FLEET_ERR_NO_DB = "fleet mode requires --database-url (Postgres) \u2014 set --database-url postgres://\u2026 or HELIPOD_DATABASE_URL.";
1267
+ var FLEET_ERR_NO_ADVERTISE = "fleet mode requires --advertise-url (or HELIPOD_ADVERTISE_URL) \u2014 the URL other fleet nodes reach this node at, e.g. --advertise-url http://10.0.0.2:3000";
1268
+ var FLEET_ERR_NO_PACKAGE = "fleet mode requires @helipod/fleet \u2014 install it (bun add @helipod/fleet).";
1269
+ function validateFleetOptions(opts) {
1270
+ if (!isPostgresUrl(opts.databaseUrl)) return { ok: false, error: FLEET_ERR_NO_DB };
1271
+ const advertiseUrl = opts.advertiseUrl?.trim();
1272
+ if (!advertiseUrl) return { ok: false, error: FLEET_ERR_NO_ADVERTISE };
1273
+ return { ok: true, databaseUrl: opts.databaseUrl, advertiseUrl };
1274
+ }
1275
+ function parseLeaseTtlMs(raw) {
1276
+ if (raw === void 0 || raw.trim() === "") return void 0;
1277
+ const n = Number(raw);
1278
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1279
+ }
1280
+ async function resolveFleetNumShards(databaseUrl, envValue) {
1281
+ const client = makePgClient(databaseUrl);
1282
+ try {
1283
+ const probe = new PostgresDocStore2(client, { readOnly: true });
1284
+ await probe.setupSchema();
1285
+ return await resolveNumShards(probe, envValue);
1286
+ } finally {
1287
+ await client.close();
1288
+ }
1289
+ }
1290
+ function resolveServeOptions(args) {
1291
+ let functionsDir = "";
1292
+ let dataPath = process.env.HELIPOD_DATA_DIR ? join8(process.env.HELIPOD_DATA_DIR, "db.sqlite") : "./data/db.sqlite";
1293
+ let ip = "0.0.0.0";
1294
+ let port = process.env.PORT ? Number(process.env.PORT) : 3e3;
1295
+ let dashboard = process.env.HELIPOD_DASHBOARD?.trim().toLowerCase() !== "off";
1296
+ let allowDeploy = process.env.HELIPOD_ALLOW_DEPLOY === "1";
1297
+ let webDir = process.env.HELIPOD_WEB_DIR;
1298
+ let databaseUrl = process.env.HELIPOD_DATABASE_URL;
1299
+ let storageBucket;
1300
+ let storageEndpoint;
1301
+ let fleet = process.env.HELIPOD_FLEET === "1" || process.env.HELIPOD_FLEET?.trim().toLowerCase() === "true";
1302
+ let advertiseUrl = process.env.HELIPOD_ADVERTISE_URL;
1303
+ let objectStoreUrl = process.env.HELIPOD_OBJECT_STORE;
1304
+ let replica = /^(1|true|yes)$/i.test(process.env.HELIPOD_REPLICA ?? "");
1305
+ let writerUrl = process.env.HELIPOD_WRITER_URL;
1306
+ let wakeUrl = process.env.HELIPOD_WAKE_URL;
1307
+ let backstopMinMs = parseLeaseTtlMs(process.env.HELIPOD_BACKSTOP_MIN_MS);
1308
+ let objectStoreShards;
1309
+ const objectStoreGcMs = parseLeaseTtlMs(process.env.HELIPOD_OBJECTSTORE_GC_MS);
1310
+ for (let i = 0; i < args.length; i++) {
1311
+ const a = args[i];
1312
+ if (a === "--dir" && args[i + 1]) functionsDir = args[++i];
1313
+ else if (a === "--data" && args[i + 1]) dataPath = args[++i];
1314
+ else if (a === "--ip" && args[i + 1]) ip = args[++i];
1315
+ else if (a === "--port" && args[i + 1]) port = Number(args[++i]);
1316
+ else if (a === "--no-dashboard") dashboard = false;
1317
+ else if (a === "--allow-deploy") allowDeploy = true;
1318
+ else if (a === "--database-url" && args[i + 1]) databaseUrl = args[++i];
1319
+ else if (a === "--storage-bucket" && args[i + 1]) storageBucket = args[++i];
1320
+ else if (a === "--storage-endpoint" && args[i + 1]) storageEndpoint = args[++i];
1321
+ else if (a === "--fleet") fleet = true;
1322
+ else if (a === "--advertise-url" && args[i + 1]) advertiseUrl = args[++i];
1323
+ else if (a === "--object-store" && args[i + 1]) objectStoreUrl = args[++i];
1324
+ else if (a === "--replica") replica = true;
1325
+ else if (a === "--writer-url" && args[i + 1]) writerUrl = args[++i];
1326
+ else if (a === "--shards" && args[i + 1]) objectStoreShards = Number(args[++i]);
1327
+ else if (a === "--wake-url" && args[i + 1]) wakeUrl = args[++i];
1328
+ else if (a === "--backstop-min-ms" && args[i + 1]) backstopMinMs = parseLeaseTtlMs(args[++i]);
1329
+ else if (a === "--web" && args[i + 1]) webDir = args[++i];
1330
+ }
1331
+ if (objectStoreShards === void 0 && objectStoreUrl !== void 0) {
1332
+ objectStoreShards = parseNumShards(process.env.HELIPOD_FLEET_SHARDS);
1333
+ }
1334
+ return {
1335
+ functionsDir,
1336
+ dataPath,
1337
+ ip,
1338
+ port,
1339
+ dashboard,
1340
+ ...webDir !== void 0 ? { webDir } : {},
1341
+ allowDeploy,
1342
+ databaseUrl,
1343
+ storageBucket,
1344
+ storageEndpoint,
1345
+ fleet,
1346
+ advertiseUrl,
1347
+ ...objectStoreUrl !== void 0 ? { objectStoreUrl } : {},
1348
+ ...objectStoreGcMs !== void 0 ? { objectStoreGcMs } : {},
1349
+ ...objectStoreShards !== void 0 ? { objectStoreShards } : {},
1350
+ replica,
1351
+ ...writerUrl !== void 0 ? { writerUrl } : {},
1352
+ ...wakeUrl !== void 0 ? { wakeUrl } : {},
1353
+ ...backstopMinMs !== void 0 ? { backstopMinMs } : {}
1354
+ };
1355
+ }
1356
+ function toDeploySchema(schemaJson) {
1357
+ const tables = {};
1358
+ for (const [name, t] of Object.entries(schemaJson.tables)) {
1359
+ const dt = t.documentType;
1360
+ tables[name] = { documentType: dt && dt.type === "object" ? dt : { type: "object", value: {} } };
1361
+ }
1362
+ return { tables };
1363
+ }
1364
+ async function startServe(opts) {
1365
+ const backstopFloorMs = opts.backstopMinMs;
1366
+ const prep = opts.fleet && opts.fleetModule && opts.fleetConfig ? await opts.fleetModule.prepareFleetNode({
1367
+ ...opts.fleetConfig,
1368
+ adminKey: opts.adminKey,
1369
+ // A sync node's local replica lives beside the data file (same dir the SQLite store uses).
1370
+ dataDir: dirname5(resolve5(opts.dataPath))
1371
+ }) : void 0;
1372
+ const { runtime, adminApi, project, store, components, storageRoutes: storageRoutes2, componentRoutes, objectStoreRelease, replicaWriterUrl } = await bootProject({
1373
+ functionsDir: opts.functionsDir,
1374
+ dataPath: opts.dataPath,
1375
+ adminKey: opts.adminKey,
1376
+ databaseUrl: opts.databaseUrl,
1377
+ storage: { bucket: opts.storageBucket, endpoint: opts.storageEndpoint },
1378
+ ...opts.storageUploadTtlMs !== void 0 ? { storageUploadTtlMs: opts.storageUploadTtlMs } : {},
1379
+ ...opts.storageReaperSweepMs !== void 0 ? { storageReaperSweepMs: opts.storageReaperSweepMs } : {},
1380
+ ...prep ? { fleet: prep.runtimeOptions } : {},
1381
+ ...opts.objectStoreUrl !== void 0 ? { objectStoreUrl: opts.objectStoreUrl } : {},
1382
+ ...opts.onObjectStoreFenced ? { objectStoreOnFenced: opts.onObjectStoreFenced } : {},
1383
+ ...opts.objectStoreGcMs !== void 0 ? { objectStoreGcMs: opts.objectStoreGcMs } : {},
1384
+ ...opts.replica ? { replica: opts.replica } : {},
1385
+ ...opts.writerUrl !== void 0 ? { writerUrl: opts.writerUrl } : {},
1386
+ ...opts.objectStoreShards !== void 0 ? { objectStoreShards: opts.objectStoreShards } : {},
1387
+ // The wake seam (`--wake-url`/`--backstop-min-ms`): a host that stops the process between
1388
+ // requests. Both unset (every existing deployment) → no keys, plain `setTimeout` + the drivers'
1389
+ // own 30s/60s backstops.
1390
+ ...opts.wakeUrl !== void 0 ? { wakeHost: httpWakeHost(opts.wakeUrl) } : {},
1391
+ ...backstopFloorMs !== void 0 ? { backstopMs: (d) => Math.max(d, backstopFloorMs) } : {}
1392
+ });
1393
+ const dashboard = opts.dashboard ? loadDashboard(void 0) : void 0;
1394
+ const fleet = prep && opts.fleetModule ? await opts.fleetModule.startFleetNode({
1395
+ client: prep.client,
1396
+ pgStore: prep.pgStore,
1397
+ runtime,
1398
+ lease: prep.lease,
1399
+ forwarder: prep.forwarder,
1400
+ replica: prep.replica,
1401
+ switchable: prep.switchable,
1402
+ replicaPath: prep.replicaPath,
1403
+ numShards: prep.numShards
1404
+ }) : void 0;
1405
+ let server;
1406
+ let currentPushedModules = /* @__PURE__ */ new Map();
1407
+ const deploy = opts.allowDeploy ? {
1408
+ apply: async (payload) => {
1409
+ const result = await applyDeploy(
1410
+ {
1411
+ runtime,
1412
+ adminApi,
1413
+ setRoutes: (r) => server.setRoutes(r),
1414
+ components,
1415
+ current: () => {
1416
+ const live = adminApi.getSchema();
1417
+ return { schemaJson: toDeploySchema(live.schemaJson), tableNumbers: live.tableNumbers };
1418
+ },
1419
+ deployRoot: join8(process.cwd(), ".helipod-deploy"),
1420
+ currentModules: currentPushedModules
1421
+ },
1422
+ payload
1423
+ );
1424
+ if (!result.ok) return result;
1425
+ currentPushedModules = result.modules;
1426
+ return { ok: true, rev: result.rev, functions: result.functions };
1427
+ },
1428
+ modules: () => Object.fromEntries([...currentPushedModules].map(([p, v]) => [p, v.sha]))
1429
+ } : void 0;
1430
+ server = await new ProcessRuntimeHost().serve(
1431
+ runtime,
1432
+ {
1433
+ port: opts.port,
1434
+ ip: opts.ip,
1435
+ ...opts.webDir !== void 0 ? { webDir: opts.webDir } : {},
1436
+ admin: { api: adminApi, key: opts.adminKey },
1437
+ dashboard,
1438
+ routes: project.routes,
1439
+ storageRoutes: storageRoutes2,
1440
+ componentRoutes,
1441
+ deploy,
1442
+ fleet,
1443
+ ...replicaWriterUrl !== void 0 ? { replicaWriterUrl } : {}
1444
+ }
1445
+ );
1446
+ return { server, store, runtime, fleet, role: prep?.role, ...objectStoreRelease ? { objectStoreRelease } : {} };
1447
+ }
1448
+ async function serveCommand(args) {
1449
+ const opts = resolveServeOptions(args);
1450
+ const adminKey = process.env.HELIPOD_ADMIN_KEY?.trim();
1451
+ if (!adminKey) {
1452
+ process.stderr.write("\u2717 HELIPOD_ADMIN_KEY is required for `serve` \u2014 set it to a strong secret.\n");
1453
+ return 1;
1454
+ }
1455
+ const { functionsDir } = await resolveFunctionsDir(opts.functionsDir || void 0, process.cwd());
1456
+ if (!ensureFunctionsDirExists(functionsDir)) return 1;
1457
+ opts.functionsDir = functionsDir;
1458
+ if (!existsSync4(join8(opts.functionsDir, "_generated", "server.ts"))) {
1459
+ process.stderr.write(
1460
+ `\u2717 ${opts.functionsDir}/_generated not found \u2014 run \`helipod codegen --dir ${opts.functionsDir}\` and commit _generated/ before deploying.
1461
+ `
1462
+ );
1463
+ return 1;
1464
+ }
1465
+ if (opts.fleet && opts.objectStoreUrl !== void 0) {
1466
+ process.stderr.write(
1467
+ "\u2717 --object-store cannot be combined with --fleet (Tier 2) \u2014 pick one write-scaling story.\n"
1468
+ );
1469
+ return 1;
1470
+ }
1471
+ if (opts.replica && opts.objectStoreUrl === void 0) {
1472
+ process.stderr.write(
1473
+ "\u2717 --replica requires --object-store \u2014 a replica materializes from an object-storage bucket; set --object-store <url> (or HELIPOD_OBJECT_STORE).\n"
1474
+ );
1475
+ return 1;
1476
+ }
1477
+ if (opts.objectStoreShards !== void 0 && opts.objectStoreShards > 1) {
1478
+ if (opts.objectStoreUrl === void 0) {
1479
+ process.stderr.write(
1480
+ "\u2717 --shards N (N>1) requires --object-store \u2014 it sizes the object-storage writer's lane count; set --object-store <url>, or drop --shards.\n"
1481
+ );
1482
+ return 1;
1483
+ }
1484
+ if (opts.replica) {
1485
+ process.stderr.write(
1486
+ "\u2717 --shards cannot be combined with --replica \u2014 a replica is single-shard (it tails shard 0); run the multi-shard WRITER with --shards and point replicas at it.\n"
1487
+ );
1488
+ return 1;
1489
+ }
1490
+ if (!Number.isInteger(opts.objectStoreShards)) {
1491
+ process.stderr.write(`\u2717 --shards must be a positive integer, got "${opts.objectStoreShards}".
1492
+ `);
1493
+ return 1;
1494
+ }
1495
+ }
1496
+ if (opts.writerUrl !== void 0 && !opts.replica) {
1497
+ process.stderr.write(
1498
+ "\u2717 --writer-url only applies to --replica (it's the writer this replica forwards mutations/actions to) \u2014 pass --replica too, or drop --writer-url (or HELIPOD_WRITER_URL).\n"
1499
+ );
1500
+ return 1;
1501
+ }
1502
+ let fleetModule;
1503
+ let fleetConfig;
1504
+ if (opts.fleet) {
1505
+ const v = validateFleetOptions(opts);
1506
+ if (!v.ok) {
1507
+ process.stderr.write(`\u2717 ${v.error}
1508
+ `);
1509
+ return 1;
1510
+ }
1511
+ try {
1512
+ const fleetSpecifier = "@helipod/fleet";
1513
+ fleetModule = await import(fleetSpecifier);
1514
+ } catch {
1515
+ process.stderr.write(`\u2717 ${FLEET_ERR_NO_PACKAGE}
1516
+ `);
1517
+ return 1;
1518
+ }
1519
+ let numShards;
1520
+ try {
1521
+ numShards = await resolveFleetNumShards(v.databaseUrl, parseNumShards(process.env.HELIPOD_FLEET_SHARDS));
1522
+ } catch (e) {
1523
+ process.stderr.write(`\u2717 ${e instanceof Error ? e.message : String(e)}
1524
+ `);
1525
+ return 1;
1526
+ }
1527
+ fleetConfig = {
1528
+ databaseUrl: v.databaseUrl,
1529
+ advertiseUrl: v.advertiseUrl,
1530
+ leaseTtlMs: parseLeaseTtlMs(process.env.HELIPOD_FLEET_LEASE_TTL_MS),
1531
+ numShards
1532
+ };
1533
+ }
1534
+ let triggerObjectStoreFencedShutdown;
1535
+ let booted;
1536
+ try {
1537
+ booted = await startServe({
1538
+ ...opts,
1539
+ adminKey,
1540
+ fleetModule,
1541
+ fleetConfig,
1542
+ onObjectStoreFenced: (e) => {
1543
+ process.stderr.write(`\u2717 object-store lease lost \u2014 shutting down: ${e.message}
1544
+ `);
1545
+ triggerObjectStoreFencedShutdown?.();
1546
+ }
1547
+ });
1548
+ } catch (e) {
1549
+ if (isObjectStoreBootFailFast(e)) {
1550
+ process.stderr.write(`\u2717 ${e.message}
1551
+ `);
1552
+ return 1;
1553
+ }
1554
+ throw e;
1555
+ }
1556
+ const { server, store, role, fleet, objectStoreRelease } = booted;
1557
+ process.stdout.write(
1558
+ JSON.stringify({
1559
+ level: "info",
1560
+ msg: "helipod serve",
1561
+ url: server.url,
1562
+ dir: opts.functionsDir,
1563
+ data: opts.dataPath,
1564
+ dashboard: opts.dashboard,
1565
+ allowDeploy: opts.allowDeploy,
1566
+ // Additive: present only in fleet mode. Task 7's 2-process E2E asserts each node's role here.
1567
+ ...role ? { fleet: true, role } : {},
1568
+ // Additive: present only in object-store mode (Tier 3 Slice 6).
1569
+ ...opts.objectStoreUrl !== void 0 ? { objectStore: true } : {},
1570
+ // Additive: present only for a read-only replica node (Tier 3 Slice 8, Task 8.2).
1571
+ ...opts.replica ? { replica: true } : {},
1572
+ // Additive: present only when this replica forwards writes (Tier 3 Slice 8 follow-on).
1573
+ ...opts.writerUrl !== void 0 ? { writerUrl: opts.writerUrl } : {}
1574
+ }) + "\n"
1575
+ );
1576
+ let closing = false;
1577
+ const shutdown = async () => {
1578
+ if (closing) return;
1579
+ closing = true;
1580
+ process.stdout.write(JSON.stringify({ level: "info", msg: "shutting down" }) + "\n");
1581
+ if (fleet) await fleet.stop();
1582
+ await server.close();
1583
+ if (objectStoreRelease) await raceWithTimeout(objectStoreRelease(), OBJECTSTORE_RELINQUISH_TIMEOUT_MS);
1584
+ await store.close();
1585
+ process.exit(0);
1586
+ };
1587
+ triggerObjectStoreFencedShutdown = () => void shutdown();
1588
+ process.on("SIGTERM", () => void shutdown());
1589
+ process.on("SIGINT", () => void shutdown());
1590
+ return new Promise(() => {
1591
+ });
1592
+ }
1593
+
1594
+ // src/deploy.ts
1595
+ import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3, mkdtempSync, rmSync, existsSync as existsSync5 } from "fs";
1596
+ import { join as join9, relative, sep as sep2 } from "path";
1597
+ import { tmpdir } from "os";
1598
+ import { transform } from "esbuild";
1599
+ import { writeGenerated } from "@helipod/codegen";
1600
+ import { resolveDeploy, loadTarget, NodeSpawner, DeployError } from "@helipod/deploy";
1601
+ function walkTs(root, dir, out) {
1602
+ for (const entry of readdirSync2(dir)) {
1603
+ const abs = join9(dir, entry);
1604
+ if (statSync2(abs).isDirectory()) walkTs(root, abs, out);
1605
+ else if (entry.endsWith(".ts") && !entry.endsWith(".d.ts")) out.push(abs);
1606
+ }
1607
+ }
1608
+ async function packageApp(functionsDir) {
1609
+ const absFiles = [];
1610
+ walkTs(functionsDir, functionsDir, absFiles);
1611
+ const out = [];
1612
+ for (const abs of absFiles) {
1613
+ const source = readFileSync3(abs, "utf8");
1614
+ const { code } = await transform(source, { loader: "ts", format: "esm", target: "esnext" });
1615
+ const rel = relative(functionsDir, abs).split(sep2).join("/").replace(/\.ts$/, ".js");
1616
+ out.push({ path: rel, code });
1617
+ }
1618
+ return out.sort((a, b) => a.path.localeCompare(b.path));
1619
+ }
1620
+ function parseDeployFlags(args) {
1621
+ let target;
1622
+ let env;
1623
+ let url = process.env.HELIPOD_DEPLOY_URL?.trim() || void 0;
1624
+ let dirFlag;
1625
+ let dryRun = false;
1626
+ let check = false;
1627
+ for (let i = 0; i < args.length; i++) {
1628
+ if (args[i] === "--target" && args[i + 1]) target = args[++i];
1629
+ else if (args[i] === "--env" && args[i + 1]) env = args[++i];
1630
+ else if (args[i] === "--url" && args[i + 1]) url = args[++i];
1631
+ else if (args[i] === "--dir" && args[i + 1]) dirFlag = args[++i];
1632
+ else if (args[i] === "--dry-run") dryRun = true;
1633
+ else if (args[i] === "--check") check = true;
1634
+ }
1635
+ return { target, env, url, dirFlag, dryRun, check };
1636
+ }
1637
+ async function checkDrift(functionsDir, components) {
1638
+ const tmp = mkdtempSync(join9(tmpdir(), "sb-codegen-"));
1639
+ try {
1640
+ const { generated } = push(await loadFunctionsDir(functionsDir), components);
1641
+ writeGenerated(generated.files, tmp);
1642
+ const genDir = join9(functionsDir, "_generated");
1643
+ return !dirsEqual(tmp, genDir);
1644
+ } finally {
1645
+ rmSync(tmp, { recursive: true, force: true });
1646
+ }
1647
+ }
1648
+ function dirsEqual(a, b) {
1649
+ const walk2 = (root) => {
1650
+ const out = /* @__PURE__ */ new Map();
1651
+ const rec = (dir, rel) => {
1652
+ if (!existsSync5(dir)) return;
1653
+ for (const e of readdirSync2(dir)) {
1654
+ const abs = join9(dir, e);
1655
+ const r = rel ? `${rel}/${e}` : e;
1656
+ if (statSync2(abs).isDirectory()) rec(abs, r);
1657
+ else out.set(r, readFileSync3(abs, "utf8"));
1658
+ }
1659
+ };
1660
+ rec(root, "");
1661
+ return out;
1662
+ };
1663
+ const ma = walk2(a);
1664
+ const mb = walk2(b);
1665
+ if (ma.size !== mb.size) return false;
1666
+ for (const [k, v] of ma) if (mb.get(k) !== v) return false;
1667
+ return true;
1668
+ }
1669
+ async function deployCommand(args, deps = {}) {
1670
+ const flags = parseDeployFlags(args);
1671
+ const cwd = deps.cwd ?? process.cwd();
1672
+ const { functionsDir } = await resolveFunctionsDir(flags.dirFlag, cwd);
1673
+ if (!ensureFunctionsDirExists(functionsDir)) return 1;
1674
+ const config = await loadConfig(cwd);
1675
+ if (flags.check) {
1676
+ const drift = await checkDrift(functionsDir, config.components);
1677
+ if (drift) {
1678
+ process.stderr.write(`\u2717 ${functionsDir}/_generated is out of date \u2014 run \`helipod codegen\` and commit the result
1679
+ `);
1680
+ return 1;
1681
+ }
1682
+ process.stdout.write(`\u2713 ${functionsDir}/_generated is up to date
1683
+ `);
1684
+ if (!flags.dryRun) return 0;
1685
+ }
1686
+ const resolved = resolveDeploy({ deploy: config.deploy, target: flags.target, env: flags.env, inlineUrl: flags.url });
1687
+ if ("error" in resolved) {
1688
+ process.stderr.write(`\u2717 ${resolved.error}
1689
+ `);
1690
+ return 1;
1691
+ }
1692
+ let target;
1693
+ try {
1694
+ target = await loadTarget(resolved.provider);
1695
+ } catch (e) {
1696
+ process.stderr.write(`\u2717 ${e instanceof Error ? e.message : String(e)}
1697
+ `);
1698
+ return 1;
1699
+ }
1700
+ const interactive = deps.interactive ?? (Boolean(process.stdin.isTTY) && !process.env.CI);
1701
+ const ctx = {
1702
+ cwd,
1703
+ functionsDir,
1704
+ env: resolved.env,
1705
+ target: resolved,
1706
+ interactive,
1707
+ spawn: deps.spawn ?? new NodeSpawner(),
1708
+ log: (m) => process.stdout.write(` ${m}
1709
+ `),
1710
+ packageApp: async () => ({ files: await packageApp(functionsDir) }),
1711
+ codegen: async () => {
1712
+ const { generated } = push(await loadFunctionsDir(functionsDir), config.components);
1713
+ writeGenerated(generated.files, join9(functionsDir, "_generated"));
1714
+ }
1715
+ };
1716
+ try {
1717
+ await target.preflight(ctx);
1718
+ await target.package(ctx);
1719
+ if (flags.dryRun) {
1720
+ process.stdout.write(`\u2713 dry-run OK (${resolved.provider} / ${resolved.env}) \u2014 push skipped
1721
+ `);
1722
+ return 0;
1723
+ }
1724
+ const result = await target.push(ctx);
1725
+ if (!result.ok) {
1726
+ process.stderr.write(`\u2717 deploy failed: ${result.error}
1727
+ `);
1728
+ return 1;
1729
+ }
1730
+ process.stdout.write(`\u2713 deployed via ${resolved.provider} (${resolved.env})${result.detail ? ` \u2014 ${result.detail}` : ""}
1731
+ `);
1732
+ if (result.url) process.stdout.write(` ${result.url}
1733
+ `);
1734
+ return 0;
1735
+ } catch (e) {
1736
+ if (e instanceof DeployError) {
1737
+ process.stderr.write(`\u2717 ${e.message}
1738
+ `);
1739
+ return 1;
1740
+ }
1741
+ throw e;
1742
+ }
1743
+ }
1744
+
1745
+ // src/build.ts
1746
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
1747
+ import { spawnSync } from "child_process";
1748
+ import { createRequire as createRequire2 } from "module";
1749
+ import { dirname as dirname6, join as join10, resolve as resolve6 } from "path";
1750
+ import { writeGenerated as writeGenerated2 } from "@helipod/codegen";
1751
+
1752
+ // src/build-entry.ts
1753
+ function generateEntrySource(inp) {
1754
+ const L = ["// AUTO-GENERATED by `helipod build` \u2014 do not edit."];
1755
+ inp.moduleImports.forEach((m, i) => L.push(`import * as m${i} from ${JSON.stringify(m.absPath)};`));
1756
+ L.push(`import schema from ${JSON.stringify(inp.schemaAbsPath)};`);
1757
+ if (inp.configAbsPath) L.push(`import * as __config from ${JSON.stringify(inp.configAbsPath)};`);
1758
+ (inp.dashboardFiles ?? []).forEach((d, i) => L.push(`import d${i} from ${JSON.stringify(d.absPath)} with { type: "file" };`));
1759
+ L.push(`import { runBinaryServer } from "@helipod/cli";`);
1760
+ L.push("");
1761
+ const modEntries = inp.moduleImports.map((m, i) => `${JSON.stringify(m.key)}: m${i}`).join(", ");
1762
+ L.push(`const loaded = { schema, modules: { ${modEntries} } };`);
1763
+ L.push(inp.configAbsPath ? `const components = (__config.default ?? __config).components ?? [];` : `const components = [];`);
1764
+ if (inp.dashboardFiles) {
1765
+ const dEntries = inp.dashboardFiles.map((d, i) => `${JSON.stringify(d.urlPath)}: d${i}`).join(", ");
1766
+ L.push(`const dashboard = { ${dEntries} };`);
1767
+ L.push(`await runBinaryServer(loaded, components, dashboard);`);
1768
+ } else {
1769
+ L.push(`await runBinaryServer(loaded, components, undefined);`);
1770
+ }
1771
+ return L.join("\n") + "\n";
1772
+ }
1773
+
1774
+ // src/build.ts
1775
+ async function resolveBuildOptions(args) {
1776
+ let dirFlag, outfile = "./helipod-server", target = null, dashboard = true, verbose = false;
1777
+ for (let i = 0; i < args.length; i++) {
1778
+ const a = args[i];
1779
+ if (a === "--dir" && args[i + 1]) dirFlag = args[++i];
1780
+ else if (a === "--outfile" && args[i + 1]) outfile = args[++i];
1781
+ else if (a === "--target" && args[i + 1]) target = args[++i];
1782
+ else if (a === "--no-dashboard") dashboard = false;
1783
+ else if (a === "--verbose") verbose = true;
1784
+ }
1785
+ const { functionsDir } = await resolveFunctionsDir(dirFlag, process.cwd());
1786
+ return { functionsDir, outfile, target, dashboard, verbose };
1787
+ }
1788
+ var TARGETS = {
1789
+ "linux-x64": "bun-linux-x64",
1790
+ "linux-arm64": "bun-linux-arm64",
1791
+ "darwin-x64": "bun-darwin-x64",
1792
+ "darwin-arm64": "bun-darwin-arm64",
1793
+ "windows-x64": "bun-windows-x64"
1794
+ };
1795
+ function bunTargetFor(friendly) {
1796
+ const t = TARGETS[friendly];
1797
+ if (!t) throw new Error(`unknown target "${friendly}" (expected one of: ${Object.keys(TARGETS).join(", ")})`);
1798
+ return t;
1799
+ }
1800
+ function dashboardFiles() {
1801
+ try {
1802
+ const indexPath = createRequire2(import.meta.url).resolve("@helipod/dashboard/dist");
1803
+ const dist = dirname6(indexPath);
1804
+ const out = [];
1805
+ const walk2 = (rel) => {
1806
+ for (const e of readdirSync3(join10(dist, rel), { withFileTypes: true })) {
1807
+ const r = rel ? `${rel}/${e.name}` : e.name;
1808
+ if (e.isDirectory()) walk2(r);
1809
+ else out.push({ urlPath: r === "index.html" ? "/" : `/${r}`, absPath: join10(dist, r) });
1810
+ }
1811
+ };
1812
+ walk2("");
1813
+ return out;
1814
+ } catch {
1815
+ return null;
1816
+ }
1817
+ }
1818
+ async function buildCommand(args) {
1819
+ const opts = await resolveBuildOptions(args);
1820
+ const functionsDirAbs = opts.functionsDir;
1821
+ if (!ensureFunctionsDirExists(functionsDirAbs)) return 1;
1822
+ const loaded = await loadFunctionsDir(functionsDirAbs);
1823
+ const config = await loadConfig(dirname6(functionsDirAbs));
1824
+ const { generated } = push(loaded, config.components);
1825
+ writeGenerated2(generated.files, join10(functionsDirAbs, "_generated"));
1826
+ const moduleImports = listFunctionModuleFiles(functionsDirAbs).map((f) => ({ key: moduleKeyForFile(f), absPath: join10(functionsDirAbs, f) }));
1827
+ const schemaAbsPath = join10(functionsDirAbs, existsSync6(join10(functionsDirAbs, "schema.ts")) ? "schema.ts" : "schema.js");
1828
+ const cfgTs = join10(dirname6(functionsDirAbs), "helipod.config.ts"), cfgJs = join10(dirname6(functionsDirAbs), "helipod.config.js");
1829
+ const configAbsPath = existsSync6(cfgTs) ? cfgTs : existsSync6(cfgJs) ? cfgJs : null;
1830
+ const entrySrc = generateEntrySource({ moduleImports, schemaAbsPath, configAbsPath, dashboardFiles: opts.dashboard ? dashboardFiles() : null });
1831
+ const buildDir = resolve6(".helipod-build");
1832
+ mkdirSync4(buildDir, { recursive: true });
1833
+ const entryPath = join10(buildDir, "entry.ts");
1834
+ writeFileSync3(entryPath, entrySrc);
1835
+ const bunArgs = ["build", "--compile", "--minify"];
1836
+ if (opts.target) bunArgs.push(`--target=${bunTargetFor(opts.target)}`);
1837
+ const outfile = opts.target === "windows-x64" && !opts.outfile.endsWith(".exe") ? `${opts.outfile}.exe` : opts.outfile;
1838
+ bunArgs.push(`--outfile=${resolve6(outfile)}`, entryPath);
1839
+ const proc = spawnSync("bun", bunArgs, { stdio: opts.verbose ? "inherit" : ["ignore", "ignore", "inherit"] });
1840
+ rmSync2(buildDir, { recursive: true, force: true });
1841
+ if (proc.error) {
1842
+ process.stderr.write(`\u2717 could not run 'bun build --compile' \u2014 is Bun installed and on PATH? (${proc.error.message})
1843
+ `);
1844
+ return 1;
1845
+ }
1846
+ if (proc.status !== 0) {
1847
+ process.stderr.write("\u2717 bun build --compile failed\n");
1848
+ return 1;
1849
+ }
1850
+ const size = (statSync3(resolve6(outfile)).size / (1024 * 1024)).toFixed(0);
1851
+ process.stdout.write(`\u2713 built ${outfile} (${size}MB)
1852
+ `);
1853
+ return 0;
1854
+ }
1855
+
1856
+ // src/migrate.ts
1857
+ import { writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync as rmSync3, renameSync } from "fs";
1858
+ import { basename as basename2, dirname as dirname7, join as join12, resolve as resolve7 } from "path";
1859
+ import { spawnSync as spawnSync2 } from "child_process";
1860
+ import { writeGenerated as writeGenerated3, generateServer } from "@helipod/codegen";
1861
+
1862
+ // src/migrate/source.ts
1863
+ function resolveSource(sources, id) {
1864
+ const source = sources[id];
1865
+ if (!source) {
1866
+ throw new Error(`unknown migration source "${id}" (available: ${Object.keys(sources).join(", ")})`);
1867
+ }
1868
+ return source;
1869
+ }
1870
+
1871
+ // src/migrate/convex-source.ts
1872
+ import { readFileSync as readFileSync4, readdirSync as readdirSync4, existsSync as existsSync7 } from "fs";
1873
+ import { join as join11, relative as relative2 } from "path";
1874
+
1875
+ // src/migrate/rewrite-imports.ts
1876
+ var SIMPLE = {
1877
+ "convex/values": "@helipod/values",
1878
+ "convex/react": "@helipod/client/react",
1879
+ "convex/browser": "@helipod/client"
1880
+ };
1881
+ var SCHEMA_SYMBOLS = /* @__PURE__ */ new Set(["defineSchema", "defineTable"]);
1882
+ var SERVER_SYMBOLS = /* @__PURE__ */ new Set(["httpRouter", "httpAction"]);
1883
+ var CRON_SYMBOLS = /* @__PURE__ */ new Set(["cronJobs"]);
1884
+ function lineOf(source, index) {
1885
+ let line = 1;
1886
+ for (let i = 0; i < index && i < source.length; i++) if (source[i] === "\n") line++;
1887
+ return line;
1888
+ }
1889
+ function rewriteImports(source, file) {
1890
+ const entries = [];
1891
+ let output = source;
1892
+ for (const [from, to] of Object.entries(SIMPLE)) {
1893
+ const re = new RegExp(`(["'])${from.replace("/", "\\/")}\\1`, "g");
1894
+ const input = output;
1895
+ output = output.replace(re, (_m, q, offset) => {
1896
+ entries.push({ severity: "auto-fixed", file, line: lineOf(input, offset), what: `import "${from}"`, fix: `rewritten to "${to}"` });
1897
+ return `${q}${to}${q}`;
1898
+ });
1899
+ }
1900
+ const serverRe = /import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(["'])convex\/server\2/g;
1901
+ const serverInput = output;
1902
+ output = output.replace(serverRe, (full, names, q, offset) => {
1903
+ const line = lineOf(serverInput, offset);
1904
+ const syms = names.split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
1905
+ const allSchema = syms.length > 0 && syms.every((s) => SCHEMA_SYMBOLS.has(s));
1906
+ const allServer = syms.length > 0 && syms.every((s) => SERVER_SYMBOLS.has(s));
1907
+ if (allSchema) {
1908
+ entries.push({ severity: "auto-fixed", file, line, what: `import "convex/server" (schema)`, fix: `rewritten to "@helipod/values"` });
1909
+ return full.replace(/["']convex\/server["']/, `${q}@helipod/values${q}`);
1910
+ }
1911
+ if (allServer) {
1912
+ entries.push({ severity: "auto-fixed", file, line, what: `import "convex/server" (http)`, fix: `rewritten to "./_generated/server"` });
1913
+ return full.replace(/["']convex\/server["']/, `${q}./_generated/server${q}`);
1914
+ }
1915
+ const hasCron = syms.some((s) => CRON_SYMBOLS.has(s));
1916
+ const hasOther = syms.some((s) => !CRON_SYMBOLS.has(s));
1917
+ const cronFix = `cronJobs \u2192 import from "@helipod/scheduler" and compose defineScheduler() in helipod.config.ts`;
1918
+ const genericFix = `defineSchema/defineTable \u2192 "@helipod/values"; httpRouter/httpAction \u2192 "./_generated/server"`;
1919
+ const fix = hasCron && hasOther ? `${cronFix}; other symbols: map manually: ${genericFix}` : hasCron ? cronFix : `map each symbol manually: ${genericFix}`;
1920
+ entries.push({ severity: "action-needed", file, line, what: `import { ${syms.join(", ")} } from "convex/server"`, fix });
1921
+ return full;
1922
+ });
1923
+ const handledRanges = [];
1924
+ serverRe.lastIndex = 0;
1925
+ let hm;
1926
+ while ((hm = serverRe.exec(output)) !== null) {
1927
+ handledRanges.push([hm.index, hm.index + hm[0].length]);
1928
+ }
1929
+ const residualRe = /(["'])convex\/server\1/g;
1930
+ let m;
1931
+ while ((m = residualRe.exec(output)) !== null) {
1932
+ if (handledRanges.some(([start, end]) => m.index >= start && m.index < end)) continue;
1933
+ entries.push({ severity: "action-needed", file, line: lineOf(output, m.index), what: `import "convex/server"`, fix: `map manually: defineSchema/defineTable \u2192 "@helipod/values"; httpRouter/httpAction \u2192 "./_generated/server"` });
1934
+ }
1935
+ return { output, entries };
1936
+ }
1937
+
1938
+ // src/migrate/scan-divergences.ts
1939
+ import { basename } from "path";
1940
+ var RULES = [
1941
+ {
1942
+ test: /\.withIndex\s*\(/,
1943
+ severity: "action-needed",
1944
+ what: ".withIndex(...) query",
1945
+ fix: `Helipod has no .withIndex \u2014 use ctx.db.query(table, "index").eq(f, v).gte(f, v).order("asc"|"desc").collect()`
1946
+ },
1947
+ {
1948
+ test: /ctx\.db\.patch\s*\(/,
1949
+ severity: "action-needed",
1950
+ what: "ctx.db.patch(...)",
1951
+ fix: `Helipod has no patch \u2014 read the doc, spread-merge, ctx.db.replace(id, { ...doc, ...changes })`
1952
+ },
1953
+ {
1954
+ test: /\.paginate\s*\(/,
1955
+ severity: "action-needed",
1956
+ what: ".paginate(...)",
1957
+ fix: `Helipod paginate({ cursor, pageSize, maxScan? }) returns { page, nextCursor, hasMore, scanCapped }`
1958
+ },
1959
+ {
1960
+ test: /ctx\.auth\b|getUserIdentity\s*\(/,
1961
+ severity: "action-needed",
1962
+ what: "ctx.auth / getUserIdentity()",
1963
+ fix: `Identity is a string token via a context provider (e.g. @helipod/auth's ctx.auth), not a JWT-claims object`
1964
+ },
1965
+ {
1966
+ test: /@convex-dev\/auth|["']convex\/auth["']/,
1967
+ severity: "unsupported",
1968
+ what: "Convex Auth",
1969
+ fix: `Auth is not auto-translated \u2014 use @helipod/auth or external JWT`
1970
+ },
1971
+ {
1972
+ test: /\bapp\.use\s*\(/,
1973
+ severity: "unsupported",
1974
+ what: "Convex Component (app.use)",
1975
+ fix: `Convex Components don't map 1:1 \u2014 recompose via helipod.config.ts`
1976
+ },
1977
+ {
1978
+ test: /\.vectorIndex\s*\(|\.searchIndex\s*\(/,
1979
+ severity: "unsupported",
1980
+ what: "vector/search index",
1981
+ fix: `search/vector is not yet supported in Helipod (see roadmap)`
1982
+ }
1983
+ ];
1984
+ function scanDivergences(source, file) {
1985
+ const entries = [];
1986
+ const lines = source.split("\n");
1987
+ const base = basename(file);
1988
+ if (base === "crons.ts" || /\bcronJobs\s*\(/.test(source)) {
1989
+ const idx = lines.findIndex((l) => /\bcronJobs\s*\(/.test(l));
1990
+ entries.push({
1991
+ severity: "action-needed",
1992
+ file,
1993
+ line: idx >= 0 ? idx + 1 : 1,
1994
+ what: "Convex crons (cronJobs)",
1995
+ fix: `Compose defineScheduler() in helipod.config.ts and use cronJobs() from "@helipod/scheduler"`
1996
+ });
1997
+ }
1998
+ if (base === "convex.config.ts") {
1999
+ entries.push({
2000
+ severity: "unsupported",
2001
+ file,
2002
+ line: 1,
2003
+ what: "Convex app config (convex.config.ts)",
2004
+ fix: `Recompose components via helipod.config.ts`
2005
+ });
2006
+ }
2007
+ for (let i = 0; i < lines.length; i++) {
2008
+ for (const rule of RULES) {
2009
+ if (rule.test.test(lines[i])) {
2010
+ entries.push({ severity: rule.severity, file, line: i + 1, what: rule.what, fix: rule.fix });
2011
+ }
2012
+ }
2013
+ }
2014
+ return entries;
2015
+ }
2016
+
2017
+ // src/migrate/convex-source.ts
2018
+ function walk(dir) {
2019
+ const out = [];
2020
+ for (const ent of readdirSync4(dir, { withFileTypes: true })) {
2021
+ if (ent.name === "_generated" || ent.name === "node_modules") continue;
2022
+ const full = join11(dir, ent.name);
2023
+ if (ent.isDirectory()) out.push(...walk(full));
2024
+ else if (/\.tsx?$/.test(ent.name)) out.push(full);
2025
+ }
2026
+ return out;
2027
+ }
2028
+ var INTRODUCED_PKG = {
2029
+ "@helipod/values": "@helipod/values",
2030
+ "@helipod/client/react": "@helipod/client",
2031
+ "@helipod/client": "@helipod/client"
2032
+ };
2033
+ var convexSource = {
2034
+ id: "convex",
2035
+ async detect(projectRoot) {
2036
+ if (existsSync7(join11(projectRoot, "convex", "schema.ts"))) return true;
2037
+ const pkgPath = join11(projectRoot, "package.json");
2038
+ if (existsSync7(pkgPath)) {
2039
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2040
+ if (pkg.dependencies?.convex) return true;
2041
+ }
2042
+ return false;
2043
+ },
2044
+ async analyze(projectRoot, appDir) {
2045
+ const edits = [];
2046
+ const report = [];
2047
+ const scaffold = [];
2048
+ const introduced = /* @__PURE__ */ new Set();
2049
+ let hasCrons = false;
2050
+ for (const file of walk(appDir)) {
2051
+ const src = readFileSync4(file, "utf8");
2052
+ const rel = relative2(projectRoot, file);
2053
+ const { output, entries } = rewriteImports(src, rel);
2054
+ report.push(...entries, ...scanDivergences(src, rel));
2055
+ for (const [spec, pkg] of Object.entries(INTRODUCED_PKG)) {
2056
+ if (output.includes(`"${spec}"`) && !src.includes(`"${spec}"`)) introduced.add(pkg);
2057
+ }
2058
+ if (output !== src) edits.push({ path: file, newContent: output });
2059
+ if (file.endsWith("crons.ts") || /\bcronJobs\s*\(/.test(src)) hasCrons = true;
2060
+ }
2061
+ const pkgPath = join11(projectRoot, "package.json");
2062
+ if (existsSync7(pkgPath)) {
2063
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2064
+ const deps = { ...pkg.dependencies ?? {} };
2065
+ for (const name of Object.keys(deps)) {
2066
+ if (name === "convex" || name.startsWith("@convex-dev/")) delete deps[name];
2067
+ }
2068
+ for (const pkgName of introduced) deps[pkgName] = "latest";
2069
+ const next = { ...pkg, dependencies: deps };
2070
+ edits.push({ path: pkgPath, newContent: JSON.stringify(next, null, 2) + "\n" });
2071
+ }
2072
+ if (hasCrons) {
2073
+ scaffold.push({
2074
+ path: join11(projectRoot, "helipod.config.ts"),
2075
+ content: `import { defineConfig } from "@helipod/component";
2076
+ import { defineScheduler } from "@helipod/scheduler";
2077
+
2078
+ // Convex crons map to Helipod's scheduler component. Move your cron definitions into
2079
+ // a helipod/crons.ts using cronJobs() from "@helipod/scheduler".
2080
+ export default defineConfig({ components: [defineScheduler()] });
2081
+ `
2082
+ });
2083
+ }
2084
+ return { edits, scaffold, report };
2085
+ }
2086
+ };
2087
+
2088
+ // src/migrate/data.ts
2089
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2090
+ function resolveOptions(args, env, fileFlag) {
2091
+ let url = "";
2092
+ let file = "";
2093
+ let adminKey = env.HELIPOD_ADMIN_KEY?.trim() ?? "";
2094
+ for (let i = 0; i < args.length; i++) {
2095
+ const a = args[i];
2096
+ if (a === "--url" && args[i + 1]) url = args[++i];
2097
+ else if (a === fileFlag && args[i + 1]) file = args[++i];
2098
+ else if (a === "--admin-key" && args[i + 1]) adminKey = args[++i];
2099
+ }
2100
+ if (!url) return { error: "missing target URL \u2014 pass --url <url>" };
2101
+ if (!file) return { error: `missing ${fileFlag} <file>` };
2102
+ if (!adminKey) return { error: "HELIPOD_ADMIN_KEY is required (or pass --admin-key)" };
2103
+ return { url, file, adminKey };
2104
+ }
2105
+ async function migrateExportCommand(args) {
2106
+ const opts = resolveOptions(args, process.env, "--out");
2107
+ if ("error" in opts) {
2108
+ process.stderr.write(`\u2717 ${opts.error}
2109
+ `);
2110
+ return 1;
2111
+ }
2112
+ let res;
2113
+ try {
2114
+ res = await fetch(`${opts.url.replace(/\/$/, "")}/_admin/export`, {
2115
+ method: "GET",
2116
+ headers: { authorization: `Bearer ${opts.adminKey}` }
2117
+ });
2118
+ } catch (e) {
2119
+ process.stderr.write(`\u2717 could not reach ${opts.url}: ${e instanceof Error ? e.message : String(e)}
2120
+ `);
2121
+ return 1;
2122
+ }
2123
+ if (res.status === 401) {
2124
+ process.stderr.write("\u2717 unauthorized \u2014 check HELIPOD_ADMIN_KEY / --admin-key\n");
2125
+ return 1;
2126
+ }
2127
+ if (!res.ok) {
2128
+ const body = await res.json().catch(() => ({}));
2129
+ process.stderr.write(`\u2717 export failed: ${body.error ?? res.statusText}
2130
+ `);
2131
+ return 1;
2132
+ }
2133
+ const dumpText = await res.text();
2134
+ writeFileSync4(opts.file, dumpText);
2135
+ const dump = JSON.parse(dumpText);
2136
+ process.stdout.write(
2137
+ `\u2713 exported ${dump.documents?.length ?? 0} documents, ${dump.indexUpdates?.length ?? 0} index rows \u2192 ${opts.file}
2138
+ `
2139
+ );
2140
+ return 0;
2141
+ }
2142
+ async function migrateImportCommand(args) {
2143
+ const opts = resolveOptions(args, process.env, "--in");
2144
+ if ("error" in opts) {
2145
+ process.stderr.write(`\u2717 ${opts.error}
2146
+ `);
2147
+ return 1;
2148
+ }
2149
+ let dumpText;
2150
+ try {
2151
+ dumpText = readFileSync5(opts.file, "utf8");
2152
+ } catch (e) {
2153
+ process.stderr.write(`\u2717 could not read ${opts.file}: ${e instanceof Error ? e.message : String(e)}
2154
+ `);
2155
+ return 1;
2156
+ }
2157
+ let res;
2158
+ try {
2159
+ res = await fetch(`${opts.url.replace(/\/$/, "")}/_admin/import`, {
2160
+ method: "POST",
2161
+ headers: { "content-type": "application/json", authorization: `Bearer ${opts.adminKey}` },
2162
+ body: dumpText
2163
+ });
2164
+ } catch (e) {
2165
+ process.stderr.write(`\u2717 could not reach ${opts.url}: ${e instanceof Error ? e.message : String(e)}
2166
+ `);
2167
+ return 1;
2168
+ }
2169
+ if (res.status === 401) {
2170
+ process.stderr.write("\u2717 unauthorized \u2014 check HELIPOD_ADMIN_KEY / --admin-key\n");
2171
+ return 1;
2172
+ }
2173
+ const body = await res.json().catch(() => ({}));
2174
+ if (!res.ok || !body.ok) {
2175
+ process.stderr.write(`\u2717 import failed: ${body.error ?? res.statusText}
2176
+ `);
2177
+ return 1;
2178
+ }
2179
+ process.stdout.write(
2180
+ `\u2713 imported ${body.imported?.documents ?? 0} documents, ${body.imported?.indexUpdates ?? 0} index rows
2181
+ `
2182
+ );
2183
+ return 0;
2184
+ }
2185
+
2186
+ // src/migrate.ts
2187
+ var SOURCES = { convex: convexSource };
2188
+ function parse(args) {
2189
+ const out = { from: "convex", functionsDir: "convex", dryRun: false, force: false };
2190
+ for (let i = 0; i < args.length; i++) {
2191
+ const a = args[i];
2192
+ if (a === "--from" && args[i + 1]) out.from = args[++i];
2193
+ else if (a === "--dir" && args[i + 1]) out.functionsDir = args[++i];
2194
+ else if (a === "--dry-run") out.dryRun = true;
2195
+ else if (a === "--force") out.force = true;
2196
+ }
2197
+ return out;
2198
+ }
2199
+ function renderReport(entries) {
2200
+ const by = (s) => entries.filter((e) => e.severity === s);
2201
+ const section = (title, items) => items.length === 0 ? "" : `
2202
+ ## ${title} (${items.length})
2203
+
2204
+ ` + items.map((e) => `- \`${e.file}${e.line ? `:${e.line}` : ""}\` \u2014 ${e.what}. **Fix:** ${e.fix}`).join("\n") + "\n";
2205
+ return `# Helipod migration report
2206
+
2207
+ ${by("auto-fixed").length} auto-fixed, ${by("action-needed").length} action-needed, ${by("unsupported").length} unsupported.
2208
+ ` + section("Auto-fixed", by("auto-fixed")) + section("Action needed", by("action-needed")) + section("Unsupported", by("unsupported"));
2209
+ }
2210
+ function gitDirty(dir) {
2211
+ const r = spawnSync2("git", ["status", "--porcelain"], { cwd: dir, encoding: "utf8" });
2212
+ if (r.status !== 0) return null;
2213
+ return r.stdout.trim().length > 0;
2214
+ }
2215
+ async function migrateCommand(args) {
2216
+ if (args[0] === "export") return migrateExportCommand(args.slice(1));
2217
+ if (args[0] === "import") return migrateImportCommand(args.slice(1));
2218
+ const opts = parse(args);
2219
+ const functionsDir = resolve7(opts.functionsDir);
2220
+ const projectRoot = dirname7(functionsDir);
2221
+ const dirty = gitDirty(projectRoot);
2222
+ if (dirty === true && !opts.force) {
2223
+ process.stderr.write(
2224
+ `refusing to migrate: ${projectRoot} has uncommitted changes (commit/stash first, or pass --force)
2225
+ `
2226
+ );
2227
+ return 1;
2228
+ }
2229
+ if (dirty === null) {
2230
+ process.stderr.write(`warning: ${projectRoot} is not a git repo \u2014 changes will be made in place with no easy revert
2231
+ `);
2232
+ }
2233
+ let source;
2234
+ try {
2235
+ source = resolveSource(SOURCES, opts.from);
2236
+ } catch (e) {
2237
+ process.stderr.write(`${String(e)}
2238
+ `);
2239
+ return 1;
2240
+ }
2241
+ if (!await source.detect(projectRoot)) {
2242
+ process.stderr.write(`no ${opts.from} project detected at ${projectRoot}
2243
+ `);
2244
+ return 1;
2245
+ }
2246
+ const { functionsDir: targetDir } = await resolveFunctionsDir(void 0, projectRoot);
2247
+ let migratedDir = functionsDir;
2248
+ let renamed = false;
2249
+ let pendingRename = false;
2250
+ if (resolve7(functionsDir) !== resolve7(targetDir)) {
2251
+ if (existsSync8(targetDir)) {
2252
+ process.stderr.write(
2253
+ `refusing to migrate: ${targetDir} already exists (remove or rename it, then re-run)
2254
+ `
2255
+ );
2256
+ return 1;
2257
+ }
2258
+ if (opts.dryRun) {
2259
+ pendingRename = true;
2260
+ } else {
2261
+ try {
2262
+ let didGitMv = false;
2263
+ if (dirty !== null) {
2264
+ const r = spawnSync2("git", ["mv", functionsDir, targetDir], { cwd: projectRoot });
2265
+ didGitMv = r.status === 0;
2266
+ }
2267
+ if (!didGitMv) renameSync(functionsDir, targetDir);
2268
+ } catch (e) {
2269
+ process.stderr.write(
2270
+ `refusing to migrate: could not rename ${functionsDir} to ${targetDir}: ${e instanceof Error ? e.message : String(e)}
2271
+ `
2272
+ );
2273
+ return 1;
2274
+ }
2275
+ migratedDir = targetDir;
2276
+ renamed = true;
2277
+ process.stdout.write(`renamed ${functionsDir} \u2192 ${targetDir}
2278
+ `);
2279
+ }
2280
+ }
2281
+ const plan = await source.analyze(projectRoot, migratedDir);
2282
+ if (renamed || pendingRename) {
2283
+ plan.report.unshift({
2284
+ severity: "auto-fixed",
2285
+ file: `${basename2(functionsDir)}/`,
2286
+ what: pendingRename ? `would be renamed to ${basename2(targetDir)}/` : `renamed to ${basename2(targetDir)}/`,
2287
+ fix: pendingRename ? `Your backend functions will move to ${basename2(targetDir)}/ when you run this migration without --dry-run. Imports inside that folder are relative and will not change.` : `Your backend functions now live in ${basename2(targetDir)}/. Imports inside that folder are relative and did not change.`
2288
+ });
2289
+ }
2290
+ writeFileSync5(join12(projectRoot, "MIGRATION-REPORT.md"), renderReport(plan.report));
2291
+ if (opts.dryRun) {
2292
+ const renameNote = pendingRename ? ` ${basename2(functionsDir)}/ would be renamed to ${basename2(targetDir)}/.` : "";
2293
+ process.stdout.write(
2294
+ `[dry-run] ${plan.edits.length} files would change, ${plan.scaffold.length} scaffolded.${renameNote} See MIGRATION-REPORT.md
2295
+ `
2296
+ );
2297
+ return 0;
2298
+ }
2299
+ for (const edit of plan.edits) writeFileSync5(edit.path, edit.newContent);
2300
+ for (const file of plan.scaffold) if (!existsSync8(file.path)) writeFileSync5(file.path, file.content);
2301
+ try {
2302
+ const generatedDir = join12(migratedDir, "_generated");
2303
+ rmSync3(generatedDir, { recursive: true, force: true });
2304
+ const config = await loadConfig(projectRoot);
2305
+ if (!existsSync8(join12(generatedDir, "server.ts"))) {
2306
+ const stub = generateServer(
2307
+ { tables: {}, schemaValidation: false },
2308
+ { components: config.components.map((c) => ({ name: c.name, contextType: c.contextType, serverExports: c.serverExports })) }
2309
+ );
2310
+ mkdirSync5(generatedDir, { recursive: true });
2311
+ writeFileSync5(join12(generatedDir, "server.ts"), stub.content);
2312
+ }
2313
+ const loaded = await loadFunctionsDir(migratedDir);
2314
+ const { generated } = push(loaded, config.components);
2315
+ writeGenerated3(generated.files, generatedDir);
2316
+ } catch (e) {
2317
+ process.stderr.write(
2318
+ `imports migrated, but codegen failed: ${String(e)}
2319
+ See MIGRATION-REPORT.md; fix the flagged items, then run \`helipod codegen\`.
2320
+ `
2321
+ );
2322
+ return 1;
2323
+ }
2324
+ const n = plan.report.filter((r) => r.severity !== "auto-fixed").length;
2325
+ process.stdout.write(`migrated ${plan.edits.length} files. ${n} item(s) need manual attention \u2014 see MIGRATION-REPORT.md
2326
+ `);
2327
+ return 0;
2328
+ }
2329
+
2330
+ // src/fleet.ts
2331
+ var FLEET_ERR_NO_PACKAGE2 = "fleet mode requires @helipod/fleet \u2014 install it (bun add @helipod/fleet).";
2332
+ function parseReshardArgs(args) {
2333
+ let shardsRaw;
2334
+ let databaseUrl = process.env.HELIPOD_DATABASE_URL;
2335
+ for (let i = 0; i < args.length; i++) {
2336
+ const a = args[i];
2337
+ if (a === "--shards" && args[i + 1]) shardsRaw = args[++i];
2338
+ else if (a === "--database-url" && args[i + 1]) databaseUrl = args[++i];
2339
+ }
2340
+ if (shardsRaw === void 0) {
2341
+ return { ok: false, error: "\u2717 --shards <M> is required \u2014 e.g. --shards 4" };
2342
+ }
2343
+ const targetShards = Number(shardsRaw);
2344
+ if (!Number.isInteger(targetShards) || targetShards < 1) {
2345
+ return { ok: false, error: `\u2717 --shards must be an integer >= 1, got ${JSON.stringify(shardsRaw)}` };
2346
+ }
2347
+ if (!isPostgresUrl(databaseUrl)) {
2348
+ return {
2349
+ ok: false,
2350
+ error: "\u2717 --database-url postgres://\u2026 is required (or HELIPOD_DATABASE_URL) \u2014 fleet reshard is Postgres-only"
2351
+ };
2352
+ }
2353
+ return { ok: true, args: { targetShards, databaseUrl } };
2354
+ }
2355
+ async function reshardCommand(args) {
2356
+ const parsed = parseReshardArgs(args);
2357
+ if (!parsed.ok) {
2358
+ process.stderr.write(parsed.error + "\n");
2359
+ return 1;
2360
+ }
2361
+ const { targetShards, databaseUrl } = parsed.args;
2362
+ let fleetModule;
2363
+ try {
2364
+ const fleetSpecifier = "@helipod/fleet";
2365
+ fleetModule = await import(fleetSpecifier);
2366
+ } catch {
2367
+ process.stderr.write(`\u2717 ${FLEET_ERR_NO_PACKAGE2}
2368
+ `);
2369
+ return 1;
2370
+ }
2371
+ const client = makePgClient(databaseUrl);
2372
+ try {
2373
+ const result = await fleetModule.reshardFleet(client, { targetShards });
2374
+ process.stdout.write(
2375
+ `\u2713 resharded ${result.previousShards} \u2192 ${result.newShards} shards (created: ${result.created.length ? result.created.join(", ") : "none"}, deleted: ${result.deleted.length ? result.deleted.join(", ") : "none"}, frontier floor: ${result.frontierFloor}); update HELIPOD_FLEET_SHARDS to ${result.newShards} (or unset) before restarting the fleet
2376
+ `
2377
+ );
2378
+ return 0;
2379
+ } catch (e) {
2380
+ process.stderr.write(`\u2717 ${e instanceof Error ? e.message : String(e)}
2381
+ `);
2382
+ return 1;
2383
+ } finally {
2384
+ await client.close();
2385
+ }
2386
+ }
2387
+ async function fleetCommand(args) {
2388
+ const [sub, ...rest] = args;
2389
+ switch (sub) {
2390
+ case "reshard":
2391
+ return reshardCommand(rest);
2392
+ default:
2393
+ process.stderr.write(
2394
+ `\u2717 unknown fleet subcommand: ${sub ?? "(none)"}
2395
+ Usage: helipod fleet reshard --shards M --database-url <url>
2396
+ `
2397
+ );
2398
+ return 1;
2399
+ }
2400
+ }
2401
+
2402
+ // src/objectstore.ts
2403
+ import { dirname as dirname8 } from "path";
2404
+ async function objectstoreCommand(args) {
2405
+ const sub = args[0];
2406
+ if (sub !== "reshard") {
2407
+ process.stderr.write(
2408
+ `\u2717 unknown \`objectstore\` subcommand '${sub ?? ""}' \u2014 usage: helipod objectstore reshard --object-store <url> --dir <functionsDir> --shards M
2409
+ `
2410
+ );
2411
+ return 1;
2412
+ }
2413
+ return reshardCommand2(args.slice(1));
2414
+ }
2415
+ async function reshardCommand2(args) {
2416
+ let objectStoreUrl = process.env.HELIPOD_OBJECT_STORE;
2417
+ let dirFlag;
2418
+ let shards;
2419
+ const VALUE_FLAGS = /* @__PURE__ */ new Set(["--object-store", "--dir", "--shards"]);
2420
+ for (let i = 0; i < args.length; i++) {
2421
+ const a = args[i];
2422
+ if (!VALUE_FLAGS.has(a)) continue;
2423
+ const val = args[i + 1];
2424
+ if (val === void 0) {
2425
+ process.stderr.write(`\u2717 ${a} requires a value.
2426
+ `);
2427
+ return 1;
2428
+ }
2429
+ i++;
2430
+ if (a === "--object-store") objectStoreUrl = val;
2431
+ else if (a === "--dir") dirFlag = val;
2432
+ else shards = Number(val);
2433
+ }
2434
+ if (!objectStoreUrl) {
2435
+ process.stderr.write("\u2717 objectstore reshard requires --object-store <url> (or HELIPOD_OBJECT_STORE).\n");
2436
+ return 1;
2437
+ }
2438
+ if (shards === void 0 || !Number.isInteger(shards) || shards < 1) {
2439
+ process.stderr.write("\u2717 objectstore reshard requires --shards <M> (a positive integer).\n");
2440
+ return 1;
2441
+ }
2442
+ try {
2443
+ const resolved = resolveObjectStore(objectStoreUrl);
2444
+ if (resolved === null) {
2445
+ process.stderr.write(`\u2717 --object-store "${objectStoreUrl}" did not resolve to a store (empty/unset value?).
2446
+ `);
2447
+ return 1;
2448
+ }
2449
+ const { functionsDir } = await resolveFunctionsDir(dirFlag, process.cwd());
2450
+ if (!ensureFunctionsDirExists(functionsDir)) return 1;
2451
+ const loaded = await loadFunctionsDir(functionsDir);
2452
+ const config = await loadConfig(dirname8(functionsDir));
2453
+ const { project } = push(loaded, config.components);
2454
+ const shardKeyFor = (tableNumber) => project.catalog.getTableByNumber(tableNumber)?.shardKey ?? null;
2455
+ const substrate = await loadObjectStoreSubstrateModule();
2456
+ await resolved.objectStore.assertCasSupported();
2457
+ const result = await substrate.reshardObjectStore({
2458
+ objectStore: resolved.objectStore,
2459
+ toShards: shards,
2460
+ now: Date.now(),
2461
+ shardKeyFor,
2462
+ makeLocal: makeInMemorySqliteStore
2463
+ });
2464
+ if (result.fromShards === result.toShards) {
2465
+ process.stdout.write(`\u2713 bucket is already at ${result.toShards} shard(s) \u2014 nothing to do.
2466
+ `);
2467
+ return 0;
2468
+ }
2469
+ const perLane = Object.entries(result.perLaneCounts).map(([lane, n]) => `${lane}=${n}`).join(", ");
2470
+ process.stdout.write(
2471
+ `\u2713 resharded ${result.fromShards} \u2192 ${result.toShards} shard(s) (moved ${result.movedDocs} doc(s); per-lane: ${perLane}). A node booting this bucket now uses ${result.toShards} shard(s) \u2014 set --shards ${result.toShards} (or HELIPOD_FLEET_SHARDS), or drop it (the bucket's persisted count is authoritative).
2472
+ `
2473
+ );
2474
+ return 0;
2475
+ } catch (e) {
2476
+ process.stderr.write(`\u2717 ${e instanceof Error ? e.message : String(e)}
2477
+ `);
2478
+ return 1;
2479
+ }
2480
+ }
2481
+
2482
+ // src/cli.ts
2483
+ function parseFlags(args) {
2484
+ const out = {};
2485
+ for (let i = 0; i < args.length; i++) {
2486
+ const a = args[i];
2487
+ if (a === "--port" && args[i + 1]) out.port = Number(args[++i]);
2488
+ else if (a === "--ip" && args[i + 1]) out.ip = args[++i];
2489
+ else if (a === "--dir" && args[i + 1]) out.functionsDir = args[++i];
2490
+ else if (a === "--data" && args[i + 1]) out.dataPath = args[++i];
2491
+ else if (a === "--web" && args[i + 1]) out.webDir = args[++i];
2492
+ else if (a === "--database-url" && args[i + 1]) out.databaseUrl = args[++i];
2493
+ else if (a === "--storage-bucket" && args[i + 1]) out.storageBucket = args[++i];
2494
+ else if (a === "--storage-endpoint" && args[i + 1]) out.storageEndpoint = args[++i];
2495
+ }
2496
+ return out;
2497
+ }
2498
+ async function devCommand(args) {
2499
+ const flags = parseFlags(args);
2500
+ const { functionsDir, projectRoot } = await resolveFunctionsDir(flags.functionsDir, process.cwd());
2501
+ if (!ensureFunctionsDirExists(functionsDir)) {
2502
+ return 1;
2503
+ }
2504
+ const opts = resolveDevOptions({ ...flags, functionsDir });
2505
+ const generatedDir = join13(opts.functionsDir, "_generated");
2506
+ const config = await loadConfig(projectRoot);
2507
+ const envKey = process.env.HELIPOD_ADMIN_KEY?.trim();
2508
+ if (process.env.HELIPOD_ADMIN_KEY !== void 0 && !envKey) {
2509
+ process.stderr.write("\u26A0 HELIPOD_ADMIN_KEY is set but empty \u2014 generating an ephemeral key instead.\n");
2510
+ }
2511
+ const adminKey = envKey || generateAdminKey();
2512
+ const ephemeralKey = !envKey;
2513
+ const loopback = ["127.0.0.1", "::1", "localhost"].includes(opts.ip);
2514
+ const { runtime, adminApi, project, generated, store, logSink, storageRoutes: storageRoutes2 } = await bootProject({
2515
+ functionsDir: opts.functionsDir,
2516
+ dataPath: opts.dataPath,
2517
+ adminKey,
2518
+ databaseUrl: opts.databaseUrl,
2519
+ storage: { bucket: opts.storageBucket, endpoint: opts.storageEndpoint }
2520
+ });
2521
+ writeGenerated4(generated.files, generatedDir);
2522
+ const dashboard = loadDashboard(ephemeralKey && loopback ? adminKey : void 0);
2523
+ const host = new ProcessRuntimeHost();
2524
+ const server = await host.serve(
2525
+ runtime,
2526
+ { port: opts.port, ip: opts.ip, webDir: opts.webDir, admin: { api: adminApi, key: adminKey }, dashboard, routes: project.routes, storageRoutes: storageRoutes2 }
2527
+ );
2528
+ process.stdout.write(`helipod dev \u2192 ${server.url} (dashboard: ${server.url}/_dashboard)
2529
+ `);
2530
+ if (!dashboard) process.stdout.write(` (dashboard SPA not built \u2014 run \`bun run --filter @helipod/dashboard build\`)
2531
+ `);
2532
+ process.stdout.write(`admin key \u2192 ${adminKey}
2533
+ `);
2534
+ if (opts.webDir) process.stdout.write(`web UI \u2192 ${server.url}/
2535
+ `);
2536
+ const watcher = createWatchLoop({
2537
+ subscribe: (onChange) => {
2538
+ const w = fsWatch(resolve8(opts.functionsDir), { recursive: true }, (_e, file) => {
2539
+ if (file && !String(file).includes("_generated")) onChange();
2540
+ });
2541
+ return () => w.close();
2542
+ },
2543
+ onTrigger: async (reason) => {
2544
+ if (reason === "initial") return;
2545
+ try {
2546
+ const next = push(await loadFunctionsDir(opts.functionsDir), config.components);
2547
+ writeGenerated4(next.generated.files, generatedDir);
2548
+ runtime.setModules(withStorageModules(next.project.moduleMap));
2549
+ server.setRoutes(next.project.routes);
2550
+ process.stdout.write(`\u21BB pushed (${Object.keys(next.project.moduleMap).length} functions)
2551
+ `);
2552
+ } catch (e) {
2553
+ process.stderr.write(`\u2717 reload failed: ${e instanceof Error ? e.message : String(e)}
2554
+ `);
2555
+ }
2556
+ }
2557
+ });
2558
+ watcher.start();
2559
+ return new Promise(() => {
2560
+ });
2561
+ }
2562
+ async function codegenCommand(args) {
2563
+ const flags = parseFlags(args);
2564
+ const { functionsDir } = await resolveFunctionsDir(flags.functionsDir, process.cwd());
2565
+ if (!ensureFunctionsDirExists(functionsDir)) return 1;
2566
+ const opts = resolveDevOptions({ ...flags, functionsDir });
2567
+ const loaded = await loadFunctionsDir(opts.functionsDir);
2568
+ const config = await loadConfig(dirname9(opts.functionsDir));
2569
+ const { generated } = push(loaded, config.components);
2570
+ writeGenerated4(generated.files, join13(opts.functionsDir, "_generated"));
2571
+ process.stdout.write(`generated ${opts.functionsDir}/_generated
2572
+ `);
2573
+ return 0;
2574
+ }
2575
+ function printHelp() {
2576
+ process.stdout.write(
2577
+ [
2578
+ "helipod - the reactive backend you self-host",
2579
+ "",
2580
+ "Usage: helipod <command> [options]",
2581
+ "",
2582
+ "Commands:",
2583
+ " dev Run the engine with hot reload + dashboard",
2584
+ " serve Run the production server (requires HELIPOD_ADMIN_KEY)",
2585
+ " deploy Deploy the app: --target <serve|cloudflare|docker|railway|fly|aws> --env <name> [--dry-run] [--check]",
2586
+ " build Compile the app to a self-contained executable (bun build --compile)",
2587
+ " migrate Migrate a Convex project into Helipod (imports + report)",
2588
+ " migrate export --url <src> --out dump.json Export app data to a portable dump",
2589
+ " migrate import --url <dst> --in dump.json Import a dump into a deployment",
2590
+ " codegen Regenerate <functionsDir>/_generated types",
2591
+ " fleet reshard --shards M --database-url <url> Change a STOPPED fleet's shard count",
2592
+ " objectstore reshard --shards M --object-store <url> --dir <functionsDir> Change a STOPPED object-storage deployment's shard count",
2593
+ " help Show this help",
2594
+ "",
2595
+ "Options: --port <n> --ip <addr> --dir <functionsDir> --data <dbPath> --database-url <url>",
2596
+ "Deploy: --target <name> --env <name> --dry-run --check (default target: serve; default env: production)",
2597
+ ""
2598
+ ].join("\n")
2599
+ );
2600
+ }
2601
+ async function runCli(argv) {
2602
+ const [cmd, ...rest] = argv;
2603
+ switch (cmd) {
2604
+ case "dev":
2605
+ return devCommand(rest);
2606
+ case "serve":
2607
+ return serveCommand(rest);
2608
+ case "deploy":
2609
+ return deployCommand(rest);
2610
+ case "build":
2611
+ return buildCommand(rest);
2612
+ case "migrate":
2613
+ return migrateCommand(rest);
2614
+ case "codegen":
2615
+ return codegenCommand(rest);
2616
+ case "fleet":
2617
+ return fleetCommand(rest);
2618
+ case "objectstore":
2619
+ return objectstoreCommand(rest);
2620
+ case "help":
2621
+ case "--help":
2622
+ case "-h":
2623
+ case void 0:
2624
+ printHelp();
2625
+ return 0;
2626
+ default:
2627
+ process.stderr.write(`unknown command: ${cmd}
2628
+ `);
2629
+ printHelp();
2630
+ return 1;
2631
+ }
2632
+ }
2633
+
2634
+ export {
2635
+ loadConfig,
2636
+ DEFAULT_FUNCTIONS_DIR,
2637
+ resolveFunctionsDir,
2638
+ functionsDirNotFoundMessage,
2639
+ resolveDevOptions,
2640
+ detectRuntime,
2641
+ loadFunctionsDir,
2642
+ push,
2643
+ withStorageModules,
2644
+ bootLoaded,
2645
+ bootProject,
2646
+ startDevServer,
2647
+ ProcessRuntimeHost,
2648
+ createWatchLoop,
2649
+ devCommand,
2650
+ codegenCommand,
2651
+ runCli
2652
+ };
2653
+ //# sourceMappingURL=chunk-IBFZ2HUR.js.map