@eudiplo/cli 8.0.2-main.3b6a116 → 8.0.2-main.a9f319d

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.
Files changed (48) hide show
  1. package/README.md +5 -1
  2. package/dist/commands/deployment/action.d.ts +1 -1
  3. package/dist/commands/deployment/action.d.ts.map +1 -1
  4. package/dist/commands/deployment/action.js.map +1 -1
  5. package/dist/commands/deployment/index.d.ts.map +1 -1
  6. package/dist/commands/deployment/index.js +7 -4
  7. package/dist/commands/deployment/index.js.map +1 -1
  8. package/dist/commands/open/action.d.ts +9 -0
  9. package/dist/commands/open/action.d.ts.map +1 -0
  10. package/dist/commands/open/action.js +41 -0
  11. package/dist/commands/open/action.js.map +1 -0
  12. package/dist/commands/open/index.d.ts +5 -0
  13. package/dist/commands/open/index.d.ts.map +1 -0
  14. package/dist/commands/open/index.js +15 -0
  15. package/dist/commands/open/index.js.map +1 -0
  16. package/dist/commands/upgrade/action.d.ts +3 -0
  17. package/dist/commands/upgrade/action.d.ts.map +1 -0
  18. package/dist/commands/upgrade/action.js +95 -0
  19. package/dist/commands/upgrade/action.js.map +1 -0
  20. package/dist/commands/upgrade/index.d.ts +5 -0
  21. package/dist/commands/upgrade/index.d.ts.map +1 -0
  22. package/dist/commands/upgrade/index.js +15 -0
  23. package/dist/commands/upgrade/index.js.map +1 -0
  24. package/dist/index.js +622 -66
  25. package/dist/runtime.d.ts.map +1 -1
  26. package/dist/runtime.js +4 -0
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/services/browser.d.ts +30 -0
  29. package/dist/services/browser.d.ts.map +1 -0
  30. package/dist/services/browser.js +102 -0
  31. package/dist/services/browser.js.map +1 -0
  32. package/dist/services/compose-args.d.ts +28 -0
  33. package/dist/services/compose-args.d.ts.map +1 -0
  34. package/dist/services/compose-args.js +117 -0
  35. package/dist/services/compose-args.js.map +1 -0
  36. package/dist/services/compose-upgrade.d.ts +40 -0
  37. package/dist/services/compose-upgrade.d.ts.map +1 -0
  38. package/dist/services/compose-upgrade.js +102 -0
  39. package/dist/services/compose-upgrade.js.map +1 -0
  40. package/dist/services/deployment-drivers.d.ts +8 -1
  41. package/dist/services/deployment-drivers.d.ts.map +1 -1
  42. package/dist/services/deployment-drivers.js +85 -19
  43. package/dist/services/deployment-drivers.js.map +1 -1
  44. package/dist/types.d.ts +1 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/templates/schemas/KeyChainConfigFile.schema.json +7 -3
  48. package/templates/schemas/KeyChainImportDto.schema.json +7 -3
package/dist/index.js CHANGED
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
2980
2980
  const schOrFunc = root.refs[ref];
2981
2981
  if (schOrFunc)
2982
2982
  return schOrFunc;
