jarvis-ai-framework 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/agents/product/prod.discovery-interviewer.md +1 -1
  2. package/bin/README.md +14 -14
  3. package/bin/commands/docs-publish.js +50 -92
  4. package/bin/commands/docs-sync.js +30 -63
  5. package/bin/lib/cli/program.js +23 -25
  6. package/bin/lib/docs/fetch-file.js +29 -26
  7. package/bin/lib/docs/publish-file.js +89 -161
  8. package/bin/lib/docs/storage/build-index.js +87 -0
  9. package/bin/lib/docs/storage/filesystem.js +203 -0
  10. package/bin/lib/docs/storage/index.js +75 -0
  11. package/bin/lib/docs/validate-frontmatter.js +14 -4
  12. package/bin/lib/vcs/api.js +0 -125
  13. package/package.json +2 -2
  14. package/rules/AGENTS.md +12 -9
  15. package/rules/engineering/eng.integrations-rules.md +2 -1
  16. package/rules/product/prod-rules.md +4 -5
  17. package/rules-on-demand/product/prod.guided-rules.md +104 -0
  18. package/skills/jarvis-docs-central/README.md +57 -269
  19. package/skills/jarvis-docs-central/SKILL.md +126 -296
  20. package/skills/jarvis-init/SKILL.md +46 -7
  21. package/templates/ENV-template.md +20 -5
  22. package/templates/engineering/ARD-template.md +39 -6
  23. package/templates/engineering/architecture-template.md +1 -0
  24. package/templates/product/prod-breakdown-template.md +20 -2
  25. package/templates/product/prod-discovery-template.md +24 -1
  26. package/templates/product/prod-epic-template.md +32 -1
  27. package/templates/product/prod-frd-template.md +29 -2
  28. package/templates/product/prod-issue-template.md +36 -1
  29. package/templates/product/prod-prd-template.md +33 -2
  30. package/workflows/engineering/eng.build-tech-spec.md +2 -2
  31. package/workflows/engineering/eng.create-ard-from-code.md +9 -11
  32. package/workflows/engineering/eng.create-ard.md +15 -13
  33. package/workflows/engineering/eng.create-rfc.md +8 -10
  34. package/workflows/engineering/eng.pr.md +4 -4
  35. package/workflows/engineering/eng.pre-pr.md +4 -4
  36. package/workflows/engineering/eng.rpa.robot.md +4 -2
  37. package/workflows/engineering/eng.start.md +17 -16
  38. package/workflows/product/prod.roadmap.preview.md +4 -1
  39. package/workflows/product/prod.spec.ard.md +7 -3
  40. package/workflows/product/prod.spec.breakdown.md +7 -3
  41. package/workflows/product/prod.spec.clarify.md +5 -1
  42. package/workflows/product/prod.spec.discovery.md +7 -3
  43. package/workflows/product/prod.spec.epic.md +13 -11
  44. package/workflows/product/prod.spec.frd.md +15 -13
  45. package/workflows/product/prod.spec.issue.md +5 -1
  46. package/workflows/product/prod.spec.md +3 -1
  47. package/workflows/product/prod.spec.prd.md +15 -13
  48. package/workflows/warm-up.md +3 -3
  49. package/bin/lib/docs/redis-cache.js +0 -49
  50. package/bin/lib/vcs/fetch-raw.js +0 -42
@@ -75,7 +75,7 @@ Não use quando já existe uma spec (use `/prod.spec.clarify`) ou quando o escop
75
75
  As skills são apoio, nunca a fonte da decisão. A busca em `$PROD_DOCS` não é uma skill: é feita sempre, antes de perguntar.
76
76
 
77
77
  ### jarvis-docs-central
78
- Para buscar documentação do produto no repositório central, quando `CENTRAL_DOCS_REPO` estiver configurado:
78
+ Para buscar documentação do produto no Central docs, quando `CENTRAL_DOCS_STORAGE` estiver configurado:
79
79
  - Arquivo: `$IDE/skills/jarvis-docs-central/SKILL.md`
80
80
  - Confiança: sempre
81
81
 
package/bin/README.md CHANGED
@@ -17,17 +17,17 @@ bin/
17
17
  │ ├── program.js # Único lugar que monta o commander — ver "Arquitetura"
18
18
  │ └── shared-options.js # .option() repetidos entre comandos (--ide, --quiet, etc.)
19
19
  ├── vcs/ # Adapters de Git host (GitLab/GitHub/Bitbucket)
20
- │ ├── api.js # Lógica HTTP real (createIssue, createMergeRequest, fetchRawFile)
21
- │ ├── create-merge.js, create-issue.js, fetch-raw.js # função exportada + CLI direto (ver abaixo)
20
+ │ ├── api.js # Lógica HTTP real (createIssue, createMergeRequest)
21
+ │ ├── create-merge.js, create-issue.js # função exportada + CLI direto (ver abaixo)
22
22
  ├── tasks/
23
23
  │ └── comment.js # Adapter de task manager (jira/linear/github/asana) — mesmo padrão
24
24
  ├── config/ # ide-config.js (fonte de verdade de IDEs), constants.js
25
25
  ├── core/ # scanner.js, sync-engine.js, profile-filter.js, token-report.js
