nexusapp-cli 3.0.0 → 3.1.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.
Files changed (81) 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 +6 -2
  65. package/src/client.ts +0 -68
  66. package/src/commands/auth.ts +0 -186
  67. package/src/commands/bucket.ts +0 -261
  68. package/src/commands/database.ts +0 -305
  69. package/src/commands/deploy.ts +0 -904
  70. package/src/commands/domain.ts +0 -167
  71. package/src/commands/exec.ts +0 -154
  72. package/src/commands/managedDb.ts +0 -170
  73. package/src/commands/member.ts +0 -168
  74. package/src/commands/project.ts +0 -81
  75. package/src/commands/secret.ts +0 -117
  76. package/src/commands/token.ts +0 -173
  77. package/src/commands/volume.ts +0 -113
  78. package/src/config.ts +0 -56
  79. package/src/index.ts +0 -39
  80. package/src/output.ts +0 -65
  81. package/tsconfig.json +0 -19
package/dist/output.js ADDED
@@ -0,0 +1,71 @@
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.statusBadge = statusBadge;
7
+ exports.printTable = printTable;
8
+ exports.printJson = printJson;
9
+ exports.spinner = spinner;
10
+ exports.timeAgo = timeAgo;
11
+ exports.success = success;
12
+ exports.errorMsg = errorMsg;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const cli_table3_1 = __importDefault(require("cli-table3"));
15
+ const ora_1 = __importDefault(require("ora"));
16
+ const STATUS_COLORS = {
17
+ RUNNING: chalk_1.default.green,
18
+ BUILDING: chalk_1.default.yellow,
19
+ DEPLOYING: chalk_1.default.yellow,
20
+ FAILED: chalk_1.default.red,
21
+ STOPPED: chalk_1.default.gray,
22
+ PENDING: chalk_1.default.blue,
23
+ QUEUED: chalk_1.default.blue,
24
+ TERMINATED: chalk_1.default.gray,
25
+ };
26
+ function statusBadge(status) {
27
+ const colorFn = STATUS_COLORS[status?.toUpperCase()] || chalk_1.default.white;
28
+ return colorFn(status || 'UNKNOWN');
29
+ }
30
+ function printTable(headers, rows) {
31
+ const table = new cli_table3_1.default({
32
+ head: headers.map((h) => chalk_1.default.bold(h)),
33
+ style: { head: [], border: [] },
34
+ chars: {
35
+ top: '', 'top-mid': '', 'top-left': '', 'top-right': '',
36
+ bottom: '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
37
+ left: '', 'left-mid': '', mid: '', 'mid-mid': '',
38
+ right: '', 'right-mid': '', middle: ' ',
39
+ },
40
+ });
41
+ for (const row of rows) {
42
+ table.push(row.map((cell) => cell ?? chalk_1.default.gray('—')));
43
+ }
44
+ console.log(table.toString());
45
+ }
46
+ function printJson(obj) {
47
+ console.log(JSON.stringify(obj, null, 2));
48
+ }
49
+ function spinner(text) {
50
+ return (0, ora_1.default)(text).start();
51
+ }
52
+ function timeAgo(dateStr) {
53
+ const diff = Date.now() - new Date(dateStr).getTime();
54
+ const s = Math.floor(diff / 1000);
55
+ if (s < 60)
56
+ return `${s}s ago`;
57
+ const m = Math.floor(s / 60);
58
+ if (m < 60)
59
+ return `${m}m ago`;
60
+ const h = Math.floor(m / 60);
61
+ if (h < 24)
62
+ return `${h}h ago`;
63
+ return `${Math.floor(h / 24)}d ago`;
64
+ }
65
+ function success(msg) {
66
+ console.log(chalk_1.default.green('✓') + ' ' + msg);
67
+ }
68
+ function errorMsg(msg) {
69
+ console.error(chalk_1.default.red('✗') + ' ' + msg);
70
+ }
71
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "nexusapp-cli",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "NEXUS AI command-line interface",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "nexus": "./bin/nexus"
8
8
  },
