nexusapp-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nexus +2 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +159 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +594 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/domain.d.ts +3 -0
- package/dist/commands/domain.d.ts.map +1 -0
- package/dist/commands/domain.js +174 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +92 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +121 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +9 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +71 -0
- package/dist/output.js.map +1 -0
- package/package.json +29 -0
- package/src/client.ts +68 -0
- package/src/commands/auth.ts +166 -0
- package/src/commands/deploy.ts +534 -0
- package/src/commands/domain.ts +167 -0
- package/src/commands/project.ts +81 -0
- package/src/commands/secret.ts +117 -0
- package/src/config.ts +56 -0
- package/src/index.ts +25 -0
- package/src/output.ts +65 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { client, apiError, unwrap } from '../client.js';
|
|
4
|
+
import { statusBadge, printTable, printJson, spinner, timeAgo, success, errorMsg } from '../output.js';
|
|
5
|
+
|
|
6
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
|
|
8
|
+
/** Resolve a deployment name or UUID to its full record. */
|
|
9
|
+
async function resolveDeployment(nameOrId: string): Promise<any> {
|
|
10
|
+
if (UUID_RE.test(nameOrId)) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await client.get(`/api/deployments/${nameOrId}`);
|
|
13
|
+
return unwrap(res.data);
|
|
14
|
+
} catch { /* fall through to name search */ }
|
|
15
|
+
}
|
|
16
|
+
// Search by name across all deployments
|
|
17
|
+
const listRes = await client.get('/api/deployments');
|
|
18
|
+
const all: any[] = Array.isArray(unwrap(listRes.data)) ? unwrap(listRes.data) : [];
|
|
19
|
+
const match = all.find((d) => d.name === nameOrId || d.displayName === nameOrId);
|
|
20
|
+
if (!match) throw new Error(`Deployment not found: "${nameOrId}"`);
|
|
21
|
+
// Fetch full record for the resolved ID
|
|
22
|
+
const res = await client.get(`/api/deployments/${match.id}`);
|
|
23
|
+
return unwrap(res.data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function pollUntilDone(deploymentId: string, spin: ReturnType<typeof spinner>): Promise<void> {
|
|
27
|
+
const terminal = new Set(['RUNNING', 'FAILED', 'TERMINATED', 'STOPPED']);
|
|
28
|
+
while (true) {
|
|
29
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
30
|
+
try {
|
|
31
|
+
const res = await client.get(`/api/deployments/${deploymentId}`);
|
|
32
|
+
const d = unwrap(res.data);
|
|
33
|
+
const status = (d.status || '').toUpperCase();
|
|
34
|
+
spin.text = `Deploying ${d.name || deploymentId}... [${status}]`;
|
|
35
|
+
if (terminal.has(status)) {
|
|
36
|
+
if (status === 'RUNNING') {
|
|
37
|
+
spin.succeed(`Deployed: ${d.name} → ${d.url || d.serviceUrl || '—'}`);
|
|
38
|
+
} else {
|
|
39
|
+
spin.fail(`Deployment ended with status: ${status}`);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
spin.fail('Failed to poll status: ' + apiError(err));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerDeploy(program: Command): void {
|
|
51
|
+
const deploy = program.command('deploy').description('Deployment commands');
|
|
52
|
+
|
|
53
|
+
// list
|
|
54
|
+
deploy
|
|
55
|
+
.command('list')
|
|
56
|
+
.description('List deployments')
|
|
57
|
+
.option('--project <id>', 'Filter by project ID')
|
|
58
|
+
.option('--status <status>', 'Filter by status')
|
|
59
|
+
.option('--json', 'Output raw JSON')
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
try {
|
|
62
|
+
const url = opts.project ? `/api/deployments/project/${opts.project}` : '/api/deployments';
|
|
63
|
+
const params: Record<string, string> = {};
|
|
64
|
+
if (opts.status) params.status = opts.status;
|
|
65
|
+
|
|
66
|
+
const res = await client.get(url, { params });
|
|
67
|
+
const raw = unwrap(res.data);
|
|
68
|
+
const deployments = Array.isArray(raw) ? raw : raw.deployments || [];
|
|
69
|
+
|
|
70
|
+
if (opts.json) { printJson(deployments); return; }
|
|
71
|
+
if (!deployments.length) { console.log('No deployments found.'); return; }
|
|
72
|
+
|
|
73
|
+
printTable(
|
|
74
|
+
['NAME', 'ID', 'STATUS', 'PROVIDER', 'URL', 'CREATED'],
|
|
75
|
+
deployments.map((d: any) => [
|
|
76
|
+
d.displayName || d.name,
|
|
77
|
+
d.id,
|
|
78
|
+
statusBadge(d.status),
|
|
79
|
+
d.provider || '—',
|
|
80
|
+
d.url || d.serviceUrl || '—',
|
|
81
|
+
d.createdAt ? timeAgo(d.createdAt) : '—',
|
|
82
|
+
])
|
|
83
|
+
);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
errorMsg(apiError(err));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// get
|
|
91
|
+
deploy
|
|
92
|
+
.command('get <name-or-id>')
|
|
93
|
+
.description('Get deployment details')
|
|
94
|
+
.option('--json', 'Output raw JSON')
|
|
95
|
+
.action(async (nameOrId, opts) => {
|
|
96
|
+
try {
|
|
97
|
+
const d = await resolveDeployment(nameOrId);
|
|
98
|
+
if (opts.json) { printJson(d); return; }
|
|
99
|
+
printTable(['Field', 'Value'], [
|
|
100
|
+
['ID', d.id],
|
|
101
|
+
['Name', d.displayName || d.name],
|
|
102
|
+
['Status', statusBadge(d.status)],
|
|
103
|
+
['Provider', d.provider],
|
|
104
|
+
['Image', d.imageName || '—'],
|
|
105
|
+
['Port', String(d.port || '—')],
|
|
106
|
+
['URL', d.url || d.serviceUrl || '—'],
|
|
107
|
+
['Replicas', String(d.replicas ?? '—')],
|
|
108
|
+
['Project', d.projectId || '—'],
|
|
109
|
+
['Created', d.createdAt ? timeAgo(d.createdAt) : '—'],
|
|
110
|
+
['Updated', d.updatedAt ? timeAgo(d.updatedAt) : '—'],
|
|
111
|
+
]);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
errorMsg(apiError(err));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// create (image-based)
|
|
119
|
+
deploy
|
|
120
|
+
.command('create')
|
|
121
|
+
.description('Create a deployment from a container image')
|
|
122
|
+
.requiredOption('--image <image>', 'Container image (e.g. nginx:latest)')
|
|
123
|
+
.requiredOption('--port <port>', 'Container port', parseInt)
|
|
124
|
+
.option('--name <name>', 'Deployment name')
|
|
125
|
+
.option('--project <id>', 'Project ID')
|
|
126
|
+
.option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
|
|
127
|
+
.option('--env <pairs...>', 'Environment variables as KEY=VALUE')
|
|
128
|
+
.option('--wait', 'Wait until deployment is RUNNING or FAILED')
|
|
129
|
+
.option('--json', 'Output raw JSON')
|
|
130
|
+
.action(async (opts) => {
|
|
131
|
+
const envVars: Record<string, string> = {};
|
|
132
|
+
if (opts.env) {
|
|
133
|
+
for (const pair of opts.env) {
|
|
134
|
+
const idx = pair.indexOf('=');
|
|
135
|
+
if (idx > 0) envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const payload: Record<string, any> = { image: opts.image, port: opts.port };
|
|
139
|
+
if (opts.name) payload.name = opts.name;
|
|
140
|
+
if (opts.project) payload.projectId = opts.project;
|
|
141
|
+
if (opts.provider) payload.provider = opts.provider;
|
|
142
|
+
if (Object.keys(envVars).length) payload.envVars = envVars;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const res = await client.post('/api/gpt/deploy', payload);
|
|
146
|
+
const d = res.data;
|
|
147
|
+
if (opts.json) { printJson(d); return; }
|
|
148
|
+
if (opts.wait) {
|
|
149
|
+
const spin = spinner(`Deploying ${d.name || opts.name || opts.image}...`);
|
|
150
|
+
await pollUntilDone(d.id, spin);
|
|
151
|
+
} else {
|
|
152
|
+
success(`Deployment queued: ${d.name || d.id}`);
|
|
153
|
+
console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
errorMsg(apiError(err));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// source (repo-based)
|
|
162
|
+
deploy
|
|
163
|
+
.command('source')
|
|
164
|
+
.description('Deploy from a Git repository')
|
|
165
|
+
.requiredOption('--repo <url>', 'Git repository URL')
|
|
166
|
+
.option('--name <name>', 'Deployment name')
|
|
167
|
+
.option('--branch <branch>', 'Git branch')
|
|
168
|
+
.option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
|
|
169
|
+
.option('--env <pairs...>', 'Environment variables as KEY=VALUE')
|
|
170
|
+
.option('--framework <framework>', 'Framework hint (e.g. node, python, go)')
|
|
171
|
+
.option('--build-command <cmd>', 'Custom build command')
|
|
172
|
+
.option('--start-command <cmd>', 'Custom start command')
|
|
173
|
+
.option('--install-command <cmd>', 'Custom install command')
|
|
174
|
+
.option('--output-dir <dir>', 'Build output directory')
|
|
175
|
+
.option('--dockerfile <path>', 'Path to Dockerfile in repo')
|
|
176
|
+
.option('--repo-secret <name>', 'Secret name containing private repo token')
|
|
177
|
+
.option('--environment <env>', 'Deployment environment (DEVELOPMENT|STAGING|PRODUCTION)', 'DEVELOPMENT')
|
|
178
|
+
.option('--auto-destroy <hours>', 'Auto-destroy after N hours', parseInt)
|
|
179
|
+
.option('--wait', 'Wait until deployment is RUNNING or FAILED')
|
|
180
|
+
.option('--json', 'Output raw JSON')
|
|
181
|
+
.action(async (opts) => {
|
|
182
|
+
const envVars: Record<string, string> = {};
|
|
183
|
+
if (opts.env) {
|
|
184
|
+
for (const pair of opts.env) {
|
|
185
|
+
const idx = pair.indexOf('=');
|
|
186
|
+
if (idx > 0) envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const payload: Record<string, any> = { sourceType: 'repo', repoUrl: opts.repo };
|
|
190
|
+
if (opts.name) payload.name = opts.name;
|
|
191
|
+
if (opts.branch) payload.repoBranch = opts.branch;
|
|
192
|
+
if (opts.provider) payload.provider = opts.provider;
|
|
193
|
+
if (opts.environment) payload.environment = opts.environment;
|
|
194
|
+
if (opts.framework) payload.framework = opts.framework;
|
|
195
|
+
if (opts.buildCommand) payload.buildCommand = opts.buildCommand;
|
|
196
|
+
if (opts.startCommand) payload.startCommand = opts.startCommand;
|
|
197
|
+
if (opts.installCommand) payload.installCommand = opts.installCommand;
|
|
198
|
+
if (opts.outputDir) payload.outputDir = opts.outputDir;
|
|
199
|
+
if (opts.dockerfile) payload.dockerfile = opts.dockerfile;
|
|
200
|
+
if (opts.repoSecret) payload.repoSecretName = opts.repoSecret;
|
|
201
|
+
if (opts.autoDestroy) payload.autoDestroyHours = opts.autoDestroy;
|
|
202
|
+
if (Object.keys(envVars).length) payload.envVars = envVars;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const res = await client.post('/api/gpt/deploy/source', payload);
|
|
206
|
+
const d = res.data;
|
|
207
|
+
if (opts.json) { printJson(d); return; }
|
|
208
|
+
if (opts.wait) {
|
|
209
|
+
const spin = spinner(`Building and deploying ${d.name || opts.name || opts.repo}...`);
|
|
210
|
+
await pollUntilDone(d.id, spin);
|
|
211
|
+
} else {
|
|
212
|
+
success(`Source deployment queued: ${d.name || d.id}`);
|
|
213
|
+
console.log(` ID: ${d.id}`);
|
|
214
|
+
console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
errorMsg(apiError(err));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// redeploy
|
|
223
|
+
deploy
|
|
224
|
+
.command('redeploy <name-or-id>')
|
|
225
|
+
.description('Redeploy an existing deployment with the same config')
|
|
226
|
+
.option('--name <name>', 'Override deployment name')
|
|
227
|
+
.option('--provider <provider>', 'Override provider')
|
|
228
|
+
.option('--env <pairs...>', 'Override / add environment variables as KEY=VALUE')
|
|
229
|
+
.option('--wait', 'Wait until deployment is RUNNING or FAILED')
|
|
230
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
231
|
+
.option('--json', 'Output raw JSON')
|
|
232
|
+
.action(async (nameOrId, opts) => {
|
|
233
|
+
let deployment: any;
|
|
234
|
+
try {
|
|
235
|
+
deployment = await resolveDeployment(nameOrId);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
errorMsg(apiError(err));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!opts.yes) {
|
|
242
|
+
const { confirm } = await inquirer.prompt([
|
|
243
|
+
{ type: 'confirm', name: 'confirm', message: `Redeploy "${deployment.displayName || deployment.name}"?`, default: true },
|
|
244
|
+
]);
|
|
245
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const baseEnvVars: Record<string, string> = { ...(deployment.envVars || {}) };
|
|
249
|
+
if (opts.env) {
|
|
250
|
+
for (const pair of (opts.env as string[])) {
|
|
251
|
+
const idx = pair.indexOf('=');
|
|
252
|
+
if (idx > 0) baseEnvVars[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const provider = opts.provider || undefined;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
if (deployment.imageName) {
|
|
259
|
+
const payload: Record<string, any> = {
|
|
260
|
+
image: deployment.imageName,
|
|
261
|
+
port: deployment.port,
|
|
262
|
+
name: opts.name || `${deployment.name}-redeploy`,
|
|
263
|
+
};
|
|
264
|
+
if (provider) payload.provider = provider;
|
|
265
|
+
if (Object.keys(baseEnvVars).length) payload.envVars = baseEnvVars;
|
|
266
|
+
|
|
267
|
+
const res = await client.post('/api/gpt/deploy', payload);
|
|
268
|
+
const d = res.data;
|
|
269
|
+
if (opts.json) { printJson(d); return; }
|
|
270
|
+
if (opts.wait) {
|
|
271
|
+
await pollUntilDone(d.id, spinner(`Redeploying ${d.name || nameOrId}...`));
|
|
272
|
+
} else {
|
|
273
|
+
success(`Redeploy queued: ${d.name || d.id}`);
|
|
274
|
+
console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Source deployment — look up repo from project
|
|
280
|
+
const projectRes = await client.get(`/api/projects/${deployment.projectId}`);
|
|
281
|
+
const project = unwrap(projectRes.data);
|
|
282
|
+
|
|
283
|
+
if (project.repoUrl) {
|
|
284
|
+
const payload: Record<string, any> = {
|
|
285
|
+
sourceType: 'repo',
|
|
286
|
+
repoUrl: project.repoUrl,
|
|
287
|
+
name: opts.name || `${deployment.name}-redeploy`,
|
|
288
|
+
};
|
|
289
|
+
if (project.gitBranch) payload.repoBranch = project.gitBranch;
|
|
290
|
+
if (project.framework) payload.framework = project.framework;
|
|
291
|
+
if (provider) payload.provider = provider;
|
|
292
|
+
if (Object.keys(baseEnvVars).length) payload.envVars = baseEnvVars;
|
|
293
|
+
|
|
294
|
+
const res = await client.post('/api/gpt/deploy/source', payload);
|
|
295
|
+
const d = res.data;
|
|
296
|
+
if (opts.json) { printJson(d); return; }
|
|
297
|
+
if (opts.wait) {
|
|
298
|
+
await pollUntilDone(d.id, spinner(`Rebuilding ${d.name || nameOrId}...`));
|
|
299
|
+
} else {
|
|
300
|
+
success(`Redeploy queued: ${d.name || d.id}`);
|
|
301
|
+
console.log(` Repo: ${project.repoUrl}${project.gitBranch ? ` @ ${project.gitBranch}` : ''}`);
|
|
302
|
+
console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
errorMsg('Cannot redeploy: no image or repo URL found. Use "nexus deploy source --repo <url>" instead.');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
errorMsg(apiError(err));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// stop
|
|
316
|
+
deploy
|
|
317
|
+
.command('stop <name-or-id>')
|
|
318
|
+
.description('Stop a running deployment')
|
|
319
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
320
|
+
.action(async (nameOrId, opts) => {
|
|
321
|
+
try {
|
|
322
|
+
const d = await resolveDeployment(nameOrId);
|
|
323
|
+
if (!opts.yes) {
|
|
324
|
+
const { confirm } = await inquirer.prompt([
|
|
325
|
+
{ type: 'confirm', name: 'confirm', message: `Stop "${d.displayName || d.name}"?`, default: false },
|
|
326
|
+
]);
|
|
327
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
328
|
+
}
|
|
329
|
+
await client.post(`/api/deployments/${d.id}/stop`);
|
|
330
|
+
success(`Deployment "${d.displayName || d.name}" stopped.`);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
errorMsg(apiError(err));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// start
|
|
338
|
+
deploy
|
|
339
|
+
.command('start <name-or-id>')
|
|
340
|
+
.description('Start a stopped deployment')
|
|
341
|
+
.action(async (nameOrId) => {
|
|
342
|
+
try {
|
|
343
|
+
const d = await resolveDeployment(nameOrId);
|
|
344
|
+
await client.post(`/api/deployments/${d.id}/start`);
|
|
345
|
+
success(`Deployment "${d.displayName || d.name}" started.`);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
errorMsg(apiError(err));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// delete
|
|
353
|
+
deploy
|
|
354
|
+
.command('delete <name-or-id>')
|
|
355
|
+
.description('Delete a deployment')
|
|
356
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
357
|
+
.action(async (nameOrId, opts) => {
|
|
358
|
+
try {
|
|
359
|
+
const d = await resolveDeployment(nameOrId);
|
|
360
|
+
if (!opts.yes) {
|
|
361
|
+
const { confirm } = await inquirer.prompt([
|
|
362
|
+
{ type: 'confirm', name: 'confirm', message: `Delete "${d.displayName || d.name}"? This cannot be undone.`, default: false },
|
|
363
|
+
]);
|
|
364
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
365
|
+
}
|
|
366
|
+
await client.delete(`/api/deployments/${d.id}`);
|
|
367
|
+
success(`Deployment "${d.displayName || d.name}" deleted.`);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
errorMsg(apiError(err));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// logs
|
|
375
|
+
deploy
|
|
376
|
+
.command('logs <name-or-id>')
|
|
377
|
+
.description('View deployment logs')
|
|
378
|
+
.option('--type <type>', 'Log type: runtime or build', 'runtime')
|
|
379
|
+
.option('--lines <n>', 'Number of log lines', '100')
|
|
380
|
+
.option('--follow', 'Poll for new logs every 2s')
|
|
381
|
+
.action(async (nameOrId, opts) => {
|
|
382
|
+
let deployId: string;
|
|
383
|
+
try {
|
|
384
|
+
const d = await resolveDeployment(nameOrId);
|
|
385
|
+
deployId = d.id;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
errorMsg(apiError(err));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const limit = parseInt(opts.lines, 10) || 100;
|
|
393
|
+
|
|
394
|
+
/** Normalise the API response into an array of { message, timestamp? } */
|
|
395
|
+
const fetchLogs = async (lastTimestamp?: string): Promise<{ message: string; timestamp?: string }[]> => {
|
|
396
|
+
const params: Record<string, any> = { type: opts.type, limit };
|
|
397
|
+
if (lastTimestamp) params.after = lastTimestamp;
|
|
398
|
+
const res = await client.get(`/api/deployments/${deployId}/logs`, { params });
|
|
399
|
+
const raw = unwrap(res.data);
|
|
400
|
+
|
|
401
|
+
// Shape: { logs: "line1\nline2\n..." }
|
|
402
|
+
if (raw && typeof raw.logs === 'string') {
|
|
403
|
+
return raw.logs
|
|
404
|
+
.split('\n')
|
|
405
|
+
.filter((l: string) => l.length > 0)
|
|
406
|
+
.map((l: string) => ({ message: l }));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Shape: [ { message, timestamp }, ... ] or [ "line1", ... ]
|
|
410
|
+
const arr: any[] = Array.isArray(raw) ? raw : Array.isArray(raw?.logs) ? raw.logs : [];
|
|
411
|
+
return arr.map((entry) =>
|
|
412
|
+
typeof entry === 'string'
|
|
413
|
+
? { message: entry }
|
|
414
|
+
: { message: entry.message || entry.log || String(entry), timestamp: entry.timestamp }
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const logs = await fetchLogs();
|
|
420
|
+
let lastTimestamp: string | undefined;
|
|
421
|
+
for (const log of logs) {
|
|
422
|
+
const ts = log.timestamp ? `[${new Date(log.timestamp).toLocaleTimeString()}] ` : '';
|
|
423
|
+
console.log(`${ts}${log.message}`);
|
|
424
|
+
lastTimestamp = log.timestamp || lastTimestamp;
|
|
425
|
+
}
|
|
426
|
+
if (!opts.follow) return;
|
|
427
|
+
while (true) {
|
|
428
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
429
|
+
try {
|
|
430
|
+
const newLogs = await fetchLogs(lastTimestamp);
|
|
431
|
+
for (const log of newLogs) {
|
|
432
|
+
const ts = log.timestamp ? `[${new Date(log.timestamp).toLocaleTimeString()}] ` : '';
|
|
433
|
+
console.log(`${ts}${log.message}`);
|
|
434
|
+
lastTimestamp = log.timestamp || lastTimestamp;
|
|
435
|
+
}
|
|
436
|
+
} catch { /* ignore transient errors in follow mode */ }
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
errorMsg(apiError(err));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// scale
|
|
445
|
+
deploy
|
|
446
|
+
.command('scale <name-or-id> <replicas>')
|
|
447
|
+
.description('Scale deployment replicas')
|
|
448
|
+
.action(async (nameOrId, replicas) => {
|
|
449
|
+
const count = parseInt(replicas, 10);
|
|
450
|
+
if (isNaN(count) || count < 1 || count > 10) {
|
|
451
|
+
errorMsg('Replicas must be a number between 1 and 10.');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const d = await resolveDeployment(nameOrId);
|
|
456
|
+
await client.post(`/api/deployments/${d.id}/scale`, { replicas: count });
|
|
457
|
+
success(`Deployment "${d.displayName || d.name}" scaled to ${count} replica(s).`);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
errorMsg(apiError(err));
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// rollback
|
|
465
|
+
deploy
|
|
466
|
+
.command('rollback <name-or-id>')
|
|
467
|
+
.description('Roll back a deployment to the previous version')
|
|
468
|
+
.option('--target <deployment-id>', 'Target deployment ID to roll back to')
|
|
469
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
470
|
+
.action(async (nameOrId, opts) => {
|
|
471
|
+
try {
|
|
472
|
+
const d = await resolveDeployment(nameOrId);
|
|
473
|
+
if (!opts.yes) {
|
|
474
|
+
const { confirm } = await inquirer.prompt([
|
|
475
|
+
{ type: 'confirm', name: 'confirm', message: `Roll back "${d.displayName || d.name}" to the previous version?`, default: false },
|
|
476
|
+
]);
|
|
477
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
478
|
+
}
|
|
479
|
+
const payload: Record<string, any> = {};
|
|
480
|
+
if (opts.target) payload.targetDeploymentId = opts.target;
|
|
481
|
+
const res = await client.post(`/api/deployments/${d.id}/rollback`, payload);
|
|
482
|
+
const result = unwrap(res.data);
|
|
483
|
+
success(`Rollback initiated → new deployment ${result.id || '?'}`);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
errorMsg(apiError(err));
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// status
|
|
491
|
+
deploy
|
|
492
|
+
.command('status <name-or-id>')
|
|
493
|
+
.description('Show deployment status')
|
|
494
|
+
.option('--watch', 'Refresh every 3s')
|
|
495
|
+
.option('--json', 'Output raw JSON')
|
|
496
|
+
.action(async (nameOrId, opts) => {
|
|
497
|
+
let deployId: string;
|
|
498
|
+
try {
|
|
499
|
+
const d = await resolveDeployment(nameOrId);
|
|
500
|
+
deployId = d.id;
|
|
501
|
+
} catch (err) {
|
|
502
|
+
errorMsg(apiError(err));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const show = async () => {
|
|
508
|
+
const res = await client.get(`/api/deployments/${deployId}`);
|
|
509
|
+
const d = unwrap(res.data);
|
|
510
|
+
if (opts.json) { printJson(d); return; }
|
|
511
|
+
if (opts.watch) process.stdout.write('\x1Bc');
|
|
512
|
+
printTable(['Field', 'Value'], [
|
|
513
|
+
['Name', d.displayName || d.name],
|
|
514
|
+
['Status', statusBadge(d.status)],
|
|
515
|
+
['Provider', d.provider || '—'],
|
|
516
|
+
['URL', d.url || d.serviceUrl || '—'],
|
|
517
|
+
['Replicas', String(d.replicas ?? '—')],
|
|
518
|
+
['Updated', d.updatedAt ? timeAgo(d.updatedAt) : '—'],
|
|
519
|
+
]);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
await show();
|
|
524
|
+
if (!opts.watch) return;
|
|
525
|
+
while (true) {
|
|
526
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
527
|
+
try { await show(); } catch { /* ignore */ }
|
|
528
|
+
}
|
|
529
|
+
} catch (err) {
|
|
530
|
+
errorMsg(apiError(err));
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|