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.
- package/LICENSE +21 -21
- package/README.md +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -22
- package/dist/client.js.map +1 -1
- package/dist/commands/backupPolicy.d.ts +29 -0
- package/dist/commands/backupPolicy.d.ts.map +1 -0
- package/dist/commands/backupPolicy.js +111 -0
- package/dist/commands/backupPolicy.js.map +1 -0
- package/dist/commands/certificates.d.ts +18 -7
- package/dist/commands/certificates.d.ts.map +1 -1
- package/dist/commands/certificates.js +121 -1
- package/dist/commands/certificates.js.map +1 -1
- package/dist/commands/databaseAccess.d.ts +37 -0
- package/dist/commands/databaseAccess.d.ts.map +1 -0
- package/dist/commands/databaseAccess.js +306 -0
- package/dist/commands/databaseAccess.js.map +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +28 -7
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/githubApps.d.ts +40 -0
- package/dist/commands/githubApps.d.ts.map +1 -0
- package/dist/commands/githubApps.js +278 -0
- package/dist/commands/githubApps.js.map +1 -0
- package/dist/commands/housekeeping.d.ts +7 -0
- package/dist/commands/housekeeping.d.ts.map +1 -1
- package/dist/commands/housekeeping.js +16 -0
- package/dist/commands/housekeeping.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +17 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/manage.d.ts +26 -10
- package/dist/commands/manage.d.ts.map +1 -1
- package/dist/commands/manage.js +219 -65
- package/dist/commands/manage.js.map +1 -1
- package/dist/commands/manifest.d.ts.map +1 -1
- package/dist/commands/manifest.js +9 -1
- package/dist/commands/manifest.js.map +1 -1
- package/dist/commands/misc.d.ts +4 -2
- package/dist/commands/misc.d.ts.map +1 -1
- package/dist/commands/misc.js +25 -11
- package/dist/commands/misc.js.map +1 -1
- package/dist/commands/multiNode.d.ts +55 -0
- package/dist/commands/multiNode.d.ts.map +1 -0
- package/dist/commands/multiNode.js +438 -0
- package/dist/commands/multiNode.js.map +1 -0
- package/dist/commands/notifications.d.ts +1 -1
- package/dist/commands/operations.d.ts +89 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +468 -0
- package/dist/commands/operations.js.map +1 -0
- package/dist/commands/panelBackup.d.ts +28 -0
- package/dist/commands/panelBackup.d.ts.map +1 -0
- package/dist/commands/panelBackup.js +153 -0
- package/dist/commands/panelBackup.js.map +1 -0
- package/dist/commands/plugins.d.ts +1 -1
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +7 -3
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/proxyConfig.d.ts +16 -0
- package/dist/commands/proxyConfig.d.ts.map +1 -0
- package/dist/commands/proxyConfig.js +122 -0
- package/dist/commands/proxyConfig.js.map +1 -0
- package/dist/commands/secretProviders.d.ts +59 -0
- package/dist/commands/secretProviders.d.ts.map +1 -0
- package/dist/commands/secretProviders.js +212 -0
- package/dist/commands/secretProviders.js.map +1 -0
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +3 -5
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/setup.d.ts +3 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +19 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sources.d.ts +10 -3
- package/dist/commands/sources.d.ts.map +1 -1
- package/dist/commands/sources.js +116 -18
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/templates.d.ts.map +1 -1
- package/dist/commands/templates.js +8 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +427 -23
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +5 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/panelBackupFile.d.ts +3 -0
- package/dist/lib/panelBackupFile.d.ts.map +1 -0
- package/dist/lib/panelBackupFile.js +71 -0
- package/dist/lib/panelBackupFile.js.map +1 -0
- package/dist/lib/serverRunner.d.ts +6 -0
- package/dist/lib/serverRunner.d.ts.map +1 -1
- package/dist/lib/serverRunner.js +18 -3
- package/dist/lib/serverRunner.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +9 -2
- package/dist/prompts.js.map +1 -1
- package/package.json +6 -5
- package/src/client.ts +137 -0
- package/src/commands/backupPolicy.ts +123 -0
- package/src/commands/branding.ts +83 -0
- package/src/commands/buildCache.ts +72 -0
- package/src/commands/certificates.ts +208 -0
- package/src/commands/communityTemplates.ts +97 -0
- package/src/commands/configCenter.ts +71 -0
- package/src/commands/configPresets.ts +215 -0
- package/src/commands/databaseAccess.ts +333 -0
- package/src/commands/demo.ts +22 -0
- package/src/commands/deploy.ts +284 -0
- package/src/commands/doctor.ts +170 -0
- package/src/commands/domains.ts +170 -0
- package/src/commands/egress.ts +135 -0
- package/src/commands/emailTemplates.ts +112 -0
- package/src/commands/firewall.ts +82 -0
- package/src/commands/githubApps.ts +292 -0
- package/src/commands/housekeeping.ts +106 -0
- package/src/commands/login.ts +58 -0
- package/src/commands/logs.ts +91 -0
- package/src/commands/manage.ts +714 -0
- package/src/commands/manifest.ts +314 -0
- package/src/commands/metrics.ts +69 -0
- package/src/commands/misc.ts +307 -0
- package/src/commands/multiNode.ts +442 -0
- package/src/commands/notifications.ts +174 -0
- package/src/commands/operations.ts +519 -0
- package/src/commands/panelBackup.ts +167 -0
- package/src/commands/pgbouncer.ts +65 -0
- package/src/commands/plugins.ts +123 -0
- package/src/commands/proxyConfig.ts +117 -0
- package/src/commands/secretProviders.ts +228 -0
- package/src/commands/server.ts +126 -0
- package/src/commands/services.ts +249 -0
- package/src/commands/setup.ts +94 -0
- package/src/commands/sources.ts +317 -0
- package/src/commands/sso.ts +106 -0
- package/src/commands/templates.ts +158 -0
- package/src/commands/token.ts +66 -0
- package/src/commands/webhooks.ts +93 -0
- package/src/commands/workspaces.ts +63 -0
- package/src/config.ts +53 -0
- package/src/index.ts +1273 -0
- package/src/lib/format.ts +182 -0
- package/src/lib/panelBackupFile.ts +67 -0
- package/src/lib/serverRunner.ts +223 -0
- 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
|
+
}
|