ninedeploy 0.10.5 → 0.16.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 (149) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1 -0
  3. package/dist/client.d.ts +34 -0
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +81 -22
  6. package/dist/client.js.map +1 -1
  7. package/dist/commands/backupPolicy.d.ts +29 -0
  8. package/dist/commands/backupPolicy.d.ts.map +1 -0
  9. package/dist/commands/backupPolicy.js +111 -0
  10. package/dist/commands/backupPolicy.js.map +1 -0
  11. package/dist/commands/certificates.d.ts +18 -7
  12. package/dist/commands/certificates.d.ts.map +1 -1
  13. package/dist/commands/certificates.js +121 -1
  14. package/dist/commands/certificates.js.map +1 -1
  15. package/dist/commands/databaseAccess.d.ts +37 -0
  16. package/dist/commands/databaseAccess.d.ts.map +1 -0
  17. package/dist/commands/databaseAccess.js +306 -0
  18. package/dist/commands/databaseAccess.js.map +1 -0
  19. package/dist/commands/deploy.d.ts.map +1 -1
  20. package/dist/commands/deploy.js +28 -7
  21. package/dist/commands/deploy.js.map +1 -1
  22. package/dist/commands/githubApps.d.ts +40 -0
  23. package/dist/commands/githubApps.d.ts.map +1 -0
  24. package/dist/commands/githubApps.js +278 -0
  25. package/dist/commands/githubApps.js.map +1 -0
  26. package/dist/commands/housekeeping.d.ts +7 -0
  27. package/dist/commands/housekeeping.d.ts.map +1 -1
  28. package/dist/commands/housekeeping.js +16 -0
  29. package/dist/commands/housekeeping.js.map +1 -1
  30. package/dist/commands/logs.d.ts.map +1 -1
  31. package/dist/commands/logs.js +17 -2
  32. package/dist/commands/logs.js.map +1 -1
  33. package/dist/commands/manage.d.ts +26 -10
  34. package/dist/commands/manage.d.ts.map +1 -1
  35. package/dist/commands/manage.js +219 -65
  36. package/dist/commands/manage.js.map +1 -1
  37. package/dist/commands/manifest.d.ts.map +1 -1
  38. package/dist/commands/manifest.js +9 -1
  39. package/dist/commands/manifest.js.map +1 -1
  40. package/dist/commands/misc.d.ts +4 -2
  41. package/dist/commands/misc.d.ts.map +1 -1
  42. package/dist/commands/misc.js +25 -11
  43. package/dist/commands/misc.js.map +1 -1
  44. package/dist/commands/multiNode.d.ts +55 -0
  45. package/dist/commands/multiNode.d.ts.map +1 -0
  46. package/dist/commands/multiNode.js +438 -0
  47. package/dist/commands/multiNode.js.map +1 -0
  48. package/dist/commands/notifications.d.ts +1 -1
  49. package/dist/commands/operations.d.ts +89 -0
  50. package/dist/commands/operations.d.ts.map +1 -0
  51. package/dist/commands/operations.js +468 -0
  52. package/dist/commands/operations.js.map +1 -0
  53. package/dist/commands/panelBackup.d.ts +28 -0
  54. package/dist/commands/panelBackup.d.ts.map +1 -0
  55. package/dist/commands/panelBackup.js +153 -0
  56. package/dist/commands/panelBackup.js.map +1 -0
  57. package/dist/commands/plugins.d.ts +1 -1
  58. package/dist/commands/plugins.d.ts.map +1 -1
  59. package/dist/commands/plugins.js +7 -3
  60. package/dist/commands/plugins.js.map +1 -1
  61. package/dist/commands/proxyConfig.d.ts +16 -0
  62. package/dist/commands/proxyConfig.d.ts.map +1 -0
  63. package/dist/commands/proxyConfig.js +122 -0
  64. package/dist/commands/proxyConfig.js.map +1 -0
  65. package/dist/commands/secretProviders.d.ts +59 -0
  66. package/dist/commands/secretProviders.d.ts.map +1 -0
  67. package/dist/commands/secretProviders.js +212 -0
  68. package/dist/commands/secretProviders.js.map +1 -0
  69. package/dist/commands/services.d.ts.map +1 -1
  70. package/dist/commands/services.js +3 -5
  71. package/dist/commands/services.js.map +1 -1
  72. package/dist/commands/setup.d.ts +3 -1
  73. package/dist/commands/setup.d.ts.map +1 -1
  74. package/dist/commands/setup.js +19 -3
  75. package/dist/commands/setup.js.map +1 -1
  76. package/dist/commands/sources.d.ts +10 -3
  77. package/dist/commands/sources.d.ts.map +1 -1
  78. package/dist/commands/sources.js +116 -18
  79. package/dist/commands/sources.js.map +1 -1
  80. package/dist/commands/templates.d.ts.map +1 -1
  81. package/dist/commands/templates.js +8 -1
  82. package/dist/commands/templates.js.map +1 -1
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +2 -1
  85. package/dist/config.js.map +1 -1
  86. package/dist/index.js +427 -23
  87. package/dist/index.js.map +1 -1
  88. package/dist/lib/format.d.ts.map +1 -1
  89. package/dist/lib/format.js +5 -4
  90. package/dist/lib/format.js.map +1 -1
  91. package/dist/lib/panelBackupFile.d.ts +3 -0
  92. package/dist/lib/panelBackupFile.d.ts.map +1 -0
  93. package/dist/lib/panelBackupFile.js +71 -0
  94. package/dist/lib/panelBackupFile.js.map +1 -0
  95. package/dist/lib/serverRunner.d.ts +6 -0
  96. package/dist/lib/serverRunner.d.ts.map +1 -1
  97. package/dist/lib/serverRunner.js +18 -3
  98. package/dist/lib/serverRunner.js.map +1 -1
  99. package/dist/prompts.d.ts.map +1 -1
  100. package/dist/prompts.js +9 -2
  101. package/dist/prompts.js.map +1 -1
  102. package/package.json +6 -5
  103. package/src/client.ts +137 -0
  104. package/src/commands/backupPolicy.ts +123 -0
  105. package/src/commands/branding.ts +83 -0
  106. package/src/commands/buildCache.ts +72 -0
  107. package/src/commands/certificates.ts +208 -0
  108. package/src/commands/communityTemplates.ts +97 -0
  109. package/src/commands/configCenter.ts +71 -0
  110. package/src/commands/configPresets.ts +215 -0
  111. package/src/commands/databaseAccess.ts +333 -0
  112. package/src/commands/demo.ts +22 -0
  113. package/src/commands/deploy.ts +284 -0
  114. package/src/commands/doctor.ts +170 -0
  115. package/src/commands/domains.ts +170 -0
  116. package/src/commands/egress.ts +135 -0
  117. package/src/commands/emailTemplates.ts +112 -0
  118. package/src/commands/firewall.ts +82 -0
  119. package/src/commands/githubApps.ts +292 -0
  120. package/src/commands/housekeeping.ts +106 -0
  121. package/src/commands/login.ts +58 -0
  122. package/src/commands/logs.ts +91 -0
  123. package/src/commands/manage.ts +714 -0
  124. package/src/commands/manifest.ts +314 -0
  125. package/src/commands/metrics.ts +69 -0
  126. package/src/commands/misc.ts +307 -0
  127. package/src/commands/multiNode.ts +442 -0
  128. package/src/commands/notifications.ts +174 -0
  129. package/src/commands/operations.ts +519 -0
  130. package/src/commands/panelBackup.ts +167 -0
  131. package/src/commands/pgbouncer.ts +65 -0
  132. package/src/commands/plugins.ts +123 -0
  133. package/src/commands/proxyConfig.ts +117 -0
  134. package/src/commands/secretProviders.ts +228 -0
  135. package/src/commands/server.ts +126 -0
  136. package/src/commands/services.ts +249 -0
  137. package/src/commands/setup.ts +94 -0
  138. package/src/commands/sources.ts +317 -0
  139. package/src/commands/sso.ts +106 -0
  140. package/src/commands/templates.ts +158 -0
  141. package/src/commands/token.ts +66 -0
  142. package/src/commands/webhooks.ts +93 -0
  143. package/src/commands/workspaces.ts +63 -0
  144. package/src/config.ts +53 -0
  145. package/src/index.ts +1273 -0
  146. package/src/lib/format.ts +182 -0
  147. package/src/lib/panelBackupFile.ts +67 -0
  148. package/src/lib/serverRunner.ts +223 -0
  149. package/src/prompts.ts +238 -0
