@nivo-lat/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/README.md +79 -0
- package/dist/api.js +26 -0
- package/dist/commands/apps.js +240 -0
- package/dist/commands/auth.js +118 -0
- package/dist/commands/link.js +227 -0
- package/dist/config.js +37 -0
- package/dist/index.js +52 -0
- package/dist/ui.js +20 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @nivo-lat/cli
|
|
2
|
+
|
|
3
|
+
Official CLI for [Nivo](https://nivo.lat) — deploy and manage applications from your terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @nivo-lat/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Authentication
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
nivo login # opens browser for device authentication
|
|
15
|
+
nivo logout # removes local session
|
|
16
|
+
nivo me # shows logged-in account info
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Linking a project
|
|
20
|
+
|
|
21
|
+
Create a `.nivo` file at the root of your project:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"name": "my-app",
|
|
26
|
+
"projectId": "PROJECT_ID",
|
|
27
|
+
"sourceType": "zip",
|
|
28
|
+
"type": "site",
|
|
29
|
+
"buildSystem": "nivopack",
|
|
30
|
+
"runtime": "node20",
|
|
31
|
+
"installCmd": "npm install",
|
|
32
|
+
"buildCmd": "npm run build",
|
|
33
|
+
"startCmd": "npm start",
|
|
34
|
+
"ramMb": 256,
|
|
35
|
+
"subdomain": "my-app"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then link the app:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
nivo link
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Deploying
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nivo deploy # deploy current directory
|
|
49
|
+
nivo deploy --watch # deploy and stream logs until done
|
|
50
|
+
nivo deploy --app APP_ID # deploy a specific app
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Monitoring
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
nivo apps # list all apps
|
|
57
|
+
nivo deployments # list deployments for linked app
|
|
58
|
+
nivo logs # fetch runtime logs
|
|
59
|
+
nivo logs --deployment DEPLOY_ID # fetch logs for a specific deployment
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## `.nivo` config reference
|
|
63
|
+
|
|
64
|
+
| Field | Type | Description |
|
|
65
|
+
|-------|------|-------------|
|
|
66
|
+
| `appId` | string | ID of an existing app (omit to create a new one) |
|
|
67
|
+
| `name` | string | App name |
|
|
68
|
+
| `projectId` | string | Project ID |
|
|
69
|
+
| `sourceType` | `zip` \| `github` | How code is delivered |
|
|
70
|
+
| `type` | `site` \| `worker` | App type |
|
|
71
|
+
| `buildSystem` | `nivopack` \| `nixpacks` \| `dockerfile` | Build system |
|
|
72
|
+
| `runtime` | string | Runtime (e.g. `node20`) |
|
|
73
|
+
| `installCmd` | string | Install command |
|
|
74
|
+
| `buildCmd` | string | Build command |
|
|
75
|
+
| `startCmd` | string | Start command |
|
|
76
|
+
| `ramMb` | number | RAM in MB (min 100) |
|
|
77
|
+
| `subdomain` | string | Subdomain on `*.app.nivo.lat` |
|
|
78
|
+
| `repoFullName` | string | GitHub repo (`owner/repo`) — required for `sourceType: github` |
|
|
79
|
+
| `repoBranch` | string | Branch to deploy from |
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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.api = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const API_URL = process.env.NIVO_API_URL || 'https://api.nivo.lat/api';
|
|
10
|
+
exports.api = axios_1.default.create({
|
|
11
|
+
baseURL: API_URL,
|
|
12
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
13
|
+
});
|
|
14
|
+
exports.api.interceptors.request.use((config) => {
|
|
15
|
+
const creds = (0, config_1.getCredentials)();
|
|
16
|
+
if (creds.token) {
|
|
17
|
+
config.headers['Authorization'] = `Bearer ${creds.token}`;
|
|
18
|
+
}
|
|
19
|
+
return config;
|
|
20
|
+
});
|
|
21
|
+
exports.api.interceptors.response.use((response) => response, (error) => {
|
|
22
|
+
if (error.response?.data?.error && !error.message?.includes(error.response.data.error)) {
|
|
23
|
+
error.message = error.response.data.error;
|
|
24
|
+
}
|
|
25
|
+
return Promise.reject(error);
|
|
26
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
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.listApps = listApps;
|
|
7
|
+
exports.deploy = deploy;
|
|
8
|
+
exports.deployments = deployments;
|
|
9
|
+
exports.logs = logs;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
14
|
+
const ora_1 = __importDefault(require("ora"));
|
|
15
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
16
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
17
|
+
const api_1 = require("../api");
|
|
18
|
+
const config_1 = require("../config");
|
|
19
|
+
const ui_1 = require("../ui");
|
|
20
|
+
const TERMINAL_STATUSES = new Set(['success', 'error', 'cancelled']);
|
|
21
|
+
const IGNORED_NAMES = new Set([
|
|
22
|
+
'.git',
|
|
23
|
+
'.nivo',
|
|
24
|
+
'.next',
|
|
25
|
+
'node_modules',
|
|
26
|
+
'.env',
|
|
27
|
+
'.env.local',
|
|
28
|
+
'.env.production',
|
|
29
|
+
'.env.development',
|
|
30
|
+
'.DS_Store',
|
|
31
|
+
]);
|
|
32
|
+
function ensureLoggedIn() {
|
|
33
|
+
if ((0, config_1.getCredentials)().token)
|
|
34
|
+
return true;
|
|
35
|
+
ui_1.logger.error('Nao autenticado. Rode `nivo login`.');
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function readLinkedAppId() {
|
|
39
|
+
return readNivoProjectConfig()?.appId ?? null;
|
|
40
|
+
}
|
|
41
|
+
function readNivoProjectConfig() {
|
|
42
|
+
const configPath = path_1.default.join(process.cwd(), '.nivo');
|
|
43
|
+
if (!fs_1.default.existsSync(configPath))
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
47
|
+
return typeof parsed === 'object' && parsed ? parsed : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function applyLocalProjectConfig(appId) {
|
|
54
|
+
const local = readNivoProjectConfig();
|
|
55
|
+
if (!local || local.appId !== appId)
|
|
56
|
+
return;
|
|
57
|
+
const body = {};
|
|
58
|
+
for (const key of ['buildSystem', 'type', 'runtime', 'installCmd', 'buildCmd', 'startCmd', 'ramMb', 'subdomain']) {
|
|
59
|
+
if (local[key] !== undefined)
|
|
60
|
+
body[key] = local[key];
|
|
61
|
+
}
|
|
62
|
+
if (Object.keys(body).length === 0)
|
|
63
|
+
return;
|
|
64
|
+
await api_1.api.patch(`/apps/${encodeURIComponent(appId)}`, body);
|
|
65
|
+
}
|
|
66
|
+
async function getAppId(explicit) {
|
|
67
|
+
if (explicit)
|
|
68
|
+
return explicit;
|
|
69
|
+
const linked = readLinkedAppId();
|
|
70
|
+
if (linked)
|
|
71
|
+
return linked;
|
|
72
|
+
ui_1.logger.error('Projeto nao linkado. Crie .nivo e rode `nivo link`.');
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function fetchApps() {
|
|
76
|
+
const res = await api_1.api.get('/apps');
|
|
77
|
+
return res.data;
|
|
78
|
+
}
|
|
79
|
+
function addDir(zip, dir, root) {
|
|
80
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
if (IGNORED_NAMES.has(entry.name) || entry.name.endsWith('.log'))
|
|
82
|
+
continue;
|
|
83
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
84
|
+
const relPath = path_1.default.relative(root, fullPath).replace(/\\/g, '/');
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
addDir(zip, fullPath, root);
|
|
87
|
+
}
|
|
88
|
+
else if (entry.isFile()) {
|
|
89
|
+
zip.addLocalFile(fullPath, path_1.default.dirname(relPath) === '.' ? '' : path_1.default.dirname(relPath));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function createZip(sourceDir) {
|
|
94
|
+
const resolved = path_1.default.resolve(sourceDir);
|
|
95
|
+
if (!fs_1.default.existsSync(resolved) || !fs_1.default.statSync(resolved).isDirectory()) {
|
|
96
|
+
throw new Error(`Diretorio nao encontrado: ${sourceDir}`);
|
|
97
|
+
}
|
|
98
|
+
const zip = new adm_zip_1.default();
|
|
99
|
+
addDir(zip, resolved, resolved);
|
|
100
|
+
const out = path_1.default.join(os_1.default.tmpdir(), `nivo-deploy-${Date.now()}.zip`);
|
|
101
|
+
zip.writeZip(out);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
async function uploadZip(appId, zipPath) {
|
|
105
|
+
const form = new form_data_1.default();
|
|
106
|
+
form.append('file', fs_1.default.createReadStream(zipPath), {
|
|
107
|
+
filename: 'app.zip',
|
|
108
|
+
contentType: 'application/zip',
|
|
109
|
+
});
|
|
110
|
+
const res = await api_1.api.post(`/apps/${encodeURIComponent(appId)}/upload`, form, {
|
|
111
|
+
headers: form.getHeaders(),
|
|
112
|
+
maxBodyLength: Infinity,
|
|
113
|
+
maxContentLength: Infinity,
|
|
114
|
+
});
|
|
115
|
+
return res.data;
|
|
116
|
+
}
|
|
117
|
+
async function waitDeployment(appId, deploymentId) {
|
|
118
|
+
let printed = 0;
|
|
119
|
+
let lastStatus = '';
|
|
120
|
+
while (true) {
|
|
121
|
+
const res = await api_1.api.get(`/apps/${encodeURIComponent(appId)}/deployments/${encodeURIComponent(deploymentId)}/logs`);
|
|
122
|
+
const data = res.data;
|
|
123
|
+
const log = data.log ?? '';
|
|
124
|
+
if (log.length > printed) {
|
|
125
|
+
process.stdout.write(log.slice(printed));
|
|
126
|
+
printed = log.length;
|
|
127
|
+
}
|
|
128
|
+
if (data.status !== lastStatus) {
|
|
129
|
+
lastStatus = data.status;
|
|
130
|
+
ui_1.logger.info(`status ${data.status}`);
|
|
131
|
+
}
|
|
132
|
+
if (TERMINAL_STATUSES.has(data.status)) {
|
|
133
|
+
if (data.status === 'success')
|
|
134
|
+
ui_1.logger.success('Deploy finalizado.');
|
|
135
|
+
else
|
|
136
|
+
ui_1.logger.error(`Deploy terminou como ${data.status}.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function listApps() {
|
|
143
|
+
if (!ensureLoggedIn())
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
const apps = await fetchApps();
|
|
147
|
+
if (!apps.length) {
|
|
148
|
+
ui_1.logger.warn('Nenhum app encontrado.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(`${chalk_1.default.dim('name'.padEnd(24))} ${chalk_1.default.dim('status'.padEnd(10))} ${chalk_1.default.dim('source'.padEnd(8))} ${chalk_1.default.dim('url')}`);
|
|
152
|
+
for (const app of apps) {
|
|
153
|
+
const url = app.customDomain ?? (app.subdomain ? `https://${app.subdomain}.app.nivo.lat` : '-');
|
|
154
|
+
console.log(`${chalk_1.default.bold(app.name.padEnd(24))} ${chalk_1.default.cyan(app.status.padEnd(10))} ${chalk_1.default.dim(app.sourceType.padEnd(8))} ${url}`);
|
|
155
|
+
console.log(`${chalk_1.default.dim(app.id)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
ui_1.logger.error(`Falha ao listar apps: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function deploy(sourceDir = '.', opts = {}) {
|
|
163
|
+
if (!ensureLoggedIn())
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
const appId = await getAppId(opts.app);
|
|
167
|
+
if (!appId)
|
|
168
|
+
return;
|
|
169
|
+
await applyLocalProjectConfig(appId);
|
|
170
|
+
const appRes = await api_1.api.get(`/apps/${encodeURIComponent(appId)}`);
|
|
171
|
+
const app = appRes.data;
|
|
172
|
+
let deploymentId;
|
|
173
|
+
if (app.sourceType === 'zip') {
|
|
174
|
+
const spinner = (0, ora_1.default)({ text: 'Compactando projeto...', color: 'green' }).start();
|
|
175
|
+
const zipPath = createZip(sourceDir);
|
|
176
|
+
spinner.text = 'Enviando deploy...';
|
|
177
|
+
try {
|
|
178
|
+
const result = await uploadZip(appId, zipPath);
|
|
179
|
+
deploymentId = result.deploymentId;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
fs_1.default.rmSync(zipPath, { force: true });
|
|
183
|
+
}
|
|
184
|
+
spinner.succeed(`Deploy criado ${deploymentId}`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const spinner = (0, ora_1.default)({ text: 'Iniciando redeploy...', color: 'green' }).start();
|
|
188
|
+
const res = await api_1.api.post(`/apps/${encodeURIComponent(appId)}/deploy`);
|
|
189
|
+
deploymentId = res.data.deploymentId;
|
|
190
|
+
spinner.succeed(`Deploy criado ${deploymentId}`);
|
|
191
|
+
}
|
|
192
|
+
if (opts.watch)
|
|
193
|
+
await waitDeployment(appId, deploymentId);
|
|
194
|
+
else
|
|
195
|
+
ui_1.logger.info(`logs: nivo logs --deployment ${deploymentId}`);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
ui_1.logger.error(`Deploy falhou: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function deployments(opts = {}) {
|
|
202
|
+
if (!ensureLoggedIn())
|
|
203
|
+
return;
|
|
204
|
+
try {
|
|
205
|
+
const appId = await getAppId(opts.app);
|
|
206
|
+
if (!appId)
|
|
207
|
+
return;
|
|
208
|
+
const res = await api_1.api.get(`/apps/${encodeURIComponent(appId)}/deployments`);
|
|
209
|
+
const items = res.data;
|
|
210
|
+
if (!items.length) {
|
|
211
|
+
ui_1.logger.warn('Nenhum deployment encontrado.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const dep of items) {
|
|
215
|
+
console.log(`${chalk_1.default.cyan(dep.status.padEnd(10))} ${chalk_1.default.dim(dep.trigger.padEnd(8))} ${new Date(dep.createdAt).toLocaleString()} ${chalk_1.default.dim(dep.id)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
ui_1.logger.error(`Falha ao listar deployments: ${err.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function logs(opts = {}) {
|
|
223
|
+
if (!ensureLoggedIn())
|
|
224
|
+
return;
|
|
225
|
+
try {
|
|
226
|
+
const appId = await getAppId(opts.app);
|
|
227
|
+
if (!appId)
|
|
228
|
+
return;
|
|
229
|
+
if (opts.deployment) {
|
|
230
|
+
const res = await api_1.api.get(`/apps/${encodeURIComponent(appId)}/deployments/${encodeURIComponent(opts.deployment)}/logs`);
|
|
231
|
+
console.log(res.data.log ?? '');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const res = await api_1.api.get(`/apps/${encodeURIComponent(appId)}/logs`);
|
|
235
|
+
console.log(res.data.log ?? '');
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
ui_1.logger.error(`Falha ao buscar logs: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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.login = login;
|
|
7
|
+
exports.logout = logout;
|
|
8
|
+
exports.me = me;
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const api_1 = require("../api");
|
|
13
|
+
const config_1 = require("../config");
|
|
14
|
+
const ui_1 = require("../ui");
|
|
15
|
+
function openBrowser(url) {
|
|
16
|
+
const command = process.platform === 'darwin' ? 'open' :
|
|
17
|
+
process.platform === 'win32' ? 'cmd' :
|
|
18
|
+
'xdg-open';
|
|
19
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url];
|
|
20
|
+
try {
|
|
21
|
+
(0, child_process_1.spawn)(command, args, { detached: true, stdio: 'ignore' }).unref();
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
async function pollForToken(deviceCode, intervalSec, expiresInSec) {
|
|
26
|
+
const deadline = Date.now() + expiresInSec * 1000;
|
|
27
|
+
const intervalMs = Math.max(1, intervalSec) * 1000;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
30
|
+
try {
|
|
31
|
+
const res = await api_1.api.post('/auth/cli/poll', { deviceCode });
|
|
32
|
+
if (res.status === 202)
|
|
33
|
+
continue;
|
|
34
|
+
const data = res.data;
|
|
35
|
+
if (data?.apiKey)
|
|
36
|
+
return data.apiKey;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const status = err?.response?.status;
|
|
40
|
+
if (status === 410) {
|
|
41
|
+
const reason = err?.response?.data?.status ?? 'expired';
|
|
42
|
+
throw new Error(`Sessao ${reason}. Rode \`nivo login\` de novo.`);
|
|
43
|
+
}
|
|
44
|
+
if (status === 404)
|
|
45
|
+
throw new Error('Sessao nao encontrada. Rode `nivo login` de novo.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Tempo esgotado. Rode `nivo login` de novo.');
|
|
49
|
+
}
|
|
50
|
+
async function login() {
|
|
51
|
+
const existing = (0, config_1.getCredentials)();
|
|
52
|
+
if (existing.token) {
|
|
53
|
+
ui_1.logger.warn('Voce ja esta logado. Rode `nivo logout` primeiro pra trocar de conta.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
ui_1.logger.step('Login');
|
|
57
|
+
let start;
|
|
58
|
+
try {
|
|
59
|
+
const res = await api_1.api.post('/auth/cli/start', {});
|
|
60
|
+
start = res.data;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
ui_1.logger.error(`Nao consegui iniciar o login: ${err.message}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const url = `${start.verifyUrl}?user_code=${encodeURIComponent(start.userCode)}`;
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(' ' + chalk_1.default.dim('Codigo:') + ' ' + chalk_1.default.bold.hex('#4ade80')(start.userCode));
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(' ' + chalk_1.default.dim('URL:') + ' ' + chalk_1.default.underline(url));
|
|
71
|
+
console.log();
|
|
72
|
+
openBrowser(url);
|
|
73
|
+
const spinner = (0, ora_1.default)({ text: 'Aguardando autorizacao...', color: 'green' }).start();
|
|
74
|
+
try {
|
|
75
|
+
const apiKey = await pollForToken(start.deviceCode, start.interval, start.expiresIn);
|
|
76
|
+
(0, config_1.saveCredentials)({ token: apiKey });
|
|
77
|
+
spinner.succeed('Login concluido');
|
|
78
|
+
try {
|
|
79
|
+
const res = await api_1.api.get('/auth/me');
|
|
80
|
+
ui_1.logger.success(`Logado como ${chalk_1.default.bold(res.data.email)}.`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
ui_1.logger.success('Token salvo em ~/.nivo-auth.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
spinner.fail(err.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function logout() {
|
|
91
|
+
(0, config_1.deleteCredentials)();
|
|
92
|
+
ui_1.logger.success('Sessao local removida.');
|
|
93
|
+
ui_1.logger.info('Para revogar a chave, acesse Configuracoes > CLI no dashboard.');
|
|
94
|
+
}
|
|
95
|
+
async function me() {
|
|
96
|
+
const creds = (0, config_1.getCredentials)();
|
|
97
|
+
if (!creds.token) {
|
|
98
|
+
ui_1.logger.error('Voce nao esta logado. Rode `nivo login`.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const res = await api_1.api.get('/auth/me');
|
|
103
|
+
const user = res.data;
|
|
104
|
+
ui_1.logger.step('Conta');
|
|
105
|
+
console.log(`${chalk_1.default.dim('email')} ${chalk_1.default.white(user.email)}`);
|
|
106
|
+
console.log(`${chalk_1.default.dim('id')} ${chalk_1.default.white(user.id)}`);
|
|
107
|
+
console.log(`${chalk_1.default.dim('tipo')} ${chalk_1.default.cyan(user.isAdmin ? 'admin' : 'user')}`);
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err?.response?.status === 401) {
|
|
112
|
+
ui_1.logger.error('Sua sessao expirou ou foi revogada. Rode `nivo login` de novo.');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
ui_1.logger.error(`Falha ao buscar usuario: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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.link = link;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const ora_1 = __importDefault(require("ora"));
|
|
12
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
13
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
14
|
+
const api_1 = require("../api");
|
|
15
|
+
const config_1 = require("../config");
|
|
16
|
+
const ui_1 = require("../ui");
|
|
17
|
+
const CONFIG_FILE = '.nivo';
|
|
18
|
+
const IGNORED_NAMES = new Set([
|
|
19
|
+
'.git',
|
|
20
|
+
'.next',
|
|
21
|
+
'node_modules',
|
|
22
|
+
'.env',
|
|
23
|
+
'.env.local',
|
|
24
|
+
'.env.production',
|
|
25
|
+
'.env.development',
|
|
26
|
+
'.DS_Store',
|
|
27
|
+
]);
|
|
28
|
+
const EXAMPLE_CONFIG = `{
|
|
29
|
+
"name": "testapp",
|
|
30
|
+
"projectId": "PROJECT_ID",
|
|
31
|
+
"sourceType": "zip",
|
|
32
|
+
"type": "site",
|
|
33
|
+
"buildSystem": "nivopack",
|
|
34
|
+
"runtime": "node20",
|
|
35
|
+
"installCmd": "npm install",
|
|
36
|
+
"buildCmd": "npm run build",
|
|
37
|
+
"startCmd": "npm start",
|
|
38
|
+
"ramMb": 256,
|
|
39
|
+
"subdomain": "testapp"
|
|
40
|
+
}`;
|
|
41
|
+
function readConfig(configPath) {
|
|
42
|
+
if (!fs_1.default.existsSync(configPath))
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
46
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new Error(`Arquivo .nivo invalido: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function validateConfig(config) {
|
|
53
|
+
const sourceType = config.sourceType ?? 'zip';
|
|
54
|
+
const missing = [];
|
|
55
|
+
if (!config.appId) {
|
|
56
|
+
if (!config.name?.trim())
|
|
57
|
+
missing.push('name');
|
|
58
|
+
if (!config.projectId?.trim())
|
|
59
|
+
missing.push('projectId');
|
|
60
|
+
if (sourceType === 'github' && !config.repoFullName?.trim())
|
|
61
|
+
missing.push('repoFullName');
|
|
62
|
+
}
|
|
63
|
+
if (!['zip', 'github'].includes(sourceType))
|
|
64
|
+
missing.push('sourceType');
|
|
65
|
+
if (config.buildSystem && !['nivopack', 'nixpacks', 'dockerfile'].includes(config.buildSystem))
|
|
66
|
+
missing.push('buildSystem');
|
|
67
|
+
if (config.type && !['site', 'worker'].includes(config.type))
|
|
68
|
+
missing.push('type');
|
|
69
|
+
if (config.ramMb !== undefined && (!Number.isFinite(Number(config.ramMb)) || Number(config.ramMb) < 100))
|
|
70
|
+
missing.push('ramMb');
|
|
71
|
+
if (missing.length) {
|
|
72
|
+
throw new Error(`.nivo incompleto ou invalido. Corrija: ${missing.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function cleanOptional(value) {
|
|
76
|
+
const trimmed = value?.trim();
|
|
77
|
+
return trimmed ? trimmed : undefined;
|
|
78
|
+
}
|
|
79
|
+
function normalizeSubdomainInput(value) {
|
|
80
|
+
const cleaned = value?.trim().toLowerCase().replace(/[^a-z0-9-]/g, '').replace(/^-+|-+$/g, '');
|
|
81
|
+
return cleaned || undefined;
|
|
82
|
+
}
|
|
83
|
+
function appPayload(config) {
|
|
84
|
+
return {
|
|
85
|
+
name: config.name?.trim(),
|
|
86
|
+
type: config.type ?? 'site',
|
|
87
|
+
buildSystem: config.buildSystem ?? 'nivopack',
|
|
88
|
+
runtime: config.runtime ?? 'node20',
|
|
89
|
+
installCmd: cleanOptional(config.installCmd),
|
|
90
|
+
buildCmd: cleanOptional(config.buildCmd),
|
|
91
|
+
startCmd: cleanOptional(config.startCmd) ?? 'npm start',
|
|
92
|
+
ramMb: Number(config.ramMb ?? (config.type === 'worker' ? 100 : 256)),
|
|
93
|
+
envVars: {},
|
|
94
|
+
subdomain: normalizeSubdomainInput(config.subdomain),
|
|
95
|
+
sourceType: config.sourceType ?? 'zip',
|
|
96
|
+
repoFullName: cleanOptional(config.repoFullName),
|
|
97
|
+
repoBranch: cleanOptional(config.repoBranch),
|
|
98
|
+
projectId: config.projectId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function updatePayload(config) {
|
|
102
|
+
const payload = {};
|
|
103
|
+
for (const key of ['name', 'type', 'buildSystem', 'runtime', 'installCmd', 'buildCmd', 'startCmd', 'ramMb', 'repoBranch']) {
|
|
104
|
+
if (config[key] !== undefined)
|
|
105
|
+
payload[key] = config[key];
|
|
106
|
+
}
|
|
107
|
+
if (config.subdomain !== undefined)
|
|
108
|
+
payload.subdomain = normalizeSubdomainInput(config.subdomain) ?? '';
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
function writeConfig(configPath, config) {
|
|
112
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify({
|
|
113
|
+
...config,
|
|
114
|
+
linkedAt: new Date().toISOString(),
|
|
115
|
+
}, null, 2));
|
|
116
|
+
}
|
|
117
|
+
function addDir(zip, dir, root) {
|
|
118
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
if (IGNORED_NAMES.has(entry.name) || entry.name.endsWith('.log'))
|
|
120
|
+
continue;
|
|
121
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
122
|
+
const relPath = path_1.default.relative(root, fullPath).replace(/\\/g, '/');
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
addDir(zip, fullPath, root);
|
|
125
|
+
}
|
|
126
|
+
else if (entry.isFile()) {
|
|
127
|
+
zip.addLocalFile(fullPath, path_1.default.dirname(relPath) === '.' ? '' : path_1.default.dirname(relPath));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function createZip(sourceDir) {
|
|
132
|
+
const resolved = path_1.default.resolve(sourceDir);
|
|
133
|
+
const zip = new adm_zip_1.default();
|
|
134
|
+
addDir(zip, resolved, resolved);
|
|
135
|
+
const out = path_1.default.join(os_1.default.tmpdir(), `nivo-link-${Date.now()}.zip`);
|
|
136
|
+
zip.writeZip(out);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
async function uploadZipSource(appId, zipPath) {
|
|
140
|
+
const form = new form_data_1.default();
|
|
141
|
+
form.append('file', fs_1.default.createReadStream(zipPath), {
|
|
142
|
+
filename: 'app.zip',
|
|
143
|
+
contentType: 'application/zip',
|
|
144
|
+
});
|
|
145
|
+
await api_1.api.post(`/apps/${encodeURIComponent(appId)}/upload-source`, form, {
|
|
146
|
+
headers: form.getHeaders(),
|
|
147
|
+
maxBodyLength: Infinity,
|
|
148
|
+
maxContentLength: Infinity,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function link() {
|
|
152
|
+
if (!(0, config_1.getCredentials)().token) {
|
|
153
|
+
ui_1.logger.error('Nao autenticado. Rode `nivo login`.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const configPath = path_1.default.join(process.cwd(), CONFIG_FILE);
|
|
157
|
+
let config;
|
|
158
|
+
try {
|
|
159
|
+
config = readConfig(configPath);
|
|
160
|
+
if (!config) {
|
|
161
|
+
ui_1.logger.error('Arquivo .nivo nao encontrado.');
|
|
162
|
+
ui_1.logger.info('Crie .nivo na raiz do projeto e rode `nivo link` de novo.');
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(chalk_1.default.gray(EXAMPLE_CONFIG));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
validateConfig(config);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
ui_1.logger.error(err.message);
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(chalk_1.default.gray(EXAMPLE_CONFIG));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const spinner = (0, ora_1.default)({ text: config.appId ? 'Aplicando .nivo...' : 'Criando app...', color: 'green' }).start();
|
|
176
|
+
let zipPath = null;
|
|
177
|
+
try {
|
|
178
|
+
let app;
|
|
179
|
+
if (config.appId) {
|
|
180
|
+
const payload = updatePayload(config);
|
|
181
|
+
if (Object.keys(payload).length) {
|
|
182
|
+
try {
|
|
183
|
+
const res = await api_1.api.patch(`/apps/${encodeURIComponent(config.appId)}`, payload);
|
|
184
|
+
app = res.data;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
if (err?.response?.status === 404) {
|
|
188
|
+
throw new Error('appId do .nivo nao existe. Remova "appId" do .nivo para criar um app novo, ou coloque o appId correto.');
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
try {
|
|
195
|
+
const res = await api_1.api.get(`/apps/${encodeURIComponent(config.appId)}`);
|
|
196
|
+
app = res.data;
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (err?.response?.status === 404) {
|
|
200
|
+
throw new Error('appId do .nivo nao existe. Remova "appId" do .nivo para criar um app novo, ou coloque o appId correto.');
|
|
201
|
+
}
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const res = await api_1.api.post('/apps', appPayload(config));
|
|
208
|
+
app = res.data;
|
|
209
|
+
config.appId = app.id;
|
|
210
|
+
}
|
|
211
|
+
if ((config.sourceType ?? 'zip') === 'zip') {
|
|
212
|
+
spinner.text = 'Enviando codigo...';
|
|
213
|
+
zipPath = createZip(process.cwd());
|
|
214
|
+
await uploadZipSource(app.id, zipPath);
|
|
215
|
+
}
|
|
216
|
+
writeConfig(configPath, config);
|
|
217
|
+
spinner.succeed(`Link concluido ${chalk_1.default.dim(app.id)}`);
|
|
218
|
+
ui_1.logger.info('config .nivo atualizada');
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
spinner.fail(`Falha no link: ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (zipPath)
|
|
225
|
+
fs_1.default.rmSync(zipPath, { force: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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.getCredentials = getCredentials;
|
|
7
|
+
exports.saveCredentials = saveCredentials;
|
|
8
|
+
exports.deleteCredentials = deleteCredentials;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const AUTH_FILE = path_1.default.join(os_1.default.homedir(), '.nivo-auth');
|
|
13
|
+
function getCredentials() {
|
|
14
|
+
if (!fs_1.default.existsSync(AUTH_FILE))
|
|
15
|
+
return {};
|
|
16
|
+
try {
|
|
17
|
+
const token = fs_1.default.readFileSync(AUTH_FILE, 'utf-8').trim();
|
|
18
|
+
return { token };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveCredentials(creds) {
|
|
25
|
+
if (creds.token) {
|
|
26
|
+
fs_1.default.writeFileSync(AUTH_FILE, creds.token, { mode: 0o600 });
|
|
27
|
+
try {
|
|
28
|
+
fs_1.default.chmodSync(AUTH_FILE, 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function deleteCredentials() {
|
|
34
|
+
if (fs_1.default.existsSync(AUTH_FILE)) {
|
|
35
|
+
fs_1.default.unlinkSync(AUTH_FILE);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const auth_1 = require("./commands/auth");
|
|
6
|
+
const link_1 = require("./commands/link");
|
|
7
|
+
const apps_1 = require("./commands/apps");
|
|
8
|
+
const ui_1 = require("./ui");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name('nivo')
|
|
12
|
+
.helpOption(false)
|
|
13
|
+
.addHelpCommand(false);
|
|
14
|
+
program
|
|
15
|
+
.command('login')
|
|
16
|
+
.description('Log in to Nivo')
|
|
17
|
+
.action(auth_1.login);
|
|
18
|
+
program
|
|
19
|
+
.command('logout')
|
|
20
|
+
.description('Log out')
|
|
21
|
+
.action(auth_1.logout);
|
|
22
|
+
program
|
|
23
|
+
.command('me')
|
|
24
|
+
.description('Print the current logged-in user')
|
|
25
|
+
.action(auth_1.me);
|
|
26
|
+
program
|
|
27
|
+
.command('link')
|
|
28
|
+
.description('Link the current directory to a Nivo application')
|
|
29
|
+
.action(link_1.link);
|
|
30
|
+
program
|
|
31
|
+
.command('apps')
|
|
32
|
+
.description('List your Nivo applications')
|
|
33
|
+
.action(apps_1.listApps);
|
|
34
|
+
program
|
|
35
|
+
.command('deploy [path]')
|
|
36
|
+
.description('Deploy the linked app. Zip apps upload the directory; GitHub apps trigger redeploy.')
|
|
37
|
+
.option('-a, --app <appId>', 'App ID. Defaults to .nivo in the current directory')
|
|
38
|
+
.option('-w, --watch', 'Stream deployment logs until it finishes')
|
|
39
|
+
.action((sourcePath = '.', opts) => (0, apps_1.deploy)(sourcePath, opts));
|
|
40
|
+
program
|
|
41
|
+
.command('deployments')
|
|
42
|
+
.description('List deployments for the linked app')
|
|
43
|
+
.option('-a, --app <appId>', 'App ID. Defaults to .nivo in the current directory')
|
|
44
|
+
.action(apps_1.deployments);
|
|
45
|
+
program
|
|
46
|
+
.command('logs')
|
|
47
|
+
.description('Print runtime logs or deployment logs for the linked app')
|
|
48
|
+
.option('-a, --app <appId>', 'App ID. Defaults to .nivo in the current directory')
|
|
49
|
+
.option('-d, --deployment <deploymentId>', 'Deployment ID')
|
|
50
|
+
.action(apps_1.logs);
|
|
51
|
+
(0, ui_1.printBanner)();
|
|
52
|
+
program.parse(process.argv);
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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.logger = void 0;
|
|
7
|
+
exports.printBanner = printBanner;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
function printBanner() {
|
|
10
|
+
if (process.env.NIVO_CLI_BANNER !== '1')
|
|
11
|
+
return;
|
|
12
|
+
console.log(chalk_1.default.bold.hex('#22c55e')('Nivo') + chalk_1.default.dim(' CLI v1.0.0'));
|
|
13
|
+
}
|
|
14
|
+
exports.logger = {
|
|
15
|
+
success: (msg) => console.log(`${chalk_1.default.green('ok')} ${msg}`),
|
|
16
|
+
error: (msg) => console.log(`${chalk_1.default.red('error')} ${msg}`),
|
|
17
|
+
info: (msg) => console.log(`${chalk_1.default.cyan('info')} ${msg}`),
|
|
18
|
+
warn: (msg) => console.log(`${chalk_1.default.yellow('warn')} ${msg}`),
|
|
19
|
+
step: (msg) => console.log(`${chalk_1.default.bold(msg)}`),
|
|
20
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nivo-lat/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Nivo CLI - Deploy and manage applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nivo": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "ts-node src/index.ts",
|
|
15
|
+
"prepack": "npm run build",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"axios": "^1.15.2",
|
|
20
|
+
"adm-zip": "^0.5.16",
|
|
21
|
+
"chalk": "^4.1.2",
|
|
22
|
+
"commander": "^11.1.0",
|
|
23
|
+
"form-data": "^4.0.0",
|
|
24
|
+
"follow-redirects": "^1.16.0",
|
|
25
|
+
"inquirer": "^8.2.6",
|
|
26
|
+
"ora": "^5.4.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/adm-zip": "^0.5.8",
|
|
30
|
+
"@types/inquirer": "^8.2.10",
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"ts-node": "^10.9.1",
|
|
33
|
+
"typescript": "^5.2.2"
|
|
34
|
+
}
|
|
35
|
+
}
|