@neetru/cli 1.0.1 → 2.1.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 (192) 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/agent-release.d.ts +13 -0
  7. package/dist/commands/agent-release.js +204 -0
  8. package/dist/commands/agent-release.js.map +1 -0
  9. package/dist/commands/agent-write.d.ts +12 -0
  10. package/dist/commands/agent-write.js +94 -0
  11. package/dist/commands/agent-write.js.map +1 -0
  12. package/dist/commands/ai.d.ts +4 -0
  13. package/dist/commands/ai.js +88 -0
  14. package/dist/commands/ai.js.map +1 -0
  15. package/dist/commands/api-catalog.d.ts +20 -0
  16. package/dist/commands/api-catalog.js +126 -0
  17. package/dist/commands/api-catalog.js.map +1 -0
  18. package/dist/commands/audit.d.ts +8 -0
  19. package/dist/commands/audit.js +69 -0
  20. package/dist/commands/audit.js.map +1 -0
  21. package/dist/commands/autocomplete.d.ts +7 -0
  22. package/dist/commands/autocomplete.js +107 -0
  23. package/dist/commands/autocomplete.js.map +1 -0
  24. package/dist/commands/billing.d.ts +6 -0
  25. package/dist/commands/billing.js +69 -0
  26. package/dist/commands/billing.js.map +1 -0
  27. package/dist/commands/build.d.ts +18 -0
  28. package/dist/commands/build.js +295 -0
  29. package/dist/commands/build.js.map +1 -0
  30. package/dist/commands/cloud-run.d.ts +11 -0
  31. package/dist/commands/cloud-run.js +87 -0
  32. package/dist/commands/cloud-run.js.map +1 -0
  33. package/dist/commands/config.d.ts +3 -0
  34. package/dist/commands/config.js +70 -0
  35. package/dist/commands/config.js.map +1 -0
  36. package/dist/commands/db.d.ts +14 -0
  37. package/dist/commands/db.js +187 -0
  38. package/dist/commands/db.js.map +1 -0
  39. package/dist/commands/deploy.d.ts +23 -3
  40. package/dist/commands/deploy.js +530 -177
  41. package/dist/commands/deploy.js.map +1 -1
  42. package/dist/commands/deployments.d.ts +11 -0
  43. package/dist/commands/deployments.js +69 -0
  44. package/dist/commands/deployments.js.map +1 -0
  45. package/dist/commands/doctor.d.ts +27 -0
  46. package/dist/commands/doctor.js +211 -0
  47. package/dist/commands/doctor.js.map +1 -0
  48. package/dist/commands/dr.d.ts +11 -0
  49. package/dist/commands/dr.js +79 -0
  50. package/dist/commands/dr.js.map +1 -0
  51. package/dist/commands/env.d.ts +15 -0
  52. package/dist/commands/env.js +56 -0
  53. package/dist/commands/env.js.map +1 -0
  54. package/dist/commands/fn.d.ts +6 -0
  55. package/dist/commands/fn.js +87 -0
  56. package/dist/commands/fn.js.map +1 -0
  57. package/dist/commands/infra-read.d.ts +9 -0
  58. package/dist/commands/infra-read.js +113 -0
  59. package/dist/commands/infra-read.js.map +1 -0
  60. package/dist/commands/init.d.ts +10 -3
  61. package/dist/commands/init.js +275 -142
  62. package/dist/commands/init.js.map +1 -1
  63. package/dist/commands/login.d.ts +6 -3
  64. package/dist/commands/login.js +222 -92
  65. package/dist/commands/login.js.map +1 -1
  66. package/dist/commands/logout.d.ts +1 -0
  67. package/dist/commands/logout.js +28 -0
  68. package/dist/commands/logout.js.map +1 -0
  69. package/dist/commands/logs.d.ts +14 -3
  70. package/dist/commands/logs.js +132 -106
  71. package/dist/commands/logs.js.map +1 -1
  72. package/dist/commands/mocks.d.ts +5 -0
  73. package/dist/commands/mocks.js +23 -0
  74. package/dist/commands/mocks.js.map +1 -0
  75. package/dist/commands/open.d.ts +4 -3
  76. package/dist/commands/open.js +53 -85
  77. package/dist/commands/open.js.map +1 -1
  78. package/dist/commands/products-db.d.ts +37 -0
  79. package/dist/commands/products-db.js +230 -0
  80. package/dist/commands/products-db.js.map +1 -0
  81. package/dist/commands/products.d.ts +12 -0
  82. package/dist/commands/products.js +97 -0
  83. package/dist/commands/products.js.map +1 -0
  84. package/dist/commands/promote.d.ts +9 -0
  85. package/dist/commands/promote.js +114 -0
  86. package/dist/commands/promote.js.map +1 -0
  87. package/dist/commands/publish.d.ts +14 -0
  88. package/dist/commands/publish.js +180 -0
  89. package/dist/commands/publish.js.map +1 -0
  90. package/dist/commands/servers.d.ts +23 -0
  91. package/dist/commands/servers.js +166 -0
  92. package/dist/commands/servers.js.map +1 -0
  93. package/dist/commands/status.d.ts +5 -3
  94. package/dist/commands/status.js +91 -93
  95. package/dist/commands/status.js.map +1 -1
  96. package/dist/commands/support.d.ts +25 -0
  97. package/dist/commands/support.js +184 -0
  98. package/dist/commands/support.js.map +1 -0
  99. package/dist/commands/surface-status.d.ts +5 -0
  100. package/dist/commands/surface-status.js +63 -0
  101. package/dist/commands/surface-status.js.map +1 -0
  102. package/dist/commands/tenants.d.ts +34 -0
  103. package/dist/commands/tenants.js +179 -0
  104. package/dist/commands/tenants.js.map +1 -0
  105. package/dist/commands/upgrade.d.ts +12 -0
  106. package/dist/commands/upgrade.js +77 -0
  107. package/dist/commands/upgrade.js.map +1 -0
  108. package/dist/commands/validate.d.ts +1 -3
  109. package/dist/commands/validate.js +83 -91
  110. package/dist/commands/validate.js.map +1 -1
  111. package/dist/commands/whoami.d.ts +5 -3
  112. package/dist/commands/whoami.js +76 -28
  113. package/dist/commands/whoami.js.map +1 -1
  114. package/dist/commands/workspaces.d.ts +15 -0
  115. package/dist/commands/workspaces.js +72 -0
  116. package/dist/commands/workspaces.js.map +1 -0
  117. package/dist/index.d.ts +0 -1
  118. package/dist/index.js +1094 -36
  119. package/dist/index.js.map +1 -1
  120. package/dist/lib/ai/context.d.ts +11 -0
  121. package/dist/lib/ai/context.js +112 -0
  122. package/dist/lib/ai/context.js.map +1 -0
  123. package/dist/lib/ai/orchestrator.d.ts +10 -0
  124. package/dist/lib/ai/orchestrator.js +92 -0
  125. package/dist/lib/ai/orchestrator.js.map +1 -0
  126. package/dist/lib/api-client.d.ts +39 -0
  127. package/dist/lib/api-client.js +185 -0
  128. package/dist/lib/api-client.js.map +1 -0
  129. package/dist/lib/auth.d.ts +15 -0
  130. package/dist/lib/auth.js +98 -0
  131. package/dist/lib/auth.js.map +1 -0
  132. package/dist/lib/cli-read.d.ts +13 -0
  133. package/dist/lib/cli-read.js +103 -0
  134. package/dist/lib/cli-read.js.map +1 -0
  135. package/dist/lib/cli-write.d.ts +47 -0
  136. package/dist/lib/cli-write.js +137 -0
  137. package/dist/lib/cli-write.js.map +1 -0
  138. package/dist/lib/config-schema.d.ts +165 -0
  139. package/dist/lib/config-schema.js +57 -0
  140. package/dist/lib/config-schema.js.map +1 -0
  141. package/dist/lib/config.d.ts +15 -0
  142. package/dist/lib/config.js +33 -0
  143. package/dist/lib/config.js.map +1 -0
  144. package/dist/lib/render.d.ts +16 -0
  145. package/dist/lib/render.js +74 -0
  146. package/dist/lib/render.js.map +1 -0
  147. package/dist/utils/logger.d.ts +13 -0
  148. package/dist/utils/logger.js +27 -0
  149. package/dist/utils/logger.js.map +1 -0
  150. package/package.json +35 -33
  151. package/templates/auth/callback.ts +22 -0
  152. package/templates/auth/sign-in.tsx +41 -0
  153. package/templates/billing/checkout.ts +22 -0
  154. package/templates/billing/page.tsx +43 -0
  155. package/templates/support/ticket-form.tsx +68 -0
  156. package/templates/usage/track.ts +30 -0
  157. package/templates/users/profile.tsx +43 -0
  158. package/LICENSE +0 -21
  159. package/dist/commands/add.d.ts.map +0 -1
  160. package/dist/commands/deploy.d.ts.map +0 -1
  161. package/dist/commands/generate-types.d.ts +0 -3
  162. package/dist/commands/generate-types.d.ts.map +0 -1
  163. package/dist/commands/generate-types.js +0 -150
  164. package/dist/commands/generate-types.js.map +0 -1
  165. package/dist/commands/init.d.ts.map +0 -1
  166. package/dist/commands/login.d.ts.map +0 -1
  167. package/dist/commands/logs.d.ts.map +0 -1
  168. package/dist/commands/open.d.ts.map +0 -1
  169. package/dist/commands/status.d.ts.map +0 -1
  170. package/dist/commands/validate.d.ts.map +0 -1
  171. package/dist/commands/whoami.d.ts.map +0 -1
  172. package/dist/config.d.ts +0 -14
  173. package/dist/config.d.ts.map +0 -1
  174. package/dist/config.js +0 -83
  175. package/dist/config.js.map +0 -1
  176. package/dist/index.d.ts.map +0 -1
  177. package/dist/scaffold/auth.d.ts +0 -3
  178. package/dist/scaffold/auth.d.ts.map +0 -1
  179. package/dist/scaffold/auth.js +0 -228
  180. package/dist/scaffold/auth.js.map +0 -1
  181. package/dist/scaffold/billing.d.ts +0 -3
  182. package/dist/scaffold/billing.d.ts.map +0 -1
  183. package/dist/scaffold/billing.js +0 -184
  184. package/dist/scaffold/billing.js.map +0 -1
  185. package/dist/scaffold/usage.d.ts +0 -3
  186. package/dist/scaffold/usage.d.ts.map +0 -1
  187. package/dist/scaffold/usage.js +0 -173
  188. package/dist/scaffold/usage.js.map +0 -1
  189. package/dist/scaffold/users.d.ts +0 -3
  190. package/dist/scaffold/users.d.ts.map +0 -1
  191. package/dist/scaffold/users.js +0 -135
  192. package/dist/scaffold/users.js.map +0 -1