26
- ├── docs/ # central-docs: fetch/publish + validação de frontmatter
27
- │ ├── validate-frontmatter.js # Valida/extrai frontmatter de PRD/FRD/ARD/RFC
28
- │ ├── redis-cache.js # Cache best-effort via `redis-cli` (get/setex/del)
29
- │ ├── fetch-file.js # fetchFile() — usado por `docs sync`/`docs fetch` e por publish-file.js
30
- │ └── publish-file.js # publishFile() — usado por `docs publish`
26
+ ├── docs/ # Central docs: fetch/publish pelo adaptador + validação de frontmatter
27
+ │ ├── validate-frontmatter.js # Valida/extrai frontmatter (PRD, FRD, épico, história, tarefa, ARD, RFC)
28
+ │ ├── fetch-file.js # openStorageFromEnv() + fetchFile() — `docs sync`/`docs fetch`/`docs publish`
29
+ │ ├── publish-file.js # tipos, resolveDestPath() e publishFile() — `docs publish`
30
+ │ └── storage/ # adaptadores (CENTRAL_DOCS_STORAGE): index.js (fábrica), filesystem.js (pasta + git), build-index.js
31
31
  ├── utils/ # logger.js, ui.js, paths.js, frontmatter.js, git-parser.js, npmrc-parser.js
32
32
  ├── flow-map/ # `jarvis map` — grafo de chamadas entre workflows/skills/agents
33
33
  ├── auth/ # session.js (auth.json legado, hoje só `jarvis logout` o lê)
@@ -77,22 +77,22 @@ O parser manual antigo nunca validava nada: qualquer `--flag` era aceito e ignor
77
77
 
78
78
  ## Argumentos: `[opcional]`, não `<obrigatório>` — de propósito
79
79
 
80
- Note que `info`, `vcs fetch-raw` e `tasks comment` declaram seus positionals como **opcionais** (`[target]`, `[file-path]`) mesmo sendo, na prática, obrigatórios para o comando funcionar. Isso é deliberado: se o commander validar "argumento obrigatório ausente" sozinho, ele intercepta **antes** da função rodar — e cada uma dessas funções já tem sua própria validação com mensagem de uso customizada e mais específica (`info.js`: `"❌ Uso incorreto\nUso: jarvis info <type/name>\nExemplo: ..."`). Deixar o positional opcional no commander e a validação dentro da função preserva essa mensagem.
80
+ Note que `info` e `tasks comment` declaram seus positionals como **opcionais** (`[target]`, `[card-key]`) mesmo sendo, na prática, obrigatórios para o comando funcionar. Isso é deliberado: se o commander validar "argumento obrigatório ausente" sozinho, ele intercepta **antes** da função rodar — e cada uma dessas funções já tem sua própria validação com mensagem de uso customizada e mais específica (`info.js`: `"❌ Uso incorreto\nUso: jarvis info <type/name>\nExemplo: ..."`). Deixar o positional opcional no commander e a validação dentro da função preserva essa mensagem.
81
81
 
82
- **A mesma regra vale para `--flag` que já tem validação própria.** Nenhum comando aqui usa `.requiredOption()` ou `.choices()` do commander, mesmo quando faria sentido à primeira vista (ex: `--tipo` em `docs publish` só aceita `prd|frd|ard|rfc|swagger|qa-report`). `docsPublish.js` já valida isso e mostra um bloco de exemplos de uso bem mais rico que o que o commander mostraria. Usar `.choices()` ali faria o commander interceptar o valor errado com uma mensagem genérica, perdendo esse texto. **Ao adicionar uma opção nova**: se a função já vai validar o valor por dentro, deixe o commander permissivo (`.option()` simples) e confie na validação interna. Se não há validação nenhuma hoje, `.choices()`/`.argParser()` são bons lugares para adicionar uma — não há essa perda a proteger.
82
+ **A mesma regra vale para `--flag` que já tem validação própria.** Nenhum comando aqui usa `.requiredOption()` ou `.choices()` do commander, mesmo quando faria sentido à primeira vista (ex: `--tipo` em `docs publish` só aceita os tipos de `VALID_TYPES`). `docsPublish.js` já valida isso e mostra um bloco de exemplos de uso bem mais rico que o que o commander mostraria. Usar `.choices()` ali faria o commander interceptar o valor errado com uma mensagem genérica, perdendo esse texto. **Ao adicionar uma opção nova**: se a função já vai validar o valor por dentro, deixe o commander permissivo (`.option()` simples) e confie na validação interna. Se não há validação nenhuma hoje, `.choices()`/`.argParser()` são bons lugares para adicionar uma — não há essa perda a proteger.
83
83
 
84
84
  ## `program.command()` vs `program.addCommand()`
85
85
 
86
86
  Este código usa `.command(nome)` (que cria e já anexa o subcomando) em todo lugar — não `.addCommand(instanciaDeCommand)`. São equivalentes; `.addCommand()` só compensa quando o `Command` é montado em outro módulo e precisa ser importado antes de anexar. Como tudo mora em `program.js`, não há motivo para o nível extra de indireção.
87
87
 