@@ -0,0 +1,170 @@
1
+ import { existsSync, mkdirSync, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import type { NineDeployClient } from '../client.js';
6
+ import { loadConfig } from '../config.js';
7
+ import { c, header, kv } from '../lib/format.js';
8
+ import { getContainerState, isDockerAvailable, isServerReachable, startServerContainer } from '../lib/serverRunner.js';
9
+
10
+ export interface DoctorOptions {
11
+ fix?: boolean;
12
+ }
13
+
14
+ export async function doctorAction(client: NineDeployClient, opts: DoctorOptions = {}): Promise<void> {
15
+ header('NineDeploy Doctor Diagnostics');
16
+
17
+ let issues = 0;
18
+ const prescriptions: string[] = [];
19
+ const fixes: string[] = [];
20
+
21
+ // 1. Environment & Memory Diagnostics
22
+ console.log(` ${c.bold('1. Local Environment & Memory:')}`);
23
+ kv('Node.js', `${process.version} (${process.arch})`);
24
+ kv('Platform', `${os.platform()} (${os.release()})`);
25
+ const totalRamGb = (os.totalmem() / (1024 * 1024 * 1024)).toFixed(1);
26
+ const freeRamGb = (os.freemem() / (1024 * 1024 * 1024)).toFixed(1);
27
+ kv('System Memory', `${freeRamGb} GB free / ${totalRamGb} GB total`);
28
+
29
+ // Swap space check (Linux)
30
+ /* v8 ignore start */
31
+ if (os.platform() === 'linux') {
32
+ try {
33
+ const fs = await import('node:fs/promises');
34
+ const meminfo = await fs.readFile('/proc/meminfo', 'utf8').catch(() => '');
35
+ const swapMatch = meminfo.match(/SwapTotal:\s+(\d+)\s+kB/);
36
+ const swapKb = swapMatch?.[1] ? Number.parseInt(swapMatch[1], 10) : 0;
37
+ const swapMb = Math.round(swapKb / 1024);
38
+ if (swapMb < 1024 && Number.parseFloat(totalRamGb) <= 4) {
39
+ kv('Swap Memory', c.yellow(`${swapMb} MB (Low - recommended ≥ 2GB for heavy Docker pulls)`));
40
+ prescriptions.push('Allocate 2GB swap space: sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile');
41
+ } else {
42
+ kv('Swap Memory', c.green(`${swapMb} MB`));
43
+ }
44
+ } catch {
45
+ /* ignore */
46
+ }
47
+ }
48
+ /* v8 ignore stop */
49
+ console.log();
50
+
51
+ // 2. Docker & Process Engine Diagnostics
52
+ console.log(` ${c.bold('2. Docker & Process Daemon:')}`);
53
+ const dockerOk = await isDockerAvailable();
54
+ if (dockerOk) {
55
+ kv('Docker CLI', c.green('available'));
56
+ const container = await getContainerState('ninedeploy');
57
+ if (container.exists) {
58
+ kv('Docker Container', container.running ? c.green('running') : c.yellow('stopped'));
59
+ if (!container.running) {
60
+ if (opts.fix) {
61
+ try {
62
+ await startServerContainer({});
63
+ fixes.push('Started stopped ninedeploy Docker container.');
64
+ } catch (err) {
65
+ prescriptions.push(`Start server container: ninedeploy server start (${String(err)})`);
66
+ }
67
+ } else {
68
+ prescriptions.push('Start server container: ninedeploy server start');
69
+ }
70
+ }
71
+ } else {
72
+ kv('Docker Container', c.gray('not in use (baremetal / systemd mode)'));
73
+ }
74
+ } else {
75
+ kv('Docker CLI', c.yellow('not found or daemon not running'));
76
+ prescriptions.push('Install or start Docker Engine: sudo systemctl start docker (or https://docs.docker.com/engine/install/)');
77
+ issues++;
78
+ }
79
+ console.log();
80
+
81
+ // 3. Local Storage & Database Integrity
82
+ console.log(` ${c.bold('3. Local Storage & Database:')}`);
83
+ /* v8 ignore start */
84
+ const optDataDir = '/opt/ninedeploy/.data';
85
+ const defaultDataDir = existsSync(path.resolve(process.cwd(), '.data'))
86
+ ? path.resolve(process.cwd(), '.data')
87
+ : (existsSync(optDataDir) ? optDataDir : path.resolve(process.cwd(), '.data'));
88
+ const dbFile = path.join(defaultDataDir, 'ninedeploy.db');
89
+ if (existsSync(defaultDataDir)) {
90
+ kv('.data Directory', c.green(`exists (${defaultDataDir})`));
91
+ if (existsSync(dbFile)) {
92
+ try {
93
+ const stats = statSync(dbFile);
94
+ kv('SQLite Database', c.green(`present (${(stats.size / 1024).toFixed(1)} KB)`));
95
+ } catch {
96
+ kv('SQLite Database', c.yellow('found (unable to read stats)'));
97
+ }
98
+ } else {
99
+ kv('SQLite Database', c.gray('fresh (will be created on first start)'));
100
+ }
101
+ } else {
102
+ kv('.data Directory', c.gray('not present in cwd or /opt/ninedeploy'));
103
+ if (opts.fix) {
104
+ try {
105
+ mkdirSync(defaultDataDir, { recursive: true });
106
+ fixes.push('Created missing .data directory.');
107
+ } catch {
108
+ /* ignore */
109
+ }
110
+ }
111
+ }
112
+ /* v8 ignore stop */
113
+ console.log();
114
+
115
+ // 4. Server Reachability & API Health
116
+ const cfg = loadConfig();
117
+ console.log(` ${c.bold('4. NineDeploy Server:')}`);
118
+ kv('Target URL', cfg.baseUrl);
119
+ const start = Date.now();
120
+ const reachable = await isServerReachable(cfg.baseUrl, 1500);
121
+ const latency = Date.now() - start;
122
+ if (reachable) {
123
+ kv('Connectivity', c.green(`connected (${latency}ms)`));
124
+ } else {
125
+ kv('Connectivity', c.red('unreachable'));
126
+ prescriptions.push(`Ensure NineDeploy is running at ${cfg.baseUrl}: sudo systemctl status ninedeploy (or ninedeploy server start)`);
127
+ issues++;
128
+ }
129
+ console.log();
130
+
131
+ // 5. Authentication Check
132
+ console.log(` ${c.bold('5. Authentication & Credentials:')}`);
133
+ if (!cfg.token) {
134
+ kv('Session', c.yellow('not logged in (run `ninedeploy login` or `ninedeploy setup`)'));
135
+ } else {
136
+ try {
137
+ const user = await client.auth.me();
138
+ kv('Logged In User', `${user.email} (${user.isOperator ? 'operator' : 'member'})`);
139
+ kv('Token Status', c.green('valid'));
140
+ } catch {
141
+ kv('Token Status', c.red('invalid or expired'));
142
+ prescriptions.push('Re-authenticate: ninedeploy login (or ninedeploy setup)');
143
+ issues++;
144
+ }
145
+ }
146
+ console.log();
147
+
148
+ // Summary & Prescriptions
149
+ if (fixes.length > 0) {
150
+ console.log(` ${c.bold('Healed Issues (--fix):')}`);
151
+ for (const fix of fixes) {
152
+ console.log(` ${c.green('✓')} ${fix}`);
153
+ }
154
+ console.log();
155
+ }
156
+
157
+ if (prescriptions.length > 0) {
158
+ console.log(` ${c.bold('Prescriptions & Solutions:')}`);
159
+ for (const rx of prescriptions) {
160
+ console.log(` ${c.cyan('➜')} ${rx}`);
161
+ }
162
+ console.log();
163
+ }
164
+
165
+ if (issues === 0) {
166
+ console.log(` ${c.green('✓ All critical checks passed. NineDeploy CLI is ready.')}\n`);
167
+ } else {
168
+ console.log(` ${c.yellow(`! Found ${issues} issue(s). Check the items marked in red above.`)}\n`);
169
+ }
170
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * `ninedeploy domains preset {list,apply}` — Sprint 2, Gap G-07, PR-D.
3
+ *
4
+ * The plugin (G-07 PR-C) reacts to the audit firehose so a `domain.add`
5
+ * from the panel can create the matching DNS record on its own. This
6
+ * CLI subcommand lets an operator do the same thing on demand without
7
+ * having to round-trip through the panel's domain flow — useful when a
8
+ * record is missing after a partial outage, or when an operator is
9
+ * testing the DNS pipeline against a fresh hostname.
10
+ *
11
+ * Split:
12
+ * - `list` calls `GET /v1/domain-presets` and prints the registered
13
+ * `IDomainProvider` names so the operator can sanity-check the
14
+ * kernel boot before chasing a misconfiguration on the provider
15
+ * side.
16
+ * - `apply <hostname>` calls `POST /v1/domain-presets/apply`, which
17
+ * resolves the active provider from the existing
18
+ * `dns_records_provider` setting, calls `findZoneForHost` +
19
+ * `createRecord` on it, and emits a `domain.preset.manual` audit
20
+ * event so the panel's activity log mirrors what the audit-bus path
21
+ * produces. The `--content` flag lets the operator override the
22
+ * resolved record content (handy for CNAME setups or smoke tests).
23
+ */
24
+ import type { NineDeployClient } from '../client.js';
25
+ import { error, header, info, success } from '../lib/format.js';
26
+
27
+ export interface DomainsPresetApplyOptions {
28
+ /** Optional override for the record content (e.g. an explicit CNAME target). */
29
+ content?: string;
30
+ }
31
+
32
+ // ── `ninedeploy domains preset list` ────────────────────────────────────────
33
+
34
+ /**
35
+ * Pure entry point — same shape used by the CLI action and the unit
36
+ * test. Returns the parsed provider list so the test can assert it
37
+ * without depending on stdout framing.
38
+ */
39
+ export async function domainsPresetList(
40
+ client: NineDeployClient,
41
+ ): Promise<{ providers: string[] }> {
42
+ return await client.domainPresets.list();
43
+ }
44
+
45
+ export async function domainsPresetListAction(client: NineDeployClient): Promise<void> {
46
+ header('Domain presets');
47
+ let result: { providers: string[] };
48
+ try {
49
+ result = await domainsPresetList(client);
50
+ } catch (err) {
51
+ error(err instanceof Error ? err.message : String(err));
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ if (result.providers.length === 0) {
56
+ info('No IDomainProvider drivers are registered on this kernel.');
57
+ info('Set dns_records_provider in Settings → DNS to enable one.');
58
+ return;
59
+ }
60
+ for (const name of result.providers) {
61
+ info(`• ${name}`);
62
+ }
63
+ }
64
+
65
+ // ── `ninedeploy domains preset apply <hostname>` ────────────────────────────
66
+
67
+ /**
68
+ * Pure entry point — same shape used by the CLI action and the unit
69
+ * test. Returns the upstream response so the test can assert it
70
+ * without depending on stdout framing.
71
+ */
72
+ export async function domainsPresetApply(
73
+ client: NineDeployClient,
74
+ hostname: string,
75
+ opts: DomainsPresetApplyOptions = {},
76
+ ): Promise<{
77
+ hostname: string;
78
+ provider: string;
79
+ zone: string;
80
+ recordId: string;
81
+ type: 'A' | 'CNAME';
82
+ content: string;
83
+ }> {
84
+ return await client.domainPresets.apply({ hostname, content: opts.content });
85
+ }
86
+
87
+ export async function domainsPresetApplyAction(
88
+ client: NineDeployClient,
89
+ hostname: string,
90
+ opts: DomainsPresetApplyOptions = {},
91
+ ): Promise<void> {
92
+ header('Domain preset apply');
93
+
94
+ if (!hostname) {
95
+ error('Usage: ninedeploy domains preset apply <hostname> [--content <value>]');
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+
100
+ let result: Awaited<ReturnType<typeof domainsPresetApply>>;
101
+ try {
102
+ result = await domainsPresetApply(client, hostname, opts);
103
+ } catch (err) {
104
+ error(err instanceof Error ? err.message : String(err));
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+
109
+ success(`Applied ${result.type} record for ${result.hostname}`);
110
+ info(`Provider: ${result.provider}`);
111
+ info(`Zone: ${result.zone}`);
112
+ info(`Record: ${result.recordId}`);
113
+ info(`Content: ${result.content}`);
114
+ }
115
+
116
+ // ── `ninedeploy domains preset add namecheap` ────────────────────────────
117
+
118
+ export interface DomainsPresetAddNamecheapOptions {
119
+ /** Namecheap username (the account owner). */
120
+ apiUser?: string;
121
+ /** Namecheap API key (encrypted at rest by the server). */
122
+ apiKey?: string;
123
+ /** Whitelisted public IP of the server. */
124
+ clientIp?: string;
125
+ }
126
+
127
+ /**
128
+ * Pure entry point for the Namecheap credential upsert. The handler
129
+ * shape mirrors the rest of the file (returns the upstream response,
130
+ * the action layer frames stdout/exit-code).
131
+ */
132
+ export async function domainsPresetAddNamecheap(
133
+ client: NineDeployClient,
134
+ opts: DomainsPresetAddNamecheapOptions,
135
+ ): Promise<{ ok: boolean; apiUser: string }> {
136
+ if (!opts.apiUser || !opts.apiKey || !opts.clientIp) {
137
+ throw new Error('Missing required flags: --api-user, --api-key, --client-ip');
138
+ }
139
+ return await client.settings.namecheap.set({
140
+ apiUser: opts.apiUser,
141
+ apiKey: opts.apiKey,
142
+ clientIp: opts.clientIp,
143
+ });
144
+ }
145
+
146
+ export async function domainsPresetAddNamecheapAction(
147
+ client: NineDeployClient,
148
+ opts: DomainsPresetAddNamecheapOptions,
149
+ ): Promise<void> {
150
+ header('Domain preset add (Namecheap)');
151
+
152
+ if (!opts.apiUser || !opts.apiKey || !opts.clientIp) {
153
+ error('Usage: ninedeploy domains preset add namecheap --api-user <u> --api-key <k> --client-ip <ip>');
154
+ error('The client IP must already be whitelisted on the Namecheap account panel.');
155
+ process.exitCode = 1;
156
+ return;
157
+ }
158
+
159
+ let result: Awaited<ReturnType<typeof domainsPresetAddNamecheap>>;
160
+ try {
161
+ result = await domainsPresetAddNamecheap(client, opts);
162
+ } catch (err) {
163
+ error(err instanceof Error ? err.message : String(err));
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+
168
+ success(`Saved Namecheap credentials for ${result.apiUser}`);
169
+ info('Set dns_records_provider=namecheap in Settings → DNS to use them.');
170
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `ninedeploy egress {list,set,clear}` — Sprint 5, Gap G-15.
3
+ *
4
+ * Operator-side wrapper around the active `IEgressIpDriver`. The
5
+ * CLI is the canonical way to attach a stable outbound IP to a
6
+ * project: hosting providers run `ninedeploy egress set <projectId>
7
+ * <ip>` after the project is created, and the next deploy's
8
+ * `service.deployed` event fires the matching iptables SNAT.
9
+ */
10
+ import type { NineDeployClient } from '../client.js';
11
+ import { error, header, info, success } from '../lib/format.js';
12
+
13
+ export interface EgressRule {
14
+ selector: { projectId: number; sourceCidr?: string };
15
+ ip: string;
16
+ createdAt: string;
17
+ }
18
+
19
+ export interface EgressDriverView {
20
+ name: string;
21
+ rules: EgressRule[];
22
+ }
23
+
24
+ // ── `ninedeploy egress list` ──────────────────────────────────────────────
25
+
26
+ /** Pure entry point — same shape used by the CLI action and the unit test. */
27
+ export async function egressList(client: NineDeployClient): Promise<{ drivers: EgressDriverView[] }> {
28
+ return await client.egress.list();
29
+ }
30
+
31
+ export async function egressListAction(client: NineDeployClient): Promise<void> {
32
+ header('Egress IP rules');
33
+ let result: { drivers: EgressDriverView[] };
34
+ try {
35
+ result = await egressList(client);
36
+ } catch (err) {
37
+ error(err instanceof Error ? err.message : String(err));
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ if (result.drivers.length === 0) {
42
+ info('No egress IP drivers are registered. Install one via Settings → Plugins.');
43
+ return;
44
+ }
45
+ for (const d of result.drivers) {
46
+ info(`Driver: ${d.name}`);
47
+ if (d.rules.length === 0) {
48
+ info(' (no rules)');
49
+ } else {
50
+ for (const r of d.rules) {
51
+ info(` project ${r.selector.projectId} → ${r.ip} (since ${r.createdAt})`);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // ── `ninedeploy egress set <projectId> <ip>` ──────────────────────────────
58
+
59
+ /** Pure entry point — same shape used by the CLI action and the unit test. */
60
+ export async function egressSet(
61
+ client: NineDeployClient,
62
+ projectId: number,
63
+ ip: string,
64
+ driver?: string,
65
+ ): Promise<{ ok: boolean; driver: string; rule: EgressRule; error?: string }> {
66
+ return await client.egress.set({ projectId, ip, driver });
67
+ }
68
+
69
+ export async function egressSetAction(
70
+ client: NineDeployClient,
71
+ projectIdStr: string,
72
+ ip: string,
73
+ opts: { driver?: string } = {},
74
+ ): Promise<void> {
75
+ const projectId = Number(projectIdStr);
76
+ if (!Number.isFinite(projectId)) {
77
+ error('Usage: ninedeploy egress set <projectId> <ip> [--driver <name>]');
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ let result: Awaited<ReturnType<typeof egressSet>>;
82
+ try {
83
+ result = await egressSet(client, projectId, ip, opts.driver);
84
+ } catch (err) {
85
+ error(err instanceof Error ? err.message : String(err));
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ // r198: the route answers a refusal as 200 `{ ok: false, error }`; this
90
+ // used to dereference the missing `rule` and crash with a TypeError.
91
+ if (!result.ok) {
92
+ error(result.error ?? 'Egress IP attach was refused');
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ success(`Egress IP ${result.rule.ip} attached to project ${projectId} via ${result.driver}`);
97
+ }
98
+
99
+ // ── `ninedeploy egress clear <projectId>` ────────────────────────────────
100
+
101
+ /** Pure entry point — same shape used by the CLI action and the unit test. */
102
+ export async function egressClear(
103
+ client: NineDeployClient,
104
+ projectId: number,
105
+ driver?: string,
106
+ ): Promise<{ ok: boolean; driver: string; error?: string }> {
107
+ return driver ? await client.egress.clear(projectId, driver) : await client.egress.clear(projectId);
108
+ }
109
+
110
+ export async function egressClearAction(
111
+ client: NineDeployClient,
112
+ projectIdStr: string,
113
+ opts: { driver?: string } = {},
114
+ ): Promise<void> {
115
+ const projectId = Number(projectIdStr);
116
+ if (!Number.isFinite(projectId)) {
117
+ error('Usage: ninedeploy egress clear <projectId> [--driver <name>]');
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ let result: Awaited<ReturnType<typeof egressClear>>;
122
+ try {
123
+ result = await egressClear(client, projectId, opts.driver);
124
+ } catch (err) {
125
+ error(err instanceof Error ? err.message : String(err));
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ if (!result.ok) {
130
+ error(result.error ?? 'Egress IP detach was refused');
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ success(`Egress IP cleared for project ${projectId} via ${result.driver}`);
135
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `ninedeploy email-templates {list,set,reset,preview}` —
3
+ * G-30 transactional email template overrides.
4
+ *
5
+ * The CLI is a thin wrapper around the SDK; the heavy
6
+ * lifting is `lib/emailTemplates.ts`. The `set` and
7
+ * `reset` commands require the workspace's admin role
8
+ * server-side, so a non-admin operator gets a clean
9
+ * 403 rather than a silent no-op.
10
+ */
11
+ import type { NineDeployClient, EmailTemplateName } from '@ninedeploy/sdk';
12
+ import { c, error, header, info, spinner, success, table } from '../lib/format.js';
13
+
14
+ const num = (v: string, usage: string): number => {
15
+ const n = Number(v);
16
+ if (Number.isNaN(n)) {
17
+ error(usage);
18
+ throw new Error(usage);
19
+ }
20
+ return n;
21
+ };
22
+
23
+ const fail = (err: unknown): void => {
24
+ error(err instanceof Error ? err.message : String(err));
25
+ };
26
+
27
+ const NAMES: EmailTemplateName[] = [
28
+ 'password-reset',
29
+ 'workspace-invitation',
30
+ 'domain-transfer',
31
+ 'backup-drill-failed',
32
+ ];
33
+
34
+ export async function emailTemplatesList(client: NineDeployClient, widStr: string): Promise<void> {
35
+ const wid = num(widStr, 'Usage: ninedeploy email-templates list <workspaceId>');
36
+ const res = await spinner('Fetching', () => client.emailTemplates.list(wid));
37
+ header(`Email templates (workspace ${wid})`);
38
+ table(
39
+ res.templates.map((t) => ({
40
+ name: t.name,
41
+ overridden: t.overridden ? 'yes' : c.dim('no'),
42
+ subject: t.subject ?? c.dim('(default)'),
43
+ })),
44
+ ['name', 'overridden', 'subject'],
45
+ );
46
+ }
47
+
48
+ export async function emailTemplatesPreview(
49
+ client: NineDeployClient,
50
+ widStr: string,
51
+ name: string,
52
+ vars: string[] = [],
53
+ ): Promise<void> {
54
+ const wid = num(widStr, 'Usage: ninedeploy email-templates preview <workspaceId> <name> [key=value ...]');
55
+ if (!NAMES.includes(name as EmailTemplateName)) {
56
+ error(`Unknown template: ${name}. Valid names: ${NAMES.join(', ')}`);
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ const parsed: Record<string, string> = {};
61
+ for (const raw of vars) {
62
+ const eq = raw.indexOf('=');
63
+ if (eq <= 0) {
64
+ error(`--var expects key=value pairs, got: ${raw}`);
65
+ return;
66
+ }
67
+ parsed[raw.slice(0, eq)] = raw.slice(eq + 1);
68
+ }
69
+ const res = await spinner('Rendering', () =>
70
+ client.emailTemplates.preview(wid, name as EmailTemplateName, parsed),
71
+ );
72
+ header(`Preview: ${name}${res.overridden ? c.cyan(' (overridden)') : ''}`);
73
+ info(`Subject: ${res.subject}`);
74
+ console.log();
75
+ console.log(res.text);
76
+ }
77
+
78
+ export async function emailTemplatesSet(
79
+ client: NineDeployClient,
80
+ widStr: string,
81
+ name: string,
82
+ opts: { subject: string; text: string },
83
+ ): Promise<void> {
84
+ const wid = num(widStr, 'Usage: ninedeploy email-templates set <workspaceId> <name> --subject <s> --text <t>');
85
+ if (!NAMES.includes(name as EmailTemplateName)) {
86
+ error(`Unknown template: ${name}. Valid names: ${NAMES.join(', ')}`);
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ if (!opts.subject || !opts.text) {
91
+ error('Both --subject and --text are required');
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ try {
96
+ await client.emailTemplates.set(wid, name as EmailTemplateName, opts.subject, opts.text);
97
+ success(`Override set for ${name}.`);
98
+ } catch (err) { fail(err); }
99
+ }
100
+
101
+ export async function emailTemplatesReset(client: NineDeployClient, widStr: string, name: string): Promise<void> {
102
+ const wid = num(widStr, 'Usage: ninedeploy email-templates reset <workspaceId> <name>');
103
+ if (!NAMES.includes(name as EmailTemplateName)) {
104
+ error(`Unknown template: ${name}. Valid names: ${NAMES.join(', ')}`);
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ try {
109
+ await client.emailTemplates.reset(wid, name as EmailTemplateName);
110
+ success(`Override removed for ${name}.`);
111
+ } catch (err) { fail(err); }
112
+ }
@@ -0,0 +1,82 @@
1
+ import type { NineDeployClient } from '@ninedeploy/sdk';
2
+
3
+ export async function firewallStatus(client: NineDeployClient): Promise<void> {
4
+ try {
5
+ const status = await client.firewall.status();
6
+ console.log();
7
+ console.log(` \x1b[1mHost Firewall (UFW) Status:\x1b[0m ${status.active ? '\x1b[32mActive (Protecting)\x1b[0m' : '\x1b[33mInactive\x1b[0m'}`);
8
+ console.log(` \x1b[90mInstalled:\x1b[0m ${status.installed ? 'yes' : 'no'}`);
9
+ console.log(` \x1b[90mDefault Policies:\x1b[0m Incoming: ${status.defaultIncoming} | Outgoing: ${status.defaultOutgoing}`);
10
+ console.log();
11
+
12
+ if (status.rules && status.rules.length > 0) {
13
+ console.log(' \x1b[1mActive Ingress & Port Rules:\x1b[0m');
14
+ console.log(` ${'-'.repeat(70)}`);
15
+ console.log(` ${'ID'.padEnd(5)} ${'To (Port)'.padEnd(20)} ${'Action'.padEnd(12)} ${'From'.padEnd(20)} Comment`);
16
+ console.log(` ${'-'.repeat(70)}`);
17
+ for (const r of status.rules) {
18
+ const actionColor = r.action.includes('ALLOW') ? '\x1b[32m' : '\x1b[31m';
19
+ console.log(
20
+ ` ${String(r.id).padEnd(5)} ${r.to.padEnd(20)} ${actionColor}${r.action.padEnd(12)}\x1b[0m ${r.from.padEnd(20)} ${r.comment || ''}`
21
+ );
22
+ }
23
+ console.log(` ${'-'.repeat(70)}`);
24
+ } else {
25
+ console.log(' \x1b[90mNo active port rules configured.\x1b[0m');
26
+ }
27
+ console.log();
28
+ } catch (err) {
29
+ console.error(` \x1b[31m✗ Could not fetch firewall status:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ export async function firewallToggle(client: NineDeployClient, enable: boolean): Promise<void> {
35
+ try {
36
+ await client.firewall.toggle(enable);
37
+ console.log(` \x1b[32m✓ Host firewall ${enable ? 'enabled' : 'disabled'}.\x1b[0m`);
38
+ } catch (err) {
39
+ console.error(` \x1b[31m✗ Failed to toggle firewall:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ export async function firewallAddRule(
45
+ client: NineDeployClient,
46
+ port: string,
47
+ opts: { proto?: 'tcp' | 'udp' | 'any'; action?: 'allow' | 'deny' | 'limit'; from?: string; comment?: string }
48
+ ): Promise<void> {
49
+ try {
50
+ await client.firewall.addRule({
51
+ port,
52
+ proto: opts.proto ?? 'tcp',
53
+ action: opts.action ?? 'allow',
54
+ from: opts.from,
55
+ comment: opts.comment,
56
+ });
57
+ console.log(` \x1b[32m✓ Rule added: ${opts.action ?? 'allow'} port ${port}/${opts.proto ?? 'tcp'}\x1b[0m`);
58
+ } catch (err) {
59
+ console.error(` \x1b[31m✗ Failed to add firewall rule:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ export async function firewallDeleteRule(client: NineDeployClient, id: string): Promise<void> {
65
+ try {
66
+ await client.firewall.deleteRule(id);
67
+ console.log(` \x1b[32m✓ Rule ${id} deleted.\x1b[0m`);
68
+ } catch (err) {
69
+ console.error(` \x1b[31m✗ Failed to delete firewall rule:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ export async function firewallApplyRecommended(client: NineDeployClient): Promise<void> {
75
+ try {
76
+ await client.firewall.applyRecommended();
77
+ console.log(' \x1b[32m✓ Recommended VPS firewall profile applied (22 SSH, 80 HTTP, 443 HTTPS allowed; UFW enabled).\x1b[0m');
78
+ } catch (err) {
79
+ console.error(` \x1b[31m✗ Failed to apply recommended firewall profile:\x1b[0m ${err instanceof Error ? err.message : String(err)}`);
80
+ process.exit(1);
81
+ }
82
+ }