spec-first-copilot 0.7.0-beta.1 → 0.7.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 +252 -167
- package/bin/cli.js +70 -70
- package/lib/init.js +92 -92
- package/lib/update.js +132 -132
- package/package.json +1 -1
- package/templates/.ai/memory/napkin.md +68 -68
- package/templates/.github/CHANGELOG.md +560 -533
- package/templates/.github/adapters/SETUP.md +314 -314
- package/templates/.github/adapters/confluence.md +295 -295
- package/templates/.github/adapters/errors.md +234 -234
- package/templates/.github/adapters/filesystem.md +353 -353
- package/templates/.github/adapters/interface.md +301 -301
- package/templates/.github/adapters/naming.md +241 -241
- package/templates/.github/adapters/registry.md +244 -244
- package/templates/.github/agents/backend-coder.md +215 -215
- package/templates/.github/agents/db-coder.md +165 -165
- package/templates/.github/agents/doc-writer.md +66 -66
- package/templates/.github/agents/frontend-coder.md +222 -222
- package/templates/.github/agents/infra-coder.md +341 -341
- package/templates/.github/agents/reviewer.md +99 -99
- package/templates/.github/agents/security-reviewer.md +153 -153
- package/templates/.github/copilot-instructions.md +272 -272
- package/templates/.github/instructions/docs.instructions.md +147 -145
- package/templates/.github/instructions/sensitive-files.instructions.md +32 -32
- package/templates/.github/rules.md +229 -229
- package/templates/.github/scripts/bootstrap-confluence.js +289 -289
- package/templates/.github/skills/sf-design/SKILL.md +161 -161
- package/templates/.github/skills/sf-dev/SKILL.md +204 -204
- package/templates/.github/skills/sf-discovery/SKILL.md +415 -415
- package/templates/.github/skills/sf-extract/SKILL.md +225 -225
- package/templates/.github/skills/sf-load/SKILL.md +296 -296
- package/templates/.github/skills/sf-mcp/SKILL.md +386 -386
- package/templates/.github/skills/sf-merge-docs/SKILL.md +152 -152
- package/templates/.github/skills/sf-plan/SKILL.md +152 -152
- package/templates/.github/skills/sf-publish/SKILL.md +144 -144
- package/templates/.github/skills/sf-session-finish/SKILL.md +93 -93
- package/templates/.github/skills/sf-start/SKILL.md +192 -192
- package/templates/.github/templates/estrutura/apiContracts.template.md +160 -159
- package/templates/.github/templates/estrutura/architecture.template.md +169 -168
- package/templates/.github/templates/estrutura/conventions.template.md +214 -212
- package/templates/.github/templates/estrutura/decisions.template.md +107 -107
- package/templates/.github/templates/estrutura/domain.template.md +161 -160
- package/templates/.github/templates/feature/PRD.template.md +279 -279
- package/templates/.github/templates/feature/Progresso.template.md +141 -141
- package/templates/.github/templates/feature/TRD.template.md +358 -358
- package/templates/.github/templates/feature/context.template.md +89 -89
- package/templates/.github/templates/feature/extract-log.template.md +49 -49
- package/templates/.github/templates/feature/projetos.template.yaml +79 -79
- package/templates/.github/templates/global/progresso_global.template.md +59 -57
- package/templates/.github/templates/specs/brief.template.md +66 -66
- package/templates/.github/templates/specs/contracts.template.md +147 -147
- package/templates/.github/templates/specs/scenarios.template.md +125 -125
- package/templates/.github/templates/specs/tasks.template.md +65 -65
- package/templates/_gitignore +35 -35
- package/templates/sfw.config.yml.example +147 -147
package/lib/init.js
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const PLACEHOLDER = '{{PROJECT_NAME}}';
|
|
5
|
-
|
|
6
|
-
function copyDir(src, dest, replacements) {
|
|
7
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
8
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
9
|
-
const srcPath = path.join(src, entry.name);
|
|
10
|
-
// Rename _gitignore → .gitignore (npm strips .gitignore from packages)
|
|
11
|
-
const destName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
12
|
-
const destPath = path.join(dest, destName);
|
|
13
|
-
if (entry.isDirectory()) {
|
|
14
|
-
copyDir(srcPath, destPath, replacements);
|
|
15
|
-
} else {
|
|
16
|
-
copyFile(srcPath, destPath, replacements);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function copyFile(src, dest, replacements) {
|
|
22
|
-
// Skip if file already exists (preserve user's files)
|
|
23
|
-
if (fs.existsSync(dest)) return;
|
|
24
|
-
|
|
25
|
-
const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
|
|
26
|
-
const ext = path.extname(src).toLowerCase();
|
|
27
|
-
const basename = path.basename(src);
|
|
28
|
-
const isText = textExtensions.includes(ext) || basename.startsWith('.') || basename === '_gitignore';
|
|
29
|
-
|
|
30
|
-
if (isText) {
|
|
31
|
-
let content = fs.readFileSync(src, 'utf-8');
|
|
32
|
-
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
33
|
-
content = content.split(placeholder).join(value);
|
|
34
|
-
}
|
|
35
|
-
fs.writeFileSync(dest, content, 'utf-8');
|
|
36
|
-
} else {
|
|
37
|
-
fs.copyFileSync(src, dest);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function init({ name, templatesDir, targetDir }) {
|
|
42
|
-
const dest = targetDir || path.resolve(process.cwd(), name);
|
|
43
|
-
|
|
44
|
-
const existed = fs.existsSync(dest);
|
|
45
|
-
console.log(`\n${existed ? 'Inicializando' : 'Criando'} projeto "${name}" em ${dest}\n`);
|
|
46
|
-
|
|
47
|
-
const replacements = { [PLACEHOLDER]: name };
|
|
48
|
-
copyDir(templatesDir, dest, replacements);
|
|
49
|
-
patchProjectName(dest, name);
|
|
50
|
-
|
|
51
|
-
console.log(`Pronto! Projeto "${name}" criado.`);
|
|
52
|
-
console.log('\nPróximos passos:');
|
|
53
|
-
console.log(` 1. cd ${name}`);
|
|
54
|
-
console.log(' 2. Crie uma pasta em workspace/Input/ com os insumos do seu projeto');
|
|
55
|
-
console.log(' (ex: workspace/Input/meu_app/requisitos.md)');
|
|
56
|
-
console.log('');
|
|
57
|
-
console.log(' 3. (Opcional) Configure o backend de input/output:');
|
|
58
|
-
console.log(' cp sfw.config.yml.example sfw.config.yml');
|
|
59
|
-
console.log(' Sem isso, o framework roda 100% local (sem Confluence/etc.)');
|
|
60
|
-
console.log('');
|
|
61
|
-
console.log(' 4. (Opcional, se vai usar Confluence) Prepare o ambiente ANTES de abrir o VS Code:');
|
|
62
|
-
console.log(' node .github/scripts/bootstrap-confluence.js');
|
|
63
|
-
console.log(' Valida uvx, coleta credenciais, gera .mcp.json e pré-cacheia o MCP.');
|
|
64
|
-
console.log(' Evita o ciclo "abrir Copilot → faltar dep → reiniciar VS Code".');
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log(' 5. Abra o VS Code com Copilot Chat e rode:');
|
|
67
|
-
console.log(' /sf-start <nome-da-pasta-em-Input>');
|
|
68
|
-
console.log(' O framework detecta first-run automaticamente (cria docs/) vs feature');
|
|
69
|
-
console.log(' (atualiza docs/ via merge).');
|
|
70
|
-
console.log('');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function patchProjectName(dest, name) {
|
|
74
|
-
const filesToPatch = [
|
|
75
|
-
path.join('.ai', 'memory', 'napkin.md'),
|
|
76
|
-
path.join('.github', 'copilot-instructions.md'),
|
|
77
|
-
'CLAUDE.md',
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
for (const relPath of filesToPatch) {
|
|
81
|
-
const filePath = path.join(dest, relPath);
|
|
82
|
-
if (!fs.existsSync(filePath)) continue;
|
|
83
|
-
|
|
84
|
-
let content = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
-
content = content.replace(/# Napkin Runbook\b/, `# Napkin Runbook — ${name}`);
|
|
86
|
-
content = content.replace(/# Copilot Instructions — Projeto\b/, `# Copilot Instructions — ${name}`);
|
|
87
|
-
content = content.replace(/# CLAUDE\.md — Projeto\b/, `# CLAUDE.md — ${name}`);
|
|
88
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { init };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const PLACEHOLDER = '{{PROJECT_NAME}}';
|
|
5
|
+
|
|
6
|
+
function copyDir(src, dest, replacements) {
|
|
7
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
8
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
9
|
+
const srcPath = path.join(src, entry.name);
|
|
10
|
+
// Rename _gitignore → .gitignore (npm strips .gitignore from packages)
|
|
11
|
+
const destName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
12
|
+
const destPath = path.join(dest, destName);
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
copyDir(srcPath, destPath, replacements);
|
|
15
|
+
} else {
|
|
16
|
+
copyFile(srcPath, destPath, replacements);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyFile(src, dest, replacements) {
|
|
22
|
+
// Skip if file already exists (preserve user's files)
|
|
23
|
+
if (fs.existsSync(dest)) return;
|
|
24
|
+
|
|
25
|
+
const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
|
|
26
|
+
const ext = path.extname(src).toLowerCase();
|
|
27
|
+
const basename = path.basename(src);
|
|
28
|
+
const isText = textExtensions.includes(ext) || basename.startsWith('.') || basename === '_gitignore';
|
|
29
|
+
|
|
30
|
+
if (isText) {
|
|
31
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
32
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
33
|
+
content = content.split(placeholder).join(value);
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
36
|
+
} else {
|
|
37
|
+
fs.copyFileSync(src, dest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function init({ name, templatesDir, targetDir }) {
|
|
42
|
+
const dest = targetDir || path.resolve(process.cwd(), name);
|
|
43
|
+
|
|
44
|
+
const existed = fs.existsSync(dest);
|
|
45
|
+
console.log(`\n${existed ? 'Inicializando' : 'Criando'} projeto "${name}" em ${dest}\n`);
|
|
46
|
+
|
|
47
|
+
const replacements = { [PLACEHOLDER]: name };
|
|
48
|
+
copyDir(templatesDir, dest, replacements);
|
|
49
|
+
patchProjectName(dest, name);
|
|
50
|
+
|
|
51
|
+
console.log(`Pronto! Projeto "${name}" criado.`);
|
|
52
|
+
console.log('\nPróximos passos:');
|
|
53
|
+
console.log(` 1. cd ${name}`);
|
|
54
|
+
console.log(' 2. Crie uma pasta em workspace/Input/ com os insumos do seu projeto');
|
|
55
|
+
console.log(' (ex: workspace/Input/meu_app/requisitos.md)');
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' 3. (Opcional) Configure o backend de input/output:');
|
|
58
|
+
console.log(' cp sfw.config.yml.example sfw.config.yml');
|
|
59
|
+
console.log(' Sem isso, o framework roda 100% local (sem Confluence/etc.)');
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(' 4. (Opcional, se vai usar Confluence) Prepare o ambiente ANTES de abrir o VS Code:');
|
|
62
|
+
console.log(' node .github/scripts/bootstrap-confluence.js');
|
|
63
|
+
console.log(' Valida uvx, coleta credenciais, gera .mcp.json e pré-cacheia o MCP.');
|
|
64
|
+
console.log(' Evita o ciclo "abrir Copilot → faltar dep → reiniciar VS Code".');
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(' 5. Abra o VS Code com Copilot Chat e rode:');
|
|
67
|
+
console.log(' /sf-start <nome-da-pasta-em-Input>');
|
|
68
|
+
console.log(' O framework detecta first-run automaticamente (cria docs/) vs feature');
|
|
69
|
+
console.log(' (atualiza docs/ via merge).');
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function patchProjectName(dest, name) {
|
|
74
|
+
const filesToPatch = [
|
|
75
|
+
path.join('.ai', 'memory', 'napkin.md'),
|
|
76
|
+
path.join('.github', 'copilot-instructions.md'),
|
|
77
|
+
'CLAUDE.md',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
for (const relPath of filesToPatch) {
|
|
81
|
+
const filePath = path.join(dest, relPath);
|
|
82
|
+
if (!fs.existsSync(filePath)) continue;
|
|
83
|
+
|
|
84
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
content = content.replace(/# Napkin Runbook\b/, `# Napkin Runbook — ${name}`);
|
|
86
|
+
content = content.replace(/# Copilot Instructions — Projeto\b/, `# Copilot Instructions — ${name}`);
|
|
87
|
+
content = content.replace(/# CLAUDE\.md — Projeto\b/, `# CLAUDE.md — ${name}`);
|
|
88
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { init };
|
package/lib/update.js
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const FRAMEWORK_DIRS = [
|
|
5
|
-
path.join('.github', 'adapters'),
|
|
6
|
-
path.join('.github', 'skills'),
|
|
7
|
-
path.join('.github', 'agents'),
|
|
8
|
-
path.join('.github', 'templates'),
|
|
9
|
-
path.join('.github', 'instructions'),
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
const FRAMEWORK_FILES = [
|
|
13
|
-
path.join('.github', 'rules.md'),
|
|
14
|
-
path.join('.github', 'copilot-instructions.md'),
|
|
15
|
-
path.join('.github', 'CHANGELOG.md'),
|
|
16
|
-
'sfw.config.yml.example',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
function update({ templatesDir, targetDir, force }) {
|
|
20
|
-
const dest = targetDir || process.cwd();
|
|
21
|
-
|
|
22
|
-
if (!fs.existsSync(dest)) {
|
|
23
|
-
console.error(`Erro: diretório "${dest}" não existe.`);
|
|
24
|
-
console.error('Rode "spec-first-copilot init --name=<nome>" primeiro pra criar o projeto.');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const githubDir = path.join(dest, '.github');
|
|
29
|
-
if (!fs.existsSync(githubDir)) {
|
|
30
|
-
console.error('Erro: não é um projeto spec-first (pasta .github/ não encontrada).');
|
|
31
|
-
console.error('Rode "spec-first-copilot init --name=<nome>" primeiro.');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
console.log(`\nAtualizando projeto em ${dest}\n`);
|
|
36
|
-
|
|
37
|
-
const stats = { added: [], updated: [], skipped: 0 };
|
|
38
|
-
syncDir(templatesDir, dest, force, stats, dest);
|
|
39
|
-
|
|
40
|
-
console.log('');
|
|
41
|
-
if (stats.added.length > 0) {
|
|
42
|
-
console.log(`Adicionados (${stats.added.length}):`);
|
|
43
|
-
for (const f of stats.added) console.log(` + ${f}`);
|
|
44
|
-
}
|
|
45
|
-
if (stats.updated.length > 0) {
|
|
46
|
-
console.log(`Atualizados (${stats.updated.length}):`);
|
|
47
|
-
for (const f of stats.updated) console.log(` ~ ${f}`);
|
|
48
|
-
}
|
|
49
|
-
if (stats.added.length === 0 && stats.updated.length === 0) {
|
|
50
|
-
console.log('Já está atualizado — nenhum arquivo novo pra adicionar.');
|
|
51
|
-
} else {
|
|
52
|
-
console.log(`\n${stats.added.length} adicionado(s), ${stats.updated.length} atualizado(s), ${stats.skipped} inalterado(s)`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
console.log('');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function syncDir(src, dest, force, stats, root) {
|
|
59
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
60
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
61
|
-
const srcPath = path.join(src, entry.name);
|
|
62
|
-
const destName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
63
|
-
const destPath = path.join(dest, destName);
|
|
64
|
-
if (entry.isDirectory()) {
|
|
65
|
-
syncDir(srcPath, destPath, force, stats, root);
|
|
66
|
-
} else {
|
|
67
|
-
syncFile(srcPath, destPath, force, stats, root);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function syncFile(src, dest, force, stats, root) {
|
|
73
|
-
const rel = path.relative(root, dest);
|
|
74
|
-
const exists = fs.existsSync(dest);
|
|
75
|
-
|
|
76
|
-
if (exists && !force) {
|
|
77
|
-
const isFramework = isFrameworkPath(rel);
|
|
78
|
-
if (!isFramework) {
|
|
79
|
-
stats.skipped++;
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
83
|
-
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
84
|
-
if (srcContent === destContent) {
|
|
85
|
-
stats.skipped++;
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
89
|
-
stats.updated.push(rel);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (exists && force) {
|
|
94
|
-
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
95
|
-
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
96
|
-
if (srcContent === destContent) {
|
|
97
|
-
stats.skipped++;
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
101
|
-
stats.updated.push(rel);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
|
|
106
|
-
const ext = path.extname(src).toLowerCase();
|
|
107
|
-
const basename = path.basename(src);
|
|
108
|
-
const isText = textExtensions.includes(ext) || basename.startsWith('.');
|
|
109
|
-
|
|
110
|
-
if (isText) {
|
|
111
|
-
const content = fs.readFileSync(src, 'utf-8');
|
|
112
|
-
fs.writeFileSync(dest, content, 'utf-8');
|
|
113
|
-
} else {
|
|
114
|
-
fs.copyFileSync(src, dest);
|
|
115
|
-
}
|
|
116
|
-
stats.added.push(rel);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isFrameworkPath(rel) {
|
|
120
|
-
const normalized = rel.split(path.sep).join('/');
|
|
121
|
-
for (const dir of FRAMEWORK_DIRS) {
|
|
122
|
-
const normalizedDir = dir.split(path.sep).join('/');
|
|
123
|
-
if (normalized.startsWith(normalizedDir + '/')) return true;
|
|
124
|
-
}
|
|
125
|
-
for (const file of FRAMEWORK_FILES) {
|
|
126
|
-
const normalizedFile = file.split(path.sep).join('/');
|
|
127
|
-
if (normalized === normalizedFile) return true;
|
|
128
|
-
}
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
module.exports = { update };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const FRAMEWORK_DIRS = [
|
|
5
|
+
path.join('.github', 'adapters'),
|
|
6
|
+
path.join('.github', 'skills'),
|
|
7
|
+
path.join('.github', 'agents'),
|
|
8
|
+
path.join('.github', 'templates'),
|
|
9
|
+
path.join('.github', 'instructions'),
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const FRAMEWORK_FILES = [
|
|
13
|
+
path.join('.github', 'rules.md'),
|
|
14
|
+
path.join('.github', 'copilot-instructions.md'),
|
|
15
|
+
path.join('.github', 'CHANGELOG.md'),
|
|
16
|
+
'sfw.config.yml.example',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function update({ templatesDir, targetDir, force }) {
|
|
20
|
+
const dest = targetDir || process.cwd();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(dest)) {
|
|
23
|
+
console.error(`Erro: diretório "${dest}" não existe.`);
|
|
24
|
+
console.error('Rode "spec-first-copilot init --name=<nome>" primeiro pra criar o projeto.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const githubDir = path.join(dest, '.github');
|
|
29
|
+
if (!fs.existsSync(githubDir)) {
|
|
30
|
+
console.error('Erro: não é um projeto spec-first (pasta .github/ não encontrada).');
|
|
31
|
+
console.error('Rode "spec-first-copilot init --name=<nome>" primeiro.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`\nAtualizando projeto em ${dest}\n`);
|
|
36
|
+
|
|
37
|
+
const stats = { added: [], updated: [], skipped: 0 };
|
|
38
|
+
syncDir(templatesDir, dest, force, stats, dest);
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
if (stats.added.length > 0) {
|
|
42
|
+
console.log(`Adicionados (${stats.added.length}):`);
|
|
43
|
+
for (const f of stats.added) console.log(` + ${f}`);
|
|
44
|
+
}
|
|
45
|
+
if (stats.updated.length > 0) {
|
|
46
|
+
console.log(`Atualizados (${stats.updated.length}):`);
|
|
47
|
+
for (const f of stats.updated) console.log(` ~ ${f}`);
|
|
48
|
+
}
|
|
49
|
+
if (stats.added.length === 0 && stats.updated.length === 0) {
|
|
50
|
+
console.log('Já está atualizado — nenhum arquivo novo pra adicionar.');
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`\n${stats.added.length} adicionado(s), ${stats.updated.length} atualizado(s), ${stats.skipped} inalterado(s)`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function syncDir(src, dest, force, stats, root) {
|
|
59
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
60
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
61
|
+
const srcPath = path.join(src, entry.name);
|
|
62
|
+
const destName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
63
|
+
const destPath = path.join(dest, destName);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
syncDir(srcPath, destPath, force, stats, root);
|
|
66
|
+
} else {
|
|
67
|
+
syncFile(srcPath, destPath, force, stats, root);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function syncFile(src, dest, force, stats, root) {
|
|
73
|
+
const rel = path.relative(root, dest);
|
|
74
|
+
const exists = fs.existsSync(dest);
|
|
75
|
+
|
|
76
|
+
if (exists && !force) {
|
|
77
|
+
const isFramework = isFrameworkPath(rel);
|
|
78
|
+
if (!isFramework) {
|
|
79
|
+
stats.skipped++;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
83
|
+
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
84
|
+
if (srcContent === destContent) {
|
|
85
|
+
stats.skipped++;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
89
|
+
stats.updated.push(rel);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (exists && force) {
|
|
94
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
95
|
+
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
96
|
+
if (srcContent === destContent) {
|
|
97
|
+
stats.skipped++;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
101
|
+
stats.updated.push(rel);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
|
|
106
|
+
const ext = path.extname(src).toLowerCase();
|
|
107
|
+
const basename = path.basename(src);
|
|
108
|
+
const isText = textExtensions.includes(ext) || basename.startsWith('.');
|
|
109
|
+
|
|
110
|
+
if (isText) {
|
|
111
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
112
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
113
|
+
} else {
|
|
114
|
+
fs.copyFileSync(src, dest);
|
|
115
|
+
}
|
|
116
|
+
stats.added.push(rel);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isFrameworkPath(rel) {
|
|
120
|
+
const normalized = rel.split(path.sep).join('/');
|
|
121
|
+
for (const dir of FRAMEWORK_DIRS) {
|
|
122
|
+
const normalizedDir = dir.split(path.sep).join('/');
|
|
123
|
+
if (normalized.startsWith(normalizedDir + '/')) return true;
|
|
124
|
+
}
|
|
125
|
+
for (const file of FRAMEWORK_FILES) {
|
|
126
|
+
const normalizedFile = file.split(path.sep).join('/');
|
|
127
|
+
if (normalized === normalizedFile) return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { update };
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
# Napkin Runbook
|
|
2
|
-
|
|
3
|
-
> Sistema de memória persistente do projeto.
|
|
4
|
-
> Lido no início de cada sessão. Curado continuamente. Runbook, não log.
|
|
5
|
-
|
|
6
|
-
## Regras de Curadoria
|
|
7
|
-
- Re-priorizar a cada leitura (mais importante primeiro)
|
|
8
|
-
- Manter apenas notas recorrentes e de alto valor
|
|
9
|
-
- Máximo 10 itens por categoria
|
|
10
|
-
- Cada item inclui data + ação concreta ("Fazer:" / "Evitar:")
|
|
11
|
-
- Remover itens obsoletos ou de baixo sinal
|
|
12
|
-
- Fundir duplicatas
|
|
13
|
-
- Adaptar categorias ao projeto conforme ele evolui
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Princípios Invioláveis
|
|
18
|
-
|
|
19
|
-
1. **Spec-first**: NUNCA gerar código sem
|
|
20
|
-
Fazer: seguir pipeline extract → design → plan → dev.
|
|
21
|
-
|
|
22
|
-
2. **Entregáveis contínuos**: toda feature é faseada em entregáveis incrementais.
|
|
23
|
-
Cada fase entrega valor ao usuário e pode ir pra produção.
|
|
24
|
-
Fazer: nunca "tudo ou nada". Sempre pequeno e constante.
|
|
25
|
-
|
|
26
|
-
3. **Nunca na main**: todo código em branch própria. Merge via PR aprovado pelo usuário.
|
|
27
|
-
Fazer: seguir git workflow (5 passos no rules.md).
|
|
28
|
-
|
|
29
|
-
4. **
|
|
30
|
-
Fazer: se falta info
|
|
31
|
-
|
|
32
|
-
5. **Docker dev = infra only**: docker-compose.yml tem APENAS dependências (banco, redis, rabbit).
|
|
33
|
-
Apps (API, Worker, Web) rodam direto: `dotnet run`, `npm run dev`.
|
|
34
|
-
Evitar: NUNCA colocar apps no docker-compose.yml de dev.
|
|
35
|
-
|
|
36
|
-
## Padrões de Execução
|
|
37
|
-
|
|
38
|
-
1. **Projeto-base = orquestrador, projetos/ = código**
|
|
39
|
-
Specs, docs, tasks, progresso ficam aqui. Código fica nos repos em projetos/.
|
|
40
|
-
Commits de código nos repos do serviço, NUNCA no projeto-base.
|
|
41
|
-
|
|
42
|
-
2. **Auth por endpoint é obrigatória**
|
|
43
|
-
Todo endpoint deve ter Autenticação e Autorização definidos no
|
|
44
|
-
Se
|
|
45
|
-
|
|
46
|
-
3. **Temas críticos geram ambiguidades automáticas**
|
|
47
|
-
Se os insumos não mencionam: auth, authz, separação de serviços, ambientes,
|
|
48
|
-
dados sensíveis → o /extract gera ambiguidades obrigatórias.
|
|
49
|
-
|
|
50
|
-
## Armadilhas do Ambiente
|
|
51
|
-
|
|
52
|
-
<!-- Adicionar conforme descoberto durante o projeto -->
|
|
53
|
-
|
|
54
|
-
## Decisões de Design
|
|
55
|
-
|
|
56
|
-
<!-- Populado pelo /design e /dev conforme o projeto evolui -->
|
|
57
|
-
|
|
58
|
-
## Regras de Negócio Aprendidas
|
|
59
|
-
|
|
60
|
-
<!-- Populado pelo /extract e feedback do usuário -->
|
|
61
|
-
|
|
62
|
-
## Preferências do Usuário
|
|
63
|
-
|
|
64
|
-
<!-- Populado conforme interações com o usuário -->
|
|
65
|
-
|
|
66
|
-
## Sessão Atual
|
|
67
|
-
|
|
68
|
-
<!-- Atualizado pelo /session-finish ao encerrar cada sessão -->
|
|
1
|
+
# Napkin Runbook
|
|
2
|
+
|
|
3
|
+
> Sistema de memória persistente do projeto.
|
|
4
|
+
> Lido no início de cada sessão. Curado continuamente. Runbook, não log.
|
|
5
|
+
|
|
6
|
+
## Regras de Curadoria
|
|
7
|
+
- Re-priorizar a cada leitura (mais importante primeiro)
|
|
8
|
+
- Manter apenas notas recorrentes e de alto valor
|
|
9
|
+
- Máximo 10 itens por categoria
|
|
10
|
+
- Cada item inclui data + ação concreta ("Fazer:" / "Evitar:")
|
|
11
|
+
- Remover itens obsoletos ou de baixo sinal
|
|
12
|
+
- Fundir duplicatas
|
|
13
|
+
- Adaptar categorias ao projeto conforme ele evolui
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Princípios Invioláveis
|
|
18
|
+
|
|
19
|
+
1. **Spec-first**: NUNCA gerar código sem PRD e/ou TRD aprovados (+ `specs/{nome}/` gerado)
|
|
20
|
+
Fazer: seguir pipeline discovery → extract → design → plan → dev.
|
|
21
|
+
|
|
22
|
+
2. **Entregáveis contínuos**: toda feature é faseada em entregáveis incrementais.
|
|
23
|
+
Cada fase entrega valor ao usuário e pode ir pra produção.
|
|
24
|
+
Fazer: nunca "tudo ou nada". Sempre pequeno e constante.
|
|
25
|
+
|
|
26
|
+
3. **Nunca na main**: todo código em branch própria. Merge via PR aprovado pelo usuário.
|
|
27
|
+
Fazer: seguir git workflow (5 passos no rules.md).
|
|
28
|
+
|
|
29
|
+
4. **specs/ é auto-contido**: o coder lê APENAS `specs/{nome}/` + task. Nunca PRD/TRD ou PM direto.
|
|
30
|
+
Fazer: se falta info em `specs/`, parar e reportar.
|
|
31
|
+
|
|
32
|
+
5. **Docker dev = infra only**: docker-compose.yml tem APENAS dependências (banco, redis, rabbit).
|
|
33
|
+
Apps (API, Worker, Web) rodam direto: `dotnet run`, `npm run dev`.
|
|
34
|
+
Evitar: NUNCA colocar apps no docker-compose.yml de dev.
|
|
35
|
+
|
|
36
|
+
## Padrões de Execução
|
|
37
|
+
|
|
38
|
+
1. **Projeto-base = orquestrador, projetos/ = código**
|
|
39
|
+
Specs, docs, tasks, progresso ficam aqui. Código fica nos repos em projetos/.
|
|
40
|
+
Commits de código nos repos do serviço, NUNCA no projeto-base.
|
|
41
|
+
|
|
42
|
+
2. **Auth por endpoint é obrigatória**
|
|
43
|
+
Todo endpoint deve ter Autenticação e Autorização definidos no TRD §2.1 (Backend) + §7 (Segurança).
|
|
44
|
+
Se TRD omitiu → PARAR e reportar. Se público → escrever "público" explicitamente.
|
|
45
|
+
|
|
46
|
+
3. **Temas críticos geram ambiguidades automáticas**
|
|
47
|
+
Se os insumos não mencionam: auth, authz, separação de serviços, ambientes,
|
|
48
|
+
dados sensíveis → o /extract gera ambiguidades obrigatórias.
|
|
49
|
+
|
|
50
|
+
## Armadilhas do Ambiente
|
|
51
|
+
|
|
52
|
+
<!-- Adicionar conforme descoberto durante o projeto -->
|
|
53
|
+
|
|
54
|
+
## Decisões de Design
|
|
55
|
+
|
|
56
|
+
<!-- Populado pelo /design e /dev conforme o projeto evolui -->
|
|
57
|
+
|
|
58
|
+
## Regras de Negócio Aprendidas
|
|
59
|
+
|
|
60
|
+
<!-- Populado pelo /extract e feedback do usuário -->
|
|
61
|
+
|
|
62
|
+
## Preferências do Usuário
|
|
63
|
+
|
|
64
|
+
<!-- Populado conforme interações com o usuário -->
|
|
65
|
+
|
|
66
|
+
## Sessão Atual
|
|
67
|
+
|
|
68
|
+
<!-- Atualizado pelo /session-finish ao encerrar cada sessão -->
|