88
- ## Os 4 "scripts standalone" (`vcs/*.js`, `tasks/comment.js`): dupla forma
88
+ ## Os 3 "scripts standalone" (`vcs/*.js`, `tasks/comment.js`): dupla forma
89
89
 
90
- `create-merge.js`, `create-issue.js`, `fetch-raw.js` e `comment.js` são chamados de duas formas:
90
+ `create-merge.js`, `create-issue.js` e `comment.js` são chamados de duas formas:
91
91
 
92
92
  1. **Como subcomando do CLI**: `jarvis vcs create-merge --source ... --target ...` (via `program.js`)
93
93
  2. **Direto, por skills/workflows que ainda shell-am pra eles**: `node bin/lib/vcs/create-merge.js --source ...`
94
94
 
95
- Cada um dos 4 arquivos segue o mesmo padrão: exporta uma função (`runCreateMerge`, `runCreateIssue`, `runFetchRaw`, `runComment`) com a lógica, e termina com um guard que só lê `process.argv` quando o arquivo é executado diretamente:
95
+ Cada um dos 3 arquivos segue o mesmo padrão: exporta uma função (`runCreateMerge`, `runCreateIssue`, `runComment`) com a lógica, e termina com um guard que só lê `process.argv` quando o arquivo é executado diretamente:
96
96
 
97
97
  ```js
98
98
  // Compatibilidade: continua funcionando como `node bin/lib/vcs/create-merge.js --source ...`
@@ -131,9 +131,9 @@ Os 3 arquivos mais críticos sem cobertura até pouco tempo atrás (`env-loader.
131
131
  - **`env-loader.js`** (e `flow-map.test.js`, mais antigo): `mkdtempSync(join(tmpdir(), "jarvis-..."))` monta um workspace fake com `.{ide}/ENV.md`, roda a função contra ele, `rmSync` no `finally`.
132
132
  - **`sync-engine.js`**: `getFrameworkRoot()` (`utils/paths.js`) já tem um escape hatch — `process.env.JARVIS_ROOT`, se definido e existir, vence sobre o framework real. O teste aponta `JARVIS_ROOT` para um fixture mínimo (`agents/`, `skills/`, etc. com 1 arquivo cada) em vez de sincronizar o framework inteiro.
133
133
  - **`vcs/api.js`**: usa `mock.method(globalThis, "fetch", ...)` do `node:test` pra controlar a resposta por vendor, sem mockar o módulo nem tocar rede real. **Atenção**: toda função de rede chama `getVcsToken()` antes do fetch — fixe `GITLAB_TOKEN`/`GITHUB_TOKEN`/`BITBUCKET_TOKEN` via env var no teste (ver `withEnv` em `vcs-api.test.js`), senão o resultado depende do que a máquina que roda o teste tiver em `.npmrc`/`gh auth`.
134
- - **`docs/fetch-file.js`/`docs/publish-file.js`**: mesmo mock de `fetch`. `redis-cli` (chamado por `docs/redis-cache.js`) **não dá pra mockar** — é um módulo nativo (`node:child_process`) não configurável, `mock.method` lança `TypeError: Cannot redefine property`. Como pode haver um `redis-cli` de verdade instalado (com ou sem servidor acessível) na máquina que roda o teste, cada teste usa uma chave de cache única (`docs:${randomUUID()}...`) em vez de mockar o cache — garante cache-miss determinístico sem depender do estado de nenhum Redis real.
134
+ - **`docs/` (Central docs)**: tudo em pasta temporária (`mkdtempSync`). A camada git do adaptador `filesystem` usa um repositório `git init --bare` local como `origin` (sem rede), e o `createMergeRequest` é injetado pelo parâmetro `deps` de `getStorage` (ver `docs-storage.test.js`).
135
135
 
136
- **Armadilha com `async`**: o helper `withEnv(vars, fn)` (repetido nos 3 arquivos acima) precisa de `return **await** fn()`, não `return fn()`, dentro do `try`. Sem o `await`, o `finally` que restaura a env var roda assim que `fn()` retorna uma Promise pendente — ou seja, **antes** da função terminar. Isso só não quebra quando a leitura da env var acontece antes do primeiro `await` de `fn` (ex: `fetchRawFile` lê o token antes do seu próprio `fetch`); quebra silenciosamente em qualquer função com mais de um `await` no meio do caminho (ex: `publishFile`, que busca o `index.md` antes de chegar no commit, que é onde o token é lido).
136
+ **Armadilha com `async`**: o helper `withEnv(vars, fn)` (em `vcs-api.test.js`) precisa de `return **await** fn()`, não `return fn()`, dentro do `try`. Sem o `await`, o `finally` que restaura a env var roda assim que `fn()` retorna uma Promise pendente — ou seja, **antes** da função terminar. Isso só não quebra quando a leitura da env var acontece antes do primeiro `await` de `fn` (ex: `createIssue` lê o token antes do seu próprio `fetch`); quebra silenciosamente em qualquer função com mais de um `await` antes de ler a variável.
137
137
 
138
138
  Ao escrever um teste novo para um arquivo que toca `fs`/rede, prefira um desses padrões a mockar o módulo inteiro ou refatorar pra injeção de dependência — nenhum dos arquivos acima precisou mudar uma linha de lógica de produção para ganhar teste.
