jarvis-ai-framework 6.0.0 → 7.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.
- package/AGENTS.md +4 -4
- package/README.md +3 -3
- package/agents/engineering/eng.docs-consolidator.agent.md +2 -2
- package/agents/engineering/qa/eng.qa.test-planner.md +2 -2
- package/bin/README.md +12 -9
- package/bin/commands/docs-publish.js +1 -1
- package/bin/commands/init.js +85 -12
- package/bin/lib/cli/program.js +22 -7
- package/bin/lib/config/ide-config.js +56 -0
- package/bin/lib/core/docs-migrate.js +17 -5
- package/bin/lib/core/env-upgrade.js +56 -10
- package/bin/lib/core/mcp-config.js +197 -0
- package/bin/lib/core/workspace-mode.js +13 -2
- package/bin/lib/docs/fetch-file.js +4 -1
- package/bin/lib/docs/publish-file.js +2 -1
- package/bin/lib/docs/storage/filesystem.js +31 -8
- package/bin/lib/docs/validate-frontmatter.js +5 -3
- package/bin/lib/integrations/cli-runner.js +412 -0
- package/bin/lib/integrations/vendors.json +243 -0
- package/bin/lib/tasks/comment.js +133 -109
- package/bin/lib/utils/git-parser.js +36 -56
- package/bin/lib/vcs/api.js +102 -158
- package/bin/lib/vcs/create-issue.js +33 -26
- package/bin/lib/vcs/create-merge.js +64 -15
- package/package.json +4 -3
- package/rules/AGENTS.md +2 -1
- package/rules/artifact-style-rules.md +1 -1
- package/rules/engineering/eng-rules.md +4 -3
- package/rules/engineering/eng.integrations-rules.md +21 -22
- package/rules/product/prod-rules.md +1 -1
- package/rules-on-demand/doc-links-rules.md +103 -0
- package/rules-on-demand/engineering/eng.start-rules.md +1 -1
- package/rules-on-demand/engineering/eng.tech-spec-rules.md +20 -4
- package/skills/AGENTS.md +8 -4
- package/skills/SKILLS-ROADMAP.md +1 -1
- package/skills/eng-backend-microservices-trace/SKILL.md +7 -8
- package/skills/eng-global-investigation/references/caso-investigar.md +1 -1
- package/skills/eng-global-pr/SKILL.md +3 -3
- package/skills/eng-global-task-comment/SKILL.md +65 -26
- package/skills/eng-global-tech-spec/SKILL.md +102 -0
- package/skills/eng-global-tech-spec/references/delivery-spec.md +846 -0
- package/skills/eng-global-tech-spec/references/reference-spec.md +107 -0
- package/skills/jarvis-docs-central/README.md +5 -8
- package/skills/jarvis-docs-central/SKILL.md +7 -6
- package/skills/jarvis-docs-index/SKILL.md +5 -0
- package/skills/jarvis-init/SKILL.md +119 -101
- package/skills/jarvis-init/assets/mcp-configs.md +57 -75
- package/skills/jarvis-init/assets/onboarding-checklist.md +6 -6
- package/skills/jarvis-init/assets/setup-guide.md +45 -22
- package/skills/jarvis-report-issue/SKILL.md +46 -10
- package/skills/product-roadmap-report/commands/status.roadmap.preview.md +1 -1
- package/templates/AGENTS.md +5 -3
- package/templates/ENV-template.md +33 -19
- package/templates/cdd-mind-map-template.md +1 -1
- package/templates/engineering/ARD-template.md +2 -2
- package/templates/engineering/PR-template.md +6 -2
- package/templates/engineering/architecture-template.md +14 -8
- package/templates/engineering/central-feature-template.md +2 -2
- package/templates/engineering/plan-template.md +4 -2
- package/templates/engineering/ref-spec-template.md +100 -0
- package/templates/engineering/tech-spec-delivery-template.md +9 -2
- package/templates/engineering/tech-spec-template.md +9 -9
- package/templates/product/prod-frd-template.md +21 -19
- package/templates/product/prod-prd-template.md +32 -30
- package/workflows/AGENTS.md +1 -1
- package/workflows/README.md +2 -2
- package/workflows/engineering/eng.breakdown-subtasks.md +2 -0
- package/workflows/engineering/eng.build-tech-spec.md +67 -902
- package/workflows/engineering/eng.create-ard-from-code.md +2 -0
- package/workflows/engineering/eng.create-ard.md +2 -0
- package/workflows/engineering/eng.create-rfc.md +2 -0
- package/workflows/engineering/eng.debug.md +3 -1
- package/workflows/engineering/eng.docs.md +3 -1
- package/workflows/engineering/eng.light-arch.md +2 -0
- package/workflows/engineering/eng.plan.md +2 -0
- package/workflows/engineering/eng.pr.md +21 -19
- package/workflows/engineering/eng.pre-pr.md +5 -1
- package/workflows/engineering/eng.review.md +2 -0
- package/workflows/engineering/eng.rpa.robot.md +3 -0
- package/workflows/engineering/eng.spec-consolidate.md +7 -5
- package/workflows/engineering/eng.start.md +6 -2
- package/workflows/engineering/eng.work.md +2 -0
- package/workflows/engineering/qa/eng.qa-refinement-entry.md +3 -3
- package/workflows/product/prod.roadmap.preview.md +1 -1
- package/workflows/product/prod.spec.ard.md +2 -0
- package/workflows/product/prod.spec.breakdown.md +2 -0
- package/workflows/product/prod.spec.clarify.md +2 -0
- package/workflows/product/prod.spec.discovery.md +2 -0
- package/workflows/product/prod.spec.epic.md +2 -0
- package/workflows/product/prod.spec.frd.md +2 -0
- package/workflows/product/prod.spec.issue.md +5 -3
- package/workflows/product/prod.spec.prd.md +2 -0
- package/workflows/warm-up.md +1 -1
- package/bin/lib/utils/npmrc-parser.js +0 -99
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Upgrade do ENV.md (`jarvis init --upgrade`): acrescenta as chaves do template que faltam.
|
|
2
2
|
// Regra: só acrescenta. Nenhuma chave existente muda, preenchida, vazia ou com lista de opções.
|
|
3
|
-
//
|
|
3
|
+
// Exceção declarada (automática): `JARVIS_PROJECT=` vazio recebe o valor oficial do template.
|
|
4
|
+
// As outras exceções (CENTRAL_DOCS_LOCATION e esvaziar o TOKEN_TASK_MANAGER) pedem confirmação
|
|
5
|
+
// e ficam no Upgrade (C) do skill jarvis-init (item 9). Mesmas regras dos itens 4, 5, 9, 10 e 11.
|
|
4
6
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
7
|
import { detectWorkspaceMode } from "./workspace-mode.js";
|
|
8
|
+
import { getVendor } from "../integrations/cli-runner.js";
|
|
6
9
|
|
|
7
10
|
const KEY_LINE = /^([A-Z][A-Z0-9_]*)=(.*)$/;
|
|
8
11
|
|
|
@@ -34,13 +37,21 @@ function isDataOnlyKey(key) {
|
|
|
34
37
|
return key.startsWith("DATA_") && key !== "DATA_SPECIALIZATIONS";
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// Chave existente e vazia que o upgrade preenche com o valor do template (exceção automática).
|
|
41
|
+
const FILL_WHEN_EMPTY = ["JARVIS_PROJECT"];
|
|
42
|
+
|
|
43
|
+
// Eixos de integração: vendor fora do catálogo vira aviso (o eixo fica em freelance).
|
|
44
|
+
const AXES = ["TASK_MANAGER", "VERSION_CONTROL"];
|
|
45
|
+
|
|
37
46
|
/**
|
|
38
47
|
* Calcula o que o upgrade acrescenta, sem gravar nada.
|
|
39
48
|
* @param {string} envText
|
|
40
49
|
* @param {string} templateText
|
|
41
50
|
* @param {{ workspaceDir?: string }} [options] - `workspaceDir`: pasta que contém o `$IDE/`, para detectar o `WORKSPACE_MODE`
|
|
42
|
-
* @returns {{ added: {key: string, value: string}[], pending: string[], warnings: string[] }}
|
|
43
|
-
* `
|
|
51
|
+
* @returns {{ added: {key: string, value: string}[], filled: {key: string, value: string}[], pending: string[], warnings: string[] }}
|
|
52
|
+
* `filled`: chaves existentes e vazias preenchidas com o valor do template (só `JARVIS_PROJECT`).
|
|
53
|
+
* `pending` e `warnings` trazem só nomes de chave, nunca valores do ENV
|
|
54
|
+
* (exceto o nome do vendor não suportado, em `UNSUPPORTED:{CHAVE}:{vendor}`).
|
|
44
55
|
*/
|
|
45
56
|
export function planEnvUpgrade(envText, templateText, { workspaceDir } = {}) {
|
|
46
57
|
const env = parseEnvEntries(envText);
|
|
@@ -49,6 +60,7 @@ export function planEnvUpgrade(envText, templateText, { workspaceDir } = {}) {
|
|
|
49
60
|
const area = (env.get("AREA") || "").toUpperCase();
|
|
50
61
|
|
|
51
62
|
const added = [];
|
|
63
|
+
const filled = [];
|
|
52
64
|
const pending = [];
|
|
53
65
|
const warnings = [];
|
|
54
66
|
|
|
@@ -75,23 +87,57 @@ export function planEnvUpgrade(envText, templateText, { workspaceDir } = {}) {
|
|
|
75
87
|
|
|
76
88
|
if (env.has("PROD_MODE") && !env.get("PROD_MODE")) pending.push("PROD_MODE");
|
|
77
89
|
|
|
90
|
+
for (const key of FILL_WHEN_EMPTY) {
|
|
91
|
+
const value = template.get(key);
|
|
92
|
+
if (env.has(key) && !env.get(key) && value && !isOptionList(value)) filled.push({ key, value });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// O token não é mais usado (o login é o da CLI do vendor). Só o nome da chave sai daqui.
|
|
96
|
+
if (env.get("TOKEN_TASK_MANAGER")) warnings.push("TOKEN_TASK_MANAGER");
|
|
97
|
+
|
|
98
|
+
for (const key of AXES) {
|
|
99
|
+
const vendor = (env.get(key) || "").toLowerCase();
|
|
100
|
+
if (vendor && !isOptionList(vendor) && !getVendor(vendor)) {
|
|
101
|
+
warnings.push(`UNSUPPORTED:${key}:${vendor.replace(/[^a-z0-9_-]/g, "")}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
if (env.get("CENTRAL_DOCS_REPO") && !env.has("CENTRAL_DOCS_STORAGE")) {
|
|
79
106
|
warnings.push("CENTRAL_DOCS_REPO");
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
return { added, pending, warnings };
|
|
109
|
+
return { added, filled, pending, warnings };
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
/**
|
|
86
|
-
* Grava as chaves novas no fim do ENV, num bloco datado
|
|
113
|
+
* Grava as chaves novas no fim do ENV, num bloco datado, e preenche as chaves de `plan.filled`
|
|
114
|
+
* (linha `CHAVE=` vazia). Nenhuma outra linha existente muda.
|
|
87
115
|
* `overrides` troca o valor de chaves novas (ex.: recusa em mover os discoveries).
|
|
88
116
|
* @returns {boolean} true se gravou
|
|
89
117
|
*/
|
|
90
118
|
export function applyEnvUpgrade(envPath, plan, today, overrides = {}) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
const filled = plan.filled || [];
|
|
120
|
+
if (plan.added.length === 0 && filled.length === 0) {
|
|
121
|
+
plan.filledApplied = [];
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
let text = readFileSync(envPath, "utf-8");
|
|
125
|
+
// `plan.filledApplied`: só as chaves que de fato mudaram (para o resumo não mentir).
|
|
126
|
+
plan.filledApplied = [];
|
|
127
|
+
for (const { key, value } of filled) {
|
|
128
|
+
const before = text;
|
|
129
|
+
// `\r?` cobre ENV com CRLF; a função evita que um `$` do valor vire referência de grupo.
|
|
130
|
+
text = text.replace(
|
|
131
|
+
new RegExp(`^([ \\t]*)${key}=[ \\t]*(\\r?)$`, "m"),
|
|
132
|
+
(_m, indent, cr) => `${indent}${key}=${value}${cr}`
|
|
133
|
+
);
|
|
134
|
+
if (text !== before) plan.filledApplied.push(key);
|
|
135
|
+
}
|
|
136
|
+
if (plan.added.length > 0) {
|
|
137
|
+
const lines = plan.added.map(({ key, value }) => `${key}=${overrides[key] ?? value}`);
|
|
138
|
+
const sep = text.endsWith("\n") ? "" : "\n";
|
|
139
|
+
text = `${text}${sep}\n# --- Upgrade ${today} ---\n${lines.join("\n")}\n`;
|
|
140
|
+
}
|
|
141
|
+
writeFileSync(envPath, text);
|
|
96
142
|
return true;
|
|
97
143
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// MCP obrigatório (Context7) no arquivo e no formato que cada IDE lê (`jarvis init`).
|
|
2
|
+
// Os caminhos vêm de `getMCPTargets` (ide-config.js). Regras:
|
|
3
|
+
// - arquivo ausente → cria só com o Context7;
|
|
4
|
+
// - arquivo presente → acrescenta o `context7` se faltar, sem tocar nos outros servidores
|
|
5
|
+
// e mantendo a indentação, o BOM e a quebra de linha (LF ou CRLF);
|
|
6
|
+
// - o que não reconhece com segurança (JSON com comentários ou inválido, raiz ou lista de
|
|
7
|
+
// servidores que não é objeto, TOML que não parseia ou em que o bloco não cabe) → não mexe
|
|
8
|
+
// e devolve o trecho para a pessoa colar. O TOML é lido com parser (smol-toml), mas nunca
|
|
9
|
+
// reescrito: o bloco é acrescentado no fim;
|
|
10
|
+
// - config só global (`~/…`) → não grava fora do workspace: devolve o trecho.
|
|
11
|
+
// Nada disso muda o código de saída do `jarvis init`: são avisos.
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { parse as parseTomlText } from "smol-toml";
|
|
15
|
+
import { getMCPTargets } from "../config/ide-config.js";
|
|
16
|
+
|
|
17
|
+
export const CONTEXT7 = Object.freeze({
|
|
18
|
+
name: "context7",
|
|
19
|
+
command: "npx",
|
|
20
|
+
args: Object.freeze(["-y", "@upstash/context7-mcp"]),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Arquivo que existe mas não dá para alterar com segurança: fica intacto e vira `manual`. */
|
|
24
|
+
class Unrecognized extends Error {}
|
|
25
|
+
|
|
26
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
27
|
+
|
|
28
|
+
const filled = (v) => typeof v === "string" && v.trim() !== "";
|
|
29
|
+
|
|
30
|
+
/** Chaves de URL de servidor remoto: `url`, `httpUrl` (Gemini CLI), `serverUrl` (Antigravity). */
|
|
31
|
+
const URL_KEYS = ["url", "httpUrl", "serverUrl"];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A entrada parece um servidor MCP utilizável? Objeto com `command` (string não vazia ou, só
|
|
35
|
+
* onde a IDE aceita, como no OpenCode, array não vazio de strings não vazias) ou com uma URL
|
|
36
|
+
* não vazia (`URL_KEYS`).
|
|
37
|
+
*/
|
|
38
|
+
function looksLikeServer(entry, commandAsList) {
|
|
39
|
+
if (!isPlainObject(entry)) return false;
|
|
40
|
+
const { command } = entry;
|
|
41
|
+
const hasCommand = Array.isArray(command)
|
|
42
|
+
? commandAsList && command.length > 0 && command.every(filled)
|
|
43
|
+
: filled(command);
|
|
44
|
+
return hasCommand || URL_KEYS.some((k) => filled(entry[k]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* O servidor `context7` já está declarado? `true` se a entrada parece um servidor
|
|
49
|
+
* (`looksLikeServer`); `false` se não existe; `Unrecognized` se existe mas não parece
|
|
50
|
+
* (`1`, `false`, `[]`, tabela vazia, `command` vazio…), para não dizer "já configurado" sem estar.
|
|
51
|
+
* Um servidor válido com `enabled = false` conta como declarado: o init não liga o que a
|
|
52
|
+
* pessoa desligou.
|
|
53
|
+
*/
|
|
54
|
+
function hasContext7(servers, target) {
|
|
55
|
+
if (!servers || !Object.hasOwn(servers, CONTEXT7.name)) return false;
|
|
56
|
+
if (!looksLikeServer(servers[CONTEXT7.name], target.style === "opencode")) {
|
|
57
|
+
throw new Unrecognized(
|
|
58
|
+
`"${CONTEXT7.name}" existe mas não parece um servidor (sem command nem url preenchidos)`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Item do servidor no formato do alvo. */
|
|
65
|
+
function serverEntry(style) {
|
|
66
|
+
if (style === "opencode") {
|
|
67
|
+
return { type: "local", command: [CONTEXT7.command, ...CONTEXT7.args], enabled: true };
|
|
68
|
+
}
|
|
69
|
+
const entry = { command: CONTEXT7.command, args: [...CONTEXT7.args] };
|
|
70
|
+
return style === "stdio" ? { type: "stdio", ...entry } : entry;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Trecho pronto para colar (usado nos alvos globais e nos arquivos não reconhecidos). */
|
|
74
|
+
export function mcpSnippet(target) {
|
|
75
|
+
if (target.format === "toml") {
|
|
76
|
+
return [
|
|
77
|
+
`[${target.key}.${CONTEXT7.name}]`,
|
|
78
|
+
`command = "${CONTEXT7.command}"`,
|
|
79
|
+
`args = [${CONTEXT7.args.map((a) => `"${a}"`).join(", ")}]`,
|
|
80
|
+
"",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
const block = { [target.key]: { [CONTEXT7.name]: serverEntry(target.style) } };
|
|
84
|
+
return `${JSON.stringify(block, null, 2)}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Indentação de um JSON existente: tab ou N espaços (2 se não der para saber). */
|
|
88
|
+
function detectIndent(text) {
|
|
89
|
+
const m = text.match(/^([ \t]+)\S/m);
|
|
90
|
+
if (!m) return 2;
|
|
91
|
+
return m[1].startsWith("\t") ? "\t" : m[1].length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Quebra de linha usada no arquivo: CRLF se aparecer, senão LF. */
|
|
95
|
+
const detectEol = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
|
|
96
|
+
|
|
97
|
+
const BOM = "\uFEFF";
|
|
98
|
+
|
|
99
|
+
function ensureJson(file, target) {
|
|
100
|
+
let data = {};
|
|
101
|
+
let indent = 2;
|
|
102
|
+
let bom = "";
|
|
103
|
+
let eol = "\n";
|
|
104
|
+
if (existsSync(file)) {
|
|
105
|
+
const raw = readFileSync(file, "utf-8");
|
|
106
|
+
bom = raw.startsWith(BOM) ? BOM : "";
|
|
107
|
+
const text = raw.slice(bom.length);
|
|
108
|
+
indent = detectIndent(text);
|
|
109
|
+
eol = detectEol(text);
|
|
110
|
+
try {
|
|
111
|
+
data = JSON.parse(text);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const detail = err.message.split("\n")[0];
|
|
114
|
+
const reason = "não consegui ler o JSON com segurança (comentários ou erro de sintaxe";
|
|
115
|
+
throw new Unrecognized(`${reason}: ${detail})`);
|
|
116
|
+
}
|
|
117
|
+
if (!isPlainObject(data)) throw new Unrecognized("o arquivo não é um objeto JSON");
|
|
118
|
+
} else if (target.style === "opencode") {
|
|
119
|
+
data.$schema = "https://opencode.ai/config.json";
|
|
120
|
+
}
|
|
121
|
+
const current = data[target.key];
|
|
122
|
+
if (current != null && !isPlainObject(current)) {
|
|
123
|
+
throw new Unrecognized(`"${target.key}" não é um objeto`);
|
|
124
|
+
}
|
|
125
|
+
const servers = current ?? {};
|
|
126
|
+
if (hasContext7(servers, target)) return false;
|
|
127
|
+
data[target.key] = { ...servers, [CONTEXT7.name]: serverEntry(target.style) };
|
|
128
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
129
|
+
const body = JSON.stringify(data, null, indent).replace(/\n/g, eol);
|
|
130
|
+
writeFileSync(file, `${bom}${body}${eol}`);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Lê um TOML; devolve `null` se não parseia. */
|
|
135
|
+
function parseToml(text) {
|
|
136
|
+
try {
|
|
137
|
+
return parseTomlText(text);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Codex: lê o `config.toml` com um parser (smol-toml) para saber se o servidor já existe.
|
|
145
|
+
* O texto nunca é reescrito pelo parser (comentários e formatação ficam); o bloco é
|
|
146
|
+
* acrescentado no fim, com o EOL do arquivo, e só se o resultado também parsear.
|
|
147
|
+
*/
|
|
148
|
+
function ensureToml(file, target) {
|
|
149
|
+
const text = existsSync(file) ? readFileSync(file, "utf-8") : "";
|
|
150
|
+
const data = parseToml(text);
|
|
151
|
+
if (!data) throw new Unrecognized("o TOML não pôde ser lido (sintaxe inválida)");
|
|
152
|
+
const servers = data[target.key];
|
|
153
|
+
if (servers !== undefined && !isPlainObject(servers)) {
|
|
154
|
+
throw new Unrecognized(`"${target.key}" não é uma tabela`);
|
|
155
|
+
}
|
|
156
|
+
if (hasContext7(servers, target)) return false;
|
|
157
|
+
const eol = detectEol(text);
|
|
158
|
+
const sep = text === "" || text.endsWith(eol + eol) ? "" : text.endsWith(eol) ? eol : eol + eol;
|
|
159
|
+
const addition = `${sep}${mcpSnippet(target).replace(/\n/g, eol)}`;
|
|
160
|
+
if (!parseToml(text + addition)?.[target.key]?.[CONTEXT7.name]) {
|
|
161
|
+
throw new Unrecognized(`acrescentar [${target.key}.${CONTEXT7.name}] deixaria o TOML inválido`);
|
|
162
|
+
}
|
|
163
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
164
|
+
appendFileSync(file, addition);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Garante o Context7 na configuração de MCP da IDE.
|
|
170
|
+
* @param {string} workspaceDir - pasta que contém o `$IDE/`
|
|
171
|
+
* @param {string} ide - slug da IDE
|
|
172
|
+
* @returns {{ written: string[], present: string[],
|
|
173
|
+
* manual: {path: string, snippet: string, reason?: string}[], errors: string[] }}
|
|
174
|
+
* caminhos relativos ao workspace (ou com `~/`, nos globais). `manual` sem `reason` é
|
|
175
|
+
* config só global; com `reason`, é arquivo do workspace que ficou intacto.
|
|
176
|
+
*/
|
|
177
|
+
export function ensureContext7(workspaceDir, ide) {
|
|
178
|
+
const result = { written: [], present: [], manual: [], errors: [] };
|
|
179
|
+
for (const target of getMCPTargets(ide)) {
|
|
180
|
+
if (target.global) {
|
|
181
|
+
result.manual.push({ path: target.path, snippet: mcpSnippet(target) });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const file = join(workspaceDir, target.path);
|
|
185
|
+
try {
|
|
186
|
+
const wrote = target.format === "toml" ? ensureToml(file, target) : ensureJson(file, target);
|
|
187
|
+
(wrote ? result.written : result.present).push(target.path);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err instanceof Unrecognized) {
|
|
190
|
+
result.manual.push({ path: target.path, snippet: mcpSnippet(target), reason: err.message });
|
|
191
|
+
} else {
|
|
192
|
+
result.errors.push(`${target.path}: ${err.message.split("\n")[0]}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
@@ -25,17 +25,28 @@ function listGitSubdirs(dir) {
|
|
|
25
25
|
.sort();
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A raiz é um repositório (tem `.git`, pasta ou arquivo)? É o critério do `.gitignore`:
|
|
30
|
+
* só se cria ou altera o `.gitignore` de uma raiz versionada. Uma pasta sem nenhum `.git`
|
|
31
|
+
* é `project` para o modo, mas não é versionada.
|
|
32
|
+
* @param {string} dir
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function rootIsRepo(dir) {
|
|
36
|
+
return existsSync(join(dir, ".git"));
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/**
|
|
29
40
|
* @param {string} workspaceDir - pasta que contém o `$IDE/`
|
|
30
41
|
* @param {{ repos?: string[] }} [options] - `repos`: allowlist (WORKSPACE_REPOS); vazia = todas
|
|
31
42
|
* @returns {{ mode: "workspace"|"project", repos: string[], warning: string|null }}
|
|
32
43
|
*/
|
|
33
44
|
export function detectWorkspaceMode(workspaceDir, { repos = [] } = {}) {
|
|
34
|
-
const
|
|
45
|
+
const isRepo = rootIsRepo(workspaceDir);
|
|
35
46
|
const allow = new Set(repos);
|
|
36
47
|
const found = listGitSubdirs(workspaceDir).filter((n) => allow.size === 0 || allow.has(n));
|
|
37
48
|
|
|
38
|
-
if (
|
|
49
|
+
if (isRepo) {
|
|
39
50
|
return {
|
|
40
51
|
mode: "project",
|
|
41
52
|
repos: [],
|
|
@@ -18,7 +18,10 @@ import { getStorage } from "./storage/index.js";
|
|
|
18
18
|
export function openStorageFromEnv(flags = {}, { cwd = process.cwd(), deps } = {}) {
|
|
19
19
|
const { path, root } = resolveEnvPath(cwd, flags);
|
|
20
20
|
const env = loadEnv(cwd, path);
|
|
21
|
-
|
|
21
|
+
// O ENV.md lido aqui (com --ide/--env-file) segue até o gate do acesso no MR do Central docs
|
|
22
|
+
// (o `docs sync` e o `fetch` não abrem MR e ignoram o fileEnvResult).
|
|
23
|
+
const storageDeps = { fileEnvResult: { vars: env, error: null }, ...deps };
|
|
24
|
+
return { env, root, storage: getStorage(env, { root, deps: storageDeps }) };
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (`CENTRAL_DOCS_STORAGE`), a consolidação do que foi implementado, gerada pelo
|
|
4
4
|
* `/eng.spec-consolidate`: o documento da funcionalidade (`feature-*.md`, em
|
|
5
5
|
* `{SQUAD}/product/{produto}/`) e o registro da entrega (`delivery-*.md`, em
|
|
6
|
-
* `{SQUAD}/engineering/{produto}/`). Documentos de trabalho (PRD, FRD, ARD, RFC…)
|
|
6
|
+
* `{SQUAD}/engineering/{produto}/`). Documentos de trabalho (PRD, FRD, ARD, RFC, SPEC…)
|
|
7
7
|
* não são publicados. O índice do squad é regenerado a cada publicação.
|
|
8
8
|
* @module docs/publish-file
|
|
9
9
|
*/
|
|
@@ -28,6 +28,7 @@ export const WORKING_DOC_TYPES = Object.freeze([
|
|
|
28
28
|
"task",
|
|
29
29
|
"ard",
|
|
30
30
|
"rfc",
|
|
31
|
+
"spec",
|
|
31
32
|
"swagger",
|
|
32
33
|
"qa-report",
|
|
33
34
|
]);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
14
14
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
-
import { createMergeRequest as defaultCreateMergeRequest } from "../../vcs/api.js";
|
|
15
|
+
import { createMergeRequest as defaultCreateMergeRequest, newMergeRequestUrl } from "../../vcs/api.js";
|
|
16
16
|
import { logger } from "../../utils/logger.js";
|
|
17
17
|
import { buildIndex, walkFiles } from "./build-index.js";
|
|
18
18
|
|
|
@@ -55,7 +55,8 @@ export function safePath(root, rel) {
|
|
|
55
55
|
* @param {string} [cfg.ref] - Branch lida no `pull` (CENTRAL_DOCS_REF)
|
|
56
56
|
* @param {string} [cfg.targetBranch] - Branch base do PR (CENTRAL_DOCS_TARGET_BRANCH)
|
|
57
57
|
* @param {string} [cfg.reviewers] - Revisores (vão na descrição do PR)
|
|
58
|
-
* @param {Object} [cfg.deps] - `runGit` e `createMergeRequest` injetáveis (testes)
|
|
58
|
+
* @param {Object} [cfg.deps] - `runGit` e `createMergeRequest` injetáveis (testes); `fileEnvResult`:
|
|
59
|
+
* o ENV.md já lido pelo comando, repassado ao gate do `VERSION_CONTROL_ACCESS` no MR
|
|
59
60
|
*/
|
|
60
61
|
export function createFilesystemStorage({
|
|
61
62
|
location,
|
|
@@ -183,12 +184,34 @@ export function createFilesystemStorage({
|
|
|
183
184
|
const remoteUrl = git(["remote", "get-url", "origin"], location);
|
|
184
185
|
let description = `## Documento\n\n- **Tipo**: ${tipo}\n- **Arquivo**: \`${destPath}\``;
|
|
185
186
|
if (reviewers) description += `\n- **Revisores**: ${reviewers}`;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
let mr = null;
|
|
188
|
+
try {
|
|
189
|
+
// O ENV.md que o comando já leu (com --ide/--env-file) vai junto para o gate do acesso.
|
|
190
|
+
mr = await createMergeRequest(
|
|
191
|
+
remoteUrl,
|
|
192
|
+
{
|
|
193
|
+
source: branch,
|
|
194
|
+
target: targetBranch,
|
|
195
|
+
title: `docs(${squad}): ${tipo} ${basename(destPath)}`,
|
|
196
|
+
description,
|
|
197
|
+
},
|
|
198
|
+
deps.fileEnvResult ? { fileEnvResult: deps.fileEnvResult } : {}
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// Indisponível (sem CLI ou sem login): o push já foi feito, o MR fica para abrir à mão.
|
|
202
|
+
// Enviado com erro continua subindo: o MR pode ter sido criado.
|
|
203
|
+
if (error.sent) throw error;
|
|
204
|
+
let link = "";
|
|
205
|
+
try {
|
|
206
|
+
link = newMergeRequestUrl(remoteUrl, branch, targetBranch);
|
|
207
|
+
} catch {
|
|
208
|
+
// remote fora de github/gitlab: sem link
|
|
209
|
+
}
|
|
210
|
+
logger.warn(
|
|
211
|
+
`⚠️ MR não aberto (${error.message.split("\n")[0]}). A branch ${branch} já está no remote` +
|
|
212
|
+
(link ? `; abra o MR à mão: ${link}` : "; abra o MR à mão no host.")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
192
215
|
|
|
193
216
|
return { destPath, indexPath, sha, branch, mr };
|
|
194
217
|
} finally {
|
|
@@ -2,7 +2,8 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { parseFrontmatter, extractFrontmatterBlock } from '../utils/frontmatter.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Valida frontmatter YAML
|
|
5
|
+
* Valida frontmatter YAML da consolidação do Central docs (feature, delivery) e de documentos
|
|
6
|
+
* de trabalho (PRD, FRD, épico, história, tarefa, ARD, RFC, relatório de QA)
|
|
6
7
|
*
|
|
7
8
|
* @param {string} filePath - Caminho do arquivo
|
|
8
9
|
* @param {string} tipo - Tipo do documento (ver VALID_TYPES em publish-file.js)
|
|
@@ -55,10 +56,11 @@ export function validateFrontmatter(filePath, tipo) {
|
|
|
55
56
|
|
|
56
57
|
// Validar status
|
|
57
58
|
// Produto (rules/product/prod-rules.md), ARD (templates/engineering/ARD-template.md),
|
|
58
|
-
// discovery (prod-discovery-template.md)
|
|
59
|
+
// discovery (prod-discovery-template.md), RFC (RFC-template.md) e
|
|
60
|
+
// SPEC (templates/engineering/ref-spec-template.md)
|
|
59
61
|
const validStatuses = [
|
|
60
62
|
'icebox', 'in_review', 'backlog', 'in_progress', 'in_production', 'cancelled', 'deprecated',
|
|
61
|
-
'proposed', 'approved',
|
|
63
|
+
'proposed', 'approved', 'superseded',
|
|
62
64
|
'Draft', 'In Review', 'Ready for Decision', 'Accepted', 'Rejected', 'Superseded'
|
|
63
65
|
]
|
|
64
66
|
|