@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +109 -152
  3. package/dist/commands/add.d.ts +8 -3
  4. package/dist/commands/add.js +70 -143
  5. package/dist/commands/add.js.map +1 -1
  6. package/dist/commands/ai.d.ts +4 -0
  7. package/dist/commands/ai.js +88 -0
  8. package/dist/commands/ai.js.map +1 -0
  9. package/dist/commands/autocomplete.d.ts +7 -0
  10. package/dist/commands/autocomplete.js +107 -0
  11. package/dist/commands/autocomplete.js.map +1 -0
  12. package/dist/commands/build.d.ts +18 -0
  13. package/dist/commands/build.js +288 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/config.d.ts +3 -0
  16. package/dist/commands/config.js +70 -0
  17. package/dist/commands/config.js.map +1 -0
  18. package/dist/commands/db.d.ts +14 -0
  19. package/dist/commands/db.js +187 -0
  20. package/dist/commands/db.js.map +1 -0
  21. package/dist/commands/deploy.d.ts +16 -3
  22. package/dist/commands/deploy.js +400 -180
  23. package/dist/commands/deploy.js.map +1 -1
  24. package/dist/commands/doctor.d.ts +27 -0
  25. package/dist/commands/doctor.js +211 -0
  26. package/dist/commands/doctor.js.map +1 -0
  27. package/dist/commands/env.d.ts +15 -0
  28. package/dist/commands/env.js +56 -0
  29. package/dist/commands/env.js.map +1 -0
  30. package/dist/commands/fn.d.ts +6 -0
  31. package/dist/commands/fn.js +87 -0
  32. package/dist/commands/fn.js.map +1 -0
  33. package/dist/commands/init.d.ts +10 -3
  34. package/dist/commands/init.js +212 -143
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/login.d.ts +6 -3
  37. package/dist/commands/login.js +222 -92
  38. package/dist/commands/login.js.map +1 -1
  39. package/dist/commands/logout.d.ts +1 -0
  40. package/dist/commands/logout.js +28 -0
  41. package/dist/commands/logout.js.map +1 -0
  42. package/dist/commands/logs.d.ts +14 -3
  43. package/dist/commands/logs.js +132 -106
  44. package/dist/commands/logs.js.map +1 -1
  45. package/dist/commands/mocks.d.ts +5 -0
  46. package/dist/commands/mocks.js +23 -0
  47. package/dist/commands/mocks.js.map +1 -0
  48. package/dist/commands/open.d.ts +4 -3
  49. package/dist/commands/open.js +53 -85
  50. package/dist/commands/open.js.map +1 -1
  51. package/dist/commands/promote.d.ts +9 -0
  52. package/dist/commands/promote.js +114 -0
  53. package/dist/commands/promote.js.map +1 -0
  54. package/dist/commands/publish.d.ts +14 -0
  55. package/dist/commands/publish.js +180 -0
  56. package/dist/commands/publish.js.map +1 -0
  57. package/dist/commands/status.d.ts +5 -3
  58. package/dist/commands/status.js +91 -93
  59. package/dist/commands/status.js.map +1 -1
  60. package/dist/commands/upgrade.d.ts +12 -0
  61. package/dist/commands/upgrade.js +77 -0
  62. package/dist/commands/upgrade.js.map +1 -0
  63. package/dist/commands/validate.d.ts +1 -3
  64. package/dist/commands/validate.js +83 -91
  65. package/dist/commands/validate.js.map +1 -1
  66. package/dist/commands/whoami.d.ts +5 -3
  67. package/dist/commands/whoami.js +76 -28
  68. package/dist/commands/whoami.js.map +1 -1
  69. package/dist/index.d.ts +0 -1
  70. package/dist/index.js +337 -36
  71. package/dist/index.js.map +1 -1
  72. package/dist/lib/ai/context.d.ts +11 -0
  73. package/dist/lib/ai/context.js +112 -0
  74. package/dist/lib/ai/context.js.map +1 -0
  75. package/dist/lib/ai/orchestrator.d.ts +10 -0
  76. package/dist/lib/ai/orchestrator.js +92 -0
  77. package/dist/lib/ai/orchestrator.js.map +1 -0
  78. package/dist/lib/api-client.d.ts +21 -0
  79. package/dist/lib/api-client.js +65 -0
  80. package/dist/lib/api-client.js.map +1 -0
  81. package/dist/lib/auth.d.ts +15 -0
  82. package/dist/lib/auth.js +98 -0
  83. package/dist/lib/auth.js.map +1 -0
  84. package/dist/lib/config-schema.d.ts +165 -0
  85. package/dist/lib/config-schema.js +57 -0
  86. package/dist/lib/config-schema.js.map +1 -0
  87. package/dist/lib/config.d.ts +15 -0
  88. package/dist/lib/config.js +33 -0
  89. package/dist/lib/config.js.map +1 -0
  90. package/dist/utils/logger.d.ts +13 -0
  91. package/dist/utils/logger.js +27 -0
  92. package/dist/utils/logger.js.map +1 -0
  93. package/package.json +35 -33
  94. package/templates/auth/callback.ts +22 -0
  95. package/templates/auth/sign-in.tsx +41 -0
  96. package/templates/billing/checkout.ts +22 -0
  97. package/templates/billing/page.tsx +43 -0
  98. package/templates/support/ticket-form.tsx +68 -0
  99. package/templates/usage/track.ts +30 -0
  100. package/templates/users/profile.tsx +43 -0
  101. package/LICENSE +0 -21
  102. package/dist/commands/add.d.ts.map +0 -1
  103. package/dist/commands/deploy.d.ts.map +0 -1
  104. package/dist/commands/generate-types.d.ts +0 -3
  105. package/dist/commands/generate-types.d.ts.map +0 -1
  106. package/dist/commands/generate-types.js +0 -150
  107. package/dist/commands/generate-types.js.map +0 -1
  108. package/dist/commands/init.d.ts.map +0 -1
  109. package/dist/commands/login.d.ts.map +0 -1
  110. package/dist/commands/logs.d.ts.map +0 -1
  111. package/dist/commands/open.d.ts.map +0 -1
  112. package/dist/commands/status.d.ts.map +0 -1
  113. package/dist/commands/validate.d.ts.map +0 -1
  114. package/dist/commands/whoami.d.ts.map +0 -1
  115. package/dist/config.d.ts +0 -14
  116. package/dist/config.d.ts.map +0 -1
  117. package/dist/config.js +0 -83
  118. package/dist/config.js.map +0 -1
  119. package/dist/index.d.ts.map +0 -1
  120. package/dist/scaffold/auth.d.ts +0 -3
  121. package/dist/scaffold/auth.d.ts.map +0 -1
  122. package/dist/scaffold/auth.js +0 -228
  123. package/dist/scaffold/auth.js.map +0 -1
  124. package/dist/scaffold/billing.d.ts +0 -3
  125. package/dist/scaffold/billing.d.ts.map +0 -1
  126. package/dist/scaffold/billing.js +0 -184
  127. package/dist/scaffold/billing.js.map +0 -1
  128. package/dist/scaffold/usage.d.ts +0 -3
  129. package/dist/scaffold/usage.d.ts.map +0 -1
  130. package/dist/scaffold/usage.js +0 -173
  131. package/dist/scaffold/usage.js.map +0 -1
  132. package/dist/scaffold/users.d.ts +0 -3
  133. package/dist/scaffold/users.d.ts.map +0 -1
  134. package/dist/scaffold/users.js +0 -135
  135. package/dist/scaffold/users.js.map +0 -1
