nexusapp-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nexus +2 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +159 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +594 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/domain.d.ts +3 -0
- package/dist/commands/domain.d.ts.map +1 -0
- package/dist/commands/domain.js +174 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +92 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +121 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +9 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +71 -0
- package/dist/output.js.map +1 -0
- package/package.json +29 -0
- package/src/client.ts +68 -0
- package/src/commands/auth.ts +166 -0
- package/src/commands/deploy.ts +534 -0
- package/src/commands/domain.ts +167 -0
- package/src/commands/project.ts +81 -0
- package/src/commands/secret.ts +117 -0
- package/src/config.ts +56 -0
- package/src/index.ts +25 -0
- package/src/output.ts +65 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
export function registerProject(program: Command): void {
|
|
7
|
+
const project = program.command('project').description('Project management commands');
|
|
8
|
+
|
|
9
|
+
// list
|
|
10
|
+
project
|
|
11
|
+
.command('list')
|
|
12
|
+
.description('List projects')
|
|
13
|
+
.option('--json', 'Output raw JSON')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
try {
|
|
16
|
+
const res = await client.get('/api/projects');
|
|
17
|
+
const raw = unwrap(res.data);
|
|
18
|
+
const projects = Array.isArray(raw) ? raw : raw.projects || [];
|
|
19
|
+
|
|
20
|
+
if (opts.json) { printJson(projects); return; }
|
|
21
|
+
if (!projects.length) { console.log('No projects found.'); return; }
|
|
22
|
+
|
|
23
|
+
printTable(
|
|
24
|
+
['ID', 'NAME', 'CREATED'],
|
|
25
|
+
projects.map((p: any) => [
|
|
26
|
+
p.id,
|
|
27
|
+
p.name,
|
|
28
|
+
p.createdAt ? timeAgo(p.createdAt) : '—',
|
|
29
|
+
])
|
|
30
|
+
);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
errorMsg(apiError(err));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// create
|
|
38
|
+
project
|
|
39
|
+
.command('create')
|
|
40
|
+
.description('Create a new project')
|
|
41
|
+
.requiredOption('--name <name>', 'Project name')
|
|
42
|
+
.option('--json', 'Output raw JSON')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await client.post('/api/projects', { name: opts.name });
|
|
46
|
+
const p = unwrap(res.data);
|
|
47
|
+
if (opts.json) { printJson(p); return; }
|
|
48
|
+
success(`Project "${p.name}" created (${p.id})`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
errorMsg(apiError(err));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// delete
|
|
56
|
+
project
|
|
57
|
+
.command('delete <id>')
|
|
58
|
+
.description('Delete a project')
|
|
59
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
60
|
+
.action(async (id, opts) => {
|
|
61
|
+
if (!opts.yes) {
|
|
62
|
+
const { confirm } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'confirm',
|
|
66
|
+
message: `Delete project "${id}"? All deployments in this project will be removed.`,
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await client.delete(`/api/projects/${id}`);
|
|
75
|
+
success(`Project ${id} deleted.`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
errorMsg(apiError(err));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
export function registerSecret(program: Command): void {
|
|
7
|
+
const secret = program.command('secret').description('Secret management commands');
|
|
8
|
+
|
|
9
|
+
// list
|
|
10
|
+
secret
|
|
11
|
+
.command('list')
|
|
12
|
+
.description('List secrets')
|
|
13
|
+
.option('--environment <env>', 'Filter by environment')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
const params: Record<string, string> = {};
|
|
18
|
+
if (opts.environment) params.environment = opts.environment;
|
|
19
|
+
|
|
20
|
+
const res = await client.get('/api/secrets', { params });
|
|
21
|
+
const raw = unwrap(res.data);
|
|
22
|
+
const secrets = Array.isArray(raw) ? raw : raw.secrets || [];
|
|
23
|
+
|
|
24
|
+
if (opts.json) { printJson(secrets); return; }
|
|
25
|
+
if (!secrets.length) { console.log('No secrets found.'); return; }
|
|
26
|
+
|
|
27
|
+
printTable(
|
|
28
|
+
['ID', 'NAME', 'ENVIRONMENT', 'CREATED'],
|
|
29
|
+
secrets.map((s: any) => [
|
|
30
|
+
s.id,
|
|
31
|
+
s.name,
|
|
32
|
+
s.environment || '—',
|
|
33
|
+
s.createdAt ? timeAgo(s.createdAt) : '—',
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
errorMsg(apiError(err));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// create
|
|
43
|
+
secret
|
|
44
|
+
.command('create')
|
|
45
|
+
.description('Create a new secret')
|
|
46
|
+
.requiredOption('--name <name>', 'Secret name')
|
|
47
|
+
.requiredOption('--environment <env>', 'Target environment')
|
|
48
|
+
.option('--value <value>', 'Secret value (prompt if omitted)')
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
let value = opts.value;
|
|
51
|
+
|
|
52
|
+
if (!value) {
|
|
53
|
+
const ans = await inquirer.prompt([
|
|
54
|
+
{ type: 'password', name: 'value', message: 'Value:', mask: '•' },
|
|
55
|
+
]);
|
|
56
|
+
value = ans.value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await client.post('/api/secrets', {
|
|
61
|
+
name: opts.name,
|
|
62
|
+
environment: opts.environment,
|
|
63
|
+
value,
|
|
64
|
+
});
|
|
65
|
+
success(`Secret ${opts.name} created (${opts.environment})`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
errorMsg(apiError(err));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// update
|
|
73
|
+
secret
|
|
74
|
+
.command('update <id>')
|
|
75
|
+
.description('Update a secret value')
|
|
76
|
+
.option('--value <value>', 'New secret value (prompt if omitted)')
|
|
77
|
+
.action(async (id, opts) => {
|
|
78
|
+
let value = opts.value;
|
|
79
|
+
|
|
80
|
+
if (!value) {
|
|
81
|
+
const ans = await inquirer.prompt([
|
|
82
|
+
{ type: 'password', name: 'value', message: 'New value:', mask: '•' },
|
|
83
|
+
]);
|
|
84
|
+
value = ans.value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await client.put(`/api/secrets/${id}`, { value });
|
|
89
|
+
success(`Secret ${id} updated.`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
errorMsg(apiError(err));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// delete
|
|
97
|
+
secret
|
|
98
|
+
.command('delete <id>')
|
|
99
|
+
.description('Delete a secret')
|
|
100
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
101
|
+
.action(async (id, opts) => {
|
|
102
|
+
if (!opts.yes) {
|
|
103
|
+
const { confirm } = await inquirer.prompt([
|
|
104
|
+
{ type: 'confirm', name: 'confirm', message: `Delete secret "${id}"?`, default: false },
|
|
105
|
+
]);
|
|
106
|
+
if (!confirm) { console.log('Cancelled.'); return; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await client.delete(`/api/secrets/${id}`);
|
|
111
|
+
success(`Secret ${id} deleted.`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
errorMsg(apiError(err));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
token: string;
|
|
8
|
+
tokenId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.nexusai');
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_API_URL = 'https://api.nexusai.run';
|
|
15
|
+
|
|
16
|
+
export function getConfig(): Partial<Config> {
|
|
17
|
+
const envToken = process.env.NEXUSAI_TOKEN;
|
|
18
|
+
const envApiUrl = process.env.NEXUSAI_API_URL;
|
|
19
|
+
|
|
20
|
+
let fileConfig: Partial<Config> = {};
|
|
21
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
22
|
+
try {
|
|
23
|
+
fileConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore malformed config
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
apiUrl: envApiUrl || fileConfig.apiUrl || DEFAULT_API_URL,
|
|
31
|
+
token: envToken || fileConfig.token || '',
|
|
32
|
+
tokenId: fileConfig.tokenId || '',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveConfig(config: Config): void {
|
|
37
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearConfig(): void {
|
|
44
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
45
|
+
fs.unlinkSync(CONFIG_PATH);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requireToken(): string {
|
|
50
|
+
const config = getConfig();
|
|
51
|
+
if (!config.token) {
|
|
52
|
+
console.error("Not logged in. Run 'nexus auth login' first.");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
return config.token;
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerAuth } from './commands/auth.js';
|
|
4
|
+
import { registerDeploy } from './commands/deploy.js';
|
|
5
|
+
import { registerSecret } from './commands/secret.js';
|
|
6
|
+
import { registerProject } from './commands/project.js';
|
|
7
|
+
import { registerDomain } from './commands/domain.js';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('nexus')
|
|
13
|
+
.description('NEXUS AI command-line interface')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
registerAuth(program);
|
|
17
|
+
registerDeploy(program);
|
|
18
|
+
registerSecret(program);
|
|
19
|
+
registerProject(program);
|
|
20
|
+
registerDomain(program);
|
|
21
|
+
|
|
22
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
23
|
+
console.error(err.message || String(err));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import ora, { Ora } from 'ora';
|
|
4
|
+
|
|
5
|
+
const STATUS_COLORS: Record<string, (s: string) => string> = {
|
|
6
|
+
RUNNING: chalk.green,
|
|
7
|
+
BUILDING: chalk.yellow,
|
|
8
|
+
DEPLOYING: chalk.yellow,
|
|
9
|
+
FAILED: chalk.red,
|
|
10
|
+
STOPPED: chalk.gray,
|
|
11
|
+
PENDING: chalk.blue,
|
|
12
|
+
QUEUED: chalk.blue,
|
|
13
|
+
TERMINATED: chalk.gray,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function statusBadge(status: string): string {
|
|
17
|
+
const colorFn = STATUS_COLORS[status?.toUpperCase()] || chalk.white;
|
|
18
|
+
return colorFn(status || 'UNKNOWN');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function printTable(headers: string[], rows: (string | undefined | null)[][]): void {
|
|
22
|
+
const table = new Table({
|
|
23
|
+
head: headers.map((h) => chalk.bold(h)),
|
|
24
|
+
style: { head: [], border: [] },
|
|
25
|
+
chars: {
|
|
26
|
+
top: '', 'top-mid': '', 'top-left': '', 'top-right': '',
|
|
27
|
+
bottom: '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
|
|
28
|
+
left: '', 'left-mid': '', mid: '', 'mid-mid': '',
|
|
29
|
+
right: '', 'right-mid': '', middle: ' ',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
table.push(row.map((cell) => cell ?? chalk.gray('—')));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(table.toString());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printJson(obj: unknown): void {
|
|
41
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function spinner(text: string): Ora {
|
|
45
|
+
return ora(text).start();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function timeAgo(dateStr: string): string {
|
|
49
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
50
|
+
const s = Math.floor(diff / 1000);
|
|
51
|
+
if (s < 60) return `${s}s ago`;
|
|
52
|
+
const m = Math.floor(s / 60);
|
|
53
|
+
if (m < 60) return `${m}m ago`;
|
|
54
|
+
const h = Math.floor(m / 60);
|
|
55
|
+
if (h < 24) return `${h}h ago`;
|
|
56
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function success(msg: string): void {
|
|
60
|
+
console.log(chalk.green('✓') + ' ' + msg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function errorMsg(msg: string): void {
|
|
64
|
+
console.error(chalk.red('✗') + ' ' + msg);
|
|
65
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|