@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 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
+ }