@ovrdev/cli 0.1.0-alpha.16 → 0.1.0-alpha.17

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/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  coerceEnv,
7
7
  collectRefTargets,
8
8
  configAsks,
9
+ defaultRoot,
9
10
  evalConfig,
10
11
  forkChain,
11
12
  forkTreeOrder,
@@ -32,14 +33,143 @@ import {
32
33
  serviceRefs,
33
34
  withLocalBins,
34
35
  workspaceOrder
35
- } from "../index-4m3gj5te.js";
36
+ } from "../index-s6rge1qt.js";
36
37
 
37
38
  // src/cli/cmd.ts
38
39
  import * as clack2 from "@clack/prompts";
39
40
  import { execFileSync as execFileSync3 } from "node:child_process";
40
41
  import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
41
42
  import { homedir } from "node:os";
42
- import { join as join9, resolve as resolve2 } from "node:path";
43
+ import { basename as basename2, join as join9, resolve as resolve2 } from "node:path";
44
+
45
+ // src/core/load.ts
46
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
47
+ import { join } from "node:path";
48
+ import { createInterface } from "node:readline/promises";
49
+ import { pathToFileURL } from "node:url";
50
+ var CONFIG_FILES = ["ovr.config.mts", "ovr.config.ts", "ovr.config.js", "ovr.config.mjs"];
51
+ var findConfigFile = (dir) => CONFIG_FILES.map((f) => join(dir, f)).find(existsSync);
52
+ var answersPath = (dir) => join(dir, ".ovr", "answers.json");
53
+ var readAnswers = (dir) => {
54
+ try {
55
+ return JSON.parse(readFileSync(answersPath(dir), "utf8"));
56
+ } catch {
57
+ return {};
58
+ }
59
+ };
60
+ var writeAnswers = (dir, a) => {
61
+ mkdirSync(join(dir, ".ovr"), { recursive: true });
62
+ writeFileSync(answersPath(dir), `${JSON.stringify(a, null, "\t")}
63
+ `);
64
+ };
65
+ function coerceAnswer(spec, raw) {
66
+ if (spec.kind === "confirm") {
67
+ if (typeof raw === "boolean")
68
+ return raw;
69
+ if (raw === "true" || raw === "y" || raw === "yes" || raw === "1")
70
+ return true;
71
+ if (raw === "false" || raw === "n" || raw === "no" || raw === "0")
72
+ return false;
73
+ return;
74
+ }
75
+ return typeof raw === "string" && spec.options.includes(raw) ? raw : undefined;
76
+ }
77
+ async function resolveAsks(asks, opts) {
78
+ const answers = {};
79
+ let changed = false;
80
+ for (const [key, spec] of Object.entries(asks)) {
81
+ const flag = opts.overrides[key] !== undefined ? coerceAnswer(spec, opts.overrides[key]) : undefined;
82
+ if (flag !== undefined) {
83
+ answers[key] = flag;
84
+ changed ||= opts.saved[key] !== flag;
85
+ continue;
86
+ }
87
+ if (!opts.reask) {
88
+ const saved = coerceAnswer(spec, opts.saved[key]);
89
+ if (saved !== undefined) {
90
+ answers[key] = saved;
91
+ continue;
92
+ }
93
+ }
94
+ if (opts.prompt) {
95
+ answers[key] = coerceAnswer(spec, await opts.prompt(key, spec)) ?? spec.default;
96
+ changed = true;
97
+ continue;
98
+ }
99
+ if (spec.default !== undefined) {
100
+ answers[key] = spec.default;
101
+ changed = true;
102
+ }
103
+ }
104
+ return { answers, changed };
105
+ }
106
+ async function promptAsk(repo, key, spec) {
107
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
108
+ try {
109
+ if (spec.kind === "confirm") {
110
+ const hint = spec.default === false ? "y/N" : spec.default === true ? "Y/n" : "y/n";
111
+ const raw = (await rl.question(`? ${repo}: ${spec.question} (${hint}) `)).trim().toLowerCase();
112
+ if (!raw && spec.default !== undefined)
113
+ return spec.default;
114
+ return raw === "y" || raw === "yes" || raw === "true" || raw === "1";
115
+ }
116
+ process.stderr.write(`? ${repo}: ${spec.question}
117
+ `);
118
+ spec.options.forEach((o, i) => {
119
+ process.stderr.write(` ${i + 1}) ${o}${o === spec.default ? " (default)" : ""}
120
+ `);
121
+ });
122
+ for (;; ) {
123
+ const raw = (await rl.question("> ")).trim();
124
+ if (!raw && spec.default !== undefined)
125
+ return spec.default;
126
+ const byNum = spec.options[Number.parseInt(raw, 10) - 1];
127
+ if (byNum)
128
+ return byNum;
129
+ if (spec.options.includes(raw))
130
+ return raw;
131
+ process.stderr.write(` pick 1-${spec.options.length}
132
+ `);
133
+ }
134
+ } finally {
135
+ rl.close();
136
+ }
137
+ }
138
+ async function loadRepoConfig(dir, workspace, repo, opts = {}) {
139
+ const file = findConfigFile(dir);
140
+ if (!file)
141
+ return null;
142
+ const mod = await import(pathToFileURL(file).href);
143
+ const config = mod.default ?? mod;
144
+ const asks = configAsks(config);
145
+ let answers;
146
+ if (Object.keys(asks).length) {
147
+ const interactive = process.stdin.isTTY && process.stderr.isTTY;
148
+ const resolved = await resolveAsks(asks, {
149
+ saved: readAnswers(dir),
150
+ overrides: opts.answers ?? {},
151
+ reask: opts.reask,
152
+ prompt: interactive ? (key, spec) => promptAsk(repo, key, spec) : undefined
153
+ });
154
+ answers = resolved.answers;
155
+ if (resolved.changed)
156
+ writeAnswers(dir, answers);
157
+ }
158
+ return evalConfig(config, workspace, repo, dir, answers);
159
+ }
160
+ function parseAskFlags(args) {
161
+ const answers = {};
162
+ for (let i = 0;i < args.length; i++) {
163
+ if (args[i] === "--mode" && args[i + 1])
164
+ answers.mode = args[++i];
165
+ else if (args[i] === "--answer" && args[i + 1]) {
166
+ const eq = args[++i].indexOf("=");
167
+ if (eq > 0)
168
+ answers[args[i].slice(0, eq)] = args[i].slice(eq + 1);
169
+ }
170
+ }
171
+ return { answers, reask: args.includes("--ask") };
172
+ }
43
173
 
44
174
  // src/util/color.ts
45
175
  var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
@@ -53,15 +183,15 @@ var magenta = (s) => paint("35", s);
53
183
  var blue = (s) => paint("34", s);
54
184
 
55
185
  // src/engine/ports.ts
56
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
186
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
57
187
  import { createServer } from "node:net";