9
+ "files": [
10
+ "bin/",
11
+ "dist/",
12
+ "README.md"
13
+ ],
9
14
  "scripts": {
10
15
  "build": "tsc",
11
16
  "dev": "ts-node src/index.ts",
@@ -24,7 +29,6 @@
24
29
  "cli-table3": "^0.6.3",
25
30
  "commander": "^12.0.0",
26
31
  "inquirer": "^9.2.0",
27
- "nexusapp-cli": "^2.1.1",
28
32
  "ora": "^8.0.0"
29
33
  },
30
34
  "devDependencies": {
package/src/client.ts DELETED
@@ -1,68 +0,0 @@
1
- import axios, { AxiosInstance, AxiosError } from 'axios';
2
- import { getConfig } from './config.js';
3
-
4
- function createClient(): AxiosInstance {
5
- const config = getConfig();
6
- const baseURL = config.apiUrl || 'https://nexusai.run';
7
-
8
- const instance = axios.create({
9
- baseURL,
10
- timeout: 30000,
11
- });
12
-
13
- instance.interceptors.request.use((req) => {
14
- const cfg = getConfig();
15
- if (cfg.token) {
16
- req.headers = req.headers || {};
17
- req.headers['Authorization'] = `Bearer ${cfg.token}`;
18
- }
19
- return req;
20
- });
21
-
22
- instance.interceptors.response.use(
23
- (res) => res,
24
- (error: AxiosError) => {
25
- if (!error.response) {
26
- const url = baseURL;
27
- console.error(`Cannot reach NEXUS AI API at ${url}.`);
28
- process.exit(1);
29
- }
30
-
31
- const status = error.response.status;
32
- const data = error.response.data as any;
33
-
34
- if (status === 401) {
35
- console.error("Session expired. Run 'nexus auth login'");
36
- process.exit(1);
37
- }
38
-
39
- if (status === 403) {
40
- console.error(data?.message || data?.error || 'Access denied.');
41
- process.exit(1);
42
- }
43
-
44
- return Promise.reject(error);
45
- }
46
- );
47
-
48
- return instance;
49
- }
50
-
51
- export const client = createClient();
52
-
53
- /** Unwrap { success, data } envelope if present, otherwise return as-is. */
54
- export function unwrap(responseData: any): any {
55
- if (responseData && typeof responseData === 'object' && 'data' in responseData) {
56
- return responseData.data;
57
- }
58
- return responseData;
59
- }
60
-
61
- export function apiError(error: unknown): string {
62
- if (axios.isAxiosError(error)) {
63
- const data = error.response?.data as any;
64
- return data?.message || data?.error || error.message;
65
- }
66
- if (error instanceof Error) return error.message;
67
- return String(error);
68
- }
@@ -1,186 +0,0 @@
1
- import { Command } from 'commander';
2
- import http from 'http';
3
- import { execFile } from 'child_process';
4
- import { AddressInfo } from 'net';
5
- import axios from 'axios';
6
- import { getConfig, saveConfig, clearConfig } from '../config.js';
7
- import { client, apiError, unwrap } from '../client.js';
8
- import { success, errorMsg, printJson, printTable, spinner } from '../output.js';
9
-
10
- function openBrowser(url: string): void {
11
- const platform = process.platform;
12
- if (platform === 'darwin') {
13
- execFile('open', [url], () => {});
14
- } else if (platform === 'win32') {
15
- execFile('cmd', ['/c', 'start', '', url], () => {});
16
- } else {
17
- execFile('xdg-open', [url], () => {});
18
- }
19
- }
20
-
21
- function successHtml(webUrl: string): string {
22
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>NEXUS AI CLI</title>
23
- <style>
24
- *{box-sizing:border-box;margin:0;padding:0}
25
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;background:#0b0c12;color:#e5e7eb}
26
- .box{text-align:center;padding:2.5rem 3rem;background:#ffffff08;border:1px solid #ffffff14;border-radius:1.25rem;max-width:360px;width:90%}
27
- .logo{display:flex;align-items:center;justify-content:center;gap:.65rem;margin-bottom:2rem}
28
- .logo img{height:2.25rem;width:2.25rem}
29
- .logo-name{font-size:1rem;font-weight:600;color:#fff;letter-spacing:-.01em}
30
- .check{width:2.75rem;height:2.75rem;background:#0ea5e920;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.25rem}
31
- .check svg{width:1.25rem;height:1.25rem;stroke:#38bdf8;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
32
- .title{font-size:1.2rem;font-weight:700;color:#fff;margin-bottom:.4rem}
33
- .sub{color:#6b7280;font-size:.85rem;line-height:1.5}
34
- </style></head>
35
- <body><div class="box">
36
- <div class="logo">
37
- <img src="${webUrl}/logo.svg" alt="NEXUS AI" onerror="this.style.display='none'">
38
- <span class="logo-name">NEXUS AI</span>
39
- </div>
40
- <div class="check"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>
41
- <div class="title">Authorization successful</div>
42
- <div class="sub">You can close this tab and return to your terminal.</div>
43
- </div></body></html>`;
44
- }
45
-
46
- function waitForCallback(server: http.Server, webUrl: string): Promise<{ token: string; tokenId: string; email: string }> {
47
- return new Promise((resolve, reject) => {
48
- const timeout = setTimeout(() => {
49
- server.close();
50
- reject(new Error('Timed out waiting for browser authorization (2 minutes).'));
51
- }, 120_000);
52
-
53
- server.on('request', (req, res) => {
54
- const url = new URL(req.url!, `http://localhost`);
55
- const token = url.searchParams.get('token');
56
- const tokenId = url.searchParams.get('tokenId') || '';
57
- const email = url.searchParams.get('email') || '';
58
-
59
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
60
- res.end(successHtml(webUrl));
61
-
62
- clearTimeout(timeout);
63
- server.close();
64
-
65
- if (!token) {
66
- reject(new Error('No token received from browser.'));
67
- } else {
68
- resolve({ token, tokenId, email });
69
- }
70
- });
71
- });
72
- }
73
-
74
- export function registerAuth(program: Command): void {
75
- const auth = program.command('auth').description('Authentication commands');
76
-
77
- // login
78
- auth
79
- .command('login')
80
- .description('Log in to NEXUS AI via browser')
81
- .option('--api-url <url>', 'Backend API base URL (e.g. http://localhost:3001)')
82
- .option('--web-url <url>', 'Frontend URL (e.g. http://localhost:3002)')
83
- .option('--token <token>', 'Use an existing nxk_* access token directly')
84
- .action(async (opts) => {
85
- const apiUrl = opts.apiUrl || process.env.NEXUSAI_API_URL || 'https://nexusai.run';
86
- const webUrl = opts.webUrl || process.env.NEXUSAI_WEB_URL
87
- || apiUrl.replace(':3001', ':3002').replace('api.nexusai.run', 'nexusai.run');
88
-
89
- // --token shortcut: skip browser
90
- if (opts.token) {
91
- try {
92
- const res = await axios.get(`${apiUrl}/api/auth/verify`, {
93
- headers: { Authorization: `Bearer ${opts.token}` },
94
- });
95
- const user = res.data;
96
- saveConfig({ apiUrl, token: opts.token, tokenId: '' });
97
- success(`Logged in as ${user.email || user.user?.email}`);
98
- } catch (err) {
99
- errorMsg('Token verification failed: ' + apiError(err));
100
- process.exit(1);
101
- }
102
- return;
103
- }
104
-
105
- // Start local callback server on a random port
106
- const server = http.createServer();
107
- await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
108
- const port = (server.address() as AddressInfo).port;
109
-
110
- const callbackUrl = `http://localhost:${port}`;
111
- const authUrl = `${webUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
112
-
113
- console.log('');
114
- console.log('Opening your browser to complete login...');
115
- console.log('');
116
- console.log(` ${authUrl}`);
117
- console.log('');
118
- console.log('If the browser did not open, copy the URL above into your browser.');
119
- console.log('');
120
-
121
- openBrowser(authUrl);
122
-
123
- const spin = spinner('Waiting for browser authorization...');
124
-
125
- try {
126
- const { token, tokenId, email } = await waitForCallback(server, webUrl);
127
- saveConfig({ apiUrl, token, tokenId });
128
- spin.stop();
129
- success(`Logged in as ${email}`);
130
- } catch (err) {
131
- spin.fail(err instanceof Error ? err.message : String(err));
132
- process.exit(1);
133
- }
134
- });
135
-
136
- // logout
137
- auth
138
- .command('logout')
139
- .description('Log out and revoke access token')
140
- .action(async () => {
141
- const config = getConfig();
142
- if (!config.token) {
143
- console.log('Not logged in.');
144
- return;
145
- }
146
-
147
- if (config.tokenId) {
148
- try {
149
- await client.post(`/api/tokens/${config.tokenId}/revoke`);
150
- } catch {
151
- // best-effort revocation
152
- }
153
- }
154
-
155
- clearConfig();
156
- success('Logged out.');
157
- });
158
-
159
- // whoami
160
- auth
161
- .command('whoami')
162
- .description('Show current authenticated user')
163
- .option('--json', 'Output raw JSON')
164
- .action(async (opts) => {
165
- try {
166
- const res = await client.get('/api/auth/verify');
167
- const user = unwrap(res.data);
168
- if (opts.json) {
169
- printJson(user);
170
- return;
171
- }
172
- const u = user.user || user;
173
- const email = u.email || 'unknown';
174
- const org = u.organization?.name || u.organizationId || '—';
175
- const role = u.role || '—';
176
- printTable(['Field', 'Value'], [
177
- ['Email', email],
178
- ['Organization', org],
179
- ['Role', role],
180
- ]);
181
- } catch (err) {
182
- errorMsg(apiError(err));
183
- process.exit(1);
184
- }
185
- });
186
- }
@@ -1,261 +0,0 @@
1
- import { Command } from 'commander';
2
- import inquirer from 'inquirer';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import { client, apiError, unwrap } from '../client.js';
6
- import { printTable, printJson, success, errorMsg, timeAgo } from '../output.js';
7
-
8
- function formatBytes(bytes: number | bigint): string {
9
- const n = typeof bytes === 'bigint' ? Number(bytes) : bytes;
10
- if (n < 1024) return `${n} B`;
11
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
12
- if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
13
- return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
14
- }
15
-
16
- export function registerBucket(program: Command): void {
17
- const bk = program
18
- .command('bucket')
19
- .description('Object storage buckets (S3-compatible MinIO buckets)');
20
-
21
- bk
22
- .command('list')
23
- .description('List buckets')
24
- .option('--json', 'Output raw JSON')
25
- .action(async (opts) => {
26
- try {
27
- const res = await client.get('/api/buckets');
28
- const buckets: any[] = unwrap(res.data) || [];
29
- if (opts.json) { printJson(buckets); return; }
30
- if (!buckets.length) { console.log('No buckets found.'); return; }
31
-
32
- printTable(
33
- ['ID', 'NAME', 'SIZE', 'OBJECTS', 'ATTACHED TO', 'CREATED'],
34
- buckets.map((b: any) => [
35
- b.id,
36
- b.displayName ? `${b.name} (${b.displayName})` : b.name,
37
- formatBytes(b.sizeBytes),
38
- String(b.objectCount),
39
- b.attachments?.length ? b.attachments.map((a: any) => a.deploymentName).join(', ') : '—',
40
- b.createdAt ? timeAgo(b.createdAt) : '—',
41
- ])
42
- );
43
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
44
- });
45
-
46
- bk
47
- .command('create <name>')
48
- .description('Create a new bucket (org-scoped)')
49
- .option('--display-name <name>', 'Friendly display name')
50
- .option('--region <region>', 'Region', 'us-east-1')
51
- .option('--json', 'Output raw JSON')
52
- .action(async (name, opts) => {
53
- try {
54
- const res = await client.post('/api/buckets', {
55
- name,
56
- displayName: opts.displayName,
57
- region: opts.region,
58
- });
59
- const b = unwrap(res.data);
60
- if (opts.json) { printJson(b); return; }
61
- success(`Bucket created: ${b.id} (${b.name})`);
62
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
63
- });
64
-
65
- bk
66
- .command('delete <id>')
67
- .description('Delete a bucket (must be detached first; deletes all objects)')
68
- .option('--yes', 'Skip confirmation prompt')
69
- .action(async (id, opts) => {
70
- if (!opts.yes) {
71
- const { confirm } = await inquirer.prompt([
72
- { type: 'confirm', name: 'confirm', message: `Delete bucket "${id}" and ALL its objects?`, default: false },
73
- ]);
74
- if (!confirm) { console.log('Cancelled.'); return; }
75
- }
76
- try {
77
- await client.delete(`/api/buckets/${id}`);
78
- success(`Bucket ${id} deleted.`);
79
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
80
- });
81
-
82
- bk
83
- .command('attach <id> <deployment-id>')
84
- .description('Attach a bucket to a deployment (injects S3_* env vars on next deploy)')
85
- .action(async (id, deploymentId) => {
86
- try {
87
- await client.post(`/api/buckets/${id}/attach`, { deploymentId });
88
- success(`Attached bucket ${id} to deployment ${deploymentId}.`);
89
- console.log(" Run 'nexus deploy redeploy <id>' for env vars to take effect.");
90
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
91
- });
92
-
93
- bk
94
- .command('detach <id> <deployment-id>')
95
- .description('Detach a bucket from a deployment')
96
- .action(async (id, deploymentId) => {
97
- try {
98
- await client.post(`/api/buckets/${id}/detach`, { deploymentId });
99
- success(`Bucket ${id} detached from ${deploymentId}.`);
100
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
101
- });
102
-
103
- bk
104
- .command('credentials <id>')
105
- .description('Reveal the S3-compatible credentials for an external client (audit-logged)')
106
- .option('--json', 'Output raw JSON')
107
- .action(async (id, opts) => {
108
- try {
109
- const res = await client.get(`/api/buckets/${id}/credentials`);
110
- const c = unwrap(res.data);
111
- if (opts.json) { printJson(c); return; }
112
- console.log(`Endpoint: ${c.endpoint}`);
113
- console.log(`Region: ${c.region}`);
114
- console.log(`Bucket: ${c.bucket}`);
115
- console.log(`Access key: ${c.accessKey}`);
116
- console.log(`Secret key: ${c.secretKey}`);
117
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
118
- });
119
-
120
- bk
121
- .command('rotate-credentials <id>')
122
- .description('Rotate the per-bucket S3 access key (use to migrate legacy buckets to scoped IAM)')
123
- .option('--yes', 'Skip confirmation prompt')
124
- .action(async (id, opts) => {
125
- if (!opts.yes) {
126
- const { confirm } = await inquirer.prompt([
127
- {
128
- type: 'confirm',
129
- name: 'confirm',
130
- message: 'Rotate credentials? Attached deployments must be redeployed to pick up new S3_* env vars.',
131
- default: false,
132
- },
133
- ]);
134
- if (!confirm) { console.log('Cancelled.'); return; }
135
- }
136
- try {
137
- await client.post(`/api/buckets/${id}/rotate-credentials`);
138
- success(`Credentials rotated. Redeploy any attached deployments.`);
139
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
140
- });
141
-
142
- bk
143
- .command('refresh-usage <id>')
144
- .description('Refresh size and object count for a bucket')
145
- .option('--json', 'Output raw JSON')
146
- .action(async (id, opts) => {
147
- try {
148
- const res = await client.post(`/api/buckets/${id}/refresh-usage`);
149
- const b = unwrap(res.data);
150
- if (opts.json) { printJson(b); return; }
151
- success(`${b.name}: ${formatBytes(b.sizeBytes)} across ${b.objectCount} object(s)`);
152
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
153
- });
154
-
155
- // ---- file ops ------------------------------------------------------------
156
-
157
- bk
158
- .command('files <id>')
159
- .description('List files in a bucket')
160
- .option('--prefix <prefix>', 'Filter by key prefix')
161
- .option('--limit <n>', 'Max keys to return (default 1000)', '1000')
162
- .option('--json', 'Output raw JSON')
163
- .action(async (id, opts) => {
164
- try {
165
- const params: Record<string, string> = { limit: opts.limit };
166
- if (opts.prefix) params.prefix = opts.prefix;
167
- const res = await client.get(`/api/buckets/${id}/files`, { params });
168
- const data = unwrap(res.data);
169
- if (opts.json) { printJson(data); return; }
170
- if (!data.objects?.length) { console.log('Empty.'); return; }
171
- printTable(
172
- ['KEY', 'SIZE', 'MODIFIED'],
173
- data.objects.map((o: any) => [
174
- o.key,
175
- formatBytes(o.sizeBytes),
176
- o.lastModified ? timeAgo(o.lastModified) : '—',
177
- ])
178
- );
179
- if (data.truncated) console.log(`(truncated — pass --limit to see more)`);
180
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
181
- });
182
-
183
- bk
184
- .command('upload <id> <local-file>')
185
- .description('Upload a local file into the bucket')
186
- .option('--key <key>', 'Object key (default: filename of local file)')
187
- .action(async (id, localFile, opts) => {
188
- try {
189
- if (!fs.existsSync(localFile)) {
190
- errorMsg(`File not found: ${localFile}`);
191
- process.exit(1);
192
- }
193
- const key = opts.key || path.basename(localFile);
194
- const stat = fs.statSync(localFile);
195
- const stream = fs.createReadStream(localFile);
196
- await client.put(
197
- `/api/buckets/${id}/files/${encodeURIComponent(key)}`,
198
- stream,
199
- { headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': String(stat.size) } }
200
- );
201
- success(`Uploaded ${formatBytes(stat.size)} → ${key}`);
202
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
203
- });
204
-
205
- bk
206
- .command('download <id> <key>')
207
- .description('Download a file from the bucket')
208
- .option('--out <path>', 'Local output path (default: key basename)')
209
- .option('--share', 'Print a short-lived signed URL instead of downloading')
210
- .option('--ttl <seconds>', 'Lifetime for --share URL in seconds (30-3600, default 300)', '300')
211
- .option('--json', 'Output raw JSON (only with --share)')
212
- .action(async (id, key, opts) => {
213
- try {
214
- if (opts.share) {
215
- const res = await client.post(
216
- `/api/buckets/${id}/files/${encodeURIComponent(key)}/download-url`,
217
- { ttlSeconds: parseInt(opts.ttl, 10) || 300 }
218
- );
219
- const data = unwrap(res.data);
220
- if (opts.json) { printJson(data); return; }
221
- console.log(`URL: ${data.url}`);
222
- console.log(`Key: ${data.key}`);
223
- console.log(`Expires at: ${data.expiresAt}`);
224
- return;
225
- }
226
-
227
- const res = await client.get(
228
- `/api/buckets/${id}/files/${encodeURIComponent(key)}/download`,
229
- { responseType: 'stream' }
230
- );
231
- const outPath = path.resolve(opts.out || path.basename(key));
232
- const dir = path.dirname(outPath);
233
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
234
- await new Promise<void>((resolve, reject) => {
235
- const writer = fs.createWriteStream(outPath);
236
- res.data.pipe(writer);
237
- writer.on('finish', () => resolve());
238
- writer.on('error', reject);
239
- res.data.on('error', reject);
240
- });
241
- success(`Downloaded → ${outPath}`);
242
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
243
- });
244
-
245
- bk
246
- .command('rm <id> <key>')
247
- .description('Delete a file from the bucket')
248
- .option('--yes', 'Skip confirmation prompt')
249
- .action(async (id, key, opts) => {
250
- if (!opts.yes) {
251
- const { confirm } = await inquirer.prompt([
252
- { type: 'confirm', name: 'confirm', message: `Delete "${key}"?`, default: false },
253
- ]);
254
- if (!confirm) { console.log('Cancelled.'); return; }
255
- }
256
- try {
257
- await client.delete(`/api/buckets/${id}/files/${encodeURIComponent(key)}`);
258
- success(`Deleted ${key}`);
259
- } catch (err) { errorMsg(apiError(err)); process.exit(1); }
260
- });
261
- }