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
package/dist/index.js
CHANGED
|
@@ -6,26 +6,33 @@ import { loadConfig, saveConfig } from './config.js';
|
|
|
6
6
|
import { banner, error } from './lib/format.js';
|
|
7
7
|
import { loginAction } from './commands/login.js';
|
|
8
8
|
import { setupAction } from './commands/setup.js';
|
|
9
|
-
import { servicesCompose, servicesCreate, servicesDelete, servicesDeploy, servicesExport, servicesGet, servicesInspect, servicesLifecycle, servicesList, servicesLogs, servicesStickyAction, } from './commands/services.js';
|
|
9
|
+
import { servicesCompose, servicesCreate, servicesDelete, servicesDeploy, servicesExport, servicesGet, servicesInspect, servicesLifecycle, servicesList, servicesLogs, servicesStickyAction, servicesPortsAction, } from './commands/services.js';
|
|
10
10
|
import { dbCreate, dbList, deploysCancel, deploysList, deploysQueue, deploysRemove, deploysRollback, systemDashboard, systemInfo, systemRotateKeys, systemUpdateCheck, tplDeploy, tplList, tokenCreate, tokenList, } from './commands/misc.js';
|
|
11
|
-
import { activityList, alertsCreate, alertsList, alertsRemove, backupsCreate, backupsDrill, backupsDrills, backupsList, backupsRestore, deploysWatch, domainsAcceptTransfer, domainsAdd, domainsCancelTransfer, domainsList, domainsPreviewTransfer, domainsRemove, domainsTransfer, envList, envRemove, envSet, networksCreate, networksList, networksRemove, sessionsList, sessionsRevoke, systemExport, systemImport, usersList, usersResetLink, volumesList, volumesRemove, } from './commands/manage.js';
|
|
11
|
+
import { activityList, alertsCreate, alertsList, alertsRemove, backupsCreate, backupsDrill, backupsDrills, backupsList, backupsRestore, deploysWatch, domainsAcceptTransfer, domainsAdd, domainsCancelTransfer, domainsList, domainsPreviewTransfer, domainsRemove, domainsTransfer, domainsVerify, envList, envRemove, envSet, networksCreate, networksList, networksRemove, sessionsList, sessionsRevoke, systemExport, systemImport, usersList, usersResetLink, volumesList, volumesRemove, } from './commands/manage.js';
|
|
12
12
|
import { notificationsCreateFcm as _notifCreateFcm, notificationsCreateWebhook as _notifCreateWebhook, notificationsList as _notifList, notificationsRemove as _notifRemove, notificationsTest as _notifTest, } from './commands/notifications.js';
|
|
13
13
|
import { databasePgbouncer } from './commands/pgbouncer.js';
|
|
14
|
+
import { backupPolicyGet, backupPolicySet } from './commands/backupPolicy.js';
|
|
14
15
|
import { logsSearch } from './commands/logs.js';
|
|
15
16
|
import { emailTemplatesList, emailTemplatesPreview, emailTemplatesReset, emailTemplatesSet, } from './commands/emailTemplates.js';
|
|
16
|
-
import { certificatesExpiring, certificatesList, } from './commands/certificates.js';
|
|
17
|
+
import { certificatesCustomDelete, certificatesCustomList, certificatesCustomReplace, certificatesCustomUpload, certificatesExpiring, certificatesList, } from './commands/certificates.js';
|
|
18
|
+
import { databaseImport, databaseImports, databasePublicAccess, } from './commands/databaseAccess.js';
|
|
19
|
+
import { proxyConfigClear, proxyConfigGet, proxyConfigSet, proxyConfigValidate } from './commands/proxyConfig.js';
|
|
20
|
+
import { secretProvidersDelete, secretProvidersList, secretProvidersSetAws, secretProvidersSetVault, secretProvidersTest, } from './commands/secretProviders.js';
|
|
21
|
+
import { accessGrantsAdd, accessGrantsList, accessGrantsRemove, accessGrantsUpdate, accessMe, terminalOpen, terminalsKill, terminalsList, terminalsShow, trafficService, trafficSettings, trafficSummary, } from './commands/operations.js';
|
|
17
22
|
import { communityTemplatesImport, communityTemplatesList, communityTemplatesRemove, } from './commands/communityTemplates.js';
|
|
18
23
|
import { pluginsList, pluginsMarketplace, pluginsMarketplaceRefresh, pluginsInstall, pluginsEnable, pluginsDisable, pluginsUninstall, pluginsInspect, pluginsReload, } from './commands/plugins.js';
|
|
19
24
|
import { configCenterList, configCenterGet, configCenterSet, configCenterDelete, } from './commands/configCenter.js';
|
|
20
25
|
import { demoSeed } from './commands/demo.js';
|
|
21
26
|
import { workspacesList, workspacesGet, workspacesCreate, workspacesDelete, } from './commands/workspaces.js';
|
|
22
|
-
import { housekeepingPrune, imagesList, imagesPrune } from './commands/housekeeping.js';
|
|
27
|
+
import { housekeepingPrune, imagesList, imagesPrune, positiveIntOption } from './commands/housekeeping.js';
|
|
23
28
|
import { serverStartAction, serverStopAction, serverStatusAction, serverLogsAction, } from './commands/server.js';
|
|
24
29
|
import { doctorAction } from './commands/doctor.js';
|
|
25
30
|
import { firewallStatus, firewallToggle, firewallAddRule, firewallDeleteRule, firewallApplyRecommended, } from './commands/firewall.js';
|
|
26
31
|
import { sourcesAdd, sourcesKeygen, sourcesList, sourcesRemove, sourcesShow, sourcesTest, } from './commands/sources.js';
|
|
32
|
+
import { githubAppAddManual, githubAppList, githubAppRemove, githubAppRotateKey, githubAppShow, githubAppSync, serviceGithub, } from './commands/githubApps.js';
|
|
27
33
|
import { webhooksAdd, webhooksList, webhooksRemove, webhooksShow, } from './commands/webhooks.js';
|
|
28
34
|
import { deployFromGithub } from './commands/deploy.js';
|
|
35
|
+
import { deployUp } from './commands/upload.js';
|
|
29
36
|
import { manifestApply, manifestInit, manifestShow, manifestValidate } from './commands/manifest.js';
|
|
30
37
|
import { templatesInit } from './commands/templates.js';
|
|
31
38
|
import { domainsPresetAddNamecheapAction, domainsPresetApplyAction, domainsPresetListAction, } from './commands/domains.js';
|
|
@@ -34,7 +41,12 @@ import { metricsFlushAction, metricsShowAction, } from './commands/metrics.js';
|
|
|
34
41
|
import { buildCacheStatsAction, } from './commands/buildCache.js';
|
|
35
42
|
import { brandingGetAction, brandingSetAction, } from './commands/branding.js';
|
|
36
43
|
import { egressClearAction, egressListAction, egressSetAction, } from './commands/egress.js';
|
|
44
|
+
import { ipsForgetAction, ipsListAction, ipsSetAction } from './commands/hostIps.js';
|
|
37
45
|
import { ssoAddAction, ssoListAction, ssoRemoveAction, } from './commands/sso.js';
|
|
46
|
+
import { panelBackupDecryptAction, panelBackupListAction, panelBackupNowAction, panelBackupSetAction, panelBackupStatusAction, } from './commands/panelBackup.js';
|
|
47
|
+
// ── 0.16 T8 surfaces ──
|
|
48
|
+
import { serversList, serversRoles, servicesPlacement, servicesSwarm, servicesTransfers, sourcesAllowOnNodes, sourcesHostKeys, sourcesPinHostKeys, sourcesResetHostKeys, swarmInit, swarmJoin, swarmLeave, swarmSetEnabled, swarmStatus, volumesCreate, } from './commands/multiNode.js';
|
|
49
|
+
// ── end 0.16 T8 ──
|
|
38
50
|
const program = new Command();
|
|
39
51
|
program
|
|
40
52
|
.name('ninedeploy')
|
|
@@ -46,27 +58,51 @@ program
|
|
|
46
58
|
program
|
|
47
59
|
.command('init')
|
|
48
60
|
.description('Initialize local instance and setup admin user (alias for setup)')
|
|
49
|
-
.
|
|
61
|
+
.option('--setup-token <token>', 'One-time setup token printed by the installer (or NINEDEPLOY_SETUP_TOKEN)')
|
|
62
|
+
.action((opts) => setupAction(opts));
|
|
50
63
|
program
|
|
51
64
|
.command('setup')
|
|
52
65
|
.description('Create the first admin user on a fresh instance')
|
|
53
|
-
.
|
|
66
|
+
.option('--setup-token <token>', 'One-time setup token printed by the installer (or NINEDEPLOY_SETUP_TOKEN)')
|
|
67
|
+
.action((opts) => setupAction(opts));
|
|
54
68
|
program
|
|
55
69
|
.command('login')
|
|
56
70
|
.description('Authenticate against a NineDeploy server')
|
|
57
71
|
.action(() => loginAction());
|
|
58
72
|
program
|
|
59
73
|
.command('logout')
|
|
60
|
-
.description('
|
|
74
|
+
.description('Revoke this CLI session and clear stored credentials')
|
|
61
75
|
.action(async () => {
|
|
62
76
|
const cfg = loadConfig();
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
// r551: revoke ONLY this CLI's session. `POST /auth/logout` bumps the
|
|
78
|
+
// user's tokenVersion and revokes every session — signing out of the CLI
|
|
79
|
+
// used to sign the user out of every browser too. It is also on the
|
|
80
|
+
// no-refresh list, so with an expired access token it failed silently and
|
|
81
|
+
// the long-lived refresh token stayed live server-side. The sessions list
|
|
82
|
+
// is refresh-aware and flags the caller's own row (`current`), which
|
|
83
|
+
// `DELETE /auth/sessions/:id` then revokes. Best-effort: local credentials
|
|
84
|
+
// are always cleared, and the outcome is printed either way.
|
|
85
|
+
let outcome = 'No stored credentials — nothing to revoke on the server.';
|
|
86
|
+
if (cfg.token || cfg.refreshToken) {
|
|
87
|
+
try {
|
|
88
|
+
const client = getClient();
|
|
89
|
+
const current = (await client.auth.sessions.list()).find((s) => s.current);
|
|
90
|
+
if (current) {
|
|
91
|
+
await client.auth.sessions.revoke(current.id);
|
|
92
|
+
outcome = `Revoked this CLI session (#${current.id}) on ${cfg.baseUrl}; other sessions stay signed in.`;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
outcome = 'The stored credential is not a login session (API token?) — nothing was revoked server-side; delete the token from the dashboard if it should stop working.';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
outcome = `Could not revoke the session on the server (${msg}). Revoke it from the dashboard or \`ninedeploy sessions revoke <id>\` after logging in again.`;
|
|
101
|
+
}
|
|
67
102
|
}
|
|
68
103
|
saveConfig({ baseUrl: cfg.baseUrl });
|
|
69
|
-
console.log(' ✓ Signed out.');
|
|
104
|
+
console.log(' ✓ Signed out (local credentials cleared).');
|
|
105
|
+
console.log(` ${outcome}`);
|
|
70
106
|
});
|
|
71
107
|
program
|
|
72
108
|
.command('whoami')
|
|
@@ -112,7 +148,7 @@ program
|
|
|
112
148
|
}
|
|
113
149
|
});
|
|
114
150
|
// ── Services ──────────────────────────────────────────────────────────────
|
|
115
|
-
const services = program.command('services').description('Manage services');
|
|
151
|
+
const services = program.command('services').alias('service').description('Manage services');
|
|
116
152
|
services.command('list').description('List all services').action(() => servicesList(getClient()));
|
|
117
153
|
services.command('create').description('Create a service (interactive wizard)').action(() => servicesCreate(getClient()));
|
|
118
154
|
services.command('get <id>').description('Show service details').action((id) => servicesGet(getClient(), id));
|
|
@@ -130,16 +166,102 @@ services.command('sticky <id>')
|
|
|
130
166
|
.option('--enable', 'Route every request to the same backend container')
|
|
131
167
|
.option('--disable', 'Remove the sticky-cookie middleware')
|
|
132
168
|
.action((id, opts) => servicesStickyAction(getClient(), id, opts));
|
|
169
|
+
// 0.16.1: extra host ports, TCP or UDP, ranges, an optional dedicated IP.
|
|
170
|
+
services.command('ports <id> [mappings...]')
|
|
171
|
+
.description('List or replace extra host-port mappings (e.g. 27015-27030/udp, 203.0.113.7:8443:443, 5432@10.0.0.0/8)')
|
|
172
|
+
.option('--clear', 'Remove every port mapping')
|
|
173
|
+
.action((id, mappings, opts) => servicesPortsAction(getClient(), id, mappings, opts));
|
|
174
|
+
// 0.13: the service's GitHub App link (show, feedback, migrate/finalize/revert).
|
|
175
|
+
services.command('github <id>')
|
|
176
|
+
.description('Show or change the GitHub App link (commit statuses, PR comments, migrate, finalize, unlink)')
|
|
177
|
+
.option('--status <onOff>', 'Commit statuses for deploys: on | off')
|
|
178
|
+
.option('--pr-comment <onOff>', 'PR preview comments: on | off')
|
|
179
|
+
.option('--migrate <sourceId>', 'Link to the GitHub App installation behind this gh-app source')
|
|
180
|
+
.option('--finalize', 'Detach the previous source and switch the per-service webhooks off')
|
|
181
|
+
.option('--unlink', 'Revert: restore the previous source and webhooks, remove the link')
|
|
182
|
+
.action((id, opts) => serviceGithub(getClient(), id, opts));
|
|
183
|
+
// ── 0.16 T8: build placement, image transfers and Swarm tasks (multi-node) ──
|
|
184
|
+
/* v8 ignore start -- the FakeCommand in test/index.test.ts records but never invokes the action;
|
|
185
|
+
* the implementations are exercised by test/multiNode.test.ts. */
|
|
186
|
+
services.command('placement <id>')
|
|
187
|
+
.description('Show or (operator) change where the image is built and which orchestrator runs it')
|
|
188
|
+
.option('--build-on <where>', 'target (where it runs) | panel | server')
|
|
189
|
+
.option('--build-server <id>', 'The build server for --build-on server (none clears it)')
|
|
190
|
+
.option('--push-registry <sourceId>', 'Ship the image through this registry credential (none = stream relay)')
|
|
191
|
+
.option('--push-repo <repo>', 'Repository for --push-registry, e.g. team/app')
|
|
192
|
+
.option('--orchestrator <name>', 'container | swarm')
|
|
193
|
+
.action((id, opts) => servicesPlacement(getClient(), id, opts));
|
|
194
|
+
services.command('transfers <id>')
|
|
195
|
+
.description('Image transfers of a service, newest first')
|
|
196
|
+
.option('--limit <n>', 'How many (1-100, default 20)')
|
|
197
|
+
.action((id, opts) => servicesTransfers(getClient(), id, opts));
|
|
198
|
+
services.command('swarm <id>').description('Swarm tasks of a service').action((id) => servicesSwarm(getClient(), id));
|
|
199
|
+
/* v8 ignore stop */
|
|
133
200
|
// ── Databases ────────────────────────────────────────────────────────────
|
|
134
201
|
const databases = program.command('databases').description('Manage databases');
|
|
135
202
|
databases.command('list').description('List all databases').action(() => dbList(getClient()));
|
|
136
|
-
databases.command('create')
|
|
203
|
+
databases.command('create')
|
|
204
|
+
.description('Create a database (interactive)')
|
|
205
|
+
.option('--server <id>', 'Run it on this node (operator; fixed for its life)')
|
|
206
|
+
.action((opts) => dbCreate(getClient(), opts));
|
|
137
207
|
// ── PgBouncer sidecar (G-32) ─────────────────────────────────────────────
|
|
138
208
|
databases
|
|
139
209
|
.command('pgbouncer <dbId> <action>')
|
|
140
210
|
.description('Manage the PgBouncer sidecar: enable | disable | status (postgres only)')
|
|
141
211
|
.option('--port <port>', 'Override the listen port (enable only)', (v) => Number(v))
|
|
142
212
|
.action((dbId, action, opts) => databasePgbouncer(getClient(), dbId, action, opts));
|
|
213
|
+
// ── Per-database backup policy (0.12) ────────────────────────────────────
|
|
214
|
+
const backupPolicyCmd = databases
|
|
215
|
+
.command('backup-policy')
|
|
216
|
+
.description('Per-database backup schedule, retention and destination');
|
|
217
|
+
backupPolicyCmd
|
|
218
|
+
.command('get <dbId>')
|
|
219
|
+
.description('Show the backup policy (or the built-in daily schedule)')
|
|
220
|
+
.action((dbId) => backupPolicyGet(getClient(), dbId));
|
|
221
|
+
backupPolicyCmd
|
|
222
|
+
.command('set <dbId>')
|
|
223
|
+
.description('Save the backup policy; unset flags keep their current value')
|
|
224
|
+
.option('--cron <expr>', '5-field cron (minute hour day month weekday), server local time')
|
|
225
|
+
.option('--preset <name>', 'daily | 6h | weekly')
|
|
226
|
+
.option('--keep <n>', 'Scheduled dumps kept locally (1-365)')
|
|
227
|
+
.option('--keep-remote <n>', 'Remote copies kept (1-365; default: same as --keep)')
|
|
228
|
+
.option('--destination <dest>', 'Backup destination id, "active" or "local"')
|
|
229
|
+
.option('--enable', 'Turn scheduled backups on')
|
|
230
|
+
.option('--disable', 'Turn scheduled backups off for this database')
|
|
231
|
+
.action((dbId, opts) => backupPolicySet(getClient(), dbId, opts));
|
|
232
|
+
// ── Public access and dump import (0.14) ────────────────────────────────
|
|
233
|
+
/* v8 ignore start -- the FakeCommand in test/index.test.ts records but never invokes the action;
|
|
234
|
+
* the implementations are exercised by test/databaseAccess.test.ts. */
|
|
235
|
+
databases
|
|
236
|
+
.command('public-access <id>')
|
|
237
|
+
.description('Show, enable/update (--enable) or disable (--disable) public access through a TCP sidecar (operator)')
|
|
238
|
+
.option('--enable', 'Enable or update public access; unset flags keep their current value')
|
|
239
|
+
.option('--disable', 'Remove the sidecar (the settings are kept)')
|
|
240
|
+
.option('--port <port>', 'Host port, 1024-65535')
|
|
241
|
+
.option('--allow <cidr...>', 'Allowed source addresses or CIDRs (replaces the allow-list)')
|
|
242
|
+
.option('--tls <mode>', 'none | terminate (not for mysql/mariadb)')
|
|
243
|
+
.option('--tls-host <host>', 'Hostname clients connect to (and the TLS name in terminate mode)')
|
|
244
|
+
.action((id, opts) => databasePublicAccess(getClient(), id, opts));
|
|
245
|
+
databases
|
|
246
|
+
.command('import <id>')
|
|
247
|
+
.description('Import a dump from a local file (chunked, resumable) or a backup destination object (operator)')
|
|
248
|
+
.option('--file <path>', 'Dump file to upload')
|
|
249
|
+
.option('--from-s3 <destinationId>', 'Backup destination to read the object from (operator)')
|
|
250
|
+
.option('--key <key>', 'Object key inside the destination prefix (with --from-s3)')
|
|
251
|
+
.option('--clean', 'postgres custom format: drop objects before recreating them')
|
|
252
|
+
.option('--no-single-transaction', 'postgres: do not wrap the restore in one transaction')
|
|
253
|
+
.option('--drop', 'mongo: drop each collection before restoring it')
|
|
254
|
+
.option('--confirm-replace', 'redis/valkey/keydb/dragonfly: required, the RDB replaces the whole dataset')
|
|
255
|
+
.option('--no-safety-backup', 'Skip the pre-import backup (operator, or a database created in the last 10 minutes)')
|
|
256
|
+
.option('--resume <importId>', 'Continue an interrupted upload of the same file')
|
|
257
|
+
.option('--no-wait', 'Return once the import has started')
|
|
258
|
+
.action((id, opts) => databaseImport(getClient(), id, opts));
|
|
259
|
+
databases
|
|
260
|
+
.command('imports <id>')
|
|
261
|
+
.description('List a database\'s imports')
|
|
262
|
+
.option('--watch', 'Keep polling until no import is uploading, pending or running')
|
|
263
|
+
.action((id, opts) => databaseImports(getClient(), id, opts));
|
|
264
|
+
/* v8 ignore stop */
|
|
143
265
|
// ── Templates ─────────────────────────────────────────────────────────────
|
|
144
266
|
const templates = program.command('templates').description('Browse the template hub and scaffold starter manifests from it');
|
|
145
267
|
templates.command('list').description('List all templates').action(() => tplList(getClient()));
|
|
@@ -188,12 +310,21 @@ system.command('rotate-keys')
|
|
|
188
310
|
.action(() => systemRotateKeys(getClient()));
|
|
189
311
|
// ── Env vars ──────────────────────────────────────────────────────────────
|
|
190
312
|
const envCmd = program.command('env').description('Manage service environment variables');
|
|
191
|
-
|
|
313
|
+
// 0.12 `--preview`: the service's preview-only set — injected ONLY into its
|
|
314
|
+
// PR previews (overriding the parent's non-secret values there), never into
|
|
315
|
+
// the service itself. Previews still never receive the service's secrets.
|
|
316
|
+
const PREVIEW_FLAG = 'Target the preview-only set: values only this service\'s PR previews receive';
|
|
317
|
+
envCmd.command('list <serviceId>').description('List a service\'s env vars')
|
|
318
|
+
.option('--preview', PREVIEW_FLAG)
|
|
319
|
+
.action((id, opts) => envList(getClient(), id, opts));
|
|
192
320
|
envCmd.command('set <serviceId> <key> <value>')
|
|
193
321
|
.description('Create or update an env var (secret by default)')
|
|
194
322
|
.option('--public', 'Store as a plain (non-secret) value')
|
|
323
|
+
.option('--preview', PREVIEW_FLAG)
|
|
195
324
|
.action((id, key, value, opts) => envSet(getClient(), id, key, value, opts));
|
|
196
|
-
envCmd.command('rm <serviceId> <key>').description('Remove an env var by key')
|
|
325
|
+
envCmd.command('rm <serviceId> <key>').description('Remove an env var by key')
|
|
326
|
+
.option('--preview', PREVIEW_FLAG)
|
|
327
|
+
.action((id, key, opts) => envRemove(getClient(), id, key, opts));
|
|
197
328
|
// ── Domains ────────────────────────────────────────────────────────────────
|
|
198
329
|
const domainsCmd = program.command('domains').description('Manage domains & routing');
|
|
199
330
|
domainsCmd.command('list').description('List all domains').action(() => domainsList(getClient()));
|
|
@@ -203,6 +334,9 @@ domainsCmd.command('add <serviceId> <host>')
|
|
|
203
334
|
.option('--no-ssl', 'Serve over plain HTTP (no TLS)')
|
|
204
335
|
.action((id, host, opts) => domainsAdd(getClient(), id, host, opts));
|
|
205
336
|
domainsCmd.command('rm <serviceId> <domainId>').description('Remove a domain').action((svcId, domId) => domainsRemove(getClient(), svcId, domId));
|
|
337
|
+
domainsCmd.command('verify <serviceId> <domainId>')
|
|
338
|
+
.description('Check a pending domain TXT ownership challenge and bring it live')
|
|
339
|
+
.action((svcId, domId) => domainsVerify(getClient(), svcId, domId));
|
|
206
340
|
// Domain transfer (G-29) — start / preview / accept / cancel.
|
|
207
341
|
domainsCmd
|
|
208
342
|
.command('transfer <domainId>')
|
|
@@ -303,6 +437,18 @@ brandingCmd
|
|
|
303
437
|
void brandingSetAction(getClient(), cleaned);
|
|
304
438
|
});
|
|
305
439
|
// ── Egress IP (G-15) ─────────────────────────────────────────────────────
|
|
440
|
+
// 0.16.1: the panel host's IP addresses (label, primary, dedicate to a service).
|
|
441
|
+
const ipsCmd = program.command('ips').description("Manage this server's IP addresses: label, primary, dedicate to a service");
|
|
442
|
+
ipsCmd.command('list').description('List the IP addresses of the panel host').action(() => ipsListAction(getClient()));
|
|
443
|
+
ipsCmd
|
|
444
|
+
.command('set <ip>')
|
|
445
|
+
.description('Label an address, make it primary, or dedicate it to a service')
|
|
446
|
+
.option('--label <text>', 'A label ("" clears it)')
|
|
447
|
+
.option('--primary', 'Make it the primary address')
|
|
448
|
+
.option('--service <id>', 'Dedicate it to a docker service on the panel host')
|
|
449
|
+
.option('--shared', 'Share it again (undo --service)')
|
|
450
|
+
.action((ip, opts) => ipsSetAction(getClient(), ip, opts));
|
|
451
|
+
ipsCmd.command('forget <ip>').description('Forget what was saved about an address').action((ip) => ipsForgetAction(getClient(), ip));
|
|
306
452
|
const egressCmd = program.command('egress').description('Manage per-project outbound IP rules (G-15)');
|
|
307
453
|
egressCmd.command('list').description('Show every egress IP rule across every registered driver').action(() => egressListAction(getClient()));
|
|
308
454
|
egressCmd
|
|
@@ -347,8 +493,20 @@ ssoCmd
|
|
|
347
493
|
ssoCmd.command('remove <id>').description('Remove a provider by id').action((id) => ssoRemoveAction(getClient(), id));
|
|
348
494
|
// ── Volumes ────────────────────────────────────────────────────────────────
|
|
349
495
|
const volumesCmd = program.command('volumes').description('Manage Docker volumes');
|
|
350
|
-
volumesCmd.command('list')
|
|
351
|
-
|
|
496
|
+
volumesCmd.command('list')
|
|
497
|
+
.description('List all volumes')
|
|
498
|
+
.option('--server <id>', 'A node\'s volumes instead of the panel host\'s')
|
|
499
|
+
.action((opts) => volumesList(getClient(), opts));
|
|
500
|
+
volumesCmd.command('rm <name>')
|
|
501
|
+
.description('Delete a volume (with confirmation)')
|
|
502
|
+
.option('--server <id>', 'Delete it on this node')
|
|
503
|
+
.action((name, opts) => volumesRemove(getClient(), name, opts));
|
|
504
|
+
/* v8 ignore start -- exercised by test/multiNode.test.ts. */
|
|
505
|
+
volumesCmd.command('create <name>')
|
|
506
|
+
.description('Create a managed volume (nd-svc-* / nd-db-*) on the panel host or a node')
|
|
507
|
+
.option('--server <id>', 'Create it on this node')
|
|
508
|
+
.action((name, opts) => volumesCreate(getClient(), name, opts));
|
|
509
|
+
/* v8 ignore stop */
|
|
352
510
|
// ── Networks ───────────────────────────────────────────────────────────────
|
|
353
511
|
const networksCmd = program.command('networks').description('Manage Docker networks');
|
|
354
512
|
networksCmd.command('list').description('List user-defined networks').action(() => networksList(getClient()));
|
|
@@ -375,7 +533,7 @@ backupsCmd
|
|
|
375
533
|
const alertsCmd = program.command('alerts').description('Manage alert rules');
|
|
376
534
|
alertsCmd.command('list').description('List alert rules').action(() => alertsList(getClient()));
|
|
377
535
|
alertsCmd.command('create <name> <metric> <operator> <threshold>')
|
|
378
|
-
.description('Create an alert rule (metric: cpu|memory|cert-expiry, operator: > or <)')
|
|
536
|
+
.description('Create an alert rule (metric: cpu|memory|cert-expiry|disk|server_offline, operator: > or <)')
|
|
379
537
|
.option('-w, --windows <n>', 'Consecutive 30s samples before firing', '1')
|
|
380
538
|
.option('-s, --service <id>', 'Scope to a service (default: host-wide)')
|
|
381
539
|
.action((name, metric, op, threshold, opts) => alertsCreate(getClient(), name, metric, op, threshold, opts));
|
|
@@ -410,8 +568,10 @@ plugins.command('list').description('List all installed plugins').action(() => p
|
|
|
410
568
|
plugins.command('marketplace').description('Browse verified marketplace extensions').action(() => pluginsMarketplace(getClient()));
|
|
411
569
|
plugins.command('marketplace-refresh').description('Bypass the cache and re-fetch the live signed marketplace index').action(() => pluginsMarketplaceRefresh(getClient()));
|
|
412
570
|
plugins.command('inspect <id>').description('Inspect plugin manifest and runtime telemetry').action((id) => pluginsInspect(getClient(), id));
|
|
413
|
-
plugins.command('install <target>').description('Install a plugin (marketplace,
|
|
414
|
-
|
|
571
|
+
plugins.command('install <target>').description('Install a plugin (marketplace, sandbox)')
|
|
572
|
+
// r440: only what the server can actually load — npm/git/local were
|
|
573
|
+
// advertised here and always answered 400 UnsupportedPluginSourceError.
|
|
574
|
+
.option('-s, --source <source>', 'Source type (marketplace, sandbox)', 'marketplace')
|
|
415
575
|
.option('-n, --name <name>', 'Custom display name')
|
|
416
576
|
.option('-v, --version <version>', 'Custom version')
|
|
417
577
|
.option('-d, --desc <description>', 'Description')
|
|
@@ -454,8 +614,10 @@ images
|
|
|
454
614
|
images
|
|
455
615
|
.command('prune')
|
|
456
616
|
.description('Prune images. Refuses to run with no filter; pass --dry-run first.')
|
|
457
|
-
|
|
458
|
-
.
|
|
617
|
+
// r554: validated whole numbers (the server's bounds) — `Number(v)` let
|
|
618
|
+
// `1.5` / `abc` through to an opaque 422.
|
|
619
|
+
.option('--keep-last <n>', 'Keep the newest N images per repo:tag (rest are candidates)', positiveIntOption(1000))
|
|
620
|
+
.option('--older-than <hours>', 'Only prune images older than N hours', positiveIntOption(8760))
|
|
459
621
|
.option('--dangling', 'Only prune dangling images (repo/tag both <none>)')
|
|
460
622
|
.option('--dry-run', 'Report what would be deleted without actually deleting')
|
|
461
623
|
.action((opts) => imagesPrune(getClient(), {
|
|
@@ -523,6 +685,33 @@ emailTemplatesCmd
|
|
|
523
685
|
// ── System export/import + deploy log streaming ────────────────────────────
|
|
524
686
|
system.command('export [file]').description('Export the full system state as JSON').action((file) => systemExport(file));
|
|
525
687
|
system.command('import <file>').description('Import a system bundle (destructive)').action((file) => systemImport(file));
|
|
688
|
+
// ── Panel self-backup (0.12) ────────────────────────────────────────────────
|
|
689
|
+
const panelBackupCmd = system.command('panel-backup').description("Scheduled, passphrase-sealed backups of the panel's own state");
|
|
690
|
+
panelBackupCmd.command('status').description('Show the schedule, destination, retention and the last run').action(() => panelBackupStatusAction(getClient()));
|
|
691
|
+
panelBackupCmd
|
|
692
|
+
.command('set')
|
|
693
|
+
.description('Change the panel backup settings (omitted options keep their value)')
|
|
694
|
+
.option('--enable', 'Turn scheduled panel backups on')
|
|
695
|
+
.option('--disable', 'Turn scheduled panel backups off')
|
|
696
|
+
.option('--cron <expr>', '5-field cron expression, e.g. "0 3 * * *"')
|
|
697
|
+
.option('--destination <id>', 'Backup destination id (see Settings → Integrations)')
|
|
698
|
+
.option('--retain <n>', 'How many panel backups to keep in the destination')
|
|
699
|
+
.option('--passphrase', 'Prompt for a new recovery passphrase (keep it off this server)')
|
|
700
|
+
.action((opts) => panelBackupSetAction(getClient(), opts));
|
|
701
|
+
panelBackupCmd
|
|
702
|
+
.command('now')
|
|
703
|
+
.description('Back up the panel now')
|
|
704
|
+
.option('--wait', 'Wait for the run to finish and report its outcome')
|
|
705
|
+
.action((opts) => panelBackupNowAction(getClient(), opts));
|
|
706
|
+
panelBackupCmd
|
|
707
|
+
.command('list')
|
|
708
|
+
.description('List the panel backups in the destination, newest first')
|
|
709
|
+
.option('--destination <id>', 'Destination to list (default: the configured one)')
|
|
710
|
+
.action((opts) => panelBackupListAction(getClient(), opts));
|
|
711
|
+
panelBackupCmd
|
|
712
|
+
.command('decrypt <file> [out]')
|
|
713
|
+
.description('Offline: open a downloaded .ndpb with the recovery passphrase into a .tar.gz for `system import`')
|
|
714
|
+
.action((file, out) => panelBackupDecryptAction(file, out));
|
|
526
715
|
// ── Certificates (G-15) ─────────────────────────────────────────────────────
|
|
527
716
|
const certificates = program.command('certificates').description('Traefik certificate inventory and renewal status');
|
|
528
717
|
certificates
|
|
@@ -535,6 +724,201 @@ certificates
|
|
|
535
724
|
.description('Focused list of certificates expiring within N days')
|
|
536
725
|
.option('--days <days>', 'Window in days (default 30)')
|
|
537
726
|
.action((opts) => certificatesExpiring(getClient(), opts));
|
|
727
|
+
// 0.14: uploaded certificates (operator).
|
|
728
|
+
const customCerts = certificates.command('custom').description('Uploaded TLS certificates (operator)');
|
|
729
|
+
/* v8 ignore start -- exercised by test/certificates.test.ts. */
|
|
730
|
+
customCerts.command('list').description('List uploaded certificates, their hostnames, expiry and covered domains')
|
|
731
|
+
.action(() => certificatesCustomList(getClient()));
|
|
732
|
+
customCerts.command('upload')
|
|
733
|
+
.description('Upload a certificate chain and its unencrypted private key')
|
|
734
|
+
.option('--name <name>', 'Display name')
|
|
735
|
+
.option('--cert <path>', 'PEM certificate chain, leaf first')
|
|
736
|
+
.option('--key <path>', 'PEM private key (unencrypted)')
|
|
737
|
+
.action((opts) => certificatesCustomUpload(getClient(), opts));
|
|
738
|
+
customCerts.command('replace <id>')
|
|
739
|
+
.description('Replace a certificate (e.g. after renewal)')
|
|
740
|
+
.option('--name <name>', 'New display name')
|
|
741
|
+
.option('--cert <path>', 'PEM certificate chain, leaf first')
|
|
742
|
+
.option('--key <path>', 'PEM private key (unencrypted)')
|
|
743
|
+
.action((id, opts) => certificatesCustomReplace(getClient(), id, opts));
|
|
744
|
+
customCerts.command('delete <id>')
|
|
745
|
+
.description('Delete a certificate (covered domains fall back to ACME)')
|
|
746
|
+
.alias('rm')
|
|
747
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
748
|
+
.action((id, opts) => certificatesCustomDelete(getClient(), id, opts));
|
|
749
|
+
/* v8 ignore stop */
|
|
750
|
+
// ── Proxy custom config (0.14) ──────────────────────────────────────────────
|
|
751
|
+
const proxyCmd = program.command('proxy').description('Traefik proxy management (operator)');
|
|
752
|
+
const proxyConfigCmd = proxyCmd.command('config').description('Custom Traefik dynamic config (custom.yml)');
|
|
753
|
+
/* v8 ignore start -- exercised by test/proxyConfig.test.ts. */
|
|
754
|
+
proxyConfigCmd.command('get').description('Show the custom config, its status and the last error').action(() => proxyConfigGet(getClient()));
|
|
755
|
+
proxyConfigCmd.command('validate')
|
|
756
|
+
.description('Validate a YAML file without applying it')
|
|
757
|
+
.option('--file <path>', 'YAML file')
|
|
758
|
+
.action((opts) => proxyConfigValidate(getClient(), opts));
|
|
759
|
+
proxyConfigCmd.command('set')
|
|
760
|
+
.description('Validate and apply a YAML file')
|
|
761
|
+
.option('--file <path>', 'YAML file')
|
|
762
|
+
.action((opts) => proxyConfigSet(getClient(), opts));
|
|
763
|
+
proxyConfigCmd.command('clear')
|
|
764
|
+
.description('Remove the custom config')
|
|
765
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
766
|
+
.action((opts) => proxyConfigClear(getClient(), opts));
|
|
767
|
+
/* v8 ignore stop */
|
|
768
|
+
// ── Secret managers (0.14) ──────────────────────────────────────────────────
|
|
769
|
+
const secretsCmd = program.command('secrets').description('Deploy-time secret references');
|
|
770
|
+
const providersCmd = secretsCmd.command('providers').description('HashiCorp Vault / OpenBao and AWS Secrets Manager (operator)');
|
|
771
|
+
/* v8 ignore start -- exercised by test/secretProviders.test.ts. */
|
|
772
|
+
providersCmd.command('list').description('List the secret managers and their last test').action(() => secretProvidersList(getClient()));
|
|
773
|
+
providersCmd.command('set-vault')
|
|
774
|
+
.description('Configure Vault / OpenBao (KV v2); token from --token-file or NINEDEPLOY_VAULT_TOKEN')
|
|
775
|
+
.option('--address <url>', 'Vault address (https)')
|
|
776
|
+
.option('--namespace <ns>', 'Vault Enterprise / OpenBao namespace')
|
|
777
|
+
.option('--mount <path>', 'KV v2 mount (default secret)')
|
|
778
|
+
.option('--auth <method>', 'token | approle (default token)')
|
|
779
|
+
.option('--approle-mount <path>', 'AppRole auth mount (default approle)')
|
|
780
|
+
.option('--token-file <path>', 'File holding the token (or NINEDEPLOY_VAULT_TOKEN)')
|
|
781
|
+
.option('--role-id-file <path>', 'File holding the AppRole role id (or NINEDEPLOY_VAULT_ROLE_ID)')
|
|
782
|
+
.option('--secret-id-file <path>', 'File holding the AppRole secret id (or NINEDEPLOY_VAULT_SECRET_ID)')
|
|
783
|
+
.option('--enable', 'Enable the provider')
|
|
784
|
+
.option('--disable', 'Disable the provider (references stay literal)')
|
|
785
|
+
.action((opts) => secretProvidersSetVault(getClient(), opts));
|
|
786
|
+
providersCmd.command('set-aws')
|
|
787
|
+
.description('Configure AWS Secrets Manager; secret key from --secret-access-key-file or NINEDEPLOY_AWS_SECRET_ACCESS_KEY')
|
|
788
|
+
.option('--region <region>', 'AWS region, e.g. eu-west-1')
|
|
789
|
+
.option('--endpoint <url>', 'VPC endpoint or compatible service (https)')
|
|
790
|
+
.option('--role-arn <arn>', 'IAM role to assume')
|
|
791
|
+
.option('--external-id <id>', 'External id for the role (with --role-arn)')
|
|
792
|
+
.option('--role-session-name <name>', 'Session name (default ninedeploy)')
|
|
793
|
+
.option('--access-key-id <id>', 'Access key id (or NINEDEPLOY_AWS_ACCESS_KEY_ID)')
|
|
794
|
+
.option('--secret-access-key-file <path>', 'File holding the secret access key (or NINEDEPLOY_AWS_SECRET_ACCESS_KEY)')
|
|
795
|
+
.option('--session-token-file <path>', 'File holding a session token (or NINEDEPLOY_AWS_SESSION_TOKEN)')
|
|
796
|
+
.option('--enable', 'Enable the provider')
|
|
797
|
+
.option('--disable', 'Disable the provider (references stay literal)')
|
|
798
|
+
.action((opts) => secretProvidersSetAws(getClient(), opts));
|
|
799
|
+
providersCmd.command('test <kind>')
|
|
800
|
+
.description('Test a provider (vault | aws), optionally reading one secret')
|
|
801
|
+
.option('--probe-path <path>', 'vault: also read this path (relative to the mount)')
|
|
802
|
+
.option('--probe-secret-id <id>', 'aws: also read this secret')
|
|
803
|
+
.action((kind, opts) => secretProvidersTest(getClient(), kind, opts));
|
|
804
|
+
providersCmd.command('delete <kind>')
|
|
805
|
+
.description('Remove a provider (vault | aws); its references stay literal at deploy')
|
|
806
|
+
.alias('rm')
|
|
807
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
808
|
+
.action((kind, opts) => secretProvidersDelete(getClient(), kind, opts));
|
|
809
|
+
/* v8 ignore stop */
|
|
810
|
+
// ── 0.15: terminals, traffic analytics, access grants ────────────────────
|
|
811
|
+
const terminalsCmd = program.command('terminals').description('Terminal session history and termination (operator)');
|
|
812
|
+
/* v8 ignore start -- exercised by test/operations.test.ts. */
|
|
813
|
+
terminalsCmd.command('list')
|
|
814
|
+
.description('List terminal sessions, newest first (metadata only)')
|
|
815
|
+
.option('--status <status>', 'pending | active | ended | failed | expired')
|
|
816
|
+
.option('--target <kind>', 'service | database | container | host')
|
|
817
|
+
.option('--limit <n>', 'Page size (1-200, default 50)')
|
|
818
|
+
.option('--before <id>', 'Page back from this session id')
|
|
819
|
+
.action((opts) => terminalsList(getClient(), opts));
|
|
820
|
+
terminalsCmd.command('show <id>').description('Show one terminal session').action((id) => terminalsShow(getClient(), id));
|
|
821
|
+
terminalsCmd.command('kill <id>')
|
|
822
|
+
.description('Terminate a live session (or revoke a pending one)')
|
|
823
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
824
|
+
.action((id, opts) => terminalsKill(getClient(), id, opts));
|
|
825
|
+
/* v8 ignore stop */
|
|
826
|
+
const terminalCmd = program.command('terminal').alias('shell').description('Open an interactive shell (operator; needs a TTY)');
|
|
827
|
+
/* v8 ignore start -- exercised by test/operations.test.ts. */
|
|
828
|
+
terminalCmd.command('service <id>')
|
|
829
|
+
.description('Shell into the container of a service')
|
|
830
|
+
.option('--replica <n>', 'Replica number (1-based)')
|
|
831
|
+
.option('--node <serverId>', 'A node the service runs on (fan-out target)')
|
|
832
|
+
.action((id, opts) => terminalOpen(getClient(), 'service', id, opts));
|
|
833
|
+
terminalCmd.command('db <id>')
|
|
834
|
+
.description('Shell into the container of a database')
|
|
835
|
+
.option('--client', 'Open the engine client (psql, mysql, redis-cli) with the stored credentials')
|
|
836
|
+
.action((id, opts) => terminalOpen(getClient(), 'db', id, opts));
|
|
837
|
+
terminalCmd.command('container <name>')
|
|
838
|
+
.description('Shell into a managed container on the panel host')
|
|
839
|
+
.action((name) => terminalOpen(getClient(), 'container', name));
|
|
840
|
+
terminalCmd.command('host [serverId]')
|
|
841
|
+
.description('Root shell on the panel host or a node (off by default; asks for your password)')
|
|
842
|
+
.action((serverId) => terminalOpen(getClient(), 'host', serverId));
|
|
843
|
+
/* v8 ignore stop */
|
|
844
|
+
const trafficCmd = program.command('traffic').description('Traffic analytics (opt-in)');
|
|
845
|
+
/* v8 ignore start -- exercised by test/operations.test.ts. */
|
|
846
|
+
trafficCmd.command('settings')
|
|
847
|
+
.description('Show or change traffic analytics (enabling or disabling recreates Traefik once)')
|
|
848
|
+
.option('--enable', 'Turn analytics on')
|
|
849
|
+
.option('--disable', 'Turn analytics off')
|
|
850
|
+
.option('--retention <days>', 'Keep hourly rows this many days (1-400)')
|
|
851
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
852
|
+
.action((opts) => trafficSettings(getClient(), opts));
|
|
853
|
+
trafficCmd.command('summary')
|
|
854
|
+
.description('Instance traffic: totals, percentiles and top domains (operator)')
|
|
855
|
+
.option('--range <range>', '1h | 24h | 7d | 30d (default 24h)')
|
|
856
|
+
.option('--top <n>', 'Top domains (1-50, default 10)')
|
|
857
|
+
.action((opts) => trafficSummary(getClient(), opts));
|
|
858
|
+
trafficCmd.command('service <id>')
|
|
859
|
+
.description('Traffic of one service, by domain')
|
|
860
|
+
.option('--range <range>', '1h | 24h | 7d | 30d (default 24h)')
|
|
861
|
+
.action((id, opts) => trafficService(getClient(), id, opts));
|
|
862
|
+
/* v8 ignore stop */
|
|
863
|
+
const accessCmd = program.command('access').description('Project and environment access grants');
|
|
864
|
+
const grantsCmd = accessCmd.command('grants').description('Manage access grants in a workspace (workspace admin)');
|
|
865
|
+
/* v8 ignore start -- exercised by test/operations.test.ts. */
|
|
866
|
+
grantsCmd.command('list')
|
|
867
|
+
.description('List the grants in a workspace')
|
|
868
|
+
.option('--workspace <id>', 'Workspace id')
|
|
869
|
+
.option('--user <id>', 'Only this user')
|
|
870
|
+
.option('--project <id>', 'Only this project')
|
|
871
|
+
.option('--environment <id>', 'Only this environment')
|
|
872
|
+
.action((opts) => accessGrantsList(getClient(), opts));
|
|
873
|
+
grantsCmd.command('add')
|
|
874
|
+
.description('Grant a role on a project, an environment, or both')
|
|
875
|
+
.option('--workspace <id>', 'Workspace id')
|
|
876
|
+
.option('--email <email>', 'The user, by email')
|
|
877
|
+
.option('--user <id>', 'The user, by id')
|
|
878
|
+
.option('--project <id>', 'Project id')
|
|
879
|
+
.option('--environment <id>', 'Environment id')
|
|
880
|
+
.option('--role <role>', 'viewer | member | admin')
|
|
881
|
+
.action((opts) => accessGrantsAdd(getClient(), opts));
|
|
882
|
+
grantsCmd.command('update <grantId>')
|
|
883
|
+
.description('Change the role of a grant, or suspend or reinstate it')
|
|
884
|
+
.option('--workspace <id>', 'Workspace id')
|
|
885
|
+
.option('--role <role>', 'viewer | member | admin')
|
|
886
|
+
.option('--suspend', 'Keep the grant but stop it giving access')
|
|
887
|
+
.option('--reinstate', 'Make a suspended grant give access again')
|
|
888
|
+
.action((grantId, opts) => accessGrantsUpdate(getClient(), grantId, opts));
|
|
889
|
+
grantsCmd.command('remove <grantId>')
|
|
890
|
+
.alias('rm')
|
|
891
|
+
.description('Revoke a grant')
|
|
892
|
+
.option('--workspace <id>', 'Workspace id')
|
|
893
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
894
|
+
.action((grantId, opts) => accessGrantsRemove(getClient(), grantId, opts));
|
|
895
|
+
accessCmd.command('me').description('Your own grants and guest workspaces').action(() => accessMe(getClient()));
|
|
896
|
+
/* v8 ignore stop */
|
|
897
|
+
// ── 0.16 T8: remote nodes and Swarm (operator) ───────────────────────────
|
|
898
|
+
const serversCmd = program.command('servers').description('Remote nodes: agent capabilities, build-server role (operator)');
|
|
899
|
+
/* v8 ignore start -- exercised by test/multiNode.test.ts. */
|
|
900
|
+
serversCmd.command('list').description('List nodes with their agent version, build role, databases and Swarm membership').action(() => serversList(getClient()));
|
|
901
|
+
serversCmd.command('roles <id>')
|
|
902
|
+
.description('Set the build-server role of a node')
|
|
903
|
+
.option('--build-server <onOff>', 'on | off')
|
|
904
|
+
.option('--build-concurrency <n>', 'Builds at once on this node (1-8)')
|
|
905
|
+
.action((id, opts) => serversRoles(getClient(), id, opts));
|
|
906
|
+
/* v8 ignore stop */
|
|
907
|
+
const swarmCmd = program.command('swarm').description('Docker Swarm on the panel host (operator, opt-in per service)');
|
|
908
|
+
/* v8 ignore start -- exercised by test/multiNode.test.ts. */
|
|
909
|
+
swarmCmd.command('status').description('Swarm state, the join address and the nodes').action(() => swarmStatus(getClient()));
|
|
910
|
+
swarmCmd.command('init')
|
|
911
|
+
.description('Initialise Swarm on the panel host (asks for your password)')
|
|
912
|
+
.option('--advertise-addr <ip>', 'The address nodes reach the panel host on')
|
|
913
|
+
.action((opts) => swarmInit(getClient(), opts));
|
|
914
|
+
swarmCmd.command('enable').description('Allow Swarm deploys (asks for your password)').action(() => swarmSetEnabled(getClient(), true));
|
|
915
|
+
swarmCmd.command('disable').description('Refuse new Swarm deploys; running stacks keep running').action(() => swarmSetEnabled(getClient(), false));
|
|
916
|
+
swarmCmd.command('join <serverId>').description('Join a node to the swarm as a worker').action((serverId) => swarmJoin(getClient(), serverId));
|
|
917
|
+
swarmCmd.command('leave <serverId>')
|
|
918
|
+
.description('Drain a node and take it out of the swarm')
|
|
919
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
920
|
+
.action((serverId, opts) => swarmLeave(getClient(), serverId, opts));
|
|
921
|
+
/* v8 ignore stop */
|
|
538
922
|
deploys.command('watch <serviceId> <deployId>').description('Stream a deployment\'s build logs live').action((svcId, depId) => deploysWatch(svcId, depId));
|
|
539
923
|
// ── Diagnostics ───────────────────────────────────────────────────────────
|
|
540
924
|
program
|
|
@@ -548,10 +932,49 @@ const sources = program.command('sources').alias('creds').description('Manage pr
|
|
|
548
932
|
* the implementation is exercised end-to-end by test/sources.test.ts. */
|
|
549
933
|
sources.command('list').description('List configured sources').action(() => sourcesList(getClient()));
|
|
550
934
|
sources.command('show <id>').description('Show one source in detail').action((id) => sourcesShow(getClient(), id));
|
|
551
|
-
sources.command('add [name]')
|
|
935
|
+
sources.command('add [name]')
|
|
936
|
+
.description('Add a new source (interactive; PAT/SSH key from env or masked prompt)')
|
|
937
|
+
.option('--base-url <url>', 'Gitea base URL, e.g. https://git.example.com (enables the live test and repo list)')
|
|
938
|
+
.action((name, opts) => sourcesAdd(getClient(), name, opts));
|
|
552
939
|
sources.command('test [id]').description('Verify that a stored source token still authenticates').action((id) => sourcesTest(getClient(), id));
|
|
553
940
|
sources.command('keygen [id]').description('Generate an ed25519 deploy-key pair on the panel and print the public key').action((id) => sourcesKeygen(getClient(), id));
|
|
554
941
|
sources.command('remove [id]').description('Remove a source (with confirmation)').alias('rm').action((id) => sourcesRemove(getClient(), id));
|
|
942
|
+
// 0.16 T8 (multi-node): send this PAT or deploy key to nodes for clones there.
|
|
943
|
+
sources.command('allow-on-nodes <id> <onOff>')
|
|
944
|
+
.description('Allow (on, asks for your password) or stop (off) sending this credential to nodes for clones')
|
|
945
|
+
.action((id, onOff) => sourcesAllowOnNodes(getClient(), id, onOff));
|
|
946
|
+
// 0.15.8: pin the git host's SSH keys for deploy-key clones the panel makes.
|
|
947
|
+
sources.command('pin-host-keys <id> <onOff>')
|
|
948
|
+
.description('Pin (on) or stop pinning (off, asks for your password) the git host SSH keys of a deploy-key source')
|
|
949
|
+
.action((id, onOff) => sourcesPinHostKeys(getClient(), id, onOff));
|
|
950
|
+
sources.command('reset-host-keys <id>')
|
|
951
|
+
.description('Forget the pinned git host keys so the next clone learns them again (asks for your password)')
|
|
952
|
+
.action((id) => sourcesResetHostKeys(getClient(), id));
|
|
953
|
+
sources.command('host-keys <id>').description('Show the git host key fingerprints a source has pinned').action((id) => sourcesHostKeys(getClient(), id));
|
|
954
|
+
/* v8 ignore stop */
|
|
955
|
+
// ── GitHub Apps (0.13) ─────────────────────────────────────────────────────
|
|
956
|
+
const githubApp = program.command('github-app').description('Manage GitHub Apps (one-click setup runs in the panel: Sources → GitHub Apps)');
|
|
957
|
+
/* v8 ignore start -- see sources.ts note above; exercised by test/githubApps.test.ts. */
|
|
958
|
+
githubApp.command('list').description('List registered GitHub Apps').action(() => githubAppList(getClient()));
|
|
959
|
+
githubApp.command('show <id>').description('Show a GitHub App and its installations').action((id) => githubAppShow(getClient(), id));
|
|
960
|
+
githubApp.command('add-manual')
|
|
961
|
+
.description('Register an existing GitHub App or one on GitHub Enterprise Server (key from --key-file or NINEDEPLOY_GITHUB_APP_KEY)')
|
|
962
|
+
.option('--name <name>', 'Display name')
|
|
963
|
+
.option('--app-id <id>', 'GitHub App ID')
|
|
964
|
+
.option('--key-file <path>', 'Path to the App private key (.pem)')
|
|
965
|
+
.option('--web-base-url <url>', 'GHES web URL, e.g. https://github.example.com (with --api-base-url)')
|
|
966
|
+
.option('--api-base-url <url>', 'GHES API URL, e.g. https://github.example.com/api/v3 (with --web-base-url)')
|
|
967
|
+
.action((opts) => githubAppAddManual(getClient(), opts));
|
|
968
|
+
githubApp.command('sync <id>').description('Sync installations from GitHub (creates their gh-app sources)').action((id) => githubAppSync(getClient(), id));
|
|
969
|
+
githubApp.command('rotate-key <id>')
|
|
970
|
+
.description('Replace the App private key (verified with GitHub first)')
|
|
971
|
+
.option('--key-file <path>', 'Path to the new private key (.pem)')
|
|
972
|
+
.action((id, opts) => githubAppRotateKey(getClient(), id, opts));
|
|
973
|
+
githubApp.command('remove <id>')
|
|
974
|
+
.description('Forget a GitHub App (its installation sources stay)')
|
|
975
|
+
.alias('rm')
|
|
976
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
977
|
+
.action((id, opts) => githubAppRemove(getClient(), id, opts));
|
|
555
978
|
/* v8 ignore stop */
|
|
556
979
|
// ── Deploy (one-shot private GitHub deploy) ────────────────────────────────
|
|
557
980
|
const deploy = program.command('deploy').description('End-to-end deploy helpers');
|
|
@@ -560,6 +983,14 @@ deploy.command('create-from-github [url]')
|
|
|
560
983
|
.description('One-shot: add source (PAT), analyse repo, create service, set env, trigger deploy, optional webhook')
|
|
561
984
|
.alias('from-github')
|
|
562
985
|
.action((url) => deployFromGithub(getClient(), url));
|
|
986
|
+
deploy.command('up [path]')
|
|
987
|
+
.description('Deploy a local folder, .zip or .tar.gz as a service source (no Git repository needed)')
|
|
988
|
+
.option('-s, --service <idOrSlug>', 'An existing service created without a repository or image')
|
|
989
|
+
.option('-c, --create <name>', 'Create a new docker service for this source')
|
|
990
|
+
.option('-p, --port <port>', 'Container port, with --create')
|
|
991
|
+
.option('-x, --exclude <pattern...>', 'Extra paths to leave out (.git, node_modules, .env… are always left out)')
|
|
992
|
+
.option('--no-deploy', 'Only upload; deploy later')
|
|
993
|
+
.action((dir, opts) => deployUp(getClient(), dir, opts));
|
|
563
994
|
/* v8 ignore stop */
|
|
564
995
|
// ── Webhooks (auto-deploy on push) ─────────────────────────────────────────
|
|
565
996
|
const webhooksCmd = program.command('webhooks').description('Manage auto-deploy webhooks');
|