@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,366 @@
1
+ import { ProjectArtifacts, LoadedProject, ResolvedRoute } from './project.js';
2
+ export { DEFAULT_INDEX, loadProject } from './project.js';
3
+ import { GeneratedBundle } from '@helipod/codegen';
4
+ export { writeGenerated } from '@helipod/codegen';
5
+ import { ComponentDefinition, HelipodConfig, WakeHost } from '@helipod/component';
6
+ export { HelipodConfig } from '@helipod/component';
7
+ import { D as DeployWireResult, F as FleetHandles } from './http-handler-BFprdo3f.js';
8
+ export { H as HttpRequest, a as HttpResponse, S as ServerInfo, h as handleHttpRequest } from './http-handler-BFprdo3f.js';
9
+ import { ServerHandle, ServeOptions, RuntimeHost, EmbeddedRuntime, WriteRouter, EmbeddedWriteFanoutAdapter } from '@helipod/runtime-embedded';
10
+ import { AdminApi } from '@helipod/admin';
11
+ import { StorageRoute } from '@helipod/storage';
12
+ import { DocStore } from '@helipod/docstore';
13
+ import { InMemoryLogSink, RegisteredFunction } from '@helipod/executor';
14
+ import { BlobStore } from '@helipod/blobstore';
15
+ import { S3Config } from '@helipod/blobstore-s3';
16
+ import '@helipod/values';
17
+
18
+ /**
19
+ * The push pipeline: load the project → run codegen → return artifacts. `helipod dev` runs
20
+ * this on startup and on every file change (hot reload re-runs `push` and re-registers the
21
+ * module map with the running engine — no restart).
22
+ */
23
+
24
+ interface PushResult {
25
+ project: ProjectArtifacts;
26
+ generated: GeneratedBundle;
27
+ }
28
+ declare function push(loaded: LoadedProject, components?: ComponentDefinition[], existingTableNumbers?: Record<string, number>): PushResult;
29
+
30
+ type RuntimeKind = "bun" | "node" | "auto";
31
+ interface DevOptions {
32
+ port?: number;
33
+ ip?: string;
34
+ functionsDir?: string;
35
+ dataPath?: string;
36
+ runtime?: RuntimeKind;
37
+ /** Optional static web UI directory to serve alongside the API/WebSocket. */
38
+ webDir?: string;
39
+ /** Postgres connection string (flag wins over `HELIPOD_DATABASE_URL`); unset → SQLite. */
40
+ databaseUrl?: string;
41
+ /** File-storage backend flag overrides (`--storage-bucket`/`--storage-endpoint`; win over env). */
42
+ storageBucket?: string;
43
+ storageEndpoint?: string;
44
+ }
45
+ interface ResolvedDevOptions {
46
+ port: number;
47
+ ip: string;
48
+ functionsDir: string;
49
+ dataPath: string;
50
+ runtime: RuntimeKind;
51
+ webDir: string | undefined;
52
+ databaseUrl: string | undefined;
53
+ storageBucket: string | undefined;
54
+ storageEndpoint: string | undefined;
55
+ }
56
+ declare function resolveDevOptions(options?: DevOptions): ResolvedDevOptions;
57
+ /** Detect the active JS runtime (Bun is primary; Node is supported). */
58
+ declare function detectRuntime(): "bun" | "node";
59
+
60
+ type DevServer = ServerHandle<ResolvedRoute>;
61
+ type DevServerOptions = ServeOptions<ResolvedRoute, AdminApi, StorageRoute, DeployWireResult, FleetHandles>;
62
+ /** Start the dev server using the best backend for the current runtime. */
63
+ declare function startDevServer(runtime: EmbeddedRuntime, options: DevServerOptions): Promise<DevServer>;
64
+ /**
65
+ * The process {@link RuntimeHost}: binds a runtime to a real TCP port via `Bun.serve` (primary) or
66
+ * `node:http` + `ws`. This class is the ONLY place in the CLI that owns those host primitives — the
67
+ * `detectRuntime()` bun/node split is its private internal (in {@link startDevServer}). Production
68
+ * entrypoints (`dev`/`serve`/`binary`) reach serving exclusively through `host.serve(...)`; a
69
+ * Durable Object host (Slice 3) will implement the same seam without touching any of the above.
70
+ * `serve` is a thin delegation to {@link startDevServer}, which stays exported for tests/benches.
71
+ */
72
+ declare class ProcessRuntimeHost implements RuntimeHost<ResolvedRoute, AdminApi, StorageRoute, DeployWireResult, FleetHandles> {
73
+ serve(runtime: EmbeddedRuntime, options: DevServerOptions): Promise<DevServer>;
74
+ }
75
+
76
+ /**
77
+ * A small debounced watch loop. The fs-watch wiring is injected so the debounce/dispatch
78
+ * logic is testable without touching the filesystem.
79
+ */
80
+ type WatchTriggerReason = "initial" | "change";
81
+ interface WatchLoopOptions {
82
+ /** Subscribe to raw change events; return an unsubscribe. */
83
+ subscribe: (onChange: () => void) => () => void;
84
+ /** Called (debounced) to rebuild; reason "initial" fires once up front. */
85
+ onTrigger: (reason: WatchTriggerReason) => void | Promise<void>;
86
+ debounceMs?: number;
87
+ /** Injectable timer for tests. */
88
+ setTimer?: (fn: () => void, ms: number) => unknown;
89
+ clearTimer?: (handle: unknown) => void;
90
+ }
91
+ interface WatchLoop {
92
+ start(): void;
93
+ stop(): void;
94
+ }
95
+ declare function createWatchLoop(options: WatchLoopOptions): WatchLoop;
96
+
97
+ declare function loadFunctionsDir(dir: string): Promise<LoadedProject>;
98
+
99
+ declare function loadConfig(projectDir: string): Promise<HelipodConfig>;
100
+
101
+ /** The default backend functions directory name. */
102
+ declare const DEFAULT_FUNCTIONS_DIR = "helipod";
103
+ interface ResolvedFunctionsDir {
104
+ /** Absolute path to the functions directory. */
105
+ functionsDir: string;
106
+ /** Absolute path to the project root (where helipod.config.ts lives). */
107
+ projectRoot: string;
108
+ }
109
+ /**
110
+ * Resolve the functions directory. Precedence, highest first:
111
+ * 1. an explicit `--dir` value
112
+ * 2. `functionsDir` in helipod.config.ts
113
+ * 3. DEFAULT_FUNCTIONS_DIR
114
+ *
115
+ * There is deliberately NO implicit fallback to `convex/`: a Convex layout is
116
+ * converted by `helipod migrate`, never adopted silently. See
117
+ * docs_old/superpowers/specs/2026-06-06-functions-dir-rename-design.md.
118
+ *
119
+ * With an explicit flag the project root is that path's parent, so a caller can point at a
120
+ * functions directory anywhere. Without one the root is the cwd, which is what makes it safe
121
+ * to read the config before the directory name is known: helipod.config.ts always sits at
122
+ * the root and never inside the functions directory.
123
+ */
124
+ declare function resolveFunctionsDir(flagValue: string | undefined, cwd: string): Promise<ResolvedFunctionsDir>;
125
+ /** The failure message when the resolved directory does not exist. The one Convex-aware string in the CLI. */
126
+ declare function functionsDirNotFoundMessage(functionsDir: string): string;
127
+
128
+ declare function devCommand(args: string[]): Promise<number>;
129
+ declare function codegenCommand(args: string[]): Promise<number>;
130
+ declare function runCli(argv: string[]): Promise<number>;
131
+
132
+ /** Resolved storage config. A `bucket` selects the S3 backend; everything else is FS. */
133
+ type StorageConfig = Partial<S3Config> & {
134
+ bucket?: string;
135
+ };
136
+
137
+ interface BootResult {
138
+ runtime: EmbeddedRuntime;
139
+ adminApi: AdminApi;
140
+ project: ProjectArtifacts;
141
+ generated: GeneratedBundle;
142
+ store: DocStore;
143
+ logSink: InMemoryLogSink;
144
+ /** The boot-time component set (from helipod.config.ts) — `applyDeploy` re-composes against it. */
145
+ components: ComponentDefinition[];
146
+ /** The always-on file-storage byte backend (FS or S3), shared by the provider/reaper/routes. */
147
+ blobStore: BlobStore;
148
+ /**
149
+ * The engine-owned `/api/storage/*` handlers (upload/confirm/serve). Reserved-path routes the
150
+ * server splices into its dispatch (NOT user `http.ts` routes) — see `server.ts`.
151
+ */
152
+ storageRoutes: StorageRoute[];
153
+ /**
154
+ * Reserved engine routes contributed by composed components (e.g. `@helipod/auth`'s
155
+ * `/api/auth/oauth/*`), each bound to `runtime.runHttpAction` and shaped as an engine-owned
156
+ * `StorageRoute` `{method,pathPrefix,handler}` so `server.ts` dispatches them exactly like the
157
+ * always-on storage routes. Fixed at boot (the component set is fixed at boot — only functions/
158
+ * schema hot-swap), so no `setRoutes` live-swap is needed. Empty when no component declares routes.
159
+ */
160
+ componentRoutes: StorageRoute[];
161
+ /**
162
+ * Set only when `objectStoreUrl` was given — the object-store node's graceful-shutdown handle.
163
+ * `serve.ts`'s shutdown calls this AFTER `server.close()` and BEFORE `store.close()`. Two shapes,
164
+ * mutually exclusive by construction (at most one of `replica`/writer ever built this boot):
165
+ * - Writer (Tier 3 Slice 6, Task 6.5): `store.relinquish()`, which best-effort CAS-clears the
166
+ * lease IN THE BUCKET, not just in-process — see `ObjectStoreDocStore.relinquish()`'s doc —
167
+ * so a challenger takes over immediately instead of waiting out the full TTL.
168
+ * - Replica (Tier 3 Slice 8, Task 8.2): stops the reactive-tailer wiring helper (Task 8.1) and
169
+ * `removeConsumer`s this replica's watermark, so a departed replica stops pinning the
170
+ * writer's gc — see `buildObjectStoreReplicaNode`'s doc.
171
+ */
172
+ objectStoreRelease?: () => Promise<void>;
173
+ /**
174
+ * Tier 3 Slice 8 follow-on (replica write-forwarding): set only when this boot is a
175
+ * `--replica` configured with `--writer-url` (i.e. a `ReplicaWriteForwarder` was wired in as
176
+ * the runtime's `writeRouter`). `serve.ts` threads this straight through to
177
+ * `startDevServer`'s `replicaWriterUrl` option, which arms `/api/run`'s single-hop defensive
178
+ * guard (`http-handler.ts`). Absent whenever `--writer-url` is unset (the pre-existing
179
+ * reject-with-message replica behavior, unchanged) or this isn't a replica boot at all.
180
+ */
181
+ replicaWriterUrl?: string;
182
+ }
183
+ /**
184
+ * Merge the always-on `_storage:*` privileged built-ins into a function map. The `_storage` modules
185
+ * must live in the runtime's `modules` map (not just `systemModules`) because the action-mode
186
+ * `ctx.storage.store` reaches them through the trusted `invoke`, and the reaper driver through
187
+ * `runFunction` — both resolve `modules`. Since `setModules` (dev reload / `helipod deploy`)
188
+ * REPLACES `modules` wholesale, every such swap must re-apply this so storage survives a hot-swap.
189
+ */
190
+ declare function withStorageModules(map: Record<string, RegisteredFunction>): Record<string, RegisteredFunction>;
191
+ /**
192
+ * Every input the boot core takes. `bootProject` DERIVES its own options from this type (rather than
193
+ * re-declaring them) and forwards them with a spread — see `BootProjectOptions`'s doc for why that
194
+ * matters. Adding an option here therefore reaches `bootProject`'s public surface automatically; the
195
+ * only keys that don't are the two `bootProject` produces itself (`loaded`/`components`).
196
+ */
197
+ interface BootLoadedOptions {
198
+ loaded: LoadedProject;
199
+ components: ComponentDefinition[];
200
+ dataPath: string;
201
+ adminKey: string;
202
+ /** Postgres connection string; when unset, falls back to the zero-config SQLite file store. */
203
+ databaseUrl?: string;
204
+ /** File-storage backend overrides (CLI flags win over env, resolved via `resolveStorageConfig`). */
205
+ storage?: StorageConfig;
206
+ /**
207
+ * Override the pending-upload TTL (ms) the `ctx.storage` provider stamps on `expiresAt`. Unset →
208
+ * the provider default (1h). Exists so tests can force a short reap deadline; production leaves
209
+ * it at the default.
210
+ */
211
+ storageUploadTtlMs?: number;
212
+ /**
213
+ * Override the orphan-reaper's sweep interval (ms). Unset → the reaper default (60s). Same
214
+ * test-only motivation as `storageUploadTtlMs`.
215
+ */
216
+ storageReaperSweepMs?: number;
217
+ /**
218
+ * Tier 2 fleet wiring (from `@helipod/fleet`'s `prepareFleetNode`). When set, `store` is the
219
+ * pre-constructed (read-only-until-promoted) Postgres store — used INSTEAD of `makeStore` — and
220
+ * the runtime is built as a fleet node: writes route through `writeRouter` when this node isn't
221
+ * the writer, drivers are deferred until promotion (`deferDrivers`), and a promoted writer's
222
+ * commits fan out via `fanoutAdapter` (pg_notify). Absent for dev / non-fleet serve.
223
+ */
224
+ fleet?: {
225
+ store: DocStore;
226
+ writeRouter?: WriteRouter;
227
+ deferDrivers?: boolean;
228
+ fanoutAdapter?: EmbeddedWriteFanoutAdapter;
229
+ /** Shards B2a: shard count — >1 builds a ShardedTransactor (per-shard parallel commits). */
230
+ numShards?: number;
231
+ /** Fleet B3 hybrid (multi-writer): the replica-backed query store (queries route here; mutations
232
+ * commit to `store`). Threaded straight into `createEmbeddedRuntime`. */
233
+ queryStore?: DocStore;
234
+ /** Receipted Outbox (verdict §(c) placement): the authoritative receipts store the Connect handshake
235
+ * classifies/prunes against — the PRIMARY on a sync node (whose `store` is the receipt-less replica),
236
+ * so the handshake never spuriously resets. Threaded straight into `createEmbeddedRuntime`. */
237
+ receiptsStore?: DocStore;
238
+ /** Fleet B3 hybrid RYOW: awaited in the runtime fan-out drain before a local commit's re-runs. */
239
+ beforeNotify?: (commitTs: bigint) => Promise<void>;
240
+ /** Fleet B4: group commit — resolved by `@helipod/fleet`'s `node.ts` from its OWN
241
+ * `HELIPOD_GROUP_COMMIT` read (mirrors how `numShards` is resolved fleet-side, before
242
+ * `bootLoaded` runs) and threaded straight into `createEmbeddedRuntime`. Unset → `false`. */
243
+ groupCommit?: boolean;
244
+ /** Triggers D1: the stable-prefix accessor for `DriverContext.readLog` (`min(shard_leases.frontier_ts)`
245
+ * in a fleet). Threaded straight into `createEmbeddedRuntime`; absent outside a fleet. */
246
+ stablePrefix?: () => Promise<bigint | null>;
247
+ /** Receipted Outbox: fleet owns the `clientReceiptsGuard()` registration on the concrete Postgres
248
+ * store (in `armWriter`, before the fence) — so `createEmbeddedRuntime` must SKIP its own, which
249
+ * would land on a sync node's `SwitchableDocStore` and vanish on the promotion swapTo. Threaded
250
+ * straight into `createEmbeddedRuntime`; absent outside a fleet (the runtime owns it there). */
251
+ externalReceiptsGuard?: boolean;
252
+ };
253
+ /**
254
+ * Tier 3 Slice 6: object-storage substrate writer node. When set, `store` (at the `opts.fleet?.
255
+ * store ?? … ?? makeStore(...)` seam below) becomes an `ObjectStoreDocStore` over this URL's
256
+ * bucket (shard "0") instead of the usual SQLite/Postgres store — see `buildObjectStoreWriterNode`'s
257
+ * doc comment. Mutually exclusive with `fleet` (Tier 2 and Tier 3 are alternative write-scaling
258
+ * stories); combining both throws.
259
+ */
260
+ objectStoreUrl?: string;
261
+ /** Called once, synchronously, the moment the lease-heartbeat driver detects this node has been
262
+ * fenced (lost the shard-0 lease to a challenger). `serve.ts` wires this to trigger graceful
263
+ * shutdown. Ignored when `objectStoreUrl` is unset. */
264
+ objectStoreOnFenced?: (e: Error) => void;
265
+ /** Test-only overrides (mirrors `storageUploadTtlMs`'s pattern) — unset → the production defaults
266
+ * (`DEFAULT_OBJECTSTORE_LEASE_TTL_MS`/`DEFAULT_OBJECTSTORE_HEARTBEAT_MS`/`leaseTtlMs + 5000`). */
267
+ objectStoreLeaseTtlMs?: number;
268
+ objectStoreHeartbeatMs?: number;
269
+ objectStoreAcquireTimeoutMs?: number;
270
+ objectStoreAcquirePollIntervalMs?: number;
271
+ /** Test-only: force a deterministic writer id instead of a fresh `randomUUID()` per boot. */
272
+ objectStoreWriterId?: string;
273
+ /** Tier 3 Slice 7, Task 7.3: the gc-driver's sweep cadence (ms). Unset → `DEFAULT_OBJECTSTORE_GC_MS`
274
+ * (~60s). `serve.ts` threads `HELIPOD_OBJECTSTORE_GC_MS` in here; tests can also set it directly
275
+ * to force an observable reclamation on a short timescale. Ignored when `objectStoreUrl` is unset. */
276
+ objectStoreGcMs?: number;
277
+ /** Tier 3 multi-shard single-node serve: the number of object-storage lanes a `--object-store`
278
+ * WRITER node owns (`serve.ts` threads `--shards`/`HELIPOD_FLEET_SHARDS` in here). Unset / `1`
279
+ * → the shipped single-shard path (shard "0"), byte-identical. `> 1` → this node opens+acquires
280
+ * all `shardIdList(N)` lanes and composes a `ShardedObjectStoreDocStore`; the runtime's
281
+ * `numShards` is sized to N so the `ShardedTransactor` routes writes to the owning lane. Ignored
282
+ * for a `--replica` boot (a replica reads its lane count from the bucket's `globals.numShards`, not
283
+ * this flag — it has no `--shards` of its own) and when `objectStoreUrl` is unset. */
284
+ objectStoreShards?: number;
285
+ /** Tier 3 Slice 8, Task 8.2: boot this node as a READ-ONLY REPLICA of `objectStoreUrl`'s shard
286
+ * instead of a writer — materializes + tails, NEVER acquires the write lease (every mutation is
287
+ * rejected with `REPLICA_WRITE_REJECTED_MESSAGE`), and runs no heartbeat/gc drivers. Requires
288
+ * `objectStoreUrl` (throws if set without it — `serve.ts` also validates this at the CLI-flag
289
+ * level, before ever reaching here). Ignored (no-op) when `objectStoreUrl` is unset. */
290
+ replica?: boolean;
291
+ /** Test-only: force a deterministic consumer-watermark id for the replica's tailer. Ignored
292
+ * unless `replica` is set. See `buildObjectStoreReplicaNode`'s `consumerId`. */
293
+ objectStoreReplicaConsumerId?: string;
294
+ /** Test-only: shorten the replica's reactive-tailer poll cadence so a materialize-and-fan-out
295
+ * round is observable within a test's timescale. Unset → `DEFAULT_OBJECTSTORE_REPLICA_POLL_MS`
296
+ * (1000ms). Ignored unless `replica` is set. */
297
+ objectStoreReplicaPollMs?: number;
298
+ /**
299
+ * Tier 3 Slice 8 follow-on: the writer node's URL, from `--writer-url`/`HELIPOD_WRITER_URL`.
300
+ * When set on a `replica` boot, every mutation/action is FORWARDED here (`ReplicaWriteForwarder`,
301
+ * wired in as `createEmbeddedRuntime`'s `writeRouter`) instead of being rejected locally.
302
+ * Ignored unless `replica` is also set. Unset (the default) → today's unchanged reject-with-
303
+ * `REPLICA_WRITE_REJECTED_MESSAGE` behavior.
304
+ */
305
+ writerUrl?: string;
306
+ /**
307
+ * The wake seam: the host's single alarm, for a host that STOPS THE PROCESS between requests (so
308
+ * `setTimeout` never fires and every driver silently goes dead). `serve.ts` builds this from
309
+ * `--wake-url`/`HELIPOD_WAKE_URL` (`httpWakeHost`); the runtime then multiplexes every driver
310
+ * timer down to one arm. Threaded straight into `createEmbeddedRuntime`. Unset (every existing
311
+ * deployment) → plain `setTimeout`, byte-for-byte unchanged.
312
+ */
313
+ wakeHost?: WakeHost;
314
+ /**
315
+ * The wake seam's other half: answers `DriverContext.backstopMs`, the floor/stretch applied to a
316
+ * driver's BACKSTOP poll cadence only (never a next-work wake). `serve.ts` builds this from
317
+ * `--backstop-min-ms`/`HELIPOD_BACKSTOP_MIN_MS`. Threaded straight into `createEmbeddedRuntime`.
318
+ * Unset → identity (the drivers' own 30s/60s), byte-for-byte unchanged.
319
+ */
320
+ backstopMs?: (defaultMs: number) => number;
321
+ }
322
+ /**
323
+ * `bootProject`'s options: everything `bootLoaded` takes, MINUS the two things `bootProject` exists
324
+ * to produce itself, PLUS the `functionsDir` it produces them from. Derived deliberately — never
325
+ * re-declared.
326
+ *
327
+ * WHY (a trap this has already cost a full debug cycle — do not re-introduce it): `bootProject` used to
328
+ * re-declare its own option type and forward every key BY HAND into a fresh object literal. That
329
+ * shape drops options SILENTLY. TypeScript's excess-property check does not apply through a spread,
330
+ * and every caller passes optional options via conditional spread
331
+ * (`...(x !== undefined ? { k: v } : {})`, see `serve.ts`) — so a caller could hand `bootProject` a
332
+ * key its type never declared, get ZERO diagnostics, and have the value vanish before it ever
333
+ * reached `createEmbeddedRuntime`. That is exactly how the wake seam shipped dead: `serve.ts` built
334
+ * a valid `httpWakeHost(--wake-url)`, `bootProject` dropped it, the runtime silently took the
335
+ * `setTimeout` branch, and scheduled work never fired on a host that stops the process between
336
+ * requests — with the whole suite green, because nothing type-checked or tested the seam BETWEEN
337
+ * the two functions.
338
+ *
339
+ * Deriving from `BootLoadedOptions` + forwarding with a spread makes the drop structurally
340
+ * impossible rather than merely caught: a new `bootLoaded` option is part of this type, and is
341
+ * forwarded, the moment it is declared. The `Omit` is the ONLY exclusion list, and it is deliberate:
342
+ * `loaded`/`components` are `bootProject`'s own outputs (`loadFunctionsDir`/`loadConfig`), so
343
+ * accepting them from a caller would be meaningless. Anything else belongs here automatically.
344
+ */
345
+ type BootProjectOptions = Omit<BootLoadedOptions, "loaded" | "components"> & {
346
+ functionsDir: string;
347
+ };
348
+ declare function bootProject(opts: BootProjectOptions): Promise<BootResult>;
349
+
350
+ interface BinaryOptions {
351
+ port: number;
352
+ ip: string;
353
+ dataDir: string;
354
+ adminKey: string;
355
+ /** Postgres connection string (flag wins over `HELIPOD_DATABASE_URL`); unset → SQLite. */
356
+ databaseUrl?: string;
357
+ }
358
+ declare function resolveBinaryOptions(argv: string[], env: Record<string, string | undefined>): BinaryOptions;
359
+ declare function startBinaryServer(loaded: LoadedProject, components: ComponentDefinition[], opts: BinaryOptions, dashboard?: Record<string, string>): Promise<{
360
+ server: DevServer;
361
+ store: DocStore;
362
+ runtime: EmbeddedRuntime;
363
+ }>;
364
+ declare function runBinaryServer(loaded: LoadedProject, components: ComponentDefinition[], dashboard?: Record<string, string>): Promise<void>;
365
+
366
+ export { type BootLoadedOptions, type BootProjectOptions, type BootResult, DEFAULT_FUNCTIONS_DIR, type DevOptions, type DevServer, type DevServerOptions, LoadedProject, ProcessRuntimeHost, ProjectArtifacts, type PushResult, type ResolvedDevOptions, type ResolvedFunctionsDir, type RuntimeKind, type WatchLoop, type WatchLoopOptions, type WatchTriggerReason, bootProject, codegenCommand, createWatchLoop, detectRuntime, devCommand, functionsDirNotFoundMessage, loadConfig, loadFunctionsDir, push, resolveBinaryOptions, resolveDevOptions, resolveFunctionsDir, runBinaryServer, runCli, startBinaryServer, startDevServer, withStorageModules };
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ import {
2
+ DEFAULT_FUNCTIONS_DIR,
3
+ ProcessRuntimeHost,
4
+ bootLoaded,
5
+ bootProject,
6
+ codegenCommand,
7
+ createWatchLoop,
8
+ detectRuntime,
9
+ devCommand,
10
+ functionsDirNotFoundMessage,
11
+ loadConfig,
12
+ loadFunctionsDir,
13
+ push,
14
+ resolveDevOptions,
15
+ resolveFunctionsDir,
16
+ runCli,
17
+ startDevServer,
18
+ withStorageModules
19
+ } from "./chunk-IBFZ2HUR.js";
20
+ import {
21
+ handleHttpRequest
22
+ } from "./chunk-N5J276OX.js";
23
+ import {
24
+ DEFAULT_INDEX,
25
+ loadProject
26
+ } from "./chunk-EPHSHGFU.js";
27
+
28
+ // src/index.ts
29
+ import { writeGenerated } from "@helipod/codegen";
30
+
31
+ // src/binary-main.ts
32
+ import { join } from "path";
33
+ function resolveBinaryOptions(argv, env) {
34
+ let port = env.PORT ? Number(env.PORT) : 3e3;
35
+ let ip = "0.0.0.0";
36
+ let dataDir = "./data";
37
+ let databaseUrl = env.HELIPOD_DATABASE_URL;
38
+ const adminKey = (env.HELIPOD_ADMIN_KEY ?? "").trim();
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === "--port" && argv[i + 1] !== void 0) port = Number(argv[++i]);
42
+ else if (a === "--hostname" && argv[i + 1] !== void 0) ip = argv[++i];
43
+ else if (a === "--data-dir" && argv[i + 1] !== void 0) dataDir = argv[++i];
44
+ else if (a === "--database-url" && argv[i + 1] !== void 0) databaseUrl = argv[++i];
45
+ }
46
+ return { port, ip, dataDir, adminKey, databaseUrl };
47
+ }
48
+ async function startBinaryServer(loaded, components, opts, dashboard) {
49
+ const boot = await bootLoaded({
50
+ loaded,
51
+ components,
52
+ dataPath: join(opts.dataDir, "db.sqlite"),
53
+ adminKey: opts.adminKey,
54
+ databaseUrl: opts.databaseUrl
55
+ });
56
+ let dash;
57
+ if (dashboard) {
58
+ const bun = globalThis.Bun;
59
+ if (!bun) throw new Error("Bun runtime not available to read the embedded dashboard");
60
+ const indexPath = dashboard["/"];
61
+ if (!indexPath) throw new Error('embedded dashboard map is missing its "/" (index.html) entry');
62
+ const html = await bun.file(indexPath).text();
63
+ dash = { html, assets: dashboard };
64
+ }
65
+ const server = await new ProcessRuntimeHost().serve(boot.runtime, {
66
+ port: opts.port,
67
+ ip: opts.ip,
68
+ admin: { api: boot.adminApi, key: opts.adminKey },
69
+ routes: boot.project.routes,
70
+ storageRoutes: boot.storageRoutes,
71
+ componentRoutes: boot.componentRoutes,
72
+ dashboard: dash
73
+ });
74
+ return { server, store: boot.store, runtime: boot.runtime };
75
+ }
76
+ async function runBinaryServer(loaded, components, dashboard) {
77
+ const opts = resolveBinaryOptions(process.argv.slice(2), process.env);
78
+ if (!opts.adminKey) {
79
+ process.stderr.write("\u2717 HELIPOD_ADMIN_KEY is required \u2014 set it to a strong secret.\n");
80
+ process.exit(1);
81
+ }
82
+ const { server, store } = await startBinaryServer(loaded, components, opts, dashboard);
83
+ process.stdout.write(JSON.stringify({ ready: true, port: opts.port, url: `http://${opts.ip}:${opts.port}` }) + "\n");
84
+ let closing = false;
85
+ const shutdown = async () => {
86
+ if (closing) return;
87
+ closing = true;
88
+ await server.close();
89
+ await store.close();
90
+ process.exit(0);
91
+ };
92
+ process.on("SIGTERM", () => void shutdown());
93
+ process.on("SIGINT", () => void shutdown());
94
+ }
95
+ export {
96
+ DEFAULT_FUNCTIONS_DIR,
97
+ DEFAULT_INDEX,
98
+ ProcessRuntimeHost,
99
+ bootProject,
100
+ codegenCommand,
101
+ createWatchLoop,
102
+ detectRuntime,
103
+ devCommand,
104
+ functionsDirNotFoundMessage,
105
+ handleHttpRequest,
106
+ loadConfig,
107
+ loadFunctionsDir,
108
+ loadProject,
109
+ push,
110
+ resolveBinaryOptions,
111
+ resolveDevOptions,
112
+ resolveFunctionsDir,
113
+ runBinaryServer,
114
+ runCli,
115
+ startBinaryServer,
116
+ startDevServer,
117
+ withStorageModules,
118
+ writeGenerated
119
+ };
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/binary-main.ts"],"sourcesContent":["/**\n * `@helipod/cli` — the `helipod` dev tooling: project loading, the push pipeline (load →\n * codegen → register), the HTTP dev server, hot-reload watch loop, and the command dispatch.\n */\nexport type { LoadedProject, ProjectArtifacts } from \"./project\";\nexport { loadProject, DEFAULT_INDEX } from \"./project\";\n\nexport type { PushResult } from \"./push-pipeline\";\nexport { push } from \"./push-pipeline\";\n\nexport type { DevOptions, ResolvedDevOptions, RuntimeKind } from \"./dev-options\";\nexport { resolveDevOptions, detectRuntime } from \"./dev-options\";\n\nexport type { HttpRequest, HttpResponse, ServerInfo } from \"./http-handler\";\nexport { handleHttpRequest } from \"./http-handler\";\n\nexport type { DevServer, DevServerOptions } from \"./server\";\nexport { startDevServer, ProcessRuntimeHost } from \"./server\";\n\nexport type { WatchLoop, WatchLoopOptions, WatchTriggerReason } from \"./watch\";\nexport { createWatchLoop } from \"./watch\";\n\nexport { loadFunctionsDir } from \"./load-modules\";\nexport type { HelipodConfig } from \"@helipod/component\";\nexport { loadConfig } from \"./load-config\";\nexport type { ResolvedFunctionsDir } from \"./functions-dir\";\nexport { resolveFunctionsDir, functionsDirNotFoundMessage, DEFAULT_FUNCTIONS_DIR } from \"./functions-dir\";\nexport { runCli, devCommand, codegenCommand } from \"./cli\";\n\n// The shared boot core + codegen writer, re-exported so an out-of-CLI host (e.g. `@helipod/vite`'s\n// in-process embed mode) can boot the engine and write `_generated` through the package boundary\n// rather than reaching into deep source paths. All already-existing internals — purely additive.\nexport { bootProject, withStorageModules } from \"./boot\";\nexport type { BootResult, BootProjectOptions, BootLoadedOptions } from \"./boot\";\nexport { writeGenerated } from \"@helipod/codegen\";\n\nexport { runBinaryServer, startBinaryServer, resolveBinaryOptions } from \"./binary-main\";\n","/**\n * The runtime entry a `helipod build` binary calls. It is compiled `serve`: boot an already-loaded\n * project (static imports, not a dir scan), start the shared server, print a machine-readable ready\n * line, and shut down gracefully. `startBinaryServer` is the testable core (no signals/exit).\n */\nimport { join } from \"node:path\";\nimport type { ComponentDefinition } from \"@helipod/component\";\nimport type { EmbeddedRuntime } from \"@helipod/runtime-embedded\";\nimport type { DocStore } from \"@helipod/docstore\";\nimport type { LoadedProject } from \"./project\";\nimport { bootLoaded } from \"./boot\";\nimport { ProcessRuntimeHost, type DevServer } from \"./server\";\n\nexport interface BinaryOptions {\n port: number;\n ip: string;\n dataDir: string;\n adminKey: string;\n /** Postgres connection string (flag wins over `HELIPOD_DATABASE_URL`); unset → SQLite. */\n databaseUrl?: string;\n}\n\n/** The materialized embedded dashboard: key-injected HTML plus the urlPath→embedded-path asset map. */\nexport interface EmbeddedDashboard { html: string; assets: Record<string, string> }\n\n/** Minimal structural surface of `Bun.file(path)`, reached only inside a compiled binary. */\ninterface BunFileLike {\n text(): Promise<string>;\n}\ninterface BunFileRuntime {\n file(path: string): BunFileLike;\n}\n\nexport function resolveBinaryOptions(argv: string[], env: Record<string, string | undefined>): BinaryOptions {\n let port = env.PORT ? Number(env.PORT) : 3000;\n let ip = \"0.0.0.0\";\n let dataDir = \"./data\";\n let databaseUrl = env.HELIPOD_DATABASE_URL;\n const adminKey = (env.HELIPOD_ADMIN_KEY ?? \"\").trim();\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === \"--port\" && argv[i + 1] !== undefined) port = Number(argv[++i]);\n else if (a === \"--hostname\" && argv[i + 1] !== undefined) ip = argv[++i] as string;\n else if (a === \"--data-dir\" && argv[i + 1] !== undefined) dataDir = argv[++i] as string;\n else if (a === \"--database-url\" && argv[i + 1] !== undefined) databaseUrl = argv[++i] as string;\n }\n return { port, ip, dataDir, adminKey, databaseUrl };\n}\n\nexport async function startBinaryServer(\n loaded: LoadedProject,\n components: ComponentDefinition[],\n opts: BinaryOptions,\n dashboard?: Record<string, string>,\n): Promise<{ server: DevServer; store: DocStore; runtime: EmbeddedRuntime }> {\n const boot = await bootLoaded({\n loaded,\n components,\n dataPath: join(opts.dataDir, \"db.sqlite\"),\n adminKey: opts.adminKey,\n databaseUrl: opts.databaseUrl,\n });\n let dash: EmbeddedDashboard | undefined;\n if (dashboard) {\n const bun = (globalThis as { Bun?: BunFileRuntime }).Bun;\n if (!bun) throw new Error(\"Bun runtime not available to read the embedded dashboard\");\n const indexPath = dashboard[\"/\"];\n if (!indexPath) throw new Error(\"embedded dashboard map is missing its \\\"/\\\" (index.html) entry\");\n const html = await bun.file(indexPath).text();\n dash = { html, assets: dashboard };\n }\n const server = await new ProcessRuntimeHost().serve(boot.runtime, {\n port: opts.port,\n ip: opts.ip,\n admin: { api: boot.adminApi, key: opts.adminKey },\n routes: boot.project.routes,\n storageRoutes: boot.storageRoutes,\n componentRoutes: boot.componentRoutes,\n dashboard: dash,\n });\n return { server, store: boot.store, runtime: boot.runtime };\n}\n\nexport async function runBinaryServer(\n loaded: LoadedProject,\n components: ComponentDefinition[],\n dashboard?: Record<string, string>,\n): Promise<void> {\n const opts = resolveBinaryOptions(process.argv.slice(2), process.env);\n if (!opts.adminKey) {\n process.stderr.write(\"✗ HELIPOD_ADMIN_KEY is required — set it to a strong secret.\\n\");\n process.exit(1);\n }\n const { server, store } = await startBinaryServer(loaded, components, opts, dashboard);\n process.stdout.write(JSON.stringify({ ready: true, port: opts.port, url: `http://${opts.ip}:${opts.port}` }) + \"\\n\");\n let closing = false;\n const shutdown = async (): Promise<void> => {\n if (closing) return;\n closing = true;\n await server.close();\n await store.close();\n process.exit(0);\n };\n process.on(\"SIGTERM\", () => void shutdown());\n process.on(\"SIGINT\", () => void shutdown());\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,sBAAsB;;;AC7B/B,SAAS,YAAY;AA4Bd,SAAS,qBAAqB,MAAgB,KAAwD;AAC3G,MAAI,OAAO,IAAI,OAAO,OAAO,IAAI,IAAI,IAAI;AACzC,MAAI,KAAK;AACT,MAAI,UAAU;AACd,MAAI,cAAc,IAAI;AACtB,QAAM,YAAY,IAAI,qBAAqB,IAAI,KAAK;AACpD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,YAAY,KAAK,IAAI,CAAC,MAAM,OAAW,QAAO,OAAO,KAAK,EAAE,CAAC,CAAC;AAAA,aAC/D,MAAM,gBAAgB,KAAK,IAAI,CAAC,MAAM,OAAW,MAAK,KAAK,EAAE,CAAC;AAAA,aAC9D,MAAM,gBAAgB,KAAK,IAAI,CAAC,MAAM,OAAW,WAAU,KAAK,EAAE,CAAC;AAAA,aACnE,MAAM,oBAAoB,KAAK,IAAI,CAAC,MAAM,OAAW,eAAc,KAAK,EAAE,CAAC;AAAA,EACtF;AACA,SAAO,EAAE,MAAM,IAAI,SAAS,UAAU,YAAY;AACpD;AAEA,eAAsB,kBACpB,QACA,YACA,MACA,WAC2E;AAC3E,QAAM,OAAO,MAAM,WAAW;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,UAAU,KAAK,KAAK,SAAS,WAAW;AAAA,IACxC,UAAU,KAAK;AAAA,IACf,aAAa,KAAK;AAAA,EACpB,CAAC;AACD,MAAI;AACJ,MAAI,WAAW;AACb,UAAM,MAAO,WAAwC;AACrD,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,0DAA0D;AACpF,UAAM,YAAY,UAAU,GAAG;AAC/B,QAAI,CAAC,UAAW,OAAM,IAAI,MAAM,8DAAgE;AAChG,UAAM,OAAO,MAAM,IAAI,KAAK,SAAS,EAAE,KAAK;AAC5C,WAAO,EAAE,MAAM,QAAQ,UAAU;AAAA,EACnC;AACA,QAAM,SAAS,MAAM,IAAI,mBAAmB,EAAE,MAAM,KAAK,SAAS;AAAA,IAChE,MAAM,KAAK;AAAA,IACX,IAAI,KAAK;AAAA,IACT,OAAO,EAAE,KAAK,KAAK,UAAU,KAAK,KAAK,SAAS;AAAA,IAChD,QAAQ,KAAK,QAAQ;AAAA,IACrB,eAAe,KAAK;AAAA,IACpB,iBAAiB,KAAK;AAAA,IACtB,WAAW;AAAA,EACb,CAAC;AACD,SAAO,EAAE,QAAQ,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAC5D;AAEA,eAAsB,gBACpB,QACA,YACA,WACe;AACf,QAAM,OAAO,qBAAqB,QAAQ,KAAK,MAAM,CAAC,GAAG,QAAQ,GAAG;AACpE,MAAI,CAAC,KAAK,UAAU;AAClB,YAAQ,OAAO,MAAM,0EAAgE;AACrF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM,kBAAkB,QAAQ,YAAY,MAAM,SAAS;AACrF,UAAQ,OAAO,MAAM,KAAK,UAAU,EAAE,OAAO,MAAM,MAAM,KAAK,MAAM,KAAK,UAAU,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG,CAAC,IAAI,IAAI;AACnH,MAAI,UAAU;AACd,QAAM,WAAW,YAA2B;AAC1C,QAAI,QAAS;AACb,cAAU;AACV,UAAM,OAAO,MAAM;AACnB,UAAM,MAAM,MAAM;AAClB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,UAAQ,GAAG,WAAW,MAAM,KAAK,SAAS,CAAC;AAC3C,UAAQ,GAAG,UAAU,MAAM,KAAK,SAAS,CAAC;AAC5C;","names":[]}
@@ -0,0 +1,53 @@
1
+ import { SimpleIndexCatalog, RegisteredFunction, ContextProvider } from '@helipod/executor';
2
+ import { SchemaDefinition, SchemaDefinitionJSON } from '@helipod/values';
3
+ import { AnalyzedFunctionManifest } from '@helipod/codegen';
4
+ import { BootContext, Driver, ResolvedComponentRoute, ComponentDefinition } from '@helipod/component';
5
+
6
+ /**
7
+ * Turn a loaded project (a live schema + function modules) into the artifacts the engine and
8
+ * codegen need: the schema JSON, an index catalog (with table numbers assigned + an implicit
9
+ * `by_creation` index per table), the `path:name → function` map, and the analyzed manifest.
10
+ */
11
+
12
+ declare const DEFAULT_INDEX = "by_creation";
13
+ interface LoadedProject {
14
+ schema: SchemaDefinition;
15
+ /** module path (without extension) → its exports (name → value). */
16
+ modules: Record<string, Record<string, unknown>>;
17
+ }
18
+ interface ProjectArtifacts {
19
+ schemaJson: SchemaDefinitionJSON;
20
+ catalog: SimpleIndexCatalog;
21
+ moduleMap: Record<string, RegisteredFunction>;
22
+ manifest: AnalyzedFunctionManifest;
23
+ tableNumbers: Record<string, number>;
24
+ componentNames: ReadonlySet<string>;
25
+ contextProviders: ContextProvider[];
26
+ /** Component boot steps (e.g. the scheduler's cron reconciler) — must run once at engine create. */
27
+ bootSteps: {
28
+ name: string;
29
+ run: (ctx: BootContext) => Promise<void>;
30
+ }[];
31
+ /**
32
+ * Component drivers (e.g. the scheduler's event loop) — must be started at engine create for a
33
+ * composed component's background work to actually run. Omitting this from
34
+ * `createEmbeddedRuntime(...)` silently leaves drivers never started (jobs enqueue but never
35
+ * dispatch) — see `../test/scheduler-e2e.test.ts` for the proof this wiring matters.
36
+ */
37
+ drivers: Driver[];
38
+ /** The app's `http.ts` router, resolved to `path:name` function paths for dispatch. */
39
+ routes: ResolvedRoute[];
40
+ /** Reserved engine routes contributed by composed components (e.g. auth's `/api/auth/oauth/*`). */
41
+ componentRoutes: ResolvedComponentRoute[];
42
+ }
43
+ /** A single `http.ts` route, with its handler resolved from a `RegisteredFunction` value to the
44
+ * `path:name` string that `runtime.runHttpAction` looks up in `composed.moduleMap`. */
45
+ interface ResolvedRoute {
46
+ method: string;
47
+ path?: string;
48
+ pathPrefix?: string;
49
+ handlerPath: string;
50
+ }
51
+ declare function loadProject(loaded: LoadedProject, components?: ComponentDefinition[], existingTableNumbers?: Record<string, number>): ProjectArtifacts;
52
+
53
+ export { DEFAULT_INDEX, type LoadedProject, type ProjectArtifacts, type ResolvedRoute, loadProject };
@@ -0,0 +1,9 @@
1
+ import {
2
+ DEFAULT_INDEX,
3
+ loadProject
4
+ } from "./chunk-EPHSHGFU.js";
5
+ export {
6
+ DEFAULT_INDEX,
7
+ loadProject
8
+ };
9
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@helipod/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "FSL-1.1-Apache-2.0",
6
+ "bin": {
7
+ "helipod": "./dist/bin.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./http-handler": {
18
+ "types": "./dist/http-handler.d.ts",
19
+ "default": "./dist/http-handler.js"
20
+ },
21
+ "./project": {
22
+ "types": "./dist/project.d.ts",
23
+ "default": "./dist/project.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "vitest run --exclude 'test/*-e2e.test.ts'",
32
+ "test:e2e": "vitest run test/*-e2e.test.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "clean": "rm -rf dist .turbo"
35
+ },
36
+ "dependencies": {
37
+ "@helipod/admin": "0.1.0",
38
+ "@helipod/blobstore": "0.1.0",
39
+ "@helipod/blobstore-fs": "0.1.0",
40
+ "@helipod/blobstore-s3": "0.1.0",
41
+ "@helipod/codegen": "0.1.0",
42
+ "@helipod/component": "0.1.0",
43
+ "@helipod/deploy": "0.1.0",
44
+ "@helipod/docstore": "0.1.0",
45
+ "@helipod/docstore-postgres": "0.1.0",
46
+ "@helipod/docstore-sqlite": "0.1.0",
47
+ "@helipod/errors": "0.1.0",
48
+ "@helipod/executor": "0.1.0",
49
+ "@helipod/id-codec": "0.1.0",
50
+ "@helipod/objectstore": "0.1.0",
51
+ "@helipod/objectstore-fs": "0.1.0",
52
+ "@helipod/objectstore-s3": "0.1.0",
53
+ "@helipod/query-engine": "0.1.0",
54
+ "@helipod/receipts": "0.1.0",
55
+ "@helipod/runtime-embedded": "0.1.0",
56
+ "@helipod/storage": "0.1.0",
57
+ "@helipod/sync": "0.1.0",
58
+ "@helipod/values": "0.1.0",
59
+ "esbuild": "^0.27.0",
60
+ "ws": "^8.18.0",
61
+ "@helipod/dashboard": "0.1.0"
62
+ },
63
+ "devDependencies": {
64
+ "@electric-sql/pglite": "^0.2.17",
65
+ "@levischuck/tiny-cbor": "0.2.11",
66
+ "@simplewebauthn/server": "13.3.2",
67
+ "@helipod/auth": "0.1.0",
68
+ "@helipod/client": "0.1.0",
69
+ "@helipod/fleet": "0.0.0",
70
+ "@helipod/notifications": "0.1.0",
71
+ "@helipod/objectstore-substrate": "0.0.0",
72
+ "@helipod/scheduler": "0.1.0",
73
+ "@helipod/triggers": "0.1.0",
74
+ "@helipod/workflow": "0.1.0",
75
+ "@types/node": "^22.10.5",
76
+ "@types/ws": "^8.5.13",
77
+ "fake-indexeddb": "^6.0.0",
78
+ "jose": "6.2.3",
79
+ "tsup": "^8.3.5",
80
+ "typescript": "^5.7.2",
81
+ "vitest": "^2.1.8"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public"
85
+ },
86
+ "repository": {
87
+ "type": "git",
88
+ "url": "git+https://github.com/helipod-sh/helipod.git",
89
+ "directory": "packages/cli"
90
+ },
91
+ "homepage": "https://github.com/helipod-sh/helipod"
92
+ }