139
139
 
@@ -1,148 +1,106 @@
1
- import { loadEnv, resolveEnvPath } from "../lib/env-loader.js";
2
1
  import { existsSync } from "node:fs";
3
- import { publishFile } from "../lib/docs/publish-file.js";
2
+ import { openStorageFromEnv } from "../lib/docs/fetch-file.js";
3
+ import { publishFile, VALID_TYPES } from "../lib/docs/publish-file.js";
4
4
  import { logger, configureFromFlags } from "../lib/utils/logger.js";
5
5
 
6
6
  /**
7
- * Publica documento no central-docs via GitLab API (branch + MR)
7
+ * Publica um documento no Central docs pelo adaptador configurado.
8
+ * Sem `.git` na pasta: copia e regenera o índice. Com `.git`: branch, commit, push e PR.
8
9
  *
9
10
  * @param {Object} flags - Flags do comando
10
11
  * @param {string} flags.file - Caminho do arquivo local
11
- * @param {string} flags.tipo - Tipo do documento (prd|frd|ard|rfc)
12
- * @param {string} flags.feature - Slug da feature (kebab-case)
13
- * @param {string} flags.squad - Squad (opcional, override do ENV.md)
14
- * @param {string} flags.workspace - Workspace (opcional, override do ENV.md)
12
+ * @param {string} flags.tipo - Tipo do documento (ver VALID_TYPES)
13
+ * @param {string} [flags.produto] - Pasta do produto (padrão: WORKSPACE do ENV.md)
14
+ * @param {string} [flags.feature] - Slug, só se o nome do arquivo fugir do padrão {tipo}-{id}-{nome}
15
+ * @param {string} [flags.squad] - Override do SQUAD do ENV.md
16
+ * @param {string} [flags.workspace] - Override do WORKSPACE (padrão do --produto)
15
17
  * @param {string} flags.envFile - Path customizado do ENV.md
16
18
  */