2983
- let _sch = resolve7.call(this, root, ref);
2983
+ let _sch = resolve9.call(this, root, ref);
2984
2984
  if (_sch === void 0) {
2985
2985
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
2986
2986
  const { schemaId } = this.opts;
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
3007
3007
  function sameSchemaEnv(s1, s2) {
3008
3008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3009
  }
3010
- function resolve7(root, ref) {
3010
+ function resolve9(root, ref) {
3011
3011
  let sch;
3012
3012
  while (typeof (sch = this.refs[ref]) == "string")
3013
3013
  ref = sch;
@@ -3832,7 +3832,7 @@ var require_fast_uri = __commonJS({
3832
3832
  }
3833
3833
  return uri;
3834
3834
  }
3835
- function resolve7(baseURI, relativeURI, options) {
3835
+ function resolve9(baseURI, relativeURI, options) {
3836
3836
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3837
3837
  const {
3838
3838
  parsed: baseParsed,
@@ -4194,7 +4194,7 @@ var require_fast_uri = __commonJS({
4194
4194
  var fastUri = {
4195
4195
  SCHEMES,
4196
4196
  normalize,
4197
- resolve: resolve7,
4197
+ resolve: resolve9,
4198
4198
  resolveComponent,
4199
4199
  equal,
4200
4200
  serialize,
@@ -21550,8 +21550,107 @@ function createConfigCommand(context, setExitCode) {
21550
21550
 
21551
21551
  // src/services/deployment-drivers.ts
21552
21552
  import { access as access2, mkdir as mkdir6, rm as rm4, stat as stat2, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
21553
- import { delimiter, join as join10, resolve as resolve5 } from "node:path";
21553
+ import { delimiter, join as join10, resolve as resolve6 } from "node:path";
21554
21554
  import { spawn } from "node:child_process";
21555
+
21556
+ // src/services/compose-args.ts
21557
+ import { resolve as resolve5 } from "node:path";
21558
+ var SERVICE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
21559
+ var DURATION = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
21560
+ var TIMESTAMP = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
21561
+ function assertServiceName(service) {
21562
+ if (!SERVICE_NAME.test(service)) {
21563
+ throw new Error(
21564
+ `Invalid service name ${JSON.stringify(service)}. Service names start with a letter or digit and contain only letters, digits, ".", "_" and "-".`
21565
+ );
21566
+ }
21567
+ }
21568
+ function assertSince(since) {
21569
+ const isDuration = DURATION.test(since);
21570
+ const isTimestamp = TIMESTAMP.test(since) && !Number.isNaN(Date.parse(since));
21571
+ if (!isDuration && !isTimestamp) {
21572
+ throw new Error(
21573
+ `Invalid --since value ${JSON.stringify(since)}. Use a duration such as 10m or 2h30m, or a timestamp such as 2026-09-01T12:00:00Z.`
21574
+ );
21575
+ }
21576
+ }
21577
+ function assertTail(tail) {
21578
+ if (!Number.isInteger(tail) || tail < 0) {
21579
+ throw new Error("--tail must be a whole number of lines, 0 or more.");
21580
+ }
21581
+ }
21582
+ function buildComposeBaseArgs(instance, projectDirectory, composeFiles) {
21583
+ const args = ["compose"];
21584
+ if (instance.envFile) {
21585
+ args.push("--env-file", resolve5(projectDirectory, instance.envFile));
21586
+ }
21587
+ for (const composeFile of composeFiles) {
21588
+ args.push("-f", resolve5(projectDirectory, composeFile));
21589
+ }
21590
+ for (const profile of instance.composeProfiles ?? []) {
21591
+ args.push("--profile", profile);
21592
+ }
21593
+ if (instance.projectName) {
21594
+ args.push("--project-name", instance.projectName);
21595
+ }
21596
+ return args;
21597
+ }
21598
+ function buildComposePsArgs() {
21599
+ return ["ps"];
21600
+ }
21601
+ function buildComposeListServicesArgs() {
21602
+ return ["config", "--services"];
21603
+ }
21604
+ function buildComposeLogsArgs(service, options = {}) {
21605
+ const args = ["logs"];
21606
+ if (options.follow === true) {
21607
+ args.push("--follow");
21608
+ }
21609
+ if (options.tail !== void 0) {
21610
+ assertTail(options.tail);
21611
+ args.push("--tail", String(options.tail));
21612
+ }
21613
+ if (options.since !== void 0) {
21614
+ assertSince(options.since);
21615
+ args.push("--since", options.since);
21616
+ }
21617
+ if (service !== void 0) {
21618
+ assertServiceName(service);
21619
+ args.push(service);
21620
+ }
21621
+ return args;
21622
+ }
21623
+ function buildComposePullArgs(service) {
21624
+ const args = ["pull"];
21625
+ if (service !== void 0) {
21626
+ assertServiceName(service);
21627
+ args.push(service);
21628
+ }
21629
+ return args;
21630
+ }
21631
+ function buildComposeUpArgs() {
21632
+ return ["up", "-d"];
21633
+ }
21634
+ function buildComposeRestartArgs(service) {
21635
+ const args = ["restart"];
21636
+ if (service !== void 0) {
21637
+ assertServiceName(service);
21638
+ args.push(service);
21639
+ }
21640
+ return args;
21641
+ }
21642
+ function parseServiceList(stdout) {
21643
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => SERVICE_NAME.test(line)).sort();
21644
+ }
21645
+ function assertKnownService(service, known) {
21646
+ if (!known.includes(service)) {
21647
+ throw new Error(
21648
+ `Unknown service ${service}. Services in this project: ${known.join(", ") || "none"}.`
21649
+ );
21650
+ }
21651
+ }
21652
+
21653
+ // src/services/deployment-drivers.ts
21555
21654
  var drivers = {
21556
21655
  compose: {
21557
21656
  target: "compose",
@@ -21567,7 +21666,7 @@ var drivers = {
21567
21666
  for (const composeFile of getComposeFiles(instance)) {
21568
21667
  let found = true;
21569
21668
  try {
21570
- await access2(resolve5(projectDirectory, composeFile));
21669
+ await access2(resolve6(projectDirectory, composeFile));
21571
21670
  } catch {
21572
21671
  found = false;
21573
21672
  }
@@ -21585,8 +21684,41 @@ var drivers = {
21585
21684
  down(options) {
21586
21685
  return runCompose(["down", ...options.args], options);
21587
21686
  },
21588
- logs(options) {
21589
- return runCompose(["logs", "-f", ...options.args], options);
21687
+ async logs(options) {
21688
+ const service = await resolveComposeService(options);
21689
+ const { follow, tail } = readLogOptions(options.flags);
21690
+ const since = typeof options.flags.since === "string" ? options.flags.since : void 0;
21691
+ return runCompose(
21692
+ [
21693
+ ...buildComposeLogsArgs(service, { follow, tail, since }),
21694
+ ...options.args
21695
+ ],
21696
+ options
21697
+ );
21698
+ },
21699
+ async ps(options) {
21700
+ return runCompose(
21701
+ [...buildComposePsArgs(), ...options.args],
21702
+ options
21703
+ );
21704
+ },
21705
+ async pull(options) {
21706
+ const service = await resolveComposeService(options);
21707
+ return runCompose(
21708
+ [...buildComposePullArgs(service), ...options.args],
21709
+ options
21710
+ );
21711
+ },
21712
+ async restart(options) {
21713
+ assertWritable(options);
21714
+ const service = await resolveComposeService(options);
21715
+ const restartArgs = buildComposeRestartArgs(service);
21716
+ const runtime = await resolveComposeRuntime(options.context.env);
21717
+ options.context.stdout.write(
21718
+ `Restarting ${service ?? "all services"} of ${options.instanceName}${runtime ? ` with ${runtime.name} compose` : ""}
21719
+ `
21720
+ );
21721
+ return runCompose([...restartArgs, ...options.args], options);
21590
21722
  }
21591
21723
  },
21592
21724
  external: {
@@ -21606,7 +21738,11 @@ var drivers = {
21606
21738
  );
21607
21739
  return runKubectl(
21608
21740
  [
21609
- ...buildLogsArgs(scope, workload, readLogOptions(options.flags)),
21741
+ ...buildLogsArgs(
21742
+ scope,
21743
+ workload,
21744
+ readLogOptions(options.flags)
21745
+ ),
21610
21746
  ...options.args
21611
21747
  ],
21612
21748
  options
@@ -21634,14 +21770,17 @@ var drivers = {
21634
21770
  [...buildRestartArgs(scope, workload), ...options.args],
21635
21771
  options
21636
21772
  );
21637
- if (restarted !== 0 || options.flags.wait === false) {
21773
+ if (restarted !== 0 || options.flags["no-wait"] === true) {
21638
21774
  return restarted;
21639
21775
  }
21640
21776
  return runKubectl(buildRolloutStatusArgs(scope, workload), options);
21641
21777
  }
21642
21778
  }
21643
21779
  };
21644
- function assertWritable({ instance, instanceName }) {
21780
+ function assertWritable({
21781
+ instance,
21782
+ instanceName
21783
+ }) {
21645
21784
  if (instance.readOnly === true) {
21646
21785
  throw new Error(
21647
21786
  `Instance ${instanceName} is registered read-only. Re-register it without --read-only to allow changes.`
@@ -21847,7 +21986,10 @@ function unreadyEndpoints(stdout) {
21847
21986
  (endpoint) => isRecord3(endpoint) && // An omitted ready condition means ready, per the API contract.
21848
21987
  (!isRecord3(endpoint.conditions) || endpoint.conditions.ready !== false)
21849
21988
  );
21850
- readyByService.set(service, (readyByService.get(service) ?? false) || ready);
21989
+ readyByService.set(
21990
+ service,
21991
+ (readyByService.get(service) ?? false) || ready
21992
+ );
21851
21993
  }
21852
21994
  return [...readyByService.entries()].filter(([, ready]) => !ready).map(([service]) => service).sort();
21853
21995
  }
@@ -21975,7 +22117,7 @@ async function ensureComposeProject(cwd, options) {
21975
22117
  composeProfiles: composeProfiles(options),
21976
22118
  envFile: envFileName,
21977
22119
  projectName: mode === "demo" ? "eudiplo-demo" : "eudiplo",
21978
- projectDirectory: resolve5(cwd)
22120
+ projectDirectory: resolve6(cwd)
21979
22121
  };
21980
22122
  }
21981
22123
  async function ensureProjectDirectory(directory) {
@@ -22029,23 +22171,14 @@ function unsupportedCommand(command, target) {
22029
22171
  }
22030
22172
  async function runCompose(args, { instance, context }) {
22031
22173
  const projectDirectory = instance.projectDirectory ?? context.cwd;
22032
- const composeArgs = ["compose"];
22033
- if (instance.envFile) {
22034
- composeArgs.push(
22035
- "--env-file",
22036
- resolve5(projectDirectory, instance.envFile)
22037
- );
22038
- }
22039
- for (const composeFile of getComposeFiles(instance)) {
22040
- composeArgs.push("-f", resolve5(projectDirectory, composeFile));
22041
- }
22042
- for (const profile of instance.composeProfiles ?? []) {
22043
- composeArgs.push("--profile", profile);
22044
- }
22045
- if (instance.projectName) {
22046
- composeArgs.push("--project-name", instance.projectName);
22047
- }
22048
- composeArgs.push(...args);
22174
+ const composeArgs = [
22175
+ ...buildComposeBaseArgs(
22176
+ instance,
22177
+ projectDirectory,
22178
+ getComposeFiles(instance)
22179
+ ),
22180
+ ...args
22181
+ ];
22049
22182
  const composeRuntime = await resolveComposeRuntime(context.env);
22050
22183
  if (!composeRuntime) {
22051
22184
  context.stderr.write(
@@ -22067,6 +22200,64 @@ async function runCompose(args, { instance, context }) {
22067
22200
  child.on("close", (code) => resolveProcess(code ?? 1));
22068
22201
  });
22069
22202
  }
22203
+ async function captureCompose(args, { instance, context }) {
22204
+ const projectDirectory = instance.projectDirectory ?? context.cwd;
22205
+ const composeRuntime = await resolveComposeRuntime(context.env);
22206
+ if (!composeRuntime) {
22207
+ throw new Error(
22208
+ "Docker or Podman was not found in a supported install location."
22209
+ );
22210
+ }
22211
+ const composeArgs = [
22212
+ ...buildComposeBaseArgs(
22213
+ instance,
22214
+ projectDirectory,
22215
+ getComposeFiles(instance)
22216
+ ),
22217
+ ...args
22218
+ ];
22219
+ return new Promise((resolveProcess) => {
22220
+ const child = spawn(composeRuntime.command, composeArgs, {
22221
+ cwd: projectDirectory,
22222
+ env: context.env,
22223
+ stdio: ["ignore", "pipe", "pipe"]
22224
+ });
22225
+ let stdout = "";
22226
+ let stderr = "";
22227
+ child.stdout?.on("data", (chunk) => {
22228
+ stdout += String(chunk);
22229
+ });
22230
+ child.stderr?.on("data", (chunk) => {
22231
+ stderr += String(chunk);
22232
+ });
22233
+ child.on(
22234
+ "error",
22235
+ (error) => resolveProcess({ code: 1, stdout, stderr: error.message })
22236
+ );
22237
+ child.on(
22238
+ "close",
22239
+ (code) => resolveProcess({ code: code ?? 1, stdout, stderr })
22240
+ );
22241
+ });
22242
+ }
22243
+ async function resolveComposeService(options) {
22244
+ const service = readServiceFlag(options.flags);
22245
+ if (service === void 0) {
22246
+ return void 0;
22247
+ }
22248
+ assertServiceName(service);
22249
+ const listed = await captureCompose(
22250
+ buildComposeListServicesArgs(),
22251
+ options
22252
+ );
22253
+ if (listed.code !== 0) {
22254
+ throw new Error(
22255
+ `Could not list Compose services: ${listed.stderr.trim() || `exit code ${listed.code}`}`
22256
+ );
22257
+ }
22258
+ assertKnownService(service, parseServiceList(listed.stdout));
22259
+ return service;
22260
+ }
22070
22261
  async function resolveComposeRuntime(env) {
22071
22262
  const preferredRuntime = parsePreferredRuntime(
22072
22263
  env.EUDIPLO_CONTAINER_RUNTIME
@@ -22164,7 +22355,7 @@ async function removeIfExists(path2) {
22164
22355
  }
22165
22356
 
22166
22357
  // src/services/project-directory.ts
22167
- import { resolve as resolve6 } from "node:path";
22358
+ import { resolve as resolve7 } from "node:path";
22168
22359
  function resolveProjectDirectory(parsed, context, promptedDirectory) {
22169
22360
  const positionalDirectory = parsed.subject;
22170
22361
  const flagDirectory = readStringFlag(parsed.flags, "directory");
@@ -22176,7 +22367,7 @@ function resolveProjectDirectory(parsed, context, promptedDirectory) {
22176
22367
  "Specify the project directory either positionally or with --directory."
22177
22368
  );
22178
22369
  }
22179
- return resolve6(
22370
+ return resolve7(
22180
22371
  context.cwd,
22181
22372
  positionalDirectory ?? flagDirectory ?? promptedDirectory ?? "."
22182
22373
  );
@@ -22302,45 +22493,52 @@ var descriptions = {
22302
22493
  down: "Stop the selected Compose deployment",
22303
22494
  logs: "Stream logs for the selected deployment",
22304
22495
  ps: "List the running containers or pods for the selected deployment",
22305
- restart: "Restart the workloads of the selected deployment"
22496
+ restart: "Restart the workloads of the selected deployment",
22497
+ pull: "Download the configured images without changing configuration"
22306
22498
  };
22307
22499
  var serviceScoped = /* @__PURE__ */ new Set([
22308
22500
  "logs",
22309
- "restart"
22501
+ "restart",
22502
+ "pull"
22310
22503
  ]);
22311
22504
  function createDeploymentCommands(context, setExitCode) {
22312
- return ["up", "down", "logs", "ps", "restart"].map((name) => {
22313
- const command = new Command(name).description(descriptions[name]).argument(
22314
- "[args...]",
22315
- "additional arguments passed to the deployment runtime"
22316
- ).option("--instance <name>", "select a configured instance");
22317
- if (serviceScoped.has(name)) {
22318
- command.option(
22319
- "--service <name>",
22320
- "select a configured workload (Kubernetes instances)"
22321
- );
22322
- }
22323
- if (name === "logs") {
22324
- command.option("--follow", "stream new output as it arrives").option("--tail <lines>", "number of recent lines to show");
22325
- }
22326
- if (name === "restart") {
22327
- command.option(
22328
- "--no-wait",
22329
- "return without waiting for the rollout to finish"
22330
- );
22505
+ return ["up", "down", "logs", "ps", "restart", "pull"].map(
22506
+ (name) => {
22507
+ const command = new Command(name).description(descriptions[name]).argument(
22508
+ "[args...]",
22509
+ "additional arguments passed to the deployment runtime"
22510
+ ).option("--instance <name>", "select a configured instance");
22511
+ if (serviceScoped.has(name)) {
22512
+ command.option(
22513
+ "--service <name>",
22514
+ "select a single service (Compose) or configured workload (Kubernetes)"
22515
+ );
22516
+ }
22517
+ if (name === "logs") {
22518
+ command.option("--follow", "stream new output as it arrives").option("--tail <lines>", "number of recent lines to show").option(
22519
+ "--since <time>",
22520
+ "show logs since a duration (10m, 2h) or timestamp (Compose instances)"
22521
+ );
22522
+ }
22523
+ if (name === "restart") {
22524
+ command.option(
22525
+ "--no-wait",
22526
+ "return without waiting for the rollout to finish (Kubernetes instances)"
22527
+ );
22528
+ }
22529
+ return command.action(async (args, options) => {
22530
+ const { config } = await loadCliState(context);
22531
+ setExitCode(
22532
+ await runDriverCommand(
22533
+ config,
22534
+ name,
22535
+ parsedArgs(name, void 0, args, options),
22536
+ context
22537
+ )
22538
+ );
22539
+ });
22331
22540
  }
22332
- return command.action(async (args, options) => {
22333
- const { config } = await loadCliState(context);
22334
- setExitCode(
22335
- await runDriverCommand(
22336
- config,
22337
- name,
22338
- parsedArgs(name, void 0, args, options),
22339
- context
22340
- )
22341
- );
22342
- });
22343
- });
22541
+ );
22344
22542
  }
22345
22543
 
22346
22544
  // src/services/diagnostics.ts
@@ -23020,6 +23218,146 @@ function createInstanceCommand(context, setExitCode) {
23020
23218
  return instance;
23021
23219
  }
23022
23220
 
23221
+ // src/services/browser.ts
23222
+ import { spawn as spawn2 } from "node:child_process";
23223
+ import { access as access3 } from "node:fs/promises";
23224
+ import { delimiter as delimiter2, join as join11 } from "node:path";
23225
+ function assertWebUrl(value) {
23226
+ let url;
23227
+ try {
23228
+ url = new URL(value);
23229
+ } catch {
23230
+ throw new Error(`${value} is not a valid absolute URL.`);
23231
+ }
23232
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
23233
+ throw new Error(
23234
+ `Refusing to open ${value}: only http and https URLs are supported.`
23235
+ );
23236
+ }
23237
+ return url;
23238
+ }
23239
+ async function chooseLauncher(url, { platform, env, which }) {
23240
+ const browser = env.BROWSER?.trim();
23241
+ if (browser && !/\s|%s/.test(browser)) {
23242
+ const command = await which(browser);
23243
+ if (command) {
23244
+ return { command, args: [url] };
23245
+ }
23246
+ }
23247
+ if (platform === "darwin") {
23248
+ return { command: "open", args: [url] };
23249
+ }
23250
+ if (platform === "win32") {
23251
+ return {
23252
+ command: "rundll32.exe",
23253
+ args: ["url.dll,FileProtocolHandler", url]
23254
+ };
23255
+ }
23256
+ if (env.WSL_DISTRO_NAME) {
23257
+ const wslview = await which("wslview");
23258
+ if (wslview) {
23259
+ return { command: wslview, args: [url] };
23260
+ }
23261
+ }
23262
+ if (env.DISPLAY || env.WAYLAND_DISPLAY) {
23263
+ const xdgOpen = await which("xdg-open");
23264
+ if (xdgOpen) {
23265
+ return { command: xdgOpen, args: [url] };
23266
+ }
23267
+ }
23268
+ return void 0;
23269
+ }
23270
+ function launchBrowser(launcher) {
23271
+ return new Promise((resolveLaunch) => {
23272
+ const child = spawn2(launcher.command, launcher.args, {
23273
+ detached: true,
23274
+ stdio: "ignore"
23275
+ });
23276
+ child.once("error", () => resolveLaunch(false));
23277
+ child.once("spawn", () => {
23278
+ child.unref();
23279
+ resolveLaunch(true);
23280
+ });
23281
+ });
23282
+ }
23283
+ function pathLookup(env, platform = process.platform) {
23284
+ const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
23285
+ return async (name) => {
23286
+ const entries = (env.PATH ?? "").split(delimiter2).filter(Boolean);
23287
+ for (const entry of entries) {
23288
+ for (const extension of extensions) {
23289
+ const candidate = join11(entry, `${name}${extension}`);
23290
+ try {
23291
+ await access3(candidate);
23292
+ return candidate;
23293
+ } catch {
23294
+ }
23295
+ }
23296
+ }
23297
+ return void 0;
23298
+ };
23299
+ }
23300
+
23301
+ // src/commands/open/action.ts
23302
+ var defaultDependencies = {
23303
+ platform: process.platform,
23304
+ launch: launchBrowser
23305
+ };
23306
+ function resolveOpenTarget(instanceName, instance, docs) {
23307
+ if (docs) {
23308
+ const base = instance.url.endsWith("/") ? instance.url : `${instance.url}/`;
23309
+ return assertWebUrl(new URL("api/docs", base).href).href;
23310
+ }
23311
+ if (!instance.clientUrl) {
23312
+ throw new Error(
23313
+ `Instance ${instanceName} has no web client URL. Use --docs to open the API documentation instead.`
23314
+ );
23315
+ }
23316
+ return assertWebUrl(instance.clientUrl).href;
23317
+ }
23318
+ async function runOpenCommand(config, parsed, context, dependencies = defaultDependencies) {
23319
+ const [instanceName, instance] = resolveInstance(config, parsed);
23320
+ const url = resolveOpenTarget(
23321
+ instanceName,
23322
+ instance,
23323
+ parsed.flags.docs === true
23324
+ );
23325
+ if (parsed.flags.print === true || context.interactive !== true) {
23326
+ context.stdout.write(`${url}
23327
+ `);
23328
+ return 0;
23329
+ }
23330
+ const launcher = await chooseLauncher(url, {
23331
+ platform: dependencies.platform,
23332
+ env: context.env,
23333
+ which: pathLookup(context.env, dependencies.platform)
23334
+ });
23335
+ if (!launcher || !await dependencies.launch(launcher)) {
23336
+ context.stdout.write(`No browser is available here. Open: ${url}
23337
+ `);
23338
+ return 0;
23339
+ }
23340
+ context.stdout.write(`Opening ${url}
23341
+ `);
23342
+ return 0;
23343
+ }
23344
+
23345
+ // src/commands/open/index.ts
23346
+ function createOpenCommand(context, setExitCode) {
23347
+ return new Command("open").description(
23348
+ "Open the web client of the selected instance in a browser"
23349
+ ).option("--instance <name>", "select a configured instance").option("--docs", "open the management API documentation instead").option("--print", "print the URL instead of opening a browser").action(async (options) => {
23350
+ const { config } = await loadCliState(context);
23351
+ setExitCode(
23352
+ await runOpenCommand(
23353
+ config,
23354
+ parsedArgs("open", void 0, [], options),
23355
+ context
23356
+ )
23357
+ );
23358
+ });
23359
+ }
23360
+
23023
23361
  // src/commands/status/action.ts
23024
23362
  async function runStatusCommand(config, parsed, context) {
23025
23363
  const [instanceName, instance] = resolveInstance(config, parsed);
@@ -23052,13 +23390,229 @@ function createStatusCommand(context, setExitCode) {
23052
23390
  });
23053
23391
  }
23054
23392
 
23393
+ // src/commands/upgrade/action.ts
23394
+ import { readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
23395
+ import { resolve as resolve8 } from "node:path";
23396
+
23397
+ // src/services/compose-upgrade.ts
23398
+ var managedImages = {
23399
+ EUDIPLO_IMAGE: "ghcr.io/openwallet-foundation/eudiplo",
23400
+ EUDIPLO_CLIENT_IMAGE: "ghcr.io/openwallet-foundation/eudiplo-client"
23401
+ };
23402
+ var TAG = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
23403
+ function assertImageTag(tag) {
23404
+ if (!TAG.test(tag)) {
23405
+ throw new Error(
23406
+ `Invalid image tag ${JSON.stringify(tag)}. Tags contain letters, digits, "_", "." and "-", and cannot start with "." or "-".`
23407
+ );
23408
+ }
23409
+ }
23410
+ function planImageUpgrade(content, tag) {
23411
+ assertImageTag(tag);
23412
+ const found = /* @__PURE__ */ new Set();
23413
+ const changes = [];
23414
+ const lines = content.split("\n").map((line) => {
23415
+ const match = /^(EUDIPLO_IMAGE|EUDIPLO_CLIENT_IMAGE)=(.*?)(\r?)$/.exec(
23416
+ line
23417
+ );
23418
+ if (!match) {
23419
+ return line;
23420
+ }
23421
+ const key = match[1];
23422
+ const value = match[2];
23423
+ const lineEnding = match[3];
23424
+ const repository = managedImages[key];
23425
+ if (found.has(key)) {
23426
+ throw new Error(
23427
+ `${key} is defined more than once in the env file.`
23428
+ );
23429
+ }
23430
+ found.add(key);
23431
+ const prefix = `${repository}:`;
23432
+ const current = value.startsWith(prefix) ? value.slice(prefix.length) : void 0;
23433
+ if (current === void 0 || !TAG.test(current)) {
23434
+ throw new Error(
23435
+ `${key} is set to ${value}, which is not a CLI-managed image. Update it yourself, or restore ${prefix}<tag> to let the CLI manage it.`
23436
+ );
23437
+ }
23438
+ if (current !== tag) {
23439
+ changes.push({ key, repository, from: current, to: tag });
23440
+ }
23441
+ return `${key}=${prefix}${tag}${lineEnding}`;
23442
+ });
23443
+ if (!found.has("EUDIPLO_IMAGE")) {
23444
+ throw new Error(
23445
+ "The env file does not define EUDIPLO_IMAGE, so there is no CLI-managed image to upgrade."
23446
+ );
23447
+ }
23448
+ return { changes, content: lines.join("\n") };
23449
+ }
23450
+ function parseVersion(tag) {
23451
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
23452
+ if (!match) {
23453
+ return void 0;
23454
+ }
23455
+ return {
23456
+ major: Number(match[1]),
23457
+ minor: Number(match[2]),
23458
+ patch: Number(match[3])
23459
+ };
23460
+ }
23461
+ function compareVersions(a, b) {
23462
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
23463
+ }
23464
+ var migrationBaseUrl = "https://docs.eudiplo.dev/migration";
23465
+ function migrationNotes(from, to) {
23466
+ const fromVersion = parseVersion(from);
23467
+ const toVersion = parseVersion(to);
23468
+ if (!fromVersion || !toVersion) {
23469
+ return [
23470
+ `Cannot compare ${from} and ${to} as versions. Check the release notes and ${migrationBaseUrl} before continuing.`
23471
+ ];
23472
+ }
23473
+ const order = compareVersions(fromVersion, toVersion);
23474
+ if (order > 0) {
23475
+ return [
23476
+ `${to} is older than ${from}. Downgrades are not supported once database migrations have run; restore a backup instead if you need to go back.`
23477
+ ];
23478
+ }
23479
+ const notes = [];
23480
+ for (let major = fromVersion.major + 1; major <= toVersion.major; major++) {
23481
+ notes.push(
23482
+ `Major version ${major}: read ${migrationBaseUrl}/${major - 1}.x-to-${major}.0 first.`
23483
+ );
23484
+ }
23485
+ return notes;
23486
+ }
23487
+
23488
+ // src/commands/upgrade/action.ts
23489
+ async function runUpgradeCommand(config, parsed, context) {
23490
+ const [instanceName, instance] = resolveInstance(config, parsed);
23491
+ if (instance.target !== "compose") {
23492
+ throw new Error(unsupportedCommand("upgrade", instance.target));
23493
+ }
23494
+ const options = {
23495
+ instanceName,
23496
+ instance,
23497
+ args: [],
23498
+ flags: parsed.flags,
23499
+ context
23500
+ };
23501
+ assertWritable(options);
23502
+ const tag = readStringFlag(parsed.flags, "image-tag");
23503
+ if (!tag) {
23504
+ throw new Error("upgrade requires --image-tag <tag>.");
23505
+ }
23506
+ if (!instance.envFile) {
23507
+ throw new Error(
23508
+ `Instance ${instanceName} has no env file, so its images are not managed by the CLI.`
23509
+ );
23510
+ }
23511
+ const envPath = resolve8(
23512
+ instance.projectDirectory ?? context.cwd,
23513
+ instance.envFile
23514
+ );
23515
+ const original = await readEnvFile(envPath);
23516
+ const plan = planImageUpgrade(original, tag);
23517
+ if (plan.changes.length === 0) {
23518
+ context.stdout.write(
23519
+ `${instanceName} already uses image tag ${tag}.
23520
+ `
23521
+ );
23522
+ return 0;
23523
+ }
23524
+ context.stdout.write(`Upgrade ${instanceName}:
23525
+ `);
23526
+ for (const change of plan.changes) {
23527
+ context.stdout.write(
23528
+ ` ${change.repository}: ${change.from} -> ${change.to}
23529
+ `
23530
+ );
23531
+ }
23532
+ const notes = new Set(
23533
+ plan.changes.flatMap(
23534
+ (change) => migrationNotes(change.from, change.to)
23535
+ )
23536
+ );
23537
+ for (const note of notes) {
23538
+ context.stdout.write(`Note: ${note}
23539
+ `);
23540
+ }
23541
+ if (!await confirmUpgrade(parsed, context)) {
23542
+ context.stdout.write("Upgrade cancelled. Nothing was changed.\n");
23543
+ return 0;
23544
+ }
23545
+ await writeFile6(envPath, plan.content, "utf8");
23546
+ const pulled = await runCompose(buildComposePullArgs(void 0), options);
23547
+ if (pulled !== 0) {
23548
+ await writeFile6(envPath, original, "utf8");
23549
+ context.stderr.write(
23550
+ `Pulling the new images failed. Restored the previous image tags in ${envPath}.
23551
+ `
23552
+ );
23553
+ return pulled;
23554
+ }
23555
+ const recreated = await runCompose(buildComposeUpArgs(), options);
23556
+ if (recreated !== 0) {
23557
+ context.stderr.write(
23558
+ `The new images were pulled and ${envPath} now uses ${tag}, but recreating the services failed. Fix the error above and run eudiplo up.
23559
+ `
23560
+ );
23561
+ return recreated;
23562
+ }
23563
+ context.stdout.write(`Upgraded ${instanceName} to ${tag}.
23564
+ `);
23565
+ return 0;
23566
+ }
23567
+ async function readEnvFile(path2) {
23568
+ try {
23569
+ return await readFile9(path2, "utf8");
23570
+ } catch (error) {
23571
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
23572
+ throw new Error(`Env file not found: ${path2}`);
23573
+ }
23574
+ throw error;
23575
+ }
23576
+ }
23577
+ async function confirmUpgrade(parsed, context) {
23578
+ if (parsed.flags.yes === true) {
23579
+ return true;
23580
+ }
23581
+ if (context.interactive !== true) {
23582
+ throw new Error("upgrade requires --yes in non-interactive mode.");
23583
+ }
23584
+ const prompter = createPrompter(context);
23585
+ try {
23586
+ const answer = (await prompter.ask("Continue? [y/N]: ")).trim().toLowerCase();
23587
+ return answer === "y" || answer === "yes";
23588
+ } finally {
23589
+ prompter.close();
23590
+ }
23591
+ }
23592
+
23593
+ // src/commands/upgrade/index.ts
23594
+ function createUpgradeCommand(context, setExitCode) {
23595
+ return new Command("upgrade").description(
23596
+ "Upgrade the EUDIPLO application images of a Compose deployment"
23597
+ ).requiredOption("--image-tag <tag>", "image tag to upgrade to").option("--instance <name>", "select a configured instance").option("--yes", "upgrade without asking for confirmation").action(async (options) => {
23598
+ const { config } = await loadCliState(context);
23599
+ setExitCode(
23600
+ await runUpgradeCommand(
23601
+ config,
23602
+ parsedArgs("upgrade", void 0, [], options),
23603
+ context
23604
+ )
23605
+ );
23606
+ });
23607
+ }
23608
+
23055
23609
  // src/commands/version/action.ts
23056
23610
  import { isSea as isSea2 } from "node:sea";
23057
23611
 
23058
23612
  // package.json
23059
23613
  var package_default = {
23060
23614
  name: "@eudiplo/cli",
23061
- version: "8.0.2-main.3b6a116",
23615
+ version: "8.0.2-main.a9f319d",
23062
23616
  description: "Deployment-aware command line tools for EUDIPLO",
23063
23617
  private: false,
23064
23618
  type: "module",
@@ -23232,6 +23786,8 @@ function createProgram(context, setExitCode) {
23232
23786
  for (const command of createDeploymentCommands(context, setExitCode)) {
23233
23787
  program2.addCommand(command);
23234
23788
  }
23789
+ program2.addCommand(createUpgradeCommand(context, setExitCode));
23790
+ program2.addCommand(createOpenCommand(context, setExitCode));
23235
23791
  program2.addCommand(createInstanceCommand(context, setExitCode));
23236
23792
  program2.addCommand(createConfigCommand(context, setExitCode));
23237
23793
  program2.addCommand(createDoctorCommand(context, setExitCode));