@@ -1,196 +1,416 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
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 deployCommand() {
49
- const cmd = new commander_1.Command('deploy');
50
- cmd
51
- .description('Dispara um deploy do workspace para o ambiente alvo')
52
- .option('-e, --env <env>', 'Ambiente alvo: dev | staging | prod', 'dev')
53
- .option('--branch <branch>', 'Branch a deployar (default: main)', 'main')
54
- .option('--watch', 'Acompanhar progresso em tempo real')
55
- .option('--yes', 'Pular confirmação')
56
- .action(async (options) => {
57
- let config;
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
- config = (0, config_1.requireConfig)();
51
+ return JSON.parse(await fsp.readFile(full, 'utf8'));
60
52
  }
61
- catch (e) {
62
- console.error(chalk_1.default.red(e.message));
63
- process.exit(1);
53
+ catch {
54
+ return null;
64
55
  }
65
- const global = (0, config_1.readGlobalConfig)();
66
- const apiKey = global.clientSecret ?? process.env.NEETRU_CLI_API_KEY;
67
- if (!apiKey) {
68
- console.error(chalk_1.default.red('Não autenticado. Execute neetru login primeiro.'));
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
- const env = options.env;
72
- if (!['dev', 'staging', 'prod'].includes(env)) {
73
- console.error(chalk_1.default.red(`Ambiente inválido: ${env}. Use dev | staging | prod.`));
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
- // Confirmation for prod
77
- if (env === 'prod' && !options.yes) {
78
- const { confirm } = await prompt([{
79
- type: 'confirm',
80
- name: 'confirm',
81
- message: chalk_1.default.yellow.bold(`⚠ Deploy para PRODUÇÃO (branch: ${options.branch}). Continuar?`),
82
- default: false,
83
- }]);
84
- if (!confirm) {
85
- console.log(chalk_1.default.dim('Cancelado.'));
86
- return;
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
- console.log('\n' + chalk_1.default.bold(`🚀 Deploy → ${chalk_1.default.cyan(env)}`));
90
- console.log(chalk_1.default.dim(` Branch: ${options.branch}`));
91
- console.log(chalk_1.default.dim(` Workspace: ${config.clientId}\n`));
92
- const spinner = (0, ora_1.default)('Disparando deploy...').start();
93
- let deployResp;
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
- const res = await fetch(`${config.coreUrl}/api/v1/cli/deploy`, {
96
- method: 'POST',
97
- headers: {
98
- Authorization: `Bearer ${apiKey}`,
99
- 'Content-Type': 'application/json',
100
- },
101
- body: JSON.stringify({
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
- deployResp = await res.json();
114
- spinner.succeed(`Deploy iniciado: ${chalk_1.default.cyan(deployResp.deploymentId)}`);
314
+ spinner.fail('Falha ao consultar status.');
315
+ handleApiError(err);
115
316
  }
116
- catch (e) {
117
- spinner.fail(`Erro: ${e.message}`);
118
- process.exit(1);
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 (!options.watch) {
121
- console.log('');
122
- console.log(chalk_1.default.dim('Acompanhe em:'));
123
- console.log(chalk_1.default.cyan(` ${config.coreUrl}/deployments/${deployResp.deploymentId}`));
124
- console.log(chalk_1.default.dim(`Ou use: neetru deploy --watch`));
125
- console.log('');
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
- // Watch mode poll status every 3s
129
- console.log('');
130
- const watchSpinner = (0, ora_1.default)('Aguardando steps...').start();
131
- const seenSteps = new Set();
132
- let attempts = 0;
133
- const maxAttempts = 200; // 10min timeout
134
- const poll = async () => {
135
- attempts++;
136
- try {
137
- const res = await fetch(`${config.coreUrl}/api/v1/cli/deployments/${deployResp.deploymentId}`, {
138
- headers: { Authorization: `Bearer ${apiKey}` },
139
- signal: AbortSignal.timeout(8000),
140
- });
141
- if (!res.ok)
142
- return false;
143
- const status = await res.json();
144
- for (const step of status.steps) {
145
- const key = `${step.name}:${step.status}`;
146
- if (seenSteps.has(key))
147
- continue;
148
- seenSteps.add(key);
149
- const icon = step.status === 'success' ? chalk_1.default.green('✓')
150
- : step.status === 'failed' ? chalk_1.default.red('✗')
151
- : step.status === 'running' ? chalk_1.default.yellow('●')
152
- : chalk_1.default.dim('○');
153
- const label = step.status === 'pending' ? chalk_1.default.dim(step.name) : step.name;
154
- if (step.status !== 'pending') {
155
- watchSpinner.stop();
156
- console.log(` ${icon} ${label} ${chalk_1.default.dim(`(${step.status})`)}`);
157
- if (status.steps.some((s) => s.status === 'pending' || s.status === 'running')) {
158
- watchSpinner.start('Aguardando próximo step...');
159
- }
160
- }
161
- }
162
- if (['success', 'failed', 'rolled_back'].includes(status.status)) {
163
- watchSpinner.stop();
164
- if (status.status === 'success') {
165
- console.log('\n' + chalk_1.default.green.bold('✓ Deploy concluído com sucesso'));
166
- if (status.url)
167
- console.log(chalk_1.default.cyan(` → ${status.url}`));
168
- }
169
- else {
170
- console.log('\n' + chalk_1.default.red.bold(`✗ Deploy falhou (${status.status})`));
171
- if (status.error)
172
- console.log(chalk_1.default.dim(` ${status.error}`));
173
- }
174
- console.log('');
175
- return true;
176
- }
177
- return false;
178
- }
179
- catch {
180
- return false;
181
- }
182
- };
183
- const interval = setInterval(async () => {
184
- const done = await poll();
185
- if (done || attempts >= maxAttempts) {
186
- clearInterval(interval);
187
- if (attempts >= maxAttempts) {
188
- watchSpinner.fail('Timeout (10min). Verifique no painel.');
189
- process.exit(1);
190
- }
191
- }
192
- }, 3000);
193
- });
194
- return cmd;
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