ninedeploy 0.10.6 → 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,314 @@
1
+ /**
2
+ * `ninedeploy manifest {init,validate,show,apply}` — the project-side CLI
3
+ * for the `.ninedeploy` file. The first three work without server auth;
4
+ * `apply` is wired to a panel endpoint once the server-side handler ships
5
+ * (see docs/NINEDEPLOY_MANIFEST.md for the rollout plan).
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { readdir } from 'node:fs/promises';
9
+ import { resolve } from 'node:path';
10
+ import {
11
+ formatManifestYaml,
12
+ ManifestParseError,
13
+ ManifestValidationError,
14
+ parseManifestYaml,
15
+ starterManifest,
16
+ detectProjectKind,
17
+ type NinedeployManifest,
18
+ type ProjectKind,
19
+ } from '@ninedeploy/sdk';
20
+ import { scanForSecrets } from '@ninedeploy/schemas';
21
+ import type { NineDeployClient } from '../client.js';
22
+ import { c, error, header, info, success } from '../lib/format.js';
23
+ import { prompt } from '../prompts.js';
24
+
25
+ const MANIFEST_FILENAMES = ['.ninedeploy', '.ninedeploy.yml', '.ninedeploy.yaml'] as const;
26
+
27
+ /** Resolve the manifest file in `cwd`, or null when none exists. */
28
+ function findManifest(cwd: string): string | null {
29
+ for (const name of MANIFEST_FILENAMES) {
30
+ const candidate = resolve(cwd, name);
31
+ if (existsSync(candidate)) return candidate;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ // ── `ninedeploy manifest init` ────────────────────────────────────────────
37
+
38
+ export async function manifestInit(cwd: string): Promise<void> {
39
+ header('Manifest init');
40
+
41
+ const existing = findManifest(cwd);
42
+ if (existing) {
43
+ error(`A manifest already exists at ${existing}. Refusing to overwrite.`);
44
+ return;
45
+ }
46
+
47
+ // Detect project kind from the files in cwd.
48
+ let kind: ProjectKind = 'unknown';
49
+ try {
50
+ const files = await readdir(cwd);
51
+ kind = detectProjectKind(files);
52
+ } catch {
53
+ // Empty / unreadable cwd — fall through to unknown.
54
+ }
55
+
56
+ const detectedLabel: Record<ProjectKind, string> = {
57
+ 'node-npm': 'Node.js (npm)',
58
+ 'node-pnpm': 'Node.js (pnpm)',
59
+ python: 'Python',
60
+ go: 'Go',
61
+ static: 'Static SPA (Vite)',
62
+ unknown: 'Unknown / other',
63
+ };
64
+ info(`Detected project kind: ${detectedLabel[kind]}`);
65
+
66
+ const override = await prompt(
67
+ 'Override detected kind? (node-npm | node-pnpm | python | go | static | unknown, leave empty to keep)',
68
+ '',
69
+ );
70
+ const finalKind = (override.trim() || kind) as ProjectKind;
71
+
72
+ const manifest = starterManifest(finalKind);
73
+ const yaml = formatManifestYaml(manifest);
74
+
75
+ // Default to `.ninedeploy`. Allow opting into the yml suffix via a hint.
76
+ const filename = (await prompt('Filename (.ninedeploy, .ninedeploy.yml, …)', '.ninedeploy'))
77
+ .trim() || '.ninedeploy';
78
+ const out = resolve(cwd, filename);
79
+ try {
80
+ writeFileSync(out, yaml, { encoding: 'utf8', flag: 'wx' });
81
+ } catch (err) {
82
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
83
+ error(`A file already exists at ${out}. Refusing to overwrite.`);
84
+ return;
85
+ }
86
+
87
+ success(`Wrote ${filename}`);
88
+ console.log();
89
+ console.log(c.dim(yaml.split('\n').slice(0, 12).join('\n')));
90
+ console.log(c.dim('…'));
91
+ console.log();
92
+ info('Edit the file to match your project, then commit it.');
93
+ info('Run `ninedeploy manifest validate` to check it before pushing.');
94
+ }
95
+
96
+ // ── `ninedeploy manifest validate` ────────────────────────────────────────
97
+
98
+ export function manifestValidate(cwd: string): void {
99
+ header('Manifest validate');
100
+ const file = findManifest(cwd);
101
+ if (!file) {
102
+ error(`No .ninedeploy (or .yml/.yaml) found in ${cwd}`);
103
+ return;
104
+ }
105
+ const text = readFileSync(file, 'utf8');
106
+ // Run the secret scan before schema validation: a credential-shaped
107
+ // value in the file is more dangerous than a schema mistake (the
108
+ // former leaks through git history; the latter only breaks the build).
109
+ // Matches the server's loader so the CLI's verdict mirrors what the
110
+ // deploy will do.
111
+ const hits = scanForSecrets(text);
112
+ if (hits.length > 0) {
113
+ error(`Secret-pattern matches in ${file}:`);
114
+ for (const h of hits) {
115
+ console.log(` ${c.yellow('•')} ${h.patternId}: ${h.description} → ${h.redacted}`);
116
+ }
117
+ console.log(c.dim('Move these values to the panel env vault; the manifest is committed to the repo.'));
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ try {
122
+ parseManifestYaml(text);
123
+ } catch (err) {
124
+ if (err instanceof ManifestParseError) {
125
+ error(`YAML parse error in ${file}:`);
126
+ console.log(c.dim((err as ManifestParseError).message));
127
+ } else if (err instanceof ManifestValidationError) {
128
+ const v = err as ManifestValidationError;
129
+ error(`Schema validation failed for ${file}:`);
130
+ for (const issue of v.issues) {
131
+ console.log(` ${c.yellow('•')} ${issue.path || '<root>'}: ${issue.message}`);
132
+ }
133
+ } else {
134
+ throw err;
135
+ }
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ success(`Valid: ${file}`);
140
+ }
141
+
142
+ // ── `ninedeploy manifest show` ────────────────────────────────────────────
143
+
144
+ export function manifestShow(cwd: string): void {
145
+ header('Manifest show');
146
+ const file = findManifest(cwd);
147
+ if (!file) {
148
+ error(`No .ninedeploy (or .yml/.yaml) found in ${cwd}`);
149
+ return;
150
+ }
151
+ const text = readFileSync(file, 'utf8');
152
+ // Same secret scan as `validate`, but as a warning here: the operator
153
+ // probably just wants to peek at the parsed contents, and a noisy error
154
+ // would be more friction than the situation warrants. A non-zero
155
+ // exitCode is still set so CI can fail the build.
156
+ const hits = scanForSecrets(text);
157
+ let manifest: NinedeployManifest;
158
+ try {
159
+ manifest = parseManifestYaml(text);
160
+ } catch (err) {
161
+ if (err instanceof ManifestParseError) error(`Parse error: ${(err as ManifestParseError).message}`);
162
+ else if (err instanceof ManifestValidationError) error(`Validation failed: ${(err as ManifestValidationError).message}`);
163
+ else throw err;
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+ if (hits.length > 0) {
168
+ info(`⚠️ ${hits.length} secret-pattern match${hits.length === 1 ? '' : 'es'} in ${file} (the manifest is committed to the repo):`);
169
+ for (const h of hits) {
170
+ console.log(` ${c.yellow('•')} ${h.patternId}: ${h.description} → ${h.redacted}`);
171
+ }
172
+ process.exitCode = 1;
173
+ }
174
+ // Print as key/value lines for human consumption. The structured YAML
175
+ // is also kept (file already exists on disk) — `show` is a debug aid.
176
+ const lines: string[] = [];
177
+ if (manifest.runtime) {
178
+ lines.push(`runtime.type: ${manifest.runtime.type}`);
179
+ if (manifest.runtime.version) lines.push(`runtime.version: ${manifest.runtime.version}`);
180
+ }
181
+ if (manifest.build) {
182
+ for (const [k, v] of Object.entries(manifest.build)) {
183
+ if (v) lines.push(`build.${k.padEnd(11)} ${v}`);
184
+ }
185
+ }
186
+ if (manifest.run) {
187
+ if (manifest.run.port) lines.push(`run.port: ${manifest.run.port}`);
188
+ if (manifest.run.healthcheck) lines.push(`run.healthcheck: ${manifest.run.healthcheck}`);
189
+ if (manifest.run.restart) lines.push(`run.restart: ${manifest.run.restart}`);
190
+ }
191
+ if (manifest.routes) {
192
+ lines.push(`routes: ${manifest.routes.length} entr${manifest.routes.length === 1 ? 'y' : 'ies'}`);
193
+ for (const r of manifest.routes) {
194
+ lines.push(` - ${r.host}${r.path} (ssl=${r.ssl})`);
195
+ }
196
+ }
197
+ if (manifest.database) {
198
+ lines.push(`database: ${manifest.database.ref} → ${manifest.database.env}`);
199
+ }
200
+ if (manifest.alerts) {
201
+ lines.push(`alerts: ${manifest.alerts.length} rule${manifest.alerts.length === 1 ? '' : 's'}`);
202
+ }
203
+ if (manifest.notifications) {
204
+ const total =
205
+ manifest.notifications.onDeploy.length +
206
+ manifest.notifications.onFailure.length +
207
+ manifest.notifications.onAlert.length;
208
+ if (total > 0) lines.push(`notifications: ${total} channel ref${total === 1 ? '' : 's'}`);
209
+ }
210
+ if (manifest.volume?.backups) {
211
+ lines.push(`volume.backups: ${manifest.volume.backups.schedule} (retention=${manifest.volume.backups.retention})`);
212
+ }
213
+ console.log(lines.join('\n') || c.dim('(empty manifest)'));
214
+ }
215
+
216
+ // ── `ninedeploy manifest apply` ───────────────────────────────────────────
217
+
218
+ /**
219
+ * Push a parsed `.ninedeploy` manifest to the panel and reconcile its
220
+ * build / run / network sections into the service + build config rows.
221
+ * Routes, alerts, and database attachments still reconcile at deploy
222
+ * time via the pipeline (see docs/NINEDEPLOY_MANIFEST.md §6).
223
+ *
224
+ * The CLI runs the same secret scan as `validate` before sending the
225
+ * body — a manifest with a literal token in it is dangerous to push
226
+ * to the server (it lands in the audit log + DB rows), and the panel's
227
+ * own scan only fires on the file path, not on the JSON body. Refuse
228
+ * early with a clear error instead of round-tripping a payload we know
229
+ * we'll reject server-side.
230
+ */
231
+ export async function manifestApply(
232
+ client: NineDeployClient,
233
+ cwd: string,
234
+ serviceId: number,
235
+ ): Promise<void> {
236
+ header('Manifest apply');
237
+ const file = findManifest(cwd);
238
+ if (!file) {
239
+ error(`No .ninedeploy (or .yml/.yaml) found in ${cwd}`);
240
+ process.exitCode = 1;
241
+ return;
242
+ }
243
+ const text = readFileSync(file, 'utf8');
244
+ const hits = scanForSecrets(text);
245
+ if (hits.length > 0) {
246
+ error(`Secret-pattern matches in ${file}:`);
247
+ for (const h of hits) {
248
+ console.log(` ${c.yellow('•')} ${h.patternId}: ${h.description} → ${h.redacted}`);
249
+ }
250
+ console.log(c.dim('Move these values to the panel env vault; the manifest is committed to the repo.'));
251
+ process.exitCode = 1;
252
+ return;
253
+ }
254
+ let manifest: NinedeployManifest;
255
+ try {
256
+ manifest = parseManifestYaml(text);
257
+ } catch (err) {
258
+ if (err instanceof ManifestParseError) {
259
+ error(`YAML parse error in ${file}:`);
260
+ console.log(c.dim((err as ManifestParseError).message));
261
+ } else if (err instanceof ManifestValidationError) {
262
+ const v = err as ManifestValidationError;
263
+ error(`Schema validation failed for ${file}:`);
264
+ for (const issue of v.issues) {
265
+ console.log(` ${c.yellow('•')} ${issue.path || '<root>'}: ${issue.message}`);
266
+ }
267
+ } else {
268
+ throw err;
269
+ }
270
+ process.exitCode = 1;
271
+ return;
272
+ }
273
+
274
+ let result: Awaited<ReturnType<NineDeployClient['services']['manifest']['apply']>>;
275
+ try {
276
+ result = await client.services.manifest.apply(serviceId, { manifest });
277
+ } catch (err) {
278
+ error(`Apply failed: ${err instanceof Error ? err.message : String(err)}`);
279
+ process.exitCode = 1;
280
+ return;
281
+ }
282
+
283
+ success(`Applied ${file} → service ${result.serviceId}`);
284
+ if (result.touched.length === 0) {
285
+ info('No sections changed (the manifest matched the current service + build config).');
286
+ return;
287
+ }
288
+ info(`Touched: ${result.touched.join(', ')}`);
289
+ const serviceKeys = Object.keys(result.diff.service);
290
+ if (serviceKeys.length > 0) {
291
+ console.log();
292
+ console.log(c.bold(' service'));
293
+ for (const k of serviceKeys) {
294
+ const v = (result.diff.service as Record<string, unknown>)[k];
295
+ console.log(` ${k.padEnd(14)} ${formatValue(v)}`);
296
+ }
297
+ }
298
+ const buildKeys = Object.keys(result.diff.build);
299
+ if (buildKeys.length > 0) {
300
+ console.log();
301
+ console.log(c.bold(' build_config'));
302
+ for (const k of buildKeys) {
303
+ const v = (result.diff.build as Record<string, unknown>)[k];
304
+ console.log(` ${k.padEnd(16)} ${formatValue(v)}`);
305
+ }
306
+ }
307
+ }
308
+
309
+ function formatValue(v: unknown): string {
310
+ if (v === null) return c.dim('null');
311
+ if (v === undefined) return c.dim('(unset)');
312
+ if (typeof v === 'string') return v;
313
+ return JSON.stringify(v);
314
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `ninedeploy metrics {show,flush}` — Sprint 3, Gap G-09, PR-A.
3
+ *
4
+ * Operator-side view of the `metric-history` kernel plugin. The plugin
5
+ * itself is the source of truth (it owns the schema); this module is
6
+ * just a thin CLI surface that calls the same HTTP module the panel
7
+ * uses, so the values an operator sees here are the same values the
8
+ * audit pipeline writes.
9
+ *
10
+ * - `show` prints the active backend, enabled flag, event list,
11
+ * retention window, and the last-flush marker.
12
+ * - `flush` runs the built-in retention sweep and reports the number
13
+ * of `metric.archived` rows trimmed.
14
+ */
15
+ import type { NineDeployClient } from '../client.js';
16
+ import { error, header, info, success } from '../lib/format.js';
17
+
18
+ export interface MetricHistoryStatus {
19
+ enabled: boolean;
20
+ backend: 'builtin' | 'prometheus' | 'influxdb';
21
+ events: string[];
22
+ retentionDays: number;
23
+ lastFlush: { ts: number; backend: string; count: number };
24
+ }
25
+
26
+ // ── `ninedeploy metrics show` ──────────────────────────────────────────────
27
+
28
+ /** Pure entry point — same shape used by the CLI action and the unit test. */
29
+ export async function metricsShow(client: NineDeployClient): Promise<MetricHistoryStatus> {
30
+ return await client.metricHistory.get();
31
+ }
32
+
33
+ export async function metricsShowAction(client: NineDeployClient): Promise<void> {
34
+ header('Metric history');
35
+ let status: MetricHistoryStatus;
36
+ try {
37
+ status = await metricsShow(client);
38
+ } catch (err) {
39
+ error(err instanceof Error ? err.message : String(err));
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ info(`Enabled: ${status.enabled ? 'yes' : 'no'}`);
44
+ info(`Backend: ${status.backend}`);
45
+ info(`Retention: ${status.retentionDays} day(s)`);
46
+ info(`Events: ${status.events.join(', ') || '(none)'}`);
47
+ const lastTs = status.lastFlush?.ts ? new Date(status.lastFlush.ts).toISOString() : 'never';
48
+ info(`Last flush: ${lastTs} (backend=${status.lastFlush?.backend ?? 'builtin'})`);
49
+ }
50
+
51
+ // ── `ninedeploy metrics flush` ────────────────────────────────────────────
52
+
53
+ /** Pure entry point — same shape used by the CLI action and the unit test. */
54
+ export async function metricsFlush(client: NineDeployClient): Promise<{ ok: boolean; backend: 'builtin'; deleted: number }> {
55
+ return await client.metricHistory.flush();
56
+ }
57
+
58
+ export async function metricsFlushAction(client: NineDeployClient): Promise<void> {
59
+ header('Metric history flush');
60
+ let result: Awaited<ReturnType<typeof metricsFlush>>;
61
+ try {
62
+ result = await metricsFlush(client);
63
+ } catch (err) {
64
+ error(err instanceof Error ? err.message : String(err));
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ success(`Flushed built-in backend — ${result.deleted} row(s) trimmed`);
69
+ }