@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
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * `flui version` — surfaces the CLI version AND the platform release it pins.
4
+ *
5
+ * The CLI's npm version is decoupled from the platform release (carried in
6
+ * RELEASE), so this command makes the mapping explicit: which bootstrap ref and
7
+ * which component image tags a `flui env create` would install. Handy when
8
+ * debugging an install ("which versions did this CLI actually use?").
9
+ */
10
+ export default class Version extends Command {
11
+ static readonly description = "Show the CLI version and the platform release it pins (component image tags + bootstrap ref). Useful for debugging an install.";
12
+ static readonly examples: string[];
13
+ static readonly flags: {
14
+ latest: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,85 @@
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 release_config_1 = require("../../../src/config/release.config");
9
+ const bootstrap_config_1 = require("../config/bootstrap.config");
10
+ /**
11
+ * `flui version` — surfaces the CLI version AND the platform release it pins.
12
+ *
13
+ * The CLI's npm version is decoupled from the platform release (carried in
14
+ * RELEASE), so this command makes the mapping explicit: which bootstrap ref and
15
+ * which component image tags a `flui env create` would install. Handy when
16
+ * debugging an install ("which versions did this CLI actually use?").
17
+ */
18
+ class Version extends core_1.Command {
19
+ async run() {
20
+ const { flags } = await this.parse(Version);
21
+ const useLatest = flags.latest;
22
+ const tags = (0, release_config_1.resolveImageTags)(useLatest);
23
+ const bootstrapRef = (0, release_config_1.resolveBootstrapRef)(useLatest);
24
+ const scriptsBaseUrl = (0, bootstrap_config_1.getScriptsBaseUrl)(useLatest);
25
+ const urlOverride = process.env.BOOTSTRAP_SCRIPTS_URL ?? null;
26
+ const images = {
27
+ 'flui-api': `ghcr.io/flui-cloud/core:${tags.fluiApi}`,
28
+ 'flui-web': `ghcr.io/flui-cloud/dashboard:${tags.fluiWeb}`,
29
+ 'flui-authz': `ghcr.io/flui-cloud/flui-authz:${tags.fluiAuthz}`,
30
+ };
31
+ if (flags.json) {
32
+ this.log(JSON.stringify({
33
+ cli: this.config.version,
34
+ mode: useLatest ? 'latest' : 'pinned',
35
+ platform: useLatest ? null : release_config_1.RELEASE.version,
36
+ bootstrapRef,
37
+ scriptsBaseUrl,
38
+ bootstrapUrlOverride: urlOverride,
39
+ images: tags,
40
+ }, null, 2));
41
+ return;
42
+ }
43
+ const label = (s) => chalk_1.default.dim(s.padEnd(15));
44
+ this.log('');
45
+ this.log(chalk_1.default.bold('Flui CLI'));
46
+ this.log('');
47
+ this.log(` ${label('CLI')}${chalk_1.default.cyan(this.config.version)} ${chalk_1.default.dim(`(${this.config.name})`)}`);
48
+ this.log(` ${label('Platform')}${useLatest
49
+ ? chalk_1.default.yellow('latest (mobile tags)')
50
+ : `${chalk_1.default.cyan(release_config_1.RELEASE.version)}${chalk_1.default.dim(' (pinned release)')}`}`);
51
+ this.log(` ${label('Bootstrap ref')}${chalk_1.default.cyan(bootstrapRef)}`);
52
+ this.log('');
53
+ this.log(chalk_1.default.dim(' Component images:'));
54
+ for (const [name, ref] of Object.entries(images)) {
55
+ this.log(` ${chalk_1.default.dim(name.padEnd(12))}${ref}`);
56
+ }
57
+ this.log('');
58
+ this.log(` ${label('Scripts URL')}${chalk_1.default.dim(scriptsBaseUrl)}`);
59
+ if (urlOverride) {
60
+ this.log(` ${label('')}${chalk_1.default.yellow('↑ overridden via BOOTSTRAP_SCRIPTS_URL')}`);
61
+ }
62
+ if (!useLatest) {
63
+ this.log('');
64
+ this.log(chalk_1.default.dim(' Tip: `flui version --latest` shows what `env create --latest` would use.'));
65
+ }
66
+ this.log('');
67
+ }
68
+ }
69
+ Version.description = 'Show the CLI version and the platform release it pins (component image tags + bootstrap ref). Useful for debugging an install.';
70
+ Version.examples = [
71
+ '<%= config.bin %> <%= command.id %>',
72
+ '<%= config.bin %> <%= command.id %> --latest',
73
+ '<%= config.bin %> <%= command.id %> --json',
74
+ ];
75
+ Version.flags = {
76
+ latest: core_1.Flags.boolean({
77
+ description: 'Show what `env create --latest` would resolve to (mobile tags: bootstrap master, :latest images) instead of the pinned release',
78
+ default: false,
79
+ }),
80
+ json: core_1.Flags.boolean({
81
+ description: 'Output as JSON',
82
+ default: false,
83
+ }),
84
+ };
85
+ exports.default = Version;
@@ -2,8 +2,17 @@
2
2
  * Bootstrap Scripts Configuration
3
3
  *
4
4
  * Configuration for downloading initialization scripts from GitHub.
5
- * Scripts are hosted in the flui-cloud/flui-bootstrap repository.
5
+ * Scripts are hosted in the flui-cloud/bootstrap-scripts repository.
6
6
  */
7
+ /**
8
+ * Base URL for the bootstrap scripts directory.
9
+ *
10
+ * Precedence:
11
+ * 1. `BOOTSTRAP_SCRIPTS_URL` env — full override (dev/CI escape hatch), wins over all.
12
+ * 2. Otherwise derived from the release pin: `<repo>/<ref>/scripts`, where the
13
+ * ref is the pinned release tag, or `master` when `useLatest`.
14
+ */
15
+ export declare function getScriptsBaseUrl(useLatest?: boolean): string;
7
16
  export interface BootstrapConfig {
8
17
  /**
9
18
  * Base URL for downloading scripts
@@ -3,17 +3,34 @@
3
3
  * Bootstrap Scripts Configuration
4
4
  *
5
5
  * Configuration for downloading initialization scripts from GitHub.
6
- * Scripts are hosted in the flui-cloud/flui-bootstrap repository.
6
+ * Scripts are hosted in the flui-cloud/bootstrap-scripts repository.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.BOOTSTRAP_CONFIG = void 0;
10
+ exports.getScriptsBaseUrl = getScriptsBaseUrl;
10
11
  exports.getScriptUrl = getScriptUrl;
12
+ const release_config_1 = require("../../../src/config/release.config");
13
+ const BOOTSTRAP_REPO_RAW_BASE = 'https://raw.githubusercontent.com/flui-cloud/bootstrap-scripts';
14
+ /**
15
+ * Base URL for the bootstrap scripts directory.
16
+ *
17
+ * Precedence:
18
+ * 1. `BOOTSTRAP_SCRIPTS_URL` env — full override (dev/CI escape hatch), wins over all.
19
+ * 2. Otherwise derived from the release pin: `<repo>/<ref>/scripts`, where the
20
+ * ref is the pinned release tag, or `master` when `useLatest`.
21
+ */
22
+ function getScriptsBaseUrl(useLatest = false) {
23
+ if (process.env.BOOTSTRAP_SCRIPTS_URL) {
24
+ return process.env.BOOTSTRAP_SCRIPTS_URL;
25
+ }
26
+ return `${BOOTSTRAP_REPO_RAW_BASE}/${(0, release_config_1.resolveBootstrapRef)(useLatest)}/scripts`;
27
+ }
11
28
  /**
12
29
  * Default bootstrap configuration
13
30
  */
14
31
  exports.BOOTSTRAP_CONFIG = {
15
- scriptsBaseUrl: process.env.BOOTSTRAP_SCRIPTS_URL ||
16
- 'https://raw.githubusercontent.com/flui-cloud/bootstrap-scripts/master/scripts',
32
+ // Pinned-release default; per-install resolution goes through getScriptsBaseUrl().
33
+ scriptsBaseUrl: getScriptsBaseUrl(false),
17
34
  scripts: {
18
35
  fluiInit: 'flui-init.sh',
19
36
  k3sMaster: 'k3s-master-init.sh',
@@ -22,7 +39,7 @@ exports.BOOTSTRAP_CONFIG = {
22
39
  repository: {
23
40
  org: 'flui-cloud',
24
41
  name: 'bootstrap-scripts',
25
- branch: 'master',
42
+ branch: (0, release_config_1.resolveBootstrapRef)(false),
26
43
  },
27
44
  };
28
45
  /**
@@ -16,15 +16,15 @@ exports.isPreferenceKey = isPreferenceKey;
16
16
  exports.PREFERENCES = {
17
17
  email: {
18
18
  key: 'email',
19
- description: 'Contact email used for ACME/Let\'s Encrypt and operational notifications',
19
+ description: "Contact email used for ACME/Let's Encrypt and operational notifications",
20
20
  envVar: 'FLUI_EMAIL',
21
21
  projectOverridable: true,
22
22
  required: true,
23
- validate: (v) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : 'Not a valid email'),
23
+ validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : 'Not a valid email',
24
24
  },
25
25
  apiPath: {
26
26
  key: 'apiPath',
27
- description: 'Path to the flui.api repo, used to locate the .env file written by env export-config',
27
+ description: 'Path to the flui-core repo, used to locate the .env file written by env export-config',
28
28
  envVar: 'FLUI_API_PATH',
29
29
  projectOverridable: true,
30
30
  defaultValue: '.',
@@ -32,10 +32,10 @@ exports.PREFERENCES = {
32
32
  },
33
33
  dashboardPath: {
34
34
  key: 'dashboardPath',
35
- description: 'Path to the flui.dashboard repo, used when syncing its config.json',
35
+ description: 'Path to the flui-dashboard repo, used when syncing its config.json',
36
36
  envVar: 'FLUI_DASHBOARD_PATH',
37
37
  projectOverridable: true,
38
- defaultValue: '../flui.dashboard',
38
+ defaultValue: '../flui-dashboard',
39
39
  required: false,
40
40
  },
41
41
  certificateMode: {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Release manifest — single source of truth for the versions Flui pins at
3
+ * install time. Bumped by hand on each CLI release.
4
+ *
5
+ * As long as the installed CLI stays at this version, every component a fresh
6
+ * cluster receives is pinned: the bootstrap scripts/manifests (a git ref on
7
+ * flui-cloud/bootstrap-scripts) and the Docker image tags injected into the
8
+ * cluster. `flui env create --latest` opts back into the mobile dev behaviour
9
+ * (bootstrap ref `master`, `:latest` image tags).
10
+ */
11
+ export interface ComponentImageTags {
12
+ /** ghcr.io/flui-cloud/core */
13
+ fluiApi: string;
14
+ /** ghcr.io/flui-cloud/dashboard */
15
+ fluiWeb: string;
16
+ /** ghcr.io/flui-cloud/flui-authz */
17
+ fluiAuthz: string;
18
+ }
19
+ export interface ReleaseManifest {
20
+ /** Platform release version, recorded on the cluster at install. */
21
+ version: string;
22
+ /** Git ref (tag) on flui-cloud/bootstrap-scripts holding scripts + manifests. */
23
+ bootstrapRef: string;
24
+ /** Pinned Docker image tags, per Flui component. */
25
+ images: ComponentImageTags;
26
+ }
27
+ export declare const RELEASE: ReleaseManifest;
28
+ /** Bootstrap-scripts git ref to install from. `--latest` → `master`. */
29
+ export declare function resolveBootstrapRef(useLatest: boolean): string;
30
+ /** Docker image tags to deploy. `--latest` → all `latest`. */
31
+ export declare function resolveImageTags(useLatest: boolean): ComponentImageTags;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /**
3
+ * Release manifest — single source of truth for the versions Flui pins at
4
+ * install time. Bumped by hand on each CLI release.
5
+ *
6
+ * As long as the installed CLI stays at this version, every component a fresh
7
+ * cluster receives is pinned: the bootstrap scripts/manifests (a git ref on
8
+ * flui-cloud/bootstrap-scripts) and the Docker image tags injected into the
9
+ * cluster. `flui env create --latest` opts back into the mobile dev behaviour
10
+ * (bootstrap ref `master`, `:latest` image tags).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.RELEASE = void 0;
14
+ exports.resolveBootstrapRef = resolveBootstrapRef;
15
+ exports.resolveImageTags = resolveImageTags;
16
+ exports.RELEASE = {
17
+ version: '0.5.0',
18
+ bootstrapRef: 'v0.5.0',
19
+ images: {
20
+ fluiApi: '0.5.0',
21
+ fluiWeb: '0.5.0',
22
+ fluiAuthz: '0.5.0',
23
+ },
24
+ };
25
+ const LATEST_BOOTSTRAP_REF = 'master';
26
+ const LATEST_IMAGE_TAGS = {
27
+ fluiApi: 'latest',
28
+ fluiWeb: 'latest',
29
+ fluiAuthz: 'latest',
30
+ };
31
+ /** Bootstrap-scripts git ref to install from. `--latest` → `master`. */
32
+ function resolveBootstrapRef(useLatest) {
33
+ return useLatest ? LATEST_BOOTSTRAP_REF : exports.RELEASE.bootstrapRef;
34
+ }
35
+ /** Docker image tags to deploy. `--latest` → all `latest`. */
36
+ function resolveImageTags(useLatest) {
37
+ return useLatest ? { ...LATEST_IMAGE_TAGS } : { ...exports.RELEASE.images };
38
+ }
@@ -15,12 +15,7 @@ export declare function selectWithArrows(title: string, items: ArrowSelectItem[]
15
15
  * Supports both typing and paste (Cmd+V), including bracketed paste mode.
16
16
  */
17
17
  export declare function promptMaskedInput(message: string): Promise<string>;
18
- /**
19
- * Interactive provider setup wizard.
20
- * Shows arrow-key provider selection, prompts for token, saves to ConfigStorage.
21
- * Returns true if setup completed successfully, false if cancelled.
22
- */
23
- export declare function runProviderSetupWizard(): Promise<boolean>;
18
+ export declare function runProviderSetupWizard(preselectedProviderId?: string): Promise<string | null>;
24
19
  /**
25
20
  * Prompt user for yes/no confirmation
26
21
  */
@@ -226,7 +226,16 @@ async function promptMaskedInput(message) {
226
226
  * Shows arrow-key provider selection, prompts for token, saves to ConfigStorage.
227
227
  * Returns true if setup completed successfully, false if cancelled.
228
228
  */
229
- async function runProviderSetupWizard() {
229
+ async function pickProviderToConfigure(preselectedProviderId) {
230
+ // When the target provider is already known (explicit --provider, or a
231
+ // single-provider profile with missing creds), configure it directly.
232
+ const preselected = preselectedProviderId
233
+ ? SUPPORTED_PROVIDERS.find((p) => p.id === preselectedProviderId && p.available)
234
+ : undefined;
235
+ if (preselected) {
236
+ console.log(chalk_1.default.yellow(`\n⚠ No ${preselected.label} API token configured.`));
237
+ return preselected;
238
+ }
230
239
  console.log(chalk_1.default.yellow('\n⚠ No cloud provider API token configured.'));
231
240
  const items = SUPPORTED_PROVIDERS.map((p) => ({
232
241
  label: p.label,
@@ -236,15 +245,11 @@ async function runProviderSetupWizard() {
236
245
  const index = await selectWithArrows('Select a provider to configure:', items);
237
246
  if (index === -1) {
238
247
  console.log(chalk_1.default.dim('\n Cancelled. Run: flui config set hetzner YOUR_TOKEN\n'));
239
- return false;
240
- }
241
- const selectedProvider = SUPPORTED_PROVIDERS[index];
242
- const schema = (0, provider_credential_schemas_1.getCredentialSchema)(selectedProvider.id);
243
- if (!schema) {
244
- console.log(chalk_1.default.red(`\n Missing credential schema for ${selectedProvider.label}\n`));
245
- return false;
248
+ return null;
246
249
  }
247
- console.log(chalk_1.default.dim(`\n Enter your ${selectedProvider.label} credentials below.\n They will be stored encrypted on disk.\n`));
250
+ return SUPPORTED_PROVIDERS[index];
251
+ }
252
+ async function collectProviderCredentials(schema) {
248
253
  const collected = {};
249
254
  for (const field of schema.fields) {
250
255
  const label = ` ${field.label}`;
@@ -255,17 +260,32 @@ async function runProviderSetupWizard() {
255
260
  })).trim();
256
261
  if (!value.trim()) {
257
262
  console.log(chalk_1.default.red(`\n ${field.label} is required. Setup cancelled.\n`));
258
- return false;
263
+ return null;
259
264
  }
260
265
  collected[field.key] = value.trim();
261
266
  }
267
+ return collected;
268
+ }
269
+ async function runProviderSetupWizard(preselectedProviderId) {
270
+ const selectedProvider = await pickProviderToConfigure(preselectedProviderId);
271
+ if (!selectedProvider)
272
+ return null;
273
+ const schema = (0, provider_credential_schemas_1.getCredentialSchema)(selectedProvider.id);
274
+ if (!schema) {
275
+ console.log(chalk_1.default.red(`\n Missing credential schema for ${selectedProvider.label}\n`));
276
+ return null;
277
+ }
278
+ console.log(chalk_1.default.dim(`\n Enter your ${selectedProvider.label} credentials below.\n They will be stored encrypted on disk.\n`));
279
+ const collected = await collectProviderCredentials(schema);
280
+ if (!collected)
281
+ return null;
262
282
  if (selectedProvider.id === 'scaleway') {
263
283
  process.stdout.write(chalk_1.default.dim('\n Validating credentials with Scaleway IAM...'));
264
284
  const result = await (0, scaleway_validator_1.validateScalewayCredentials)(collected.accessKey, collected.secretKey);
265
285
  process.stdout.write('\r\u001B[2K');
266
286
  if (!result.success) {
267
287
  console.log(chalk_1.default.red(` ✖ ${result.message}\n`));
268
- return false;
288
+ return null;
269
289
  }
270
290
  console.log(chalk_1.default.green(` ✔ ${result.message}`));
271
291
  }
@@ -278,11 +298,11 @@ async function runProviderSetupWizard() {
278
298
  storage.saveToken(selectedProvider.id, collected[schema.fields[0].key]);
279
299
  }
280
300
  console.log(chalk_1.default.green(`\n ✔ ${selectedProvider.label} credentials saved (AES-256-GCM encrypted)\n`));
281
- return true;
301
+ return selectedProvider.id;
282
302
  }
283
303
  catch (err) {
284
304
  console.log(chalk_1.default.red(`\n Failed to save credentials: ${err instanceof Error ? err.message : String(err)}\n`));
285
- return false;
305
+ return null;
286
306
  }
287
307
  }
288
308
  /**
@@ -10,6 +10,24 @@ export interface AppSummary {
10
10
  lastDeployedAt?: string;
11
11
  clusterId: string;
12
12
  }
13
+ export interface AppGroupComponent extends AppSummary {
14
+ isPrimary?: boolean;
15
+ }
16
+ export interface AppGroup {
17
+ id: string;
18
+ type: 'standalone' | 'composed';
19
+ name: string;
20
+ slug: string;
21
+ status: string;
22
+ category: string;
23
+ clusterId: string;
24
+ url?: string;
25
+ catalogSlug?: string;
26
+ catalogInstallId?: string;
27
+ primaryComponentId?: string;
28
+ componentCount: number;
29
+ components: AppGroupComponent[];
30
+ }
13
31
  export interface AppRuntime {
14
32
  appId: string;
15
33
  deploymentName: string;
@@ -186,14 +204,29 @@ export interface AvailableVersionsResponse {
186
204
  nextPage: number | null;
187
205
  allowedPatterns: string[] | null;
188
206
  }
207
+ export interface AppEndpoint {
208
+ id: string;
209
+ clusterId: string;
210
+ applicationId?: string;
211
+ endpointType: string;
212
+ hostnameMode: string;
213
+ fqdn: string;
214
+ tlsEnabled: boolean;
215
+ certificateStatus?: string;
216
+ certificateMessage?: string;
217
+ reconciliationStatus?: string;
218
+ errorMessage?: string;
219
+ }
189
220
  export declare class CliAppService {
190
221
  private readonly apiClient;
191
222
  private readonly clusterId;
192
223
  constructor(apiClient: ApiClient, clusterId: string);
193
224
  static create(clusterId: string): Promise<CliAppService>;
194
225
  listApps(): Promise<AppSummary[]>;
226
+ listAppGroups(): Promise<AppGroup[]>;
195
227
  getAppByName(name: string): Promise<AppSummary>;
196
228
  getRuntime(appId: string): Promise<AppRuntime>;
229
+ listEndpoints(applicationId?: string): Promise<AppEndpoint[]>;
197
230
  getLogs(options: AppLogsOptions): Promise<AppLogsResponse>;
198
231
  scale(appId: string, replicas: number): Promise<AppRuntime>;
199
232
  restart(appId: string): Promise<void>;
@@ -21,6 +21,9 @@ class CliAppService {
21
21
  async listApps() {
22
22
  return this.apiClient.get(`/clusters/${this.clusterId}/applications`);
23
23
  }
24
+ async listAppGroups() {
25
+ return this.apiClient.get(`/clusters/${this.clusterId}/applications/grouped`);
26
+ }
24
27
  async getAppByName(name) {
25
28
  const apps = await this.listApps();
26
29
  const app = apps.find((a) => a.name.toLowerCase() === name.toLowerCase() ||
@@ -33,6 +36,12 @@ class CliAppService {
33
36
  async getRuntime(appId) {
34
37
  return this.apiClient.get(`/applications/${appId}/runtime`);
35
38
  }
39
+ async listEndpoints(applicationId) {
40
+ const all = await this.apiClient.get(`/clusters/${this.clusterId}/endpoints`);
41
+ return applicationId
42
+ ? all.filter((e) => e.applicationId === applicationId)
43
+ : all;
44
+ }
36
45
  async getLogs(options) {
37
46
  const params = new URLSearchParams();
38
47
  if (options.app)
@@ -72,7 +72,7 @@ class ReconciliationService {
72
72
  return {
73
73
  type: ReconciliationType.DNS,
74
74
  success: true,
75
- message: 'No observability cluster found, skipping DNS reconciliation',
75
+ message: 'No control cluster found, skipping DNS reconciliation',
76
76
  };
77
77
  }
78
78
  try {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @deprecated Import from shared template instead:
3
- * import { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
3
+ * import { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
4
4
  *
5
5
  * This file is kept for backward compatibility with existing CLI code.
6
6
  * New code should use the shared template.
7
7
  */
8
- export { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS, WORKLOAD_FIREWALL_RULES, WORKLOAD_PORTS, validateFirewallRules, getFirewallRulesForClusterType, } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
8
+ export { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS, WORKLOAD_FIREWALL_RULES, WORKLOAD_PORTS, validateFirewallRules, getFirewallRulesForClusterType, } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
@@ -1,15 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFirewallRulesForClusterType = exports.validateFirewallRules = exports.WORKLOAD_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.OBSERVABILITY_PORTS = exports.OBSERVABILITY_FIREWALL_RULES = void 0;
3
+ exports.getFirewallRulesForClusterType = exports.validateFirewallRules = exports.WORKLOAD_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.OBSERVABILITY_PORTS = exports.CONTROL_FIREWALL_RULES = void 0;
4
4
  /**
5
5
  * @deprecated Import from shared template instead:
6
- * import { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
6
+ * import { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
7
7
  *
8
8
  * This file is kept for backward compatibility with existing CLI code.
9
9
  * New code should use the shared template.
10
10
  */
11
11
  var firewall_rules_template_1 = require("../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template");
12
- Object.defineProperty(exports, "OBSERVABILITY_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.OBSERVABILITY_FIREWALL_RULES; } });
12
+ Object.defineProperty(exports, "CONTROL_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.CONTROL_FIREWALL_RULES; } });
13
13
  Object.defineProperty(exports, "OBSERVABILITY_PORTS", { enumerable: true, get: function () { return firewall_rules_template_1.OBSERVABILITY_PORTS; } });
14
14
  Object.defineProperty(exports, "WORKLOAD_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.WORKLOAD_FIREWALL_RULES; } });
15
15
  Object.defineProperty(exports, "WORKLOAD_PORTS", { enumerable: true, get: function () { return firewall_rules_template_1.WORKLOAD_PORTS; } });
@@ -58,7 +58,7 @@ const label_service_1 = require("../../../src/modules/infrastructure/shared/serv
58
58
  const cli_k3s_script_service_1 = require("../services/cli-k3s-script.service");
59
59
  const cli_cluster_creator_service_1 = require("../services/cli-cluster-creator.service");
60
60
  const cli_clusters_service_1 = require("../services/cli-clusters.service");
61
- const cli_observability_cluster_service_1 = require("../services/cli-observability-cluster.service");
61
+ const cli_control_cluster_service_1 = require("../services/cli-control-cluster.service");
62
62
  const cli_ssh_service_1 = require("../services/cli-ssh.service");
63
63
  const cli_ca_service_1 = require("../services/cli-ca.service");
64
64
  const cli_logger_service_1 = require("../services/cli-logger.service");
@@ -174,7 +174,7 @@ exports.CliInfrastructureModule = CliInfrastructureModule = __decorate([
174
174
  label_service_1.LabelService,
175
175
  kubernetes_service_1.KubernetesService,
176
176
  cli_clusters_service_1.CliClustersService,
177
- cli_observability_cluster_service_1.CliObservabilityClusterService,
177
+ cli_control_cluster_service_1.CliControlClusterService,
178
178
  cli_ssh_service_1.CliSshService,
179
179
  cli_ca_service_1.CliCaService,
180
180
  cli_logger_service_1.CliLoggerService,
@@ -233,7 +233,7 @@ exports.CliInfrastructureModule = CliInfrastructureModule = __decorate([
233
233
  cluster_node_scaling_service_1.ClusterNodeScalingService,
234
234
  ],
235
235
  exports: [
236
- cli_observability_cluster_service_1.CliObservabilityClusterService,
236
+ cli_control_cluster_service_1.CliControlClusterService,
237
237
  cli_clusters_service_1.CliClustersService,
238
238
  kubernetes_service_1.KubernetesService,
239
239
  label_service_1.LabelService,
@@ -61,6 +61,7 @@ const node_child_process_1 = require("node:child_process");
61
61
  const fs = __importStar(require("node:fs"));
62
62
  const path = __importStar(require("node:path"));
63
63
  const os = __importStar(require("node:os"));
64
+ const profile_manager_1 = require("../lib/profile-manager");
64
65
  const https = __importStar(require("node:https"));
65
66
  const hetzner_firewall_service_1 = require("../../../src/modules/providers/services/hetzner-firewall.service");
66
67
  const cli_firewall_repository_1 = require("../lib/repositories/cli-firewall.repository");
@@ -167,7 +168,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
167
168
  provider: cluster.provider,
168
169
  caPublicKey,
169
170
  operationId: operation.id,
170
- deployObservabilityStack: cluster.clusterType === cluster_entity_1.ClusterType.OBSERVABILITY,
171
+ deployObservabilityStack: (0, cluster_entity_1.isControlClusterType)(cluster.clusterType),
171
172
  postgresPassword,
172
173
  redisPassword,
173
174
  grafanaPassword,
@@ -190,6 +191,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
190
191
  clusterFirewallId: firewallId || '',
191
192
  nipIoCertEnabled: !clusterMeta?.zitadelDomain,
192
193
  acmeStaging: !!clusterMeta?.acmeStaging,
194
+ useLatest: !!clusterMeta?.useLatest,
193
195
  nipHostnameToken: cluster.nipHostnameToken || null,
194
196
  envVnet: envVnet
195
197
  ? {
@@ -277,7 +279,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
277
279
  sharedStorageVolumeSizeGb: cluster.sharedStorageVolumeSizeGb ?? undefined,
278
280
  });
279
281
  // Step 3c: Informational — Zitadel PAT injected on demand via sync-auth-domain
280
- if (cluster.clusterType === cluster_entity_1.ClusterType.OBSERVABILITY) {
282
+ if ((0, cluster_entity_1.isControlClusterType)(cluster.clusterType)) {
281
283
  this.log(opId, 'ℹ️ Zitadel service account PAT will be injected when sync-auth-domain is called.');
282
284
  this.log(opId, ' After DNS is configured, call: POST /api/v1/clusters/:id/dns-zone/sync-auth-domain');
283
285
  }
@@ -332,6 +334,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
332
334
  provider: cluster.provider,
333
335
  caPublicKey,
334
336
  operationId: operation.id,
337
+ useLatest: !!clusterMeta?.useLatest,
335
338
  sharedStorage: workerSharedStorage,
336
339
  });
337
340
  const workerServer = await provider.createServer({
@@ -362,12 +365,15 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
362
365
  try {
363
366
  const firewallRecord = await this.firewallRepository.findById(firewallId);
364
367
  if (firewallRecord) {
368
+ // Cluster-scoped name so destroy matches by exact name only.
369
+ const scopedFirewallName = `flui-control-firewall-${cluster.id}`;
365
370
  firewallRecord.clusterId = cluster.id;
371
+ firewallRecord.name = scopedFirewallName;
366
372
  // Get server IDs from cluster nodes
367
373
  const serverIds = cluster.nodes.map((node) => node.providerResourceId);
368
374
  firewallRecord.appliedToServerIds = serverIds;
369
375
  await this.firewallRepository.save(firewallRecord);
370
- // Update Hetzner firewall labels with cluster ID
376
+ // Update Hetzner firewall labels + name with cluster ID
371
377
  try {
372
378
  const existingLabels = Object.fromEntries(firewallRecord.labels.map((l) => [l.key, l.value]));
373
379
  const updatedLabels = {
@@ -375,7 +381,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
375
381
  'flui-cluster-id': cluster.id,
376
382
  'flui-cluster-name': cluster.name,
377
383
  };
378
- await this.firewallService.updateFirewallLabels(firewallId, updatedLabels);
384
+ await this.firewallService.updateFirewallLabels(firewallId, updatedLabels, scopedFirewallName);
379
385
  this.log(opId, `✅ Firewall ${firewallId} labels updated on Hetzner with cluster ID`);
380
386
  }
381
387
  catch (labelError) {
@@ -399,6 +405,23 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
399
405
  }
400
406
  catch (error) {
401
407
  this.log(opId, `Failed to create cluster: ${error.message}`, 'ERROR');
408
+ // Roll back the pre-created firewall. If creation failed before the
409
+ // rename/label step it stays with a temporary name and no
410
+ // flui-cluster-id label, which makes it invisible to `env destroy`
411
+ // (it matches neither the label query nor the scoped-name fallback)
412
+ // and leaks as an orphan that blocks the next run's name.
413
+ const orphanFirewallId = operation.metadata?.firewallId;
414
+ if (orphanFirewallId) {
415
+ try {
416
+ await this.firewallService.deleteFirewall(orphanFirewallId);
417
+ await this.firewallRepository.delete(orphanFirewallId);
418
+ this.log(opId, `✅ Rolled back firewall ${orphanFirewallId} after failed cluster creation`);
419
+ }
420
+ catch (cleanupError) {
421
+ this.log(opId, `Failed to roll back firewall ${orphanFirewallId}: ${cleanupError.message}. ` +
422
+ `Delete it manually on the provider to avoid an orphan.`, 'WARN');
423
+ }
424
+ }
402
425
  // Mark cluster as FAILED
403
426
  cluster.status = cluster_entity_1.ClusterStatus.ERROR;
404
427
  await this.clusterRepository.save(cluster);
@@ -571,8 +594,10 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
571
594
  async createApiCredentialsSecret(operationId, masterIp, bootstrap) {
572
595
  this.log(operationId, 'Patching Kubernetes secret with SSH CA keys + bootstrap vars...');
573
596
  try {
574
- // Load CLI CA keys to share with API for unified SSH access
575
- const caKeyDir = path.join(os.homedir(), '.flui', 'ca');
597
+ // Load CLI CA keys to share with API for unified SSH access.
598
+ // CA keys are profile-scoped (~/.flui/profiles/<profile>/ca), not global —
599
+ // the old global path threw ENOENT and silently skipped the whole patch.
600
+ const caKeyDir = path.join(profile_manager_1.ProfileManager.getProfileDir(), 'ca');
576
601
  const caPrivateKey = fs
577
602
  .readFileSync(path.join(caKeyDir, 'ca_key'), 'utf8')
578
603
  .trim();