@flui-cloud/cli 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/lib/cli/src/commands/app/list.d.ts +3 -0
  2. package/lib/cli/src/commands/app/list.js +72 -18
  3. package/lib/cli/src/commands/app/status.d.ts +1 -0
  4. package/lib/cli/src/commands/app/status.js +27 -2
  5. package/lib/cli/src/commands/cluster/destroy.d.ts +1 -1
  6. package/lib/cli/src/commands/cluster/destroy.js +2 -2
  7. package/lib/cli/src/commands/deploy.d.ts +3 -0
  8. package/lib/cli/src/commands/deploy.js +19 -0
  9. package/lib/cli/src/commands/dev/creds.d.ts +0 -1
  10. package/lib/cli/src/commands/dev/creds.js +6 -27
  11. package/lib/cli/src/commands/dev/tunnel.js +8 -8
  12. package/lib/cli/src/commands/env/capacity.js +4 -4
  13. package/lib/cli/src/commands/env/create.d.ts +4 -1
  14. package/lib/cli/src/commands/env/create.js +78 -52
  15. package/lib/cli/src/commands/env/credentials.js +12 -12
  16. package/lib/cli/src/commands/env/destroy.d.ts +2 -1
  17. package/lib/cli/src/commands/env/destroy.js +45 -28
  18. package/lib/cli/src/commands/env/diag-ca.js +5 -5
  19. package/lib/cli/src/commands/env/export-config.d.ts +0 -17
  20. package/lib/cli/src/commands/env/export-config.js +50 -47
  21. package/lib/cli/src/commands/env/force-ready.d.ts +1 -1
  22. package/lib/cli/src/commands/env/force-ready.js +8 -8
  23. package/lib/cli/src/commands/env/inspect.js +5 -5
  24. package/lib/cli/src/commands/env/refresh-kubeconfig.js +4 -4
  25. package/lib/cli/src/commands/env/repair-ssh-ca.js +4 -4
  26. package/lib/cli/src/commands/env/repair-storage.d.ts +9 -0
  27. package/lib/cli/src/commands/env/repair-storage.js +82 -0
  28. package/lib/cli/src/commands/env/restart.d.ts +1 -1
  29. package/lib/cli/src/commands/env/restart.js +9 -9
  30. package/lib/cli/src/commands/env/scale-master.js +4 -4
  31. package/lib/cli/src/commands/env/scale-node.js +4 -4
  32. package/lib/cli/src/commands/env/set-master-protection.d.ts +16 -0
  33. package/lib/cli/src/commands/env/set-master-protection.js +120 -0
  34. package/lib/cli/src/commands/env/status.d.ts +1 -1
  35. package/lib/cli/src/commands/env/status.js +10 -10
  36. package/lib/cli/src/commands/env/stop.d.ts +1 -1
  37. package/lib/cli/src/commands/env/stop.js +8 -8
  38. package/lib/cli/src/commands/env/storage-expand.js +4 -4
  39. package/lib/cli/src/commands/env/storage.d.ts +1 -1
  40. package/lib/cli/src/commands/env/storage.js +5 -5
  41. package/lib/cli/src/commands/env/sync.js +5 -5
  42. package/lib/cli/src/commands/env/uncordon.js +4 -4
  43. package/lib/cli/src/commands/env/update-firewall.d.ts +13 -1
  44. package/lib/cli/src/commands/env/update-firewall.js +232 -126
  45. package/lib/cli/src/commands/integration/connect.d.ts +1 -0
  46. package/lib/cli/src/commands/integration/connect.js +19 -1
  47. package/lib/cli/src/commands/integration/reset.d.ts +13 -0
  48. package/lib/cli/src/commands/integration/reset.js +95 -0
  49. package/lib/cli/src/commands/integration/setup.d.ts +18 -0
  50. package/lib/cli/src/commands/integration/setup.js +320 -0
  51. package/lib/cli/src/commands/integration/status.d.ts +9 -0
  52. package/lib/cli/src/commands/integration/status.js +117 -0
  53. package/lib/cli/src/commands/node/list.d.ts +1 -0
  54. package/lib/cli/src/commands/node/list.js +19 -2
  55. package/lib/cli/src/commands/server-types/list.d.ts +3 -0
  56. package/lib/cli/src/commands/server-types/list.js +84 -0
  57. package/lib/cli/src/commands/ssh.js +5 -5
  58. package/lib/cli/src/commands/version.d.ts +18 -0
  59. package/lib/cli/src/commands/version.js +100 -0
  60. package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
  61. package/lib/cli/src/config/bootstrap.config.js +24 -4
  62. package/lib/cli/src/config/preferences-schema.js +5 -5
  63. package/lib/cli/src/config/release-override.d.ts +43 -0
  64. package/lib/cli/src/config/release-override.js +203 -0
  65. package/lib/cli/src/config/release.config.d.ts +31 -0
  66. package/lib/cli/src/config/release.config.js +38 -0
  67. package/lib/cli/src/lib/prompts.d.ts +1 -6
  68. package/lib/cli/src/lib/prompts.js +33 -13
  69. package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
  70. package/lib/cli/src/lib/services/cli-app.service.js +9 -0
  71. package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
  72. package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
  73. package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
  74. package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
  75. package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
  76. package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
  77. package/lib/cli/src/services/cli-clusters.service.js +57 -34
  78. package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
  79. package/lib/cli/src/services/cli-control-cluster.service.js +545 -0
  80. package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
  81. package/lib/cli/src/services/cli-endpoint-resolver.service.js +25 -11
  82. package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
  83. package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
  84. package/lib/src/config/release.config.d.ts +28 -0
  85. package/lib/src/config/release.config.js +35 -0
  86. package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
  87. package/lib/src/modules/applications/entities/application.entity.js +12 -0
  88. package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
  89. package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
  90. package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
  91. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
  92. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
  93. package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
  94. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
  95. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
  96. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
  97. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
  98. package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
  99. package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
  100. package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
  101. package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
  102. package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
  103. package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
  104. package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
  105. package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
  106. package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
  107. package/oclif.manifest.json +1201 -854
  108. package/package.json +2 -2
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const core_1 = require("@oclif/core");
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const ora_1 = __importDefault(require("ora"));
42
+ const http = __importStar(require("node:http"));
43
+ const os = __importStar(require("node:os"));
44
+ const api_client_1 = require("../../lib/api-client");
45
+ const config_storage_1 = require("../../lib/config-storage");
46
+ const browser_callback_1 = require("../../lib/browser-callback");
47
+ const prompts_1 = require("../../lib/prompts");
48
+ const PAT_SCOPES = [
49
+ 'repo',
50
+ 'workflow',
51
+ 'user:email',
52
+ 'admin:repo_hook',
53
+ 'write:packages',
54
+ 'read:packages',
55
+ 'delete:packages',
56
+ ];
57
+ const PAT_DEEP_LINK = `https://github.com/settings/tokens/new?scopes=${PAT_SCOPES.join(',')}&description=Flui+CLI`;
58
+ const MANIFEST_POLL_INTERVAL_MS = 2_000;
59
+ const MANIFEST_POLL_TIMEOUT_MS = 10 * 60 * 1000;
60
+ class IntegrationSetup extends core_1.Command {
61
+ async run() {
62
+ const { args, flags } = await this.parse(IntegrationSetup);
63
+ if (args.provider !== 'github') {
64
+ this.error(`Unknown provider "${args.provider}"`, { exit: 1 });
65
+ }
66
+ const configStorage = new config_storage_1.ConfigStorage();
67
+ const apiUrl = configStorage.getApiUrlOrThrow();
68
+ const apiKey = configStorage.getApiKey();
69
+ if (!apiKey) {
70
+ this.error('Not logged in. Run `flui auth login` first.', { exit: 1 });
71
+ }
72
+ const api = new api_client_1.ApiClient({ baseUrl: apiUrl, apiKey });
73
+ const status = await this.fetchStatus(api);
74
+ if (status?.configured) {
75
+ console.log(chalk_1.default.yellow(`\n GitHub integration is already configured (authMethod=${status.authMethod}${status.appSlug ? `, appSlug=${status.appSlug}` : ''}).`));
76
+ const ok = await (0, prompts_1.confirmPrompt)('Overwrite existing configuration?', false);
77
+ if (!ok) {
78
+ console.log(chalk_1.default.dim('\n Cancelled.\n'));
79
+ return;
80
+ }
81
+ }
82
+ const choice = await (0, prompts_1.selectWithArrows)('Choose setup method', [
83
+ { label: 'GitHub App (recommended) — one-click create on GitHub' },
84
+ { label: 'Personal Access Token — paste a classic PAT' },
85
+ ]);
86
+ if (choice === -1) {
87
+ console.log(chalk_1.default.dim('\n Cancelled.\n'));
88
+ return;
89
+ }
90
+ if (choice === 0) {
91
+ await this.runManifestFlow(api, apiUrl, flags.headless);
92
+ }
93
+ else {
94
+ await this.runPatFlow(api, flags.headless);
95
+ }
96
+ }
97
+ async fetchStatus(api) {
98
+ try {
99
+ return await api.get('/repositories/github/setup/status');
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ async runManifestFlow(api, apiUrl, headless) {
106
+ console.log('');
107
+ const defaultName = `flui-${os.hostname().split('.')[0]}`;
108
+ const name = await (0, prompts_1.promptInput)({
109
+ message: 'GitHub App name (must be unique across GitHub)',
110
+ default: defaultName,
111
+ });
112
+ const publicApiUrl = await (0, prompts_1.promptInput)({
113
+ message: 'Flui public URL (must be reachable from github.com; OAuth callback, manifest redirect and webhook URL are derived from this)',
114
+ default: apiUrl.replace(/\/api(\/v\d+)?$/, '').replace(/\/$/, ''),
115
+ validate: (v) => /^https?:\/\//.test(v) ? null : 'Must be an http(s) URL',
116
+ });
117
+ if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)/i.test(publicApiUrl)) {
118
+ console.log(chalk_1.default.yellow(" ! This URL points to localhost — GitHub won't reach the webhook (if enabled) nor complete the OAuth redirect from a different machine. Use a tunnel for development."));
119
+ }
120
+ const webhooksEnabled = await (0, prompts_1.confirmPrompt)('Enable webhooks for deploy-on-push?', false);
121
+ const publicApp = await (0, prompts_1.confirmPrompt)('Allow installation on other accounts / organizations? (off = only the owner account can install)', false);
122
+ const spinner = (0, ora_1.default)('Requesting manifest…').start();
123
+ let manifest;
124
+ try {
125
+ manifest = await api.post('/repositories/github/setup/github-app/manifest-start', { name, webhooksEnabled, publicApp, publicApiUrl });
126
+ spinner.succeed('Manifest ready');
127
+ }
128
+ catch (error) {
129
+ spinner.fail('Failed to request manifest');
130
+ this.printApiError(error);
131
+ this.exit(1);
132
+ }
133
+ const port = await (0, browser_callback_1.findFreeCallbackPort)();
134
+ const localUrl = `http://127.0.0.1:${port}/`;
135
+ const server = this.startManifestSubmitServer(port, manifest);
136
+ try {
137
+ if (headless) {
138
+ console.log('');
139
+ console.log(chalk_1.default.dim(' Open this URL in a browser to create the App:'));
140
+ console.log(` ${chalk_1.default.cyan(localUrl)}`);
141
+ }
142
+ else if ((0, browser_callback_1.openInBrowser)(localUrl)) {
143
+ console.log(chalk_1.default.dim('\n Opened browser. Confirm the App creation on GitHub…'));
144
+ }
145
+ else {
146
+ console.log(chalk_1.default.yellow(`\n Could not open browser. Open this URL manually:\n ${localUrl}\n`));
147
+ }
148
+ const ok = await this.pollUntilConfigured(api);
149
+ if (ok) {
150
+ const fresh = await this.fetchStatus(api);
151
+ console.log(chalk_1.default.green(`\n ✔ GitHub App configured (${chalk_1.default.bold(fresh?.appSlug ?? '?')})\n`));
152
+ console.log(chalk_1.default.dim(` Next: \`flui integration connect github\` to install on your account/org.\n`));
153
+ }
154
+ else {
155
+ console.log(chalk_1.default.red('\n ✖ Timed out waiting for the manifest callback. If the browser already redirected, check the dashboard.\n'));
156
+ this.exit(1);
157
+ }
158
+ }
159
+ finally {
160
+ server.close();
161
+ }
162
+ }
163
+ startManifestSubmitServer(port, manifest) {
164
+ const html = renderManifestSubmitPage(manifest);
165
+ const server = http.createServer((req, res) => {
166
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
167
+ if (url.pathname === '/') {
168
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
169
+ res.end(html);
170
+ return;
171
+ }
172
+ res.writeHead(404);
173
+ res.end();
174
+ });
175
+ server.listen(port, '127.0.0.1');
176
+ return server;
177
+ }
178
+ async pollUntilConfigured(api) {
179
+ const spinner = (0, ora_1.default)('Waiting for GitHub App credentials…').start();
180
+ const start = Date.now();
181
+ while (Date.now() - start < MANIFEST_POLL_TIMEOUT_MS) {
182
+ const status = await this.fetchStatus(api);
183
+ if (status?.configured &&
184
+ status.authMethod === 'github_app' &&
185
+ status.appSlug) {
186
+ spinner.succeed('GitHub App credentials persisted');
187
+ return true;
188
+ }
189
+ await new Promise((resolve) => setTimeout(resolve, MANIFEST_POLL_INTERVAL_MS));
190
+ }
191
+ spinner.fail('Manifest callback never arrived');
192
+ return false;
193
+ }
194
+ async runPatFlow(api, headless) {
195
+ console.log('');
196
+ console.log(chalk_1.default.dim(' Create a classic PAT with the required scopes. The same token covers'));
197
+ console.log(chalk_1.default.dim(' cloning private repos, webhooks, and GHCR container pulls.'));
198
+ console.log(` ${chalk_1.default.cyan(PAT_DEEP_LINK)}`);
199
+ if (!headless) {
200
+ (0, browser_callback_1.openInBrowser)(PAT_DEEP_LINK);
201
+ }
202
+ console.log('');
203
+ let token = '';
204
+ let validation = null;
205
+ while (true) {
206
+ token = await (0, prompts_1.promptMaskedInput)('Paste your PAT');
207
+ if (!token) {
208
+ console.log(chalk_1.default.dim(' Cancelled.'));
209
+ return;
210
+ }
211
+ const spinner = (0, ora_1.default)('Validating token with GitHub…').start();
212
+ try {
213
+ validation = await api.post('/repositories/github/validate-pat', { token });
214
+ spinner.stop();
215
+ }
216
+ catch (error) {
217
+ spinner.fail('Validation failed');
218
+ this.printApiError(error);
219
+ this.exit(1);
220
+ }
221
+ if (!validation?.valid) {
222
+ const label = patErrorLabel(validation?.error, validation?.message);
223
+ console.log(chalk_1.default.red(` ✖ ${label}`));
224
+ const retry = await (0, prompts_1.confirmPrompt)('Try another token?', true);
225
+ if (!retry)
226
+ return;
227
+ continue;
228
+ }
229
+ console.log(chalk_1.default.green(` ✔ Authenticated as @${validation.login}. Scopes: ${(validation.scopes ?? []).join(', ') || '<none>'}`));
230
+ if ((validation.missingScopes?.length ?? 0) > 0) {
231
+ console.log(chalk_1.default.yellow(` ! Missing scopes: ${validation.missingScopes.join(', ')}`));
232
+ const cont = await (0, prompts_1.confirmPrompt)('Save anyway? (webhooks/packages may not work)', false);
233
+ if (!cont)
234
+ continue;
235
+ }
236
+ break;
237
+ }
238
+ const spinner = (0, ora_1.default)('Saving token…').start();
239
+ try {
240
+ await api.post('/repositories/github/setup/pat');
241
+ await api.post('/repositories/github/connect-pat', {
242
+ personalAccessToken: token,
243
+ });
244
+ spinner.succeed('PAT saved');
245
+ }
246
+ catch (error) {
247
+ spinner.fail('Failed to save PAT');
248
+ this.printApiError(error);
249
+ this.exit(1);
250
+ }
251
+ console.log(chalk_1.default.green(`\n ✔ Connected as @${validation.login} via PAT.\n`));
252
+ console.log(chalk_1.default.dim(` Next: \`flui repo list\` to see repositories or \`flui integration status github\` for details.\n`));
253
+ }
254
+ printApiError(error) {
255
+ if (error instanceof api_client_1.ApiError) {
256
+ console.log(chalk_1.default.red(` ${error.statusCode}: ${error.message}`));
257
+ if (error.statusCode === 403) {
258
+ console.log(chalk_1.default.yellow(' Admin privileges required for this operation.'));
259
+ }
260
+ }
261
+ else {
262
+ console.log(chalk_1.default.red(` ${error.message}`));
263
+ }
264
+ }
265
+ }
266
+ IntegrationSetup.description = 'Guided GitHub integration setup (admin). Pick GitHub App (recommended, creates the App on GitHub via manifest flow) or Personal Access Token (validates and saves a token).';
267
+ IntegrationSetup.examples = [
268
+ '<%= config.bin %> <%= command.id %> github',
269
+ '<%= config.bin %> <%= command.id %> github --headless',
270
+ ];
271
+ IntegrationSetup.args = {
272
+ provider: core_1.Args.string({
273
+ description: 'Integration provider (currently only `github`)',
274
+ required: true,
275
+ options: ['github'],
276
+ }),
277
+ };
278
+ IntegrationSetup.flags = {
279
+ headless: core_1.Flags.boolean({
280
+ description: 'Print URLs instead of opening a browser',
281
+ default: false,
282
+ }),
283
+ };
284
+ exports.default = IntegrationSetup;
285
+ function patErrorLabel(error, message) {
286
+ switch (error) {
287
+ case 'invalid_token':
288
+ return 'Invalid token — GitHub rejected it (401).';
289
+ case 'sso_required':
290
+ return 'Token needs SSO authorization for one of your orgs. Authorize on GitHub and try again.';
291
+ case 'empty_token':
292
+ return 'Token is empty.';
293
+ case 'github_unreachable':
294
+ return `Could not reach GitHub: ${message ?? 'unknown error'}`;
295
+ default:
296
+ return `Token validation failed${message ? `: ${message}` : '.'}`;
297
+ }
298
+ }
299
+ function renderManifestSubmitPage(manifest) {
300
+ const manifestJsonEscaped = JSON.stringify(manifest.manifestJson)
301
+ .replace(/&/g, '&amp;')
302
+ .replace(/"/g, '&quot;')
303
+ .replace(/</g, '&lt;')
304
+ .replace(/>/g, '&gt;');
305
+ return `<!doctype html>
306
+ <html>
307
+ <head><meta charset="utf-8"><title>Creating Flui GitHub App…</title>
308
+ <style>body{font-family:system-ui,sans-serif;padding:48px;max-width:520px;margin:auto;color:#1f2937}h2{margin-bottom:8px}p{color:#6b7280}</style>
309
+ </head>
310
+ <body>
311
+ <h2>Redirecting to GitHub…</h2>
312
+ <p>This page will submit the GitHub App manifest. If nothing happens within a few seconds, ensure JavaScript is enabled and forms are not blocked.</p>
313
+ <form id="f" method="POST" action="${manifest.githubUrl}">
314
+ <input type="hidden" name="manifest" value="${manifestJsonEscaped}" />
315
+ <noscript><button type="submit">Continue to GitHub</button></noscript>
316
+ </form>
317
+ <script>document.getElementById('f').submit();</script>
318
+ </body>
319
+ </html>`;
320
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class IntegrationStatus extends Command {
3
+ static readonly description = "Show the current GitHub integration status: configured mode, live health check, and (in PAT mode) your own connection state.";
4
+ static readonly examples: string[];
5
+ static readonly args: {
6
+ provider: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,117 @@
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 api_client_1 = require("../../lib/api-client");
10
+ const config_storage_1 = require("../../lib/config-storage");
11
+ class IntegrationStatus extends core_1.Command {
12
+ async run() {
13
+ const { args } = await this.parse(IntegrationStatus);
14
+ if (args.provider !== 'github') {
15
+ this.error(`Unknown provider "${args.provider}"`, { exit: 1 });
16
+ }
17
+ const configStorage = new config_storage_1.ConfigStorage();
18
+ const apiUrl = configStorage.getApiUrlOrThrow();
19
+ const apiKey = configStorage.getApiKey();
20
+ if (!apiKey) {
21
+ this.error('Not logged in. Run `flui auth login` first.', { exit: 1 });
22
+ }
23
+ const api = new api_client_1.ApiClient({ baseUrl: apiUrl, apiKey });
24
+ const spinner = (0, ora_1.default)('Fetching GitHub integration status…').start();
25
+ let status = null;
26
+ let health = null;
27
+ try {
28
+ [status, health] = await Promise.all([
29
+ api.get('/repositories/github/setup/status'),
30
+ api
31
+ .get('/repositories/github/setup/health')
32
+ .catch((err) => {
33
+ if (err instanceof api_client_1.ApiError && err.statusCode === 403) {
34
+ return null;
35
+ }
36
+ throw err;
37
+ }),
38
+ ]);
39
+ spinner.stop();
40
+ }
41
+ catch (error) {
42
+ spinner.fail('Failed to fetch status');
43
+ if (error instanceof api_client_1.ApiError) {
44
+ console.log(chalk_1.default.red(` ${error.statusCode}: ${error.message}`));
45
+ }
46
+ else {
47
+ console.log(chalk_1.default.red(` ${error.message}`));
48
+ }
49
+ this.exit(1);
50
+ }
51
+ console.log('');
52
+ console.log(chalk_1.default.bold(' Instance configuration'));
53
+ if (!status?.configured) {
54
+ console.log(` ${chalk_1.default.dim('Mode:')} ${chalk_1.default.yellow('not configured')}`);
55
+ console.log('');
56
+ console.log(chalk_1.default.dim(` Run \`flui integration setup github\` to configure it.\n`));
57
+ return;
58
+ }
59
+ console.log(` ${chalk_1.default.dim('Mode:')} ${chalk_1.default.cyan(status.authMethod ?? '?')}`);
60
+ if (status.appSlug) {
61
+ console.log(` ${chalk_1.default.dim('App slug:')} ${status.appSlug}`);
62
+ }
63
+ if (health) {
64
+ const healthSummary = summariseHealth(health);
65
+ console.log(` ${chalk_1.default.dim('Health:')} ${healthSummary}`);
66
+ }
67
+ else {
68
+ console.log(` ${chalk_1.default.dim('Health:')} ${chalk_1.default.dim('(admin only)')}`);
69
+ }
70
+ if (status.authMethod === 'pat') {
71
+ let userStatus = null;
72
+ try {
73
+ userStatus = await api.get('/repositories/github/status');
74
+ }
75
+ catch {
76
+ userStatus = null;
77
+ }
78
+ console.log('');
79
+ console.log(chalk_1.default.bold(' Your connection'));
80
+ if (userStatus?.connected) {
81
+ console.log(` ${chalk_1.default.dim('Authenticated:')} @${userStatus.githubUsername}`);
82
+ if (userStatus.scopes) {
83
+ console.log(` ${chalk_1.default.dim('Scopes:')} ${userStatus.scopes}`);
84
+ }
85
+ }
86
+ else {
87
+ console.log(` ${chalk_1.default.dim('Authenticated:')} ${chalk_1.default.yellow('not connected')}`);
88
+ console.log(chalk_1.default.dim(' Run `flui integration setup github` (PAT branch) to connect.'));
89
+ }
90
+ }
91
+ console.log('');
92
+ }
93
+ }
94
+ IntegrationStatus.description = 'Show the current GitHub integration status: configured mode, live health check, and (in PAT mode) your own connection state.';
95
+ IntegrationStatus.examples = ['<%= config.bin %> <%= command.id %> github'];
96
+ IntegrationStatus.args = {
97
+ provider: core_1.Args.string({
98
+ description: 'Integration provider (currently only `github`)',
99
+ required: true,
100
+ options: ['github'],
101
+ }),
102
+ };
103
+ exports.default = IntegrationStatus;
104
+ function summariseHealth(health) {
105
+ if (!health.ok) {
106
+ const reason = health.details.error ?? 'unknown';
107
+ return chalk_1.default.red(`✖ ${reason}${health.details.message ? ` — ${health.details.message}` : ''}`);
108
+ }
109
+ if (health.mode === 'github_app') {
110
+ const installCount = health.details.installationsCount ?? 0;
111
+ return chalk_1.default.green(`✔ App auth ok — ${installCount} installation${installCount === 1 ? '' : 's'}`);
112
+ }
113
+ if (health.mode === 'pat') {
114
+ return chalk_1.default.green('✔ PAT mode enabled (per-user credentials)');
115
+ }
116
+ return chalk_1.default.dim('—');
117
+ }
@@ -7,5 +7,6 @@ export default class NodeList extends Command {
7
7
  output: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
8
  };
9
9
  run(): Promise<void>;
10
+ private masterProtectionLabel;
10
11
  private colorStatus;
11
12
  }
@@ -13,9 +13,12 @@ class NodeList extends core_1.Command {
13
13
  const { flags } = await this.parse(NodeList);
14
14
  const spinner = (0, ora_1.default)('Fetching nodes...').start();
15
15
  try {
16
- const { id: clusterId } = await (0, resolve_cluster_1.resolveCluster)(flags.cluster);
16
+ const { id: clusterId, entity } = await (0, resolve_cluster_1.resolveCluster)(flags.cluster);
17
17
  const service = await cli_node_service_1.CliNodeService.create(clusterId);
18
18
  const nodes = await service.listNodes();
19
+ const isControlCluster = entity?.clusterType === 'control' ||
20
+ entity?.clusterType === 'observability';
21
+ const masterProtected = !!entity?.metadata?.masterProtection;
19
22
  spinner.stop();
20
23
  if (flags.output === 'json') {
21
24
  console.log(JSON.stringify(nodes, null, 2));
@@ -39,11 +42,18 @@ class NodeList extends core_1.Command {
39
42
  const statusPadded = (node.status || 'unknown').padEnd(12);
40
43
  const statusColored = this.colorStatus(statusPadded);
41
44
  const ip = (node.ipAddress || '-').padEnd(18);
42
- console.log(` ${node.id.padEnd(38)} ${roleColored} ${statusColored} ${ip} ${name}`);
45
+ const taint = node.nodeType === 'master' && masterProtected
46
+ ? chalk_1.default.yellow(' 🔒 control-plane:NoSchedule')
47
+ : '';
48
+ console.log(` ${node.id.padEnd(38)} ${roleColored} ${statusColored} ${ip} ${name}${taint}`);
43
49
  }
44
50
  const workers = nodes.filter((n) => n.nodeType === 'worker').length;
45
51
  console.log('');
46
52
  console.log(chalk_1.default.dim(` ${nodes.length} node${nodes.length === 1 ? '' : 's'} total (1 master, ${workers} worker${workers === 1 ? '' : 's'})`));
53
+ if (isControlCluster) {
54
+ console.log(chalk_1.default.dim(' Master protection: ') +
55
+ this.masterProtectionLabel(workers, masterProtected));
56
+ }
47
57
  console.log('');
48
58
  }
49
59
  catch (error) {
@@ -52,6 +62,13 @@ class NodeList extends core_1.Command {
52
62
  this.exit(1);
53
63
  }
54
64
  }
65
+ masterProtectionLabel(workers, protectedFlag) {
66
+ if (workers === 0)
67
+ return chalk_1.default.dim('n/a (single-node)');
68
+ return protectedFlag
69
+ ? chalk_1.default.green('auto (master tainted)')
70
+ : chalk_1.default.yellow('off');
71
+ }
55
72
  colorStatus(status) {
56
73
  const s = status.trim().toLowerCase();
57
74
  if (s === 'ready')
@@ -5,6 +5,9 @@ export default class ServerTypesList extends Command {
5
5
  static readonly flags: {
6
6
  provider: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
7
  region: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ memory: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ sort: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ desc: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
11
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
12
  'force-refresh': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
13
  };
@@ -10,6 +10,40 @@ const server_type_cache_service_1 = require("../../services/server-type-cache.se
10
10
  const server_type_validator_service_1 = require("../../services/server-type-validator.service");
11
11
  const defaults_1 = require("../../config/defaults");
12
12
  const provider_factory_1 = require("../../../../src/modules/providers/core/factories/provider.factory");
13
+ /**
14
+ * Parse a numeric filter expression into an inclusive {min, max} range.
15
+ * Accepts "16" (exact), "8-32" (range), "8-" (min only), "-32" (max only).
16
+ * Returns null on invalid input.
17
+ */
18
+ function parseNumericRange(raw) {
19
+ const value = raw.trim();
20
+ if (!value)
21
+ return null;
22
+ const num = (s) => {
23
+ if (s === '')
24
+ return undefined;
25
+ const n = Number.parseFloat(s);
26
+ return Number.isFinite(n) && n >= 0 ? n : Number.NaN;
27
+ };
28
+ if (!value.includes('-')) {
29
+ const exact = num(value);
30
+ if (exact === undefined || Number.isNaN(exact))
31
+ return null;
32
+ return { min: exact, max: exact };
33
+ }
34
+ const [minPart, maxPart, ...rest] = value.split('-');
35
+ if (rest.length > 0)
36
+ return null;
37
+ const min = num(minPart);
38
+ const max = num(maxPart);
39
+ if (Number.isNaN(min) || Number.isNaN(max))
40
+ return null;
41
+ if (min === undefined && max === undefined)
42
+ return null;
43
+ if (min !== undefined && max !== undefined && min > max)
44
+ return null;
45
+ return { min, max };
46
+ }
13
47
  class ServerTypesList extends core_1.Command {
14
48
  async run() {
15
49
  const { flags } = await this.parse(ServerTypesList);
@@ -62,6 +96,15 @@ class ServerTypesList extends core_1.Command {
62
96
  return type.locations.some((loc) => loc.name === flags.region);
63
97
  });
64
98
  }
99
+ // Filter by memory (GB): "16" | "8-32" | "8-" | "-32"
100
+ if (flags.memory && serverTypes) {
101
+ const range = parseNumericRange(flags.memory);
102
+ if (!range) {
103
+ throw new Error(`Invalid --memory value "${flags.memory}". Use "16", "8-32", "8-", or "-32".`);
104
+ }
105
+ serverTypes = serverTypes.filter((type) => (range.min === undefined || type.memory >= range.min) &&
106
+ (range.max === undefined || type.memory <= range.max));
107
+ }
65
108
  // Filter out deprecated types
66
109
  const activeTypes = serverTypes?.filter((type) => !type.deprecated) || [];
67
110
  const deprecatedTypes = serverTypes?.filter((type) => type.deprecated) || [];
@@ -69,6 +112,28 @@ class ServerTypesList extends core_1.Command {
69
112
  this.log(chalk_1.default.yellow('No active server types found'));
70
113
  return;
71
114
  }
115
+ // Sort (default: cheapest first). Missing/zero prices sort last —
116
+ // €0.00 means the provider didn't return a price (e.g. bare-metal types).
117
+ const priceOf = (type) => {
118
+ const raw = validatorService.getFormattedPrice(type).monthly;
119
+ const n = raw ? Number.parseFloat(raw) : Number.NaN;
120
+ return Number.isFinite(n) && n > 0 ? n : Number.POSITIVE_INFINITY;
121
+ };
122
+ const dir = flags.desc ? -1 : 1;
123
+ activeTypes.sort((a, b) => {
124
+ switch (flags.sort) {
125
+ case 'memory':
126
+ return (a.memory - b.memory) * dir;
127
+ case 'cores':
128
+ return (a.cores - b.cores) * dir;
129
+ case 'disk':
130
+ return (a.disk - b.disk) * dir;
131
+ case 'name':
132
+ return a.name.localeCompare(b.name) * dir;
133
+ default:
134
+ return (priceOf(a) - priceOf(b)) * dir;
135
+ }
136
+ });
72
137
  // JSON output
73
138
  if (flags.json) {
74
139
  this.log(JSON.stringify({ active: activeTypes, deprecated: deprecatedTypes }, null, 2));
@@ -179,6 +244,11 @@ ServerTypesList.description = 'List available server types for a provider';
179
244
  ServerTypesList.examples = [
180
245
  '<%= config.bin %> <%= command.id %> --provider hetzner',
181
246
  '<%= config.bin %> <%= command.id %> --provider hetzner --region fsn1',
247
+ '<%= config.bin %> <%= command.id %> --provider scaleway --memory 16',
248
+ '<%= config.bin %> <%= command.id %> --provider scaleway --memory 8-32',
249
+ '<%= config.bin %> <%= command.id %> --provider scaleway --memory -32',
250
+ '<%= config.bin %> <%= command.id %> --provider scaleway --sort memory',
251
+ '<%= config.bin %> <%= command.id %> --provider scaleway --sort price --desc',
182
252
  '<%= config.bin %> <%= command.id %> --provider hetzner --json',
183
253
  '<%= config.bin %> <%= command.id %> --provider hetzner --force-refresh',
184
254
  ];
@@ -193,6 +263,20 @@ ServerTypesList.flags = {
193
263
  char: 'r',
194
264
  description: 'Filter by region/location',
195
265
  }),
266
+ memory: core_1.Flags.string({
267
+ char: 'm',
268
+ description: 'Filter by RAM in GB: exact "16", range "8-32", min "8-", or max "-32"',
269
+ }),
270
+ sort: core_1.Flags.string({
271
+ char: 's',
272
+ description: 'Sort by field',
273
+ options: ['price', 'memory', 'cores', 'disk', 'name'],
274
+ default: 'price',
275
+ }),
276
+ desc: core_1.Flags.boolean({
277
+ description: 'Sort in descending order',
278
+ default: false,
279
+ }),
196
280
  json: core_1.Flags.boolean({
197
281
  description: 'Output as JSON',
198
282
  default: false,
@@ -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
  class Ssh extends core_1.Command {
13
13
  async run() {
@@ -15,13 +15,13 @@ class Ssh extends core_1.Command {
15
15
  const spinner = (0, ora_1.default)('Connecting to cluster...').start();
16
16
  try {
17
17
  const app = await (0, nest_app_1.getNestApp)();
18
- const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
18
+ const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
19
19
  const sshService = app.get(cli_ssh_service_1.CliSshService);
20
20
  // Get cluster
21
- const cluster = await observabilityService.getObservabilityCluster();
21
+ const cluster = await controlService.getControlCluster();
22
22
  if (!cluster) {
23
- spinner.fail('No observability cluster found');
24
- console.log(chalk_1.default.yellow('\n⚠️ No observability cluster exists.\n'));
23
+ spinner.fail('No control cluster found');
24
+ console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
25
25
  console.log(chalk_1.default.dim('Create one with:'));
26
26
  console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
27
27
  return;