nexusapp-cli 3.0.0 → 3.1.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.
Files changed (64) hide show
  1. package/dist/client.d.ts +6 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +63 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/commands/auth.d.ts +3 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +178 -0
  8. package/dist/commands/auth.js.map +1 -0
  9. package/dist/commands/bucket.d.ts +3 -0
  10. package/dist/commands/bucket.d.ts.map +1 -0
  11. package/dist/commands/bucket.js +354 -0
  12. package/dist/commands/bucket.js.map +1 -0
  13. package/dist/commands/database.d.ts +3 -0
  14. package/dist/commands/database.d.ts.map +1 -0
  15. package/dist/commands/database.js +350 -0
  16. package/dist/commands/database.js.map +1 -0
  17. package/dist/commands/deploy.d.ts +3 -0
  18. package/dist/commands/deploy.d.ts.map +1 -0
  19. package/dist/commands/deploy.js +1009 -0
  20. package/dist/commands/deploy.js.map +1 -0
  21. package/dist/commands/domain.d.ts +3 -0
  22. package/dist/commands/domain.d.ts.map +1 -0
  23. package/dist/commands/domain.js +174 -0
  24. package/dist/commands/domain.js.map +1 -0
  25. package/dist/commands/exec.d.ts +3 -0
  26. package/dist/commands/exec.d.ts.map +1 -0
  27. package/dist/commands/exec.js +176 -0
  28. package/dist/commands/exec.js.map +1 -0
  29. package/dist/commands/managedDb.d.ts +3 -0
  30. package/dist/commands/managedDb.d.ts.map +1 -0
  31. package/dist/commands/managedDb.js +227 -0
  32. package/dist/commands/managedDb.js.map +1 -0
  33. package/dist/commands/member.d.ts +3 -0
  34. package/dist/commands/member.d.ts.map +1 -0
  35. package/dist/commands/member.js +175 -0
  36. package/dist/commands/member.js.map +1 -0
  37. package/dist/commands/project.d.ts +3 -0
  38. package/dist/commands/project.d.ts.map +1 -0
  39. package/dist/commands/project.js +92 -0
  40. package/dist/commands/project.js.map +1 -0
  41. package/dist/commands/secret.d.ts +3 -0
  42. package/dist/commands/secret.d.ts.map +1 -0
  43. package/dist/commands/secret.js +121 -0
  44. package/dist/commands/secret.js.map +1 -0
  45. package/dist/commands/token.d.ts +3 -0
  46. package/dist/commands/token.d.ts.map +1 -0
  47. package/dist/commands/token.js +179 -0
  48. package/dist/commands/token.js.map +1 -0
  49. package/dist/commands/volume.d.ts +3 -0
  50. package/dist/commands/volume.d.ts.map +1 -0
  51. package/dist/commands/volume.js +149 -0
  52. package/dist/commands/volume.js.map +1 -0
  53. package/dist/config.d.ts +10 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +53 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/output.d.ts +9 -0
  61. package/dist/output.d.ts.map +1 -0
  62. package/dist/output.js +71 -0
  63. package/dist/output.js.map +1 -0
  64. package/package.json +1 -1
