@neetru/cli 1.0.0 → 2.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/CHANGELOG.md +136 -0
- package/README.md +109 -152
- package/dist/commands/add.d.ts +8 -3
- package/dist/commands/add.js +70 -143
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/ai.d.ts +4 -0
- package/dist/commands/ai.js +88 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/autocomplete.d.ts +7 -0
- package/dist/commands/autocomplete.js +107 -0
- package/dist/commands/autocomplete.js.map +1 -0
- package/dist/commands/build.d.ts +18 -0
- package/dist/commands/build.js +288 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +70 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/db.d.ts +14 -0
- package/dist/commands/db.js +187 -0
- package/dist/commands/db.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -3
- package/dist/commands/deploy.js +400 -180
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.d.ts +27 -0
- package/dist/commands/doctor.js +211 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env.d.ts +15 -0
- package/dist/commands/env.js +56 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/fn.d.ts +6 -0
- package/dist/commands/fn.js +87 -0
- package/dist/commands/fn.js.map +1 -0
- package/dist/commands/init.d.ts +10 -3
- package/dist/commands/init.js +212 -143
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +6 -3
- package/dist/commands/login.js +222 -92
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +28 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/logs.d.ts +14 -3
- package/dist/commands/logs.js +132 -106
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/mocks.d.ts +5 -0
- package/dist/commands/mocks.js +23 -0
- package/dist/commands/mocks.js.map +1 -0
- package/dist/commands/open.d.ts +4 -3
- package/dist/commands/open.js +53 -85
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/promote.d.ts +9 -0
- package/dist/commands/promote.js +114 -0
- package/dist/commands/promote.js.map +1 -0
- package/dist/commands/publish.d.ts +14 -0
- package/dist/commands/publish.js +180 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/status.d.ts +5 -3
- package/dist/commands/status.js +91 -93
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +77 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/validate.d.ts +1 -3
- package/dist/commands/validate.js +83 -91
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.d.ts +5 -3
- package/dist/commands/whoami.js +76 -28
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +337 -36
- package/dist/index.js.map +1 -1
- package/dist/lib/ai/context.d.ts +11 -0
- package/dist/lib/ai/context.js +112 -0
- package/dist/lib/ai/context.js.map +1 -0
- package/dist/lib/ai/orchestrator.d.ts +10 -0
- package/dist/lib/ai/orchestrator.js +92 -0
- package/dist/lib/ai/orchestrator.js.map +1 -0
- package/dist/lib/api-client.d.ts +21 -0
- package/dist/lib/api-client.js +65 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth.d.ts +15 -0
- package/dist/lib/auth.js +98 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config-schema.d.ts +165 -0
- package/dist/lib/config-schema.js +57 -0
- package/dist/lib/config-schema.js.map +1 -0
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.js +33 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +35 -33
- package/templates/auth/callback.ts +22 -0
- package/templates/auth/sign-in.tsx +41 -0
- package/templates/billing/checkout.ts +22 -0
- package/templates/billing/page.tsx +43 -0
- package/templates/support/ticket-form.tsx +68 -0
- package/templates/usage/track.ts +30 -0
- package/templates/users/profile.tsx +43 -0
- package/LICENSE +0 -21
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/deploy.d.ts.map +0 -1
- package/dist/commands/generate-types.d.ts +0 -3
- package/dist/commands/generate-types.d.ts.map +0 -1
- package/dist/commands/generate-types.js +0 -150
- package/dist/commands/generate-types.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/open.d.ts.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/config.d.ts +0 -14
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -83
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/scaffold/auth.d.ts +0 -3
- package/dist/scaffold/auth.d.ts.map +0 -1
- package/dist/scaffold/auth.js +0 -228
- package/dist/scaffold/auth.js.map +0 -1
- package/dist/scaffold/billing.d.ts +0 -3
- package/dist/scaffold/billing.d.ts.map +0 -1
- package/dist/scaffold/billing.js +0 -184
- package/dist/scaffold/billing.js.map +0 -1
- package/dist/scaffold/usage.d.ts +0 -3
- package/dist/scaffold/usage.d.ts.map +0 -1
- package/dist/scaffold/usage.js +0 -173
- package/dist/scaffold/usage.js.map +0 -1
- package/dist/scaffold/users.d.ts +0 -3
- package/dist/scaffold/users.d.ts.map +0 -1
- package/dist/scaffold/users.js +0 -135
- package/dist/scaffold/users.js.map +0 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -1,196 +1,416 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.deployCommand = deployCommand;
|
|
40
|
-
const commander_1 = require("commander");
|
|
41
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
-
const ora_1 = __importDefault(require("ora"));
|
|
43
|
-
const config_1 = require("../config");
|
|
44
|
-
async function prompt(questions) {
|
|
45
|
-
const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
|
|
46
|
-
return inquirer.prompt(questions);
|
|
1
|
+
/**
|
|
2
|
+
* `neetru deploy` — pipeline interativo de deploy do produto.
|
|
3
|
+
*
|
|
4
|
+
* Sprint 3 (P1-1): substitui o stub vazio. Fluxo:
|
|
5
|
+
* 1. Resolve produto (lista via /api/v1/cli/catalog).
|
|
6
|
+
* 2. Detecta stack (product_config + fallback local).
|
|
7
|
+
* 3. Escolhe target (cloud-run | vm-existente | provisionar-nova).
|
|
8
|
+
* 4. Para target=vm: lista servers online (/api/v1/cli/servers) +
|
|
9
|
+
* escolha capacity-aware.
|
|
10
|
+
* 5. Domínio + porta (auto-detect ou input).
|
|
11
|
+
* 6. Build (chama runBuild se artifact ausente).
|
|
12
|
+
* 7. Upload artifact (Sprint 1: usa file:// local — owner roda em VM com
|
|
13
|
+
* acesso ao mesmo FS, OU define `NEETRU_ARTIFACT_BUCKET` e o CLI faz
|
|
14
|
+
* upload via signed URL — Sprint 2.5).
|
|
15
|
+
* 8. POST /api/v1/cli/deploy → recebe deploymentId + statusUrl.
|
|
16
|
+
* 9. Polling /api/v1/cli/deploy/status até estado terminal.
|
|
17
|
+
*
|
|
18
|
+
* NÃO bloqueia em modo `--non-interactive` quando flags suficientes vieram.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import { promises as fsp } from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import inquirer from 'inquirer';
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import ora from 'ora';
|
|
26
|
+
import { log } from '../utils/logger.js';
|
|
27
|
+
import { apiRequest, CliApiError, CliNetworkError } from '../lib/api-client.js';
|
|
28
|
+
import { runBuild } from './build.js';
|
|
29
|
+
const TERMINAL_STATUSES = new Set(['success', 'failed', 'cancelled']);
|
|
30
|
+
function fileExists(p) {
|
|
31
|
+
try {
|
|
32
|
+
fs.statSync(p);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
47
38
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
39
|
+
function isTty() {
|
|
40
|
+
return !!process.stdout.isTTY && !!process.stdin.isTTY;
|
|
41
|
+
}
|
|
42
|
+
function ensureNonInteractive(opts) {
|
|
43
|
+
return !!opts.nonInteractive || !isTty();
|
|
44
|
+
}
|
|
45
|
+
async function loadLocalConfig(cwd) {
|
|
46
|
+
for (const filename of ['neetru.config.json', '.neetru.json']) {
|
|
47
|
+
const full = path.join(cwd, filename);
|
|
48
|
+
if (!fileExists(full))
|
|
49
|
+
continue;
|
|
58
50
|
try {
|
|
59
|
-
|
|
51
|
+
return JSON.parse(await fsp.readFile(full, 'utf8'));
|
|
60
52
|
}
|
|
61
|
-
catch
|
|
62
|
-
|
|
63
|
-
process.exit(1);
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
64
55
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async function loadEnvFile(envFile) {
|
|
60
|
+
const content = await fsp.readFile(envFile, 'utf8');
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const line of content.split(/\r?\n/)) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
65
|
+
continue;
|
|
66
|
+
const eq = trimmed.indexOf('=');
|
|
67
|
+
if (eq === -1)
|
|
68
|
+
continue;
|
|
69
|
+
const k = trimmed.slice(0, eq).trim();
|
|
70
|
+
let v = trimmed.slice(eq + 1).trim();
|
|
71
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
72
|
+
v = v.slice(1, -1);
|
|
73
|
+
}
|
|
74
|
+
if (k)
|
|
75
|
+
out[k] = v;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function handleApiError(error) {
|
|
80
|
+
if (error instanceof CliApiError) {
|
|
81
|
+
if (error.status === 401) {
|
|
82
|
+
log.error('Token inválido ou expirado. Execute: neetru login');
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
85
|
+
if (error.status === 403) {
|
|
86
|
+
log.error(`Permissão negada: ${error.message}`);
|
|
87
|
+
process.exit(3);
|
|
88
|
+
}
|
|
89
|
+
if (error.status === 501) {
|
|
90
|
+
log.error(`Recurso não implementado: ${error.message}`);
|
|
91
|
+
process.exit(5);
|
|
92
|
+
}
|
|
93
|
+
log.error(`Erro do servidor (${error.status}): ${error.message}`);
|
|
94
|
+
process.exit(4);
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof CliNetworkError) {
|
|
97
|
+
log.error(error.message);
|
|
98
|
+
process.exit(4);
|
|
99
|
+
}
|
|
100
|
+
log.error(error.message);
|
|
101
|
+
process.exit(4);
|
|
102
|
+
}
|
|
103
|
+
async function pickProduct(opts, nonInteractive, cwd) {
|
|
104
|
+
if (opts.product)
|
|
105
|
+
return opts.product;
|
|
106
|
+
const local = await loadLocalConfig(cwd);
|
|
107
|
+
if (local?.slug)
|
|
108
|
+
return local.slug;
|
|
109
|
+
if (nonInteractive) {
|
|
110
|
+
log.error('--product <slug> é obrigatório em modo não interativo.');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const spinner = ora({ text: 'Buscando catálogo…', color: 'blue' }).start();
|
|
114
|
+
let catalog;
|
|
115
|
+
try {
|
|
116
|
+
catalog = await apiRequest('/api/v1/cli/catalog');
|
|
117
|
+
spinner.stop();
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
spinner.fail('Não consegui buscar o catálogo.');
|
|
121
|
+
handleApiError(err);
|
|
122
|
+
}
|
|
123
|
+
if (catalog.products.length === 0) {
|
|
124
|
+
log.error('Nenhum produto público encontrado. Crie um via `neetru publish` antes.');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const { slug } = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
type: 'list',
|
|
130
|
+
name: 'slug',
|
|
131
|
+
message: 'Produto a deployar:',
|
|
132
|
+
choices: catalog.products.map((p) => ({
|
|
133
|
+
name: `${p.name} ${chalk.dim('(' + p.slug + ')')} ${chalk.dim('· ' + p.status)}`,
|
|
134
|
+
value: p.slug,
|
|
135
|
+
})),
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
return slug;
|
|
139
|
+
}
|
|
140
|
+
async function pickTarget(opts, nonInteractive) {
|
|
141
|
+
if (opts.target)
|
|
142
|
+
return opts.target;
|
|
143
|
+
if (nonInteractive)
|
|
144
|
+
return 'vm';
|
|
145
|
+
const { target } = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: 'list',
|
|
148
|
+
name: 'target',
|
|
149
|
+
message: 'Onde deployar?',
|
|
150
|
+
default: 'vm',
|
|
151
|
+
choices: [
|
|
152
|
+
{ name: 'VM existente (custo fixo, sem cold start)', value: 'vm' },
|
|
153
|
+
{ name: 'Workspace (Sprint 4 runtime per-tenant — Cloud Run privado)', value: 'workspace' },
|
|
154
|
+
{ name: 'Cloud Run (escala 0→N) — não implementado ainda', value: 'cloud-run' },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
return target;
|
|
159
|
+
}
|
|
160
|
+
async function pickServer(opts, nonInteractive) {
|
|
161
|
+
const spinner = ora({ text: 'Buscando servidores online…', color: 'blue' }).start();
|
|
162
|
+
let resp;
|
|
163
|
+
try {
|
|
164
|
+
resp = await apiRequest('/api/v1/cli/servers?status=online&capacity=true');
|
|
165
|
+
spinner.stop();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
spinner.fail('Não consegui listar servers.');
|
|
169
|
+
handleApiError(err);
|
|
170
|
+
}
|
|
171
|
+
if (resp.servers.length === 0) {
|
|
172
|
+
log.error('Nenhum servidor online encontrado.');
|
|
173
|
+
log.dim(' Provisione um com: POST /api/v1/cli/servers/provision');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (opts.server) {
|
|
177
|
+
const found = resp.servers.find((s) => s.id === opts.server || s.name === opts.server);
|
|
178
|
+
if (!found) {
|
|
179
|
+
log.error(`Server "${opts.server}" não encontrado entre os online.`);
|
|
69
180
|
process.exit(1);
|
|
70
181
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
182
|
+
return found;
|
|
183
|
+
}
|
|
184
|
+
if (nonInteractive) {
|
|
185
|
+
log.error('--server <id> é obrigatório em modo não interativo.');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
const { serverId } = await inquirer.prompt([
|
|
189
|
+
{
|
|
190
|
+
type: 'list',
|
|
191
|
+
name: 'serverId',
|
|
192
|
+
message: 'Server alvo:',
|
|
193
|
+
choices: resp.servers.map((s) => {
|
|
194
|
+
const ramFree = s.availableRamMb !== null && s.availableRamMb !== undefined
|
|
195
|
+
? `${s.availableRamMb}MB livres`
|
|
196
|
+
: 'capacidade desconhecida';
|
|
197
|
+
return {
|
|
198
|
+
name: `${s.name ?? s.id} ${chalk.dim('(' + (s.provider ?? '?') + '/' + (s.region ?? '?') + ')')} · ${ramFree}`,
|
|
199
|
+
value: s.id,
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
return resp.servers.find((s) => s.id === serverId);
|
|
205
|
+
}
|
|
206
|
+
async function pickDomainPort(opts, nonInteractive, productSlug) {
|
|
207
|
+
let domain = opts.domain;
|
|
208
|
+
let port = opts.port;
|
|
209
|
+
if (nonInteractive)
|
|
210
|
+
return { domain, port };
|
|
211
|
+
if (!domain) {
|
|
212
|
+
const { d } = await inquirer.prompt([
|
|
213
|
+
{
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'd',
|
|
216
|
+
message: `Domínio (vazio = sem vhost):`,
|
|
217
|
+
default: `${productSlug}.neetru.com`,
|
|
218
|
+
},
|
|
219
|
+
]);
|
|
220
|
+
domain = d.trim() || undefined;
|
|
221
|
+
}
|
|
222
|
+
if (!port) {
|
|
223
|
+
const { p } = await inquirer.prompt([
|
|
224
|
+
{
|
|
225
|
+
type: 'input',
|
|
226
|
+
name: 'p',
|
|
227
|
+
message: 'Porta interna do app:',
|
|
228
|
+
default: '3000',
|
|
229
|
+
validate: (v) => {
|
|
230
|
+
const n = Number.parseInt(v, 10);
|
|
231
|
+
if (Number.isNaN(n) || n < 1 || n > 65535)
|
|
232
|
+
return 'Porta inválida (1-65535).';
|
|
233
|
+
return true;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
port = Number.parseInt(p, 10);
|
|
238
|
+
}
|
|
239
|
+
return { domain, port };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Sprint 1: artifactUrl pode ser:
|
|
243
|
+
* - `--artifact-url=<URL>` flag (caller já fez upload).
|
|
244
|
+
* - `--artifact=<file>` flag → CLI calcula sha256 + retorna `file://`
|
|
245
|
+
* (apenas pra deploy interno, com VM acessando o mesmo FS — Sprint 1).
|
|
246
|
+
* - Nada → CLI roda `neetru build` e devolve `file://` do tarball local.
|
|
247
|
+
*
|
|
248
|
+
* Sprint 2.5 vai introduzir signed-URL upload pra GCS — endpoint novo
|
|
249
|
+
* `POST /api/v1/cli/artifacts/sign`.
|
|
250
|
+
*/
|
|
251
|
+
async function resolveArtifact(opts, productSlug, cwd) {
|
|
252
|
+
if (opts.artifactUrl && opts.artifactSha256) {
|
|
253
|
+
return { artifactUrl: opts.artifactUrl, artifactSha256: opts.artifactSha256 };
|
|
254
|
+
}
|
|
255
|
+
if (opts.artifact) {
|
|
256
|
+
const abs = path.resolve(cwd, opts.artifact);
|
|
257
|
+
if (!fileExists(abs)) {
|
|
258
|
+
log.error(`Artifact não encontrado: ${abs}`);
|
|
74
259
|
process.exit(1);
|
|
75
260
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
261
|
+
const { createHash } = await import('node:crypto');
|
|
262
|
+
const hash = createHash('sha256');
|
|
263
|
+
const stream = fs.createReadStream(abs);
|
|
264
|
+
for await (const chunk of stream)
|
|
265
|
+
hash.update(chunk);
|
|
266
|
+
const stat = await fsp.stat(abs);
|
|
267
|
+
return {
|
|
268
|
+
artifactUrl: `file://${abs.replace(/\\/g, '/')}`,
|
|
269
|
+
artifactSha256: hash.digest('hex'),
|
|
270
|
+
artifactSizeBytes: stat.size,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// Procura manifest existente, senão chama runBuild.
|
|
274
|
+
const manifestPath = path.join(cwd, '.neetru-build', 'manifest.json');
|
|
275
|
+
let manifest = null;
|
|
276
|
+
if (fileExists(manifestPath)) {
|
|
277
|
+
try {
|
|
278
|
+
manifest = JSON.parse(await fsp.readFile(manifestPath, 'utf8'));
|
|
279
|
+
if (manifest.slug !== productSlug)
|
|
280
|
+
manifest = null;
|
|
88
281
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
282
|
+
catch {
|
|
283
|
+
manifest = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!manifest) {
|
|
287
|
+
log.info('Nenhum artifact encontrado. Rodando `neetru build`…');
|
|
288
|
+
manifest = await runBuild({ product: productSlug, version: opts.version });
|
|
289
|
+
}
|
|
290
|
+
const tarAbs = path.resolve(cwd, manifest.tarball);
|
|
291
|
+
return {
|
|
292
|
+
artifactUrl: `file://${tarAbs.replace(/\\/g, '/')}`,
|
|
293
|
+
artifactSha256: manifest.sha256,
|
|
294
|
+
artifactSizeBytes: manifest.sizeBytes,
|
|
295
|
+
manifest,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function pollDeploymentStatus(deploymentId) {
|
|
299
|
+
const start = Date.now();
|
|
300
|
+
const timeoutMs = 20 * 60 * 1000; // 20min
|
|
301
|
+
let lastStatus = '';
|
|
302
|
+
const spinner = ora({ text: 'Aguardando agente…', color: 'blue' }).start();
|
|
303
|
+
while (Date.now() - start < timeoutMs) {
|
|
304
|
+
let resp;
|
|
94
305
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
clientId: config.clientId,
|
|
103
|
-
environment: env,
|
|
104
|
-
branch: options.branch,
|
|
105
|
-
}),
|
|
106
|
-
signal: AbortSignal.timeout(15000),
|
|
107
|
-
});
|
|
108
|
-
if (!res.ok) {
|
|
109
|
-
const body = await res.json().catch(() => ({}));
|
|
110
|
-
spinner.fail(`HTTP ${res.status}: ${body.error ?? 'Erro desconhecido'}`);
|
|
111
|
-
process.exit(1);
|
|
306
|
+
resp = await apiRequest(`/api/v1/cli/deploy/status?id=${encodeURIComponent(deploymentId)}`);
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
// transient — segue tentando.
|
|
310
|
+
if (err instanceof CliNetworkError) {
|
|
311
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
312
|
+
continue;
|
|
112
313
|
}
|
|
113
|
-
|
|
114
|
-
|
|
314
|
+
spinner.fail('Falha ao consultar status.');
|
|
315
|
+
handleApiError(err);
|
|
115
316
|
}
|
|
116
|
-
|
|
117
|
-
spinner.
|
|
118
|
-
|
|
317
|
+
if (resp.status !== lastStatus) {
|
|
318
|
+
spinner.text = `Status: ${chalk.bold(resp.status)} · ${resp.steps
|
|
319
|
+
.map((s) => `${s.name}=${s.status}`)
|
|
320
|
+
.join(', ')}`;
|
|
321
|
+
lastStatus = resp.status;
|
|
119
322
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return;
|
|
323
|
+
if (TERMINAL_STATUSES.has(resp.status)) {
|
|
324
|
+
if (resp.status === 'success')
|
|
325
|
+
spinner.succeed(`Deploy ${chalk.bold('success')}.`);
|
|
326
|
+
else
|
|
327
|
+
spinner.fail(`Deploy ${chalk.bold(resp.status)}.`);
|
|
328
|
+
return resp;
|
|
127
329
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
194
|
-
|
|
330
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
331
|
+
}
|
|
332
|
+
spinner.fail('Timeout aguardando deploy.');
|
|
333
|
+
throw new Error('deploy_timeout');
|
|
334
|
+
}
|
|
335
|
+
export async function runDeploy(opts) {
|
|
336
|
+
log.banner();
|
|
337
|
+
const cwd = process.cwd();
|
|
338
|
+
const nonInteractive = ensureNonInteractive(opts);
|
|
339
|
+
const productSlug = await pickProduct(opts, nonInteractive, cwd);
|
|
340
|
+
const target = await pickTarget(opts, nonInteractive);
|
|
341
|
+
let serverChoice = null;
|
|
342
|
+
if (target === 'vm') {
|
|
343
|
+
serverChoice = await pickServer(opts, nonInteractive);
|
|
344
|
+
}
|
|
345
|
+
// target='workspace' não exige server — backend resolve runtime per-tenant.
|
|
346
|
+
const { domain, port } = await pickDomainPort(opts, nonInteractive, productSlug);
|
|
347
|
+
// Build / artifact.
|
|
348
|
+
const artifact = await resolveArtifact(opts, productSlug, cwd);
|
|
349
|
+
// EnvVars.
|
|
350
|
+
let envVars;
|
|
351
|
+
if (opts.envFile) {
|
|
352
|
+
envVars = await loadEnvFile(path.resolve(cwd, opts.envFile));
|
|
353
|
+
}
|
|
354
|
+
// Confirmação interativa.
|
|
355
|
+
if (!nonInteractive) {
|
|
356
|
+
log.heading('Resumo');
|
|
357
|
+
log.dim(` produto: ${productSlug}`);
|
|
358
|
+
log.dim(` versão: ${artifact.manifest?.version ?? opts.version ?? '(detectada no build)'}`);
|
|
359
|
+
log.dim(` stack: ${opts.stack ?? artifact.manifest?.stack ?? '(detectada no build)'}`);
|
|
360
|
+
log.dim(` target: ${target}`);
|
|
361
|
+
if (serverChoice)
|
|
362
|
+
log.dim(` server: ${serverChoice.name ?? serverChoice.id}`);
|
|
363
|
+
if (domain)
|
|
364
|
+
log.dim(` domain: ${domain}`);
|
|
365
|
+
if (port)
|
|
366
|
+
log.dim(` port: ${port}`);
|
|
367
|
+
log.dim(` artifact: ${artifact.artifactUrl}`);
|
|
368
|
+
log.dim(` sha256: ${artifact.artifactSha256.slice(0, 16)}…`);
|
|
369
|
+
const { ok } = await inquirer.prompt([
|
|
370
|
+
{ type: 'confirm', name: 'ok', message: 'Confirmar deploy?', default: true },
|
|
371
|
+
]);
|
|
372
|
+
if (!ok) {
|
|
373
|
+
log.warn('Deploy cancelado.');
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const body = {
|
|
378
|
+
productSlug,
|
|
379
|
+
version: artifact.manifest?.version ?? opts.version ?? '0.0.0',
|
|
380
|
+
stack: opts.stack ?? artifact.manifest?.stack ?? 'node',
|
|
381
|
+
target,
|
|
382
|
+
artifactUrl: artifact.artifactUrl,
|
|
383
|
+
artifactSha256: artifact.artifactSha256,
|
|
384
|
+
artifactSizeBytes: artifact.artifactSizeBytes,
|
|
385
|
+
};
|
|
386
|
+
if (target === 'vm') {
|
|
387
|
+
body.serverId = serverChoice.id;
|
|
388
|
+
body.port = port;
|
|
389
|
+
}
|
|
390
|
+
if (domain)
|
|
391
|
+
body.domain = domain;
|
|
392
|
+
if (envVars && Object.keys(envVars).length > 0)
|
|
393
|
+
body.envVars = envVars;
|
|
394
|
+
let deploy;
|
|
395
|
+
const dispatchSpinner = ora({ text: 'Enviando comando ao Core…', color: 'blue' }).start();
|
|
396
|
+
try {
|
|
397
|
+
deploy = await apiRequest('/api/v1/cli/deploy', {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
body,
|
|
400
|
+
});
|
|
401
|
+
dispatchSpinner.succeed(`Deploy criado: ${chalk.bold(deploy.deploymentId)}`);
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
dispatchSpinner.fail('Falha ao criar deploy.');
|
|
405
|
+
handleApiError(err);
|
|
406
|
+
}
|
|
407
|
+
const final = await pollDeploymentStatus(deploy.deploymentId);
|
|
408
|
+
if (final.status !== 'success') {
|
|
409
|
+
log.error(final.lastError ?? `Deploy terminou com status ${final.status}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
if (final.domain)
|
|
413
|
+
log.success(`Live em https://${final.domain}`);
|
|
414
|
+
process.exit(0);
|
|
195
415
|
}
|
|
196
416
|
//# sourceMappingURL=deploy.js.map
|