nexusapp-cli 2.1.1 → 2.2.2
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/dist/index.js +4 -0
- package/nexusapp-cli-2.0.0.tgz +0 -0
- package/package.json +8 -2
- package/src/commands/bucket.ts +261 -0
- package/src/commands/database.ts +35 -0
- package/src/commands/deploy.ts +12 -0
- package/src/commands/volume.ts +113 -0
- package/src/index.ts +4 -0
- package/dist/client.d.ts +0 -6
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -63
- package/dist/client.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -3
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js +0 -178
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/database.d.ts +0 -3
- package/dist/commands/database.d.ts.map +0 -1
- package/dist/commands/database.js +0 -312
- package/dist/commands/database.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -3
- package/dist/commands/deploy.d.ts.map +0 -1
- package/dist/commands/deploy.js +0 -868
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/domain.d.ts +0 -3
- package/dist/commands/domain.d.ts.map +0 -1
- package/dist/commands/domain.js +0 -174
- package/dist/commands/domain.js.map +0 -1
- package/dist/commands/member.d.ts +0 -3
- package/dist/commands/member.d.ts.map +0 -1
- package/dist/commands/member.js +0 -175
- package/dist/commands/member.js.map +0 -1
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -92
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/secret.d.ts +0 -3
- package/dist/commands/secret.d.ts.map +0 -1
- package/dist/commands/secret.js +0 -121
- package/dist/commands/secret.js.map +0 -1
- package/dist/commands/token.d.ts +0 -3
- package/dist/commands/token.d.ts.map +0 -1
- package/dist/commands/token.js +0 -179
- package/dist/commands/token.js.map +0 -1
- package/dist/config.d.ts +0 -10
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -53
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/output.d.ts +0 -9
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -71
- package/dist/output.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ const domain_js_1 = require("./commands/domain.js");
|
|
|
10
10
|
const token_js_1 = require("./commands/token.js");
|
|
11
11
|
const member_js_1 = require("./commands/member.js");
|
|
12
12
|
const database_js_1 = require("./commands/database.js");
|
|
13
|
+
const volume_js_1 = require("./commands/volume.js");
|
|
14
|
+
const bucket_js_1 = require("./commands/bucket.js");
|
|
13
15
|
const program = new commander_1.Command();
|
|
14
16
|
program
|
|
15
17
|
.name('nexus')
|
|
@@ -23,6 +25,8 @@ program
|
|
|
23
25
|
(0, token_js_1.registerToken)(program);
|
|
24
26
|
(0, member_js_1.registerMember)(program);
|
|
25
27
|
(0, database_js_1.registerDatabase)(program);
|
|
28
|
+
(0, volume_js_1.registerVolume)(program);
|
|
29
|
+
(0, bucket_js_1.registerBucket)(program);
|
|
26
30
|
program.parseAsync(process.argv).catch((err) => {
|
|
27
31
|
console.error(err.message || String(err));
|
|
28
32
|
process.exit(1);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexusapp-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "NEXUS AI command-line interface",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"dev": "ts-node src/index.ts",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"nexusai",
|
|
16
|
+
"cli",
|
|
17
|
+
"deployments",
|
|
18
|
+
"cloud"
|
|
19
|
+
],
|
|
15
20
|
"license": "MIT",
|
|
16
21
|
"dependencies": {
|
|
17
22
|
"axios": "^1.6.0",
|
|
@@ -19,6 +24,7 @@
|
|
|
19
24
|
"cli-table3": "^0.6.3",
|
|
20
25
|
"commander": "^12.0.0",
|
|
21
26
|
"inquirer": "^9.2.0",
|
|
27
|
+
"nexusapp-cli": "^2.1.1",
|
|
22
28
|
"ora": "^8.0.0"
|
|
23
29
|
},
|
|
24
30
|
"devDependencies": {
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
}
|
package/src/commands/database.ts
CHANGED
|
@@ -216,6 +216,41 @@ export function registerDatabase(program: Command): void {
|
|
|
216
216
|
}
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
// restore-to (cross-service restore)
|
|
220
|
+
db
|
|
221
|
+
.command('restore-to <target-service-id> <backup-id>')
|
|
222
|
+
.description('Restore a backup into a DIFFERENT deployment service in the same org (engines must match)')
|
|
223
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
224
|
+
.option('--json', 'Output raw JSON')
|
|
225
|
+
.action(async (targetServiceId, backupId, opts) => {
|
|
226
|
+
if (!opts.yes) {
|
|
227
|
+
const { confirm } = await inquirer.prompt([
|
|
228
|
+
{
|
|
229
|
+
type: 'confirm',
|
|
230
|
+
name: 'confirm',
|
|
231
|
+
message: `Restore backup ${backupId} into service ${targetServiceId}? Existing data on the target will be overwritten.`,
|
|
232
|
+
default: false,
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const spin = spinner('Restoring database into target service...');
|
|
239
|
+
try {
|
|
240
|
+
const res = await client.post(
|
|
241
|
+
`/api/deployment-services/${targetServiceId}/restore-from/${backupId}`
|
|
242
|
+
);
|
|
243
|
+
const result = unwrap(res.data);
|
|
244
|
+
spin.stop();
|
|
245
|
+
if (opts.json) { printJson(result); return; }
|
|
246
|
+
success('Database restored successfully into target service.');
|
|
247
|
+
} catch (err) {
|
|
248
|
+
spin.stop();
|
|
249
|
+
errorMsg(apiError(err));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
219
254
|
// backup-delete
|
|
220
255
|
db
|
|
221
256
|
.command('backup-delete <service-id> <backup-id>')
|
package/src/commands/deploy.ts
CHANGED
|
@@ -223,6 +223,8 @@ export function registerDeploy(program: Command): void {
|
|
|
223
223
|
.option('--environment <env>', 'Deployment environment (DEVELOPMENT|STAGING|PRODUCTION)', 'DEVELOPMENT')
|
|
224
224
|
.option('--auto-destroy <hours>', 'Auto-destroy after N hours', parseInt)
|
|
225
225
|
.option('--services <types>', 'Comma-separated database services to provision (e.g. postgresql,redis)')
|
|
226
|
+
.option('--worker-command <cmd>', 'Run a background worker sidecar with this command')
|
|
227
|
+
.option('--worker-name <name>', 'Worker service name', 'worker')
|
|
226
228
|
.option('--no-health-check', 'Disable health checks for this deployment')
|
|
227
229
|
.option('--wait', 'Wait until deployment is RUNNING or FAILED')
|
|
228
230
|
.option('--json', 'Output raw JSON')
|
|
@@ -252,6 +254,16 @@ export function registerDeploy(program: Command): void {
|
|
|
252
254
|
if (opts.services) {
|
|
253
255
|
payload.services = opts.services.split(',').map((s: string) => ({ type: s.trim() }));
|
|
254
256
|
}
|
|
257
|
+
if (opts.workerCommand) {
|
|
258
|
+
payload.services = [
|
|
259
|
+
...(payload.services || []),
|
|
260
|
+
{
|
|
261
|
+
type: 'worker',
|
|
262
|
+
displayName: opts.workerName || 'worker',
|
|
263
|
+
command: opts.workerCommand,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
}
|
|
255
267
|
if (Object.keys(envVars).length) payload.envVars = envVars;
|
|
256
268
|
|
|
257
269
|
try {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { client, apiError, unwrap } from '../client.js';
|
|
4
|
+
import { printTable, printJson, success, errorMsg, timeAgo } from '../output.js';
|
|
5
|
+
|
|
6
|
+
function formatBytes(bytes: number | bigint): string {
|
|
7
|
+
const n = typeof bytes === 'bigint' ? Number(bytes) : bytes;
|
|
8
|
+
if (n < 1024) return `${n} B`;
|
|
9
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
10
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
11
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerVolume(program: Command): void {
|
|
15
|
+
const vol = program
|
|
16
|
+
.command('volume')
|
|
17
|
+
.description('Persistent storage volumes (filesystem mounts attached to deployments)');
|
|
18
|
+
|
|
19
|
+
vol
|
|
20
|
+
.command('list')
|
|
21
|
+
.description('List volumes')
|
|
22
|
+
.option('--json', 'Output raw JSON')
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
try {
|
|
25
|
+
const res = await client.get('/api/volumes');
|
|
26
|
+
const volumes: any[] = unwrap(res.data) || [];
|
|
27
|
+
if (opts.json) { printJson(volumes); return; }
|
|
28
|
+
if (!volumes.length) { console.log('No volumes found.'); return; }
|
|
29
|
+
|
|
30
|
+
printTable(
|
|
31
|
+
['ID', 'NAME', 'SIZE', 'STATUS', 'ATTACHED TO', 'CREATED'],
|
|
32
|
+
volumes.map((v: any) => [
|
|
33
|
+
v.id,
|
|
34
|
+
v.displayName ? `${v.name} (${v.displayName})` : v.name,
|
|
35
|
+
formatBytes(v.sizeBytes),
|
|
36
|
+
v.status,
|
|
37
|
+
v.attachment ? `${v.attachment.deploymentName}:${v.attachment.mountPath}` : '—',
|
|
38
|
+
v.createdAt ? timeAgo(v.createdAt) : '—',
|
|
39
|
+
])
|
|
40
|
+
);
|
|
41
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
vol
|
|
45
|
+
.command('create <name>')
|
|
46
|
+
.description('Create a new volume (org-scoped)')
|
|
47
|
+
.option('--display-name <name>', 'Friendly display name')
|
|
48
|
+
.option('--json', 'Output raw JSON')
|
|
49
|
+
.action(async (name, opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const res = await client.post('/api/volumes', { name, displayName: opts.displayName });
|
|
52
|
+
const v = unwrap(res.data);
|
|
53
|
+
if (opts.json) { printJson(v); return; }
|
|
54
|
+
success(`Volume created: ${v.id} (${v.name})`);
|
|
55
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
vol
|
|
59
|
+
.command('delete <id>')
|
|
60
|
+
.description('Delete a volume (must be detached first)')
|
|
61
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
62
|
+
.action(async (id, opts) => {
|
|
63
|
+
if (!opts.yes) {
|
|
64
|
+
const { confirm } = await inquirer.prompt([
|
|
65
|
+
{ type: 'confirm', name: 'confirm', message: `Delete volume "${id}"? Data is destroyed.`, default: false },
|
|
66
|
+
]);
|
|
67
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await client.delete(`/api/volumes/${id}`);
|
|
71
|
+
success(`Volume ${id} deleted.`);
|
|
72
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vol
|
|
76
|
+
.command('attach <id> <deployment-id>')
|
|
77
|
+
.description('Attach a volume to a deployment (requires redeploy to take effect)')
|
|
78
|
+
.option('--mount <path>', 'Mount path inside the container', '/data')
|
|
79
|
+
.action(async (id, deploymentId, opts) => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await client.post(`/api/volumes/${id}/attach`, {
|
|
82
|
+
deploymentId,
|
|
83
|
+
mountPath: opts.mount,
|
|
84
|
+
});
|
|
85
|
+
const dv = unwrap(res.data);
|
|
86
|
+
success(`Attached volume ${id} to deployment ${deploymentId} at ${dv.mountPath}.`);
|
|
87
|
+
console.log(" Run 'nexus deploy redeploy <id>' to mount it.");
|
|
88
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
vol
|
|
92
|
+
.command('detach <id>')
|
|
93
|
+
.description('Detach a volume from its deployment')
|
|
94
|
+
.action(async (id) => {
|
|
95
|
+
try {
|
|
96
|
+
await client.post(`/api/volumes/${id}/detach`);
|
|
97
|
+
success(`Volume ${id} detached.`);
|
|
98
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
vol
|
|
102
|
+
.command('refresh-usage <id>')
|
|
103
|
+
.description('Refresh on-disk usage for a volume')
|
|
104
|
+
.option('--json', 'Output raw JSON')
|
|
105
|
+
.action(async (id, opts) => {
|
|
106
|
+
try {
|
|
107
|
+
const res = await client.post(`/api/volumes/${id}/refresh-usage`);
|
|
108
|
+
const v = unwrap(res.data);
|
|
109
|
+
if (opts.json) { printJson(v); return; }
|
|
110
|
+
success(`${v.name}: ${formatBytes(v.sizeBytes)}`);
|
|
111
|
+
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { registerDomain } from './commands/domain.js';
|
|
|
8
8
|
import { registerToken } from './commands/token.js';
|
|
9
9
|
import { registerMember } from './commands/member.js';
|
|
10
10
|
import { registerDatabase } from './commands/database.js';
|
|
11
|
+
import { registerVolume } from './commands/volume.js';
|
|
12
|
+
import { registerBucket } from './commands/bucket.js';
|
|
11
13
|
|
|
12
14
|
const program = new Command();
|
|
13
15
|
|
|
@@ -24,6 +26,8 @@ registerDomain(program);
|
|
|
24
26
|
registerToken(program);
|
|
25
27
|
registerMember(program);
|
|
26
28
|
registerDatabase(program);
|
|
29
|
+
registerVolume(program);
|
|
30
|
+
registerBucket(program);
|
|
27
31
|
|
|
28
32
|
program.parseAsync(process.argv).catch((err) => {
|
|
29
33
|
console.error(err.message || String(err));
|
package/dist/client.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { AxiosInstance } from 'axios';
|
|
2
|
-
export declare const client: AxiosInstance;
|
|
3
|
-
/** Unwrap { success, data } envelope if present, otherwise return as-is. */
|
|
4
|
-
export declare function unwrap(responseData: any): any;
|
|
5
|
-
export declare function apiError(error: unknown): string;
|
|
6
|
-
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAc,MAAM,OAAO,CAAC;AAkDzD,eAAO,MAAM,MAAM,eAAiB,CAAC;AAErC,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,YAAY,EAAE,GAAG,GAAG,GAAG,CAK7C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAO/C"}
|
package/dist/client.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
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.client = void 0;
|
|
7
|
-
exports.unwrap = unwrap;
|
|
8
|
-
exports.apiError = apiError;
|
|
9
|
-
const axios_1 = __importDefault(require("axios"));
|
|
10
|
-
const config_js_1 = require("./config.js");
|
|
11
|
-
function createClient() {
|
|
12
|
-
const config = (0, config_js_1.getConfig)();
|
|
13
|
-
const baseURL = config.apiUrl || 'https://nexusai.run';
|
|
14
|
-
const instance = axios_1.default.create({
|
|
15
|
-
baseURL,
|
|
16
|
-
timeout: 30000,
|
|
17
|
-
});
|
|
18
|
-
instance.interceptors.request.use((req) => {
|
|
19
|
-
const cfg = (0, config_js_1.getConfig)();
|
|
20
|
-
if (cfg.token) {
|
|
21
|
-
req.headers = req.headers || {};
|
|
22
|
-
req.headers['Authorization'] = `Bearer ${cfg.token}`;
|
|
23
|
-
}
|
|
24
|
-
return req;
|
|
25
|
-
});
|
|
26
|
-
instance.interceptors.response.use((res) => res, (error) => {
|
|
27
|
-
if (!error.response) {
|
|
28
|
-
const url = baseURL;
|
|
29
|
-
console.error(`Cannot reach NEXUS AI API at ${url}.`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
const status = error.response.status;
|
|
33
|
-
const data = error.response.data;
|
|
34
|
-
if (status === 401) {
|
|
35
|
-
console.error("Session expired. Run 'nexus auth login'");
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
if (status === 403) {
|
|
39
|
-
console.error(data?.message || data?.error || 'Access denied.');
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
return Promise.reject(error);
|
|
43
|
-
});
|
|
44
|
-
return instance;
|
|
45
|
-
}
|
|
46
|
-
exports.client = createClient();
|
|
47
|
-
/** Unwrap { success, data } envelope if present, otherwise return as-is. */
|
|
48
|
-
function unwrap(responseData) {
|
|
49
|
-
if (responseData && typeof responseData === 'object' && 'data' in responseData) {
|
|
50
|
-
return responseData.data;
|
|
51
|
-
}
|
|
52
|
-
return responseData;
|
|
53
|
-
}
|
|
54
|
-
function apiError(error) {
|
|
55
|
-
if (axios_1.default.isAxiosError(error)) {
|
|
56
|
-
const data = error.response?.data;
|
|
57
|
-
return data?.message || data?.error || error.message;
|
|
58
|
-
}
|
|
59
|
-
if (error instanceof Error)
|
|
60
|
-
return error.message;
|
|
61
|
-
return String(error);
|
|
62
|
-
}
|
|
63
|
-
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;;;;AAqDA,wBAKC;AAED,4BAOC;AAnED,kDAAyD;AACzD,2CAAwC;AAExC,SAAS,YAAY;IACnB,MAAM,MAAM,GAAG,IAAA,qBAAS,GAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,qBAAqB,CAAC;IAEvD,MAAM,QAAQ,GAAG,eAAK,CAAC,MAAM,CAAC;QAC5B,OAAO;QACP,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACxC,MAAM,GAAG,GAAG,IAAA,qBAAS,GAAE,CAAC;QACxB,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;YAChC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,GAAG,CAAC,KAAK,EAAE,CAAC;QACvD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAChC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EACZ,CAAC,KAAiB,EAAE,EAAE;QACpB,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpB,MAAM,GAAG,GAAG,OAAO,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,gCAAgC,GAAG,GAAG,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAW,CAAC;QAExC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,IAAI,IAAI,EAAE,KAAK,IAAI,gBAAgB,CAAC,CAAC;YAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC,CACF,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAEY,QAAA,MAAM,GAAG,YAAY,EAAE,CAAC;AAErC,4EAA4E;AAC5E,SAAgB,MAAM,CAAC,YAAiB;IACtC,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,MAAM,IAAI,YAAY,EAAE,CAAC;QAC/E,OAAO,YAAY,CAAC,IAAI,CAAC;IAC3B,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAgB,QAAQ,CAAC,KAAc;IACrC,IAAI,eAAK,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAW,CAAC;QACzC,OAAO,IAAI,EAAE,OAAO,IAAI,IAAI,EAAE,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;IACvD,CAAC;IACD,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IACjD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/commands/auth.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyEpC,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgHnD"}
|