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,299 @@
|
|
|
1
|
+
import { NineDeployError } from '@ninedeploy/sdk';
|
|
2
|
+
import { formatPortMapping, type PortMapping, parsePortMapping, portMappingsProblem } from '@ninedeploy/schemas';
|
|
3
|
+
import type { NineDeployClient } from '../client.js';
|
|
4
|
+
import { c, error, fmtTime, header, info, kv, spinner, statusColor, success, table } from '../lib/format.js';
|
|
5
|
+
import { prompt } from '../prompts.js';
|
|
6
|
+
|
|
7
|
+
/** `ninedeploy services list` */
|
|
8
|
+
export async function servicesList(client: NineDeployClient): Promise<void> {
|
|
9
|
+
header('Services');
|
|
10
|
+
await spinner('Fetching services', async () => {
|
|
11
|
+
const services = await client.services.list();
|
|
12
|
+
if (services.length === 0) { info('No services yet. Run `ninedeploy services create`.'); return; }
|
|
13
|
+
table(
|
|
14
|
+
services.map((s) => ({
|
|
15
|
+
id: s.id,
|
|
16
|
+
name: s.name,
|
|
17
|
+
type: s.type,
|
|
18
|
+
status: s.status,
|
|
19
|
+
port: s.port ?? '—',
|
|
20
|
+
updated: fmtTime(s.updatedAt),
|
|
21
|
+
})),
|
|
22
|
+
['id', 'name', 'type', 'status', 'port', 'updated'],
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `ninedeploy services create` — interactive wizard */
|
|
28
|
+
export async function servicesCreate(client: NineDeployClient): Promise<void> {
|
|
29
|
+
header('Create Service');
|
|
30
|
+
console.log(c.dim(' Answer a few questions to deploy your app.\n'));
|
|
31
|
+
|
|
32
|
+
const name = await prompt('Service name');
|
|
33
|
+
if (!name) return error('Name is required');
|
|
34
|
+
|
|
35
|
+
const mode = await prompt('Deploy from (1) Git repo (2) Docker image', '1');
|
|
36
|
+
const useRepo = mode !== '2';
|
|
37
|
+
|
|
38
|
+
let repoUrl = '';
|
|
39
|
+
let image = '';
|
|
40
|
+
let branch = 'main';
|
|
41
|
+
if (useRepo) {
|
|
42
|
+
repoUrl = await prompt('Repository URL', '');
|
|
43
|
+
if (!repoUrl) return error('Repository URL is required');
|
|
44
|
+
branch = await prompt('Branch', 'main');
|
|
45
|
+
} else {
|
|
46
|
+
image = await prompt('Docker image (e.g. nginx:alpine)');
|
|
47
|
+
if (!image) return error('Image is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const portStr = await prompt('Port (optional)', '');
|
|
51
|
+
const port = portStr ? Number(portStr) : undefined;
|
|
52
|
+
const pubPortStr = await prompt('Direct host port (optional)', '');
|
|
53
|
+
const publishedPort = pubPortStr ? Number(pubPortStr) : undefined;
|
|
54
|
+
const volume = await prompt('Persistent volume path (optional)', '');
|
|
55
|
+
|
|
56
|
+
console.log();
|
|
57
|
+
try {
|
|
58
|
+
const svc = await spinner('Creating service', () =>
|
|
59
|
+
client.services.create({
|
|
60
|
+
name, type: 'docker',
|
|
61
|
+
repoUrl: useRepo ? repoUrl : undefined,
|
|
62
|
+
image: useRepo ? undefined : image,
|
|
63
|
+
branch, port, publishedPort, volumeMount: volume || undefined,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
success(`Service "${svc.name}" created (id: ${svc.id})`);
|
|
67
|
+
|
|
68
|
+
const deployNow = await prompt('Deploy now? (y/n)', 'y');
|
|
69
|
+
if (deployNow.toLowerCase().startsWith('y')) {
|
|
70
|
+
const res = await spinner('Deploying', () => client.deploys.trigger(svc.id));
|
|
71
|
+
success(`Deployment #${res.deploymentId} queued. Check logs: ninedeploy services logs ${svc.id}`);
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
error(err instanceof NineDeployError ? err.message : String(err));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `ninedeploy services get <id>` */
|
|
79
|
+
export async function servicesGet(client: NineDeployClient, idStr: string): Promise<void> {
|
|
80
|
+
const id = Number(idStr);
|
|
81
|
+
if (!id) return error('Usage: ninedeploy services get <id>');
|
|
82
|
+
header('Service Details');
|
|
83
|
+
await spinner('Fetching', async () => {
|
|
84
|
+
const svc = await client.services.get(id);
|
|
85
|
+
kv('ID', svc.id);
|
|
86
|
+
kv('Name', svc.name);
|
|
87
|
+
kv('Slug', svc.slug);
|
|
88
|
+
kv('Type', svc.type);
|
|
89
|
+
kv('Status', statusColor(svc.status));
|
|
90
|
+
kv('Repo', svc.repoUrl ?? '—');
|
|
91
|
+
kv('Branch', svc.branch);
|
|
92
|
+
kv('Image', svc.image ?? '—');
|
|
93
|
+
kv('Port', svc.port ?? '—');
|
|
94
|
+
if (svc.publishedPort) kv('Host Port', c.green(`:${svc.publishedPort}`));
|
|
95
|
+
if (svc.portMappings?.length) kv('Port Mappings', c.green(svc.portMappings.map(formatPortMapping).join(', ')));
|
|
96
|
+
kv('Volume', svc.volumeMount ?? '—');
|
|
97
|
+
kv('Health', svc.healthCheck === 'running' ? 'container running (no port probe)' : svc.healthPath);
|
|
98
|
+
kv('Runtime', svc.runtimeId ?? '—');
|
|
99
|
+
kv('Commit', svc.commitSha ?? '—');
|
|
100
|
+
if (svc.autoUrl) kv('URL', c.cyan(`http://${svc.autoUrl}`));
|
|
101
|
+
kv('Created', fmtTime(svc.createdAt));
|
|
102
|
+
kv('Updated', fmtTime(svc.updatedAt));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `ninedeploy services deploy <id>` */
|
|
107
|
+
export async function servicesDeploy(client: NineDeployClient, idStr: string): Promise<void> {
|
|
108
|
+
const id = Number(idStr);
|
|
109
|
+
if (!id) return error('Usage: ninedeploy services deploy <id>');
|
|
110
|
+
try {
|
|
111
|
+
const res = await spinner('Triggering deploy', () => client.deploys.trigger(id));
|
|
112
|
+
success(`Deployment #${res.deploymentId} queued.`);
|
|
113
|
+
info(`Watch logs: ninedeploy services logs ${id}`);
|
|
114
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `ninedeploy services logs <id>` */
|
|
118
|
+
export async function servicesLogs(client: NineDeployClient, idStr: string): Promise<void> {
|
|
119
|
+
const id = Number(idStr);
|
|
120
|
+
if (!id) return error('Usage: ninedeploy services logs <id>');
|
|
121
|
+
header('Runtime Logs');
|
|
122
|
+
try {
|
|
123
|
+
const { lines } = await client.services.logs(id);
|
|
124
|
+
if (!lines.trim()) { info('No logs yet.'); return; }
|
|
125
|
+
console.log(lines.split('\n').slice(0, 50).map((l) => ` ${c.dim(l)}`).join('\n'));
|
|
126
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `ninedeploy services stop|start|restart <id>` */
|
|
130
|
+
export async function servicesLifecycle(client: NineDeployClient, action: 'stop' | 'start' | 'restart', idStr: string): Promise<void> {
|
|
131
|
+
const id = Number(idStr);
|
|
132
|
+
if (!id) return error(`Usage: ninedeploy services ${action} <id>`);
|
|
133
|
+
try {
|
|
134
|
+
await spinner(`${action}ing service`, () => client.services[action](id));
|
|
135
|
+
success(`Service ${action}ed.`);
|
|
136
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `ninedeploy services delete <id>` */
|
|
140
|
+
export async function servicesDelete(client: NineDeployClient, idStr: string): Promise<void> {
|
|
141
|
+
const id = Number(idStr);
|
|
142
|
+
if (!id) return error('Usage: ninedeploy services delete <id>');
|
|
143
|
+
const confirmText = await prompt(`Type the service ID (${id}) to confirm deletion`);
|
|
144
|
+
if (confirmText !== String(id)) return error('Cancelled.');
|
|
145
|
+
try {
|
|
146
|
+
await spinner('Deleting service', () => client.services.remove(id));
|
|
147
|
+
success('Service deleted.');
|
|
148
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Pure entry point for `ninedeploy services sticky <id> --enable|--disable` (G-28). */
|
|
152
|
+
export async function servicesSticky(
|
|
153
|
+
client: NineDeployClient,
|
|
154
|
+
idStr: string,
|
|
155
|
+
opts: { enable?: boolean; disable?: boolean } = {},
|
|
156
|
+
): Promise<{ id: number; enabled: boolean; active: boolean }> {
|
|
157
|
+
const id = Number(idStr);
|
|
158
|
+
if (!id) throw new Error('Usage: ninedeploy services sticky <id> --enable|--disable');
|
|
159
|
+
if (opts.enable === undefined && opts.disable === undefined) {
|
|
160
|
+
throw new Error('Specify either --enable or --disable');
|
|
161
|
+
}
|
|
162
|
+
if (opts.enable && opts.disable) {
|
|
163
|
+
throw new Error('Specify only one of --enable or --disable');
|
|
164
|
+
}
|
|
165
|
+
return await client.domains.setStickySession(id, Boolean(opts.enable));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Formatted wrapper for the CLI. */
|
|
169
|
+
export async function servicesStickyAction(
|
|
170
|
+
client: NineDeployClient,
|
|
171
|
+
idStr: string,
|
|
172
|
+
opts: { enable?: boolean; disable?: boolean } = {},
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
header('Sticky session');
|
|
175
|
+
let result: Awaited<ReturnType<typeof servicesSticky>>;
|
|
176
|
+
try {
|
|
177
|
+
result = await servicesSticky(client, idStr, opts);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
error(err instanceof Error ? err.message : String(err));
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
success(result.enabled
|
|
184
|
+
? `Sticky session enabled for service #${result.id} (Traefik will pick this up on the next reload)`
|
|
185
|
+
: `Sticky session disabled for service #${result.id}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Pure entry point for `ninedeploy services ports <id> [mappings...]` (0.16.1):
|
|
190
|
+
* no mappings lists them; mappings replace the list; `--clear` removes them.
|
|
191
|
+
* Each mapping is `[ip:]host[-end][:container][/tcp|udp][@allowed,ranges]`.
|
|
192
|
+
*/
|
|
193
|
+
export async function servicesPorts(
|
|
194
|
+
client: NineDeployClient,
|
|
195
|
+
idStr: string,
|
|
196
|
+
specs: string[] = [],
|
|
197
|
+
opts: { clear?: boolean } = {},
|
|
198
|
+
): Promise<{ id: number; mappings: PortMapping[]; changed: boolean }> {
|
|
199
|
+
const id = Number(idStr);
|
|
200
|
+
if (!id) throw new Error('Usage: ninedeploy services ports <id> [mapping...] [--clear]');
|
|
201
|
+
if (opts.clear && specs.length > 0) throw new Error('Give mappings or --clear, not both');
|
|
202
|
+
if (!opts.clear && specs.length === 0) {
|
|
203
|
+
const svc = await client.services.get(id);
|
|
204
|
+
return { id, mappings: svc.portMappings ?? [], changed: false };
|
|
205
|
+
}
|
|
206
|
+
const mappings: PortMapping[] = [];
|
|
207
|
+
for (const spec of specs) {
|
|
208
|
+
const parsed = parsePortMapping(spec);
|
|
209
|
+
if (typeof parsed === 'string') throw new Error(parsed);
|
|
210
|
+
mappings.push(parsed);
|
|
211
|
+
}
|
|
212
|
+
const problem = portMappingsProblem(mappings);
|
|
213
|
+
if (problem) throw new Error(problem);
|
|
214
|
+
const svc = await client.services.update(id, { portMappings: opts.clear ? null : mappings });
|
|
215
|
+
return { id, mappings: svc.portMappings ?? [], changed: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Formatted wrapper for the CLI. */
|
|
219
|
+
export async function servicesPortsAction(client: NineDeployClient, idStr: string, specs: string[] = [], opts: { clear?: boolean } = {}): Promise<void> {
|
|
220
|
+
header('Port mappings');
|
|
221
|
+
let result: Awaited<ReturnType<typeof servicesPorts>>;
|
|
222
|
+
try {
|
|
223
|
+
result = await servicesPorts(client, idStr, specs, opts);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
error(err instanceof Error ? err.message : String(err));
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (result.mappings.length === 0) info(result.changed ? `Port mappings removed from service #${result.id}.` : `Service #${result.id} publishes no extra ports.`);
|
|
230
|
+
for (const m of result.mappings) {
|
|
231
|
+
kv(formatPortMapping(m), `${m.hostIp ? `on ${m.hostIp}` : 'on every address'}${m.allowFrom?.length ? `, only from ${m.allowFrom.join(', ')}` : ''}`);
|
|
232
|
+
}
|
|
233
|
+
if (result.changed) success('Saved. Redeploy the service to apply.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** `ninedeploy services export <id>` */
|
|
237
|
+
export async function servicesExport(client: NineDeployClient, idStr: string): Promise<void> {
|
|
238
|
+
const id = Number(idStr);
|
|
239
|
+
if (!id) return error('Usage: ninedeploy services export <id>');
|
|
240
|
+
const { writeFileSync } = await import('node:fs');
|
|
241
|
+
const { authedFetch } = await import('../client.js');
|
|
242
|
+
try {
|
|
243
|
+
const svc = await client.services.get(id);
|
|
244
|
+
const data = await spinner('Exporting', () =>
|
|
245
|
+
(async () => {
|
|
246
|
+
// r550: refresh-aware and base-path preserving (was a bare fetch).
|
|
247
|
+
const res = await authedFetch(`/v1/services/${id}/export`);
|
|
248
|
+
if (!res.ok) throw new Error(`Export failed (HTTP ${res.status})`);
|
|
249
|
+
return res.text();
|
|
250
|
+
})(),
|
|
251
|
+
);
|
|
252
|
+
const filename = `${svc.slug}-export.json`;
|
|
253
|
+
writeFileSync(filename, data);
|
|
254
|
+
success(`Exported to ${filename}`);
|
|
255
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** `ninedeploy services compose <id>` */
|
|
259
|
+
export async function servicesCompose(client: NineDeployClient, idStr: string): Promise<void> {
|
|
260
|
+
const id = Number(idStr);
|
|
261
|
+
if (!id) return error('Usage: ninedeploy services compose <id>');
|
|
262
|
+
header('Runtime Docker Compose Manifest');
|
|
263
|
+
try {
|
|
264
|
+
const svc = await client.services.get(id);
|
|
265
|
+
const container = svc.runtimeId || `nd-svc-${svc.slug}-1`;
|
|
266
|
+
const res = await spinner('Generating manifest', () => client.containers.compose(container));
|
|
267
|
+
console.log(c.dim(`\n # Container: ${container}\n`));
|
|
268
|
+
console.log(res.yaml.split('\n').map((l) => ` ${c.cyan(l)}`).join('\n'));
|
|
269
|
+
console.log();
|
|
270
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** `ninedeploy services inspect <id>` */
|
|
274
|
+
export async function servicesInspect(client: NineDeployClient, idStr: string): Promise<void> {
|
|
275
|
+
const id = Number(idStr);
|
|
276
|
+
if (!id) return error('Usage: ninedeploy services inspect <id>');
|
|
277
|
+
header('Container Runtime Inspection');
|
|
278
|
+
try {
|
|
279
|
+
const svc = await client.services.get(id);
|
|
280
|
+
const container = svc.runtimeId || `nd-svc-${svc.slug}-1`;
|
|
281
|
+
const data = await spinner('Inspecting container', () => client.containers.inspect(container));
|
|
282
|
+
kv('Container ID', data.id);
|
|
283
|
+
kv('Name', data.name);
|
|
284
|
+
kv('Image', data.image);
|
|
285
|
+
kv('Status', statusColor(data.state.status));
|
|
286
|
+
kv('Running', data.state.running ? c.green('true') : c.red('false'));
|
|
287
|
+
if (data.resources.memoryLimitBytes > 0) kv('Memory Limit', `${Math.round(data.resources.memoryLimitBytes / (1024 * 1024))} MB`);
|
|
288
|
+
if (data.resources.cpuShares > 0) kv('CPU Shares', `${data.resources.cpuShares}`);
|
|
289
|
+
kv('Restart Policy', data.resources.restartPolicy);
|
|
290
|
+
|
|
291
|
+
if (Object.keys(data.traefikTags).length > 0) {
|
|
292
|
+
console.log(c.dim('\n Traefik Dynamic Ingress Tags:'));
|
|
293
|
+
for (const [k, v] of Object.entries(data.traefikTags)) {
|
|
294
|
+
console.log(` ${c.yellow(k)}: ${c.dim(v)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
console.log();
|
|
298
|
+
} catch (err) { error(err instanceof Error ? err.message : String(err)); }
|
|
299
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createClient, NineDeployError } from '@ninedeploy/sdk';
|
|
2
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
3
|
+
import { prompt, promptHidden } from '../prompts.js';
|
|
4
|
+
import {
|
|
5
|
+
isDockerAvailable,
|
|
6
|
+
isServerReachable,
|
|
7
|
+
normalizeServerUrl,
|
|
8
|
+
readLocalSetupToken,
|
|
9
|
+
startServerContainer,
|
|
10
|
+
waitForServerReady,
|
|
11
|
+
} from '../lib/serverRunner.js';
|
|
12
|
+
import { spinner, success } from '../lib/format.js';
|
|
13
|
+
|
|
14
|
+
/** `ninedeploy setup` — create the first admin user on a fresh instance. */
|
|
15
|
+
export async function setupAction(opts: { setupToken?: string } = {}): Promise<void> {
|
|
16
|
+
const existing = loadConfig();
|
|
17
|
+
const rawUrl = await prompt('Server URL', existing.baseUrl);
|
|
18
|
+
const baseUrl = normalizeServerUrl(rawUrl);
|
|
19
|
+
|
|
20
|
+
// If local host and not currently reachable, check if user wants to bootstrap it via Docker
|
|
21
|
+
const isLocal = baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');
|
|
22
|
+
if (isLocal) {
|
|
23
|
+
const reachable = await isServerReachable(baseUrl, 600);
|
|
24
|
+
if (!reachable) {
|
|
25
|
+
const dockerOk = await isDockerAvailable();
|
|
26
|
+
if (dockerOk) {
|
|
27
|
+
console.log(`\n ℹ No NineDeploy server detected at ${baseUrl}`);
|
|
28
|
+
const startDocker = (await prompt(' Start a local NineDeploy server with Docker now? (Y/n)', 'Y')).toLowerCase().startsWith('y');
|
|
29
|
+
if (startDocker) {
|
|
30
|
+
try {
|
|
31
|
+
await spinner('Starting local NineDeploy server container', async () => {
|
|
32
|
+
const url = new URL(baseUrl);
|
|
33
|
+
const port = url.port ? Number(url.port) : 3000;
|
|
34
|
+
await startServerContainer({ port });
|
|
35
|
+
const ready = await waitForServerReady(baseUrl, 30, 1000);
|
|
36
|
+
if (!ready) throw new Error('Server container did not become ready in 30s');
|
|
37
|
+
});
|
|
38
|
+
success(`Server is ready at ${baseUrl}\n`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(` ✗ Failed to start Docker container: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const email = await prompt('Admin email');
|
|
48
|
+
if (!email) {
|
|
49
|
+
console.error('Email is required.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const name = (await prompt('Display name (optional)')) || undefined;
|
|
54
|
+
const password = await promptHidden('Password (min 8 chars)');
|
|
55
|
+
if (!password || password.length < 8) {
|
|
56
|
+
console.error('Password must be at least 8 characters.');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const client = createClient({ baseUrl });
|
|
62
|
+
// 0.16: a fresh production instance gates the bootstrap on the installer's
|
|
63
|
+
// one-time setup token. Ask only when the server says it needs one (an
|
|
64
|
+
// older server has no such field and takes none).
|
|
65
|
+
let setupToken = (opts.setupToken ?? process.env['NINEDEPLOY_SETUP_TOKEN'] ?? '').trim();
|
|
66
|
+
if (!setupToken) {
|
|
67
|
+
const status = await Promise.resolve().then(() => client.auth.status()).catch(() => null);
|
|
68
|
+
if (status?.setupTokenRequired) {
|
|
69
|
+
// A panel this CLI started locally keeps its generated token in the container.
|
|
70
|
+
if (isLocal) setupToken = (await readLocalSetupToken()) ?? '';
|
|
71
|
+
if (!setupToken) setupToken = (await prompt('Setup token (printed by the installer)')).trim();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const session = await client.auth.setup(setupToken ? { email, password, name, setupToken } : { email, password, name });
|
|
76
|
+
// Both tokens: the access token alone dies with the 15-minute TTL.
|
|
77
|
+
saveConfig({ baseUrl, token: session.tokens.accessToken, refreshToken: session.tokens.refreshToken });
|
|
78
|
+
console.log(`✓ Admin account created: ${session.user.email}`);
|
|
79
|
+
console.log(' Credentials saved — you are logged in.');
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof NineDeployError) {
|
|
82
|
+
console.error(`✗ Setup failed (${err.status}): ${err.message}`);
|
|
83
|
+
if (err.status === 409) console.error(' The instance already has an admin user.');
|
|
84
|
+
if (err.status === 403) console.error(' Pass the setup token the installer printed: ninedeploy setup --setup-token <token>');
|
|
85
|
+
} else {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
console.error('✗ Setup failed:', msg);
|
|
88
|
+
if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
89
|
+
console.error(` Could not connect to NineDeploy server at ${baseUrl}. Ensure the server is running.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|