@h1veframework/cli 0.3.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 +24 -12
- package/dist/index.js +274 -43
- 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,26 +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
|
-
export H1VE_PROJECT_ID="..." # OBRIGATÓRIO só se você tem 2+ projetos (o id do projeto)
|
|
29
|
-
nf health # testa a conexão
|
|
24
|
+
nf login # abre o navegador → você autoriza → pronto
|
|
25
|
+
nf health # já funciona
|
|
30
26
|
```
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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`.
|
|
35
32
|
|
|
36
|
-
> Coloque os `export` no seu `~/.zshrc` / `~/.bashrc` para não repetir a cada sessão.
|
|
37
33
|
> "Nenhum snapshot registrado" no `nf health` é **sucesso** (conectou; projeto sem métricas ainda).
|
|
38
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
|
+
|
|
39
49
|
## Comandos
|
|
40
50
|
|
|
41
51
|
| Comando | O que faz |
|
|
42
52
|
|---|---|
|
|
53
|
+
| `nf login` | Autoriza no navegador e salva a credencial local (sem colar nada) |
|
|
54
|
+
| `nf logout` | Remove a credencial local |
|
|
43
55
|
| `nf health` | Últimos snapshots de saúde técnica do projeto |
|
|
44
56
|
| `nf status` | Estado da feature da branch atual (stage, dias ativo, blockers, sign-offs) |
|
|
45
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) {
|
|
@@ -140,42 +187,38 @@ function createClient(config) {
|
|
|
140
187
|
}
|
|
141
188
|
};
|
|
142
189
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
import { execFile } from "child_process";
|
|
146
|
-
import { promisify } from "util";
|
|
147
|
-
var execFileAsync = promisify(execFile);
|
|
148
|
-
var defaultGitRunner = async (args) => {
|
|
149
|
-
const { stdout } = await execFileAsync("git", args, { cwd: process.cwd() });
|
|
150
|
-
return stdout;
|
|
151
|
-
};
|
|
152
|
-
async function currentBranch(runner = defaultGitRunner) {
|
|
153
|
-
let out;
|
|
190
|
+
async function resolveProjectId(config, repo) {
|
|
191
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
154
192
|
try {
|
|
155
|
-
|
|
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;
|
|
156
198
|
} catch {
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const branch = out.trim();
|
|
160
|
-
if (!branch || branch === "HEAD") throw new Error("NOT_A_GIT_BRANCH");
|
|
161
|
-
return branch;
|
|
162
|
-
}
|
|
163
|
-
function gitErrText(err) {
|
|
164
|
-
if (err && typeof err === "object" && "stderr" in err) {
|
|
165
|
-
return String(err.stderr ?? "");
|
|
199
|
+
return null;
|
|
166
200
|
}
|
|
167
|
-
return err instanceof Error ? err.message : "";
|
|
168
201
|
}
|
|
169
|
-
async function
|
|
202
|
+
async function autoResolveProjectId(config, runner) {
|
|
170
203
|
try {
|
|
171
|
-
await
|
|
172
|
-
} catch
|
|
173
|
-
|
|
174
|
-
if (/already exists/i.test(text)) throw new Error("BRANCH_EXISTS");
|
|
175
|
-
if (/not a git repository/i.test(text)) throw new Error("NOT_A_GIT_REPO");
|
|
176
|
-
throw new Error("GIT_BRANCH_FAILED");
|
|
204
|
+
return await resolveProjectId(config, await gitRemoteRepo(runner));
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
177
207
|
}
|
|
178
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
|
+
}
|
|
179
222
|
|
|
180
223
|
// ../core/src/env.ts
|
|
181
224
|
import { readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
|
@@ -258,6 +301,60 @@ async function writeEnvVars(pairs, opts = {}) {
|
|
|
258
301
|
return { file, keys: Object.keys(pairs) };
|
|
259
302
|
}
|
|
260
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
|
+
|
|
261
358
|
// src/errors.ts
|
|
262
359
|
var CliError = class extends Error {
|
|
263
360
|
constructor(message, exitCode = 1) {
|
|
@@ -269,20 +366,35 @@ var CliError = class extends Error {
|
|
|
269
366
|
};
|
|
270
367
|
|
|
271
368
|
// src/config.ts
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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) {
|
|
275
374
|
const projectId = env.H1VE_PROJECT_ID?.trim() || env.NEXUS_FLOW_PROJECT_ID?.trim();
|
|
276
|
-
|
|
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) {
|
|
277
388
|
throw new CliError(
|
|
278
|
-
"
|
|
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)",
|
|
279
390
|
2
|
|
280
391
|
);
|
|
281
392
|
}
|
|
282
393
|
return { baseUrl, apiKey, projectId };
|
|
283
394
|
}
|
|
284
|
-
function clientFromConfig(cfg) {
|
|
285
|
-
|
|
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 });
|
|
286
398
|
}
|
|
287
399
|
|
|
288
400
|
// src/format.ts
|
|
@@ -1005,11 +1117,127 @@ Cole o c\xF3digo no painel /connections. Ctrl+C para parar.
|
|
|
1005
1117
|
return { human: "", data: null };
|
|
1006
1118
|
}
|
|
1007
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
|
+
|
|
1008
1234
|
// src/index.ts
|
|
1009
|
-
var VERSION = "0.
|
|
1235
|
+
var VERSION = "0.5.0";
|
|
1010
1236
|
var HELP = `nf \u2014 CLI do Nexus Flow
|
|
1011
1237
|
|
|
1012
1238
|
Uso:
|
|
1239
|
+
nf login autoriza no navegador e salva a credencial local (sem colar nada)
|
|
1240
|
+
nf logout remove a credencial local
|
|
1013
1241
|
nf start [<n\xBA|id>] [--slug <s>]
|
|
1014
1242
|
inicia uma feature atribu\xEDda: cria a branch e grava o slug
|
|
1015
1243
|
nf status estado da feature da branch atual
|
|
@@ -1037,12 +1265,13 @@ Flags:
|
|
|
1037
1265
|
-h, --help esta ajuda
|
|
1038
1266
|
-v, --version vers\xE3o do CLI
|
|
1039
1267
|
|
|
1040
|
-
Ambiente:
|
|
1041
|
-
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)
|
|
1042
1270
|
H1VE_API_KEY PAT (nf_pat_\u2026) p/ escrever, ou a chave de servi\xE7o (s\xF3 leitura)
|
|
1043
1271
|
(nomes legados aceitos: NEXUS_FLOW_API_URL / NEXUS_FLOW_API_KEY)
|
|
1044
1272
|
`;
|
|
1045
|
-
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"]);
|
|
1046
1275
|
async function readStdin() {
|
|
1047
1276
|
if (process.stdin.isTTY) return "";
|
|
1048
1277
|
const chunks = [];
|
|
@@ -1098,15 +1327,17 @@ async function main(argv) {
|
|
|
1098
1327
|
${HELP}`);
|
|
1099
1328
|
return 1;
|
|
1100
1329
|
}
|
|
1101
|
-
const cfg = command
|
|
1330
|
+
const cfg = NO_CONFIG.has(command) ? void 0 : await resolveConfig();
|
|
1331
|
+
const client = cfg ? await clientFromConfig(cfg) : {};
|
|
1102
1332
|
const io = {
|
|
1103
|
-
client
|
|
1333
|
+
client,
|
|
1104
1334
|
resolveBranch: () => currentBranch(defaultGitRunner),
|
|
1105
1335
|
createBranch: (name) => createBranch(name, defaultGitRunner),
|
|
1106
1336
|
readFile: (path) => readFile(path, "utf8"),
|
|
1107
1337
|
readStdin,
|
|
1108
1338
|
writeEnvVars: (pairs, opts) => writeEnvVars(pairs, { file: opts?.file }),
|
|
1109
|
-
removeEnvVars: (keys, opts) => removeEnvVars(keys, { file: opts?.file })
|
|
1339
|
+
removeEnvVars: (keys, opts) => removeEnvVars(keys, { file: opts?.file }),
|
|
1340
|
+
credentials: defaultCredentialStore
|
|
1110
1341
|
};
|
|
1111
1342
|
const args = {
|
|
1112
1343
|
positional: positionals[1],
|
package/package.json
CHANGED