@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
@@ -42,7 +42,7 @@ const ora_1 = __importDefault(require("ora"));
42
42
  const fs = __importStar(require("node:fs"));
43
43
  const path = __importStar(require("node:path"));
44
44
  const nest_app_1 = require("../../lib/nest-app");
45
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
45
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
46
46
  const cli_endpoint_resolver_service_1 = require("../../services/cli-endpoint-resolver.service");
47
47
  const config_storage_1 = require("../../lib/config-storage");
48
48
  const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
@@ -59,12 +59,12 @@ class EnvExportConfig extends core_1.Command {
59
59
  let spinner = (0, ora_1.default)('Reading cluster configuration...').start();
60
60
  try {
61
61
  const app = await (0, nest_app_1.getNestApp)();
62
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
62
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
63
63
  const resolver = app.get(cli_endpoint_resolver_service_1.CliEndpointResolverService);
64
- const cluster = await observabilityService.getObservabilityCluster();
64
+ const cluster = await controlService.getControlCluster();
65
65
  if (!cluster) {
66
- spinner.fail('No observability cluster found');
67
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
66
+ spinner.fail('No control cluster found');
67
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
68
68
  console.log(chalk_1.default.dim('Create one with:'));
69
69
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
70
70
  return;
@@ -83,17 +83,12 @@ class EnvExportConfig extends core_1.Command {
83
83
  spinner = (0, ora_1.default)('Resolving endpoints from cluster...').start();
84
84
  const endpoints = await resolver.resolveEndpoints(masterIp, cluster.nipHostnameToken);
85
85
  spinner.succeed('Endpoints resolved');
86
- // ADMIN_EMAIL and SSH_CA_PUBLIC_KEY live in flui-secrets but reading
87
- // them here would require kube-API access (closed by the new firewall
88
- // policy). They are written to .env.local by `flui dev creds`, which
89
- // reaches the secret over SSH+kubectl. Here we fall back to the
90
- // 'email' preference and skip the public CA key.
86
+ // Written to .env.local by `flui dev creds` (reads flui-secrets over SSH+kubectl).
91
87
  const adminEmail = '';
92
88
  const sshCaPublicKey = '';
93
89
  const authMode = endpoints.authMode === 'unknown' ? 'local' : endpoints.authMode;
94
90
  const resolvedIssuer = this.resolveIssuer(endpoints);
95
91
  const resolvedJwks = this.resolveJwks(endpoints, resolvedIssuer);
96
- // Display configuration
97
92
  console.log(chalk_1.default.cyan('\n📋 Exporting Cluster Configuration\n'));
98
93
  console.log(` ${chalk_1.default.bold('Cluster:')} ${cluster.name}`);
99
94
  console.log(` ${chalk_1.default.bold('Master IP:')} ${masterIp}`);
@@ -113,7 +108,16 @@ class EnvExportConfig extends core_1.Command {
113
108
  console.log(` OIDC_ISSUER=${resolvedIssuer || chalk_1.default.dim('(not set)')}`);
114
109
  console.log(` OIDC_JWKS_URI=${resolvedJwks || chalk_1.default.dim('(not set)')}`);
115
110
  console.log(` OIDC_AUDIENCE=${endpoints.oidcAudience || chalk_1.default.dim('(set after first Zitadel setup)')}`);
111
+ const adminUrlPreview = endpoints.zitadel.fqdn
112
+ ? `https://${endpoints.zitadel.fqdn}`
113
+ : chalk_1.default.dim('(zitadel ingress not found)');
114
+ console.log(` OIDC_PROVIDER_ADMIN_URL=${adminUrlPreview}`);
115
+ console.log(` OIDC_CLI_CLIENT_ID=${endpoints.oidcCliClientId || chalk_1.default.dim('(not yet provisioned)')}`);
116
116
  }
117
+ const publicWebUrlPreview = endpoints.fluiWeb.fqdn
118
+ ? `https://${endpoints.fluiWeb.fqdn}`
119
+ : chalk_1.default.dim('(flui-web ingress not found)');
120
+ console.log(` PUBLIC_WEB_URL=${publicWebUrlPreview}`);
117
121
  console.log(` AUTH_MODE=${authMode}`);
118
122
  console.log(` ADMIN_EMAIL=${adminEmail || chalk_1.default.dim('(not set)')}`);
119
123
  console.log(` SSH_CA_PUBLIC_KEY=${sshCaPublicKey ? chalk_1.default.green('(present)') : chalk_1.default.dim('(not set)')}`);
@@ -138,8 +142,6 @@ class EnvExportConfig extends core_1.Command {
138
142
  skipDashboard: flags['no-dashboard'],
139
143
  });
140
144
  spinner = (0, ora_1.default)('Updating .env file...').start();
141
- // .env lives next to the resolved apiPath. Defaults to '.' (cwd) so legacy
142
- // invocations from inside flui.api keep working without any preference set.
143
145
  const apiDir = path.isAbsolute(preferences.apiPath)
144
146
  ? preferences.apiPath
145
147
  : path.resolve(process.cwd(), preferences.apiPath);
@@ -184,8 +186,15 @@ class EnvExportConfig extends core_1.Command {
184
186
  envVars.OIDC_JWKS_URI = resolvedJwks;
185
187
  if (endpoints.oidcAudience)
186
188
  envVars.OIDC_AUDIENCE = endpoints.oidcAudience;
187
- // ADMIN_EMAIL: prefer the value baked into the cluster's flui-secrets, fall back to
188
- // the resolved 'email' preference. Either way the API gets a populated value.
189
+ // Zitadel admin API enforces Host header matching ExternalDomain.
190
+ if (endpoints.zitadel.fqdn)
191
+ envVars.OIDC_PROVIDER_ADMIN_URL = `https://${endpoints.zitadel.fqdn}`;
192
+ // Avoids OidcBootstrapSeeder re-provisioning the CLI app on every boot.
193
+ if (endpoints.oidcCliClientId)
194
+ envVars.OIDC_CLI_CLIENT_ID = endpoints.oidcCliClientId;
195
+ // Consumed by flui-authz install for the login-redirect URL.
196
+ if (endpoints.fluiWeb.fqdn)
197
+ envVars.PUBLIC_WEB_URL = `https://${endpoints.fluiWeb.fqdn}`;
189
198
  const resolvedEmail = adminEmail || preferences.email;
190
199
  if (resolvedEmail)
191
200
  envVars.ADMIN_EMAIL = resolvedEmail;
@@ -220,15 +229,6 @@ class EnvExportConfig extends core_1.Command {
220
229
  await (0, nest_app_1.closeNestApp)();
221
230
  }
222
231
  }
223
- /**
224
- * Resolve every preference this command consumes (email, dashboardPath, certificateMode)
225
- * through the layered config: flag > env > project-local > user > default > prompt.
226
- *
227
- * Prompted values are persisted to the active profile only when --save is set;
228
- * otherwise they are used for this run and forgotten (no surprise side effects).
229
- *
230
- * `skipDashboard` shortcuts the dashboard-only preferences when --no-dashboard is set.
231
- */
232
232
  async resolvePreferences(opts) {
233
233
  const storage = new config_storage_1.ConfigStorage();
234
234
  const resolver = new preferences_resolver_1.PreferencesResolver(storage);
@@ -264,10 +264,6 @@ class EnvExportConfig extends core_1.Command {
264
264
  }
265
265
  return out;
266
266
  }
267
- /**
268
- * Patch the dashboard's config.json with the localhost API endpoints + cluster auth +
269
- * the resolved certificateMode. Caller is responsible for resolving inputs.
270
- */
271
267
  async syncDashboardConfig(opts) {
272
268
  const dashboardPath = opts.dashboardPath;
273
269
  const certificateMode = opts.certificateMode;
@@ -275,20 +271,31 @@ class EnvExportConfig extends core_1.Command {
275
271
  ? dashboardPath
276
272
  : path.resolve(process.cwd(), dashboardPath);
277
273
  const configPath = path.join(absoluteDashboardPath, 'src', 'assets', 'config.json');
274
+ const examplePath = path.join(absoluteDashboardPath, 'src', 'assets', 'config.example.json');
278
275
  const spinner = (0, ora_1.default)(`Updating dashboard config (${configPath})...`).start();
279
- if (!fs.existsSync(configPath)) {
280
- spinner.warn(`Dashboard config not found at ${configPath} — skipping. Adjust dashboardPath or pass --no-dashboard.`);
281
- return;
282
- }
283
276
  let current = {};
284
- try {
285
- current = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
277
+ if (fs.existsSync(configPath)) {
278
+ try {
279
+ current = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
280
+ }
281
+ catch (err) {
282
+ spinner.fail(`Dashboard config.json is not valid JSON (${err.message}) — skipping`);
283
+ return;
284
+ }
285
+ }
286
+ else if (fs.existsSync(examplePath)) {
287
+ try {
288
+ current = JSON.parse(fs.readFileSync(examplePath, 'utf-8'));
289
+ spinner.info(`config.json missing — seeded from ${path.basename(examplePath)}`);
290
+ }
291
+ catch (err) {
292
+ spinner.warn(`config.example.json is not valid JSON (${err.message}) — starting from empty config`);
293
+ }
286
294
  }
287
- catch (err) {
288
- spinner.fail(`Dashboard config.json is not valid JSON (${err.message}) — skipping`);
289
- return;
295
+ else {
296
+ spinner.info('config.json and config.example.json both missing — creating from scratch');
290
297
  }
291
- if (opts.backup) {
298
+ if (opts.backup && fs.existsSync(configPath)) {
292
299
  const timestamp = new Date()
293
300
  .toISOString()
294
301
  .replaceAll(':', '-')
@@ -308,10 +315,6 @@ class EnvExportConfig extends core_1.Command {
308
315
  fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8');
309
316
  spinner.succeed(`Dashboard config updated (authMode=${opts.authMode}, certificateMode=${certificateMode}, oidcClientId=${opts.oidcClientId ? 'set' : '(empty)'})`);
310
317
  }
311
- /**
312
- * Prompt for a missing preference value, validating against the schema.
313
- * Used only when every other layer (flag, env, project, user, default) failed to provide a value.
314
- */
315
318
  async promptForPreference(key) {
316
319
  const def = preferences_schema_1.PREFERENCES[key];
317
320
  return (0, prompts_1.promptInput)({
@@ -363,8 +366,6 @@ EnvExportConfig.flags = {
363
366
  description: 'Skip updating the dashboard config.json',
364
367
  default: false,
365
368
  }),
366
- // Direct overrides for the layered preferences resolver.
367
- // Without these, values resolve from env > project file > user config > prompt.
368
369
  'api-path': core_1.Flags.string({
369
370
  description: `Override resolved value for the "apiPath" preference (${preferences_schema_1.PREFERENCES.apiPath.description})`,
370
371
  }),
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class EnvForceReady extends Command {
3
- static readonly description = "Force observability cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)";
3
+ static readonly description = "Force control cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)";
4
4
  static readonly examples: string[];
5
5
  static readonly flags: {
6
6
  force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -41,7 +41,7 @@ const chalk_1 = __importDefault(require("chalk"));
41
41
  const ora_1 = __importDefault(require("ora"));
42
42
  const nest_app_1 = require("../../lib/nest-app");
43
43
  const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
44
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
44
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
45
45
  const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
46
46
  const cli_operation_repository_1 = require("../../lib/repositories/cli-operation.repository");
47
47
  const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
@@ -59,14 +59,14 @@ class EnvForceReady extends core_1.Command {
59
59
  spinner.stop();
60
60
  console.log(chalk_1.default.yellow('\n⚠️ Force Cluster Status to READY\n'));
61
61
  spinner = (0, ora_1.default)('Loading cluster...').start();
62
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
62
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
63
63
  const clusterRepository = app.get(cli_cluster_repository_1.CliClusterRepository);
64
64
  const operationRepository = app.get(cli_operation_repository_1.CliOperationRepository);
65
- // Find observability cluster
66
- const cluster = await observabilityService.getObservabilityCluster();
65
+ // Find control cluster
66
+ const cluster = await controlService.getControlCluster();
67
67
  if (!cluster) {
68
- spinner.fail('No observability cluster found');
69
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
68
+ spinner.fail('No control cluster found');
69
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
70
70
  console.log(chalk_1.default.dim('Create one with:'));
71
71
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
72
72
  return;
@@ -96,7 +96,7 @@ class EnvForceReady extends core_1.Command {
96
96
  console.log(chalk_1.default.cyan('\n🔍 Checking cluster health...\n'));
97
97
  spinner = (0, ora_1.default)('Checking observability services...').start();
98
98
  try {
99
- healthStatus = await observabilityService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
99
+ healthStatus = await controlService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
100
100
  spinner.succeed('Health checks completed');
101
101
  // Display health status
102
102
  console.log(chalk_1.default.cyan('\n📊 Service Health Status:\n'));
@@ -202,7 +202,7 @@ class EnvForceReady extends core_1.Command {
202
202
  }
203
203
  }
204
204
  }
205
- EnvForceReady.description = 'Force observability cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)';
205
+ EnvForceReady.description = 'Force control cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)';
206
206
  EnvForceReady.examples = [
207
207
  '<%= config.bin %> <%= command.id %>',
208
208
  '<%= config.bin %> <%= command.id %> --force',
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
7
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
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
10
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
11
11
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
12
12
  const context_banner_1 = require("../../lib/context-banner");
13
13
  class EnvInspect extends core_1.Command {
@@ -17,13 +17,13 @@ class EnvInspect extends core_1.Command {
17
17
  let spinner = (0, ora_1.default)('Loading cluster information...').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
22
  // Get cluster
23
- const cluster = await observabilityService.getObservabilityCluster();
23
+ const cluster = await controlService.getControlCluster();
24
24
  if (!cluster) {
25
- spinner.fail('No observability cluster found');
26
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
25
+ spinner.fail('No control cluster found');
26
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
27
27
  console.log(chalk_1.default.dim('Create one with:'));
28
28
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
29
29
  return;
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
7
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
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
10
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
11
11
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
12
12
  const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
13
13
  const encryption_service_1 = require("../../../../src/modules/shared/encryption/services/encryption.service");
@@ -19,13 +19,13 @@ class EnvRefreshKubeconfig extends core_1.Command {
19
19
  const spinner = (0, ora_1.default)('Fetching kubeconfig from K3s master...').start();
20
20
  try {
21
21
  const app = await (0, nest_app_1.getNestApp)();
22
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
22
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
23
23
  const sshService = app.get(cli_ssh_service_1.CliSshService);
24
24
  const clusterRepo = app.get(cli_cluster_repository_1.CliClusterRepository);
25
25
  const encryptionService = app.get(encryption_service_1.EncryptionService);
26
- const cluster = await observabilityService.getObservabilityCluster();
26
+ const cluster = await controlService.getControlCluster();
27
27
  if (!cluster) {
28
- spinner.fail('No observability cluster found');
28
+ spinner.fail('No control cluster found');
29
29
  return;
30
30
  }
31
31
  if (cluster.status !== cluster_entity_1.ClusterStatus.READY) {
@@ -43,7 +43,7 @@ const fs = __importStar(require("node:fs/promises"));
43
43
  const path = __importStar(require("node:path"));
44
44
  const os = __importStar(require("node:os"));
45
45
  const nest_app_1 = require("../../lib/nest-app");
46
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
46
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
47
47
  const cli_ssh_service_1 = require("../../services/cli-ssh.service");
48
48
  const context_banner_1 = require("../../lib/context-banner");
49
49
  class EnvRepairSshCa extends core_1.Command {
@@ -71,11 +71,11 @@ class EnvRepairSshCa extends core_1.Command {
71
71
  }
72
72
  const app = await (0, nest_app_1.getNestApp)();
73
73
  try {
74
- const obs = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
74
+ const obs = app.get(cli_control_cluster_service_1.CliControlClusterService);
75
75
  const ssh = app.get(cli_ssh_service_1.CliSshService);
76
- const cluster = await obs.getObservabilityCluster();
76
+ const cluster = await obs.getControlCluster();
77
77
  if (!cluster?.masterIpAddress) {
78
- this.log(chalk_1.default.red(' No observability cluster found in this profile.'));
78
+ this.log(chalk_1.default.red(' No control cluster found in this profile.'));
79
79
  this.exit(1);
80
80
  return;
81
81
  }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class EnvRepairStorage extends Command {
3
+ static readonly description = "Backfill the shared-storage volume id into the cluster DB. Repairs clusters whose flui-secrets/DB never received FLUI_SHARED_STORAGE_VOLUME_ID at create \u2014 symptom is sharedStorageVolumeId NULL in the DB and \"no Flui-managed shared storage volume\" on storage expand. Reads the volume id from the active profile, patches flui-secrets over SSH, then restarts flui-api so the bootstrap seeder backfills the DB.";
4
+ static readonly examples: string[];
5
+ static readonly flags: {
6
+ 'no-restart': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const nest_app_1 = require("../../lib/nest-app");
10
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
11
+ const cli_ssh_service_1 = require("../../services/cli-ssh.service");
12
+ const context_banner_1 = require("../../lib/context-banner");
13
+ class EnvRepairStorage extends core_1.Command {
14
+ async run() {
15
+ const { flags } = await this.parse(EnvRepairStorage);
16
+ (0, context_banner_1.printContextBanner)();
17
+ const app = await (0, nest_app_1.getNestApp)();
18
+ try {
19
+ const control = app.get(cli_control_cluster_service_1.CliControlClusterService);
20
+ const ssh = app.get(cli_ssh_service_1.CliSshService);
21
+ const cluster = await control.getControlCluster();
22
+ if (!cluster?.masterIpAddress) {
23
+ this.log(chalk_1.default.red(' No control cluster found in this profile.'));
24
+ this.exit(1);
25
+ return;
26
+ }
27
+ const volumeId = cluster.sharedStorageVolumeId;
28
+ const sizeGb = cluster.sharedStorageVolumeSizeGb;
29
+ if (!volumeId) {
30
+ this.log(chalk_1.default.red(' No shared-storage volume id in this profile — nothing to backfill.'));
31
+ this.exit(1);
32
+ return;
33
+ }
34
+ const b64 = (s) => Buffer.from(s).toString('base64');
35
+ const ops = [
36
+ `{"op":"add","path":"/data/FLUI_SHARED_STORAGE_VOLUME_ID","value":"${b64(volumeId)}"}`,
37
+ ];
38
+ if (sizeGb) {
39
+ ops.push(`{"op":"add","path":"/data/FLUI_SHARED_STORAGE_VOLUME_GB","value":"${b64(String(sizeGb))}"}`);
40
+ }
41
+ const patchSpinner = (0, ora_1.default)(`Patching flui-secrets on ${cluster.masterIpAddress}...`).start();
42
+ const patchCmd = `kubectl -n flui-system patch secret flui-secrets ` +
43
+ `--type='json' -p='[${ops.join(',')}]'`;
44
+ try {
45
+ await ssh.sshExec(cluster.masterIpAddress, patchCmd);
46
+ patchSpinner.succeed(`Secret patched (FLUI_SHARED_STORAGE_VOLUME_ID=${volumeId})`);
47
+ }
48
+ catch (err) {
49
+ patchSpinner.fail(`Patch failed: ${err.message}`);
50
+ this.exit(1);
51
+ return;
52
+ }
53
+ if (!flags['no-restart']) {
54
+ const restartSpinner = (0, ora_1.default)('Restarting flui-api...').start();
55
+ try {
56
+ await ssh.sshExec(cluster.masterIpAddress, 'kubectl -n flui-system rollout restart deployment/flui-api');
57
+ restartSpinner.succeed('flui-api rolling restart triggered');
58
+ }
59
+ catch (err) {
60
+ restartSpinner.warn(`Restart failed (Secret was patched OK): ${err.message}`);
61
+ }
62
+ }
63
+ this.log('');
64
+ this.log(chalk_1.default.green(' ✅ Storage repair complete. On flui-api boot the seeder backfills sharedStorageVolumeId in the DB.'));
65
+ this.log(chalk_1.default.dim(' Verify with `flui env storage` or the storage-expand endpoint once flui-api is back up.\n'));
66
+ }
67
+ finally {
68
+ await (0, nest_app_1.closeNestApp)();
69
+ }
70
+ }
71
+ }
72
+ EnvRepairStorage.description = 'Backfill the shared-storage volume id into the cluster DB. Repairs clusters whose flui-secrets/DB never received FLUI_SHARED_STORAGE_VOLUME_ID at create — symptom is sharedStorageVolumeId NULL in the DB and "no Flui-managed shared storage volume" on storage expand. Reads the volume id from the active profile, patches flui-secrets over SSH, then restarts flui-api so the bootstrap seeder backfills the DB.';
73
+ EnvRepairStorage.examples = [
74
+ '<%= config.bin %> <%= command.id %>',
75
+ '<%= config.bin %> <%= command.id %> --no-restart',
76
+ ];
77
+ EnvRepairStorage.flags = {
78
+ 'no-restart': core_1.Flags.boolean({
79
+ description: 'Skip the flui-api rolling restart after patching the Secret',
80
+ }),
81
+ };
82
+ exports.default = EnvRepairStorage;
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class EnvRestart extends Command {
3
- static readonly description = "Restart stopped observability cluster servers";
3
+ static readonly description = "Restart stopped control cluster servers";
4
4
  static readonly examples: string[];
5
5
  run(): Promise<void>;
6
6
  /**
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
7
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
- const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
10
+ const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
11
11
  const hetzner_provider_service_1 = require("../../../../src/modules/providers/services/hetzner-provider.service");
12
12
  const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
13
13
  const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
@@ -20,14 +20,14 @@ class EnvRestart extends core_1.Command {
20
20
  // Bootstrap NestJS and get services
21
21
  const app = await (0, nest_app_1.getNestApp)();
22
22
  spinner.succeed('Initialized');
23
- console.log(chalk_1.default.cyan('\n🔄 Restarting Observability Cluster\n'));
23
+ console.log(chalk_1.default.cyan('\n🔄 Restarting Control Cluster\n'));
24
24
  spinner = (0, ora_1.default)('Finding cluster...').start();
25
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
26
- // Get observability cluster
27
- const cluster = await observabilityService.getObservabilityCluster();
25
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
26
+ // Get control cluster
27
+ const cluster = await controlService.getControlCluster();
28
28
  if (!cluster) {
29
- spinner.fail('No observability cluster found');
30
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
29
+ spinner.fail('No control cluster found');
30
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
31
31
  console.log(`Create one with: ${chalk_1.default.cyan('flui env create')}\n`);
32
32
  return;
33
33
  }
@@ -117,7 +117,7 @@ class EnvRestart extends core_1.Command {
117
117
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
118
118
  spinner = (0, ora_1.default)(`Checking services (attempt ${attempt}/${maxAttempts})...`).start();
119
119
  try {
120
- const servicesHealth = await observabilityService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
120
+ const servicesHealth = await controlService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
121
121
  const allHealthy = servicesHealth.prometheus === 'healthy' &&
122
122
  servicesHealth.grafana === 'healthy' &&
123
123
  servicesHealth.loki === 'healthy';
@@ -182,6 +182,6 @@ class EnvRestart extends core_1.Command {
182
182
  return new Promise((resolve) => setTimeout(resolve, ms));
183
183
  }
184
184
  }
185
- EnvRestart.description = 'Restart stopped observability cluster servers';
185
+ EnvRestart.description = 'Restart stopped control cluster servers';
186
186
  EnvRestart.examples = ['<%= config.bin %> <%= command.id %>'];
187
187
  exports.default = EnvRestart;
@@ -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 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 cluster_node_scaling_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-node-scaling.service");
13
13
  const cluster_capacity_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-capacity.service");
14
14
  const cli_node_repository_1 = require("../../lib/repositories/cli-node.repository");
@@ -21,13 +21,13 @@ class EnvScaleMaster extends core_1.Command {
21
21
  const spinner = (0, ora_1.default)('Preparing scale-master plan...').start();
22
22
  try {
23
23
  const app = await (0, nest_app_1.getNestApp)();
24
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
24
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
25
25
  const nodeRepo = app.get(cli_node_repository_1.CliNodeRepository);
26
26
  const capacityService = app.get(cluster_capacity_service_1.ClusterCapacityService);
27
27
  const scalingService = app.get(cluster_node_scaling_service_1.ClusterNodeScalingService);
28
- const cluster = await observabilityService.getObservabilityCluster();
28
+ const cluster = await controlService.getControlCluster();
29
29
  if (!cluster) {
30
- spinner.fail('No observability cluster found');
30
+ spinner.fail('No control cluster found');
31
31
  return;
32
32
  }
33
33
  const nodes = await nodeRepo.find({ where: { clusterId: cluster.id } });
@@ -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 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 cluster_node_scaling_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-node-scaling.service");
13
13
  const cluster_capacity_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-capacity.service");
14
14
  const cli_node_repository_1 = require("../../lib/repositories/cli-node.repository");
@@ -20,13 +20,13 @@ class EnvScaleNode extends core_1.Command {
20
20
  const spinner = (0, ora_1.default)('Preparing scale-node plan...').start();
21
21
  try {
22
22
  const app = await (0, nest_app_1.getNestApp)();
23
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
23
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
24
24
  const nodeRepo = app.get(cli_node_repository_1.CliNodeRepository);
25
25
  const capacityService = app.get(cluster_capacity_service_1.ClusterCapacityService);
26
26
  const scalingService = app.get(cluster_node_scaling_service_1.ClusterNodeScalingService);
27
- const cluster = await observabilityService.getObservabilityCluster();
27
+ const cluster = await controlService.getControlCluster();
28
28
  if (!cluster) {
29
- spinner.fail('No observability cluster found');
29
+ spinner.fail('No control cluster found');
30
30
  return;
31
31
  }
32
32
  const nodes = await nodeRepo.find({ where: { clusterId: cluster.id } });
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class EnvSetMasterProtection extends Command {
3
+ static readonly description: string;
4
+ static readonly examples: string[];
5
+ static readonly args: {
6
+ action: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ run(): Promise<void>;
9
+ private countWorkers;
10
+ private masterTainted;
11
+ private protectionLabel;
12
+ private showState;
13
+ private turnOn;
14
+ private turnOff;
15
+ private persist;
16
+ }