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.
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +178 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/bucket.d.ts +3 -0
- package/dist/commands/bucket.d.ts.map +1 -0
- package/dist/commands/bucket.js +354 -0
- package/dist/commands/bucket.js.map +1 -0
- package/dist/commands/database.d.ts +3 -0
- package/dist/commands/database.d.ts.map +1 -0
- package/dist/commands/database.js +350 -0
- package/dist/commands/database.js.map +1 -0
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +1009 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/domain.d.ts +3 -0
- package/dist/commands/domain.d.ts.map +1 -0
- package/dist/commands/domain.js +174 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/exec.d.ts +3 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +176 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/managedDb.d.ts +3 -0
- package/dist/commands/managedDb.d.ts.map +1 -0
- package/dist/commands/managedDb.js +227 -0
- package/dist/commands/managedDb.js.map +1 -0
- package/dist/commands/member.d.ts +3 -0
- package/dist/commands/member.d.ts.map +1 -0
- package/dist/commands/member.js +175 -0
- package/dist/commands/member.js.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +92 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +121 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/commands/token.d.ts +3 -0
- package/dist/commands/token.d.ts.map +1 -0
- package/dist/commands/token.js +179 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/volume.d.ts +3 -0
- package/dist/commands/volume.d.ts.map +1 -0
- package/dist/commands/volume.js +149 -0
- package/dist/commands/volume.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +9 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +71 -0
- package/dist/output.js.map +1 -0
- package/package.json +6 -2
- package/src/client.ts +0 -68
- package/src/commands/auth.ts +0 -186
- package/src/commands/bucket.ts +0 -261
- package/src/commands/database.ts +0 -305
- package/src/commands/deploy.ts +0 -904
- package/src/commands/domain.ts +0 -167
- package/src/commands/exec.ts +0 -154
- package/src/commands/managedDb.ts +0 -170
- package/src/commands/member.ts +0 -168
- package/src/commands/project.ts +0 -81
- package/src/commands/secret.ts +0 -117
- package/src/commands/token.ts +0 -173
- package/src/commands/volume.ts +0 -113
- package/src/config.ts +0 -56
- package/src/index.ts +0 -39
- package/src/output.ts +0 -65
- package/tsconfig.json +0 -19
package/src/commands/domain.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
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
|
-
import chalk from 'chalk';
|
|
6
|
-
|
|
7
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
-
|
|
9
|
-
async function resolveDeployment(nameOrId: string): Promise<any> {
|
|
10
|
-
if (UUID_RE.test(nameOrId)) {
|
|
11
|
-
try {
|
|
12
|
-
const res = await client.get(`/api/deployments/${nameOrId}`);
|
|
13
|
-
return unwrap(res.data);
|
|
14
|
-
} catch { /* fall through */ }
|
|
15
|
-
}
|
|
16
|
-
const listRes = await client.get('/api/deployments');
|
|
17
|
-
const all: any[] = Array.isArray(unwrap(listRes.data)) ? unwrap(listRes.data) : [];
|
|
18
|
-
const match = all.find((d) => d.name === nameOrId || d.displayName === nameOrId);
|
|
19
|
-
if (!match) throw new Error(`Deployment not found: "${nameOrId}"`);
|
|
20
|
-
const res = await client.get(`/api/deployments/${match.id}`);
|
|
21
|
-
return unwrap(res.data);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function verificationBadge(status: string): string {
|
|
25
|
-
switch ((status || '').toUpperCase()) {
|
|
26
|
-
case 'VERIFIED': return chalk.green('VERIFIED');
|
|
27
|
-
case 'PENDING': return chalk.yellow('PENDING');
|
|
28
|
-
case 'FAILED': return chalk.red('FAILED');
|
|
29
|
-
default: return chalk.gray(status || 'UNKNOWN');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function registerDomain(program: Command): void {
|
|
34
|
-
const domain = program.command('domain').description('Custom domain management');
|
|
35
|
-
|
|
36
|
-
// list
|
|
37
|
-
domain
|
|
38
|
-
.command('list <deployment>')
|
|
39
|
-
.description('List custom domains for a deployment')
|
|
40
|
-
.option('--json', 'Output raw JSON')
|
|
41
|
-
.action(async (nameOrId, opts) => {
|
|
42
|
-
try {
|
|
43
|
-
const d = await resolveDeployment(nameOrId);
|
|
44
|
-
const res = await client.get(`/api/deployments/${d.id}/domains`);
|
|
45
|
-
const domains: any[] = unwrap(res.data) || [];
|
|
46
|
-
|
|
47
|
-
if (opts.json) { printJson(domains); return; }
|
|
48
|
-
if (!domains.length) {
|
|
49
|
-
console.log(`No custom domains on "${d.displayName || d.name}".`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
printTable(
|
|
54
|
-
['ID', 'DOMAIN', 'STATUS', 'SSL', 'ADDED'],
|
|
55
|
-
domains.map((dom: any) => [
|
|
56
|
-
dom.id,
|
|
57
|
-
dom.domain,
|
|
58
|
-
verificationBadge(dom.verificationStatus),
|
|
59
|
-
dom.sslStatus ? chalk.green('✓') : chalk.gray('—'),
|
|
60
|
-
dom.createdAt ? timeAgo(dom.createdAt) : '—',
|
|
61
|
-
])
|
|
62
|
-
);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
errorMsg(apiError(err));
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// add
|
|
70
|
-
domain
|
|
71
|
-
.command('add <deployment> <domain>')
|
|
72
|
-
.description('Add a custom domain to a deployment')
|
|
73
|
-
.option('--json', 'Output raw JSON')
|
|
74
|
-
.action(async (nameOrId, domainName, opts) => {
|
|
75
|
-
try {
|
|
76
|
-
const d = await resolveDeployment(nameOrId);
|
|
77
|
-
const res = await client.post(`/api/deployments/${d.id}/domains`, { domain: domainName });
|
|
78
|
-
const dom = unwrap(res.data);
|
|
79
|
-
|
|
80
|
-
if (opts.json) { printJson(dom); return; }
|
|
81
|
-
|
|
82
|
-
success(`Domain "${dom.domain}" added to "${d.displayName || d.name}"`);
|
|
83
|
-
console.log('');
|
|
84
|
-
console.log(' Next steps to verify ownership:');
|
|
85
|
-
if (dom.txtRecord || dom.verificationToken) {
|
|
86
|
-
console.log(` 1. Add a DNS TXT record:`);
|
|
87
|
-
console.log(` Name: ${chalk.cyan(dom.txtRecordName || `_nexusai-verify.${domainName}`)}`);
|
|
88
|
-
console.log(` Value: ${chalk.cyan(dom.txtRecord || dom.verificationToken)}`);
|
|
89
|
-
} else {
|
|
90
|
-
console.log(` 1. Point your DNS to the deployment URL`);
|
|
91
|
-
}
|
|
92
|
-
console.log(` 2. Run: ${chalk.bold(`nexus domain verify ${nameOrId} ${dom.id}`)}`);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
errorMsg(apiError(err));
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// verify
|
|
100
|
-
domain
|
|
101
|
-
.command('verify <deployment> <domain-id>')
|
|
102
|
-
.description('Trigger DNS verification for a custom domain')
|
|
103
|
-
.option('--json', 'Output raw JSON')
|
|
104
|
-
.action(async (nameOrId, domainId, opts) => {
|
|
105
|
-
try {
|
|
106
|
-
const d = await resolveDeployment(nameOrId);
|
|
107
|
-
const res = await client.post(`/api/deployments/${d.id}/domains/${domainId}/verify`);
|
|
108
|
-
const result = unwrap(res.data);
|
|
109
|
-
|
|
110
|
-
if (opts.json) { printJson(result); return; }
|
|
111
|
-
|
|
112
|
-
const dom = result.domain || result;
|
|
113
|
-
const status = (dom.verificationStatus || '').toUpperCase();
|
|
114
|
-
|
|
115
|
-
if (status === 'VERIFIED') {
|
|
116
|
-
success(`Domain "${dom.domain}" verified successfully`);
|
|
117
|
-
if (dom.sslStatus) {
|
|
118
|
-
console.log(` SSL: ${chalk.green('active')}`);
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
console.log(` Status: ${verificationBadge(status)}`);
|
|
122
|
-
console.log('');
|
|
123
|
-
if (result.verificationResult?.error) {
|
|
124
|
-
console.log(` ${chalk.yellow('!')} ${result.verificationResult.error}`);
|
|
125
|
-
}
|
|
126
|
-
console.log(` DNS changes can take up to 48h to propagate.`);
|
|
127
|
-
console.log(` Run this command again once DNS has updated.`);
|
|
128
|
-
}
|
|
129
|
-
} catch (err) {
|
|
130
|
-
errorMsg(apiError(err));
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// remove
|
|
136
|
-
domain
|
|
137
|
-
.command('remove <deployment> <domain-id>')
|
|
138
|
-
.description('Remove a custom domain from a deployment')
|
|
139
|
-
.option('--yes', 'Skip confirmation prompt')
|
|
140
|
-
.action(async (nameOrId, domainId, opts) => {
|
|
141
|
-
try {
|
|
142
|
-
const d = await resolveDeployment(nameOrId);
|
|
143
|
-
|
|
144
|
-
if (!opts.yes) {
|
|
145
|
-
// Show the domain name in the prompt
|
|
146
|
-
let domainName = domainId;
|
|
147
|
-
try {
|
|
148
|
-
const listRes = await client.get(`/api/deployments/${d.id}/domains`);
|
|
149
|
-
const domains: any[] = unwrap(listRes.data) || [];
|
|
150
|
-
const found = domains.find((dom) => dom.id === domainId);
|
|
151
|
-
if (found) domainName = found.domain;
|
|
152
|
-
} catch { /* use id */ }
|
|
153
|
-
|
|
154
|
-
const { confirm } = await inquirer.prompt([
|
|
155
|
-
{ type: 'confirm', name: 'confirm', message: `Remove domain "${domainName}" from "${d.displayName || d.name}"?`, default: false },
|
|
156
|
-
]);
|
|
157
|
-
if (!confirm) { console.log('Cancelled.'); return; }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
await client.delete(`/api/deployments/${d.id}/domains/${domainId}`);
|
|
161
|
-
success(`Domain removed.`);
|
|
162
|
-
} catch (err) {
|
|
163
|
-
errorMsg(apiError(err));
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
package/src/commands/exec.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { client, apiError, unwrap } from '../client.js';
|
|
5
|
-
import { success, errorMsg, printJson } from '../output.js';
|
|
6
|
-
|
|
7
|
-
function formatBytes(n: number): string {
|
|
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
|
-
/**
|
|
15
|
-
* Parse a "<deploymentId>:<path>" argument. Returns null if the arg has no
|
|
16
|
-
* colon (i.e. it's a local path). UUIDs contain hyphens but no colons, so the
|
|
17
|
-
* first ":" cleanly separates the two halves.
|
|
18
|
-
*/
|
|
19
|
-
function parseRemoteRef(arg: string): { deploymentId: string; remotePath: string } | null {
|
|
20
|
-
const idx = arg.indexOf(':');
|
|
21
|
-
if (idx < 0) return null;
|
|
22
|
-
const deploymentId = arg.slice(0, idx);
|
|
23
|
-
const remotePath = arg.slice(idx + 1);
|
|
24
|
-
if (!deploymentId || !remotePath) return null;
|
|
25
|
-
return { deploymentId, remotePath };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function registerExec(program: Command): void {
|
|
29
|
-
program
|
|
30
|
-
.command('exec <deploymentId> [command...]')
|
|
31
|
-
.description('Run a command inside the running deployment container (LOCAL_DOCKER only)')
|
|
32
|
-
.option('--timeout <seconds>', 'Max seconds before the command is killed (default 60, max 1800)', (v) => parseInt(v, 10))
|
|
33
|
-
.option('--workdir <path>', 'Working directory inside the container')
|
|
34
|
-
.option('--json', 'Output raw JSON result')
|
|
35
|
-
.allowUnknownOption(false)
|
|
36
|
-
.action(async (deploymentId, commandParts, opts) => {
|
|
37
|
-
if (!commandParts || commandParts.length === 0) {
|
|
38
|
-
errorMsg('Provide a command to run, e.g. `nexus exec <id> ls -la /app`');
|
|
39
|
-
process.exit(2);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const res = await client.post(
|
|
44
|
-
`/api/deployments/${deploymentId}/exec`,
|
|
45
|
-
{
|
|
46
|
-
command: commandParts,
|
|
47
|
-
timeoutSeconds: opts.timeout,
|
|
48
|
-
workingDir: opts.workdir,
|
|
49
|
-
},
|
|
50
|
-
{ timeout: 0 }
|
|
51
|
-
);
|
|
52
|
-
const result = unwrap(res.data);
|
|
53
|
-
|
|
54
|
-
if (opts.json) {
|
|
55
|
-
printJson(result);
|
|
56
|
-
process.exit(result.exitCode === 0 ? 0 : 1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
60
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
61
|
-
if (result.truncated) {
|
|
62
|
-
process.stderr.write('\n[output truncated at 2MB per stream]\n');
|
|
63
|
-
}
|
|
64
|
-
process.exit(result.exitCode === 0 ? 0 : result.exitCode || 1);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
errorMsg(apiError(err));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
program
|
|
72
|
-
.command('cp <source> <destination>')
|
|
73
|
-
.description(
|
|
74
|
-
'Copy a single file between a local path and a running deployment container.\n' +
|
|
75
|
-
' Upload: nexus cp ./index.html <deploymentId>:/app/public/index.html\n' +
|
|
76
|
-
' Download: nexus cp <deploymentId>:/app/config.json ./config.json'
|
|
77
|
-
)
|
|
78
|
-
.action(async (source, destination) => {
|
|
79
|
-
const srcRemote = parseRemoteRef(source);
|
|
80
|
-
const dstRemote = parseRemoteRef(destination);
|
|
81
|
-
|
|
82
|
-
if (srcRemote && dstRemote) {
|
|
83
|
-
errorMsg('Container-to-container copy is not supported. Download first, then upload.');
|
|
84
|
-
process.exit(2);
|
|
85
|
-
}
|
|
86
|
-
if (!srcRemote && !dstRemote) {
|
|
87
|
-
errorMsg('At least one side must be a container reference (e.g. `<deploymentId>:/app/file.txt`).');
|
|
88
|
-
process.exit(2);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
if (!srcRemote && dstRemote) {
|
|
93
|
-
// Upload: local -> container
|
|
94
|
-
const localPath = path.resolve(source);
|
|
95
|
-
if (!fs.existsSync(localPath)) {
|
|
96
|
-
errorMsg(`Local file not found: ${localPath}`);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
const stat = fs.statSync(localPath);
|
|
100
|
-
if (!stat.isFile()) {
|
|
101
|
-
errorMsg(`Not a regular file: ${localPath} (directory uploads not supported — tar + exec instead)`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
const stream = fs.createReadStream(localPath);
|
|
105
|
-
const res = await client.post(
|
|
106
|
-
`/api/deployments/${dstRemote.deploymentId}/files`,
|
|
107
|
-
stream,
|
|
108
|
-
{
|
|
109
|
-
params: { path: dstRemote.remotePath },
|
|
110
|
-
headers: {
|
|
111
|
-
'Content-Type': 'application/octet-stream',
|
|
112
|
-
'Content-Length': String(stat.size),
|
|
113
|
-
},
|
|
114
|
-
timeout: 0,
|
|
115
|
-
maxBodyLength: Infinity,
|
|
116
|
-
maxContentLength: Infinity,
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
const data = unwrap(res.data);
|
|
120
|
-
success(`Uploaded ${formatBytes(data.bytesWritten || stat.size)} → ${dstRemote.deploymentId}:${data.targetPath || dstRemote.remotePath}`);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Download: container -> local
|
|
125
|
-
const remote = srcRemote!;
|
|
126
|
-
const localPath = path.resolve(destination);
|
|
127
|
-
const dir = path.dirname(localPath);
|
|
128
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
129
|
-
|
|
130
|
-
const res = await client.get(`/api/deployments/${remote.deploymentId}/files`, {
|
|
131
|
-
params: { path: remote.remotePath },
|
|
132
|
-
responseType: 'stream',
|
|
133
|
-
timeout: 0,
|
|
134
|
-
maxContentLength: Infinity,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
let bytes = 0;
|
|
138
|
-
res.data.on('data', (chunk: Buffer) => { bytes += chunk.length; });
|
|
139
|
-
|
|
140
|
-
await new Promise<void>((resolve, reject) => {
|
|
141
|
-
const writer = fs.createWriteStream(localPath);
|
|
142
|
-
res.data.pipe(writer);
|
|
143
|
-
writer.on('finish', () => resolve());
|
|
144
|
-
writer.on('error', reject);
|
|
145
|
-
res.data.on('error', reject);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
success(`Downloaded ${formatBytes(bytes)} → ${localPath}`);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
errorMsg(apiError(err));
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { client, apiError } from '../client.js';
|
|
3
|
-
import { printTable, printJson, success, errorMsg, timeAgo } from '../output.js';
|
|
4
|
-
|
|
5
|
-
export function registerManagedDb(program: Command): void {
|
|
6
|
-
const md = program
|
|
7
|
-
.command('managed-db')
|
|
8
|
-
.description('Managed cloud databases (AWS RDS instances)');
|
|
9
|
-
|
|
10
|
-
md
|
|
11
|
-
.command('list')
|
|
12
|
-
.description('List managed databases')
|
|
13
|
-
.option('--json', 'Output raw JSON')
|
|
14
|
-
.action(async (opts) => {
|
|
15
|
-
try {
|
|
16
|
-
const res = await client.get('/api/managed-databases');
|
|
17
|
-
const dbs: any[] = res.data?.managedDatabases || [];
|
|
18
|
-
if (opts.json) { printJson(dbs); return; }
|
|
19
|
-
if (!dbs.length) { console.log('No managed databases found.'); return; }
|
|
20
|
-
printTable(
|
|
21
|
-
['ID', 'NAME', 'ENGINE', 'STATUS', 'ENDPOINT', 'CREATED'],
|
|
22
|
-
dbs.map((d: any) => [
|
|
23
|
-
d.id,
|
|
24
|
-
d.displayName ? `${d.name} (${d.displayName})` : d.name,
|
|
25
|
-
`${d.engine} ${d.engineVersion}`,
|
|
26
|
-
d.status,
|
|
27
|
-
d.endpointHost ? `${d.endpointHost}:${d.endpointPort}` : '—',
|
|
28
|
-
d.createdAt ? timeAgo(d.createdAt) : '—',
|
|
29
|
-
])
|
|
30
|
-
);
|
|
31
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
md
|
|
35
|
-
.command('create <name>')
|
|
36
|
-
.description('Provision a new managed database')
|
|
37
|
-
.requiredOption('--engine <engine>', 'POSTGRES or MYSQL')
|
|
38
|
-
.requiredOption('--engine-version <version>', 'e.g. 17.10 (postgres), 8.0.46 (mysql)')
|
|
39
|
-
.option('--provider <provider>', 'AWS_RDS, GCP_CLOUD_SQL, or AZURE_DATABASE', 'AWS_RDS')
|
|
40
|
-
.option('--network-mode <mode>', 'public or vpc (AWS only; vpc = private, App Runner-only)')
|
|
41
|
-
.option('--display-name <name>', 'Friendly display name')
|
|
42
|
-
.option('--instance-class <class>', 'e.g. db.t3.micro (AWS) / db-custom-1-3840 (GCP)')
|
|
43
|
-
.option('--allocated-gb <gb>', 'Storage in GB (20-1000)')
|
|
44
|
-
.option('--region <region>', 'Cloud region')
|
|
45
|
-
.option('--json', 'Output raw JSON')
|
|
46
|
-
.action(async (name, opts) => {
|
|
47
|
-
try {
|
|
48
|
-
const res = await client.post('/api/managed-databases', {
|
|
49
|
-
name,
|
|
50
|
-
provider: opts.provider,
|
|
51
|
-
networkMode: opts.networkMode,
|
|
52
|
-
engine: opts.engine,
|
|
53
|
-
engineVersion: opts.engineVersion,
|
|
54
|
-
displayName: opts.displayName,
|
|
55
|
-
instanceClass: opts.instanceClass,
|
|
56
|
-
allocatedGb: opts.allocatedGb ? Number(opts.allocatedGb) : undefined,
|
|
57
|
-
region: opts.region,
|
|
58
|
-
});
|
|
59
|
-
const db = res.data?.managedDatabase;
|
|
60
|
-
if (opts.json) { printJson(db); return; }
|
|
61
|
-
success(`Managed database created: ${db?.id} (${db?.status})`);
|
|
62
|
-
console.log('Provisioning is async — run `nexus managed-db list` until status is AVAILABLE.');
|
|
63
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
md
|
|
67
|
-
.command('delete <id>')
|
|
68
|
-
.description('Delete a managed database (destroys data)')
|
|
69
|
-
.option('--json', 'Output raw JSON')
|
|
70
|
-
.action(async (id, opts) => {
|
|
71
|
-
try {
|
|
72
|
-
await client.delete(`/api/managed-databases/${id}`);
|
|
73
|
-
if (opts.json) { printJson({ success: true }); return; }
|
|
74
|
-
success(`Managed database ${id} deleted.`);
|
|
75
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
md
|
|
79
|
-
.command('attach <id>')
|
|
80
|
-
.description('Attach a managed database to a deployment')
|
|
81
|
-
.requiredOption('--deployment <deploymentId>', 'Deployment ID')
|
|
82
|
-
.option('--env-prefix <prefix>', 'Env var namespace (default DATABASE)')
|
|
83
|
-
.option('--json', 'Output raw JSON')
|
|
84
|
-
.action(async (id, opts) => {
|
|
85
|
-
try {
|
|
86
|
-
const res = await client.post(`/api/managed-databases/${id}/attach`, {
|
|
87
|
-
deploymentId: opts.deployment,
|
|
88
|
-
envPrefix: opts.envPrefix,
|
|
89
|
-
});
|
|
90
|
-
if (opts.json) { printJson(res.data?.attachment); return; }
|
|
91
|
-
success(`Attached to ${opts.deployment}. Redeploy for DATABASE_* env vars to take effect.`);
|
|
92
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
md
|
|
96
|
-
.command('detach <id>')
|
|
97
|
-
.description('Detach a managed database from a deployment')
|
|
98
|
-
.requiredOption('--deployment <deploymentId>', 'Deployment ID')
|
|
99
|
-
.option('--json', 'Output raw JSON')
|
|
100
|
-
.action(async (id, opts) => {
|
|
101
|
-
try {
|
|
102
|
-
await client.post(`/api/managed-databases/${id}/detach`, { deploymentId: opts.deployment });
|
|
103
|
-
if (opts.json) { printJson({ success: true }); return; }
|
|
104
|
-
success(`Detached from ${opts.deployment}.`);
|
|
105
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
md
|
|
109
|
-
.command('snapshot <id>')
|
|
110
|
-
.description('Create a snapshot backup of a managed database')
|
|
111
|
-
.option('--notes <notes>', 'Optional notes')
|
|
112
|
-
.option('--json', 'Output raw JSON')
|
|
113
|
-
.action(async (id, opts) => {
|
|
114
|
-
try {
|
|
115
|
-
const res = await client.post(`/api/managed-databases/${id}/snapshots`, { notes: opts.notes });
|
|
116
|
-
const snap = res.data?.snapshot;
|
|
117
|
-
if (opts.json) { printJson(snap); return; }
|
|
118
|
-
success(`Snapshot ${snap?.id} started (${snap?.status}).`);
|
|
119
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
md
|
|
123
|
-
.command('snapshots <id>')
|
|
124
|
-
.description('List snapshot backups for a managed database')
|
|
125
|
-
.option('--json', 'Output raw JSON')
|
|
126
|
-
.action(async (id, opts) => {
|
|
127
|
-
try {
|
|
128
|
-
const res = await client.get(`/api/managed-databases/${id}/snapshots`);
|
|
129
|
-
const snaps: any[] = res.data?.snapshots || [];
|
|
130
|
-
if (opts.json) { printJson(snaps); return; }
|
|
131
|
-
if (!snaps.length) { console.log('No snapshots found.'); return; }
|
|
132
|
-
printTable(
|
|
133
|
-
['ID', 'STATUS', 'SIZE (B)', 'NOTES', 'CREATED'],
|
|
134
|
-
snaps.map((s: any) => [s.id, s.status, String(s.sizeBytes), s.notes || '—', s.createdAt ? timeAgo(s.createdAt) : '—'])
|
|
135
|
-
);
|
|
136
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
md
|
|
140
|
-
.command('query <id> <sql>')
|
|
141
|
-
.description('Run a SQL statement against a managed database')
|
|
142
|
-
.option('--json', 'Output raw JSON')
|
|
143
|
-
.action(async (id, sql, opts) => {
|
|
144
|
-
try {
|
|
145
|
-
const res = await client.post(`/api/managed-databases/${id}/query`, { sql });
|
|
146
|
-
if (opts.json) { printJson(res.data); return; }
|
|
147
|
-
const rows = res.data?.rows || [];
|
|
148
|
-
console.log(`${res.data?.classification}: ${res.data?.rowCount} row(s)`);
|
|
149
|
-
if (rows.length) printJson(rows);
|
|
150
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
md
|
|
154
|
-
.command('restore <id>')
|
|
155
|
-
.description('Restore a snapshot into a NEW managed database (non-destructive)')
|
|
156
|
-
.requiredOption('--snapshot <snapshotId>', 'Snapshot ID to restore from')
|
|
157
|
-
.requiredOption('--new-name <name>', 'Name for the restored database')
|
|
158
|
-
.option('--json', 'Output raw JSON')
|
|
159
|
-
.action(async (id, opts) => {
|
|
160
|
-
try {
|
|
161
|
-
const res = await client.post(`/api/managed-databases/${id}/restore`, {
|
|
162
|
-
snapshotId: opts.snapshot,
|
|
163
|
-
newName: opts.newName,
|
|
164
|
-
});
|
|
165
|
-
const db = res.data?.managedDatabase;
|
|
166
|
-
if (opts.json) { printJson(db); return; }
|
|
167
|
-
success(`Restore started: new database ${db?.id} (${db?.status}). Run \`nexus managed-db list\` until AVAILABLE.`);
|
|
168
|
-
} catch (err) { errorMsg(apiError(err)); process.exit(1); }
|
|
169
|
-
});
|
|
170
|
-
}
|
package/src/commands/member.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { client, apiError, unwrap } from '../client.js';
|
|
5
|
-
import { printTable, printJson, success, errorMsg, timeAgo } from '../output.js';
|
|
6
|
-
|
|
7
|
-
const VALID_ROLES = ['OWNER', 'ADMIN', 'MEMBER', 'DEPLOYMENT_MANAGER', 'AUDITOR', 'BILLING_MANAGER'];
|
|
8
|
-
|
|
9
|
-
const ROLE_LABELS: Record<string, string> = {
|
|
10
|
-
OWNER: 'Owner',
|
|
11
|
-
ADMIN: 'Admin',
|
|
12
|
-
MEMBER: 'Developer',
|
|
13
|
-
DEPLOYMENT_MANAGER: 'Deployment Manager',
|
|
14
|
-
AUDITOR: 'Auditor',
|
|
15
|
-
BILLING_MANAGER: 'Billing Manager',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function roleLabel(role: string): string {
|
|
19
|
-
return ROLE_LABELS[role] || role;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function statusBadge(status: string): string {
|
|
23
|
-
if (status === 'active') return chalk.green(status);
|
|
24
|
-
if (status === 'suspended') return chalk.yellow(status);
|
|
25
|
-
return chalk.gray(status);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function registerMember(program: Command): void {
|
|
29
|
-
const member = program.command('member').description('Team member management commands');
|
|
30
|
-
|
|
31
|
-
// list
|
|
32
|
-
member
|
|
33
|
-
.command('list')
|
|
34
|
-
.description('List all team members in the organization')
|
|
35
|
-
.option('--json', 'Output raw JSON')
|
|
36
|
-
.action(async (opts) => {
|
|
37
|
-
try {
|
|
38
|
-
const res = await client.get('/api/organizations/users');
|
|
39
|
-
let members: any[] = unwrap(res.data);
|
|
40
|
-
if (!Array.isArray(members)) members = [];
|
|
41
|
-
|
|
42
|
-
if (opts.json) { printJson(members); return; }
|
|
43
|
-
if (!members.length) { console.log('No members found.'); return; }
|
|
44
|
-
|
|
45
|
-
printTable(
|
|
46
|
-
['ID', 'NAME', 'EMAIL', 'ROLE', 'STATUS', 'JOINED'],
|
|
47
|
-
members.map((m: any) => [
|
|
48
|
-
m.id,
|
|
49
|
-
m.name || '—',
|
|
50
|
-
m.email,
|
|
51
|
-
roleLabel(m.role),
|
|
52
|
-
statusBadge(m.status),
|
|
53
|
-
m.createdAt ? timeAgo(m.createdAt) : '—',
|
|
54
|
-
])
|
|
55
|
-
);
|
|
56
|
-
} catch (err) {
|
|
57
|
-
errorMsg(apiError(err));
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// invite
|
|
63
|
-
member
|
|
64
|
-
.command('invite <email>')
|
|
65
|
-
.description('Invite a new member to the organization')
|
|
66
|
-
.requiredOption('--role <role>', `Role to assign (${VALID_ROLES.filter(r => r !== 'OWNER').join(', ')})`)
|
|
67
|
-
.option('--json', 'Output raw JSON')
|
|
68
|
-
.action(async (email, opts) => {
|
|
69
|
-
const role = opts.role.toUpperCase();
|
|
70
|
-
|
|
71
|
-
if (!VALID_ROLES.includes(role) || role === 'OWNER') {
|
|
72
|
-
errorMsg(`Invalid role "${opts.role}". Valid roles: ${VALID_ROLES.filter(r => r !== 'OWNER').join(', ')}`);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const res = await client.post('/api/organizations/users/invite', { email, role });
|
|
78
|
-
const data = unwrap(res.data);
|
|
79
|
-
const user = data.user || data;
|
|
80
|
-
|
|
81
|
-
if (opts.json) { printJson(data); return; }
|
|
82
|
-
|
|
83
|
-
success(`Invited ${email} as ${roleLabel(role)}.`);
|
|
84
|
-
console.log(` ${chalk.dim('User ID:')} ${user.id}`);
|
|
85
|
-
console.log(` ${chalk.dim('An email with login credentials has been sent.')}`);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
errorMsg(apiError(err));
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// role
|
|
93
|
-
member
|
|
94
|
-
.command('role <userId> <role>')
|
|
95
|
-
.description('Update the role of a team member')
|
|
96
|
-
.option('--json', 'Output raw JSON')
|
|
97
|
-
.action(async (userId, roleArg, opts) => {
|
|
98
|
-
const role = roleArg.toUpperCase();
|
|
99
|
-
|
|
100
|
-
if (!VALID_ROLES.includes(role) || role === 'OWNER') {
|
|
101
|
-
errorMsg(`Invalid role "${roleArg}". Valid roles: ${VALID_ROLES.filter(r => r !== 'OWNER').join(', ')}`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const res = await client.patch(`/api/organizations/users/${userId}/role`, { role });
|
|
107
|
-
const updated = unwrap(res.data);
|
|
108
|
-
|
|
109
|
-
if (opts.json) { printJson(updated); return; }
|
|
110
|
-
|
|
111
|
-
success(`Updated ${updated.email} to ${roleLabel(updated.role)}.`);
|
|
112
|
-
} catch (err) {
|
|
113
|
-
errorMsg(apiError(err));
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// suspend
|
|
119
|
-
member
|
|
120
|
-
.command('suspend <userId>')
|
|
121
|
-
.description('Suspend a team member (blocks their access)')
|
|
122
|
-
.option('--yes', 'Skip confirmation prompt')
|
|
123
|
-
.option('--json', 'Output raw JSON')
|
|
124
|
-
.action(async (userId, opts) => {
|
|
125
|
-
if (!opts.yes) {
|
|
126
|
-
const { confirm } = await inquirer.prompt([
|
|
127
|
-
{
|
|
128
|
-
type: 'confirm',
|
|
129
|
-
name: 'confirm',
|
|
130
|
-
message: `Suspend member "${userId}"? They will lose access immediately.`,
|
|
131
|
-
default: false,
|
|
132
|
-
},
|
|
133
|
-
]);
|
|
134
|
-
if (!confirm) { console.log('Cancelled.'); return; }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const res = await client.post(`/api/organizations/users/${userId}/suspend`, {});
|
|
139
|
-
const updated = unwrap(res.data);
|
|
140
|
-
|
|
141
|
-
if (opts.json) { printJson(updated); return; }
|
|
142
|
-
|
|
143
|
-
success(`${updated.email} suspended.`);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
errorMsg(apiError(err));
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// activate
|
|
151
|
-
member
|
|
152
|
-
.command('activate <userId>')
|
|
153
|
-
.description('Restore access for a suspended team member')
|
|
154
|
-
.option('--json', 'Output raw JSON')
|
|
155
|
-
.action(async (userId, opts) => {
|
|
156
|
-
try {
|
|
157
|
-
const res = await client.post(`/api/organizations/users/${userId}/activate`, {});
|
|
158
|
-
const updated = unwrap(res.data);
|
|
159
|
-
|
|
160
|
-
if (opts.json) { printJson(updated); return; }
|
|
161
|
-
|
|
162
|
-
success(`${updated.email} activated.`);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
errorMsg(apiError(err));
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|