@flui-cloud/cli 0.0.1 → 0.1.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 (106) 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 +73 -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 +45 -44
  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 +85 -0
  60. package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
  61. package/lib/cli/src/config/bootstrap.config.js +21 -4
  62. package/lib/cli/src/config/preferences-schema.js +5 -5
  63. package/lib/cli/src/config/release.config.d.ts +31 -0
  64. package/lib/cli/src/config/release.config.js +38 -0
  65. package/lib/cli/src/lib/prompts.d.ts +1 -6
  66. package/lib/cli/src/lib/prompts.js +33 -13
  67. package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
  68. package/lib/cli/src/lib/services/cli-app.service.js +9 -0
  69. package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
  70. package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
  71. package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
  72. package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
  73. package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
  74. package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
  75. package/lib/cli/src/services/cli-clusters.service.js +57 -34
  76. package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
  77. package/lib/cli/src/services/cli-control-cluster.service.js +544 -0
  78. package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
  79. package/lib/cli/src/services/cli-endpoint-resolver.service.js +8 -2
  80. package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
  81. package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
  82. package/lib/src/config/release.config.d.ts +28 -0
  83. package/lib/src/config/release.config.js +35 -0
  84. package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
  85. package/lib/src/modules/applications/entities/application.entity.js +12 -0
  86. package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
  87. package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
  88. package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
  89. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
  90. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
  91. package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
  92. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
  93. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
  94. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
  95. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
  96. package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
  97. package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
  98. package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
  99. package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
  100. package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
  101. package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
  102. package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
  103. package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
  104. package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
  105. package/oclif.manifest.json +1025 -678
  106. 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");
@@ -28,9 +29,36 @@ const prompts_1 = require("../../lib/prompts");
28
29
  const preferences_resolver_1 = require("../../config/preferences-resolver");
29
30
  const preferences_schema_1 = require("../../config/preferences-schema");
