@flui-cloud/cli 0.0.1 → 0.2.0

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 (108) hide show
  1. package/lib/cli/src/commands/app/list.d.ts +3 -0
  2. package/lib/cli/src/commands/app/list.js +72 -18
  3. package/lib/cli/src/commands/app/status.d.ts +1 -0
  4. package/lib/cli/src/commands/app/status.js +27 -2
  5. package/lib/cli/src/commands/cluster/destroy.d.ts +1 -1
  6. package/lib/cli/src/commands/cluster/destroy.js +2 -2
  7. package/lib/cli/src/commands/deploy.d.ts +3 -0
  8. package/lib/cli/src/commands/deploy.js +19 -0
  9. package/lib/cli/src/commands/dev/creds.d.ts +0 -1
  10. package/lib/cli/src/commands/dev/creds.js +6 -27
  11. package/lib/cli/src/commands/dev/tunnel.js +8 -8
  12. package/lib/cli/src/commands/env/capacity.js +4 -4
  13. package/lib/cli/src/commands/env/create.d.ts +4 -1
  14. package/lib/cli/src/commands/env/create.js +78 -52
  15. package/lib/cli/src/commands/env/credentials.js +12 -12
  16. package/lib/cli/src/commands/env/destroy.d.ts +2 -1
  17. package/lib/cli/src/commands/env/destroy.js +45 -28
  18. package/lib/cli/src/commands/env/diag-ca.js +5 -5
  19. package/lib/cli/src/commands/env/export-config.d.ts +0 -17
  20. package/lib/cli/src/commands/env/export-config.js +50 -47
  21. package/lib/cli/src/commands/env/force-ready.d.ts +1 -1
  22. package/lib/cli/src/commands/env/force-ready.js +8 -8
  23. package/lib/cli/src/commands/env/inspect.js +5 -5
  24. package/lib/cli/src/commands/env/refresh-kubeconfig.js +4 -4
  25. package/lib/cli/src/commands/env/repair-ssh-ca.js +4 -4
  26. package/lib/cli/src/commands/env/repair-storage.d.ts +9 -0
  27. package/lib/cli/src/commands/env/repair-storage.js +82 -0
  28. package/lib/cli/src/commands/env/restart.d.ts +1 -1
  29. package/lib/cli/src/commands/env/restart.js +9 -9
  30. package/lib/cli/src/commands/env/scale-master.js +4 -4
  31. package/lib/cli/src/commands/env/scale-node.js +4 -4
  32. package/lib/cli/src/commands/env/set-master-protection.d.ts +16 -0
  33. package/lib/cli/src/commands/env/set-master-protection.js +120 -0
  34. package/lib/cli/src/commands/env/status.d.ts +1 -1
  35. package/lib/cli/src/commands/env/status.js +10 -10
  36. package/lib/cli/src/commands/env/stop.d.ts +1 -1
  37. package/lib/cli/src/commands/env/stop.js +8 -8
  38. package/lib/cli/src/commands/env/storage-expand.js +4 -4
  39. package/lib/cli/src/commands/env/storage.d.ts +1 -1
  40. package/lib/cli/src/commands/env/storage.js +5 -5
  41. package/lib/cli/src/commands/env/sync.js +5 -5
  42. package/lib/cli/src/commands/env/uncordon.js +4 -4
  43. package/lib/cli/src/commands/env/update-firewall.d.ts +13 -1
  44. package/lib/cli/src/commands/env/update-firewall.js +232 -126
  45. package/lib/cli/src/commands/integration/connect.d.ts +1 -0
  46. package/lib/cli/src/commands/integration/connect.js +19 -1
  47. package/lib/cli/src/commands/integration/reset.d.ts +13 -0
  48. package/lib/cli/src/commands/integration/reset.js +95 -0
  49. package/lib/cli/src/commands/integration/setup.d.ts +18 -0
  50. package/lib/cli/src/commands/integration/setup.js +320 -0
  51. package/lib/cli/src/commands/integration/status.d.ts +9 -0
  52. package/lib/cli/src/commands/integration/status.js +117 -0
  53. package/lib/cli/src/commands/node/list.d.ts +1 -0
  54. package/lib/cli/src/commands/node/list.js +19 -2
  55. package/lib/cli/src/commands/server-types/list.d.ts +3 -0
  56. package/lib/cli/src/commands/server-types/list.js +84 -0
  57. package/lib/cli/src/commands/ssh.js +5 -5
  58. package/lib/cli/src/commands/version.d.ts +18 -0
  59. package/lib/cli/src/commands/version.js +100 -0
  60. package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
  61. package/lib/cli/src/config/bootstrap.config.js +24 -4
  62. package/lib/cli/src/config/preferences-schema.js +5 -5
  63. package/lib/cli/src/config/release-override.d.ts +43 -0
  64. package/lib/cli/src/config/release-override.js +203 -0
  65. package/lib/cli/src/config/release.config.d.ts +31 -0
  66. package/lib/cli/src/config/release.config.js +38 -0
  67. package/lib/cli/src/lib/prompts.d.ts +1 -6
  68. package/lib/cli/src/lib/prompts.js +33 -13
  69. package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
  70. package/lib/cli/src/lib/services/cli-app.service.js +9 -0
  71. package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
  72. package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
  73. package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
  74. package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
  75. package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
  76. package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
  77. package/lib/cli/src/services/cli-clusters.service.js +57 -34
  78. package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
  79. package/lib/cli/src/services/cli-control-cluster.service.js +545 -0
  80. package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
  81. package/lib/cli/src/services/cli-endpoint-resolver.service.js +25 -11
  82. package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
  83. package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
  84. package/lib/src/config/release.config.d.ts +28 -0
  85. package/lib/src/config/release.config.js +35 -0
  86. package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
  87. package/lib/src/modules/applications/entities/application.entity.js +12 -0
  88. package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
  89. package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
  90. package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
  91. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
  92. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
  93. package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
  94. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
  95. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
  96. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
  97. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
  98. package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
  99. package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
  100. package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
  101. package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
  102. package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
  103. package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
  104. package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
  105. package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
  106. package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
  107. package/oclif.manifest.json +1201 -854
  108. package/package.json +2 -2
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_crypto_1 = require("node:crypto");
6
7
  const core_1 = require("@oclif/core");
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const ora_1 = __importDefault(require("ora"));
9
10
  const nest_app_1 = require("../../lib/nest-app");