@@ -1,196 +1,549 @@
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.'));
69
- process.exit(1);
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);
70
73
  }
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.`));
74
- process.exit(1);
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);
75
84
  }
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
- }
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
+ // `cloud-run` é aceito via flag explícita `--target=cloud-run` (staff/forward-
146
+ // compat), mas NÃO aparece no menu interativo — runtime ainda não está pronto
147
+ // e não se anuncia feature incompleta pro dev.
148
+ const { target } = await inquirer.prompt([
149
+ {
150
+ type: 'list',
151
+ name: 'target',
152
+ message: 'Onde deployar?',
153
+ default: 'vm',
154
+ choices: [
155
+ { name: 'VM existente (custo fixo, sem cold start)', value: 'vm' },
156
+ { name: 'Workspace (runtime gerenciado per-tenant — Cloud Run privado)', value: 'workspace' },
157
+ ],
158
+ },
159
+ ]);
160
+ return target;
161
+ }
162
+ async function pickServer(opts, nonInteractive) {
163
+ const spinner = ora({ text: 'Buscando servidores online…', color: 'blue' }).start();
164
+ let resp;
165
+ try {
166
+ resp = await apiRequest('/api/cli/v1/servers?status=online&capacity=true');
167
+ spinner.stop();
168
+ }
169
+ catch (err) {
170
+ spinner.fail('Não consegui listar servers.');
171
+ handleApiError(err);
172
+ }
173
+ if (resp.servers.length === 0) {
174
+ log.error('Nenhum servidor online encontrado.');
175
+ log.dim(' Provisione um com: POST /api/v1/cli/servers/provision');
176
+ process.exit(1);
177
+ }
178
+ if (opts.server) {
179
+ const found = resp.servers.find((s) => s.id === opts.server || s.name === opts.server);
180
+ if (!found) {
181
+ log.error(`Server "${opts.server}" não encontrado entre os online.`);
182
+ process.exit(1);
88
183
  }
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;
184
+ return found;
185
+ }
186
+ if (nonInteractive) {
187
+ log.error('--server <id> é obrigatório em modo não interativo.');
188
+ process.exit(1);
189
+ }
190
+ const { serverId } = await inquirer.prompt([
191
+ {
192
+ type: 'list',
193
+ name: 'serverId',
194
+ message: 'Server alvo:',
195
+ choices: resp.servers.map((s) => {
196
+ const ramFree = s.availableRamMb !== null && s.availableRamMb !== undefined
197
+ ? `${s.availableRamMb}MB livres`
198
+ : 'capacidade desconhecida';
199
+ return {
200
+ name: `${s.name ?? s.id} ${chalk.dim('(' + (s.provider ?? '?') + '/' + (s.region ?? '?') + ')')} · ${ramFree}`,
201
+ value: s.id,
202
+ };
203
+ }),
204
+ },
205
+ ]);
206
+ return resp.servers.find((s) => s.id === serverId);
207
+ }
208
+ async function pickDomainPort(opts, nonInteractive, productSlug) {
209
+ let domain = opts.domain;
210
+ let port = opts.port;
211
+ if (nonInteractive)
212
+ return { domain, port };
213
+ if (!domain) {
214
+ const { d } = await inquirer.prompt([
215
+ {
216
+ type: 'input',
217
+ name: 'd',
218
+ message: `Domínio (vazio = sem vhost):`,
219
+ default: `${productSlug}.neetru.com`,
220
+ },
221
+ ]);
222
+ domain = d.trim() || undefined;
223
+ }
224
+ if (!port) {
225
+ const { p } = await inquirer.prompt([
226
+ {
227
+ type: 'input',
228
+ name: 'p',
229
+ message: 'Porta interna do app:',
230
+ default: '3000',
231
+ validate: (v) => {
232
+ const n = Number.parseInt(v, 10);
233
+ if (Number.isNaN(n) || n < 1 || n > 65535)
234
+ return 'Porta inválida (1-65535).';
235
+ return true;
236
+ },
237
+ },
238
+ ]);
239
+ port = Number.parseInt(p, 10);
240
+ }
241
+ return { domain, port };
242
+ }
243
+ /**
244
+ * F-11 (2026-05-16): determina se devemos manter o artifactUrl como `file://`
245
+ * em vez de fazer upload pra GCS. Default = false (upload é o caminho normal).
246
+ *
247
+ * Razões pra retornar true:
248
+ * - `--local-artifact` explícito (dev sabe o que está fazendo).
249
+ * - `NEETRU_DEV_LOCAL_ARTIFACT=true` no env.
250
+ * - `--artifact-url` já é `file://` (caller forçou — não fazemos upload de
251
+ * um path que não controlamos).
252
+ * - `--artifact-url` já é `gs://` ou `https://` (caller fez upload sozinho).
253
+ */
254
+ function shouldKeepLocalArtifact(opts) {
255
+ if (opts.localArtifact)
256
+ return true;
257
+ if (process.env.NEETRU_DEV_LOCAL_ARTIFACT === 'true')
258
+ return true;
259
+ return false;
260
+ }
261
+ /**
262
+ * F-11: sobe o tarball local pra GCS via signed URL V4. 1 retry em caso de
263
+ * falha de rede; falha persistente aborta com mensagem clara.
264
+ *
265
+ * @returns `gs://...` canônico pra usar como artifactUrl no POST de deploy.
266
+ */
267
+ async function uploadArtifactToGcs(tarballAbs, productSlug, envName, sizeBytes) {
268
+ const filename = path.basename(tarballAbs);
269
+ // Pede signed URL ao Core.
270
+ const signSpinner = ora({
271
+ text: `Solicitando signed URL pra ${filename}…`,
272
+ color: 'blue',
273
+ }).start();
274
+ let signed;
275
+ try {
276
+ signed = await apiRequest('/api/cli/v1/deploy/upload-url', {
277
+ method: 'POST',
278
+ body: {
279
+ productId: productSlug,
280
+ env: envName,
281
+ filename,
282
+ contentLength: sizeBytes,
283
+ contentType: 'application/gzip',
284
+ },
285
+ });
286
+ signSpinner.succeed(`Signed URL emitida (expira em ${signed.expiresAt}). deployId=${chalk.bold(signed.deployId)}`);
287
+ }
288
+ catch (err) {
289
+ signSpinner.fail('Falha ao obter signed URL.');
290
+ handleApiError(err);
291
+ }
292
+ // PUT direto pro GCS com 1 retry. global.fetch nativo (Node 18+) aceita
293
+ // ReadableStream e respeita Content-Length quando duplex='half'.
294
+ const attempt = async (attemptNumber) => {
295
+ const stream = fs.createReadStream(tarballAbs);
296
+ const uploadSpinner = ora({
297
+ text: attemptNumber === 1
298
+ ? `Subindo ${(sizeBytes / 1024 / 1024).toFixed(2)} MB pra GCS…`
299
+ : `Retry upload (${attemptNumber}/2)…`,
300
+ color: 'blue',
301
+ }).start();
94
302
  try {
95
- const res = await fetch(`${config.coreUrl}/api/v1/cli/deploy`, {
96
- method: 'POST',
303
+ const resp = await fetch(signed.uploadUrl, {
304
+ method: 'PUT',
305
+ body: stream,
97
306
  headers: {
98
- Authorization: `Bearer ${apiKey}`,
99
- 'Content-Type': 'application/json',
307
+ 'Content-Type': signed.contentType,
308
+ 'Content-Length': String(sizeBytes),
100
309
  },
101
- body: JSON.stringify({
102
- clientId: config.clientId,
103
- environment: env,
104
- branch: options.branch,
105
- }),
106
- signal: AbortSignal.timeout(15000),
310
+ // Node fetch exige duplex='half' quando o body é stream legível.
311
+ // O TS lib.dom não conhece o campo; cast tipado.
312
+ ...{ duplex: 'half' },
107
313
  });
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);
314
+ if (!resp.ok) {
315
+ const text = await resp.text().catch(() => '');
316
+ throw new Error(`HTTP ${resp.status}${text ? ` — ${text.slice(0, 200)}` : ''}`);
112
317
  }
113
- deployResp = await res.json();
114
- spinner.succeed(`Deploy iniciado: ${chalk_1.default.cyan(deployResp.deploymentId)}`);
318
+ uploadSpinner.succeed(`Upload concluído (${signed.gcsPath}).`);
319
+ }
320
+ catch (err) {
321
+ uploadSpinner.fail(`Upload falhou: ${err.message}`);
322
+ throw err;
323
+ }
324
+ };
325
+ try {
326
+ await attempt(1);
327
+ }
328
+ catch {
329
+ try {
330
+ await attempt(2);
331
+ }
332
+ catch (err) {
333
+ log.error(`Upload do artifact falhou após 2 tentativas: ${err.message}. ` +
334
+ 'Verifique conectividade com storage.googleapis.com ou rode com --local-artifact ' +
335
+ 'se o agente está na mesma máquina.');
336
+ process.exit(4);
115
337
  }
116
- catch (e) {
117
- spinner.fail(`Erro: ${e.message}`);
338
+ }
339
+ return { artifactUrl: signed.gcsPath, deployId: signed.deployId };
340
+ }
341
+ /**
342
+ * F-11 (2026-05-16): resolve o artifact pra o POST de deploy.
343
+ *
344
+ * Cascata:
345
+ * 1. `--artifact-url` + `--artifact-sha256` → usa direto (caller fez upload).
346
+ * 2. `--artifact` (arquivo local) → calcula sha256 e parte pra (4).
347
+ * 3. Sem artifact → procura manifest, senão roda `neetru build`. Parte pra (4).
348
+ * 4. Default: faz upload pra GCS via signed URL e devolve `gs://…`.
349
+ * Override: `--local-artifact` ou `NEETRU_DEV_LOCAL_ARTIFACT=true` mantém
350
+ * `file://` (dev em mesma máquina do agent — modo legacy).
351
+ *
352
+ * BUG-008 fix continua válido no backend: `file://` é rejeitado pelo POST de
353
+ * deploy a não ser que o agent rode no mesmo FS. O caminho default agora é gs://.
354
+ */
355
+ async function resolveArtifact(opts, productSlug, envName, cwd) {
356
+ // Caso 1: caller já fez upload e passou URL+sha pronta.
357
+ if (opts.artifactUrl && opts.artifactSha256) {
358
+ return { artifactUrl: opts.artifactUrl, artifactSha256: opts.artifactSha256 };
359
+ }
360
+ // Decide se vamos fazer upload pra GCS ou manter file://.
361
+ const keepLocal = shouldKeepLocalArtifact(opts);
362
+ // Caso 2: --artifact <file> local.
363
+ if (opts.artifact) {
364
+ const abs = path.resolve(cwd, opts.artifact);
365
+ if (!fileExists(abs)) {
366
+ log.error(`Artifact não encontrado: ${abs}`);
118
367
  process.exit(1);
119
368
  }
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;
127
- }
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
- }
369
+ const { createHash } = await import('node:crypto');
370
+ const hash = createHash('sha256');
371
+ const stream = fs.createReadStream(abs);
372
+ for await (const chunk of stream)
373
+ hash.update(chunk);
374
+ const stat = await fsp.stat(abs);
375
+ const sha256 = hash.digest('hex');
376
+ if (keepLocal) {
377
+ log.warn('Modo --local-artifact: mandando file:// pro Core. funciona se o agent compartilha FS com este CLI.');
378
+ return {
379
+ artifactUrl: `file://${abs.replace(/\\/g, '/')}`,
380
+ artifactSha256: sha256,
381
+ artifactSizeBytes: stat.size,
382
+ };
383
+ }
384
+ const uploaded = await uploadArtifactToGcs(abs, productSlug, envName, stat.size);
385
+ return {
386
+ artifactUrl: uploaded.artifactUrl,
387
+ artifactSha256: sha256,
388
+ artifactSizeBytes: stat.size,
389
+ deployId: uploaded.deployId,
390
+ };
391
+ }
392
+ // Caso 3: procura manifest existente, senão chama runBuild.
393
+ const manifestPath = path.join(cwd, '.neetru-build', 'manifest.json');
394
+ let manifest = null;
395
+ if (fileExists(manifestPath)) {
396
+ try {
397
+ manifest = JSON.parse(await fsp.readFile(manifestPath, 'utf8'));
398
+ if (manifest.slug !== productSlug)
399
+ manifest = null;
400
+ }
401
+ catch {
402
+ manifest = null;
403
+ }
404
+ }
405
+ if (!manifest) {
406
+ log.info('Nenhum artifact encontrado. Rodando `neetru build`…');
407
+ manifest = await runBuild({ product: productSlug, version: opts.version });
408
+ }
409
+ const tarAbs = path.resolve(cwd, manifest.tarball);
410
+ if (keepLocal) {
411
+ log.warn('Modo --local-artifact: mandando file:// pro Core. Só funciona se o agent compartilha FS com este CLI.');
412
+ return {
413
+ artifactUrl: `file://${tarAbs.replace(/\\/g, '/')}`,
414
+ artifactSha256: manifest.sha256,
415
+ artifactSizeBytes: manifest.sizeBytes,
416
+ manifest,
182
417
  };
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
- }
418
+ }
419
+ const uploaded = await uploadArtifactToGcs(tarAbs, productSlug, envName, manifest.sizeBytes);
420
+ return {
421
+ artifactUrl: uploaded.artifactUrl,
422
+ artifactSha256: manifest.sha256,
423
+ artifactSizeBytes: manifest.sizeBytes,
424
+ manifest,
425
+ deployId: uploaded.deployId,
426
+ };
427
+ }
428
+ async function pollDeploymentStatus(deploymentId) {
429
+ const start = Date.now();
430
+ const timeoutMs = 20 * 60 * 1000; // 20min
431
+ let lastStatus = '';
432
+ const spinner = ora({ text: 'Aguardando agente…', color: 'blue' }).start();
433
+ while (Date.now() - start < timeoutMs) {
434
+ let resp;
435
+ try {
436
+ resp = await apiRequest(`/api/v1/cli/deploy/status?id=${encodeURIComponent(deploymentId)}`);
437
+ }
438
+ catch (err) {
439
+ // transient — segue tentando.
440
+ if (err instanceof CliNetworkError) {
441
+ await new Promise((r) => setTimeout(r, 3000));
442
+ continue;
191
443
  }
192
- }, 3000);
193
- });
194
- return cmd;
444
+ spinner.fail('Falha ao consultar status.');
445
+ handleApiError(err);
446
+ }
447
+ if (resp.status !== lastStatus) {
448
+ spinner.text = `Status: ${chalk.bold(resp.status)} · ${resp.steps
449
+ .map((s) => `${s.name}=${s.status}`)
450
+ .join(', ')}`;
451
+ lastStatus = resp.status;
452
+ }
453
+ if (TERMINAL_STATUSES.has(resp.status)) {
454
+ if (resp.status === 'success')
455
+ spinner.succeed(`Deploy ${chalk.bold('success')}.`);
456
+ else
457
+ spinner.fail(`Deploy ${chalk.bold(resp.status)}.`);
458
+ return resp;
459
+ }
460
+ await new Promise((r) => setTimeout(r, 4000));
461
+ }
462
+ spinner.fail('Timeout aguardando deploy.');
463
+ throw new Error('deploy_timeout');
464
+ }
465
+ export async function runDeploy(opts) {
466
+ log.banner();
467
+ const cwd = process.cwd();
468
+ const nonInteractive = ensureNonInteractive(opts);
469
+ const productSlug = await pickProduct(opts, nonInteractive, cwd);
470
+ const target = await pickTarget(opts, nonInteractive);
471
+ let serverChoice = null;
472
+ if (target === 'vm') {
473
+ serverChoice = await pickServer(opts, nonInteractive);
474
+ }
475
+ // target='workspace' não exige server — backend resolve runtime per-tenant.
476
+ const { domain, port } = await pickDomainPort(opts, nonInteractive, productSlug);
477
+ // F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
478
+ // Default 'dev' quando não definido — alinhado com o middleware OAuth.
479
+ const envName = (opts.env ?? 'dev').toLowerCase();
480
+ // Build / artifact (default: faz upload pra GCS via signed URL).
481
+ const artifact = await resolveArtifact(opts, productSlug, envName, cwd);
482
+ // EnvVars.
483
+ let envVars;
484
+ if (opts.envFile) {
485
+ envVars = await loadEnvFile(path.resolve(cwd, opts.envFile));
486
+ }
487
+ // Confirmação interativa.
488
+ if (!nonInteractive) {
489
+ log.heading('Resumo');
490
+ log.dim(` produto: ${productSlug}`);
491
+ log.dim(` versão: ${artifact.manifest?.version ?? opts.version ?? '(detectada no build)'}`);
492
+ log.dim(` stack: ${opts.stack ?? artifact.manifest?.stack ?? '(detectada no build)'}`);
493
+ log.dim(` target: ${target}`);
494
+ if (serverChoice)
495
+ log.dim(` server: ${serverChoice.name ?? serverChoice.id}`);
496
+ if (domain)
497
+ log.dim(` domain: ${domain}`);
498
+ if (port)
499
+ log.dim(` port: ${port}`);
500
+ log.dim(` artifact: ${artifact.artifactUrl}`);
501
+ log.dim(` sha256: ${artifact.artifactSha256.slice(0, 16)}…`);
502
+ const { ok } = await inquirer.prompt([
503
+ { type: 'confirm', name: 'ok', message: 'Confirmar deploy?', default: true },
504
+ ]);
505
+ if (!ok) {
506
+ log.warn('Deploy cancelado.');
507
+ process.exit(0);
508
+ }
509
+ }
510
+ const body = {
511
+ productSlug,
512
+ version: artifact.manifest?.version ?? opts.version ?? '0.0.0',
513
+ stack: opts.stack ?? artifact.manifest?.stack ?? 'node',
514
+ target,
515
+ artifactUrl: artifact.artifactUrl,
516
+ artifactSha256: artifact.artifactSha256,
517
+ artifactSizeBytes: artifact.artifactSizeBytes,
518
+ };
519
+ if (target === 'vm') {
520
+ body.serverId = serverChoice.id;
521
+ body.port = port;
522
+ }
523
+ if (domain)
524
+ body.domain = domain;
525
+ if (envVars && Object.keys(envVars).length > 0)
526
+ body.envVars = envVars;
527
+ let deploy;
528
+ const dispatchSpinner = ora({ text: 'Enviando comando ao Core…', color: 'blue' }).start();
529
+ try {
530
+ deploy = await apiRequest('/api/v1/cli/deploy', {
531
+ method: 'POST',
532
+ body,
533
+ });
534
+ dispatchSpinner.succeed(`Deploy criado: ${chalk.bold(deploy.deploymentId)}`);
535
+ }
536
+ catch (err) {
537
+ dispatchSpinner.fail('Falha ao criar deploy.');
538
+ handleApiError(err);
539
+ }
540
+ const final = await pollDeploymentStatus(deploy.deploymentId);
541
+ if (final.status !== 'success') {
542
+ log.error(final.lastError ?? `Deploy terminou com status ${final.status}`);
543
+ process.exit(1);
544
+ }
545
+ if (final.domain)
546
+ log.success(`Live em https://${final.domain}`);
547
+ process.exit(0);
195
548
  }
196
549
  //# sourceMappingURL=deploy.js.map