@h1veframework/cli 0.2.0 → 0.5.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 +25 -8
- package/dist/index.js +294 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,9 +9,7 @@ Parte do tooling do H1VE junto com o **[servidor MCP](https://www.npmjs.com/pack
|
|
|
9
9
|
## Pré-requisitos
|
|
10
10
|
|
|
11
11
|
- **Node.js 18.18+** (`node --version`).
|
|
12
|
-
- Uma conta no H1VE Flow
|
|
13
|
-
- PAT autentica **como você**: leitura **e escrita**. A chave de serviço é **só leitura**.
|
|
14
|
-
- Guarde o PAT com cuidado — é um segredo, mostrado uma vez.
|
|
12
|
+
- Uma conta no H1VE Flow. **Não precisa criar/colar token** — o `nf login` cuida disso.
|
|
15
13
|
|
|
16
14
|
## Instalação
|
|
17
15
|
|
|
@@ -20,21 +18,40 @@ npm i -g @h1veframework/cli
|
|
|
20
18
|
nf --version
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
##
|
|
21
|
+
## Login (recomendado — sem colar nada)
|
|
24
22
|
|
|
25
23
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
nf health # testa a conexão
|
|
24
|
+
nf login # abre o navegador → você autoriza → pronto
|
|
25
|
+
nf health # já funciona
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
O `nf login` abre o navegador (onde você já está logado no H1VE), você clica **Autorizar**, e o CLI **guarda
|
|
29
|
+
a credencial sozinho** em `~/.config/h1ve/credentials.json` (permissão 600). O **[MCP](https://www.npmjs.com/package/@h1veframework/mcp)**
|
|
30
|
+
lê a mesma credencial — um `nf login` configura os dois. `nf logout` remove. **100% automático** (SPEC-078):
|
|
31
|
+
o projeto também é resolvido sozinho pelo repo (SPEC-077), então nada de `H1VE_PROJECT_ID`.
|
|
32
|
+
|
|
32
33
|
> "Nenhum snapshot registrado" no `nf health` é **sucesso** (conectou; projeto sem métricas ainda).
|
|
33
34
|
|
|
35
|
+
## Alternativa: variáveis de ambiente (CI / headless)
|
|
36
|
+
|
|
37
|
+
Sem navegador (ex.: CI), configure por env — tem **precedência** sobre o `nf login`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export H1VE_API_URL="https://app.h1ve.org" # opcional (default já é este)
|
|
41
|
+
export H1VE_API_KEY="nf_pat_..." # PAT criado em app.h1ve.org/api-tokens
|
|
42
|
+
# export H1VE_PROJECT_ID="..." # OPCIONAL — o CLI resolve o projeto pelo repo; use só p/ forçar
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **`H1VE_PROJECT_ID`** (SPEC-076/077): **opcional**. Rodando dentro de um repo conectado a um projeto, o
|
|
46
|
+
> CLI **resolve o projeto sozinho** pelo `git remote` — zero config, mesmo com 2+ projetos. Use só pra
|
|
47
|
+
> **forçar** um projeto (override).
|
|
48
|
+
|
|
34
49
|
## Comandos
|
|
35
50
|
|
|
36
51
|
| Comando | O que faz |
|
|
37
52
|
|---|---|
|
|
53
|
+
| `nf login` | Autoriza no navegador e salva a credencial local (sem colar nada) |
|
|
54
|
+
| `nf logout` | Remove a credencial local |
|
|
38
55
|
| `nf health` | Últimos snapshots de saúde técnica do projeto |
|
|
39
56
|
| `nf status` | Estado da feature da branch atual (stage, dias ativo, blockers, sign-offs) |
|
|
40
57
|
| `nf start [<nº\|id>] [--slug <s>]` | Inicia uma feature atribuída: cria a branch `feat/{você}/{slug}` e grava o slug |
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,53 @@
|
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
5
|
import { readFile } from "fs/promises";
|
|
6
6
|
|
|
7
|
+
// ../core/src/branch.ts
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
var execFileAsync = promisify(execFile);
|
|
11
|
+
var defaultGitRunner = async (args) => {
|
|
12
|
+
const { stdout } = await execFileAsync("git", args, { cwd: process.cwd() });
|
|
13
|
+
return stdout;
|
|
14
|
+
};
|
|
15
|
+
async function currentBranch(runner = defaultGitRunner) {
|
|
16
|
+
let out;
|
|
17
|
+
try {
|
|
18
|
+
out = await runner(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error("NOT_A_GIT_BRANCH");
|
|
21
|
+
}
|
|
22
|
+
const branch = out.trim();
|
|
23
|
+
if (!branch || branch === "HEAD") throw new Error("NOT_A_GIT_BRANCH");
|
|
24
|
+
return branch;
|
|
25
|
+
}
|
|
26
|
+
async function gitRemoteRepo(runner = defaultGitRunner) {
|
|
27
|
+
let out;
|
|
28
|
+
try {
|
|
29
|
+
out = await runner(["remote", "get-url", "origin"]);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("NO_GIT_REMOTE");
|
|
32
|
+
}
|
|
33
|
+
const m = out.trim().match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
34
|
+
if (!m) throw new Error("NO_GIT_REMOTE");
|
|
35
|
+
return `${m[1]}/${m[2]}`;
|
|
36
|
+
}
|
|
37
|
+
function gitErrText(err) {
|
|
38
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
39
|
+
return String(err.stderr ?? "");
|
|
40
|
+
}
|
|
41
|
+
return err instanceof Error ? err.message : "";
|
|
42
|
+
}
|
|
43
|
+
async function createBranch(name, runner = defaultGitRunner) {
|
|
44
|
+
try {
|
|
45
|
+
await runner(["switch", "-c", name]);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const text = gitErrText(err);
|
|
48
|
+
if (/already exists/i.test(text)) throw new Error("BRANCH_EXISTS");
|
|
49
|
+
if (/not a git repository/i.test(text)) throw new Error("NOT_A_GIT_REPO");
|
|
50
|
+
throw new Error("GIT_BRANCH_FAILED");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
7
54
|
// ../core/src/client.ts
|
|
8
55
|
var NexusApiError = class extends Error {
|
|
9
56
|
constructor(code, message) {
|
|
@@ -13,7 +60,13 @@ var NexusApiError = class extends Error {
|
|
|
13
60
|
}
|
|
14
61
|
code;
|
|
15
62
|
};
|
|
16
|
-
function errorForStatus(status2) {
|
|
63
|
+
function errorForStatus(status2, code) {
|
|
64
|
+
if (code === "NO_PROJECT") {
|
|
65
|
+
return new NexusApiError(
|
|
66
|
+
"NO_PROJECT",
|
|
67
|
+
"Voc\xEA tem mais de um projeto. Defina H1VE_PROJECT_ID (o id do projeto) na config do CLI/MCP."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
17
70
|
switch (status2) {
|
|
18
71
|
case 401:
|
|
19
72
|
return new NexusApiError("UNAUTHENTICATED", "H1VE_API_KEY ausente ou inv\xE1lida.");
|
|
@@ -68,7 +121,13 @@ function rootUrl(baseUrl) {
|
|
|
68
121
|
}
|
|
69
122
|
function createClient(config) {
|
|
70
123
|
const doFetch = config.fetchImpl ?? fetch;
|
|
71
|
-
const authHeaders = {
|
|
124
|
+
const authHeaders = {
|
|
125
|
+
"x-api-key": config.apiKey,
|
|
126
|
+
accept: "application/json",
|
|
127
|
+
// SPEC-076: envia o projeto ativo quando configurado. O servidor valida a membership
|
|
128
|
+
// do id e ignora o header onde não escopa — então mandar sempre é simples e seguro.
|
|
129
|
+
...config.projectId ? { "x-project-id": config.projectId } : {}
|
|
130
|
+
};
|
|
72
131
|
async function postAction(featureId, action, body) {
|
|
73
132
|
const url = `${rootUrl(config.baseUrl)}/api/features/${encodeURIComponent(featureId)}/${action}`;
|
|
74
133
|
const res = await doFetch(url, {
|
|
@@ -83,19 +142,19 @@ function createClient(config) {
|
|
|
83
142
|
async getContext(branch) {
|
|
84
143
|
const url = `${rootUrl(config.baseUrl)}/api/context?branch=${encodeURIComponent(branch)}`;
|
|
85
144
|
const res = await doFetch(url, { headers: authHeaders });
|
|
86
|
-
if (!res.ok) throw errorForStatus(res.status);
|
|
145
|
+
if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
|
|
87
146
|
return await res.json();
|
|
88
147
|
},
|
|
89
148
|
async listHealth() {
|
|
90
149
|
const url = `${rootUrl(config.baseUrl)}/api/health`;
|
|
91
150
|
const res = await doFetch(url, { headers: authHeaders });
|
|
92
|
-
if (!res.ok) throw errorForStatus(res.status);
|
|
151
|
+
if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
|
|
93
152
|
return await res.json();
|
|
94
153
|
},
|
|
95
154
|
async listFeatures() {
|
|
96
155
|
const url = `${rootUrl(config.baseUrl)}/api/features`;
|
|
97
156
|
const res = await doFetch(url, { headers: authHeaders });
|
|
98
|
-
if (!res.ok) throw errorForStatus(res.status);
|
|
157
|
+
if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
|
|
99
158
|
return await res.json();
|
|
100
159
|
},
|
|
101
160
|
startFeature(featureId, slug) {
|
|
@@ -113,7 +172,7 @@ function createClient(config) {
|
|
|
113
172
|
async listProjects() {
|
|
114
173
|
const url = `${rootUrl(config.baseUrl)}/api/projects`;
|
|
115
174
|
const res = await doFetch(url, { headers: authHeaders });
|
|
116
|
-
if (!res.ok) throw errorForStatus(res.status);
|
|
175
|
+
if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
|
|
117
176
|
return await res.json();
|
|
118
177
|
},
|
|
119
178
|
async createConnection(projectId, input) {
|
|
@@ -128,42 +187,38 @@ function createClient(config) {
|
|
|
128
187
|
}
|
|
129
188
|
};
|
|
130
189
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
import { execFile } from "child_process";
|
|
134
|
-
import { promisify } from "util";
|
|
135
|
-
var execFileAsync = promisify(execFile);
|
|
136
|
-
var defaultGitRunner = async (args) => {
|
|
137
|
-
const { stdout } = await execFileAsync("git", args, { cwd: process.cwd() });
|
|
138
|
-
return stdout;
|
|
139
|
-
};
|
|
140
|
-
async function currentBranch(runner = defaultGitRunner) {
|
|
141
|
-
let out;
|
|
190
|
+
async function resolveProjectId(config, repo) {
|
|
191
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
142
192
|
try {
|
|
143
|
-
|
|
193
|
+
const url = `${rootUrl(config.baseUrl)}/api/projects/resolve-by-repo?repo=${encodeURIComponent(repo)}`;
|
|
194
|
+
const res = await doFetch(url, { headers: { "x-api-key": config.apiKey, accept: "application/json" } });
|
|
195
|
+
if (!res.ok) return null;
|
|
196
|
+
const body = await res.json();
|
|
197
|
+
return body.project_id ?? null;
|
|
144
198
|
} catch {
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
const branch = out.trim();
|
|
148
|
-
if (!branch || branch === "HEAD") throw new Error("NOT_A_GIT_BRANCH");
|
|
149
|
-
return branch;
|
|
150
|
-
}
|
|
151
|
-
function gitErrText(err) {
|
|
152
|
-
if (err && typeof err === "object" && "stderr" in err) {
|
|
153
|
-
return String(err.stderr ?? "");
|
|
199
|
+
return null;
|
|
154
200
|
}
|
|
155
|
-
return err instanceof Error ? err.message : "";
|
|
156
201
|
}
|
|
157
|
-
async function
|
|
202
|
+
async function autoResolveProjectId(config, runner) {
|
|
158
203
|
try {
|
|
159
|
-
await
|
|
160
|
-
} catch
|
|
161
|
-
|
|
162
|
-
if (/already exists/i.test(text)) throw new Error("BRANCH_EXISTS");
|
|
163
|
-
if (/not a git repository/i.test(text)) throw new Error("NOT_A_GIT_REPO");
|
|
164
|
-
throw new Error("GIT_BRANCH_FAILED");
|
|
204
|
+
return await resolveProjectId(config, await gitRemoteRepo(runner));
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
165
207
|
}
|
|
166
208
|
}
|
|
209
|
+
async function exchangeCliLoginCode(config, code) {
|
|
210
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
211
|
+
const url = `${rootUrl(config.baseUrl)}/api/cli-login/exchange`;
|
|
212
|
+
const res = await doFetch(url, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
215
|
+
body: JSON.stringify({ code })
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
|
|
218
|
+
const body = await res.json();
|
|
219
|
+
if (!body.api_key) throw new NexusApiError("LOGIN_FAILED", "Resposta de login inv\xE1lida do servidor.");
|
|
220
|
+
return { apiKey: body.api_key };
|
|
221
|
+
}
|
|
167
222
|
|
|
168
223
|
// ../core/src/env.ts
|
|
169
224
|
import { readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
|
@@ -246,6 +301,60 @@ async function writeEnvVars(pairs, opts = {}) {
|
|
|
246
301
|
return { file, keys: Object.keys(pairs) };
|
|
247
302
|
}
|
|
248
303
|
|
|
304
|
+
// ../core/src/credentials.ts
|
|
305
|
+
import {
|
|
306
|
+
readFile as fsReadFile2,
|
|
307
|
+
writeFile as fsWriteFile2,
|
|
308
|
+
mkdir as fsMkdir,
|
|
309
|
+
rm as fsRm,
|
|
310
|
+
chmod as fsChmod
|
|
311
|
+
} from "fs/promises";
|
|
312
|
+
import { homedir } from "os";
|
|
313
|
+
import { join as join2 } from "path";
|
|
314
|
+
function credentialDir(home = homedir()) {
|
|
315
|
+
return join2(home, ".config", "h1ve");
|
|
316
|
+
}
|
|
317
|
+
function credentialPath(home = homedir()) {
|
|
318
|
+
return join2(credentialDir(home), "credentials.json");
|
|
319
|
+
}
|
|
320
|
+
function parseCredential(raw) {
|
|
321
|
+
try {
|
|
322
|
+
const v = JSON.parse(raw);
|
|
323
|
+
if (v && typeof v === "object") {
|
|
324
|
+
const o = v;
|
|
325
|
+
if (typeof o.apiKey === "string" && o.apiKey && typeof o.baseUrl === "string" && o.baseUrl) {
|
|
326
|
+
return { apiKey: o.apiKey, baseUrl: o.baseUrl };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function serializeCredential(cred) {
|
|
334
|
+
return JSON.stringify({ apiKey: cred.apiKey, baseUrl: cred.baseUrl }, null, 2) + "\n";
|
|
335
|
+
}
|
|
336
|
+
var defaultCredentialStore = {
|
|
337
|
+
read: async () => {
|
|
338
|
+
try {
|
|
339
|
+
return parseCredential(await fsReadFile2(credentialPath(), "utf8"));
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
write: async (cred) => {
|
|
345
|
+
const path = credentialPath();
|
|
346
|
+
await fsMkdir(credentialDir(), { recursive: true, mode: 448 });
|
|
347
|
+
await fsWriteFile2(path, serializeCredential(cred), { mode: 384 });
|
|
348
|
+
await fsChmod(path, 384);
|
|
349
|
+
},
|
|
350
|
+
clear: async () => {
|
|
351
|
+
try {
|
|
352
|
+
await fsRm(credentialPath());
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
249
358
|
// src/errors.ts
|
|
250
359
|
var CliError = class extends Error {
|
|
251
360
|
constructor(message, exitCode = 1) {
|
|
@@ -257,19 +366,35 @@ var CliError = class extends Error {
|
|
|
257
366
|
};
|
|
258
367
|
|
|
259
368
|
// src/config.ts
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
369
|
+
var DEFAULT_BASE_URL = "https://app.h1ve.org";
|
|
370
|
+
function resolveBaseUrl(env = process.env) {
|
|
371
|
+
return env.H1VE_API_URL?.trim() || env.NEXUS_FLOW_API_URL?.trim() || DEFAULT_BASE_URL;
|
|
372
|
+
}
|
|
373
|
+
async function resolveConfig(env = process.env, store = defaultCredentialStore) {
|
|
374
|
+
const projectId = env.H1VE_PROJECT_ID?.trim() || env.NEXUS_FLOW_PROJECT_ID?.trim();
|
|
375
|
+
const envKey = env.H1VE_API_KEY?.trim() || env.NEXUS_FLOW_API_KEY?.trim();
|
|
376
|
+
const envUrl = env.H1VE_API_URL?.trim() || env.NEXUS_FLOW_API_URL?.trim();
|
|
377
|
+
let apiKey;
|
|
378
|
+
let baseUrl;
|
|
379
|
+
if (envKey) {
|
|
380
|
+
apiKey = envKey;
|
|
381
|
+
baseUrl = envUrl || DEFAULT_BASE_URL;
|
|
382
|
+
} else {
|
|
383
|
+
const stored = await store.read();
|
|
384
|
+
apiKey = stored?.apiKey;
|
|
385
|
+
baseUrl = stored?.baseUrl || DEFAULT_BASE_URL;
|
|
386
|
+
}
|
|
387
|
+
if (!apiKey) {
|
|
264
388
|
throw new CliError(
|
|
265
|
-
"
|
|
389
|
+
"Voc\xEA n\xE3o est\xE1 logado. Rode `nf login` (abre o navegador) \u2014 ou defina H1VE_API_KEY.\n (o nome legado NEXUS_FLOW_API_KEY tamb\xE9m \xE9 aceito)",
|
|
266
390
|
2
|
|
267
391
|
);
|
|
268
392
|
}
|
|
269
|
-
return { baseUrl, apiKey };
|
|
393
|
+
return { baseUrl, apiKey, projectId };
|
|
270
394
|
}
|
|
271
|
-
function clientFromConfig(cfg) {
|
|
272
|
-
|
|
395
|
+
async function clientFromConfig(cfg) {
|
|
396
|
+
const projectId = cfg.projectId ?? await autoResolveProjectId({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey }) ?? void 0;
|
|
397
|
+
return createClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, projectId });
|
|
273
398
|
}
|
|
274
399
|
|
|
275
400
|
// src/format.ts
|
|
@@ -992,11 +1117,127 @@ Cole o c\xF3digo no painel /connections. Ctrl+C para parar.
|
|
|
992
1117
|
return { human: "", data: null };
|
|
993
1118
|
}
|
|
994
1119
|
|
|
1120
|
+
// src/commands/login.ts
|
|
1121
|
+
import { createServer as createServer2 } from "http";
|
|
1122
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1123
|
+
import { execFile as execFile2 } from "child_process";
|
|
1124
|
+
var LOGIN_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
1125
|
+
function buildAuthorizeUrl(baseUrl, port, state) {
|
|
1126
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
1127
|
+
return `${base}/cli-login?port=${port}&state=${encodeURIComponent(state)}`;
|
|
1128
|
+
}
|
|
1129
|
+
function parseCallbackUrl(reqUrl, expectedState) {
|
|
1130
|
+
let u;
|
|
1131
|
+
try {
|
|
1132
|
+
u = new URL(reqUrl, "http://127.0.0.1");
|
|
1133
|
+
} catch {
|
|
1134
|
+
return { error: "bad_url" };
|
|
1135
|
+
}
|
|
1136
|
+
if (u.pathname !== "/cb") return { error: "not_cb" };
|
|
1137
|
+
const code = u.searchParams.get("code");
|
|
1138
|
+
const state = u.searchParams.get("state");
|
|
1139
|
+
if (!code || !state) return { error: "missing" };
|
|
1140
|
+
if (state !== expectedState) return { error: "state_mismatch" };
|
|
1141
|
+
return { code };
|
|
1142
|
+
}
|
|
1143
|
+
var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>H1VE</title></head>
|
|
1144
|
+
<body style="font-family:system-ui;background:#0f1115;color:#e7e9ee;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0">
|
|
1145
|
+
<div style="text-align:center"><div style="font-size:22px;font-weight:800;letter-spacing:.12em">H<span style="color:#f5a623">1</span>VE</div>
|
|
1146
|
+
<p style="color:#9aa0aa;margin-top:12px">\u2713 Autorizado. Pode fechar esta aba e voltar ao terminal.</p></div></body></html>`;
|
|
1147
|
+
function openBrowser(url) {
|
|
1148
|
+
const platform = process.platform;
|
|
1149
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
1150
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1151
|
+
try {
|
|
1152
|
+
const child = execFile2(cmd, args, () => {
|
|
1153
|
+
});
|
|
1154
|
+
child.on("error", () => {
|
|
1155
|
+
});
|
|
1156
|
+
child.unref();
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async function startCallbackServer(expectedState) {
|
|
1161
|
+
let resolveCode;
|
|
1162
|
+
const codePromise = new Promise((resolve) => {
|
|
1163
|
+
resolveCode = resolve;
|
|
1164
|
+
});
|
|
1165
|
+
const server = createServer2((req, res) => {
|
|
1166
|
+
const parsed = parseCallbackUrl(req.url ?? "/", expectedState);
|
|
1167
|
+
if ("code" in parsed) {
|
|
1168
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1169
|
+
res.end(SUCCESS_HTML);
|
|
1170
|
+
resolveCode(parsed.code);
|
|
1171
|
+
} else {
|
|
1172
|
+
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
|
1173
|
+
res.end("aguardando autorizacao");
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
await new Promise((resolve, reject) => {
|
|
1177
|
+
server.on("error", reject);
|
|
1178
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1179
|
+
});
|
|
1180
|
+
const addr = server.address();
|
|
1181
|
+
const port = addr ? addr.port : 0;
|
|
1182
|
+
const waitForCode = () => new Promise((resolve, reject) => {
|
|
1183
|
+
const timer = setTimeout(() => {
|
|
1184
|
+
server.close();
|
|
1185
|
+
reject(new CliError("Tempo esgotado esperando a autoriza\xE7\xE3o. Rode `nf login` de novo."));
|
|
1186
|
+
}, LOGIN_TIMEOUT_MS);
|
|
1187
|
+
void codePromise.then((code) => {
|
|
1188
|
+
clearTimeout(timer);
|
|
1189
|
+
server.close();
|
|
1190
|
+
resolve(code);
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
return { port, waitForCode, close: () => server.close() };
|
|
1194
|
+
}
|
|
1195
|
+
async function login(io, _args) {
|
|
1196
|
+
const baseUrl = resolveBaseUrl();
|
|
1197
|
+
const state = randomBytes2(18).toString("base64url");
|
|
1198
|
+
const cb = await startCallbackServer(state);
|
|
1199
|
+
const authorizeUrl = buildAuthorizeUrl(baseUrl, cb.port, state);
|
|
1200
|
+
openBrowser(authorizeUrl);
|
|
1201
|
+
process.stderr.write(
|
|
1202
|
+
`Abrindo o navegador para autorizar em ${baseUrl} \u2026
|
|
1203
|
+
Se n\xE3o abrir, acesse:
|
|
1204
|
+
${authorizeUrl}
|
|
1205
|
+
|
|
1206
|
+
Aguardando autoriza\xE7\xE3o\u2026
|
|
1207
|
+
`
|
|
1208
|
+
);
|
|
1209
|
+
let code;
|
|
1210
|
+
try {
|
|
1211
|
+
code = await cb.waitForCode();
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
cb.close();
|
|
1214
|
+
throw err;
|
|
1215
|
+
}
|
|
1216
|
+
const { apiKey } = await exchangeCliLoginCode({ baseUrl }, code);
|
|
1217
|
+
await io.credentials.write({ apiKey, baseUrl });
|
|
1218
|
+
return {
|
|
1219
|
+
human: `\u2713 Logado. Credencial salva em ${credentialPath()} (o CLI e o MCP j\xE1 a usam).`,
|
|
1220
|
+
data: { baseUrl, saved: true }
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/commands/logout.ts
|
|
1225
|
+
async function logout(io, _args) {
|
|
1226
|
+
await io.credentials.clear();
|
|
1227
|
+
return {
|
|
1228
|
+
human: `\u2713 Deslogado localmente (credencial removida de ${credentialPath()}).
|
|
1229
|
+
O token segue v\xE1lido no servidor at\xE9 expirar (90 dias) \u2014 revogue j\xE1 em /api-tokens se precisar.`,
|
|
1230
|
+
data: { ok: true }
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
995
1234
|
// src/index.ts
|
|
996
|
-
var VERSION = "0.
|
|
1235
|
+
var VERSION = "0.5.0";
|
|
997
1236
|
var HELP = `nf \u2014 CLI do Nexus Flow
|
|
998
1237
|
|
|
999
1238
|
Uso:
|
|
1239
|
+
nf login autoriza no navegador e salva a credencial local (sem colar nada)
|
|
1240
|
+
nf logout remove a credencial local
|
|
1000
1241
|
nf start [<n\xBA|id>] [--slug <s>]
|
|
1001
1242
|
inicia uma feature atribu\xEDda: cria a branch e grava o slug
|
|
1002
1243
|
nf status estado da feature da branch atual
|
|
@@ -1024,12 +1265,13 @@ Flags:
|
|
|
1024
1265
|
-h, --help esta ajuda
|
|
1025
1266
|
-v, --version vers\xE3o do CLI
|
|
1026
1267
|
|
|
1027
|
-
Ambiente:
|
|
1028
|
-
H1VE_API_URL URL do H1VE Flow (
|
|
1268
|
+
Ambiente (opcional \u2014 'nf login' dispensa):
|
|
1269
|
+
H1VE_API_URL URL do H1VE Flow (default https://app.h1ve.org)
|
|
1029
1270
|
H1VE_API_KEY PAT (nf_pat_\u2026) p/ escrever, ou a chave de servi\xE7o (s\xF3 leitura)
|
|
1030
1271
|
(nomes legados aceitos: NEXUS_FLOW_API_URL / NEXUS_FLOW_API_KEY)
|
|
1031
1272
|
`;
|
|
1032
|
-
var COMMANDS = { start, status, spec, move, blocker, done, health, connect, serve };
|
|
1273
|
+
var COMMANDS = { start, status, spec, move, blocker, done, health, connect, serve, login, logout };
|
|
1274
|
+
var NO_CONFIG = /* @__PURE__ */ new Set(["serve", "login", "logout"]);
|
|
1033
1275
|
async function readStdin() {
|
|
1034
1276
|
if (process.stdin.isTTY) return "";
|
|
1035
1277
|
const chunks = [];
|
|
@@ -1085,15 +1327,17 @@ async function main(argv) {
|
|
|
1085
1327
|
${HELP}`);
|
|
1086
1328
|
return 1;
|
|
1087
1329
|
}
|
|
1088
|
-
const cfg = command
|
|
1330
|
+
const cfg = NO_CONFIG.has(command) ? void 0 : await resolveConfig();
|
|
1331
|
+
const client = cfg ? await clientFromConfig(cfg) : {};
|
|
1089
1332
|
const io = {
|
|
1090
|
-
client
|
|
1333
|
+
client,
|
|
1091
1334
|
resolveBranch: () => currentBranch(defaultGitRunner),
|
|
1092
1335
|
createBranch: (name) => createBranch(name, defaultGitRunner),
|
|
1093
1336
|
readFile: (path) => readFile(path, "utf8"),
|
|
1094
1337
|
readStdin,
|
|
1095
1338
|
writeEnvVars: (pairs, opts) => writeEnvVars(pairs, { file: opts?.file }),
|
|
1096
|
-
removeEnvVars: (keys, opts) => removeEnvVars(keys, { file: opts?.file })
|
|
1339
|
+
removeEnvVars: (keys, opts) => removeEnvVars(keys, { file: opts?.file }),
|
|
1340
|
+
credentials: defaultCredentialStore
|
|
1097
1341
|
};
|
|
1098
1342
|
const args = {
|
|
1099
1343
|
positional: positionals[1],
|
package/package.json
CHANGED