@@ -0,0 +1,1009 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerDeploy = registerDeploy;
7
+ const crypto_1 = require("crypto");
8
+ const fs_1 = require("fs");
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const client_js_1 = require("../client.js");
11
+ const output_js_1 = require("../output.js");
12
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
+ /** Resolve a deployment name or UUID to its full record. */
14
+ async function resolveDeployment(nameOrId) {
15
+ if (UUID_RE.test(nameOrId)) {
16
+ try {
17
+ const res = await client_js_1.client.get(`/api/deployments/${nameOrId}`);
18
+ return (0, client_js_1.unwrap)(res.data);
19
+ }
20
+ catch { /* fall through to name search */ }
21
+ }
22
+ // Search by name across all deployments
23
+ const listRes = await client_js_1.client.get('/api/deployments');
24
+ const all = Array.isArray((0, client_js_1.unwrap)(listRes.data)) ? (0, client_js_1.unwrap)(listRes.data) : [];
25
+ const match = all.find((d) => d.name === nameOrId || d.displayName === nameOrId);
26
+ if (!match)
27
+ throw new Error(`Deployment not found: "${nameOrId}"`);
28
+ // Fetch full record for the resolved ID
29
+ const res = await client_js_1.client.get(`/api/deployments/${match.id}`);
30
+ return (0, client_js_1.unwrap)(res.data);
31
+ }
32
+ function isInternalDeploymentImage(image) {
33
+ if (!image)
34
+ return false;
35
+ return /^deploy-[0-9a-f-]+(?::|$)/i.test(image.trim());
36
+ }
37
+ function parseDurationToDate(value) {
38
+ const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d)$/);
39
+ if (!match) {
40
+ throw new Error(`Invalid duration "${value}". Use format like: 30m, 4h, 2d`);
41
+ }
42
+ const amount = Number(match[1]);
43
+ const unit = match[2];
44
+ const msMap = { m: 60000, h: 3600000, d: 86400000 };
45
+ return new Date(Date.now() + amount * msMap[unit]);
46
+ }
47
+ function formatAutoDestroy(value) {
48
+ if (!value)
49
+ return 'disabled';
50
+ const date = new Date(value);
51
+ if (Number.isNaN(date.getTime()))
52
+ return String(value);
53
+ return `${date.toISOString()} (${(0, output_js_1.timeAgo)(value)})`;
54
+ }
55
+ async function pollUntilDone(deploymentId, spin) {
56
+ const terminal = new Set(['RUNNING', 'FAILED', 'TERMINATED', 'STOPPED']);
57
+ while (true) {
58
+ await new Promise((r) => setTimeout(r, 3000));
59
+ try {
60
+ const res = await client_js_1.client.get(`/api/deployments/${deploymentId}`);
61
+ const d = (0, client_js_1.unwrap)(res.data);
62
+ const status = (d.status || '').toUpperCase();
63
+ spin.text = `Deploying ${d.name || deploymentId}... [${status}]`;
64
+ if (terminal.has(status)) {
65
+ if (status === 'RUNNING') {
66
+ spin.succeed(`Deployed: ${d.name} → ${d.url || d.serviceUrl || '—'}`);
67
+ }
68
+ else {
69
+ spin.fail(`Deployment ended with status: ${status}`);
70
+ }
71
+ return;
72
+ }
73
+ }
74
+ catch (err) {
75
+ spin.fail('Failed to poll status: ' + (0, client_js_1.apiError)(err));
76
+ return;
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Parse a .env-style file into a key→value map.
82
+ * Supports: KEY=VALUE, KEY="quoted value", KEY='quoted value', # comments, blank lines.
83
+ * --env pairs always win over file values (caller merges file first, then pairs).
84
+ */
85
+ function parseEnvFile(filePath) {
86
+ let raw;
87
+ try {
88
+ raw = (0, fs_1.readFileSync)(filePath, 'utf8');
89
+ }
90
+ catch {
91
+ (0, output_js_1.errorMsg)(`Cannot read env file: ${filePath}`);
92
+ process.exit(1);
93
+ }
94
+ const result = {};
95
+ for (const line of raw.split('\n')) {
96
+ const trimmed = line.trim();
97
+ if (!trimmed || trimmed.startsWith('#'))
98
+ continue;
99
+ const idx = trimmed.indexOf('=');
100
+ if (idx <= 0)
101
+ continue;
102
+ const key = trimmed.slice(0, idx).trim();
103
+ let val = trimmed.slice(idx + 1);
104
+ // Strip inline comments after unquoted values
105
+ if ((val.startsWith('"') && val.includes('"', 1)) || (val.startsWith("'") && val.includes("'", 1))) {
106
+ const q = val[0];
107
+ const close = val.indexOf(q, 1);
108
+ val = val.slice(1, close);
109
+ }
110
+ else {
111
+ val = val.split('#')[0].trim();
112
+ }
113
+ if (key)
114
+ result[key] = val;
115
+ }
116
+ return result;
117
+ }
118
+ function registerDeploy(program) {
119
+ const deploy = program.command('deploy').description('Deployment commands');
120
+ // list
121
+ deploy
122
+ .command('list')
123
+ .description('List deployments')
124
+ .option('--project <id>', 'Filter by project ID')
125
+ .option('--status <status>', 'Filter by status')
126
+ .option('--json', 'Output raw JSON')
127
+ .action(async (opts) => {
128
+ try {
129
+ const url = opts.project ? `/api/deployments/project/${opts.project}` : '/api/deployments';
130
+ const params = {};
131
+ if (opts.status)
132
+ params.status = opts.status;
133
+ const res = await client_js_1.client.get(url, { params });
134
+ const raw = (0, client_js_1.unwrap)(res.data);
135
+ const deployments = Array.isArray(raw) ? raw : raw.deployments || [];
136
+ if (opts.json) {
137
+ (0, output_js_1.printJson)(deployments);
138
+ return;
139
+ }
140
+ if (!deployments.length) {
141
+ console.log('No deployments found.');
142
+ return;
143
+ }
144
+ (0, output_js_1.printTable)(['NAME', 'ID', 'STATUS', 'PROVIDER', 'URL', 'CREATED'], deployments.map((d) => [
145
+ d.displayName || d.name,
146
+ d.id,
147
+ (0, output_js_1.statusBadge)(d.status),
148
+ d.provider || '—',
149
+ d.url || d.serviceUrl || '—',
150
+ d.createdAt ? (0, output_js_1.timeAgo)(d.createdAt) : '—',
151
+ ]));
152
+ }
153
+ catch (err) {
154
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
155
+ process.exit(1);
156
+ }
157
+ });
158
+ // get
159
+ deploy
160
+ .command('get <name-or-id>')
161
+ .description('Get deployment details')
162
+ .option('--json', 'Output raw JSON')
163
+ .action(async (nameOrId, opts) => {
164
+ try {
165
+ const d = await resolveDeployment(nameOrId);
166
+ if (opts.json) {
167
+ (0, output_js_1.printJson)(d);
168
+ return;
169
+ }
170
+ (0, output_js_1.printTable)(['Field', 'Value'], [
171
+ ['ID', d.id],
172
+ ['Name', d.displayName || d.name],
173
+ ['Status', (0, output_js_1.statusBadge)(d.status)],
174
+ ['Provider', d.provider],
175
+ ['Image', d.imageName || '—'],
176
+ ['Port', String(d.port || '—')],
177
+ ['URL', d.url || d.serviceUrl || '—'],
178
+ ['Replicas', String(d.replicas ?? '—')],
179
+ ['Auto Destroy', formatAutoDestroy(d.autoDestroyAt)],
180
+ ['Project', d.projectId || '—'],
181
+ ['Created', d.createdAt ? (0, output_js_1.timeAgo)(d.createdAt) : '—'],
182
+ ['Updated', d.updatedAt ? (0, output_js_1.timeAgo)(d.updatedAt) : '—'],
183
+ ]);
184
+ }
185
+ catch (err) {
186
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
187
+ process.exit(1);
188
+ }
189
+ });
190
+ // create (image-based)
191
+ deploy
192
+ .command('create')
193
+ .description('Create a deployment from a container image')
194
+ .requiredOption('--image <image>', 'Container image (e.g. nginx:latest)')
195
+ .requiredOption('--port <port>', 'Container port', parseInt)
196
+ .option('--name <name>', 'Deployment name')
197
+ .option('--project <id>', 'Project ID')
198
+ .option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
199
+ .option('--env <pairs...>', 'Environment variables as KEY=VALUE')
200
+ .option('--env-file <file>', 'Load environment variables from a .env file')
201
+ .option('--no-health-check', 'Disable health checks for this deployment')
202
+ .option('--wait', 'Wait until deployment is RUNNING or FAILED')
203
+ .option('--json', 'Output raw JSON')
204
+ .action(async (opts) => {
205
+ const envVars = {};
206
+ if (opts.envFile)
207
+ Object.assign(envVars, parseEnvFile(opts.envFile));
208
+ if (opts.env) {
209
+ for (const pair of opts.env) {
210
+ const idx = pair.indexOf('=');
211
+ if (idx > 0)
212
+ envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
213
+ }
214
+ }
215
+ const payload = { image: opts.image, port: opts.port };
216
+ if (opts.name)
217
+ payload.name = opts.name;
218
+ if (opts.project)
219
+ payload.projectId = opts.project;
220
+ if (opts.provider)
221
+ payload.provider = opts.provider;
222
+ if (opts.healthCheck === false)
223
+ payload.healthCheckEnabled = false;
224
+ if (Object.keys(envVars).length)
225
+ payload.envVars = envVars;
226
+ try {
227
+ const res = await client_js_1.client.post('/api/gpt/deploy', payload);
228
+ const d = res.data;
229
+ if (opts.json) {
230
+ (0, output_js_1.printJson)(d);
231
+ return;
232
+ }
233
+ if (opts.wait) {
234
+ const spin = (0, output_js_1.spinner)(`Deploying ${d.name || opts.name || opts.image}...`);
235
+ await pollUntilDone(d.id, spin);
236
+ }
237
+ else {
238
+ (0, output_js_1.success)(`Deployment queued: ${d.name || d.id}`);
239
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
240
+ }
241
+ }
242
+ catch (err) {
243
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
244
+ process.exit(1);
245
+ }
246
+ });
247
+ // source (repo-based)
248
+ deploy
249
+ .command('source')
250
+ .description('Deploy from a Git repository')
251
+ .requiredOption('--repo <url>', 'Git repository URL')
252
+ .option('--name <name>', 'Deployment name')
253
+ .option('--branch <branch>', 'Git branch')
254
+ .option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
255
+ .option('--region <region>', 'Cloud region to deploy into (e.g. us-central1, canadacentral)')
256
+ .option('--env <pairs...>', 'Environment variables as KEY=VALUE')
257
+ .option('--env-file <file>', 'Load environment variables from a .env file')
258
+ .option('--framework <framework>', 'Framework hint (e.g. node, python, go)')
259
+ .option('--build-command <cmd>', 'Custom build command')
260
+ .option('--start-command <cmd>', 'Custom start command')
261
+ .option('--install-command <cmd>', 'Custom install command')
262
+ .option('--output-dir <dir>', 'Build output directory')
263
+ .option('--dockerfile <path>', 'Path to Dockerfile in repo')
264
+ .option('--repo-secret <name>', 'Secret name containing private repo token')
265
+ .option('--environment <env>', 'Deployment environment (DEVELOPMENT|STAGING|PRODUCTION)', 'DEVELOPMENT')
266
+ .option('--auto-destroy <hours>', 'Auto-destroy after N hours', parseInt)
267
+ .option('--services <types>', 'Comma-separated services (e.g. postgres,redis). On cloud providers a database (postgres|mysql) is provisioned as a managed DB; on docker it runs as a sidecar container')
268
+ .option('--create-db <engine>', 'Alias for --services with a single database (postgres|mysql); provisions a managed cloud DB and injects its connection env')
269
+ .option('--managed-db <id>', 'Attach an existing managed database by ID and inject its connection env')
270
+ .option('--db-version <version>', 'Engine version for --create-db (default: postgres 17 / mysql 8.0)')
271
+ .option('--db-region <region>', 'Cloud region for --create-db (defaults to the deploy region)')
272
+ .option('--worker-command <cmd>', 'Run a background worker sidecar with this command')
273
+ .option('--worker-name <name>', 'Worker service name', 'worker')
274
+ .option('--no-health-check', 'Disable health checks for this deployment')
275
+ .option('--wait', 'Wait until deployment is RUNNING or FAILED')
276
+ .option('--json', 'Output raw JSON')
277
+ .action(async (opts) => {
278
+ const envVars = {};
279
+ if (opts.envFile)
280
+ Object.assign(envVars, parseEnvFile(opts.envFile));
281
+ if (opts.env) {
282
+ for (const pair of opts.env) {
283
+ const idx = pair.indexOf('=');
284
+ if (idx > 0)
285
+ envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
286
+ }
287
+ }
288
+ const payload = { sourceType: 'repo', repoUrl: opts.repo };
289
+ if (opts.name)
290
+ payload.name = opts.name;
291
+ if (opts.branch)
292
+ payload.repoBranch = opts.branch;
293
+ if (opts.provider)
294
+ payload.provider = opts.provider;
295
+ if (opts.region)
296
+ payload.region = opts.region;
297
+ if (opts.environment)
298
+ payload.environment = opts.environment;
299
+ if (opts.framework)
300
+ payload.framework = opts.framework;
301
+ if (opts.buildCommand)
302
+ payload.buildCommand = opts.buildCommand;
303
+ if (opts.startCommand)
304
+ payload.startCommand = opts.startCommand;
305
+ if (opts.installCommand)
306
+ payload.installCommand = opts.installCommand;
307
+ if (opts.outputDir)
308
+ payload.outputDir = opts.outputDir;
309
+ if (opts.dockerfile)
310
+ payload.dockerfile = opts.dockerfile;
311
+ if (opts.repoSecret)
312
+ payload.repoSecretName = opts.repoSecret;
313
+ if (opts.autoDestroy)
314
+ payload.autoDestroyHours = opts.autoDestroy;
315
+ if (opts.healthCheck === false)
316
+ payload.healthCheckEnabled = false;
317
+ if (opts.createDb)
318
+ payload.createDb = opts.createDb;
319
+ if (opts.managedDb)
320
+ payload.managedDbId = opts.managedDb;
321
+ if (opts.dbVersion)
322
+ payload.dbEngineVersion = opts.dbVersion;
323
+ if (opts.dbRegion)
324
+ payload.dbRegion = opts.dbRegion;
325
+ if (opts.services) {
326
+ payload.services = opts.services.split(',').map((s) => ({ type: s.trim() }));
327
+ }
328
+ if (opts.workerCommand) {
329
+ payload.services = [
330
+ ...(payload.services || []),
331
+ {
332
+ type: 'worker',
333
+ displayName: opts.workerName || 'worker',
334
+ command: opts.workerCommand,
335
+ },
336
+ ];
337
+ }
338
+ if (Object.keys(envVars).length)
339
+ payload.envVars = envVars;
340
+ try {
341
+ const res = await client_js_1.client.post('/api/gpt/deploy/source', payload);
342
+ const d = res.data;
343
+ if (opts.json) {
344
+ (0, output_js_1.printJson)(d);
345
+ return;
346
+ }
347
+ if (opts.wait) {
348
+ const spin = (0, output_js_1.spinner)(`Building and deploying ${d.name || opts.name || opts.repo}...`);
349
+ await pollUntilDone(d.id, spin);
350
+ }
351
+ else {
352
+ (0, output_js_1.success)(`Source deployment queued: ${d.name || d.id}`);
353
+ console.log(` ID: ${d.id}`);
354
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
355
+ }
356
+ }
357
+ catch (err) {
358
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
359
+ process.exit(1);
360
+ }
361
+ });
362
+ // redeploy
363
+ deploy
364
+ .command('redeploy <name-or-id>')
365
+ .description('Redeploy an existing deployment with the same config')
366
+ .option('--name <name>', 'Override deployment name')
367
+ .option('--provider <provider>', 'Override provider')
368
+ .option('--env <pairs...>', 'Override / add environment variables as KEY=VALUE')
369
+ .option('--env-file <file>', 'Load environment variables from a .env file (merged with existing, --env wins)')
370
+ .option('--wait', 'Wait until deployment is RUNNING or FAILED')
371
+ .option('--yes', 'Skip confirmation prompt')
372
+ .option('--json', 'Output raw JSON')
373
+ .action(async (nameOrId, opts) => {
374
+ let deployment;
375
+ try {
376
+ deployment = await resolveDeployment(nameOrId);
377
+ }
378
+ catch (err) {
379
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
380
+ process.exit(1);
381
+ }
382
+ if (!opts.yes) {
383
+ const { confirm } = await inquirer_1.default.prompt([
384
+ { type: 'confirm', name: 'confirm', message: `Redeploy "${deployment.displayName || deployment.name}"?`, default: true },
385
+ ]);
386
+ if (!confirm) {
387
+ console.log('Cancelled.');
388
+ return;
389
+ }
390
+ }
391
+ // Env the USER explicitly provides on this redeploy (--env-file / --env).
392
+ // Kept separate from the deployment's existing env: the API masks secret
393
+ // values in responses, so re-sending deployment.envVars would overwrite real
394
+ // values with "***". The in-place redeploy preserves existing env
395
+ // server-side, so we only send explicit overrides.
396
+ const explicitOverrides = {};
397
+ if (opts.envFile)
398
+ Object.assign(explicitOverrides, parseEnvFile(opts.envFile));
399
+ if (opts.env) {
400
+ for (const pair of opts.env) {
401
+ const idx = pair.indexOf('=');
402
+ if (idx > 0)
403
+ explicitOverrides[pair.slice(0, idx)] = pair.slice(idx + 1);
404
+ }
405
+ }
406
+ // For the legacy "reconstruct as a new deployment" fallback only, start from
407
+ // the deployment's env. The API masks secret values ("***" or user:***@ in
408
+ // connection strings); re-sending those would bake literal "***" into the
409
+ // clone, so drop masked entries (the server regenerates service credentials
410
+ // and DATABASE_URL for attached services anyway).
411
+ const baseEnvVars = {};
412
+ for (const [k, v] of Object.entries((deployment.envVars || {}))) {
413
+ if (v === '***' || /:\/\/[^:/@\s]+:\*\*\*@/.test(String(v)))
414
+ continue;
415
+ baseEnvVars[k] = v;
416
+ }
417
+ Object.assign(baseEnvVars, explicitOverrides);
418
+ const provider = opts.provider || undefined;
419
+ try {
420
+ // Default: rebuild the SAME deployment in place. This preserves
421
+ // everything keyed by deployment id — attached managed databases,
422
+ // volumes, secrets — so "deploy -> attach db -> redeploy" injects the DB
423
+ // on the rebuild. Renaming or switching provider can't be done in place,
424
+ // so only the no-override path uses it; anything the server rejects
425
+ // (zip/folder source, no prior job) falls through to the legacy flow.
426
+ if (!opts.name && !provider) {
427
+ try {
428
+ const body = {};
429
+ // Only send explicit overrides — the server preserves existing env.
430
+ if (Object.keys(explicitOverrides).length)
431
+ body.envVars = explicitOverrides;
432
+ const res = await client_js_1.client.post(`/api/deployments/${deployment.id}/redeploy`, body);
433
+ const d = (0, client_js_1.unwrap)(res.data);
434
+ if (opts.json) {
435
+ (0, output_js_1.printJson)(d);
436
+ return;
437
+ }
438
+ if (opts.wait) {
439
+ await pollUntilDone(d.id, (0, output_js_1.spinner)(`Rebuilding ${d.name || nameOrId}...`));
440
+ }
441
+ else {
442
+ (0, output_js_1.success)(`Redeploy queued (in place): ${d.name || d.id}`);
443
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
444
+ }
445
+ return;
446
+ }
447
+ catch (err) {
448
+ const status = err?.response?.status;
449
+ if (status !== 400 && status !== 404)
450
+ throw err;
451
+ }
452
+ }
453
+ let project = null;
454
+ if (deployment.projectId) {
455
+ const projectRes = await client_js_1.client.get(`/api/projects/${deployment.projectId}`);
456
+ project = (0, client_js_1.unwrap)(projectRes.data);
457
+ }
458
+ const hasAttachedServices = Array.isArray(deployment.services) && deployment.services.length > 0;
459
+ if (project?.repoUrl && !(hasAttachedServices && deployment.code)) {
460
+ const payload = {
461
+ sourceType: 'repo',
462
+ repoUrl: project.repoUrl,
463
+ name: opts.name || `${deployment.name}-redeploy`,
464
+ };
465
+ if (project.gitBranch)
466
+ payload.repoBranch = project.gitBranch;
467
+ if (project.framework)
468
+ payload.framework = project.framework;
469
+ if (provider)
470
+ payload.provider = provider;
471
+ if (Object.keys(baseEnvVars).length)
472
+ payload.envVars = baseEnvVars;
473
+ const res = await client_js_1.client.post('/api/gpt/deploy/source', payload);
474
+ const d = res.data;
475
+ if (opts.json) {
476
+ (0, output_js_1.printJson)(d);
477
+ return;
478
+ }
479
+ if (opts.wait) {
480
+ await pollUntilDone(d.id, (0, output_js_1.spinner)(`Rebuilding ${d.name || nameOrId}...`));
481
+ }
482
+ else {
483
+ (0, output_js_1.success)(`Redeploy queued: ${d.name || d.id}`);
484
+ console.log(` Repo: ${project.repoUrl}${project.gitBranch ? ` @ ${project.gitBranch}` : ''}`);
485
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
486
+ }
487
+ return;
488
+ }
489
+ if (deployment.code) {
490
+ const payload = {
491
+ deploymentId: deployment.id,
492
+ projectId: deployment.projectId,
493
+ name: opts.name || `${deployment.name}-redeploy`,
494
+ displayName: opts.name || `Redeploy of ${deployment.displayName || deployment.name}`,
495
+ code: deployment.code,
496
+ dockerfile: deployment.dockerfile,
497
+ deploymentProvider: provider || deployment.provider,
498
+ environment: deployment.environment,
499
+ healthCheckEnabled: deployment.healthCheckEnabled,
500
+ healthCheckType: deployment.healthCheckType,
501
+ healthCheckUrl: deployment.healthCheckUrl,
502
+ };
503
+ if (hasAttachedServices) {
504
+ payload.services = deployment.services.map((service) => ({
505
+ type: service.serviceType,
506
+ displayName: service.displayName,
507
+ }));
508
+ }
509
+ if (Object.keys(baseEnvVars).length)
510
+ payload.envVars = baseEnvVars;
511
+ const res = await client_js_1.client.post('/api/deployments', payload);
512
+ const d = (0, client_js_1.unwrap)(res.data);
513
+ if (opts.json) {
514
+ (0, output_js_1.printJson)(d);
515
+ return;
516
+ }
517
+ if (opts.wait) {
518
+ await pollUntilDone(d.id, (0, output_js_1.spinner)(`Redeploying ${d.name || nameOrId}...`));
519
+ }
520
+ else {
521
+ (0, output_js_1.success)(`Redeploy queued: ${d.name || d.id}`);
522
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
523
+ }
524
+ return;
525
+ }
526
+ if (deployment.imageName && !isInternalDeploymentImage(deployment.imageName)) {
527
+ const payload = {
528
+ image: deployment.imageName,
529
+ port: deployment.port,
530
+ name: opts.name || `${deployment.name}-redeploy`,
531
+ };
532
+ if (provider)
533
+ payload.provider = provider;
534
+ if (Object.keys(baseEnvVars).length)
535
+ payload.envVars = baseEnvVars;
536
+ const res = await client_js_1.client.post('/api/gpt/deploy', payload);
537
+ const d = res.data;
538
+ if (opts.json) {
539
+ (0, output_js_1.printJson)(d);
540
+ return;
541
+ }
542
+ if (opts.wait) {
543
+ await pollUntilDone(d.id, (0, output_js_1.spinner)(`Redeploying ${d.name || nameOrId}...`));
544
+ }
545
+ else {
546
+ (0, output_js_1.success)(`Redeploy queued: ${d.name || d.id}`);
547
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
548
+ }
549
+ return;
550
+ }
551
+ (0, output_js_1.errorMsg)('Cannot redeploy: no image or repo URL found. Use "nexus deploy source --repo <url>" instead.');
552
+ process.exit(1);
553
+ }
554
+ catch (err) {
555
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
556
+ process.exit(1);
557
+ }
558
+ });
559
+ // stop
560
+ deploy
561
+ .command('stop <name-or-id>')
562
+ .description('Stop a running deployment')
563
+ .option('--yes', 'Skip confirmation prompt')
564
+ .action(async (nameOrId, opts) => {
565
+ try {
566
+ const d = await resolveDeployment(nameOrId);
567
+ if (!opts.yes) {
568
+ const { confirm } = await inquirer_1.default.prompt([
569
+ { type: 'confirm', name: 'confirm', message: `Stop "${d.displayName || d.name}"?`, default: false },
570
+ ]);
571
+ if (!confirm) {
572
+ console.log('Cancelled.');
573
+ return;
574
+ }
575
+ }
576
+ await client_js_1.client.post(`/api/deployments/${d.id}/stop`);
577
+ (0, output_js_1.success)(`Deployment "${d.displayName || d.name}" stopped.`);
578
+ }
579
+ catch (err) {
580
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
581
+ process.exit(1);
582
+ }
583
+ });
584
+ // start
585
+ deploy
586
+ .command('start <name-or-id>')
587
+ .description('Start a stopped deployment')
588
+ .action(async (nameOrId) => {
589
+ try {
590
+ const d = await resolveDeployment(nameOrId);
591
+ await client_js_1.client.post(`/api/deployments/${d.id}/start`);
592
+ (0, output_js_1.success)(`Deployment "${d.displayName || d.name}" started.`);
593
+ }
594
+ catch (err) {
595
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
596
+ process.exit(1);
597
+ }
598
+ });
599
+ // delete
600
+ deploy
601
+ .command('delete <name-or-id>')
602
+ .description('Delete a deployment')
603
+ .option('--yes', 'Skip confirmation prompt')
604
+ .action(async (nameOrId, opts) => {
605
+ try {
606
+ const d = await resolveDeployment(nameOrId);
607
+ if (!opts.yes) {
608
+ const { confirm } = await inquirer_1.default.prompt([
609
+ { type: 'confirm', name: 'confirm', message: `Delete "${d.displayName || d.name}"? This cannot be undone.`, default: false },
610
+ ]);
611
+ if (!confirm) {
612
+ console.log('Cancelled.');
613
+ return;
614
+ }
615
+ }
616
+ await client_js_1.client.delete(`/api/deployments/${d.id}`);
617
+ (0, output_js_1.success)(`Deployment "${d.displayName || d.name}" deleted.`);
618
+ }
619
+ catch (err) {
620
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
621
+ process.exit(1);
622
+ }
623
+ });
624
+ // logs
625
+ deploy
626
+ .command('logs <name-or-id>')
627
+ .description('View deployment logs')
628
+ .option('--type <type>', 'Log type: runtime or build', 'runtime')
629
+ .option('--lines <n>', 'Number of log lines', '100')
630
+ .option('--follow', 'Poll for new logs every 2s')
631
+ .action(async (nameOrId, opts) => {
632
+ let deployId;
633
+ try {
634
+ const d = await resolveDeployment(nameOrId);
635
+ deployId = d.id;
636
+ }
637
+ catch (err) {
638
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
639
+ process.exit(1);
640
+ return;
641
+ }
642
+ const limit = parseInt(opts.lines, 10) || 100;
643
+ /** Normalise the API response into an array of { message, timestamp? } */
644
+ const fetchLogs = async (lastTimestamp) => {
645
+ const params = { type: opts.type, limit };
646
+ if (lastTimestamp)
647
+ params.after = lastTimestamp;
648
+ const res = await client_js_1.client.get(`/api/deployments/${deployId}/logs`, { params });
649
+ const raw = (0, client_js_1.unwrap)(res.data);
650
+ // Shape: { logs: "line1\nline2\n..." }
651
+ if (raw && typeof raw.logs === 'string') {
652
+ return raw.logs
653
+ .split('\n')
654
+ .filter((l) => l.length > 0)
655
+ .map((l) => ({ message: l }));
656
+ }
657
+ // Shape: [ { message, timestamp }, ... ] or [ "line1", ... ]
658
+ const arr = Array.isArray(raw) ? raw : Array.isArray(raw?.logs) ? raw.logs : [];
659
+ return arr.map((entry) => typeof entry === 'string'
660
+ ? { message: entry }
661
+ : { message: entry.message || entry.log || String(entry), timestamp: entry.timestamp });
662
+ };
663
+ try {
664
+ const logs = await fetchLogs();
665
+ let lastTimestamp;
666
+ for (const log of logs) {
667
+ const ts = log.timestamp ? `[${new Date(log.timestamp).toLocaleTimeString()}] ` : '';
668
+ console.log(`${ts}${log.message}`);
669
+ lastTimestamp = log.timestamp || lastTimestamp;
670
+ }
671
+ if (!opts.follow)
672
+ return;
673
+ while (true) {
674
+ await new Promise((r) => setTimeout(r, 2000));
675
+ try {
676
+ const newLogs = await fetchLogs(lastTimestamp);
677
+ for (const log of newLogs) {
678
+ const ts = log.timestamp ? `[${new Date(log.timestamp).toLocaleTimeString()}] ` : '';
679
+ console.log(`${ts}${log.message}`);
680
+ lastTimestamp = log.timestamp || lastTimestamp;
681
+ }
682
+ }
683
+ catch { /* ignore transient errors in follow mode */ }
684
+ }
685
+ }
686
+ catch (err) {
687
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
688
+ process.exit(1);
689
+ }
690
+ });
691
+ // scale
692
+ deploy
693
+ .command('scale <name-or-id> <replicas>')
694
+ .description('Scale deployment replicas')
695
+ .action(async (nameOrId, replicas) => {
696
+ const count = parseInt(replicas, 10);
697
+ if (isNaN(count) || count < 1 || count > 10) {
698
+ (0, output_js_1.errorMsg)('Replicas must be a number between 1 and 10.');
699
+ process.exit(1);
700
+ }
701
+ try {
702
+ const d = await resolveDeployment(nameOrId);
703
+ await client_js_1.client.post(`/api/deployments/${d.id}/scale`, { replicas: count });
704
+ (0, output_js_1.success)(`Deployment "${d.displayName || d.name}" scaled to ${count} replica(s).`);
705
+ }
706
+ catch (err) {
707
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
708
+ process.exit(1);
709
+ }
710
+ });
711
+ // rollback
712
+ deploy
713
+ .command('rollback <name-or-id>')
714
+ .description('Roll back a deployment to the previous version')
715
+ .option('--target <deployment-id>', 'Target deployment ID to roll back to')
716
+ .option('--yes', 'Skip confirmation prompt')
717
+ .action(async (nameOrId, opts) => {
718
+ try {
719
+ const d = await resolveDeployment(nameOrId);
720
+ if (!opts.yes) {
721
+ const { confirm } = await inquirer_1.default.prompt([
722
+ { type: 'confirm', name: 'confirm', message: `Roll back "${d.displayName || d.name}" to the previous version?`, default: false },
723
+ ]);
724
+ if (!confirm) {
725
+ console.log('Cancelled.');
726
+ return;
727
+ }
728
+ }
729
+ const payload = {};
730
+ if (opts.target)
731
+ payload.targetDeploymentId = opts.target;
732
+ const res = await client_js_1.client.post(`/api/deployments/${d.id}/rollback`, payload);
733
+ const result = (0, client_js_1.unwrap)(res.data);
734
+ (0, output_js_1.success)(`Rollback initiated → new deployment ${result.id || '?'}`);
735
+ }
736
+ catch (err) {
737
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
738
+ process.exit(1);
739
+ }
740
+ });
741
+ // openclaw
742
+ deploy
743
+ .command('openclaw')
744
+ .description('Deploy an OpenClaw gateway (alpine/openclaw:latest) on port 18789')
745
+ .option('--name <name>', 'Deployment name', 'openclaw-gateway')
746
+ .option('--gateway-token <token>', 'OpenClaw gateway auth token (auto-generated if not set)')
747
+ .option('--claude-api-key <key>', 'CLAUDE_AI_SESSION_KEY value')
748
+ .option('--claude-web-session <key>', 'CLAUDE_WEB_SESSION_KEY value')
749
+ .option('--claude-web-cookie <cookie>', 'CLAUDE_WEB_COOKIE value')
750
+ .option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
751
+ .option('--env <pairs...>', 'Additional environment variables as KEY=VALUE')
752
+ .option('--env-file <file>', 'Load environment variables from a .env file')
753
+ .option('--wait', 'Wait until deployment is RUNNING or FAILED')
754
+ .option('--json', 'Output raw JSON')
755
+ .action(async (opts) => {
756
+ const gatewayToken = opts.gatewayToken || (0, crypto_1.randomBytes)(32).toString('hex');
757
+ const envVars = {
758
+ HOME: '/home/node',
759
+ OPENCLAW_GATEWAY_TOKEN: gatewayToken,
760
+ OPENCLAW_GATEWAY_BIND: 'lan',
761
+ OPENCLAW_GATEWAY_CONTROL_UI_DANGEROUSLY_ALLOW_HOST_HEADER_ORIGIN_FALLBACK: 'true',
762
+ };
763
+ if (opts.claudeApiKey)
764
+ envVars['CLAUDE_AI_SESSION_KEY'] = opts.claudeApiKey;
765
+ if (opts.claudeWebSession)
766
+ envVars['CLAUDE_WEB_SESSION_KEY'] = opts.claudeWebSession;
767
+ if (opts.claudeWebCookie)
768
+ envVars['CLAUDE_WEB_COOKIE'] = opts.claudeWebCookie;
769
+ if (opts.envFile)
770
+ Object.assign(envVars, parseEnvFile(opts.envFile));
771
+ if (opts.env) {
772
+ for (const pair of opts.env) {
773
+ const idx = pair.indexOf('=');
774
+ if (idx > 0)
775
+ envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
776
+ }
777
+ }
778
+ const payload = {
779
+ image: 'alpine/openclaw:latest',
780
+ port: 18789,
781
+ name: opts.name,
782
+ envVars,
783
+ startCommand: 'mkdir -p /home/node/.openclaw && echo \'{"gateway":{"controlUi":{"dangerouslyAllowHostHeaderOriginFallback":true,"dangerouslyDisableDeviceAuth":true},"trustedProxies":["172.16.0.0/12","10.0.0.0/8"]}}\' > /home/node/.openclaw/openclaw.json && node dist/index.js gateway --bind lan --port 18789 --allow-unconfigured',
784
+ healthCheckEnabled: false, // OpenClaw gateway has no HTTP health endpoint
785
+ };
786
+ if (opts.provider)
787
+ payload.provider = opts.provider;
788
+ try {
789
+ const res = await client_js_1.client.post('/api/gpt/deploy', payload);
790
+ const d = res.data;
791
+ if (opts.json) {
792
+ (0, output_js_1.printJson)({ ...d, gatewayToken });
793
+ return;
794
+ }
795
+ if (opts.wait) {
796
+ const spin = (0, output_js_1.spinner)('Deploying OpenClaw gateway...');
797
+ await pollUntilDone(d.id, spin);
798
+ }
799
+ else {
800
+ (0, output_js_1.success)(`OpenClaw gateway queued: ${d.name || d.id}`);
801
+ console.log(` Gateway token: ${gatewayToken}`);
802
+ console.log(` Port: 18789`);
803
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
804
+ }
805
+ }
806
+ catch (err) {
807
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
808
+ process.exit(1);
809
+ }
810
+ });
811
+ // flixty
812
+ deploy
813
+ .command('flixty')
814
+ .description('Deploy Flixty social media creator studio from source (github.com/nexusrun/flixty)')
815
+ .option('--name <name>', 'Deployment name', 'flixty')
816
+ .option('--session-secret <secret>', 'Express session secret (auto-generated if not set)')
817
+ .option('--base-url <url>', 'Public URL of the deployment (for OAuth redirect URIs)')
818
+ .option('--anthropic-api-key <key>', 'Anthropic API key for AI Assist')
819
+ .option('--x-client-id <id>', 'X/Twitter OAuth 2.0 Client ID')
820
+ .option('--x-client-secret <secret>', 'X/Twitter OAuth 2.0 Client Secret')
821
+ .option('--linkedin-client-id <id>', 'LinkedIn OAuth Client ID')
822
+ .option('--linkedin-client-secret <secret>', 'LinkedIn OAuth Client Secret')
823
+ .option('--fb-app-id <id>', 'Facebook App ID')
824
+ .option('--fb-app-secret <secret>', 'Facebook App Secret')
825
+ .option('--tiktok-client-key <key>', 'TikTok Client Key')
826
+ .option('--tiktok-client-secret <secret>', 'TikTok Client Secret')
827
+ .option('--google-client-id <id>', 'Google Client ID (YouTube)')
828
+ .option('--google-client-secret <secret>', 'Google Client Secret')
829
+ .option('--provider <provider>', 'Provider (docker|gcp_cloud_run|aws_ecs_fargate|azure_container_apps)')
830
+ .option('--env <pairs...>', 'Additional environment variables as KEY=VALUE')
831
+ .option('--env-file <file>', 'Load environment variables from a .env file')
832
+ .option('--wait', 'Wait until deployment is RUNNING or FAILED')
833
+ .option('--json', 'Output raw JSON')
834
+ .action(async (opts) => {
835
+ const sessionSecret = opts.sessionSecret || (0, crypto_1.randomBytes)(32).toString('hex');
836
+ const envVars = {
837
+ SESSION_SECRET: sessionSecret,
838
+ PORT: '3000',
839
+ NODE_ENV: 'production',
840
+ };
841
+ if (opts.baseUrl)
842
+ envVars['BASE_URL'] = opts.baseUrl;
843
+ if (opts.anthropicApiKey)
844
+ envVars['ANTHROPIC_API_KEY'] = opts.anthropicApiKey;
845
+ if (opts.xClientId)
846
+ envVars['X_CLIENT_ID'] = opts.xClientId;
847
+ if (opts.xClientSecret)
848
+ envVars['X_CLIENT_SECRET'] = opts.xClientSecret;
849
+ if (opts.linkedinClientId)
850
+ envVars['LINKEDIN_CLIENT_ID'] = opts.linkedinClientId;
851
+ if (opts.linkedinClientSecret)
852
+ envVars['LINKEDIN_CLIENT_SECRET'] = opts.linkedinClientSecret;
853
+ if (opts.fbAppId)
854
+ envVars['FB_APP_ID'] = opts.fbAppId;
855
+ if (opts.fbAppSecret)
856
+ envVars['FB_APP_SECRET'] = opts.fbAppSecret;
857
+ if (opts.tiktokClientKey)
858
+ envVars['TIKTOK_CLIENT_KEY'] = opts.tiktokClientKey;
859
+ if (opts.tiktokClientSecret)
860
+ envVars['TIKTOK_CLIENT_SECRET'] = opts.tiktokClientSecret;
861
+ if (opts.googleClientId)
862
+ envVars['GOOGLE_CLIENT_ID'] = opts.googleClientId;
863
+ if (opts.googleClientSecret)
864
+ envVars['GOOGLE_CLIENT_SECRET'] = opts.googleClientSecret;
865
+ if (opts.envFile)
866
+ Object.assign(envVars, parseEnvFile(opts.envFile));
867
+ if (opts.env) {
868
+ for (const pair of opts.env) {
869
+ const idx = pair.indexOf('=');
870
+ if (idx > 0)
871
+ envVars[pair.slice(0, idx)] = pair.slice(idx + 1);
872
+ }
873
+ }
874
+ const payload = {
875
+ sourceType: 'repo',
876
+ repoUrl: 'https://github.com/nexusrun/flixty.git',
877
+ name: opts.name,
878
+ environment: 'PRODUCTION',
879
+ startCommand: 'node server.js',
880
+ envVars,
881
+ healthCheckEnabled: true,
882
+ };
883
+ if (opts.provider)
884
+ payload.provider = opts.provider;
885
+ try {
886
+ const res = await client_js_1.client.post('/api/gpt/deploy/source', payload);
887
+ const d = res.data;
888
+ if (opts.json) {
889
+ (0, output_js_1.printJson)({ ...d, sessionSecret });
890
+ return;
891
+ }
892
+ if (opts.wait) {
893
+ const spin = (0, output_js_1.spinner)('Deploying Flixty...');
894
+ await pollUntilDone(d.id, spin);
895
+ }
896
+ else {
897
+ (0, output_js_1.success)(`Flixty queued: ${d.name || d.id}`);
898
+ console.log(` Session secret: ${sessionSecret}`);
899
+ console.log(` Port: 3000`);
900
+ if (!opts.baseUrl) {
901
+ console.log(` Note: once running, redeploy with --base-url <public-url> for OAuth to work`);
902
+ }
903
+ console.log(` Run 'nexus deploy status ${d.id} --watch' to track progress`);
904
+ }
905
+ }
906
+ catch (err) {
907
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
908
+ process.exit(1);
909
+ }
910
+ });
911
+ // status
912
+ deploy
913
+ .command('status <name-or-id>')
914
+ .description('Show deployment status')
915
+ .option('--watch', 'Refresh every 3s')
916
+ .option('--json', 'Output raw JSON')
917
+ .action(async (nameOrId, opts) => {
918
+ let deployId;
919
+ try {
920
+ const d = await resolveDeployment(nameOrId);
921
+ deployId = d.id;
922
+ }
923
+ catch (err) {
924
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
925
+ process.exit(1);
926
+ return;
927
+ }
928
+ const show = async () => {
929
+ const res = await client_js_1.client.get(`/api/deployments/${deployId}`);
930
+ const d = (0, client_js_1.unwrap)(res.data);
931
+ if (opts.json) {
932
+ (0, output_js_1.printJson)(d);
933
+ return;
934
+ }
935
+ if (opts.watch)
936
+ process.stdout.write('\x1Bc');
937
+ (0, output_js_1.printTable)(['Field', 'Value'], [
938
+ ['Name', d.displayName || d.name],
939
+ ['Status', (0, output_js_1.statusBadge)(d.status)],
940
+ ['Provider', d.provider || '—'],
941
+ ['URL', d.url || d.serviceUrl || '—'],
942
+ ['Replicas', String(d.replicas ?? '—')],
943
+ ['Auto Destroy', formatAutoDestroy(d.autoDestroyAt)],
944
+ ['Updated', d.updatedAt ? (0, output_js_1.timeAgo)(d.updatedAt) : '—'],
945
+ ]);
946
+ };
947
+ try {
948
+ await show();
949
+ if (!opts.watch)
950
+ return;
951
+ while (true) {
952
+ await new Promise((r) => setTimeout(r, 3000));
953
+ try {
954
+ await show();
955
+ }
956
+ catch { /* ignore */ }
957
+ }
958
+ }
959
+ catch (err) {
960
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
961
+ process.exit(1);
962
+ }
963
+ });
964
+ deploy
965
+ .command('auto-destroy <name-or-id>')
966
+ .description('Set, extend, reduce, or disable deployment auto-destroy without restarting the deployment')
967
+ .option('--in <duration>', 'Destroy after a relative duration, e.g. 30m, 4h, 2d')
968
+ .option('--at <iso-date>', 'Destroy at an absolute ISO timestamp')
969
+ .option('--off', 'Disable auto-destroy')
970
+ .option('--json', 'Output raw JSON')
971
+ .action(async (nameOrId, opts) => {
972
+ const selected = [opts.in, opts.at, opts.off].filter(Boolean);
973
+ if (selected.length !== 1) {
974
+ (0, output_js_1.errorMsg)('Choose exactly one of --in, --at, or --off');
975
+ process.exit(1);
976
+ }
977
+ try {
978
+ const deployment = await resolveDeployment(nameOrId);
979
+ const payload = {};
980
+ if (opts.off) {
981
+ payload.autoDestroyAt = null;
982
+ }
983
+ else if (opts.in) {
984
+ payload.autoDestroyAt = parseDurationToDate(opts.in).toISOString();
985
+ }
986
+ else if (opts.at) {
987
+ const date = new Date(opts.at);
988
+ if (Number.isNaN(date.getTime())) {
989
+ throw new Error(`Invalid ISO date: ${opts.at}`);
990
+ }
991
+ payload.autoDestroyAt = date.toISOString();
992
+ }
993
+ const res = await client_js_1.client.patch(`/api/deployments/${deployment.id}/auto-destroy`, payload);
994
+ const updated = (0, client_js_1.unwrap)(res.data);
995
+ if (opts.json) {
996
+ (0, output_js_1.printJson)(updated);
997
+ return;
998
+ }
999
+ (0, output_js_1.success)(updated.autoDestroyAt
1000
+ ? `Auto-destroy set for ${updated.displayName || updated.name}: ${formatAutoDestroy(updated.autoDestroyAt)}`
1001
+ : `Auto-destroy disabled for ${updated.displayName || updated.name}`);
1002
+ }
1003
+ catch (err) {
1004
+ (0, output_js_1.errorMsg)((0, client_js_1.apiError)(err));
1005
+ process.exit(1);
1006
+ }
1007
+ });
1008
+ }
1009
+ //# sourceMappingURL=deploy.js.map