ninedeploy 0.10.6 → 0.16.1
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/hostIps.d.ts +21 -0
- package/dist/commands/hostIps.d.ts.map +1 -0
- package/dist/commands/hostIps.js +68 -0
- package/dist/commands/hostIps.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 +17 -0
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +55 -6
- 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/commands/upload.d.ts +24 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +96 -0
- package/dist/commands/upload.js.map +1 -0
- 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 +455 -24
- 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/hostIps.ts +71 -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 +299 -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/upload.ts +111 -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 +1303 -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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ninedeploy databases pgbouncer <dbId> <action>` — G-32
|
|
3
|
+
* PgBouncer sidecar. Only the `postgres` engine is
|
|
4
|
+
* supported; the server returns 422 for any other engine.
|
|
5
|
+
*
|
|
6
|
+
* Actions:
|
|
7
|
+
* - enable : start a co-located pgbouncer container and
|
|
8
|
+
* stamp the row.
|
|
9
|
+
* - disable : stop + remove the sidecar, clear the row.
|
|
10
|
+
* - status : print the current sidecar state and the
|
|
11
|
+
* pooled connection string.
|
|
12
|
+
*/
|
|
13
|
+
import type { NineDeployClient } from '../client.js';
|
|
14
|
+
import { c, error, header, info, spinner, success } from '../lib/format.js';
|
|
15
|
+
|
|
16
|
+
const num = (v: string, usage: string): number => {
|
|
17
|
+
const n = Number(v);
|
|
18
|
+
if (Number.isNaN(n)) {
|
|
19
|
+
error(usage);
|
|
20
|
+
throw new Error(usage);
|
|
21
|
+
}
|
|
22
|
+
return n;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export async function databasePgbouncer(
|
|
26
|
+
client: NineDeployClient,
|
|
27
|
+
dbIdStr: string,
|
|
28
|
+
action: string,
|
|
29
|
+
opts: { port?: number } = {},
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const dbId = num(dbIdStr, 'Usage: ninedeploy databases pgbouncer <dbId> <enable|disable|status>');
|
|
32
|
+
const normalized = action.toLowerCase();
|
|
33
|
+
if (!['enable', 'disable', 'status'].includes(normalized)) {
|
|
34
|
+
error('Action must be one of: enable, disable, status');
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (normalized === 'enable') {
|
|
39
|
+
const res = await spinner('Starting PgBouncer sidecar', () =>
|
|
40
|
+
client.databases.enablePgbouncer(dbId, opts.port ? { port: opts.port } : {}),
|
|
41
|
+
);
|
|
42
|
+
success(`PgBouncer sidecar started on port ${res.port}.`);
|
|
43
|
+
info(`Connection URL: ${res.pooledConnectionString ?? '(pending)'}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (normalized === 'disable') {
|
|
47
|
+
const res = await spinner('Stopping PgBouncer sidecar', () => client.databases.disablePgbouncer(dbId));
|
|
48
|
+
success('PgBouncer sidecar stopped.');
|
|
49
|
+
if (res.enabled) info(`Sidecar still reporting enabled: ${res.containerName ?? '(unknown)'}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// status
|
|
53
|
+
const res = await spinner('Reading sidecar status', () => client.databases.pgbouncerStatus(dbId));
|
|
54
|
+
header('PgBouncer sidecar');
|
|
55
|
+
info(`Enabled: ${res.enabled ? 'yes' : 'no'}`);
|
|
56
|
+
info(`Container: ${res.containerName ?? c.dim('(none)')}`);
|
|
57
|
+
info(`Port: ${res.port}`);
|
|
58
|
+
info(`Running: ${res.running ? 'yes' : c.yellow('no — enabled but container is down')}`);
|
|
59
|
+
if (res.poolMode) info(`Pool mode: ${res.poolMode}`);
|
|
60
|
+
if (res.pooledConnectionString) {
|
|
61
|
+
console.log();
|
|
62
|
+
info(`Pooled URL (use this for new attachments):`);
|
|
63
|
+
console.log(` ${c.cyan(res.pooledConnectionString)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { NineDeployClient } from '@ninedeploy/sdk';
|
|
2
|
+
import { table } from '../lib/format.js';
|
|
3
|
+
|
|
4
|
+
export async function pluginsList(client: NineDeployClient): Promise<void> {
|
|
5
|
+
const { plugins } = await client.plugins.list();
|
|
6
|
+
if (plugins.length === 0) {
|
|
7
|
+
console.log(' No plugins installed.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
table(
|
|
12
|
+
plugins.map((p) => ({
|
|
13
|
+
id: p.id,
|
|
14
|
+
name: p.name,
|
|
15
|
+
version: `v${p.version}`,
|
|
16
|
+
type: p.isOfficial ? 'Official' : 'Community',
|
|
17
|
+
status: p.status,
|
|
18
|
+
})),
|
|
19
|
+
['id', 'name', 'version', 'type', 'status'],
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function pluginsMarketplace(client: NineDeployClient): Promise<void> {
|
|
24
|
+
const { catalog, live, keyId, fetchedAt } = await client.plugins.marketplace();
|
|
25
|
+
if (catalog.length === 0) {
|
|
26
|
+
console.log(' Marketplace catalog is currently empty.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Surface the source (live signed index vs. static
|
|
31
|
+
// fallback) so the operator knows whether a planned
|
|
32
|
+
// install was discovered automatically or is a
|
|
33
|
+
// roadmap item.
|
|
34
|
+
const source = live
|
|
35
|
+
? `live signed index (key=${keyId ?? 'unknown'})`
|
|
36
|
+
: 'static fallback (upstream unreachable or NINEDEPLOY_MARKETPLACE_URL not set)';
|
|
37
|
+
console.log(` Source: ${source}`);
|
|
38
|
+
if (fetchedAt) {
|
|
39
|
+
const ageMin = Math.max(0, Math.round((Date.now() - fetchedAt) / 60_000));
|
|
40
|
+
console.log(` Fetched: ${ageMin} min ago`);
|
|
41
|
+
}
|
|
42
|
+
console.log();
|
|
43
|
+
|
|
44
|
+
table(
|
|
45
|
+
catalog.map((c) => ({
|
|
46
|
+
id: c.id,
|
|
47
|
+
name: c.name,
|
|
48
|
+
category: c.category,
|
|
49
|
+
version: `v${c.version}`,
|
|
50
|
+
type: c.isOfficial ? 'Official' : 'Community',
|
|
51
|
+
status: c.isInstalled ? '✓ Installed' : 'Available',
|
|
52
|
+
})),
|
|
53
|
+
['id', 'name', 'category', 'version', 'type', 'status'],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `ninedeploy plugins marketplace refresh` — bypass the
|
|
58
|
+
* 5-minute cache and re-fetch the live signed index. */
|
|
59
|
+
export async function pluginsMarketplaceRefresh(client: NineDeployClient): Promise<void> {
|
|
60
|
+
const res = await client.plugins.marketplace({ refresh: true });
|
|
61
|
+
console.log(
|
|
62
|
+
` ✓ Refreshed (${res.catalog.length} entries, live=${res.live}, keyId=${res.keyId ?? 'n/a'}).`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function pluginsInstall(
|
|
67
|
+
client: NineDeployClient,
|
|
68
|
+
target: string,
|
|
69
|
+
// r440: matches the server's LOADABLE_SOURCES — the old union advertised
|
|
70
|
+
// npm/git/local, which the server refuses with 400 every time.
|
|
71
|
+
opts: { source?: 'marketplace' | 'sandbox'; name?: string; version?: string; desc?: string },
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const source = opts.source ?? 'marketplace';
|
|
74
|
+
const res = await client.plugins.install({
|
|
75
|
+
source,
|
|
76
|
+
target,
|
|
77
|
+
name: opts.name,
|
|
78
|
+
version: opts.version,
|
|
79
|
+
description: opts.desc,
|
|
80
|
+
});
|
|
81
|
+
console.log(` ✓ Installed plugin "${res.id}" (status: ${res.status}).`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function pluginsEnable(client: NineDeployClient, id: string): Promise<void> {
|
|
85
|
+
const res = await client.plugins.enable(id);
|
|
86
|
+
console.log(` ✓ Plugin "${res.id}" enabled.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function pluginsDisable(client: NineDeployClient, id: string): Promise<void> {
|
|
90
|
+
const res = await client.plugins.disable(id);
|
|
91
|
+
console.log(` ✓ Plugin "${res.id}" disabled.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function pluginsUninstall(client: NineDeployClient, id: string): Promise<void> {
|
|
95
|
+
const res = await client.plugins.uninstall(id);
|
|
96
|
+
console.log(` ✓ Plugin "${res.id}" uninstalled.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function pluginsInspect(client: NineDeployClient, id: string): Promise<void> {
|
|
100
|
+
const p = await client.plugins.inspect(id);
|
|
101
|
+
console.log(` Plugin: ${p.name} (${p.id})`);
|
|
102
|
+
console.log(` Version: v${p.version}`);
|
|
103
|
+
console.log(` Type: ${p.isOfficial ? 'Official' : 'Community'}`);
|
|
104
|
+
console.log(` Status: ${p.status}${p.enabled ? ' (enabled)' : ' (disabled)'}`);
|
|
105
|
+
console.log(` Author: ${p.author ?? 'N/A'}`);
|
|
106
|
+
console.log(` Description: ${p.description ?? 'N/A'}`);
|
|
107
|
+
console.log(` Dependencies: ${p.dependencies.length > 0 ? p.dependencies.join(', ') : 'none'}`);
|
|
108
|
+
console.log(` Tapped Hooks: ${p.hooks.length > 0 ? p.hooks.join(', ') : 'none'}`);
|
|
109
|
+
console.log(` Backgrounds: ${p.services.length > 0 ? p.services.join(', ') : 'none'}`);
|
|
110
|
+
console.log(` Config Keys: ${p.configSchema.length} registered`);
|
|
111
|
+
console.log(` Nav Menus: ${p.menus.length} registered`);
|
|
112
|
+
if (p.error) {
|
|
113
|
+
console.log(` Error: ${p.error}`);
|
|
114
|
+
}
|
|
115
|
+
// r286: null = the kernel does not track this per plugin.
|
|
116
|
+
console.log(` Events Run: ${p.runtimeStats.eventsHandled ?? 'not tracked'}`);
|
|
117
|
+
console.log(` Uptime: ${p.runtimeStats.uptimeSeconds == null ? 'not tracked' : `${p.runtimeStats.uptimeSeconds}s`}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function pluginsReload(client: NineDeployClient, id: string): Promise<void> {
|
|
121
|
+
const res = await client.plugins.reload(id);
|
|
122
|
+
console.log(` ✓ Plugin "${res.id}" reloaded (status: ${res.status}).`);
|
|
123
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import type { TraefikCustomConfigIssue } from '@ninedeploy/sdk';
|
|
3
|
+
import type { NineDeployClient } from '../client.js';
|
|
4
|
+
import { prompt } from '../prompts.js';
|
|
5
|
+
import { c, error, header, info, kv, spinner, success } from '../lib/format.js';
|
|
6
|
+
import { plain } from './sources.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 0.14: the operator's custom Traefik dynamic config (`custom.yml`) —
|
|
10
|
+
* `ninedeploy proxy config get|validate|set|clear`. Operator only on the
|
|
11
|
+
* server. Findings, errors and the stored YAML are printed through the F1011
|
|
12
|
+
* sanitiser (line by line, so the YAML keeps its line breaks).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const message = (err: unknown): string => plain(err instanceof Error ? err.message : String(err));
|
|
16
|
+
|
|
17
|
+
/** Multi-line text with every control sequence removed from each line. */
|
|
18
|
+
const lines = (text: string): string =>
|
|
19
|
+
text
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.map((l) => plain(l))
|
|
22
|
+
.join('\n');
|
|
23
|
+
|
|
24
|
+
function printIssues(label: string, issues: TraefikCustomConfigIssue[], color: (s: string) => string): void {
|
|
25
|
+
if (issues.length === 0) return;
|
|
26
|
+
console.log(` ${color(label)}`);
|
|
27
|
+
for (const i of issues) console.log(` ${color('•')} ${i.path ? `${plain(i.path)}: ` : ''}${plain(i.message)}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readConfigFile(file: string | undefined, usage: string): string | null {
|
|
31
|
+
if (!file) {
|
|
32
|
+
error(usage);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(file, 'utf8');
|
|
37
|
+
if (!content.trim()) {
|
|
38
|
+
error(`${file} is empty`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return content;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
error(`Cannot read ${file}: ${message(err)}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `ninedeploy proxy config get` */
|
|
49
|
+
export async function proxyConfigGet(client: NineDeployClient): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
const cfg = await spinner('Reading the custom config', () => client.traefik.customConfig.get());
|
|
52
|
+
header('Custom Traefik config');
|
|
53
|
+
const color = cfg.status === 'applied' ? c.green : cfg.status === 'rejected' ? c.red : c.gray;
|
|
54
|
+
kv('Status', color(cfg.status));
|
|
55
|
+
kv('SHA-256', cfg.sha256);
|
|
56
|
+
kv('Updated', cfg.updatedAt ? new Date(cfg.updatedAt).toLocaleString() : null);
|
|
57
|
+
kv('Updated by', cfg.updatedBy === null ? null : `user #${cfg.updatedBy}`);
|
|
58
|
+
if (cfg.lastError) kv('Last error', c.red(plain(cfg.lastError)));
|
|
59
|
+
if (cfg.content === null) {
|
|
60
|
+
info('No custom config saved. Write one and run `ninedeploy proxy config set --file <path>`.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(lines(cfg.content));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
error(message(err));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `ninedeploy proxy config validate --file <path>` — no state change. */
|
|
71
|
+
export async function proxyConfigValidate(client: NineDeployClient, opts: { file?: string } = {}): Promise<void> {
|
|
72
|
+
const content = readConfigFile(opts.file, 'Usage: ninedeploy proxy config validate --file <path>');
|
|
73
|
+
if (content === null) return;
|
|
74
|
+
try {
|
|
75
|
+
const res = await spinner('Validating', () => client.traefik.customConfig.validate(content));
|
|
76
|
+
printIssues('Errors', res.errors, c.red);
|
|
77
|
+
printIssues('Warnings', res.warnings, c.yellow);
|
|
78
|
+
if (res.ok) success('The config is valid');
|
|
79
|
+
else error(`The config has ${res.errors.length} error(s)`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
error(message(err));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `ninedeploy proxy config set --file <path>` — validated and applied atomically by the server. */
|
|
86
|
+
export async function proxyConfigSet(client: NineDeployClient, opts: { file?: string } = {}): Promise<void> {
|
|
87
|
+
const content = readConfigFile(opts.file, 'Usage: ninedeploy proxy config set --file <path>');
|
|
88
|
+
if (content === null) return;
|
|
89
|
+
try {
|
|
90
|
+
const res = await spinner('Validating and applying', () => client.traefik.customConfig.set(content));
|
|
91
|
+
success(`Custom config applied (sha256 ${res.sha256.slice(0, 12)})`);
|
|
92
|
+
printIssues('Warnings', res.warnings, c.yellow);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const details = (err as { details?: { errors?: TraefikCustomConfigIssue[]; warnings?: TraefikCustomConfigIssue[] } }).details;
|
|
95
|
+
if (details?.errors) printIssues('Errors', details.errors, c.red);
|
|
96
|
+
if (details?.warnings) printIssues('Warnings', details.warnings, c.yellow);
|
|
97
|
+
error(message(err));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `ninedeploy proxy config clear [--yes]` — removes every custom router, service and middleware. */
|
|
102
|
+
export async function proxyConfigClear(client: NineDeployClient, opts: { yes?: boolean } = {}): Promise<void> {
|
|
103
|
+
if (!opts.yes) {
|
|
104
|
+
const confirm = await prompt('Type "clear" to remove the custom Traefik config', '');
|
|
105
|
+
if (confirm.trim() !== 'clear') {
|
|
106
|
+
info('Aborted.');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const res = await spinner('Clearing the custom config', () => client.traefik.customConfig.clear());
|
|
112
|
+
if (res.cleared) success('Custom config removed');
|
|
113
|
+
else info('There was no custom config to remove.');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
error(message(err));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import type { AwsProviderPut, SecretProviderKind, SecretProviderView, VaultProviderPut } from '@ninedeploy/sdk';
|
|
4
|
+
import type { NineDeployClient } from '../client.js';
|
|
5
|
+
import { prompt } from '../prompts.js';
|
|
6
|
+
import { c, error, fmtTime, header, info, kv, spinner, success, table } from '../lib/format.js';
|
|
7
|
+
import { plain } from './sources.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 0.14: HashiCorp Vault / OpenBao and AWS Secrets Manager —
|
|
11
|
+
* `ninedeploy secrets providers list|set-vault|set-aws|test|delete`.
|
|
12
|
+
* Operator only on the server.
|
|
13
|
+
*
|
|
14
|
+
* Secrets never travel on argv (they would land in shell history and `ps`):
|
|
15
|
+
* a Vault token, AppRole role/secret id, AWS secret access key and session
|
|
16
|
+
* token come from a file (`--token-file`, …) or an environment variable.
|
|
17
|
+
* The AWS access key id is an identifier and may be passed as a flag. An
|
|
18
|
+
* omitted credential keeps the stored value. Provider detail and test errors
|
|
19
|
+
* are relayed provider text, so they pass through the F1011 sanitiser.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const VAULT_TOKEN_ENV = 'NINEDEPLOY_VAULT_TOKEN';
|
|
23
|
+
export const VAULT_ROLE_ID_ENV = 'NINEDEPLOY_VAULT_ROLE_ID';
|
|
24
|
+
export const VAULT_SECRET_ID_ENV = 'NINEDEPLOY_VAULT_SECRET_ID';
|
|
25
|
+
export const AWS_ACCESS_KEY_ID_ENV = 'NINEDEPLOY_AWS_ACCESS_KEY_ID';
|
|
26
|
+
export const AWS_SECRET_ACCESS_KEY_ENV = 'NINEDEPLOY_AWS_SECRET_ACCESS_KEY';
|
|
27
|
+
export const AWS_SESSION_TOKEN_ENV = 'NINEDEPLOY_AWS_SESSION_TOKEN';
|
|
28
|
+
|
|
29
|
+
const message = (err: unknown): string => plain(err instanceof Error ? err.message : String(err));
|
|
30
|
+
|
|
31
|
+
/** A secret from `file`, else the environment variable `env`; undefined when neither is set. */
|
|
32
|
+
function readSecret(file: string | undefined, env: string): string | undefined {
|
|
33
|
+
if (file) return readFileSync(file, 'utf8').trim() || undefined;
|
|
34
|
+
return process.env[env]?.trim() || undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseKind(raw: string | undefined): SecretProviderKind | null {
|
|
38
|
+
return raw === 'vault' || raw === 'aws' ? raw : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function enabledFrom(opts: { enable?: boolean; disable?: boolean }): { enabled?: boolean } | null {
|
|
42
|
+
if (opts.enable && opts.disable) return null;
|
|
43
|
+
if (opts.enable) return { enabled: true };
|
|
44
|
+
if (opts.disable) return { enabled: false };
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configText = (config: Record<string, unknown>): string => {
|
|
49
|
+
const entries = Object.entries(config);
|
|
50
|
+
return entries.length > 0 ? entries.map(([k, v]) => `${plain(k)}=${plain(typeof v === 'string' ? v : JSON.stringify(v))}`).join(' ') : '—';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function printView(v: SecretProviderView): void {
|
|
54
|
+
kv('Configured', v.configured ? c.green('yes') : c.gray('no'));
|
|
55
|
+
kv('Enabled', v.enabled ? 'yes' : 'no');
|
|
56
|
+
kv('Credentials', v.hasCredential ? 'stored' : c.yellow('missing'));
|
|
57
|
+
kv('Settings', configText(v.config));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `ninedeploy secrets providers list` */
|
|
61
|
+
export async function secretProvidersList(client: NineDeployClient): Promise<void> {
|
|
62
|
+
try {
|
|
63
|
+
const rows = await spinner('Reading secret managers', () => client.settings.secretProviders.list());
|
|
64
|
+
header('Secret managers');
|
|
65
|
+
table(
|
|
66
|
+
rows.map((r) => ({
|
|
67
|
+
kind: r.kind,
|
|
68
|
+
configured: r.configured ? c.green('yes') : c.gray('no'),
|
|
69
|
+
enabled: r.enabled ? 'yes' : 'no',
|
|
70
|
+
credentials: r.hasCredential ? 'stored' : c.dim('—'),
|
|
71
|
+
tested: fmtTime(r.lastTestedAt),
|
|
72
|
+
lastError: r.lastTestError ? c.red(plain(r.lastTestError).slice(0, 80)) : '',
|
|
73
|
+
settings: configText(r.config),
|
|
74
|
+
})),
|
|
75
|
+
['kind', 'configured', 'enabled', 'credentials', 'tested', 'lastError', 'settings'],
|
|
76
|
+
);
|
|
77
|
+
info(`Reference syntax: \${{vault:<path>#<field>}} and \${{aws:<secretId>}} or \${{aws:<secretId>#<jsonKey>}}`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
error(message(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VaultCliOptions {
|
|
84
|
+
address?: string;
|
|
85
|
+
namespace?: string;
|
|
86
|
+
mount?: string;
|
|
87
|
+
auth?: string;
|
|
88
|
+
approleMount?: string;
|
|
89
|
+
tokenFile?: string;
|
|
90
|
+
roleIdFile?: string;
|
|
91
|
+
secretIdFile?: string;
|
|
92
|
+
enable?: boolean;
|
|
93
|
+
disable?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `ninedeploy secrets providers set-vault --address <url> [--auth token|approle] ...` */
|
|
97
|
+
export async function secretProvidersSetVault(client: NineDeployClient, opts: VaultCliOptions = {}): Promise<void> {
|
|
98
|
+
const address = opts.address?.trim();
|
|
99
|
+
if (!address) return error('Usage: ninedeploy secrets providers set-vault --address <https://vault…> [--auth token|approle] [--token-file <path>]');
|
|
100
|
+
const authMethod = opts.auth ?? 'token';
|
|
101
|
+
if (authMethod !== 'token' && authMethod !== 'approle') return error('--auth must be "token" or "approle"');
|
|
102
|
+
const enabled = enabledFrom(opts);
|
|
103
|
+
if (!enabled) return error('--enable and --disable cannot be combined');
|
|
104
|
+
const credentials: NonNullable<VaultProviderPut['credentials']> = {};
|
|
105
|
+
try {
|
|
106
|
+
if (authMethod === 'token') {
|
|
107
|
+
const token = readSecret(opts.tokenFile, VAULT_TOKEN_ENV);
|
|
108
|
+
if (token) credentials.token = token;
|
|
109
|
+
} else {
|
|
110
|
+
const roleId = readSecret(opts.roleIdFile, VAULT_ROLE_ID_ENV);
|
|
111
|
+
const secretId = readSecret(opts.secretIdFile, VAULT_SECRET_ID_ENV);
|
|
112
|
+
if (roleId) credentials.roleId = roleId;
|
|
113
|
+
if (secretId) credentials.secretId = secretId;
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return error(`Could not read a credential file: ${message(err)}`);
|
|
117
|
+
}
|
|
118
|
+
const input: VaultProviderPut = {
|
|
119
|
+
...enabled,
|
|
120
|
+
config: {
|
|
121
|
+
address,
|
|
122
|
+
authMethod,
|
|
123
|
+
...(opts.namespace ? { namespace: opts.namespace } : {}),
|
|
124
|
+
...(opts.mount ? { mount: opts.mount } : {}),
|
|
125
|
+
...(opts.approleMount ? { approleMount: opts.approleMount } : {}),
|
|
126
|
+
},
|
|
127
|
+
...(Object.keys(credentials).length > 0 ? { credentials } : {}),
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const view = await spinner('Saving the Vault settings', () => client.settings.secretProviders.set('vault', input));
|
|
131
|
+
success('Vault / OpenBao saved');
|
|
132
|
+
printView(view);
|
|
133
|
+
info('Run `ninedeploy secrets providers test vault` to check the connection.');
|
|
134
|
+
} catch (err) {
|
|
135
|
+
error(message(err));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AwsCliOptions {
|
|
140
|
+
region?: string;
|
|
141
|
+
endpoint?: string;
|
|
142
|
+
roleArn?: string;
|
|
143
|
+
externalId?: string;
|
|
144
|
+
roleSessionName?: string;
|
|
145
|
+
accessKeyId?: string;
|
|
146
|
+
secretAccessKeyFile?: string;
|
|
147
|
+
sessionTokenFile?: string;
|
|
148
|
+
enable?: boolean;
|
|
149
|
+
disable?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `ninedeploy secrets providers set-aws --region <region> ...` */
|
|
153
|
+
export async function secretProvidersSetAws(client: NineDeployClient, opts: AwsCliOptions = {}): Promise<void> {
|
|
154
|
+
const region = opts.region?.trim();
|
|
155
|
+
if (!region) return error('Usage: ninedeploy secrets providers set-aws --region <region> [--access-key-id <id>] [--secret-access-key-file <path>]');
|
|
156
|
+
const enabled = enabledFrom(opts);
|
|
157
|
+
if (!enabled) return error('--enable and --disable cannot be combined');
|
|
158
|
+
const credentials: NonNullable<AwsProviderPut['credentials']> = {};
|
|
159
|
+
try {
|
|
160
|
+
const accessKeyId = opts.accessKeyId?.trim() || process.env[AWS_ACCESS_KEY_ID_ENV]?.trim();
|
|
161
|
+
const secretAccessKey = readSecret(opts.secretAccessKeyFile, AWS_SECRET_ACCESS_KEY_ENV);
|
|
162
|
+
const sessionToken = readSecret(opts.sessionTokenFile, AWS_SESSION_TOKEN_ENV);
|
|
163
|
+
if (accessKeyId) credentials.accessKeyId = accessKeyId;
|
|
164
|
+
if (secretAccessKey) credentials.secretAccessKey = secretAccessKey;
|
|
165
|
+
if (sessionToken) credentials.sessionToken = sessionToken;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return error(`Could not read a credential file: ${message(err)}`);
|
|
168
|
+
}
|
|
169
|
+
const input: AwsProviderPut = {
|
|
170
|
+
...enabled,
|
|
171
|
+
config: {
|
|
172
|
+
region,
|
|
173
|
+
...(opts.endpoint ? { endpoint: opts.endpoint } : {}),
|
|
174
|
+
...(opts.roleArn ? { roleArn: opts.roleArn } : {}),
|
|
175
|
+
...(opts.externalId ? { externalId: opts.externalId } : {}),
|
|
176
|
+
...(opts.roleSessionName ? { roleSessionName: opts.roleSessionName } : {}),
|
|
177
|
+
},
|
|
178
|
+
...(Object.keys(credentials).length > 0 ? { credentials } : {}),
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
const view = await spinner('Saving the AWS settings', () => client.settings.secretProviders.set('aws', input));
|
|
182
|
+
success('AWS Secrets Manager saved');
|
|
183
|
+
printView(view);
|
|
184
|
+
info('Run `ninedeploy secrets providers test aws` to check the connection.');
|
|
185
|
+
} catch (err) {
|
|
186
|
+
error(message(err));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `ninedeploy secrets providers test <kind> [--probe-path <path>] [--probe-secret-id <id>]` */
|
|
191
|
+
export async function secretProvidersTest(
|
|
192
|
+
client: NineDeployClient,
|
|
193
|
+
kindArg: string,
|
|
194
|
+
opts: { probePath?: string; probeSecretId?: string } = {},
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const kind = parseKind(kindArg);
|
|
197
|
+
if (!kind) return error('Usage: ninedeploy secrets providers test <vault|aws> [--probe-path <path>] [--probe-secret-id <id>]');
|
|
198
|
+
if (kind === 'vault' && opts.probeSecretId) return error('--probe-secret-id belongs to the aws provider');
|
|
199
|
+
if (kind === 'aws' && opts.probePath) return error('--probe-path belongs to the vault provider');
|
|
200
|
+
const probe = kind === 'vault' ? (opts.probePath ? { probePath: opts.probePath } : {}) : opts.probeSecretId ? { probeSecretId: opts.probeSecretId } : {};
|
|
201
|
+
try {
|
|
202
|
+
const res = await spinner(`Testing the ${kind} secret manager`, () => client.settings.secretProviders.test(kind, probe));
|
|
203
|
+
if (res.ok) success(plain(res.detail));
|
|
204
|
+
else error(plain(res.detail));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
error(message(err));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `ninedeploy secrets providers delete <kind> [--yes]` */
|
|
211
|
+
export async function secretProvidersDelete(client: NineDeployClient, kindArg: string, opts: { yes?: boolean } = {}): Promise<void> {
|
|
212
|
+
const kind = parseKind(kindArg);
|
|
213
|
+
if (!kind) return error('Usage: ninedeploy secrets providers delete <vault|aws> [--yes]');
|
|
214
|
+
if (!opts.yes) {
|
|
215
|
+
const confirm = await prompt(`Type "delete" to remove the ${kind} secret manager (its references stay literal at deploy)`, '');
|
|
216
|
+
if (confirm.trim() !== 'delete') {
|
|
217
|
+
info('Aborted.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const res = await spinner(`Removing the ${kind} secret manager`, () => client.settings.secretProviders.delete(kind));
|
|
223
|
+
if (res.deleted) success(`The ${kind} secret manager was removed`);
|
|
224
|
+
else info(`No ${kind} secret manager was configured.`);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
error(message(err));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { c, error, header, info, kv, spinner, success } from '../lib/format.js';
|
|
2
|
+
import {
|
|
3
|
+
getContainerState,
|
|
4
|
+
getServerLogs,
|
|
5
|
+
isDockerAvailable,
|
|
6
|
+
isServerReachable,
|
|
7
|
+
startServerContainer,
|
|
8
|
+
stopServerContainer,
|
|
9
|
+
waitForServerReady,
|
|
10
|
+
} from '../lib/serverRunner.js';
|
|
11
|
+
|
|
12
|
+
export async function serverStartAction(opts: {
|
|
13
|
+
port?: string;
|
|
14
|
+
image?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
header('Start Local Server');
|
|
18
|
+
|
|
19
|
+
const dockerOk = await isDockerAvailable();
|
|
20
|
+
if (!dockerOk) {
|
|
21
|
+
return error('Docker is not installed or the Docker daemon is not running.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const port = Number(opts.port ?? 3000);
|
|
25
|
+
// `--port abc` used to mint `http://localhost:NaN` and a baffling docker
|
|
26
|
+
// error two steps later — validate where it is typed.
|
|
27
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
28
|
+
return error(`Invalid port: ${opts.port ?? 3000} (expected 1–65535)`);
|
|
29
|
+
}
|
|
30
|
+
const containerName = opts.name ?? 'ninedeploy';
|
|
31
|
+
const baseUrl = `http://localhost:${port}`;
|
|
32
|
+
|
|
33
|
+
const reachable = await isServerReachable(baseUrl, 500);
|
|
34
|
+
if (reachable) {
|
|
35
|
+
info(`NineDeploy server is already running and reachable at ${baseUrl}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await spinner(`Starting NineDeploy server container (${containerName})`, async () => {
|
|
41
|
+
await startServerContainer({
|
|
42
|
+
port,
|
|
43
|
+
image: opts.image,
|
|
44
|
+
containerName,
|
|
45
|
+
});
|
|
46
|
+
const ready = await waitForServerReady(baseUrl, 30, 1000);
|
|
47
|
+
if (!ready) {
|
|
48
|
+
throw new Error('Server container started but failed to respond within 30s.');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
success(`NineDeploy server is now running at ${baseUrl}`);
|
|
53
|
+
console.log(`\n Next steps:`);
|
|
54
|
+
console.log(` 1. Run ${c.cyan('ninedeploy setup')} to create your admin account.`);
|
|
55
|
+
console.log(` 2. Open ${c.cyan(baseUrl)} to view the web dashboard.\n`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function serverStopAction(opts: { name?: string } = {}): Promise<void> {
|
|
62
|
+
header('Stop Local Server');
|
|
63
|
+
|
|
64
|
+
const dockerOk = await isDockerAvailable();
|
|
65
|
+
if (!dockerOk) {
|
|
66
|
+
return error('Docker is not installed or the Docker daemon is not running.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const containerName = opts.name ?? 'ninedeploy';
|
|
70
|
+
const state = await getContainerState(containerName);
|
|
71
|
+
if (!state.exists || !state.running) {
|
|
72
|
+
info(`Server container (${containerName}) is not running.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await spinner(`Stopping container ${containerName}`, () => stopServerContainer(containerName));
|
|
78
|
+
success(`NineDeploy server stopped.`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
error(`Failed to stop server: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function serverStatusAction(opts: { name?: string; port?: string } = {}): Promise<void> {
|
|
85
|
+
header('Local Server Status');
|
|
86
|
+
|
|
87
|
+
const dockerOk = await isDockerAvailable();
|
|
88
|
+
if (!dockerOk) {
|
|
89
|
+
kv('Docker', c.red('not available'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const containerName = opts.name ?? 'ninedeploy';
|
|
94
|
+
const port = Number(opts.port ?? 3000);
|
|
95
|
+
const baseUrl = `http://localhost:${port}`;
|
|
96
|
+
|
|
97
|
+
const state = await getContainerState(containerName);
|
|
98
|
+
const reachable = await isServerReachable(baseUrl, 800);
|
|
99
|
+
|
|
100
|
+
kv('Container', containerName);
|
|
101
|
+
kv('Container ID', state.id ?? c.gray('—'));
|
|
102
|
+
kv('Docker Status', state.running ? c.green(state.status) : c.yellow(state.status));
|
|
103
|
+
kv('HTTP API', reachable ? c.green(`reachable @ ${baseUrl}`) : c.red(`unreachable @ ${baseUrl}`));
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function serverLogsAction(opts: { name?: string; lines?: string } = {}): Promise<void> {
|
|
108
|
+
const dockerOk = await isDockerAvailable();
|
|
109
|
+
if (!dockerOk) {
|
|
110
|
+
return error('Docker is not installed or the Docker daemon is not running.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const containerName = opts.name ?? 'ninedeploy';
|
|
114
|
+
const lines = Number(opts.lines ?? 50);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const logs = await getServerLogs(containerName, lines);
|
|
118
|
+
if (!logs.trim()) {
|
|
119
|
+
console.log(c.gray(' (no logs recorded)'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log(logs);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
error(`Failed to get server logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|