10
11
  const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
11
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
12
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
12
13
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
13
14
  const cloud_provider_enum_1 = require("../../../../src/modules/providers/enums/cloud-provider.enum");
14
15
  const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
@@ -21,6 +22,7 @@ const api_client_1 = require("../../lib/api-client");
21
22
  const config_storage_1 = require("../../lib/config-storage");
22
23
  const provider_credential_schemas_1 = require("../../lib/provider-credential-schemas");
23
24
  const context_banner_1 = require("../../lib/context-banner");
25
+ const release_override_1 = require("../../config/release-override");
24
26
  const server_type_cache_service_1 = require("../../services/server-type-cache.service");
25
27
  const server_type_validator_service_1 = require("../../services/server-type-validator.service");
26
28
  const defaults_1 = require("../../config/defaults");
@@ -28,9 +30,36 @@ const prompts_1 = require("../../lib/prompts");
28
30
  const preferences_resolver_1 = require("../../config/preferences-resolver");
29
31
  const preferences_schema_1 = require("../../config/preferences-schema");
30
32
  class EnvCreate extends core_1.Command {
33
+ /** The sole provider with configured credentials, or undefined if none/both. */
34
+ detectConfiguredProvider(configStorage) {
35
+ const configured = ['hetzner', 'scaleway'].filter((p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
36
+ ? configStorage.hasCredentials(p)
37
+ : configStorage.hasToken(p));
38
+ return configured.length === 1 ? configured[0] : undefined;
39
+ }
31
40
  async run() {
32
41
  const { flags } = await this.parse(EnvCreate);
33
- const providerKey = flags.provider;
42
+ if (flags['auth-mode'] !== 'oidc') {
43
+ this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
44
+ }
45
+ const configStorage = new config_storage_1.ConfigStorage();
46
+ const hasCreds = (p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
47
+ ? configStorage.hasCredentials(p)
48
+ : configStorage.hasToken(p);
49
+ // Resolve the target provider BEFORE deriving provider-specific defaults.
50
+ // Precedence: explicit --provider > the profile's single configured
51
+ // provider > the setup wizard's choice. (Previously hard-defaulted to
52
+ // hetzner, so a Scaleway-only profile still tried — and failed — to build
53
+ // on Hetzner.)
54
+ let providerKey = flags.provider ?? this.detectConfiguredProvider(configStorage);
55
+ if (!providerKey || !hasCreds(providerKey)) {
56
+ const configured = await (0, prompts_1.runProviderSetupWizard)(providerKey);
57
+ if (!configured) {
58
+ console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey ?? '<provider>'}\n`));
59
+ this.exit(1);
60
+ }
61
+ providerKey = configured;
62
+ }
34
63
  const cloudProvider = providerKey === 'scaleway'
35
64
  ? cloud_provider_enum_1.CloudProvider.SCALEWAY
36
65
  : cloud_provider_enum_1.CloudProvider.HETZNER;
@@ -40,9 +69,6 @@ class EnvCreate extends core_1.Command {
40
69
  if (!allowedRegions.includes(region)) {
41
70
  this.error(`Region '${region}' is not supported for provider '${providerKey}'. Allowed: ${allowedRegions.join(', ')}`, { exit: 1 });
42
71
  }
43
- if (flags['auth-mode'] !== 'oidc') {
44
- this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
45
- }
46
72
  (0, context_banner_1.printContextBanner)({
47
73
  cluster: { provider: providerKey, region },
48
74
  });
@@ -55,9 +81,8 @@ class EnvCreate extends core_1.Command {
55
81
  app = await (0, nest_app_1.getNestApp)();
56
82
  spinner.succeed('Initialized');
57
83
  spinner = (0, ora_1.default)('Loading services...').start();
58
- const configStorage = new config_storage_1.ConfigStorage();
59
84
  const apiUrl = configStorage.getApiUrl();
60
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
85
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
61
86
  const apiClient = new api_client_1.ApiClient({
62
87
  baseUrl: apiUrl,
63
88
  apiKey: configStorage.getApiKey(),
@@ -65,20 +90,6 @@ class EnvCreate extends core_1.Command {
65
90
  const cacheService = new server_type_cache_service_1.ServerTypeCacheService();
66
91
  const validatorService = new server_type_validator_service_1.ServerTypeValidatorService();
67
92
  spinner.succeed('Services loaded');
68
- // 2. Early credential check — must happen before any provider call
69
- const hasProviderCreds = (0, provider_credential_schemas_1.isCompoundProvider)(providerKey)
70
- ? configStorage.hasCredentials(providerKey)
71
- : configStorage.hasToken(providerKey);
72
- if (!hasProviderCreds) {
73
- spinner.stop();
74
- const configured = await (0, prompts_1.runProviderSetupWizard)();
75
- if (!configured) {
76
- console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey}\n`));
77
- this.exit(1);
78
- }
79
- spinner = (0, ora_1.default)('Resuming...').start();
80
- spinner.succeed('Provider configured');
81
- }
82
93
  // 3. Resolve admin email (prompt if not set, then persist)
83
94
  const emailResolver = new preferences_resolver_1.PreferencesResolver(configStorage);
84
95
  const emailResult = emailResolver.resolve('email');
@@ -102,7 +113,12 @@ class EnvCreate extends core_1.Command {
102
113
  // server-side, so the LE 5-certs-per-7-days rate limit never trips on
103
114
  // repeated test creations.
104
115
  const acmeStaging = flags['acme-staging'];
116
+ const useLatest = flags.latest;
105
117
  spinner.stop();
118
+ const overrideBanner = (0, release_override_1.formatReleaseOverrideBanner)((0, release_override_1.getEffectiveRelease)(useLatest));
119
+ if (overrideBanner) {
120
+ console.log(overrideBanner);
121
+ }
106
122
  if (acmeStaging) {
107
123
  console.log(chalk_1.default.yellow(`\n⚠ ACME endpoint: Let's Encrypt STAGING — cert will not be browser-trusted (warning expected).\n`));
108
124
  }
@@ -231,17 +247,17 @@ class EnvCreate extends core_1.Command {
231
247
  spinner.succeed('Server type validation skipped (no data available)');
232
248
  }
233
249
  // Display cluster configuration
234
- console.log(chalk_1.default.cyan('\n🚀 Creating Flui Observability Cluster (K3s)\n'));
250
+ console.log(chalk_1.default.cyan('\n🚀 Creating Flui Control Cluster (K3s)\n'));
235
251
  console.log(chalk_1.default.dim(` Provider: ${providerKey}`));
236
252
  console.log(chalk_1.default.dim(` Node Size: ${validatedNodeSize}`));
237
253
  console.log(chalk_1.default.dim(` Region: ${validatedRegion}`));
238
254
  console.log(chalk_1.default.dim(` Worker Nodes: ${flags['node-count']}\n`));
239
- // 3. Check if observability cluster already exists
255
+ // 3. Check if control cluster already exists
240
256
  spinner = (0, ora_1.default)('Checking for existing cluster...').start();
241
- const existingCluster = await observabilityService.getObservabilityCluster();
257
+ const existingCluster = await controlService.getControlCluster();
242
258
  if (existingCluster && existingCluster.status !== cluster_entity_1.ClusterStatus.DELETED) {
243
- spinner.fail('Observability cluster already exists!');
244
- console.log(chalk_1.default.yellow('\n⚠️ An observability cluster is already running:\n'));
259
+ spinner.fail('Control cluster already exists!');
260
+ console.log(chalk_1.default.yellow('\n⚠️ An control cluster is already running:\n'));
245
261
  console.log(` ${chalk_1.default.bold('Name:')} ${existingCluster.name}`);
246
262
  console.log(` ${chalk_1.default.bold('ID:')} ${existingCluster.id}`);
247
263
  console.log(` ${chalk_1.default.bold('Status:')} ${existingCluster.status}`);
@@ -263,7 +279,7 @@ class EnvCreate extends core_1.Command {
263
279
  const vnetService = app.get(vnet_provisioning_service_1.VnetProvisioningService);
264
280
  envVnetInfo = await vnetService.ensureEnvVnet({
265
281
  provider: cloudProvider,
266
- name: 'flui-env-vnet',
282
+ name: `flui-env-${(0, node_crypto_1.randomBytes)(3).toString('hex')}`,
267
283
  ipRange: '10.10.0.0/16',
268
284
  subnetIpRange: '10.10.1.0/24',
269
285
  networkZone: cloudProvider === cloud_provider_enum_1.CloudProvider.HETZNER
@@ -277,7 +293,7 @@ class EnvCreate extends core_1.Command {
277
293
  }
278
294
  catch (error) {
279
295
  spinner.fail(`VNet provisioning failed: ${error.message}`);
280
- console.log(chalk_1.default.red('\n Cannot proceed without an environment VNet. The observability cluster and all workload clusters must share a private network.'));
296
+ console.log(chalk_1.default.red('\n Cannot proceed without an environment VNet. The control cluster and all workload clusters must share a private network.'));
281
297
  this.exit(1);
282
298
  }
283
299
  // 4. Create firewall BEFORE cluster (if enabled)
@@ -296,15 +312,18 @@ class EnvCreate extends core_1.Command {
296
312
  const publicIp = await ipService.getPublicIp();
297
313
  sourceCidrs = [ipService.toCidr(publicIp)];
298
314
  }
299
- const rules = (0, firewall_rules_1.OBSERVABILITY_FIREWALL_RULES)(sourceCidrs);
300
- const temporaryFirewallName = `flui-observability-firewall-${Date.now()}`;
315
+ const rules = (0, firewall_rules_1.CONTROL_FIREWALL_RULES)(sourceCidrs);
316
+ // Unique temporary name to avoid colliding with orphaned firewalls
317
+ // from previous runs; renamed to flui-control-firewall-<clusterId>
318
+ // (by firewallId) once the cluster is ready.
319
+ const temporaryFirewallName = `flui-control-firewall-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`;
301
320
  // Create firewall with temporary name (will be renamed when cluster is ready)
302
321
  const result = await firewallService.createFirewall({
303
322
  name: temporaryFirewallName,
304
323
  labels: [
305
324
  { key: 'managed-by', value: 'flui-cloud' },
306
325
  { key: 'flui-resource-type', value: 'firewall' },
307
- { key: 'flui-cluster-type', value: 'observability' },
326
+ { key: 'flui-cluster-type', value: 'control' },
308
327
  ],
309
328
  rules,
310
329
  // Label selector will be applied later when cluster servers exist
@@ -326,10 +345,10 @@ class EnvCreate extends core_1.Command {
326
345
  spinner.succeed(`Firewall created (Source: ${sourceCidrs.join(', ')})`);
327
346
  }
328
347
  catch (error) {
329
- spinner.warn(`Firewall creation failed: ${error.message}`);
330
- console.log(chalk_1.default.yellow(' Continuing without firewall. You can configure it manually later.'));
331
- firewallId = undefined;
332
- sourceCidrs = undefined;
348
+ spinner.fail(`Firewall creation failed: ${error.message}`);
349
+ console.log(chalk_1.default.red('\n Aborting: the control cluster must not be provisioned without a firewall.'));
350
+ console.log(chalk_1.default.dim(' Resolve the cause (e.g. remove a leftover/orphaned firewall on the provider) and retry,\n or pass --no-configure-firewall to explicitly opt out of firewall protection.'));
351
+ this.exit(1);
333
352
  }
334
353
  }
335
354
  // 5. Create K3s cluster
@@ -337,7 +356,7 @@ class EnvCreate extends core_1.Command {
337
356
  console.log(chalk_1.default.dim(flags['node-count'] > 0
338
357
  ? ' This will create master node + worker nodes'
339
358
  : ' This will create master node only'));
340
- const clusterId = await observabilityService.createObservabilityCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
359
+ const clusterId = await controlService.createControlCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
341
360
  ? {
342
361
  vnetProviderResourceId: envVnetInfo.vnetProviderResourceId,
343
362
  vnetIpRange: envVnetInfo.vnetIpRange,
@@ -349,6 +368,7 @@ class EnvCreate extends core_1.Command {
349
368
  : undefined, adminEmail, acmeStaging, flags['disk-size'], {
350
369
  sharedStorageEnabled: !flags['no-shared-storage'],
351
370
  sharedStorageVolumeSizeGb: flags['shared-storage-size'],
371
+ useLatest,
352
372
  });
353
373
  spinner.succeed('Cluster creation started!');
354
374
  if (firewallId && clusterId) {
@@ -369,7 +389,7 @@ class EnvCreate extends core_1.Command {
369
389
  }
370
390
  if (flags.detached) {
371
391
  // DETACHED MODE: Exit immediately with monitoring instructions
372
- console.log(chalk_1.default.green('\n✅ Observability Cluster Creation Started!\n'));
392
+ console.log(chalk_1.default.green('\n✅ Control Cluster Creation Started!\n'));
373
393
  console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
374
394
  console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
375
395
  console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
@@ -391,7 +411,7 @@ class EnvCreate extends core_1.Command {
391
411
  // WAIT MODE: Wait for cluster to be ready
392
412
  spinner = (0, ora_1.default)('Waiting for cluster to be ready...').start();
393
413
  try {
394
- await observabilityService.waitForClusterReady(clusterId, 600000);
414
+ await controlService.waitForClusterReady(clusterId, 600000);
395
415
  spinner.succeed('Cluster is ready!');
396
416
  }
397
417
  catch (error) {
@@ -399,7 +419,7 @@ class EnvCreate extends core_1.Command {
399
419
  console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
400
420
  this.exit(1);
401
421
  }
402
- console.log(chalk_1.default.green('\n✅ Observability Cluster Created Successfully!\n'));
422
+ console.log(chalk_1.default.green('\n✅ Control Cluster Created Successfully!\n'));
403
423
  console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
404
424
  console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
405
425
  console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
@@ -428,7 +448,7 @@ class EnvCreate extends core_1.Command {
428
448
  // This ensures reconciliation runs even if SSH setup fails
429
449
  let shouldExit = false;
430
450
  const onReconcile = async () => {
431
- const cluster = await observabilityService.getObservabilityCluster();
451
+ const cluster = await controlService.getControlCluster();
432
452
  const masterIp = cluster?.masterIpAddress;
433
453
  const nipToken = cluster?.nipHostnameToken;
434
454
  const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp ?? '', nipToken);
@@ -440,13 +460,13 @@ class EnvCreate extends core_1.Command {
440
460
  const interval = 15_000;
441
461
  const deadline = Date.now() + maxWait;
442
462
  while (Date.now() < deadline) {
443
- const valid = await observabilityService.waitForValidTls(certUrl, interval, interval, acmeStaging);
463
+ const valid = await controlService.waitForValidTls(certUrl, interval, interval, acmeStaging);
444
464
  if (valid)
445
465
  break;
446
466
  elapsed += interval;
447
467
  console.log(chalk_1.default.dim(` Certificate pending... (${Math.round(elapsed / 1000)}s / ${maxWait / 1000}s)`));
448
468
  }
449
- const tlsReady = await observabilityService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
469
+ const tlsReady = await controlService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
450
470
  if (tlsReady) {
451
471
  console.log(chalk_1.default.green(acmeStaging
452
472
  ? "✅ TLS certificate valid (Let's Encrypt STAGING — not browser-trusted)"
@@ -477,12 +497,14 @@ class EnvCreate extends core_1.Command {
477
497
  }
478
498
  }
479
499
  console.log(chalk_1.default.dim(`\n→ Waiting for OIDC client provisioning...`));
480
- const oidcReady = await observabilityService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
500
+ const oidcReady = await controlService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
481
501
  if (oidcReady) {
482
502
  console.log(chalk_1.default.green('✅ OIDC login ready'));
483
503
  }
484
504
  else {
485
- console.log(chalk_1.default.yellow('⚠ OIDC not ready — run `flui env force-ready` to retry bootstrap'));
505
+ console.log(chalk_1.default.yellow('⚠ OIDC login not ready — the dashboard sign-in will fail until this is resolved.'));
506
+ console.log(chalk_1.default.dim(' Diagnose with `flui env status`, or `flui ssh master` + kubectl (check the zitadel and flui-web pods).\n' +
507
+ ' `flui env force-ready` does NOT retry bootstrap — it only overrides the local status, so use it only after the cause is fixed.'));
486
508
  }
487
509
  }
488
510
  }
@@ -507,12 +529,12 @@ class EnvCreate extends core_1.Command {
507
529
  '\n'));
508
530
  shouldExit = true;
509
531
  };
510
- pollerHandle = observabilityService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
532
+ pollerHandle = controlService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
511
533
  // Phase 1: Wait for server provisioning (master IP)
512
534
  spinner = (0, ora_1.default)('Provisioning server...').start();
513
535
  let masterIp;
514
536
  try {
515
- masterIp = await observabilityService.waitForMasterIp(clusterId, 600000, 5000);
537
+ masterIp = await controlService.waitForMasterIp(clusterId, 600000, 5000);
516
538
  spinner.succeed(`Server provisioned (${masterIp})`);
517
539
  }
518
540
  catch (error) {
@@ -528,7 +550,7 @@ class EnvCreate extends core_1.Command {
528
550
  // Phase 2: Wait for server boot (SSH port open)
529
551
  spinner = (0, ora_1.default)('Server booting...').start();
530
552
  try {
531
- await observabilityService.waitForPortReady(masterIp, 22, 600000, 5000);
553
+ await controlService.waitForPortReady(masterIp, 22, 600000, 5000);
532
554
  spinner.succeed('Server online');
533
555
  }
534
556
  catch (error) {
@@ -542,7 +564,7 @@ class EnvCreate extends core_1.Command {
542
564
  // Phase 3: Wait for SSH authentication (CA enrollment via cloud-init)
543
565
  spinner = (0, ora_1.default)('Waiting for SSH access (CA enrollment)...').start();
544
566
  try {
545
- await observabilityService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
567
+ await controlService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
546
568
  spinner.succeed('SSH access ready');
547
569
  }
548
570
  catch (error) {
@@ -611,7 +633,7 @@ class EnvCreate extends core_1.Command {
611
633
  }
612
634
  }
613
635
  catch (error) {
614
- spinner.fail('Failed to create observability cluster');
636
+ spinner.fail('Failed to create control cluster');
615
637
  if (firewallId) {
616
638
  try {
617
639
  spinner = (0, ora_1.default)('Cleaning up orphaned firewall...').start();
@@ -653,7 +675,7 @@ class EnvCreate extends core_1.Command {
653
675
  }
654
676
  }
655
677
  }
656
- EnvCreate.description = 'Create observability cluster infrastructure on K3s';
678
+ EnvCreate.description = 'Create control cluster infrastructure on K3s';
657
679
  EnvCreate.examples = [
658
680
  '<%= config.bin %> <%= command.id %>',
659
681
  '<%= config.bin %> <%= command.id %> --node-size cx32',
@@ -664,8 +686,7 @@ EnvCreate.examples = [
664
686
  EnvCreate.flags = {
665
687
  provider: core_1.Flags.string({
666
688
  char: 'p',
667
- description: 'Cloud provider for the observability cluster',
668
- default: 'hetzner',
689
+ description: "Cloud provider for the control cluster (default: the profile's configured provider)",
669
690
  options: ['hetzner', 'scaleway'],
670
691
  }),
671
692
  'node-size': core_1.Flags.string({
@@ -696,6 +717,7 @@ EnvCreate.flags = {
696
717
  'configure-firewall': core_1.Flags.boolean({
697
718
  description: 'Automatically configure firewall after cluster creation',
698
719
  default: true,
720
+ allowNo: true,
699
721
  }),
700
722
  'firewall-ip': core_1.Flags.string({
701
723
  description: 'Source IP/CIDR for firewall (comma-separated, default: auto-detect)',
@@ -708,6 +730,10 @@ EnvCreate.flags = {
708
730
  description: "Use Let's Encrypt staging endpoint (untrusted cert, no rate limits) — useful while iterating to avoid burning prod quota",
709
731
  default: false,
710
732
  }),
733
+ latest: core_1.Flags.boolean({
734
+ description: 'Install mobile dev tags instead of the pinned release: bootstrap scripts from `master` and `:latest` Docker images. Default: every component is pinned to the CLI release version.',
735
+ default: false,
736
+ }),
711
737
  'no-shared-storage': core_1.Flags.boolean({
712
738
  description: 'Disable Flui shared storage (NFS+fscache). Default: shared storage enabled — master gets a Volume hosting the NFS export, workers mount it. Disable to fall back to local-path on each node bundled disk.',
713
739
  default: false,
@@ -45,7 +45,7 @@ const os = __importStar(require("node:os"));
45
45
  const nest_app_1 = require("../../lib/nest-app");
46
46
  const context_banner_1 = require("../../lib/context-banner");
47
47
  const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
48
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
48
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
49
49
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
50
50
  const encryption_service_1 = require("../../../../src/modules/shared/encryption/services/encryption.service");
51
51
  const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
@@ -95,9 +95,9 @@ class EnvCredentials extends core_1.Command {
95
95
  return '';
96
96
  }
97
97
  }
98
- async runHealthCheck(observabilityService, masterIp, nipHostnameToken) {
98
+ async runHealthCheck(controlService, masterIp, nipHostnameToken) {
99
99
  const s = (0, ora_1.default)('Testing connections...').start();
100
- const result = await observabilityService.checkObservabilityServices(masterIp, nipHostnameToken);
100
+ const result = await controlService.checkObservabilityServices(masterIp, nipHostnameToken);
101
101
  s.succeed('Connection tests completed');
102
102
  return result;
103
103
  }
@@ -141,13 +141,13 @@ class EnvCredentials extends core_1.Command {
141
141
  let spinner = (0, ora_1.default)('Fetching credentials...').start();
142
142
  try {
143
143
  const app = await (0, nest_app_1.getNestApp)();
144
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
144
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
145
145
  const encryptionService = app.get(encryption_service_1.EncryptionService);
146
- // Get observability cluster
147
- const cluster = await observabilityService.getObservabilityCluster();
146
+ // Get control cluster
147
+ const cluster = await controlService.getControlCluster();
148
148
  if (!cluster) {
149
- spinner.fail('No observability cluster found');
150
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
149
+ spinner.fail('No control cluster found');
150
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
151
151
  console.log(chalk_1.default.dim('Create one with:'));
152
152
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
153
153
  return;
@@ -159,7 +159,7 @@ class EnvCredentials extends core_1.Command {
159
159
  }
160
160
  spinner.succeed('Cluster found');
161
161
  // Get endpoints
162
- const endpoints = await observabilityService.getObservabilityEndpoints(cluster.id);
162
+ const endpoints = await controlService.getObservabilityEndpoints(cluster.id);
163
163
  const masterIp = cluster.masterIpAddress;
164
164
  if (!masterIp) {
165
165
  spinner.fail('Master IP address not available');
@@ -204,7 +204,7 @@ class EnvCredentials extends core_1.Command {
204
204
  ? this.redact(zitadelAdminTempPassword, showSecrets)
205
205
  : '';
206
206
  const healthStatus = flags.test
207
- ? await this.runHealthCheck(observabilityService, masterIp, cluster.nipHostnameToken)
207
+ ? await this.runHealthCheck(controlService, masterIp, cluster.nipHostnameToken)
208
208
  : null;
209
209
  const secretStatus = flags.verify
210
210
  ? await this.runSecretVerify(app.get(cli_ssh_service_1.CliSshService), masterIp)
@@ -295,7 +295,7 @@ class EnvCredentials extends core_1.Command {
295
295
  }
296
296
  }
297
297
  displayTextOutput(masterIp, endpoints, passwords, admin, healthStatus, secretStatus, zitadel = null) {
298
- console.log(chalk_1.default.cyan('\n📋 Observability Cluster Credentials'));
298
+ console.log(chalk_1.default.cyan('\n📋 Control Cluster Credentials'));
299
299
  console.log(chalk_1.default.cyan('━'.repeat(50)));
300
300
  // Endpoints section
301
301
  console.log(chalk_1.default.cyan('\n🌐 Endpoints:\n'));
@@ -404,7 +404,7 @@ class EnvCredentials extends core_1.Command {
404
404
  return `${chalk_1.default.red('❌ unreachable')}`;
405
405
  }
406
406
  }
407
- EnvCredentials.description = 'Display observability cluster connection information.\n' +
407
+ EnvCredentials.description = 'Display control cluster connection information.\n' +
408
408
  'Secrets are hidden by default; pass --show-secrets to print them. To populate\n' +
409
409
  'a local .env.local for development use `flui dev creds` instead.';
410
410
  EnvCredentials.examples = [
@@ -1,9 +1,10 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class EnvDestroy extends Command {
3
- static readonly description = "Permanently delete observability cluster (WARNING: All data will be lost!)";
3
+ static readonly description = "Permanently delete control cluster (WARNING: All data will be lost!)";
4
4
  static readonly examples: string[];
5
5
  static readonly flags: {
6
6
  force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
7
  };
8
+ private cleanupClusterScopedConfig;
8
9
  run(): Promise<void>;
9
10
  }
@@ -8,11 +8,45 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
9
  const nest_app_1 = require("../../lib/nest-app");
10
10
  const context_banner_1 = require("../../lib/context-banner");
11
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
11
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
12
12
  const config_storage_1 = require("../../lib/config-storage");
13
13
  const prompts_1 = require("../../lib/prompts");
14
14
  const vnet_provisioning_service_1 = require("../../lib/services/vnet-provisioning.service");
15
15
  class EnvDestroy extends core_1.Command {
16
+ // Drop config tied to the deleted cluster: registration, API endpoint and key.
17
+ cleanupClusterScopedConfig() {
18
+ const spinner = (0, ora_1.default)('Cleaning up configuration...').start();
19
+ try {
20
+ const configStorage = new config_storage_1.ConfigStorage();
21
+ const config = configStorage['readConfig']();
22
+ let changed = false;
23
+ if (config.credentials?.['observability-cluster-registration']) {
24
+ delete config.credentials['observability-cluster-registration'];
25
+ changed = true;
26
+ }
27
+ if (config.apiUrl) {
28
+ delete config.apiUrl;
29
+ if (config.metadata)
30
+ delete config.metadata.apiUrlUpdatedAt;
31
+ changed = true;
32
+ }
33
+ if (config.apiKey) {
34
+ delete config.apiKey;
35
+ changed = true;
36
+ }
37
+ if (changed) {
38
+ configStorage['writeConfig'](config);
39
+ spinner.succeed('Configuration cleaned up');
40
+ }
41
+ else {
42
+ spinner.info('No configuration to clean up');
43
+ }
44
+ }
45
+ catch (error) {
46
+ spinner.warn(`Failed to clean up configuration: ${error.message}`);
47
+ console.log(chalk_1.default.yellow(' This is not critical, continuing...'));
48
+ }
49
+ }
16
50
  async run() {
17
51
  const { flags } = await this.parse(EnvDestroy);
18
52
  (0, context_banner_1.printContextBanner)();
@@ -21,14 +55,14 @@ class EnvDestroy extends core_1.Command {
21
55
  // Bootstrap NestJS and get services
22
56
  const app = await (0, nest_app_1.getNestApp)();
23
57
  spinner.stop();
24
- console.log(chalk_1.default.red('\n⚠️ DESTROY Observability Cluster\n'));
58
+ console.log(chalk_1.default.red('\n⚠️ DESTROY Control Cluster\n'));
25
59
  spinner = (0, ora_1.default)('Checking for cluster...').start();
26
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
27
- // Find observability cluster
28
- const cluster = await observabilityService.getObservabilityCluster();
60
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
61
+ // Find control cluster
62
+ const cluster = await controlService.getControlCluster();
29
63
  if (!cluster) {
30
- spinner.fail('No observability cluster found');
31
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
64
+ spinner.fail('No control cluster found');
65
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
32
66
  console.log(chalk_1.default.dim('Create one with:'));
33
67
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
34
68
  return;
@@ -64,31 +98,14 @@ class EnvDestroy extends core_1.Command {
64
98
  color: 'yellow',
65
99
  }).start();
66
100
  try {
67
- await observabilityService.deleteObservabilityCluster();
101
+ await controlService.deleteControlCluster();
68
102
  spinner.succeed('All cluster resources deleted successfully');
69
103
  }
70
104
  catch (error) {
71
105
  spinner.fail('Cluster deletion encountered an error');
72
106
  throw error;
73
107
  }
74
- // Clean up observability-cluster-registration from config.json
75
- spinner = (0, ora_1.default)('Cleaning up configuration...').start();
76
- try {
77
- const configStorage = new config_storage_1.ConfigStorage();
78
- const config = configStorage['readConfig']();
79
- if (config.credentials?.['observability-cluster-registration']) {
80
- delete config.credentials['observability-cluster-registration'];
81
- configStorage['writeConfig'](config);
82
- spinner.succeed('Configuration cleaned up');
83
- }
84
- else {
85
- spinner.info('No registration to clean up');
86
- }
87
- }
88
- catch (error) {
89
- spinner.warn(`Failed to clean up configuration: ${error.message}`);
90
- console.log(chalk_1.default.yellow(' This is not critical, continuing...'));
91
- }
108
+ this.cleanupClusterScopedConfig();
92
109
  // Tear down environment VNet/Subnet (must run AFTER all servers are deleted —
93
110
  // Hetzner refuses to delete a network that still has attached servers).
94
111
  try {
@@ -101,7 +118,7 @@ class EnvDestroy extends core_1.Command {
101
118
  spinner.warn(`VNet teardown failed: ${error.message}`);
102
119
  console.log(chalk_1.default.yellow(' You may need to delete the VNet manually from the Hetzner console.'));
103
120
  }
104
- console.log(chalk_1.default.green('\n✅ Observability Cluster Deleted Successfully\n'));
121
+ console.log(chalk_1.default.green('\n✅ Control Cluster Deleted Successfully\n'));
105
122
  console.log(chalk_1.default.dim(' All cluster resources have been removed:'));
106
123
  console.log(chalk_1.default.dim(' • Servers (master and worker nodes)'));
107
124
  console.log(chalk_1.default.dim(' • Firewalls and security rules'));
@@ -130,7 +147,7 @@ class EnvDestroy extends core_1.Command {
130
147
  }
131
148
  }
132
149
  }
133
- EnvDestroy.description = 'Permanently delete observability cluster (WARNING: All data will be lost!)';
150
+ EnvDestroy.description = 'Permanently delete control cluster (WARNING: All data will be lost!)';
134
151
  EnvDestroy.examples = [
135
152
  '<%= config.bin %> <%= command.id %>',
136
153
  '<%= config.bin %> <%= command.id %> --force',
@@ -8,7 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
9
  const nest_app_1 = require("../../lib/nest-app");
10
10
  const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
11
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
11
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
12
12
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
13
13
  const context_banner_1 = require("../../lib/context-banner");
14
14
  class EnvDiagCA extends core_1.Command {
@@ -17,9 +17,9 @@ class EnvDiagCA extends core_1.Command {
17
17
  const spinner = (0, ora_1.default)('Connecting to cluster...').start();
18
18
  try {
19
19
  const app = await (0, nest_app_1.getNestApp)();
20
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
20
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
21
21
  const sshService = app.get(cli_ssh_service_1.CliSshService);
22
- const cluster = await observabilityService.getObservabilityCluster();
22
+ const cluster = await controlService.getControlCluster();
23
23
  if (!this.assertClusterReady(cluster, spinner)) {
24
24
  return;
25
25
  }
@@ -42,8 +42,8 @@ class EnvDiagCA extends core_1.Command {
42
42
  }
43
43
  assertClusterReady(cluster, spinner) {
44
44
  if (!cluster) {
45
- spinner.fail('No observability cluster found');
46
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
45
+ spinner.fail('No control cluster found');
46
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
47
47
  console.log(chalk_1.default.dim('Create one with:'));
48
48
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
49
49
  return false;
@@ -14,25 +14,8 @@ export default class EnvExportConfig extends Command {
14
14
  save: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
- /**
18
- * Resolve every preference this command consumes (email, dashboardPath, certificateMode)
19
- * through the layered config: flag > env > project-local > user > default > prompt.
20
- *
21
- * Prompted values are persisted to the active profile only when --save is set;
22
- * otherwise they are used for this run and forgotten (no surprise side effects).
23
- *
24
- * `skipDashboard` shortcuts the dashboard-only preferences when --no-dashboard is set.
25
- */
26
17
  private resolvePreferences;
27
- /**
28
- * Patch the dashboard's config.json with the localhost API endpoints + cluster auth +
29
- * the resolved certificateMode. Caller is responsible for resolving inputs.
30
- */
31
18
  private syncDashboardConfig;
32
- /**
33
- * Prompt for a missing preference value, validating against the schema.
34
- * Used only when every other layer (flag, env, project, user, default) failed to provide a value.
35
- */
36
19
  private promptForPreference;
37
20
  private resolveIssuer;
38
21
  private resolveJwks;