@ovrdev/cli 0.1.0-alpha.16

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,393 @@
1
+ // src/plugins/shell.ts
2
+ import { spawn } from "node:child_process";
3
+
4
+ // src/util/ready.ts
5
+ import { connect } from "node:net";
6
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+ async function waitUntil(probe, opts = {}) {
8
+ const timeout = opts.timeoutMs ?? 60000;
9
+ const interval = opts.intervalMs ?? 500;
10
+ const deadline = Date.now() + timeout;
11
+ for (;; ) {
12
+ try {
13
+ if (await probe())
14
+ return;
15
+ } catch {}
16
+ if (Date.now() >= deadline) {
17
+ throw new Error(`readiness timed out after ${timeout}ms${opts.label ? `: ${opts.label}` : ""}`);
18
+ }
19
+ await sleep(interval);
20
+ }
21
+ }
22
+ function waitHttp(url, opts = {}) {
23
+ return waitUntil(async () => {
24
+ const res = await fetch(url, { redirect: "manual" }).catch(() => null);
25
+ if (!res)
26
+ return false;
27
+ return opts.status ? res.status === opts.status : res.status < 500;
28
+ }, { label: `GET ${url}`, ...opts });
29
+ }
30
+ function waitTcp(port, opts = {}) {
31
+ const host = opts.host ?? "127.0.0.1";
32
+ return waitUntil(() => new Promise((resolve) => {
33
+ const sock = connect({ host, port });
34
+ const done = (ok) => {
35
+ sock.destroy();
36
+ resolve(ok);
37
+ };
38
+ sock.once("connect", () => done(true));
39
+ sock.once("error", () => done(false));
40
+ sock.setTimeout(2000, () => done(false));
41
+ }), { label: `tcp ${host}:${port}`, ...opts });
42
+ }
43
+ // src/util/random.ts
44
+ import { randomBytes } from "node:crypto";
45
+ var ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
46
+ var SYMBOLS = "!#$%*+-_=?@.";
47
+ function randomString(length = 24, alphabet = ALNUM) {
48
+ if (length <= 0 || alphabet.length === 0)
49
+ return "";
50
+ const max = Math.floor(256 / alphabet.length) * alphabet.length;
51
+ let out = "";
52
+ while (out.length < length) {
53
+ for (const byte of randomBytes(length * 2)) {
54
+ if (byte >= max)
55
+ continue;
56
+ out += alphabet[byte % alphabet.length];
57
+ if (out.length === length)
58
+ break;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function randomPassword(length = 24, opts = {}) {
64
+ return randomString(length, opts.symbols === false ? ALNUM : ALNUM + SYMBOLS);
65
+ }
66
+ function randomHex(bytes = 24) {
67
+ return randomBytes(bytes).toString("hex");
68
+ }
69
+
70
+ // src/core/core.ts
71
+ var REF = Symbol.for("ovr.ref");
72
+ var isRef = (v) => typeof v === "object" && v !== null && (REF in v);
73
+ var refTarget = (r) => r[REF];
74
+ var makeRef = (repo, service, key) => ({ [REF]: { repo, service, key } });
75
+ function collectRefTargets(spec, out = []) {
76
+ if (isRef(spec)) {
77
+ out.push(refTarget(spec));
78
+ return out;
79
+ }
80
+ if (Array.isArray(spec)) {
81
+ for (const v of spec)
82
+ collectRefTargets(v, out);
83
+ return out;
84
+ }
85
+ if (spec && typeof spec === "object") {
86
+ for (const v of Object.values(spec))
87
+ collectRefTargets(v, out);
88
+ }
89
+ return out;
90
+ }
91
+ function resolveRefs(spec, resolve) {
92
+ if (isRef(spec))
93
+ return resolve(refTarget(spec));
94
+ if (Array.isArray(spec))
95
+ return spec.map((v) => resolveRefs(v, resolve));
96
+ if (spec && typeof spec === "object") {
97
+ const out = {};
98
+ for (const [k, v] of Object.entries(spec)) {
99
+ if (isRef(v)) {
100
+ const r = resolve(refTarget(v));
101
+ if (r !== undefined)
102
+ out[k] = r;
103
+ } else {
104
+ out[k] = resolveRefs(v, resolve);
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+ return spec;
110
+ }
111
+ function coerceEnv(obj) {
112
+ const out = {};
113
+ for (const [k, v] of Object.entries(obj)) {
114
+ if (v === undefined || v === null)
115
+ continue;
116
+ out[k] = typeof v === "object" ? JSON.stringify(v) : String(v);
117
+ }
118
+ return out;
119
+ }
120
+ var sym = (p) => typeof p === "symbol";
121
+ function buildWorkspace(name, ownRepo, leaf) {
122
+ const serviceNode = (repo, s) => new Proxy({ [REF]: { repo, service: s, key: "" } }, {
123
+ get: (t, k) => {
124
+ if (k === REF)
125
+ return t[REF];
126
+ if (k === "announce")
127
+ return (payload) => ({ to: `${repo}.${s}`, payload });
128
+ return sym(k) ? undefined : leaf(repo, s, k);
129
+ }
130
+ });
131
+ const services = (repo) => new Proxy({}, { get: (_t, s) => sym(s) ? undefined : serviceNode(repo, s) });
132
+ const repos = new Proxy({}, { get: (_t, r) => sym(r) ? undefined : { services: services(r) } });
133
+ return { name, services: services(ownRepo), repos };
134
+ }
135
+ function makeContext(workspaceName, repo) {
136
+ return {
137
+ workspace: buildWorkspace(workspaceName, repo, (r, s, k) => makeRef(r, s, k)),
138
+ kind: buildKinds([])
139
+ };
140
+ }
141
+ function resolvingWorkspace(name, ownRepo, resolve) {
142
+ return buildWorkspace(name, ownRepo, (repo, service, key) => resolve({ repo, service, key }));
143
+ }
144
+ function recordingWorkspace(name, ownRepo) {
145
+ const rec = [];
146
+ const workspace = buildWorkspace(name, ownRepo, (repo, service, key) => {
147
+ rec.push({ repo, service, key });
148
+ return "";
149
+ });
150
+ return { workspace, taken: () => rec };
151
+ }
152
+ var input = {
153
+ text: (label, opts = {}) => ({
154
+ kind: "text",
155
+ label,
156
+ ...opts
157
+ }),
158
+ choice: (label, options, opts = {}) => ({
159
+ kind: "choice",
160
+ label,
161
+ options,
162
+ ...opts
163
+ }),
164
+ confirm: (label, opts = {}) => ({
165
+ kind: "confirm",
166
+ label,
167
+ ...opts
168
+ })
169
+ };
170
+ var ask = {
171
+ choice: (question, options, opts = {}) => ({
172
+ kind: "choice",
173
+ question,
174
+ options,
175
+ ...opts
176
+ }),
177
+ confirm: (question, opts = {}) => ({
178
+ kind: "confirm",
179
+ question,
180
+ ...opts
181
+ })
182
+ };
183
+ function applyAskDefaults(asks, answers, repo) {
184
+ const out = { ...answers };
185
+ for (const [key, spec] of Object.entries(asks)) {
186
+ if (out[key] !== undefined)
187
+ continue;
188
+ if (spec.default !== undefined) {
189
+ out[key] = spec.default;
190
+ continue;
191
+ }
192
+ const hint = spec.kind === "choice" ? spec.options.join("|") : "true|false";
193
+ throw new Error(`${repo}: '${key}' needs an answer — pass --answer ${key}=<${hint}> (or run interactively)`);
194
+ }
195
+ return out;
196
+ }
197
+ var FACTORY = Symbol.for("ovr.factory");
198
+ var HEAD = Symbol.for("ovr.head");
199
+ var configAsks = (mod) => isConfigModule(mod) ? mod[HEAD]?.asks ?? {} : {};
200
+ function buildKinds(plugins) {
201
+ const kind = { shell };
202
+ for (const p of plugins)
203
+ if (p.kinds)
204
+ kind[p.name] = p.kinds;
205
+ return kind;
206
+ }
207
+ function defineConfig(a, b) {
208
+ if (b) {
209
+ const head = a;
210
+ const asks = { ...head.asks ?? {} };
211
+ if (head.modes?.length) {
212
+ asks.mode = {
213
+ kind: "choice",
214
+ question: "How should services run?",
215
+ options: head.modes,
216
+ ...head.defaultMode !== undefined ? { default: head.defaultMode } : {}
217
+ };
218
+ }
219
+ return {
220
+ [HEAD]: { asks },
221
+ [FACTORY]: ({ workspace, repo, dir, answers }) => {
222
+ const plugins = typeof head.plugins === "function" ? head.plugins({ workspace: { name: workspace }, repo, dir }) : head.plugins ?? [];
223
+ const resolved = applyAskDefaults(asks, answers ?? {}, repo);
224
+ const { workspace: proj } = makeContext(workspace, repo);
225
+ const body = b({
226
+ workspace: proj,
227
+ kind: buildKinds(plugins),
228
+ mode: resolved.mode ?? "default",
229
+ answers: resolved
230
+ });
231
+ return { ...body, plugins: [...plugins] };
232
+ }
233
+ };
234
+ }
235
+ const input2 = a;
236
+ const factory = typeof input2 === "function" ? input2 : () => input2;
237
+ return { [FACTORY]: ({ workspace, repo }) => factory(makeContext(workspace, repo)) };
238
+ }
239
+ var isConfigModule = (v) => typeof v === "object" && v !== null && (FACTORY in v);
240
+ function evalConfig(mod, workspace, repo, dir = "", answers) {
241
+ if (isConfigModule(mod))
242
+ return mod[FACTORY]({ workspace, repo, dir, answers });
243
+ const isPlain = mod && typeof mod === "object" && (("services" in mod) || ("setup" in mod));
244
+ const factory = typeof mod === "function" ? mod : isPlain ? () => ({ services: {}, ...mod }) : null;
245
+ if (!factory)
246
+ throw new Error("config default export must be defineConfig(...) or { services: … }");
247
+ return factory(makeContext(workspace, repo));
248
+ }
249
+ var action = {
250
+ session: (spec) => ({ mode: "session", ...spec }),
251
+ fn: (spec) => ({ mode: "function", ...spec }),
252
+ handover: (spec) => ({ mode: "handover", ...spec })
253
+ };
254
+ async function serviceRefs(def, tools) {
255
+ return def.plugin.refs ? await def.plugin.refs(def.spec, tools) : collectRefTargets(def.spec);
256
+ }
257
+
258
+ // src/engine/localbin.ts
259
+ import { existsSync } from "node:fs";
260
+ import { delimiter, join } from "node:path";
261
+ var LOCAL_BIN_DIRS = [["node_modules", ".bin"]];
262
+ function withLocalBins(dir, env) {
263
+ const bins = LOCAL_BIN_DIRS.map((p) => join(dir, ...p)).filter(existsSync);
264
+ if (!bins.length)
265
+ return env;
266
+ return { ...env, PATH: [...bins, env.PATH ?? ""].join(delimiter) };
267
+ }
268
+
269
+ // src/plugins/shell.ts
270
+ var placeholderBag = () => new Proxy(() => {
271
+ return;
272
+ }, {
273
+ get: (_t, p) => p === Symbol.toPrimitive ? () => "" : placeholderBag(),
274
+ apply: () => placeholderBag()
275
+ });
276
+ function spawnArgs(cmd) {
277
+ if (Array.isArray(cmd))
278
+ return { command: cmd[0], args: cmd.slice(1), shell: false };
279
+ return { command: cmd, args: [], shell: true };
280
+ }
281
+ var shellPlugin = {
282
+ name: "shell",
283
+ status(handle) {
284
+ const pid = handle?.pid;
285
+ if (!pid)
286
+ return "down";
287
+ try {
288
+ process.kill(pid, 0);
289
+ return "up";
290
+ } catch (e) {
291
+ return e.code === "EPERM" ? "up" : "down";
292
+ }
293
+ },
294
+ stop(handle) {
295
+ const pid = handle?.pid;
296
+ if (!pid)
297
+ return;
298
+ const sig = (s) => {
299
+ try {
300
+ process.kill(-pid, s);
301
+ } catch {
302
+ try {
303
+ process.kill(pid, s);
304
+ } catch {}
305
+ }
306
+ };
307
+ sig("SIGTERM");
308
+ const t = setTimeout(() => sig("SIGKILL"), 3000);
309
+ t.unref?.();
310
+ },
311
+ refs(spec) {
312
+ const env = typeof spec.env === "function" ? spec.env(placeholderBag()) : spec.env ?? {};
313
+ return collectRefTargets(env);
314
+ },
315
+ async plan(spec, ctx) {
316
+ const bag = spec.prepare ? await spec.prepare({
317
+ workspace: ctx.workspace,
318
+ scope: ctx.scope,
319
+ ports: { alloc: ctx.allocPort },
320
+ secrets: { get: ctx.secret },
321
+ paths: { get: ctx.path },
322
+ ...ctx.caps
323
+ }) : undefined;
324
+ const exp = spec.exports?.(bag) ?? {};
325
+ const clean = {};
326
+ for (const [k, v] of Object.entries(exp)) {
327
+ if (v !== undefined && !JSON.stringify(v).includes(ctx.pending))
328
+ clean[k] = v;
329
+ }
330
+ return clean;
331
+ },
332
+ async start(spec, ctx) {
333
+ const bag = ctx.bag;
334
+ const cmd = typeof spec.command === "function" ? spec.command(bag) : spec.command;
335
+ const { command, args, shell: useShell } = spawnArgs(cmd);
336
+ const rawEnv = typeof spec.env === "function" ? spec.env(bag) : spec.env ?? {};
337
+ const ownEnv = coerceEnv(resolveRefs(rawEnv, ctx.resolve));
338
+ const env = withLocalBins(ctx.scope.dir, { ...ctx.env, ...ownEnv });
339
+ const child = spawn(command, args, {
340
+ cwd: ctx.scope.dir,
341
+ env,
342
+ shell: useShell,
343
+ detached: true,
344
+ stdio: ["ignore", "pipe", "pipe"]
345
+ });
346
+ ctx.pipe(child.stdout);
347
+ ctx.pipe(child.stderr);
348
+ let killTimer = null;
349
+ const killGroup = (sig) => {
350
+ try {
351
+ if (child.pid)
352
+ process.kill(-child.pid, sig);
353
+ } catch {
354
+ try {
355
+ child.kill(sig);
356
+ } catch {}
357
+ }
358
+ };
359
+ child.on("exit", () => {
360
+ if (killTimer)
361
+ clearTimeout(killTimer);
362
+ });
363
+ const exports = spec.exports?.(bag) ?? {};
364
+ ctx.log(`▶ ${Array.isArray(cmd) ? cmd.join(" ") : cmd}${exports.url ? ` (${exports.url})` : ""}`);
365
+ const exited = new Promise((resolve) => child.on("exit", () => resolve()));
366
+ const ready = (spec.ready ? spec.ready(bag) : Promise.resolve()).then(() => spec.onReady?.(bag));
367
+ const actions = [
368
+ {
369
+ id: "shell",
370
+ label: "shell here (service env)",
371
+ command: process.env.SHELL || "/bin/sh",
372
+ cwd: ctx.scope.dir,
373
+ env
374
+ }
375
+ ];
376
+ return {
377
+ exports,
378
+ info: exports.url != null ? String(exports.url) : exports.port != null ? `:${exports.port}` : undefined,
379
+ actions,
380
+ ready,
381
+ exited,
382
+ handle: { pid: child.pid ?? 0 },
383
+ stop: () => {
384
+ killGroup("SIGTERM");
385
+ killTimer = setTimeout(() => killGroup("SIGKILL"), 3000);
386
+ killTimer.unref?.();
387
+ }
388
+ };
389
+ }
390
+ };
391
+ var shell = (spec) => ({ plugin: shellPlugin, spec });
392
+
393
+ export { withLocalBins, shellPlugin, shell, waitUntil, waitHttp, waitTcp, randomString, randomPassword, randomHex, isRef, refTarget, collectRefTargets, resolveRefs, coerceEnv, makeContext, resolvingWorkspace, recordingWorkspace, input, ask, applyAskDefaults, configAsks, defineConfig, isConfigModule, evalConfig, action, serviceRefs };
@@ -0,0 +1,57 @@
1
+ import { type Announcement, type DiscoveryFn, type EnvMap, type JsonValue, type OvrPlugin, type Plugin, type PrepareCtx, type ReadyFn, type Ref, type ServiceAction, type ServiceDef } from "../core/core.js";
2
+ /** A command: a shell string (`sh -c`, so pipes/globs work) or an argv array (exec'd, no shell). */
3
+ export type Cmd = string | string[];
4
+ export type ShellServiceDef<P = void> = {
5
+ /** Service-scoped plugins — provided at this service's start, torn down with it. */
6
+ plugins?: readonly OvrPlugin[];
7
+ /** Reserve ports/resources before the command runs; its return is the bag passed below. */
8
+ prepare?: (ctx: never) => P | Promise<P>;
9
+ /** The command to run — value or a (pure) function of the bag. */
10
+ command: Cmd | ((bag: P) => Cmd);
11
+ /**
12
+ * This service's process env. Static (Refs + literals) or a (pure) function of the bag —
13
+ * the function form lets you inject prepare-derived values (a provisioned bucket name)
14
+ * alongside cross-service Refs (`workspace.repos[...].url`).
15
+ */
16
+ env?: EnvMap | ((bag: P) => EnvMap);
17
+ /** Named values this service publishes for others to reference — a function of the bag (any JSON value). */
18
+ exports?: (bag: P) => Record<string, JsonValue>;
19
+ /** Run after the service is healthy. */
20
+ onReady?: (bag: P) => void | Promise<void>;
21
+ /** Config-declared actions for this service's menu (cwd defaults to the repo dir). */
22
+ actions?: ServiceAction[];
23
+ /** Resolves when up (gates dependents); e.g. `({ port }) => waitHttp(...)`. Default: immediate. */
24
+ ready?: ReadyFn<P>;
25
+ /** Services that must be UP before this one starts — typed readiness refs (`x.ready`). */
26
+ waitFor?: readonly Ref[];
27
+ /** Announce payloads to other services: `({ url }) => [other.services.x.announce({ redirect: url })]`. */
28
+ announce?: (bag: P) => Announcement[];
29
+ /** Reconcile the live set of announcements targeting this service (e.g. rewrite an allowlist). */
30
+ discovery?: DiscoveryFn;
31
+ };
32
+ /**
33
+ * The `kind.shell` constructor, generic over the config's prepare context (base ctx +
34
+ * whatever the registered plugins contribute). Bag + service-level plugin types infer
35
+ * from the call.
36
+ */
37
+ export interface ShellKind<Ctx> {
38
+ <Bag = void, const SP extends readonly OvrPlugin[] = readonly []>(spec: {
39
+ plugins?: SP;
40
+ prepare?: (ctx: Ctx & import("../core/core.js").ContextOf<SP>) => Bag;
41
+ command: Cmd | ((bag: NoInfer<Awaited<Bag>>) => Cmd);
42
+ env?: EnvMap | ((bag: NoInfer<Awaited<Bag>>) => EnvMap);
43
+ exports?: (bag: NoInfer<Awaited<Bag>>) => Record<string, JsonValue>;
44
+ onReady?: (bag: NoInfer<Awaited<Bag>>) => void | Promise<void>;
45
+ actions?: ServiceAction[];
46
+ ready?: ReadyFn<NoInfer<Awaited<Bag>>>;
47
+ waitFor?: readonly Ref[];
48
+ announce?: (bag: NoInfer<Awaited<Bag>>) => Announcement[];
49
+ discovery?: DiscoveryFn;
50
+ }): ServiceDef;
51
+ }
52
+ export declare const shellPlugin: Plugin<ShellServiceDef<unknown>>;
53
+ /**
54
+ * Declare a plain process service. Canonically reached as `kind.shell` in the two-arg
55
+ * defineConfig (typed with your plugins' capabilities); this export is the plugin-less form.
56
+ */
57
+ export declare const shell: ShellKind<PrepareCtx>;
@@ -0,0 +1,8 @@
1
+ import {
2
+ shell,
3
+ shellPlugin
4
+ } from "../index-4m3gj5te.js";
5
+ export {
6
+ shellPlugin,
7
+ shell
8
+ };
@@ -0,0 +1,79 @@
1
+ import type { OvrPlugin, Plugin, ServiceAction, ServiceDef } from "../core/core.js";
2
+ export type ConfigPatch = {
3
+ projectId: string;
4
+ /** section → key → value ("" = top level). Existing keys rewritten, missing ones inserted. */
5
+ set?: Record<string, Record<string, string | number | boolean>>;
6
+ /** Entries merged (deduped) into [auth].additional_redirect_urls; block created if missing. */
7
+ redirectUrls?: string[];
8
+ /** Retarget `redirect_uri` localhost ports to this stack's api port (OAuth ignores it). */
9
+ apiPort?: number;
10
+ };
11
+ /**
12
+ * Patch a supabase config.toml TEXTUALLY — comments, ordering, and everything ovr doesn't
13
+ * own survive verbatim (the repo's file stays the source of truth for auth/OAuth/etc.).
14
+ * Section-aware line rewriting: keys are only touched inside their `[section]`.
15
+ */
16
+ export declare function patchSupabaseConfig(text: string, patch: ConfigPatch): string;
17
+ /** Starter config for repos without a supabase/config.toml (patched like any other). */
18
+ export declare const DEFAULT_CONFIG = "project_id = \"ovr\"\n\n[api]\nenabled = true\nport = 54321\n\n[db]\nport = 54322\nshadow_port = 54320\nmajor_version = 17\n\n[db.seed]\nenabled = false\n\n[realtime]\nenabled = true\n\n[storage]\nenabled = true\n\n[auth]\nenabled = true\nsite_url = \"http://localhost:3000\"\n\n[studio]\nenabled = false\n\n[inbucket]\nenabled = false\n\n[analytics]\nenabled = false\n";
19
+ /** Map `supabase status -o json` output to ovr exports (sb_* key names only). */
20
+ export declare function statusExports(s: Record<string, string>): Record<string, string>;
21
+ /**
22
+ * What a migration set needs from the stack. Baselines captured from a full instance
23
+ * reference storage tables (`storage.objects`) and realtime-owned roles — replay breaks
24
+ * if those services are disabled (learned the hard way; see module doc).
25
+ */
26
+ export declare function migrationNeeds(sql: string): {
27
+ storage: boolean;
28
+ realtime: boolean;
29
+ };
30
+ /** Toggleable stack members. Core services default ON; UI/observability default OFF. */
31
+ export type StackServices = Partial<Record<"realtime" | "storage" | "studio" | "inbucket" | "analytics", boolean>>;
32
+ export type SupabaseExportsCtx = {
33
+ /** Raw `supabase status -o json` variables. */
34
+ status: Record<string, string>;
35
+ ports: {
36
+ api: number;
37
+ db: number;
38
+ };
39
+ };
40
+ type StackSpec = {
41
+ services?: StackServices;
42
+ /**
43
+ * Pin ports instead of sticky allocation. Pin `api` when OAuth is in play — the
44
+ * provider's redirect-URI allowlist registers an exact port (e.g. :54321 for Azure).
45
+ */
46
+ ports?: {
47
+ api?: number;
48
+ db?: number;
49
+ };
50
+ /** Extra [auth].additional_redirect_urls entries (deduped into the repo's list). */
51
+ redirectUrls?: string[];
52
+ exports?: (ctx: SupabaseExportsCtx) => Record<string, string>;
53
+ onReady?: () => void | Promise<void>;
54
+ plugins?: readonly OvrPlugin[];
55
+ /** Config-declared actions for this service's menu (cwd defaults to the repo dir). */
56
+ actions?: ServiceAction[];
57
+ /** Extra readiness beyond `supabase start` (which already blocks until healthy). */
58
+ ready?: (ctx: SupabaseExportsCtx) => Promise<void>;
59
+ waitFor?: readonly import("../core/core.js").Ref[];
60
+ };
61
+ export declare const supabasePlugin: Plugin<StackSpec>;
62
+ /** The `kind.supabase.*` constructors. `stack` today; `functions`/`db` are natural later kinds. */
63
+ export interface SupabaseKinds<Ctx> {
64
+ /** The whole local stack (`supabase start`) from a generated shadow workdir. */
65
+ stack(opts?: StackSpec & {
66
+ prepare?: (ctx: Ctx) => unknown;
67
+ }): ServiceDef;
68
+ }
69
+ declare module "../core/core.js" {
70
+ interface KindRegistry<Ctx> {
71
+ supabase: SupabaseKinds<Ctx>;
72
+ }
73
+ }
74
+ declare const kinds: {
75
+ stack: (opts?: Record<string, unknown>) => ServiceDef;
76
+ };
77
+ /** Register `kind.supabase.stack` + environment diagnostics (docker daemon, CLI presence). */
78
+ export declare function supabase(): import("../core/core.js").KindsPlugin<"supabase", typeof kinds>;
79
+ export {};