@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.
- package/README.md +111 -0
- package/dist/cli/index.js +6153 -0
- package/dist/core/config.d.ts +138 -0
- package/dist/core/core.d.ts +748 -0
- package/dist/core/core.js +58 -0
- package/dist/engine/localbin.d.ts +2 -0
- package/dist/env/dotenv-plugin.d.ts +6 -0
- package/dist/index-34kn28k6.js +358 -0
- package/dist/index-3kvnzs8b.js +340 -0
- package/dist/index-4m3gj5te.js +638 -0
- package/dist/index-83n937pv.js +174 -0
- package/dist/index-85xjm9cd.js +349 -0
- package/dist/index-9wb3pkgv.js +384 -0
- package/dist/index-as2kwj3d.js +174 -0
- package/dist/index-bj59btcy.js +181 -0
- package/dist/index-dfy2jqk5.js +179 -0
- package/dist/index-grafpw62.js +340 -0
- package/dist/index-gxamezdw.js +341 -0
- package/dist/index-kdse4p7d.js +387 -0
- package/dist/index-mcgazrah.js +439 -0
- package/dist/index-pchhjv88.js +637 -0
- package/dist/index-rh33sm1y.js +370 -0
- package/dist/index-sqkwnp9b.js +618 -0
- package/dist/index-w64536he.js +431 -0
- package/dist/index-x4hj4et6.js +393 -0
- package/dist/plugins/shell.d.ts +57 -0
- package/dist/plugins/shell.js +8 -0
- package/dist/plugins/supabase.d.ts +79 -0
- package/dist/plugins/supabase.js +356 -0
- package/dist/util/dotenv.d.ts +2 -0
- package/dist/util/random.d.ts +11 -0
- package/dist/util/ready.d.ts +18 -0
- package/package.json +45 -0
- package/scripts/postinstall.mjs +59 -0
|
@@ -0,0 +1,340 @@
|
|
|
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
|
+
var sym = (p) => typeof p === "symbol";
|
|
112
|
+
function buildWorkspace(name, ownRepo, leaf) {
|
|
113
|
+
const services = (repo) => new Proxy({}, { get: (_t, s) => sym(s) ? undefined : new Proxy({}, { get: (_t2, k) => sym(k) ? undefined : leaf(repo, s, k) }) });
|
|
114
|
+
const repos = new Proxy({}, { get: (_t, r) => sym(r) ? undefined : { services: services(r) } });
|
|
115
|
+
return { name, services: services(ownRepo), repos };
|
|
116
|
+
}
|
|
117
|
+
function makeContext(workspaceName, repo) {
|
|
118
|
+
return {
|
|
119
|
+
workspace: buildWorkspace(workspaceName, repo, (r, s, k) => makeRef(r, s, k)),
|
|
120
|
+
kind: buildKinds([])
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function resolvingWorkspace(name, ownRepo, resolve) {
|
|
124
|
+
return buildWorkspace(name, ownRepo, (repo, service, key) => resolve({ repo, service, key }));
|
|
125
|
+
}
|
|
126
|
+
function recordingWorkspace(name, ownRepo) {
|
|
127
|
+
const rec = [];
|
|
128
|
+
const workspace = buildWorkspace(name, ownRepo, (repo, service, key) => {
|
|
129
|
+
rec.push({ repo, service, key });
|
|
130
|
+
return "";
|
|
131
|
+
});
|
|
132
|
+
return { workspace, taken: () => rec };
|
|
133
|
+
}
|
|
134
|
+
var input = {
|
|
135
|
+
text: (label, opts = {}) => ({
|
|
136
|
+
kind: "text",
|
|
137
|
+
label,
|
|
138
|
+
...opts
|
|
139
|
+
}),
|
|
140
|
+
choice: (label, options, opts = {}) => ({
|
|
141
|
+
kind: "choice",
|
|
142
|
+
label,
|
|
143
|
+
options,
|
|
144
|
+
...opts
|
|
145
|
+
}),
|
|
146
|
+
confirm: (label, opts = {}) => ({ kind: "confirm", label, ...opts })
|
|
147
|
+
};
|
|
148
|
+
var ask = {
|
|
149
|
+
choice: (question, options, opts = {}) => ({
|
|
150
|
+
kind: "choice",
|
|
151
|
+
question,
|
|
152
|
+
options,
|
|
153
|
+
...opts
|
|
154
|
+
}),
|
|
155
|
+
confirm: (question, opts = {}) => ({
|
|
156
|
+
kind: "confirm",
|
|
157
|
+
question,
|
|
158
|
+
...opts
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
function applyAskDefaults(asks, answers, repo) {
|
|
162
|
+
const out = { ...answers };
|
|
163
|
+
for (const [key, spec] of Object.entries(asks)) {
|
|
164
|
+
if (out[key] !== undefined)
|
|
165
|
+
continue;
|
|
166
|
+
if (spec.default !== undefined) {
|
|
167
|
+
out[key] = spec.default;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const hint = spec.kind === "choice" ? spec.options.join("|") : "true|false";
|
|
171
|
+
throw new Error(`${repo}: '${key}' needs an answer — pass --answer ${key}=<${hint}> (or run interactively)`);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
var FACTORY = Symbol.for("ovr.factory");
|
|
176
|
+
var HEAD = Symbol.for("ovr.head");
|
|
177
|
+
var configAsks = (mod) => isConfigModule(mod) ? mod[HEAD]?.asks ?? {} : {};
|
|
178
|
+
function buildKinds(plugins) {
|
|
179
|
+
const kind = { shell };
|
|
180
|
+
for (const p of plugins)
|
|
181
|
+
if (p.kinds)
|
|
182
|
+
kind[p.name] = p.kinds;
|
|
183
|
+
return kind;
|
|
184
|
+
}
|
|
185
|
+
function defineConfig(a, b) {
|
|
186
|
+
if (b) {
|
|
187
|
+
const head = a;
|
|
188
|
+
const asks = { ...head.asks ?? {} };
|
|
189
|
+
if (head.modes?.length) {
|
|
190
|
+
asks.mode = {
|
|
191
|
+
kind: "choice",
|
|
192
|
+
question: "How should services run?",
|
|
193
|
+
options: head.modes,
|
|
194
|
+
...head.defaultMode !== undefined ? { default: head.defaultMode } : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
[HEAD]: { asks },
|
|
199
|
+
[FACTORY]: ({ workspace, repo, dir, answers }) => {
|
|
200
|
+
const plugins = typeof head.plugins === "function" ? head.plugins({ workspace: { name: workspace }, repo, dir }) : head.plugins ?? [];
|
|
201
|
+
const resolved = applyAskDefaults(asks, answers ?? {}, repo);
|
|
202
|
+
const { workspace: proj } = makeContext(workspace, repo);
|
|
203
|
+
const body = b({
|
|
204
|
+
workspace: proj,
|
|
205
|
+
kind: buildKinds(plugins),
|
|
206
|
+
mode: resolved.mode ?? "default",
|
|
207
|
+
answers: resolved
|
|
208
|
+
});
|
|
209
|
+
return { ...body, plugins: [...plugins] };
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const input2 = a;
|
|
214
|
+
const factory = typeof input2 === "function" ? input2 : () => input2;
|
|
215
|
+
return { [FACTORY]: ({ workspace, repo }) => factory(makeContext(workspace, repo)) };
|
|
216
|
+
}
|
|
217
|
+
var isConfigModule = (v) => typeof v === "object" && v !== null && (FACTORY in v);
|
|
218
|
+
function evalConfig(mod, workspace, repo, dir = "", answers) {
|
|
219
|
+
if (isConfigModule(mod))
|
|
220
|
+
return mod[FACTORY]({ workspace, repo, dir, answers });
|
|
221
|
+
const isPlain = mod && typeof mod === "object" && (("services" in mod) || ("setup" in mod));
|
|
222
|
+
const factory = typeof mod === "function" ? mod : isPlain ? () => ({ services: {}, ...mod }) : null;
|
|
223
|
+
if (!factory)
|
|
224
|
+
throw new Error("config default export must be defineConfig(...) or { services: … }");
|
|
225
|
+
return factory(makeContext(workspace, repo));
|
|
226
|
+
}
|
|
227
|
+
async function serviceRefs(def, tools) {
|
|
228
|
+
return def.plugin.refs ? await def.plugin.refs(def.spec, tools) : collectRefTargets(def.spec);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/engine/localbin.ts
|
|
232
|
+
import { existsSync } from "node:fs";
|
|
233
|
+
import { delimiter, join } from "node:path";
|
|
234
|
+
var LOCAL_BIN_DIRS = [["node_modules", ".bin"]];
|
|
235
|
+
function withLocalBins(dir, env) {
|
|
236
|
+
const bins = LOCAL_BIN_DIRS.map((p) => join(dir, ...p)).filter(existsSync);
|
|
237
|
+
if (!bins.length)
|
|
238
|
+
return env;
|
|
239
|
+
return { ...env, PATH: [...bins, env.PATH ?? ""].join(delimiter) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/plugins/shell.ts
|
|
243
|
+
var placeholderBag = () => new Proxy(() => {
|
|
244
|
+
return;
|
|
245
|
+
}, {
|
|
246
|
+
get: (_t, p) => p === Symbol.toPrimitive ? () => "" : placeholderBag(),
|
|
247
|
+
apply: () => placeholderBag()
|
|
248
|
+
});
|
|
249
|
+
function spawnArgs(cmd) {
|
|
250
|
+
if (Array.isArray(cmd))
|
|
251
|
+
return { command: cmd[0], args: cmd.slice(1), shell: false };
|
|
252
|
+
return { command: cmd, args: [], shell: true };
|
|
253
|
+
}
|
|
254
|
+
var shellPlugin = {
|
|
255
|
+
name: "shell",
|
|
256
|
+
refs(spec) {
|
|
257
|
+
const env = typeof spec.env === "function" ? spec.env(placeholderBag()) : spec.env ?? {};
|
|
258
|
+
return collectRefTargets(env);
|
|
259
|
+
},
|
|
260
|
+
async plan(spec, ctx) {
|
|
261
|
+
const bag = spec.prepare ? await spec.prepare({
|
|
262
|
+
workspace: ctx.workspace,
|
|
263
|
+
scope: ctx.scope,
|
|
264
|
+
ports: { alloc: ctx.allocPort },
|
|
265
|
+
secrets: { get: ctx.secret },
|
|
266
|
+
...ctx.caps
|
|
267
|
+
}) : undefined;
|
|
268
|
+
const exp = spec.exports?.(bag) ?? {};
|
|
269
|
+
const clean = {};
|
|
270
|
+
for (const [k, v] of Object.entries(exp)) {
|
|
271
|
+
const s = String(v);
|
|
272
|
+
if (!s.includes(ctx.pending))
|
|
273
|
+
clean[k] = s;
|
|
274
|
+
}
|
|
275
|
+
return clean;
|
|
276
|
+
},
|
|
277
|
+
async start(spec, ctx) {
|
|
278
|
+
const bag = ctx.bag;
|
|
279
|
+
const cmd = typeof spec.command === "function" ? spec.command(bag) : spec.command;
|
|
280
|
+
const { command, args, shell: useShell } = spawnArgs(cmd);
|
|
281
|
+
const rawEnv = typeof spec.env === "function" ? spec.env(bag) : spec.env ?? {};
|
|
282
|
+
const ownEnv = resolveRefs(rawEnv, ctx.resolve);
|
|
283
|
+
const env = withLocalBins(ctx.scope.dir, { ...ctx.env, ...ownEnv });
|
|
284
|
+
const child = spawn(command, args, {
|
|
285
|
+
cwd: ctx.scope.dir,
|
|
286
|
+
env,
|
|
287
|
+
shell: useShell,
|
|
288
|
+
detached: true,
|
|
289
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
290
|
+
});
|
|
291
|
+
ctx.pipe(child.stdout);
|
|
292
|
+
ctx.pipe(child.stderr);
|
|
293
|
+
let killTimer = null;
|
|
294
|
+
const killGroup = (sig) => {
|
|
295
|
+
try {
|
|
296
|
+
if (child.pid)
|
|
297
|
+
process.kill(-child.pid, sig);
|
|
298
|
+
} catch {
|
|
299
|
+
try {
|
|
300
|
+
child.kill(sig);
|
|
301
|
+
} catch {}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const onProcExit = () => killGroup("SIGKILL");
|
|
305
|
+
process.once("exit", onProcExit);
|
|
306
|
+
child.on("exit", () => {
|
|
307
|
+
process.removeListener("exit", onProcExit);
|
|
308
|
+
if (killTimer)
|
|
309
|
+
clearTimeout(killTimer);
|
|
310
|
+
});
|
|
311
|
+
const exports = spec.exports?.(bag) ?? {};
|
|
312
|
+
ctx.log(`▶ ${Array.isArray(cmd) ? cmd.join(" ") : cmd}${exports.url ? ` (${exports.url})` : ""}`);
|
|
313
|
+
const exited = new Promise((resolve) => child.on("exit", () => resolve()));
|
|
314
|
+
const ready = (spec.ready ? spec.ready(bag) : Promise.resolve()).then(() => spec.onReady?.(bag));
|
|
315
|
+
const actions = [
|
|
316
|
+
{
|
|
317
|
+
id: "shell",
|
|
318
|
+
label: "shell here (service env)",
|
|
319
|
+
command: process.env.SHELL || "/bin/sh",
|
|
320
|
+
cwd: ctx.scope.dir,
|
|
321
|
+
env
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
return {
|
|
325
|
+
exports,
|
|
326
|
+
info: exports.url ?? (exports.port ? `:${exports.port}` : undefined),
|
|
327
|
+
actions,
|
|
328
|
+
ready,
|
|
329
|
+
exited,
|
|
330
|
+
stop: () => {
|
|
331
|
+
killGroup("SIGTERM");
|
|
332
|
+
killTimer = setTimeout(() => killGroup("SIGKILL"), 3000);
|
|
333
|
+
killTimer.unref?.();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var shell = (spec) => ({ plugin: shellPlugin, spec });
|
|
339
|
+
|
|
340
|
+
export { withLocalBins, shellPlugin, shell, waitUntil, waitHttp, waitTcp, randomString, randomPassword, randomHex, isRef, refTarget, collectRefTargets, resolveRefs, makeContext, resolvingWorkspace, recordingWorkspace, input, ask, applyAskDefaults, configAsks, defineConfig, isConfigModule, evalConfig, serviceRefs };
|