17
19
  export async function docsPublish(flags = {}) {
18
20
  configureFromFlags(flags);
19
21
 
20
- const { file, tipo, feature, squad: squadOverride, workspace: workspaceOverride } = flags;
22
+ const {
23
+ file,
24
+ tipo,
25
+ feature,
26
+ produto,
27
+ squad: squadOverride,
28
+ workspace: workspaceOverride,
29
+ } = flags;
21
30
 
22
- // Validar parâmetros obrigatórios
23
- if (!file || !tipo || !feature) {
31
+ if (!file || !tipo) {
24
32
  logger.error("❌ Parâmetros obrigatórios faltando\n");
25
33
  logger.error(
26
- "Uso: jarvis docs publish --file <path> --tipo <prd|frd|ard|rfc|swagger|qa-report> --feature <slug> [--squad <squad>] [--workspace <workspace>]\n"
34
+ `Uso: jarvis docs publish --file <path> --tipo <${VALID_TYPES.join("|")}> [--produto <produto>] [--feature <slug>] [--squad <squad>]\n`
27
35
  );
28
- logger.error("Exemplo:");
29
- logger.error(" jarvis docs publish \\");
30
- logger.error(" --file ./docs/engineering/ard-api-wallet.md \\");
31
- logger.error(" --tipo ard \\");
32
- logger.error(" --feature api-wallet-auth-jwt");
36
+ logger.error("Exemplo (o nome do arquivo já segue {tipo}-{id}-{nome}):");
37
+ logger.error(" jarvis docs publish --file ./docs/product/prd-001-checkout.md --tipo prd");
33
38
  logger.error("");
34
- logger.error(" # Publicar Swagger/OpenAPI:");
35
- logger.error(" jarvis docs publish \\");
36
- logger.error(" --file ./docs/engineering/swagger/api-wallet.yaml \\");
37
- logger.error(" --tipo swagger \\");
38
- logger.error(" --feature api-wallet");
39
- logger.error("");
40
- logger.error(" # Com override de squad/workspace:");
41
- logger.error(" jarvis docs publish \\");
42
- logger.error(" --file ./docs/engineering/ard-api-wallet.md \\");
43
- logger.error(" --tipo ard \\");
44
- logger.error(" --feature api-wallet-auth-jwt \\");
45
- logger.error(" --squad core \\");
46
- logger.error(" --workspace meu-workspace");
39
+ logger.error(" # Arquivo fora do padrão de nome:");
40
+ logger.error(
41
+ " jarvis docs publish --file ./docs/engineering/swagger/api.yaml --tipo swagger --feature api-wallet"
42
+ );
47
43
  process.exit(1);
48
44
  }
49
45
 
50
- // Validar tipo
51
- const validTypes = ["prd", "frd", "ard", "rfc", "swagger", "qa-report"];
52
- if (!validTypes.includes(tipo)) {
46
+ if (!VALID_TYPES.includes(tipo)) {
53
47
  logger.error(`❌ Tipo inválido: ${tipo}`);
54
- logger.error(`Tipos válidos: ${validTypes.join(", ")}`);
48
+ logger.error(`Tipos válidos: ${VALID_TYPES.join(", ")}`);
55
49
  process.exit(1);
56
50
  }
57
51
 
58
- // Validar se arquivo existe
59
52
  if (!existsSync(file)) {
60
53
  logger.error(`❌ Arquivo não encontrado: ${file}`);
61
54
  process.exit(1);
62
55
  }
63
56
 
64
- const cwd = process.cwd();
65
-
66
- // Resolver path do ENV.md (suporta --ide e --env-file)
67
- let envPath;
68
- try {
69
- const resolved = resolveEnvPath(cwd, flags);
70
- envPath = resolved.path;
71
- } catch (error) {
72
- logger.error("❌ Erro ao resolver ENV.md:", error.message);
73
- process.exit(1);
74
- }
75
-
76
- // Carregar ENV.md
77
57
  let env;
58
+ let storage;
78
59
  try {
79
- env = loadEnv(cwd, envPath);
60
+ ({ env, storage } = openStorageFromEnv(flags));
80
61
  } catch (error) {
81
- logger.error("❌ Erro ao carregar ENV.md:", error.message);
62
+ logger.error("❌ Central docs:", error.message);
82
63
  process.exit(1);
83
64
  }
84
65
 
85
- // Verificar se central-docs está configurado
86
- if (!env.CENTRAL_DOCS_REPO) {
87
- logger.error("❌ CENTRAL_DOCS_REPO não configurado no ENV.md");
88
- logger.error("\nConfigure a URL do repositório central:");
89
- logger.error(" CENTRAL_DOCS_REPO=https://gitlab.com/org/central-docs.git");
66
+ if (!storage) {
67
+ logger.error("❌ Central docs desligado (CENTRAL_DOCS_STORAGE vazio no ENV.md).");
68
+ logger.error(" Configure-o com o /jarvis-init.");
90
69
  process.exit(1);
91
70
  }
92
71
 
93
- // Usar override de squad/workspace se fornecido, senão usar do ENV.md
94
72
  const squad = squadOverride || env.SQUAD;
95
- const workspace = workspaceOverride || env.WORKSPACE;
73
+ const product = produto || workspaceOverride || env.WORKSPACE;
96
74
 
97
- if (!squad || !workspace) {
75
+ if (!squad || !product) {
98
76
  logger.error("❌ SQUAD ou WORKSPACE não definidos");
99
- logger.error("\nOpções:");
100
- logger.error(" 1. Definir no ENV.md:");
101
- logger.error(" SQUAD=core");
102
- logger.error(" WORKSPACE=meu-workspace");
103
- logger.error("");
104
- logger.error(" 2. Passar via flags:");
105
- logger.error(" --squad core --workspace meu-workspace");
77
+ logger.error(" Defina no ENV.md ou passe --squad <squad> --produto <produto>");
106
78
  process.exit(1);
107
79
  }
108
80
 
109
- logger.info(`📤 Publicando ${tipo.toUpperCase()} ${feature}...\n`);
81
+ logger.info(`📤 Publicando ${tipo.toUpperCase()} (${file})...\n`);
110
82
 
111
83
  try {
112
- await publishFile({
113
- localFile: file,
114
- tipo,
115
- feature,
116
- squad,
117
- workspace,
118
- repo: env.CENTRAL_DOCS_REPO,
119
- targetBranch: env.CENTRAL_DOCS_TARGET_BRANCH || "dev",
120
- cacheTtl: env.CENTRAL_DOCS_CACHE_TTL || "3600",
121
- });
84
+ await publishFile({ storage, localFile: file, tipo, squad, produto: product, feature });
85
+ logger.info("");
86
+ logger.info("✅ Publicação concluída!");
122
87
  } catch (error) {
123
88
  logger.error("\n❌ Erro ao publicar documento\n");
124
89
  logger.error(error.message);
125
90
 
126
- // Dicas de troubleshooting
127
- logger.error("\n💡 Troubleshooting:\n");
128
-
129
- if (error.message.includes("Token GitLab")) {
130
- logger.error("1. Configure o token GitLab no .npmrc:");
131
- logger.error(" //gitlab.com/api/v4/packages/npm/:_authToken=seu-token\n");
132
- }
133
-
134
91
  if (error.message.includes("Campos obrigatórios")) {
135
- logger.error("2. Verifique o frontmatter do documento:");
136
- logger.error(` Tipo ${tipo} requer campos específicos no YAML\n`);
92
+ logger.error(
93
+ `\n💡 Verifique o frontmatter do documento: o tipo ${tipo} exige campos específicos.`
94
+ );
137
95
  }
138
-
139
- if (error.message.includes("401") || error.message.includes("403")) {
140
- logger.error("4. Verifique permissões no GitLab:");
141
- logger.error(" Token precisa de scopes: api, read_api, write_repository\n");
96
+ if (storage.isGit) {
97
+ logger.error(
98
+ "\n💡 Central docs com git: confira se a pasta está limpa (git status), se você tem permissão de push"
99
+ );
100
+ logger.error(
101
+ " e se o token do vendor está configurado (GitLab .npmrc, GitHub gh auth, BITBUCKET_TOKEN)."
102
+ );
142
103
  }
143
-
144
- logger.error("Para mais detalhes, execute com --verbose");
145
-
146
104
  process.exit(1);
147
105
  }
148
106
  }
@@ -1,10 +1,11 @@
1
- import { loadEnv, resolveEnvPath } from "../lib/env-loader.js";
2
- import { fetchFile } from "../lib/docs/fetch-file.js";
1
+ import { openStorageFromEnv } from "../lib/docs/fetch-file.js";
3
2
  import { logger, configureFromFlags } from "../lib/utils/logger.js";
4
3
 
4
+ const COUNTED_TYPES = ["prd", "frd", "feat", "epic", "story", "task", "ard", "rfc"];
5
+
5
6
  /**
6
- * Sincroniza documentação do central-docs
7
- * Executado automaticamente em /warm-up
7
+ * Sincroniza a documentação do Central docs (lê `{SQUAD}/index.md` pelo adaptador).
8
+ * Executado automaticamente em /warm-up.
8
9
  *
9
10
  * @param {Object} flags - Flags do comando
10
11
  * @param {boolean} flags.silent - Modo silencioso (nem erros aparecem)
@@ -15,87 +16,53 @@ import { logger, configureFromFlags } from "../lib/utils/logger.js";
15
16
  export async function docsSync(flags = {}) {
16
17
  configureFromFlags(flags);
17
18
 
18
- const cwd = process.cwd();
19
-
20
- // Resolver path do ENV.md (suporta --ide e --env-file)
21
- let envPath;
22
- try {
23
- const resolved = resolveEnvPath(cwd, flags);
24
- envPath = resolved.path;
25
- } catch (error) {
26
- logger.error("❌ Erro ao resolver ENV.md:", error.message);
27
- process.exit(1);
28
- }
29
-
30
- // Carregar ENV.md
31
19
  let env;
20
+ let storage;
32
21
  try {
33
- env = loadEnv(cwd, envPath);
22
+ ({ env, storage } = openStorageFromEnv(flags));
34
23
  } catch (error) {
35
- logger.error("❌ Erro ao carregar ENV.md:", error.message);
24
+ logger.error("❌ Central docs:", error.message);
36
25
  process.exit(1);
37
26
  }
38
27
 
39
- // Verificar se central-docs está configurado
40
- if (!env.CENTRAL_DOCS_REPO) {
41
- logger.info("ℹ️ Central docs não configurado (CENTRAL_DOCS_REPO vazio)");
28
+ if (!storage) {
29
+ logger.info("ℹ️ Central docs desligado (CENTRAL_DOCS_STORAGE vazio)");
42
30
  return;
43
31
  }
44
32
 
45
- const { SQUAD, WORKSPACE, CENTRAL_DOCS_REF = "main" } = env;
46
-
47
- if (!SQUAD || !WORKSPACE) {
48
- logger.error("❌ SQUAD ou WORKSPACE não definidos no ENV.md");
33
+ const { SQUAD } = env;
34
+ if (!SQUAD) {
35
+ logger.error("❌ SQUAD não definido no ENV.md");
49
36
  process.exit(1);
50
37
  }
51
38
 
52
- logger.info(`🔄 Sincronizando docs do squad ${SQUAD}...`);
53
-
54
- // Buscar index.md do squad
55
- const indexPath = `${SQUAD}/index.md`;
39
+ logger.info(
40
+ `🔄 Sincronizando docs do squad ${SQUAD} (${storage.kind}${storage.isGit ? " + git" : ""})...`
41
+ );
56
42
 
57
43
  try {
58
- const content = await fetchFile(indexPath, CENTRAL_DOCS_REF, {
59
- repo: env.CENTRAL_DOCS_REPO,
60
- cacheTtl: env.CENTRAL_DOCS_CACHE_TTL || "3600",
61
- });
62
-
44
+ const content = await storage.fetch(`${SQUAD}/index.md`);
63
45
  logger.info("✅ Docs sincronizados");
64
46
 
65
- if (content) {
66
- logger.debug("\n📄 Conteúdo do index.md:\n");
67
- logger.debug(content);
68
- }
47
+ logger.debug("\n📄 Conteúdo do index.md:\n");
48
+ logger.debug(content);
69
49
 
70
- // Parsear index.md para contar documentos
71
- if (content) {
72
- const prdCount = (content.match(/\[prd-.*\.md\]/g) || []).length;
73
- const frdCount = (content.match(/\[frd-.*\.md\]/g) || []).length;
74
- const ardCount = (content.match(/\[ard-.*\.md\]/g) || []).length;
75
- const rfcCount = (content.match(/\[rfc-.*\.md\]/g) || []).length;
50
+ const counts = COUNTED_TYPES.map((tipo) => [
51
+ tipo,
52
+ (content.match(new RegExp(`\\[${tipo}-[^\\]]*\\]`, "g")) || []).length,
53
+ ]).filter(([, n]) => n > 0);
54
+ const total = counts.reduce((sum, [, n]) => sum + n, 0);
76
55
 
77
- const total = prdCount + frdCount + ardCount + rfcCount;
78
-
79
- if (total > 0) {
80
- logger.info(`📊 ${total} documentos disponíveis:`);
81
- if (prdCount > 0) logger.info(` - ${prdCount} PRD(s)`);
82
- if (frdCount > 0) logger.info(` - ${frdCount} FRD(s)`);
83
- if (ardCount > 0) logger.info(` - ${ardCount} ARD(s)`);
84
- if (rfcCount > 0) logger.info(` - ${rfcCount} RFC(s)`);
85
- }
56
+ if (total > 0) {
57
+ logger.info(`📊 ${total} documentos disponíveis:`);
58
+ for (const [tipo, n] of counts) logger.info(` - ${n} ${tipo.toUpperCase()}(s)`);
86
59
  }
87
60
  } catch (error) {
88
61
  logger.error("❌ Erro ao sincronizar docs:", error.message);
89
-
90
- // Dicas de troubleshooting
91
- if (error.message.includes("Token GitLab")) {
92
- logger.error("\n💡 Configure o token GitLab no .npmrc:");
93
- logger.error(" //gitlab.com/api/v4/packages/npm/:_authToken=seu-token");
94
- } else if (error.message.includes("não encontrado")) {
95
- logger.error("\n💡 Verifique se o squad existe no central-docs:");
96
- logger.error(` ${SQUAD}/index.md`);
62
+ if (error.code === 404) {
63
+ logger.error("\n💡 O squad ainda não tem índice no Central docs:");
64
+ logger.error(` ${SQUAD}/index.md (é criado na primeira publicação: jarvis docs publish)`);
97
65
  }
98
-
99
66
  process.exit(1);
100
67
  }
101
68
  }
@@ -28,7 +28,6 @@ import { tokens } from "../../commands/tokens.js";
28
28
  import { map } from "../../commands/map.js";
29
29
  import { runCreateMerge } from "../vcs/create-merge.js";
30
30
  import { runCreateIssue } from "../vcs/create-issue.js";
31
- import { runFetchRaw } from "../vcs/fetch-raw.js";
32
31
  import { runComment } from "../tasks/comment.js";
33
32
  import { fetchFile } from "../docs/fetch-file.js";
34
33
  import { formatIDEsList } from "../config/ide-config.js";
@@ -112,33 +111,38 @@ export function buildProgram() {
112
111
  const docs = cmd(program, "docs").description("Sincronizar ou publicar documentação");
113
112
 
114
113
  const docsSyncCmd = cmd(docs, "sync").description(
115
- "Sincronizar docs do central-docs (usado por /warm-up)"
114
+ "Sincronizar docs do Central docs pelo adaptador de CENTRAL_DOCS_STORAGE (usado por /warm-up)"
116
115
  );
117
116
  withEnvOptions(withLogOptions(docsSyncCmd)).action((options) => docsSync(toLegacyFlags(options)));
118
117
 
119
- cmd(docs, "fetch")
118
+ const docsFetchCmd = cmd(docs, "fetch")
120
119
  .description(
121
- "Buscar um arquivo do central-docs, com cache Redis best-effort (lê CENTRAL_DOCS_REPO do ambiente)"
120
+ "Buscar um arquivo do Central docs pelo adaptador de CENTRAL_DOCS_STORAGE (com git: pull antes de ler)"
122
121
  )
123
- .argument("<file-path>", "Path do arquivo dentro do repo central-docs")
124
- .argument("[ref]", "Branch/ref", "main")
125
- .action(async (filePath, ref) => {
126
- try {
127
- const content = await fetchFile(filePath, ref);
128
- process.stdout.write(content);
129
- } catch (error) {
130
- console.error(error.message);
131
- process.exit(error.code === 404 ? 2 : 1);
132
- }
133
- });
122
+ .argument("<file-path>", "Caminho do arquivo dentro do Central docs (ex: CORE/index.md)");
123
+ withEnvOptions(docsFetchCmd).action(async (filePath, options) => {
124
+ try {
125
+ const content = await fetchFile(filePath, { flags: toLegacyFlags(options) });
126
+ process.stdout.write(content);
127
+ } catch (error) {
128
+ console.error(error.message);
129
+ process.exit(error.code === 404 ? 2 : 1);
130
+ }
131
+ });
134
132
 
135
133
  const docsPublishCmd = cmd(docs, "publish")
136
- .description("Publicar documento no central-docs (branch + MR/PR)")
134
+ .description(
135
+ "Publicar documento no Central docs (pasta: copia e regenera o índice; com git: branch + PR)"
136
+ )
137
137
  .option("--file <path>", "Caminho do arquivo local")
138
- .option("--tipo <tipo>", "prd|frd|ard|rfc|swagger|qa-report")
139
- .option("--feature <slug>", "Slug da feature (kebab-case)")
138
+ .option("--tipo <tipo>", "prd|frd|feat|epic|story|task|ard|rfc|swagger|qa-report")
139
+ .option("--produto <produto>", "Pasta do produto no Central docs (padrão: WORKSPACE do ENV.md)")
140
+ .option("--feature <slug>", "Só se o nome do arquivo fugir do padrão {tipo}-{id}-{nome}")
140
141
  .option("--squad <squad>", "Override do SQUAD do ENV.md")
141
- .option("--workspace <workspace>", "Override do WORKSPACE do ENV.md");
142
+ .option(
143
+ "--workspace <workspace>",
144
+ "Override do WORKSPACE do ENV.md (mesmo efeito de --produto)"
145
+ );
142
146
  withEnvOptions(withLogOptions(docsPublishCmd)).action((options) =>
143
147
  docsPublish(toLegacyFlags(options))
144
148
  );
@@ -201,12 +205,6 @@ export function buildProgram() {
201
205
  runCreateIssue({ repo: options.repo, title: options.title, bodyFile: options.bodyFile })
202
206
  );
203
207
 
204
- cmd(vcs, "fetch-raw")
205
- .description("Buscar o conteúdo bruto de um arquivo no central-docs (lê CENTRAL_DOCS_REPO)")
206
- .argument("[file-path]")
207
- .argument("[ref]", "Branch/ref", "main")
208
- .action((filePath, ref) => runFetchRaw(filePath, ref));
209
-
210
208
  const tasks = cmd(program, "tasks").description("Adapters de task manager");
211
209
 
212
210
  cmd(tasks, "comment")
@@ -1,39 +1,42 @@
1
1
  /**
2
- * @fileoverview Busca um arquivo do central-docs (GitLab/GitHub/Bitbucket), com
3
- * cache Redis best-effort. Substitui `fetch-file.sh` (removido) — mesmo
4
- * comportamento, sem subprocess: chama `fetchRawFile` de `vcs/api.js` direto.
2
+ * @fileoverview Leitura do Central docs pelo adaptador configurado
3
+ * (`CENTRAL_DOCS_STORAGE`), venha o conteúdo de onde vier.
5
4
  * @module docs/fetch-file
6
5
  */
7
6
 
8
- import { fetchRawFile } from "../vcs/api.js";
9
- import { redisGet, redisSetex } from "./redis-cache.js";
7
+ import { loadEnv, resolveEnvPath } from "../env-loader.js";
8
+ import { getStorage } from "./storage/index.js";
10
9
 
11
10
  /**
12
- * @param {string} filePath - Path do arquivo dentro do repo central-docs
13
- * @param {string} [ref] - Branch/ref (padrão: "main")
11
+ * Carrega o ENV.md e devolve o adaptador do Central docs.
12
+ * @param {Object} [flags] - `envFile`/`ide` (mesmas opções dos outros comandos)
14
13
  * @param {Object} [opts]
15
- * @param {string} [opts.repo] - CENTRAL_DOCS_REPO (padrão: `process.env.CENTRAL_DOCS_REPO`)
16
- * @param {string|number} [opts.cacheTtl] - TTL do cache em segundos (padrão: `process.env.CENTRAL_DOCS_CACHE_TTL` ou 3600)
17
- * @returns {Promise<string>} conteúdo do arquivo
18
- * @throws {Error} sem `filePath`, sem `repo`, ou erro do adapter (404/401/403 — ver `fetchRawFile`)
14
+ * @param {string} [opts.cwd]
15
+ * @param {Object} [opts.deps] - Dependências injetáveis do adaptador (testes)
16
+ * @returns {{ env: Record<string, string>, root: string, storage: Object|null }}
19
17
  */
20
- export async function fetchFile(filePath, ref = "main", opts = {}) {
18
+ export function openStorageFromEnv(flags = {}, { cwd = process.cwd(), deps } = {}) {
19
+ const { path, root } = resolveEnvPath(cwd, flags);
20
+ const env = loadEnv(cwd, path);
21
+ return { env, root, storage: getStorage(env, { root, deps }) };
22
+ }
23
+
24
+ /**
25
+ * Lê um arquivo do Central docs.
26
+ * @param {string} filePath - Caminho dentro do Central docs (ex: `CORE/index.md`)
27
+ * @param {Object} [opts]
28
+ * @param {Object} [opts.storage] - Adaptador já aberto; sem ele, abre pelo ENV.md
29
+ * @param {Object} [opts.flags]
30
+ * @returns {Promise<string>}
31
+ * @throws {Error} sem `filePath`, Central docs desligado, ou `code = 404`
32
+ */
33
+ export async function fetchFile(filePath, opts = {}) {
21
34
  if (!filePath) {
22
35
  throw new Error("fetchFile: file-path é obrigatório");
23
36
  }
24
-
25
- const repo = opts.repo ?? process.env.CENTRAL_DOCS_REPO;
26
- if (!repo) {
27
- throw new Error("CENTRAL_DOCS_REPO não definido no ENV.md");
37
+ const storage = opts.storage ?? openStorageFromEnv(opts.flags).storage;
38
+ if (!storage) {
39
+ throw new Error("Central docs desligado (CENTRAL_DOCS_STORAGE vazio no ENV.md).");
28
40
  }
29
-
30
- const cacheKey = `docs:${filePath}:${ref}`;
31
- const cached = redisGet(cacheKey);
32
- if (cached) return cached;
33
-
34
- const content = await fetchRawFile(repo, filePath, ref);
35
-
36
- redisSetex(cacheKey, opts.cacheTtl ?? process.env.CENTRAL_DOCS_CACHE_TTL ?? "3600", content);
37
-
38
- return content;
41
+ return storage.fetch(filePath);
39
42
  }