58
- import { dirname, join } from "node:path";
59
- var PORTS_PATH = join(CONFIG_DIR, "ports.json");
188
+ import { dirname, join as join2 } from "node:path";
189
+ var PORTS_PATH = join2(CONFIG_DIR, "ports.json");
60
190
  var loadFile = (file) => {
61
- if (!existsSync(file))
191
+ if (!existsSync2(file))
62
192
  return {};
63
193
  try {
64
- return JSON.parse(readFileSync(file, "utf8"));
194
+ return JSON.parse(readFileSync2(file, "utf8"));
65
195
  } catch {
66
196
  return {};
67
197
  }
@@ -87,7 +217,7 @@ function releasePorts(workspace, file = PORTS_PATH) {
87
217
  if (!(workspace in all))
88
218
  return;
89
219
  delete all[workspace];
90
- writeFileSync(file, `${JSON.stringify(all, null, "\t")}
220
+ writeFileSync2(file, `${JSON.stringify(all, null, "\t")}
91
221
  `);
92
222
  }
93
223
  function createPortRegistry(workspace, file = PORTS_PATH) {
@@ -95,8 +225,8 @@ function createPortRegistry(workspace, file = PORTS_PATH) {
95
225
  const save = () => {
96
226
  const all = loadFile(file);
97
227
  all[workspace] = recorded;
98
- mkdirSync(dirname(file), { recursive: true });
99
- writeFileSync(file, `${JSON.stringify(all, null, "\t")}
228
+ mkdirSync2(dirname(file), { recursive: true });
229
+ writeFileSync2(file, `${JSON.stringify(all, null, "\t")}
100
230
  `);
101
231
  };
102
232
  const reservedElsewhere = (service, slot) => {
@@ -138,10 +268,10 @@ function createPortRegistry(workspace, file = PORTS_PATH) {
138
268
  }
139
269
 
140
270
  // src/engine/paths.ts
141
- import { mkdirSync as mkdirSync2, rmSync as rmSync2 } from "node:fs";
271
+ import { mkdirSync as mkdirSync3, rmSync as rmSync2 } from "node:fs";
142
272
  import { tmpdir } from "node:os";
143
- import { join as join2 } from "node:path";
144
- var DATA_ROOT = join2(CONFIG_DIR, "data");
273
+ import { join as join3 } from "node:path";
274
+ var DATA_ROOT = join3(CONFIG_DIR, "data");
145
275
  function safeSegments(label) {
146
276
  const segs = label.split(/[/\\]+/).filter((s) => s && s !== ".");
147
277
  for (const s of segs)
@@ -150,14 +280,14 @@ function safeSegments(label) {
150
280
  return segs;
151
281
  }
152
282
  function releaseData(workspace, root = DATA_ROOT) {
153
- rmSync2(join2(root, workspace), { recursive: true, force: true });
283
+ rmSync2(join3(root, workspace), { recursive: true, force: true });
154
284
  }
155
285
  function createPathRegistry(workspace, root = DATA_ROOT) {
156
286
  return {
157
287
  forService(service) {
158
288
  return (label) => {
159
- const dir = join2(root, workspace, service, ...label ? safeSegments(label) : []);
160
- mkdirSync2(dir, { recursive: true });
289
+ const dir = join3(root, workspace, service, ...label ? safeSegments(label) : []);
290
+ mkdirSync3(dir, { recursive: true });
161
291
  return dir;
162
292
  };
163
293
  },
@@ -166,15 +296,15 @@ function createPathRegistry(workspace, root = DATA_ROOT) {
166
296
  }
167
297
  };
168
298
  }
169
- var dataDirPath = (workspace, service, root = DATA_ROOT) => join2(root, workspace, service);
170
- var SCRATCH_ROOT = join2(tmpdir(), "ovr-scratch");
299
+ var dataDirPath = (workspace, service, root = DATA_ROOT) => join3(root, workspace, service);
300
+ var SCRATCH_ROOT = join3(tmpdir(), "ovr-scratch");
171
301
  function createScratchRegistry(workspace, root = SCRATCH_ROOT) {
172
- rmSync2(join2(root, workspace), { recursive: true, force: true });
302
+ rmSync2(join3(root, workspace), { recursive: true, force: true });
173
303
  return {
174
304
  forService(service) {
175
305
  return (label) => {
176
- const dir = join2(root, workspace, service, ...label ? safeSegments(label) : []);
177
- mkdirSync2(dir, { recursive: true });
306
+ const dir = join3(root, workspace, service, ...label ? safeSegments(label) : []);
307
+ mkdirSync3(dir, { recursive: true });
178
308
  return dir;
179
309
  };
180
310
  }
@@ -183,8 +313,8 @@ function createScratchRegistry(workspace, root = SCRATCH_ROOT) {
183
313
 
184
314
  // src/engine/keys.ts
185
315
  import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";
186
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
187
- import { dirname as dirname2, join as join3 } from "node:path";
316
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
317
+ import { dirname as dirname2, join as join4 } from "node:path";
188
318
  var ALG = { ed25519: "EdDSA", rsa: "RS256", ec: "ES256" };
189
319
  function jwkThumbprint(jwk) {
190
320
  const members = jwk.kty === "RSA" ? ["e", "kty", "n"] : jwk.kty === "EC" ? ["crv", "kty", "x", "y"] : ["crv", "kty", "x"];
@@ -197,12 +327,12 @@ function sshField(v) {
197
327
  len.writeUInt32BE(b.length);
198
328
  return Buffer.concat([len, b]);
199
329
  }
200
- var KEYS_PATH = join3(CONFIG_DIR, "keys.json");
330
+ var KEYS_PATH = join4(CONFIG_DIR, "keys.json");
201
331
  var loadFile2 = (file) => {
202
- if (!existsSync2(file))
332
+ if (!existsSync3(file))
203
333
  return {};
204
334
  try {
205
- return JSON.parse(readFileSync2(file, "utf8"));
335
+ return JSON.parse(readFileSync3(file, "utf8"));
206
336
  } catch {
207
337
  return {};
208
338
  }
@@ -216,7 +346,7 @@ function releaseKeys(workspace, file = KEYS_PATH) {
216
346
  if (!(workspace in all))
217
347
  return;
218
348
  delete all[workspace];
219
- writeFileSync2(file, `${JSON.stringify(all, null, "\t")}
349
+ writeFileSync3(file, `${JSON.stringify(all, null, "\t")}
220
350
  `);
221
351
  }
222
352
  function createKeyRegistry(workspace, file = KEYS_PATH, dataRoot = DATA_ROOT) {
@@ -224,8 +354,8 @@ function createKeyRegistry(workspace, file = KEYS_PATH, dataRoot = DATA_ROOT) {
224
354
  const save = () => {
225
355
  const all = loadFile2(file);
226
356
  all[workspace] = recorded;
227
- mkdirSync3(dirname2(file), { recursive: true });
228
- writeFileSync2(file, `${JSON.stringify(all, null, "\t")}
357
+ mkdirSync4(dirname2(file), { recursive: true });
358
+ writeFileSync3(file, `${JSON.stringify(all, null, "\t")}
229
359
  `);
230
360
  };
231
361
  return {
@@ -258,13 +388,13 @@ function createKeyRegistry(workspace, file = KEYS_PATH, dataRoot = DATA_ROOT) {
258
388
  return { publicKey: `ssh-ed25519 ${wire.toString("base64")} ${sopts.comment ?? "ovr"}`, privateKey };
259
389
  },
260
390
  write(wopts = {}) {
261
- const base = join3(dataDirPath(workspace, service, dataRoot), "keys");
262
- mkdirSync3(base, { recursive: true });
391
+ const base = join4(dataDirPath(workspace, service, dataRoot), "keys");
392
+ mkdirSync4(base, { recursive: true });
263
393
  const nm = wopts.name ?? name;
264
- const privateKeyPath = join3(base, `${nm}.key`);
265
- const publicKeyPath = join3(base, `${nm}.pub`);
266
- writeFileSync2(privateKeyPath, privateKey);
267
- writeFileSync2(publicKeyPath, publicKey);
394
+ const privateKeyPath = join4(base, `${nm}.key`);
395
+ const publicKeyPath = join4(base, `${nm}.pub`);
396
+ writeFileSync3(privateKeyPath, privateKey);
397
+ writeFileSync3(publicKeyPath, publicKey);
268
398
  return { privateKeyPath, publicKeyPath };
269
399
  }
270
400
  };
@@ -279,21 +409,21 @@ function createKeyRegistry(workspace, file = KEYS_PATH, dataRoot = DATA_ROOT) {
279
409
  }
280
410
 
281
411
  // src/engine/running.ts
282
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
283
- import { dirname as dirname3, join as join4 } from "node:path";
284
- var RUNNING_PATH = join4(CONFIG_DIR, "running.json");
412
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
413
+ import { dirname as dirname3, join as join5 } from "node:path";
414
+ var RUNNING_PATH = join5(CONFIG_DIR, "running.json");
285
415
  var load = (file) => {
286
- if (!existsSync3(file))
416
+ if (!existsSync4(file))
287
417
  return {};
288
418
  try {
289
- return JSON.parse(readFileSync3(file, "utf8"));
419
+ return JSON.parse(readFileSync4(file, "utf8"));
290
420
  } catch {
291
421
  return {};
292
422
  }
293
423
  };
294
424
  var save = (file, data) => {
295
- mkdirSync4(dirname3(file), { recursive: true });
296
- writeFileSync3(file, `${JSON.stringify(data, null, "\t")}
425
+ mkdirSync5(dirname3(file), { recursive: true });
426
+ writeFileSync4(file, `${JSON.stringify(data, null, "\t")}
297
427
  `);
298
428
  };
299
429
  var pidAlive = (pid) => {
@@ -398,137 +528,6 @@ import { execSync } from "node:child_process";
398
528
  import { createHash as createHash2 } from "node:crypto";
399
529
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
400
530
  import { join as join6 } from "node:path";
401
-
402
- // src/core/load.ts
403
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
404
- import { join as join5 } from "node:path";
405
- import { createInterface } from "node:readline/promises";
406
- import { pathToFileURL } from "node:url";
407
- var CONFIG_FILES = ["ovr.config.mts", "ovr.config.ts", "ovr.config.js", "ovr.config.mjs"];
408
- var findConfigFile = (dir) => CONFIG_FILES.map((f) => join5(dir, f)).find(existsSync4);
409
- var answersPath = (dir) => join5(dir, ".ovr", "answers.json");
410
- var readAnswers = (dir) => {
411
- try {
412
- return JSON.parse(readFileSync4(answersPath(dir), "utf8"));
413
- } catch {
414
- return {};
415
- }
416
- };
417
- var writeAnswers = (dir, a) => {
418
- mkdirSync5(join5(dir, ".ovr"), { recursive: true });
419
- writeFileSync4(answersPath(dir), `${JSON.stringify(a, null, "\t")}
420
- `);
421
- };
422
- function coerceAnswer(spec, raw) {
423
- if (spec.kind === "confirm") {
424
- if (typeof raw === "boolean")
425
- return raw;
426
- if (raw === "true" || raw === "y" || raw === "yes" || raw === "1")
427
- return true;
428
- if (raw === "false" || raw === "n" || raw === "no" || raw === "0")
429
- return false;
430
- return;
431
- }
432
- return typeof raw === "string" && spec.options.includes(raw) ? raw : undefined;
433
- }
434
- async function resolveAsks(asks, opts) {
435
- const answers = {};
436
- let changed = false;
437
- for (const [key, spec] of Object.entries(asks)) {
438
- const flag = opts.overrides[key] !== undefined ? coerceAnswer(spec, opts.overrides[key]) : undefined;
439
- if (flag !== undefined) {
440
- answers[key] = flag;
441
- changed ||= opts.saved[key] !== flag;
442
- continue;
443
- }
444
- if (!opts.reask) {
445
- const saved = coerceAnswer(spec, opts.saved[key]);
446
- if (saved !== undefined) {
447
- answers[key] = saved;
448
- continue;
449
- }
450
- }
451
- if (opts.prompt) {
452
- answers[key] = coerceAnswer(spec, await opts.prompt(key, spec)) ?? spec.default;
453
- changed = true;
454
- continue;
455
- }
456
- if (spec.default !== undefined) {
457
- answers[key] = spec.default;
458
- changed = true;
459
- }
460
- }
461
- return { answers, changed };
462
- }
463
- async function promptAsk(repo, key, spec) {
464
- const rl = createInterface({ input: process.stdin, output: process.stderr });
465
- try {
466
- if (spec.kind === "confirm") {
467
- const hint = spec.default === false ? "y/N" : spec.default === true ? "Y/n" : "y/n";
468
- const raw = (await rl.question(`? ${repo}: ${spec.question} (${hint}) `)).trim().toLowerCase();
469
- if (!raw && spec.default !== undefined)
470
- return spec.default;
471
- return raw === "y" || raw === "yes" || raw === "true" || raw === "1";
472
- }
473
- process.stderr.write(`? ${repo}: ${spec.question}
474
- `);
475
- spec.options.forEach((o, i) => {
476
- process.stderr.write(` ${i + 1}) ${o}${o === spec.default ? " (default)" : ""}
477
- `);
478
- });
479
- for (;; ) {
480
- const raw = (await rl.question("> ")).trim();
481
- if (!raw && spec.default !== undefined)
482
- return spec.default;
483
- const byNum = spec.options[Number.parseInt(raw, 10) - 1];
484
- if (byNum)
485
- return byNum;
486
- if (spec.options.includes(raw))
487
- return raw;
488
- process.stderr.write(` pick 1-${spec.options.length}
489
- `);
490
- }
491
- } finally {
492
- rl.close();
493
- }
494
- }
495
- async function loadRepoConfig(dir, workspace, repo, opts = {}) {
496
- const file = findConfigFile(dir);
497
- if (!file)
498
- return null;
499
- const mod = await import(pathToFileURL(file).href);
500
- const config = mod.default ?? mod;
501
- const asks = configAsks(config);
502
- let answers;
503
- if (Object.keys(asks).length) {
504
- const interactive = process.stdin.isTTY && process.stderr.isTTY;
505
- const resolved = await resolveAsks(asks, {
506
- saved: readAnswers(dir),
507
- overrides: opts.answers ?? {},
508
- reask: opts.reask,
509
- prompt: interactive ? (key, spec) => promptAsk(repo, key, spec) : undefined
510
- });
511
- answers = resolved.answers;
512
- if (resolved.changed)
513
- writeAnswers(dir, answers);
514
- }
515
- return evalConfig(config, workspace, repo, dir, answers);
516
- }
517
- function parseAskFlags(args) {
518
- const answers = {};
519
- for (let i = 0;i < args.length; i++) {
520
- if (args[i] === "--mode" && args[i + 1])
521
- answers.mode = args[++i];
522
- else if (args[i] === "--answer" && args[i + 1]) {
523
- const eq = args[++i].indexOf("=");
524
- if (eq > 0)
525
- answers[args[i].slice(0, eq)] = args[i].slice(eq + 1);
526
- }
527
- }
528
- return { answers, reask: args.includes("--ask") };
529
- }
530
-
531
- // src/core/setup.ts
532
531
  var markerPath = (dir) => join6(dir, ".ovr", "setup");
533
532
  var stepsHash = (steps) => createHash2("sha1").update(JSON.stringify(steps)).digest("hex").slice(0, 12);
534
533
  function provisioned(dir, hash) {
@@ -809,6 +808,11 @@ function seedServiceConfig(cfg, workspace, repo) {
809
808
  }
810
809
  }
811
810
  }
811
+ return seedStarterConfig(dir);
812
+ }
813
+ function seedStarterConfig(dir) {
814
+ if (CONFIG_NAMES.some((f) => existsSync6(join8(dir, f))))
815
+ return;
812
816
  writeFileSync7(join8(dir, "ovr.config.mts"), STARTER_CONFIG);
813
817
  return { file: "ovr.config.mts" };
814
818
  }
@@ -1517,9 +1521,6 @@ async function readSource(src) {
1517
1521
  }
1518
1522
  return readFileSync7(src, "utf8");
1519
1523
  }
1520
- function defaultRoot() {
1521
- return join9(CONFIG_DIR, "workspaces");
1522
- }
1523
1524
  async function cmdBootstrap(args) {
1524
1525
  const cfg = loadConfig();
1525
1526
  let methodFlag;
@@ -2021,22 +2022,34 @@ function cmdWorkspaceCreate(args) {
2021
2022
  console.log(dim(" add a repo: ovr repo add <url|org/repo> then ovr run"));
2022
2023
  }
2023
2024
  async function cmdInit(args) {
2024
- const positional = args.filter((a) => !a.startsWith("-"));
2025
- const name = positional[0] || "dev";
2026
- const root = positional[1];
2027
- if (!/^[a-z0-9._-]+$/i.test(name))
2028
- throw new Error(`Invalid workspace name: ${name}`);
2029
- await cmdBootstrap([...root ? [root] : [], ...args.filter((a) => a.startsWith("-"))]);
2030
2025
  const cfg = loadConfig();
2031
- if (!cfg.root)
2026
+ let methodFlag;
2027
+ const positional = [];
2028
+ for (let i = 0;i < args.length; i++) {
2029
+ const a = args[i];
2030
+ if (a === "--ssh")
2031
+ methodFlag = "ssh";
2032
+ else if (a === "--https")
2033
+ methodFlag = "https";
2034
+ else if (!a.startsWith("-"))
2035
+ positional.push(a);
2036
+ }
2037
+ if (!cfg.root) {
2038
+ const root = positional[0] ? resolve2(positional[0]) : defaultRoot();
2039
+ cfg.root = root;
2040
+ cfg.cloneMethod = methodFlag ?? cfg.cloneMethod ?? DEFAULT_CLONE_METHOD;
2041
+ mkdirSync9(root, { recursive: true });
2042
+ saveConfig(cfg);
2043
+ console.log(`✓ bootstrapped — root → ${root} · clone → ${cfg.cloneMethod}`);
2044
+ }
2045
+ const cwd = process.cwd();
2046
+ const existing = findConfigFile(cwd);
2047
+ if (existing) {
2048
+ console.log(`✓ already an ovr project (${basename2(existing)}) — run: ${bold("ovr run")}`);
2032
2049
  return;
2033
- if (!cfg.workspaces[name])
2034
- cfg.workspaces[name] = { branch: "main" };
2035
- cfg.active = name;
2036
- saveConfig(cfg);
2037
- console.log(`
2038
- ✓ workspace '${name}' ready and active`);
2039
- console.log(dim(" next: ovr repo add <url|org/repo> · ovr run"));
2050
+ }
2051
+ const seeded = seedStarterConfig(cwd);
2052
+ console.log(`✓ scaffolded ${green(seeded?.file ?? "ovr.config.mts")} — declare your services, then: ${bold("ovr run")}`);
2040
2053
  }
2041
2054
  function cmdWorkspaceInfo(args, override, inferredRepo) {
2042
2055
  const cfg = loadConfig();
@@ -2396,7 +2409,7 @@ compdef _ovr ovr`;
2396
2409
  // src/engine/run.ts
2397
2410
  import { execFileSync as execFileSync6 } from "node:child_process";
2398
2411
  import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "node:fs";
2399
- import { basename as basename4, join as join14 } from "node:path";
2412
+ import { basename as basename5, join as join14 } from "node:path";
2400
2413
 
2401
2414
  // src/engine/envname.ts
2402
2415
  import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "node:fs";
@@ -2526,7 +2539,7 @@ function createSecretRegistry(workspace, file = SECRETS_PATH) {
2526
2539
 
2527
2540
  // src/engine/prepare-helpers.ts
2528
2541
  import { existsSync as existsSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync11 } from "node:fs";
2529
- import { basename as basename2, dirname as dirname6, isAbsolute, join as join13 } from "node:path";
2542
+ import { basename as basename3, dirname as dirname6, isAbsolute, join as join13 } from "node:path";
2530
2543
  function makeEnv(base) {
2531
2544
  return {
2532
2545
  get: (key, fallback) => base[key] ?? fallback,
@@ -2547,7 +2560,7 @@ function makeFiles(repoDir2, dataDir) {
2547
2560
  if (typeof template === "object") {
2548
2561
  const src = isAbsolute(template.from) ? template.from : join13(repoDir2, template.from);
2549
2562
  text2 = readFileSync10(src, "utf8");
2550
- name = opts.name ?? basename2(src).replace(/\.(tmpl|hbs|mustache|template)$/, "");
2563
+ name = opts.name ?? basename3(src).replace(/\.(tmpl|hbs|mustache|template)$/, "");
2551
2564
  } else {
2552
2565
  text2 = template;
2553
2566
  }
@@ -4980,8 +4993,8 @@ function createTui(services, handlers, options = {}) {
4980
4993
 
4981
4994
  // src/workspace/onboard.ts
4982
4995
  import { execFileSync as execFileSync5 } from "node:child_process";
4983
- import { readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
4984
- import { basename as basename3, resolve as resolve3 } from "node:path";
4996
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
4997
+ import { basename as basename4, resolve as resolve3 } from "node:path";
4985
4998
  import * as clack3 from "@clack/prompts";
4986
4999
  var real = (p) => {
4987
5000
  try {
@@ -5002,7 +5015,7 @@ function inferRepoName(dir) {
5002
5015
  return sanitizeName(base);
5003
5016
  }
5004
5017
  } catch {}
5005
- return sanitizeName(basename3(dir));
5018
+ return sanitizeName(basename4(dir));
5006
5019
  }
5007
5020
  var git2 = (dir, args) => {
5008
5021
  try {
@@ -5039,6 +5052,10 @@ function freeName(cfg, base) {
5039
5052
  return `${base}-${i}`;
5040
5053
  }
5041
5054
  async function autoOnboard(cfg, cwd) {
5055
+ if (!cfg.root) {
5056
+ cfg.root = defaultRoot();
5057
+ mkdirSync14(cfg.root, { recursive: true });
5058
+ }
5042
5059
  const { repo, decl } = onboardDecl(cwd);
5043
5060
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
5044
5061
  let target = freeName(cfg, repo);
@@ -5814,7 +5831,7 @@ function cmdLogs(args = [], override) {
5814
5831
  }
5815
5832
  for (const p of paths) {
5816
5833
  if (paths.length > 1)
5817
- process.stdout.write(dim(`── ${basename4(p, ".log")} ──
5834
+ process.stdout.write(dim(`── ${basename5(p, ".log")} ──
5818
5835
  `));
5819
5836
  process.stdout.write(readFileSync12(p, "utf8"));
5820
5837
  }
@@ -5841,8 +5858,8 @@ var tree = {
5841
5858
  },
5842
5859
  {
5843
5860
  name: "init",
5844
- summary: "Get started: set the workspaces root + create a first workspace",
5845
- usage: "ovr init [workspace] [root] [--ssh|--https]",
5861
+ summary: "Make the current directory an ovr project (bootstrap if needed + scaffold a config)",
5862
+ usage: "ovr init [root] [--ssh|--https]",
5846
5863
  run: (c) => cmdInit(c.args)
5847
5864
  },
5848
5865
  {
@@ -67,6 +67,12 @@ export type TeamConfig = {
67
67
  };
68
68
  export declare const CONFIG_DIR: string;
69
69
  export declare const CONFIG_PATH: string;
70
+ /**
71
+ * The default workspaces root — holds ONLY fork worktrees (base repos run in place). Under the
72
+ * config folder so a fresh user owns nothing and chooses nothing. Used by the interactive
73
+ * bootstrap and by the silent bootstrap `init`/`run` do on first use.
74
+ */
75
+ export declare const defaultRoot: () => string;
70
76
  /** Empty by default — nothing exists until you add it. */
71
77
  export declare const DEFAULT_CONFIG: GlobalConfig;
72
78
  export declare function loadConfig(): GlobalConfig;
package/dist/core/core.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  waitHttp,
27
27
  waitTcp,
28
28
  waitUntil
29
- } from "../index-4m3gj5te.js";
29
+ } from "../index-s6rge1qt.js";
30
30
  export {
31
31
  waitUntil,
32
32
  waitTcp,
@@ -0,0 +1,639 @@
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/core/config.ts
44
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
45
+ import { homedir } from "node:os";
46
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
47
+ var CONFIG_DIR = process.env.OVR_CONFIG_DIR || join(homedir(), ".config", "ovr");
48
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
49
+ var defaultRoot = () => join(CONFIG_DIR, "workspaces");
50
+ var DEFAULT_CONFIG = { workspaces: {} };
51
+ function rename(obj, from, to) {
52
+ if (obj && from in obj) {
53
+ obj[to] = obj[from];
54
+ delete obj[from];
55
+ }
56
+ }
57
+ function mapKeys(cfg, kebabToCamel) {
58
+ const swap = (o, kebab, camel) => kebabToCamel ? rename(o, kebab, camel) : rename(o, camel, kebab);
59
+ swap(cfg, "clone-method", "cloneMethod");
60
+ swap(cfg, "infer-from-cwd", "inferFromCwd");
61
+ swap(cfg, "team-config", "teamConfig");
62
+ const tui = cfg.tui;
63
+ swap(tui, "tab-width", "tabWidth");
64
+ swap(tui, "buffer-lines", "bufferLines");
65
+ swap(tui, "history-lines", "historyLines");
66
+ swap(tui, "throttle-ms", "throttleMs");
67
+ swap(cfg.infisical, "project-id", "projectId");
68
+ const wss = cfg.workspaces ?? cfg.projects;
69
+ for (const p of Object.values(wss ?? {})) {
70
+ swap(p?.infisical, "project-id", "projectId");
71
+ }
72
+ return cfg;
73
+ }
74
+ function loadConfig() {
75
+ if (!existsSync(CONFIG_PATH))
76
+ return structuredClone(DEFAULT_CONFIG);
77
+ const raw = mapKeys(JSON.parse(readFileSync(CONFIG_PATH, "utf8")), true);
78
+ const { projects, ...rest } = raw;
79
+ return { ...DEFAULT_CONFIG, ...rest, workspaces: raw.workspaces ?? projects ?? {} };
80
+ }
81
+ function saveConfig(cfg) {
82
+ mkdirSync(CONFIG_DIR, { recursive: true });
83
+ const onDisk = mapKeys(structuredClone(cfg), false);
84
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(onDisk, null, 2)}
85
+ `);
86
+ }
87
+ function serializeTeam(cfg) {
88
+ const slice = { workspaces: cfg.workspaces };
89
+ if (cfg.infisical)
90
+ slice.infisical = cfg.infisical;
91
+ return `${JSON.stringify(mapKeys(structuredClone(slice), false), null, 2)}
92
+ `;
93
+ }
94
+ function parseTeam(text) {
95
+ const raw = mapKeys(JSON.parse(text), true);
96
+ return {
97
+ infisical: raw.infisical,
98
+ workspaces: raw.workspaces
99
+ };
100
+ }
101
+ function mergeTeam(cfg, team) {
102
+ if (team.infisical)
103
+ cfg.infisical = { ...cfg.infisical, ...team.infisical };
104
+ const added = [];
105
+ const updated = [];
106
+ const conflicts = [];
107
+ for (const [name, incoming] of Object.entries(team.workspaces ?? {})) {
108
+ const local = cfg.workspaces[name];
109
+ if (!local) {
110
+ cfg.workspaces[name] = incoming;
111
+ added.push(name);
112
+ continue;
113
+ }
114
+ updated.push(name);
115
+ const localRepos = local.repos ?? {};
116
+ const repos = { ...localRepos };
117
+ for (const [r, decl] of Object.entries(incoming.repos ?? {})) {
118
+ if (!localRepos[r])
119
+ repos[r] = decl;
120
+ else if (JSON.stringify(localRepos[r]) !== JSON.stringify(decl)) {
121
+ conflicts.push({ workspace: name, repo: r, local: localRepos[r], incoming: decl });
122
+ }
123
+ }
124
+ const merged = { ...local, ...incoming };
125
+ if (Object.keys(repos).length)
126
+ merged.repos = repos;
127
+ else
128
+ delete merged.repos;
129
+ if (local.infisical || incoming.infisical) {
130
+ merged.infisical = { ...local.infisical, ...incoming.infisical };
131
+ }
132
+ cfg.workspaces[name] = merged;
133
+ }
134
+ return { added, updated, conflicts };
135
+ }
136
+ function getWorkspace(cfg, name) {
137
+ const p = cfg.workspaces[name];
138
+ if (!p) {
139
+ throw new Error(`Unknown workspace '${name}'. Known: ${Object.keys(cfg.workspaces).join(", ") || "(none)"}`);
140
+ }
141
+ return p;
142
+ }
143
+ var activeName = (cfg, override) => override || cfg.active;
144
+ var DEFAULT_BRANCH = "main";
145
+ var DEFAULT_HOST = "github.com";
146
+ var DEFAULT_CLONE_METHOD = "ssh";
147
+ function normalizeRemote(remote, defaultOrg) {
148
+ if (/^(git@|ssh:\/\/|https?:\/\/)/.test(remote))
149
+ return remote;
150
+ if (remote.includes("/"))
151
+ return remote;
152
+ return defaultOrg ? `${defaultOrg}/${remote}` : remote;
153
+ }
154
+ function repoUrl(remote, method = DEFAULT_CLONE_METHOD) {
155
+ if (/^(git@|ssh:\/\/|https?:\/\/|file:\/\/)/.test(remote) || /^[/.~]/.test(remote))
156
+ return remote;
157
+ const parts = remote.replace(/\.git$/, "").split("/");
158
+ const host = parts.length >= 3 ? parts[0] : DEFAULT_HOST;
159
+ const slug = (parts.length >= 3 ? parts.slice(1) : parts).join("/");
160
+ return method === "ssh" ? `git@${host}:${slug}.git` : `https://${host}/${slug}.git`;
161
+ }
162
+ function resolveRepoPath(p) {
163
+ if (p === "~" || p.startsWith("~/"))
164
+ return resolve(join(homedir(), p.slice(1)));
165
+ return resolve(p);
166
+ }
167
+ function forkChain(cfg, name) {
168
+ const chain = [];
169
+ const seen = new Set;
170
+ let cur = name;
171
+ while (cur && !seen.has(cur)) {
172
+ seen.add(cur);
173
+ getWorkspace(cfg, cur);
174
+ chain.push(cur);
175
+ cur = cfg.workspaces[cur].base;
176
+ }
177
+ return chain.reverse();
178
+ }
179
+ var workspaceOrder = (cfg) => (a, b) => (cfg.workspaces[a].protected ? 0 : 1) - (cfg.workspaces[b].protected ? 0 : 1) || a.localeCompare(b);
180
+ function forkTreeOrder(cfg) {
181
+ const names = Object.keys(cfg.workspaces);
182
+ const cmp = workspaceOrder(cfg);
183
+ const childrenOf = (parent) => names.filter((n) => cfg.workspaces[n].base === parent).sort(cmp);
184
+ const roots = names.filter((n) => !cfg.workspaces[n].base || !cfg.workspaces[cfg.workspaces[n].base]).sort(cmp);
185
+ const out = [];
186
+ const seen = new Set;
187
+ const walk = (name) => {
188
+ if (seen.has(name))
189
+ return;
190
+ seen.add(name);
191
+ out.push(name);
192
+ for (const k of childrenOf(name))
193
+ walk(k);
194
+ };
195
+ roots.forEach(walk);
196
+ names.sort(cmp).forEach(walk);
197
+ return out;
198
+ }
199
+ function resolveRepos(cfg, name) {
200
+ const out = {};
201
+ for (const proj of forkChain(cfg, name)) {
202
+ Object.assign(out, cfg.workspaces[proj].repos ?? {});
203
+ }
204
+ return out;
205
+ }
206
+ function nearest(cfg, name, pick) {
207
+ for (const proj of forkChain(cfg, name).reverse()) {
208
+ const v = pick(cfg.workspaces[proj]);
209
+ if (v !== undefined)
210
+ return v;
211
+ }
212
+ return;
213
+ }
214
+ var resolveBranch = (cfg, name) => nearest(cfg, name, (p) => p.branch) ?? DEFAULT_BRANCH;
215
+ function resolveInfisical(cfg, name) {
216
+ let out = { ...cfg.infisical ?? {} };
217
+ for (const proj of forkChain(cfg, name)) {
218
+ if (cfg.workspaces[proj].infisical)
219
+ out = { ...out, ...cfg.workspaces[proj].infisical };
220
+ }
221
+ return out;
222
+ }
223
+ var resolveEnvSlug = (cfg, name) => resolveInfisical(cfg, name).env;
224
+ var repoBranch = (cfg, workspace, decl) => decl.branch ?? resolveBranch(cfg, workspace);
225
+ function inferContext(cfg, cwd = process.cwd()) {
226
+ if (cfg.inferFromCwd === false || !cfg.root)
227
+ return {};
228
+ const real = (p) => {
229
+ try {
230
+ return realpathSync(p);
231
+ } catch {
232
+ return resolve(p);
233
+ }
234
+ };
235
+ const rel = relative(real(cfg.root), real(cwd));
236
+ if (!rel || rel.startsWith("..") || isAbsolute(rel))
237
+ return {};
238
+ const parts = rel.split(sep);
239
+ const workspace = cfg.workspaces[parts[0]] ? parts[0] : undefined;
240
+ const repo = workspace && parts[1] ? parts[1] : undefined;
241
+ return { workspace, repo };
242
+ }
243
+ // src/util/random.ts
244
+ import { randomBytes } from "node:crypto";
245
+ var ALNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
246
+ var SYMBOLS = "!#$%*+-_=?@.";
247
+ function randomString(length = 24, alphabet = ALNUM) {
248
+ if (length <= 0 || alphabet.length === 0)
249
+ return "";
250
+ const max = Math.floor(256 / alphabet.length) * alphabet.length;
251
+ let out = "";
252
+ while (out.length < length) {
253
+ for (const byte of randomBytes(length * 2)) {
254
+ if (byte >= max)
255
+ continue;
256
+ out += alphabet[byte % alphabet.length];
257
+ if (out.length === length)
258
+ break;
259
+ }
260
+ }
261
+ return out;
262
+ }
263
+ function randomPassword(length = 24, opts = {}) {
264
+ return randomString(length, opts.symbols === false ? ALNUM : ALNUM + SYMBOLS);
265
+ }
266
+ function randomHex(bytes = 24) {
267
+ return randomBytes(bytes).toString("hex");
268
+ }
269
+ // src/util/dotenv.ts
270
+ function parseDotenv(text) {
271
+ const out = {};
272
+ for (const line of text.split(`
273
+ `)) {
274
+ if (line.trimStart().startsWith("#"))
275
+ continue;
276
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
277
+ if (!m)
278
+ continue;
279
+ let v = m[2].trim();
280
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))
281
+ v = v.slice(1, -1);
282
+ out[m[1]] = v;
283
+ }
284
+ return out;
285
+ }
286
+ // src/env/dotenv-plugin.ts
287
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
288
+ import { join as join2 } from "node:path";
289
+ function dotenv(opts = {}) {
290
+ return {
291
+ name: "dotenv",
292
+ envSource: {
293
+ name: "dotenv",
294
+ load({ dir, env }) {
295
+ const files = opts.files ?? [".env", ".env.local", ...env ? [`.env.${env}`, `.env.${env}.local`] : []];
296
+ let out = {};
297
+ for (const f of files) {
298
+ const p = join2(dir, f);
299
+ if (existsSync2(p))
300
+ out = { ...out, ...parseDotenv(readFileSync2(p, "utf8")) };
301
+ }
302
+ return out;
303
+ }
304
+ }
305
+ };
306
+ }
307
+
308
+ // src/core/core.ts
309
+ var REF = Symbol.for("ovr.ref");
310
+ var isRef = (v) => typeof v === "object" && v !== null && (REF in v);
311
+ var refTarget = (r) => r[REF];
312
+ var makeRef = (repo, service, key) => ({ [REF]: { repo, service, key } });
313
+ function collectRefTargets(spec, out = []) {
314
+ if (isRef(spec)) {
315
+ out.push(refTarget(spec));
316
+ return out;
317
+ }
318
+ if (Array.isArray(spec)) {
319
+ for (const v of spec)
320
+ collectRefTargets(v, out);
321
+ return out;
322
+ }
323
+ if (spec && typeof spec === "object") {
324
+ for (const v of Object.values(spec))
325
+ collectRefTargets(v, out);
326
+ }
327
+ return out;
328
+ }
329
+ function resolveRefs(spec, resolve2) {
330
+ if (isRef(spec))
331
+ return resolve2(refTarget(spec));
332
+ if (Array.isArray(spec))
333
+ return spec.map((v) => resolveRefs(v, resolve2));
334
+ if (spec && typeof spec === "object") {
335
+ const out = {};
336
+ for (const [k, v] of Object.entries(spec)) {
337
+ if (isRef(v)) {
338
+ const r = resolve2(refTarget(v));
339
+ if (r !== undefined)
340
+ out[k] = r;
341
+ } else {
342
+ out[k] = resolveRefs(v, resolve2);
343
+ }
344
+ }
345
+ return out;
346
+ }
347
+ return spec;
348
+ }
349
+ function coerceEnv(obj) {
350
+ const out = {};
351
+ for (const [k, v] of Object.entries(obj)) {
352
+ if (v === undefined || v === null)
353
+ continue;
354
+ out[k] = typeof v === "object" ? JSON.stringify(v) : String(v);
355
+ }
356
+ return out;
357
+ }
358
+ var sym = (p) => typeof p === "symbol";
359
+ function buildWorkspace(name, ownRepo, leaf) {
360
+ const serviceNode = (repo, s) => new Proxy({ [REF]: { repo, service: s, key: "" } }, {
361
+ get: (t, k) => {
362
+ if (k === REF)
363
+ return t[REF];
364
+ if (k === "announce")
365
+ return (payload) => ({ to: `${repo}.${s}`, payload });
366
+ return sym(k) ? undefined : leaf(repo, s, k);
367
+ }
368
+ });
369
+ const services = (repo) => new Proxy({}, { get: (_t, s) => sym(s) ? undefined : serviceNode(repo, s) });
370
+ const repos = new Proxy({}, { get: (_t, r) => sym(r) ? undefined : { services: services(r) } });
371
+ return { name, services: services(ownRepo), repos };
372
+ }
373
+ function makeContext(workspaceName, repo) {
374
+ return {
375
+ workspace: buildWorkspace(workspaceName, repo, (r, s, k) => makeRef(r, s, k)),
376
+ kind: buildKinds([])
377
+ };
378
+ }
379
+ function resolvingWorkspace(name, ownRepo, resolve2) {
380
+ return buildWorkspace(name, ownRepo, (repo, service, key) => resolve2({ repo, service, key }));
381
+ }
382
+ function recordingWorkspace(name, ownRepo) {
383
+ const rec = [];
384
+ const workspace = buildWorkspace(name, ownRepo, (repo, service, key) => {
385
+ rec.push({ repo, service, key });
386
+ return "";
387
+ });
388
+ return { workspace, taken: () => rec };
389
+ }
390
+ var input = {
391
+ text: (label, opts = {}) => ({
392
+ kind: "text",
393
+ label,
394
+ ...opts
395
+ }),
396
+ choice: (label, options, opts = {}) => ({
397
+ kind: "choice",
398
+ label,
399
+ options,
400
+ ...opts
401
+ }),
402
+ confirm: (label, opts = {}) => ({
403
+ kind: "confirm",
404
+ label,
405
+ ...opts
406
+ })
407
+ };
408
+ var ask = {
409
+ choice: (question, options, opts = {}) => ({
410
+ kind: "choice",
411
+ question,
412
+ options,
413
+ ...opts
414
+ }),
415
+ confirm: (question, opts = {}) => ({
416
+ kind: "confirm",
417
+ question,
418
+ ...opts
419
+ })
420
+ };
421
+ function applyAskDefaults(asks, answers, repo) {
422
+ const out = { ...answers };
423
+ for (const [key, spec] of Object.entries(asks)) {
424
+ if (out[key] !== undefined)
425
+ continue;
426
+ if (spec.default !== undefined) {
427
+ out[key] = spec.default;
428
+ continue;
429
+ }
430
+ const hint = spec.kind === "choice" ? spec.options.join("|") : "true|false";
431
+ throw new Error(`${repo}: '${key}' needs an answer — pass --answer ${key}=<${hint}> (or run interactively)`);
432
+ }
433
+ return out;
434
+ }
435
+ var FACTORY = Symbol.for("ovr.factory");
436
+ var HEAD = Symbol.for("ovr.head");
437
+ var configAsks = (mod) => isConfigModule(mod) ? mod[HEAD]?.asks ?? {} : {};
438
+ function buildKinds(plugins) {
439
+ const kind = { shell };
440
+ for (const p of plugins)
441
+ if (p.kinds)
442
+ kind[p.name] = p.kinds;
443
+ return kind;
444
+ }
445
+ function buildActions(plugins) {
446
+ const out = { ...action };
447
+ for (const p of plugins)
448
+ if (p.actionKinds)
449
+ out[p.name] = p.actionKinds;
450
+ return out;
451
+ }
452
+ function defineConfig(a, b) {
453
+ if (b) {
454
+ const head = a;
455
+ const asks = { ...head.asks ?? {} };
456
+ if (head.modes?.length) {
457
+ asks.mode = {
458
+ kind: "choice",
459
+ question: "How should services run?",
460
+ options: head.modes,
461
+ ...head.defaultMode !== undefined ? { default: head.defaultMode } : {}
462
+ };
463
+ }
464
+ return {
465
+ [HEAD]: { asks },
466
+ [FACTORY]: ({ workspace, repo, dir, answers }) => {
467
+ const plugins = typeof head.plugins === "function" ? head.plugins({ workspace: { name: workspace }, repo, dir }) : head.plugins ?? [];
468
+ const resolved = applyAskDefaults(asks, answers ?? {}, repo);
469
+ const { workspace: proj } = makeContext(workspace, repo);
470
+ const body = b({
471
+ workspace: proj,
472
+ kind: buildKinds(plugins),
473
+ action: buildActions(plugins),
474
+ mode: resolved.mode ?? "default",
475
+ answers: resolved
476
+ });
477
+ return { ...body, plugins: [...plugins] };
478
+ }
479
+ };
480
+ }
481
+ const input2 = a;
482
+ const factory = typeof input2 === "function" ? input2 : () => input2;
483
+ return { [FACTORY]: ({ workspace, repo }) => factory(makeContext(workspace, repo)) };
484
+ }
485
+ var isConfigModule = (v) => typeof v === "object" && v !== null && (FACTORY in v);
486
+ function evalConfig(mod, workspace, repo, dir = "", answers) {
487
+ if (isConfigModule(mod))
488
+ return mod[FACTORY]({ workspace, repo, dir, answers });
489
+ const isPlain = mod && typeof mod === "object" && (("services" in mod) || ("setup" in mod));
490
+ const factory = typeof mod === "function" ? mod : isPlain ? () => ({ services: {}, ...mod }) : null;
491
+ if (!factory)
492
+ throw new Error("config default export must be defineConfig(...) or { services: … }");
493
+ return factory(makeContext(workspace, repo));
494
+ }
495
+ var action = {
496
+ session: (spec) => ({ mode: "session", ...spec }),
497
+ fn: (spec) => ({ mode: "function", ...spec }),
498
+ handover: (spec) => ({ mode: "handover", ...spec })
499
+ };
500
+ async function serviceRefs(def, tools) {
501
+ return def.plugin.refs ? await def.plugin.refs(def.spec, tools) : collectRefTargets(def.spec);
502
+ }
503
+
504
+ // src/engine/localbin.ts
505
+ import { existsSync as existsSync3 } from "node:fs";
506
+ import { delimiter, join as join3 } from "node:path";
507
+ var LOCAL_BIN_DIRS = [["node_modules", ".bin"]];
508
+ function withLocalBins(dir, env) {
509
+ const bins = LOCAL_BIN_DIRS.map((p) => join3(dir, ...p)).filter(existsSync3);
510
+ if (!bins.length)
511
+ return env;
512
+ return { ...env, PATH: [...bins, env.PATH ?? ""].join(delimiter) };
513
+ }
514
+
515
+ // src/plugins/shell.ts
516
+ var placeholderBag = () => new Proxy(() => {
517
+ return;
518
+ }, {
519
+ get: (_t, p) => p === Symbol.toPrimitive ? () => "" : placeholderBag(),
520
+ apply: () => placeholderBag()
521
+ });
522
+ function spawnArgs(cmd) {
523
+ if (Array.isArray(cmd))
524
+ return { command: cmd[0], args: cmd.slice(1), shell: false };
525
+ return { command: cmd, args: [], shell: true };
526
+ }
527
+ var shellPlugin = {
528
+ name: "shell",
529
+ status(handle) {
530
+ const pid = handle?.pid;
531
+ if (!pid)
532
+ return "down";
533
+ try {
534
+ process.kill(pid, 0);
535
+ return "up";
536
+ } catch (e) {
537
+ return e.code === "EPERM" ? "up" : "down";
538
+ }
539
+ },
540
+ stop(handle) {
541
+ const pid = handle?.pid;
542
+ if (!pid)
543
+ return;
544
+ const sig = (s) => {
545
+ try {
546
+ process.kill(-pid, s);
547
+ } catch {
548
+ try {
549
+ process.kill(pid, s);
550
+ } catch {}
551
+ }
552
+ };
553
+ sig("SIGTERM");
554
+ const t = setTimeout(() => sig("SIGKILL"), 3000);
555
+ t.unref?.();
556
+ },
557
+ refs(spec) {
558
+ const env = typeof spec.env === "function" ? spec.env(placeholderBag()) : spec.env ?? {};
559
+ return collectRefTargets(env);
560
+ },
561
+ async plan(spec, ctx) {
562
+ const bag = spec.prepare ? await spec.prepare({
563
+ workspace: ctx.workspace,
564
+ scope: ctx.scope,
565
+ ports: { alloc: ctx.allocPort },
566
+ secrets: { get: ctx.secret },
567
+ paths: { get: ctx.path },
568
+ ...ctx.caps
569
+ }) : undefined;
570
+ const exp = spec.exports?.(bag) ?? {};
571
+ const clean = {};
572
+ for (const [k, v] of Object.entries(exp)) {
573
+ if (v !== undefined && !JSON.stringify(v).includes(ctx.pending))
574
+ clean[k] = v;
575
+ }
576
+ return clean;
577
+ },
578
+ async start(spec, ctx) {
579
+ const bag = ctx.bag;
580
+ const cmd = typeof spec.command === "function" ? spec.command(bag) : spec.command;
581
+ const { command, args, shell: useShell } = spawnArgs(cmd);
582
+ const rawEnv = typeof spec.env === "function" ? spec.env(bag) : spec.env ?? {};
583
+ const ownEnv = coerceEnv(resolveRefs(rawEnv, ctx.resolve));
584
+ const env = withLocalBins(ctx.scope.dir, { ...ctx.env, ...ownEnv });
585
+ const child = spawn(command, args, {
586
+ cwd: ctx.scope.dir,
587
+ env,
588
+ shell: useShell,
589
+ detached: true,
590
+ stdio: ["ignore", "pipe", "pipe"]
591
+ });
592
+ ctx.pipe(child.stdout);
593
+ ctx.pipe(child.stderr);
594
+ let killTimer = null;
595
+ const killGroup = (sig) => {
596
+ try {
597
+ if (child.pid)
598
+ process.kill(-child.pid, sig);
599
+ } catch {
600
+ try {
601
+ child.kill(sig);
602
+ } catch {}
603
+ }
604
+ };
605
+ child.on("exit", () => {
606
+ if (killTimer)
607
+ clearTimeout(killTimer);
608
+ });
609
+ const exports = spec.exports?.(bag) ?? {};
610
+ ctx.log(`▶ ${Array.isArray(cmd) ? cmd.join(" ") : cmd}${exports.url ? ` (${exports.url})` : ""}`);
611
+ const exited = new Promise((resolve2) => child.on("exit", () => resolve2()));
612
+ const ready = (spec.ready ? spec.ready(bag) : Promise.resolve()).then(() => spec.onReady?.(bag));
613
+ const actions = [
614
+ {
615
+ id: "shell",
616
+ label: "shell here (service env)",
617
+ command: process.env.SHELL || "/bin/sh",
618
+ cwd: ctx.scope.dir,
619
+ env
620
+ }
621
+ ];
622
+ return {
623
+ exports,
624
+ info: exports.url != null ? String(exports.url) : exports.port != null ? `:${exports.port}` : undefined,
625
+ actions,
626
+ ready,
627
+ exited,
628
+ handle: { pid: child.pid ?? 0 },
629
+ stop: () => {
630
+ killGroup("SIGTERM");
631
+ killTimer = setTimeout(() => killGroup("SIGKILL"), 3000);
632
+ killTimer.unref?.();
633
+ }
634
+ };
635
+ }
636
+ };
637
+ var shell = (spec) => ({ plugin: shellPlugin, spec });
638
+
639
+ export { CONFIG_DIR, CONFIG_PATH, defaultRoot, loadConfig, saveConfig, serializeTeam, parseTeam, mergeTeam, getWorkspace, activeName, DEFAULT_CLONE_METHOD, normalizeRemote, repoUrl, resolveRepoPath, forkChain, workspaceOrder, forkTreeOrder, resolveRepos, resolveBranch, resolveInfisical, resolveEnvSlug, repoBranch, inferContext, withLocalBins, shellPlugin, shell, waitUntil, waitHttp, waitTcp, randomString, randomPassword, randomHex, parseDotenv, dotenv, isRef, refTarget, collectRefTargets, resolveRefs, coerceEnv, makeContext, resolvingWorkspace, recordingWorkspace, input, ask, applyAskDefaults, configAsks, defineConfig, isConfigModule, evalConfig, action, serviceRefs };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  shell,
3
3
  shellPlugin
4
- } from "../index-4m3gj5te.js";
4
+ } from "../index-s6rge1qt.js";
5
5
  export {
6
6
  shellPlugin,
7
7
  shell
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovrdev/cli",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "Override — the dev control plane: environments, repos, services, tasks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,13 @@
21
21
  },
22
22
  "./package.json": "./package.json"
23
23
  },
24
+ "scripts": {
25
+ "build": "bun build src/cli/index.ts src/core/core.ts src/plugins/shell.ts --root src --outdir=dist --target=node --splitting --format=esm --packages=external && tsc -p tsconfig.build.json && node scripts/fix-dts.mjs && bun build packages/plugin-portless/src/index.ts --outdir=packages/plugin-portless/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-portless/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-portless/dist && bun build packages/plugin-docker/src/index.ts --outdir=packages/plugin-docker/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-docker/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-docker/dist && bun build packages/plugin-infisical/src/index.ts --outdir=packages/plugin-infisical/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-infisical/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-infisical/dist && bun build packages/plugin-js/src/index.ts --outdir=packages/plugin-js/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-js/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-js/dist && bun build packages/plugin-postgres/src/index.ts --outdir=packages/plugin-postgres/dist --target=node --format=esm --packages=external && bun build packages/plugin-postgres/src/sql-runner.ts --outdir=packages/plugin-postgres/dist --target=node --format=esm && tsc -p packages/plugin-postgres/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-postgres/dist && bun build packages/plugin-supabase/src/index.ts --outdir=packages/plugin-supabase/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-supabase/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-supabase/dist",
26
+ "dev": "bun build src/cli/index.ts src/core/core.ts src/plugins/shell.ts --root src --outdir=dist --target=node --splitting --format=esm --packages=external --watch",
27
+ "postinstall": "node scripts/postinstall.mjs",
28
+ "prepare": "bun run build",
29
+ "test": "bun run build && bun test"
30
+ },
24
31
  "engines": {
25
32
  "node": ">=22.6"
26
33
  },
@@ -35,11 +42,5 @@
35
42
  "@types/node": "^22.10.0",
36
43
  "@types/react": "^19.2.17",
37
44
  "typescript": "^5.7.2"
38
- },
39
- "scripts": {
40
- "build": "bun build src/cli/index.ts src/core/core.ts src/plugins/shell.ts --root src --outdir=dist --target=node --splitting --format=esm --packages=external && tsc -p tsconfig.build.json && node scripts/fix-dts.mjs && bun build packages/plugin-portless/src/index.ts --outdir=packages/plugin-portless/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-portless/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-portless/dist && bun build packages/plugin-docker/src/index.ts --outdir=packages/plugin-docker/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-docker/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-docker/dist && bun build packages/plugin-infisical/src/index.ts --outdir=packages/plugin-infisical/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-infisical/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-infisical/dist && bun build packages/plugin-js/src/index.ts --outdir=packages/plugin-js/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-js/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-js/dist && bun build packages/plugin-postgres/src/index.ts --outdir=packages/plugin-postgres/dist --target=node --format=esm --packages=external && bun build packages/plugin-postgres/src/sql-runner.ts --outdir=packages/plugin-postgres/dist --target=node --format=esm && tsc -p packages/plugin-postgres/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-postgres/dist && bun build packages/plugin-supabase/src/index.ts --outdir=packages/plugin-supabase/dist --target=node --format=esm --packages=external && tsc -p packages/plugin-supabase/tsconfig.build.json && node scripts/fix-dts.mjs packages/plugin-supabase/dist",
41
- "dev": "bun build src/cli/index.ts src/core/core.ts src/plugins/shell.ts --root src --outdir=dist --target=node --splitting --format=esm --packages=external --watch",
42
- "postinstall": "node scripts/postinstall.mjs",
43
- "test": "bun run build && bun test"
44
45
  }
45
- }
46
+ }