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

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) {
@@ -782,6 +781,22 @@ import { defineConfig } from "@ovrdev/cli"
782
781
 
783
782
  export default defineConfig({ plugins: [] }, ({ workspace, kind }) => ({
784
783
  services: {
784
+ // A placeholder so \`ovr run\` boots something right away. It prints a few tips and
785
+ // stays up until you quit (qq). Replace it with your real service(s) — see the example.
786
+ hello: kind.shell({
787
+ command: [
788
+ "echo ''",
789
+ "echo ' \uD83D\uDC4B ovr is running this repo.'",
790
+ "echo ''",
791
+ "echo ' • Edit ovr.config.mts to declare your real services, then rerun ovr run.'",
792
+ "echo ' • A service is a process: kind.shell({ command, env, ready, exports }).'",
793
+ "echo ' • Every service gets its own allocated port — cross-wire with refs.'",
794
+ "echo ' • Press qq to quit; a service that stays up keeps the run alive.'",
795
+ "echo ''",
796
+ "tail -f /dev/null", // hang until ovr stops it (SIGTERM on quit)
797
+ ].join(" && "),
798
+ }),
799
+
785
800
  // web: kind.shell({
786
801
  // prepare: async ({ ports }) => ({ port: await ports.alloc() }),
787
802
  // command: ({ port }) => \`pnpm dev --port \${port}\`,
@@ -809,6 +824,11 @@ function seedServiceConfig(cfg, workspace, repo) {
809
824
  }
810
825
  }
811
826
  }
827
+ return seedStarterConfig(dir);
828
+ }
829
+ function seedStarterConfig(dir) {
830
+ if (CONFIG_NAMES.some((f) => existsSync6(join8(dir, f))))
831
+ return;
812
832
  writeFileSync7(join8(dir, "ovr.config.mts"), STARTER_CONFIG);
813
833
  return { file: "ovr.config.mts" };
814
834
  }
@@ -893,6 +913,46 @@ async function cmdSetup(args = [], override) {
893
913
  if (!touched)
894
914
  console.log(dim("Nothing to provision (no setup declared, or nothing overridden)."));
895
915
  }
916
+ function methodOfUrl(url) {
917
+ if (/^(git@|ssh:\/\/)/.test(url))
918
+ return "ssh";
919
+ if (/^https?:\/\//.test(url))
920
+ return "https";
921
+ return;
922
+ }
923
+ function inferCloneMethod(cfg) {
924
+ for (const def of Object.values(cfg.workspaces)) {
925
+ for (const decl of Object.values(def.repos ?? {})) {
926
+ if (decl.path) {
927
+ const m = methodOfUrl(decl.remote);
928
+ if (m)
929
+ return m;
930
+ }
931
+ }
932
+ }
933
+ return;
934
+ }
935
+ async function ensureCloneMethod(cfg, remote) {
936
+ if (cfg.cloneMethod)
937
+ return;
938
+ if (/^(git@|ssh:\/\/|https?:\/\/|file:\/\/)/.test(remote) || /^[/.~]/.test(remote))
939
+ return;
940
+ if (!process.stdin.isTTY)
941
+ return;
942
+ const initialValue = inferCloneMethod(cfg) ?? DEFAULT_CLONE_METHOD;
943
+ const sel = await clack.select({
944
+ message: "Clone method (how org/repo slugs expand)",
945
+ options: [
946
+ { value: "ssh", label: "ssh", hint: "git@github.com:org/repo.git" },
947
+ { value: "https", label: "https" }
948
+ ],
949
+ initialValue
950
+ });
951
+ if (clack.isCancel(sel))
952
+ return;
953
+ cfg.cloneMethod = sel;
954
+ saveConfig(cfg);
955
+ }
896
956
  function cloneInto(cfg, workspace, name, decl) {
897
957
  const dir = repoDir(cfg, workspace, name);
898
958
  if (existsSync6(dir))
@@ -903,7 +963,7 @@ function cloneInto(cfg, workspace, name, decl) {
903
963
  console.log(`Cloning ${name} (${url}) → ${dir}`);
904
964
  execFileSync2("git", ["clone", "--branch", branch, url, dir], { stdio: "inherit" });
905
965
  }
906
- function cmdRepoAdd(args, override) {
966
+ async function cmdRepoAdd(args, override) {
907
967
  let name;
908
968
  let branch;
909
969
  let noClone = false;
@@ -932,8 +992,10 @@ function cmdRepoAdd(args, override) {
932
992
  const decl = { remote };
933
993
  if (branch)
934
994
  decl.branch = branch;
935
- if (!noClone)
995
+ if (!noClone) {
996
+ await ensureCloneMethod(cfg, remote);
936
997
  cloneInto(cfg, workspace, repoName, decl);
998
+ }
937
999
  proj.repos ??= {};
938
1000
  proj.repos[repoName] = decl;
939
1001
  saveConfig(cfg);
@@ -1257,7 +1319,7 @@ ${bold(name)} ${dim("$ git fetch --prune")}`);
1257
1319
  console.log(fetched.size ? `
1258
1320
  ✓ fetched ${fetched.size} repo(s)` : "Nothing to fetch.");
1259
1321
  }
1260
- function cmdRepoClone(args, override) {
1322
+ async function cmdRepoClone(args, override) {
1261
1323
  const all = args.includes("--all") || args.includes("-a");
1262
1324
  const name = args.find((a) => !a.startsWith("-"));
1263
1325
  const cfg = loadConfig();
@@ -1278,8 +1340,10 @@ function cmdRepoClone(args, override) {
1278
1340
  }
1279
1341
  if (!targets.length)
1280
1342
  return console.log("Nothing to clone — all declared repos are present.");
1281
- for (const r of targets)
1343
+ for (const r of targets) {
1344
+ await ensureCloneMethod(cfg, repos[r].remote);
1282
1345
  cloneInto(cfg, workspace, r, repos[r]);
1346
+ }
1283
1347
  console.log(`✓ cloned ${targets.length} repo(s) into '${workspace}'`);
1284
1348
  }
1285
1349
  async function cmdFork(args, override) {
@@ -1517,9 +1581,6 @@ async function readSource(src) {
1517
1581
  }
1518
1582
  return readFileSync7(src, "utf8");
1519
1583
  }
1520
- function defaultRoot() {
1521
- return join9(CONFIG_DIR, "workspaces");
1522
- }
1523
1584
  async function cmdBootstrap(args) {
1524
1585
  const cfg = loadConfig();
1525
1586
  let methodFlag;
@@ -1666,7 +1727,7 @@ Shell integration — add to ${rc.display}:`);
1666
1727
  seeded.active = base;
1667
1728
  saveConfig(seeded);
1668
1729
  }
1669
- cmdRepoClone(["--all"], base);
1730
+ await cmdRepoClone(["--all"], base);
1670
1731
  }
1671
1732
  }
1672
1733
  clack2.outro("Bootstrap complete.");
@@ -2021,22 +2082,35 @@ function cmdWorkspaceCreate(args) {
2021
2082
  console.log(dim(" add a repo: ovr repo add <url|org/repo> then ovr run"));
2022
2083
  }
2023
2084
  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
2085
  const cfg = loadConfig();
2031
- if (!cfg.root)
2086
+ let methodFlag;
2087
+ const positional = [];
2088
+ for (let i = 0;i < args.length; i++) {
2089
+ const a = args[i];
2090
+ if (a === "--ssh")
2091
+ methodFlag = "ssh";
2092
+ else if (a === "--https")
2093
+ methodFlag = "https";
2094
+ else if (!a.startsWith("-"))
2095
+ positional.push(a);
2096
+ }
2097
+ if (!cfg.root) {
2098
+ const root = positional[0] ? resolve2(positional[0]) : defaultRoot();
2099
+ cfg.root = root;
2100
+ if (methodFlag)
2101
+ cfg.cloneMethod = methodFlag;
2102
+ mkdirSync9(root, { recursive: true });
2103
+ saveConfig(cfg);
2104
+ console.log(`✓ bootstrapped — root → ${root}${cfg.cloneMethod ? ` · clone → ${cfg.cloneMethod}` : ""}`);
2105
+ }
2106
+ const cwd = process.cwd();
2107
+ const existing = findConfigFile(cwd);
2108
+ if (existing) {
2109
+ console.log(`✓ already an ovr project (${basename2(existing)}) — run: ${bold("ovr run")}`);
2032
2110
  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"));
2111
+ }
2112
+ const seeded = seedStarterConfig(cwd);
2113
+ console.log(`✓ scaffolded ${green(seeded?.file ?? "ovr.config.mts")} — declare your services, then: ${bold("ovr run")}`);
2040
2114
  }
2041
2115
  function cmdWorkspaceInfo(args, override, inferredRepo) {
2042
2116
  const cfg = loadConfig();
@@ -2396,7 +2470,7 @@ compdef _ovr ovr`;
2396
2470
  // src/engine/run.ts
2397
2471
  import { execFileSync as execFileSync6 } from "node:child_process";
2398
2472
  import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "node:fs";
2399
- import { basename as basename4, join as join14 } from "node:path";
2473
+ import { basename as basename5, join as join14 } from "node:path";
2400
2474
 
2401
2475
  // src/engine/envname.ts
2402
2476
  import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "node:fs";
@@ -2526,7 +2600,7 @@ function createSecretRegistry(workspace, file = SECRETS_PATH) {
2526
2600
 
2527
2601
  // src/engine/prepare-helpers.ts
2528
2602
  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";
2603
+ import { basename as basename3, dirname as dirname6, isAbsolute, join as join13 } from "node:path";
2530
2604
  function makeEnv(base) {
2531
2605
  return {
2532
2606
  get: (key, fallback) => base[key] ?? fallback,
@@ -2547,7 +2621,7 @@ function makeFiles(repoDir2, dataDir) {
2547
2621
  if (typeof template === "object") {
2548
2622
  const src = isAbsolute(template.from) ? template.from : join13(repoDir2, template.from);
2549
2623
  text2 = readFileSync10(src, "utf8");
2550
- name = opts.name ?? basename2(src).replace(/\.(tmpl|hbs|mustache|template)$/, "");
2624
+ name = opts.name ?? basename3(src).replace(/\.(tmpl|hbs|mustache|template)$/, "");
2551
2625
  } else {
2552
2626
  text2 = template;
2553
2627
  }
@@ -4980,8 +5054,8 @@ function createTui(services, handlers, options = {}) {
4980
5054
 
4981
5055
  // src/workspace/onboard.ts
4982
5056
  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";
5057
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
5058
+ import { basename as basename4, resolve as resolve3 } from "node:path";
4985
5059
  import * as clack3 from "@clack/prompts";
4986
5060
  var real = (p) => {
4987
5061
  try {
@@ -5002,7 +5076,7 @@ function inferRepoName(dir) {
5002
5076
  return sanitizeName(base);
5003
5077
  }
5004
5078
  } catch {}
5005
- return sanitizeName(basename3(dir));
5079
+ return sanitizeName(basename4(dir));
5006
5080
  }
5007
5081
  var git2 = (dir, args) => {
5008
5082
  try {
@@ -5039,6 +5113,10 @@ function freeName(cfg, base) {
5039
5113
  return `${base}-${i}`;
5040
5114
  }
5041
5115
  async function autoOnboard(cfg, cwd) {
5116
+ if (!cfg.root) {
5117
+ cfg.root = defaultRoot();
5118
+ mkdirSync14(cfg.root, { recursive: true });
5119
+ }
5042
5120
  const { repo, decl } = onboardDecl(cwd);
5043
5121
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
5044
5122
  let target = freeName(cfg, repo);
@@ -5814,7 +5892,7 @@ function cmdLogs(args = [], override) {
5814
5892
  }
5815
5893
  for (const p of paths) {
5816
5894
  if (paths.length > 1)
5817
- process.stdout.write(dim(`── ${basename4(p, ".log")} ──
5895
+ process.stdout.write(dim(`── ${basename5(p, ".log")} ──
5818
5896
  `));
5819
5897
  process.stdout.write(readFileSync12(p, "utf8"));
5820
5898
  }
@@ -5841,8 +5919,8 @@ var tree = {
5841
5919
  },
5842
5920
  {
5843
5921
  name: "init",
5844
- summary: "Get started: set the workspaces root + create a first workspace",
5845
- usage: "ovr init [workspace] [root] [--ssh|--https]",
5922
+ summary: "Make the current directory an ovr project (bootstrap if needed + scaffold a config)",
5923
+ usage: "ovr init [root] [--ssh|--https]",
5846
5924
  run: (c) => cmdInit(c.args)
5847
5925
  },
5848
5926
  {