@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.
Files changed (3) hide show
  1. package/README.md +25 -8
  2. package/dist/index.js +294 -50
  3. 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 e um **PAT** (`nf_pat_…`), criado em **`app.h1ve.org/api-tokens`**.
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
- ## Configuração
21
+ ## Login (recomendado — sem colar nada)
24
22
 
25
23
  ```bash
26
- export H1VE_API_URL="https://app.h1ve.org"
27
- export H1VE_API_KEY="nf_pat_..." # seu PAT
28
- nf health # testa a conexão
24
+ nf login # abre o navegador → você autoriza → pronto
25
+ nf health # funciona
29
26
  ```
30
27
 
31
- > Coloque os `export` no seu `~/.zshrc` / `~/.bashrc` para não repetir a cada sessão.
28
+ O `nf login` abre o navegador (onde você 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 = { "x-api-key": config.apiKey, accept: "application/json" };
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
- // ../core/src/branch.ts
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
- out = await runner(["rev-parse", "--abbrev-ref", "HEAD"]);
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
- throw new Error("NOT_A_GIT_BRANCH");
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 createBranch(name, runner = defaultGitRunner) {
202
+ async function autoResolveProjectId(config, runner) {
158
203
  try {
159
- await runner(["switch", "-c", name]);
160
- } catch (err) {
161
- const text = gitErrText(err);
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
- function readConfig(env = process.env) {
261
- const baseUrl = env.H1VE_API_URL?.trim() || env.NEXUS_FLOW_API_URL?.trim();
262
- const apiKey = env.H1VE_API_KEY?.trim() || env.NEXUS_FLOW_API_KEY?.trim();
263
- if (!baseUrl || !apiKey) {
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
- "Defina H1VE_API_URL e H1VE_API_KEY no ambiente.\n export H1VE_API_URL=https://app.h1ve.org\n export H1VE_API_KEY=nf_pat_... (crie um token em /api-tokens)\n (os nomes legados NEXUS_FLOW_API_URL/KEY ainda s\xE3o aceitos)",
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
- return createClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
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.2.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 (ex.: https://app.h1ve.org)
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 === "serve" ? void 0 : readConfig();
1330
+ const cfg = NO_CONFIG.has(command) ? void 0 : await resolveConfig();
1331
+ const client = cfg ? await clientFromConfig(cfg) : {};
1089
1332
  const io = {
1090
- client: cfg ? clientFromConfig(cfg) : {},
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h1veframework/cli",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI `nf` do H1VE Flow: opera o fluxo da feature (status/move/blocker/spec/done/health/start) da própria branch, via PAT.",
5
5
  "license": "MIT",
6
6
  "type": "module",