30
31
  class EnvCreate extends core_1.Command {
32
+ /** The sole provider with configured credentials, or undefined if none/both. */
33
+ detectConfiguredProvider(configStorage) {
34
+ const configured = ['hetzner', 'scaleway'].filter((p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
35
+ ? configStorage.hasCredentials(p)
36
+ : configStorage.hasToken(p));
37
+ return configured.length === 1 ? configured[0] : undefined;
38
+ }
31
39
  async run() {
32
40
  const { flags } = await this.parse(EnvCreate);
33
- const providerKey = flags.provider;
41
+ if (flags['auth-mode'] !== 'oidc') {
42
+ this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
43
+ }
44
+ const configStorage = new config_storage_1.ConfigStorage();
45
+ const hasCreds = (p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
46
+ ? configStorage.hasCredentials(p)
47
+ : configStorage.hasToken(p);
48
+ // Resolve the target provider BEFORE deriving provider-specific defaults.
49
+ // Precedence: explicit --provider > the profile's single configured
50
+ // provider > the setup wizard's choice. (Previously hard-defaulted to
51
+ // hetzner, so a Scaleway-only profile still tried — and failed — to build
52
+ // on Hetzner.)
53
+ let providerKey = flags.provider ?? this.detectConfiguredProvider(configStorage);
54
+ if (!providerKey || !hasCreds(providerKey)) {
55
+ const configured = await (0, prompts_1.runProviderSetupWizard)(providerKey);
56
+ if (!configured) {
57
+ console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey ?? '<provider>'}\n`));
58
+ this.exit(1);
59
+ }
60
+ providerKey = configured;
61
+ }
34
62
  const cloudProvider = providerKey === 'scaleway'
35
63
  ? cloud_provider_enum_1.CloudProvider.SCALEWAY
36
64
  : cloud_provider_enum_1.CloudProvider.HETZNER;
@@ -40,9 +68,6 @@ class EnvCreate extends core_1.Command {
40
68
  if (!allowedRegions.includes(region)) {
41
69
  this.error(`Region '${region}' is not supported for provider '${providerKey}'. Allowed: ${allowedRegions.join(', ')}`, { exit: 1 });
42
70
  }
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
71
  (0, context_banner_1.printContextBanner)({
47
72
  cluster: { provider: providerKey, region },
48
73
  });
@@ -55,9 +80,8 @@ class EnvCreate extends core_1.Command {
55
80
  app = await (0, nest_app_1.getNestApp)();
56
81
  spinner.succeed('Initialized');
57
82
  spinner = (0, ora_1.default)('Loading services...').start();
58
- const configStorage = new config_storage_1.ConfigStorage();
59
83
  const apiUrl = configStorage.getApiUrl();
60
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
84
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
61
85
  const apiClient = new api_client_1.ApiClient({
62
86
  baseUrl: apiUrl,
63
87
  apiKey: configStorage.getApiKey(),
@@ -65,20 +89,6 @@ class EnvCreate extends core_1.Command {
65
89
  const cacheService = new server_type_cache_service_1.ServerTypeCacheService();
66
90
  const validatorService = new server_type_validator_service_1.ServerTypeValidatorService();
67
91
  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
92
  // 3. Resolve admin email (prompt if not set, then persist)
83
93
  const emailResolver = new preferences_resolver_1.PreferencesResolver(configStorage);
84
94
  const emailResult = emailResolver.resolve('email');
@@ -102,6 +112,7 @@ class EnvCreate extends core_1.Command {
102
112
  // server-side, so the LE 5-certs-per-7-days rate limit never trips on
103
113
  // repeated test creations.
104
114
  const acmeStaging = flags['acme-staging'];
115
+ const useLatest = flags.latest;
105
116
  spinner.stop();
106
117
  if (acmeStaging) {
107
118
  console.log(chalk_1.default.yellow(`\n⚠ ACME endpoint: Let's Encrypt STAGING — cert will not be browser-trusted (warning expected).\n`));
@@ -231,17 +242,17 @@ class EnvCreate extends core_1.Command {
231
242
  spinner.succeed('Server type validation skipped (no data available)');
232
243
  }
233
244
  // Display cluster configuration
234
- console.log(chalk_1.default.cyan('\n🚀 Creating Flui Observability Cluster (K3s)\n'));
245
+ console.log(chalk_1.default.cyan('\n🚀 Creating Flui Control Cluster (K3s)\n'));
235
246
  console.log(chalk_1.default.dim(` Provider: ${providerKey}`));
236
247
  console.log(chalk_1.default.dim(` Node Size: ${validatedNodeSize}`));
237
248
  console.log(chalk_1.default.dim(` Region: ${validatedRegion}`));
238
249
  console.log(chalk_1.default.dim(` Worker Nodes: ${flags['node-count']}\n`));
239
- // 3. Check if observability cluster already exists
250
+ // 3. Check if control cluster already exists
240
251
  spinner = (0, ora_1.default)('Checking for existing cluster...').start();
241
- const existingCluster = await observabilityService.getObservabilityCluster();
252
+ const existingCluster = await controlService.getControlCluster();
242
253
  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'));
254
+ spinner.fail('Control cluster already exists!');
255
+ console.log(chalk_1.default.yellow('\n⚠️ An control cluster is already running:\n'));
245
256
  console.log(` ${chalk_1.default.bold('Name:')} ${existingCluster.name}`);
246
257
  console.log(` ${chalk_1.default.bold('ID:')} ${existingCluster.id}`);
247
258
  console.log(` ${chalk_1.default.bold('Status:')} ${existingCluster.status}`);
@@ -263,7 +274,7 @@ class EnvCreate extends core_1.Command {
263
274
  const vnetService = app.get(vnet_provisioning_service_1.VnetProvisioningService);
264
275
  envVnetInfo = await vnetService.ensureEnvVnet({
265
276
  provider: cloudProvider,
266
- name: 'flui-env-vnet',
277
+ name: `flui-env-${(0, node_crypto_1.randomBytes)(3).toString('hex')}`,
267
278
  ipRange: '10.10.0.0/16',
268
279
  subnetIpRange: '10.10.1.0/24',
269
280
  networkZone: cloudProvider === cloud_provider_enum_1.CloudProvider.HETZNER
@@ -277,7 +288,7 @@ class EnvCreate extends core_1.Command {
277
288
  }
278
289
  catch (error) {
279
290
  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.'));
291
+ 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
292
  this.exit(1);
282
293
  }
283
294
  // 4. Create firewall BEFORE cluster (if enabled)
@@ -296,15 +307,18 @@ class EnvCreate extends core_1.Command {
296
307
  const publicIp = await ipService.getPublicIp();
297
308
  sourceCidrs = [ipService.toCidr(publicIp)];
298
309
  }
299
- const rules = (0, firewall_rules_1.OBSERVABILITY_FIREWALL_RULES)(sourceCidrs);
300
- const temporaryFirewallName = `flui-observability-firewall-${Date.now()}`;
310
+ const rules = (0, firewall_rules_1.CONTROL_FIREWALL_RULES)(sourceCidrs);
311
+ // Unique temporary name to avoid colliding with orphaned firewalls
312
+ // from previous runs; renamed to flui-control-firewall-<clusterId>
313
+ // (by firewallId) once the cluster is ready.
314
+ const temporaryFirewallName = `flui-control-firewall-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`;
301
315
  // Create firewall with temporary name (will be renamed when cluster is ready)
302
316
  const result = await firewallService.createFirewall({
303
317
  name: temporaryFirewallName,
304
318
  labels: [
305
319
  { key: 'managed-by', value: 'flui-cloud' },
306
320
  { key: 'flui-resource-type', value: 'firewall' },
307
- { key: 'flui-cluster-type', value: 'observability' },
321
+ { key: 'flui-cluster-type', value: 'control' },
308
322
  ],
309
323
  rules,
310
324
  // Label selector will be applied later when cluster servers exist
@@ -326,10 +340,10 @@ class EnvCreate extends core_1.Command {
326
340
  spinner.succeed(`Firewall created (Source: ${sourceCidrs.join(', ')})`);
327
341
  }
328
342
  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;
343
+ spinner.fail(`Firewall creation failed: ${error.message}`);
344
+ console.log(chalk_1.default.red('\n Aborting: the control cluster must not be provisioned without a firewall.'));
345
+ 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.'));
346
+ this.exit(1);
333
347
  }
334
348
  }
335
349
  // 5. Create K3s cluster
@@ -337,7 +351,7 @@ class EnvCreate extends core_1.Command {
337
351
  console.log(chalk_1.default.dim(flags['node-count'] > 0
338
352
  ? ' This will create master node + worker nodes'
339
353
  : ' This will create master node only'));
340
- const clusterId = await observabilityService.createObservabilityCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
354
+ const clusterId = await controlService.createControlCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
341
355
  ? {
342
356
  vnetProviderResourceId: envVnetInfo.vnetProviderResourceId,
343
357
  vnetIpRange: envVnetInfo.vnetIpRange,
@@ -349,6 +363,7 @@ class EnvCreate extends core_1.Command {
349
363
  : undefined, adminEmail, acmeStaging, flags['disk-size'], {
350
364
  sharedStorageEnabled: !flags['no-shared-storage'],
351
365
  sharedStorageVolumeSizeGb: flags['shared-storage-size'],
366
+ useLatest,
352
367
  });
353
368
  spinner.succeed('Cluster creation started!');
354
369
  if (firewallId && clusterId) {
@@ -369,7 +384,7 @@ class EnvCreate extends core_1.Command {
369
384
  }
370
385
  if (flags.detached) {
371
386
  // DETACHED MODE: Exit immediately with monitoring instructions
372
- console.log(chalk_1.default.green('\n✅ Observability Cluster Creation Started!\n'));
387
+ console.log(chalk_1.default.green('\n✅ Control Cluster Creation Started!\n'));
373
388
  console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
374
389
  console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
375
390
  console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
@@ -391,7 +406,7 @@ class EnvCreate extends core_1.Command {
391
406
  // WAIT MODE: Wait for cluster to be ready
392
407
  spinner = (0, ora_1.default)('Waiting for cluster to be ready...').start();
393
408
  try {
394
- await observabilityService.waitForClusterReady(clusterId, 600000);
409
+ await controlService.waitForClusterReady(clusterId, 600000);
395
410
  spinner.succeed('Cluster is ready!');
396
411
  }
397
412
  catch (error) {
@@ -399,7 +414,7 @@ class EnvCreate extends core_1.Command {
399
414
  console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
400
415
  this.exit(1);
401
416
  }
402
- console.log(chalk_1.default.green('\n✅ Observability Cluster Created Successfully!\n'));
417
+ console.log(chalk_1.default.green('\n✅ Control Cluster Created Successfully!\n'));
403
418
  console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
404
419
  console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
405
420
  console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
@@ -428,7 +443,7 @@ class EnvCreate extends core_1.Command {
428
443
  // This ensures reconciliation runs even if SSH setup fails
429
444
  let shouldExit = false;
430
445
  const onReconcile = async () => {
431
- const cluster = await observabilityService.getObservabilityCluster();
446
+ const cluster = await controlService.getControlCluster();
432
447
  const masterIp = cluster?.masterIpAddress;
433
448
  const nipToken = cluster?.nipHostnameToken;
434
449
  const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp ?? '', nipToken);
@@ -440,13 +455,13 @@ class EnvCreate extends core_1.Command {
440
455
  const interval = 15_000;
441
456
  const deadline = Date.now() + maxWait;
442
457
  while (Date.now() < deadline) {
443
- const valid = await observabilityService.waitForValidTls(certUrl, interval, interval, acmeStaging);
458
+ const valid = await controlService.waitForValidTls(certUrl, interval, interval, acmeStaging);
444
459
  if (valid)
445
460
  break;
446
461
  elapsed += interval;
447
462
  console.log(chalk_1.default.dim(` Certificate pending... (${Math.round(elapsed / 1000)}s / ${maxWait / 1000}s)`));
448
463
  }
449
- const tlsReady = await observabilityService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
464
+ const tlsReady = await controlService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
450
465
  if (tlsReady) {
451
466
  console.log(chalk_1.default.green(acmeStaging
452
467
  ? "✅ TLS certificate valid (Let's Encrypt STAGING — not browser-trusted)"
@@ -477,12 +492,14 @@ class EnvCreate extends core_1.Command {
477
492
  }
478
493
  }
479
494
  console.log(chalk_1.default.dim(`\n→ Waiting for OIDC client provisioning...`));
480
- const oidcReady = await observabilityService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
495
+ const oidcReady = await controlService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
481
496
  if (oidcReady) {
482
497
  console.log(chalk_1.default.green('✅ OIDC login ready'));
483
498
  }
484
499
  else {
485
- console.log(chalk_1.default.yellow('⚠ OIDC not ready — run `flui env force-ready` to retry bootstrap'));
500
+ console.log(chalk_1.default.yellow('⚠ OIDC login not ready — the dashboard sign-in will fail until this is resolved.'));
501
+ console.log(chalk_1.default.dim(' Diagnose with `flui env status`, or `flui ssh master` + kubectl (check the zitadel and flui-web pods).\n' +
502
+ ' `flui env force-ready` does NOT retry bootstrap — it only overrides the local status, so use it only after the cause is fixed.'));
486
503
  }
487
504
  }
488
505
  }
@@ -507,12 +524,12 @@ class EnvCreate extends core_1.Command {
507
524
  '\n'));
508
525
  shouldExit = true;
509
526
  };
510
- pollerHandle = observabilityService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
527
+ pollerHandle = controlService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
511
528
  // Phase 1: Wait for server provisioning (master IP)
512
529
  spinner = (0, ora_1.default)('Provisioning server...').start();
513
530
  let masterIp;
514
531
  try {
515
- masterIp = await observabilityService.waitForMasterIp(clusterId, 600000, 5000);
532
+ masterIp = await controlService.waitForMasterIp(clusterId, 600000, 5000);
516
533
  spinner.succeed(`Server provisioned (${masterIp})`);
517
534
  }
518
535
  catch (error) {
@@ -528,7 +545,7 @@ class EnvCreate extends core_1.Command {
528
545
  // Phase 2: Wait for server boot (SSH port open)
529
546
  spinner = (0, ora_1.default)('Server booting...').start();
530
547
  try {
531
- await observabilityService.waitForPortReady(masterIp, 22, 600000, 5000);
548
+ await controlService.waitForPortReady(masterIp, 22, 600000, 5000);
532
549
  spinner.succeed('Server online');
533
550
  }
534
551
  catch (error) {
@@ -542,7 +559,7 @@ class EnvCreate extends core_1.Command {
542
559
  // Phase 3: Wait for SSH authentication (CA enrollment via cloud-init)
543
560
  spinner = (0, ora_1.default)('Waiting for SSH access (CA enrollment)...').start();
544
561
  try {
545
- await observabilityService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
562
+ await controlService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
546
563
  spinner.succeed('SSH access ready');
547
564
  }
548
565
  catch (error) {
@@ -611,7 +628,7 @@ class EnvCreate extends core_1.Command {
611
628
  }
612
629
  }
613
630
  catch (error) {
614
- spinner.fail('Failed to create observability cluster');
631
+ spinner.fail('Failed to create control cluster');
615
632
  if (firewallId) {
616
633
  try {
617
634
  spinner = (0, ora_1.default)('Cleaning up orphaned firewall...').start();
@@ -653,7 +670,7 @@ class EnvCreate extends core_1.Command {
653
670
  }
654
671
  }
655
672
  }
656
- EnvCreate.description = 'Create observability cluster infrastructure on K3s';
673
+ EnvCreate.description = 'Create control cluster infrastructure on K3s';
657
674
  EnvCreate.examples = [
658
675
  '<%= config.bin %> <%= command.id %>',
659
676
  '<%= config.bin %> <%= command.id %> --node-size cx32',
@@ -664,8 +681,7 @@ EnvCreate.examples = [
664
681
  EnvCreate.flags = {
665
682
  provider: core_1.Flags.string({
666
683
  char: 'p',
667
- description: 'Cloud provider for the observability cluster',
668
- default: 'hetzner',
684
+ description: "Cloud provider for the control cluster (default: the profile's configured provider)",
669
685
  options: ['hetzner', 'scaleway'],
670
686
  }),
671
687
  'node-size': core_1.Flags.string({
@@ -696,6 +712,7 @@ EnvCreate.flags = {
696
712
  'configure-firewall': core_1.Flags.boolean({
697
713
  description: 'Automatically configure firewall after cluster creation',
698
714
  default: true,
715
+ allowNo: true,
699
716
  }),
700
717
  'firewall-ip': core_1.Flags.string({
701
718
  description: 'Source IP/CIDR for firewall (comma-separated, default: auto-detect)',
@@ -708,6 +725,10 @@ EnvCreate.flags = {
708
725
  description: "Use Let's Encrypt staging endpoint (untrusted cert, no rate limits) — useful while iterating to avoid burning prod quota",
709
726
  default: false,
710
727
  }),
728
+ latest: core_1.Flags.boolean({
729
+ 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.',
730
+ default: false,
731
+ }),
711
732
  'no-shared-storage': core_1.Flags.boolean({
712
733
  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
734
  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;