grimoire-framework 1.6.0 → 1.7.1
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/.grimoire/install-manifest.yaml +2 -2
- package/bin/commands/analyze.js +242 -0
- package/bin/commands/context.js +116 -2
- package/bin/commands/dashboard.js +358 -0
- package/bin/commands/story.js +53 -13
- package/bin/grimoire-cli.js +12 -1
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
# - SHA256 hashes for change detection
|
|
8
8
|
# - File types for categorization
|
|
9
9
|
#
|
|
10
|
-
version: 1.
|
|
11
|
-
generated_at: "2026-02-
|
|
10
|
+
version: 1.7.1
|
|
11
|
+
generated_at: "2026-02-22T18:21:15.311Z"
|
|
12
12
|
generator: scripts/generate-install-manifest.js
|
|
13
13
|
file_count: 1011
|
|
14
14
|
files:
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grimoire analyze — Project Stack Analyzer
|
|
3
|
+
*
|
|
4
|
+
* 🎨 Da Vinci: detecta stack, frameworks, estrutura e sugere atividades
|
|
5
|
+
*
|
|
6
|
+
* grimoire analyze Analisa pasta atual e sugere atividades
|
|
7
|
+
* grimoire analyze --save Salva análise como memórias no RAG
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
function findGrimoireDir() {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
if (fs.existsSync(path.join(cwd, '.grimoire'))) return path.join(cwd, '.grimoire');
|
|
18
|
+
if (fs.existsSync(path.join(cwd, 'grimoire', '.grimoire'))) return path.join(cwd, 'grimoire', '.grimoire');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Detectors ─────────────────────────────────────────────────────────────────
|
|
23
|
+
function detectFromPackageJson(cwd) {
|
|
24
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
25
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
26
|
+
|
|
27
|
+
let pkg;
|
|
28
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch (_) { return null; }
|
|
29
|
+
|
|
30
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
31
|
+
const result = { name: pkg.name, version: pkg.version, runtime: 'Node.js', frameworks: [], tools: [], suggestions: [] };
|
|
32
|
+
|
|
33
|
+
// Frameworks
|
|
34
|
+
if (deps['next']) result.frameworks.push('Next.js');
|
|
35
|
+
if (deps['nuxt']) result.frameworks.push('Nuxt.js');
|
|
36
|
+
if (deps['react']) result.frameworks.push('React');
|
|
37
|
+
if (deps['vue']) result.frameworks.push('Vue');
|
|
38
|
+
if (deps['@angular/core']) result.frameworks.push('Angular');
|
|
39
|
+
if (deps['svelte']) result.frameworks.push('Svelte');
|
|
40
|
+
if (deps['express']) result.frameworks.push('Express');
|
|
41
|
+
if (deps['fastify']) result.frameworks.push('Fastify');
|
|
42
|
+
if (deps['@nestjs/core']) result.frameworks.push('NestJS');
|
|
43
|
+
if (deps['hono']) result.frameworks.push('Hono');
|
|
44
|
+
|
|
45
|
+
// Databases
|
|
46
|
+
if (deps['prisma'] || deps['@prisma/client']) result.tools.push('Prisma ORM');
|
|
47
|
+
if (deps['mongoose']) result.tools.push('MongoDB/Mongoose');
|
|
48
|
+
if (deps['sequelize']) result.tools.push('Sequelize');
|
|
49
|
+
if (deps['drizzle-orm']) result.tools.push('Drizzle ORM');
|
|
50
|
+
if (deps['pg']) result.tools.push('PostgreSQL');
|
|
51
|
+
if (deps['mysql2']) result.tools.push('MySQL');
|
|
52
|
+
if (deps['sqlite3'] || deps['better-sqlite3']) result.tools.push('SQLite');
|
|
53
|
+
if (deps['redis'] || deps['ioredis']) result.tools.push('Redis');
|
|
54
|
+
|
|
55
|
+
// Auth
|
|
56
|
+
if (deps['jsonwebtoken'] || deps['jose']) result.tools.push('JWT Auth');
|
|
57
|
+
if (deps['passport']) result.tools.push('Passport.js');
|
|
58
|
+
if (deps['next-auth']) result.tools.push('NextAuth');
|
|
59
|
+
if (deps['clerk']) result.tools.push('Clerk');
|
|
60
|
+
|
|
61
|
+
// Testing
|
|
62
|
+
if (deps['jest']) result.tools.push('Jest');
|
|
63
|
+
if (deps['vitest']) result.tools.push('Vitest');
|
|
64
|
+
if (deps['cypress']) result.tools.push('Cypress E2E');
|
|
65
|
+
if (deps['playwright']) result.tools.push('Playwright E2E');
|
|
66
|
+
|
|
67
|
+
// TypeScript
|
|
68
|
+
if (deps['typescript'] || pkg.scripts?.['typecheck']) result.tools.push('TypeScript');
|
|
69
|
+
|
|
70
|
+
// Hosting/Deploy
|
|
71
|
+
if (fs.existsSync(path.join(cwd, 'vercel.json'))) result.tools.push('Vercel');
|
|
72
|
+
if (fs.existsSync(path.join(cwd, 'railway.json'))) result.tools.push('Railway');
|
|
73
|
+
if (fs.existsSync(path.join(cwd, '.fly.toml'))) result.tools.push('Fly.io');
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectFromFiles(cwd) {
|
|
79
|
+
const result = { runtime: null, frameworks: [], tools: [] };
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(path.join(cwd, 'requirements.txt')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) {
|
|
82
|
+
result.runtime = 'Python';
|
|
83
|
+
try {
|
|
84
|
+
const req = fs.readFileSync(path.join(cwd, 'requirements.txt'), 'utf8');
|
|
85
|
+
if (req.includes('fastapi')) result.frameworks.push('FastAPI');
|
|
86
|
+
if (req.includes('django')) result.frameworks.push('Django');
|
|
87
|
+
if (req.includes('flask')) result.frameworks.push('Flask');
|
|
88
|
+
} catch (_) { }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) result.runtime = 'Rust';
|
|
92
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) result.runtime = 'Go';
|
|
93
|
+
if (fs.existsSync(path.join(cwd, 'pom.xml'))) result.runtime = 'Java/Maven';
|
|
94
|
+
if (fs.existsSync(path.join(cwd, 'build.gradle'))) result.runtime = 'Java/Gradle';
|
|
95
|
+
|
|
96
|
+
if (fs.existsSync(path.join(cwd, 'docker-compose.yml'))) result.tools.push('Docker Compose');
|
|
97
|
+
if (fs.existsSync(path.join(cwd, 'Dockerfile'))) result.tools.push('Docker');
|
|
98
|
+
if (fs.existsSync(path.join(cwd, '.github', 'workflows'))) result.tools.push('GitHub Actions CI/CD');
|
|
99
|
+
if (fs.existsSync(path.join(cwd, 'terraform'))) result.tools.push('Terraform');
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countFiles(cwd) {
|
|
105
|
+
const counts = {};
|
|
106
|
+
function walk(dir, depth = 0) {
|
|
107
|
+
if (depth > 3) return;
|
|
108
|
+
try {
|
|
109
|
+
for (const f of fs.readdirSync(dir)) {
|
|
110
|
+
if (f.startsWith('.') || f === 'node_modules' || f === '.git') continue;
|
|
111
|
+
const full = path.join(dir, f);
|
|
112
|
+
const stat = fs.statSync(full);
|
|
113
|
+
if (stat.isDirectory()) { walk(full, depth + 1); }
|
|
114
|
+
else {
|
|
115
|
+
const ext = path.extname(f).toLowerCase();
|
|
116
|
+
if (ext) counts[ext] = (counts[ext] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (_) { }
|
|
120
|
+
}
|
|
121
|
+
walk(cwd);
|
|
122
|
+
return counts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generateSuggestions(pkg, files, extra) {
|
|
126
|
+
const suggestions = [];
|
|
127
|
+
|
|
128
|
+
const testFiles = (files['.test.js'] || 0) + (files['.spec.js'] || 0) + (files['.test.ts'] || 0);
|
|
129
|
+
if (testFiles === 0) suggestions.push({ title: 'Configurar cobertura de testes', priority: 'alta' });
|
|
130
|
+
|
|
131
|
+
if (!extra.has_ci) suggestions.push({ title: 'Configurar CI/CD (GitHub Actions)', priority: 'média' });
|
|
132
|
+
if (!extra.has_readme) suggestions.push({ title: 'Criar/melhorar README.md', priority: 'média' });
|
|
133
|
+
if (!extra.has_env_example) suggestions.push({ title: 'Criar .env.example', priority: 'baixa' });
|
|
134
|
+
if (!extra.has_docker) suggestions.push({ title: 'Containerizar com Docker', priority: 'baixa' });
|
|
135
|
+
|
|
136
|
+
if (pkg?.frameworks?.includes('Next.js') || pkg?.frameworks?.includes('React')) {
|
|
137
|
+
suggestions.push({ title: 'Auditoria de acessibilidade (a11y)', priority: 'média' });
|
|
138
|
+
suggestions.push({ title: 'Implementar SEO/meta tags', priority: 'baixa' });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (pkg?.tools?.includes('PostgreSQL') || pkg?.tools?.includes('Prisma ORM')) {
|
|
142
|
+
suggestions.push({ title: 'Implementar connection pooling', priority: 'média' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return suggestions.slice(0, 6);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── run ───────────────────────────────────────────────────────────────────────
|
|
149
|
+
async function run(args) {
|
|
150
|
+
const save = args.includes('--save');
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
const sep = '─'.repeat(52);
|
|
153
|
+
|
|
154
|
+
console.log(`\n🔍 Grimoire Analyze — ${path.basename(cwd)}\n${sep}\n`);
|
|
155
|
+
console.log(' Analisando estrutura do projeto...\n');
|
|
156
|
+
|
|
157
|
+
// Detect
|
|
158
|
+
const pkg = detectFromPackageJson(cwd);
|
|
159
|
+
const extra = detectFromFiles(cwd);
|
|
160
|
+
const files = countFiles(cwd);
|
|
161
|
+
|
|
162
|
+
const runtime = pkg?.runtime || extra.runtime || 'Desconhecido';
|
|
163
|
+
const frameworks = [...(pkg?.frameworks || []), ...(extra.frameworks || [])];
|
|
164
|
+
const tools = [...(pkg?.tools || []), ...(extra.tools || [])];
|
|
165
|
+
|
|
166
|
+
const extraFlags = {
|
|
167
|
+
has_ci: fs.existsSync(path.join(cwd, '.github', 'workflows')),
|
|
168
|
+
has_readme: fs.existsSync(path.join(cwd, 'README.md')),
|
|
169
|
+
has_env_example: fs.existsSync(path.join(cwd, '.env.example')),
|
|
170
|
+
has_docker: fs.existsSync(path.join(cwd, 'Dockerfile')) || fs.existsSync(path.join(cwd, 'docker-compose.yml')),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Output
|
|
174
|
+
if (pkg?.name) console.log(` 📦 Projeto: ${pkg.name}${pkg.version ? ` v${pkg.version}` : ''}`);
|
|
175
|
+
console.log(` ⚙️ Runtime: ${runtime}`);
|
|
176
|
+
if (frameworks.length) console.log(` 🏗️ Frameworks: ${frameworks.join(', ')}`);
|
|
177
|
+
if (tools.length) console.log(` 🔧 Tools: ${tools.join(', ')}`);
|
|
178
|
+
|
|
179
|
+
// File counts
|
|
180
|
+
const topExts = Object.entries(files).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
181
|
+
if (topExts.length) {
|
|
182
|
+
console.log(`\n 📁 Arquivos: ${topExts.map(([e, n]) => `${n}×${e}`).join(' ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check flags
|
|
186
|
+
console.log('\n 📋 Checklist:');
|
|
187
|
+
console.log(` ${extraFlags.has_readme ? '✅' : '❌'} README.md`);
|
|
188
|
+
console.log(` ${extraFlags.has_env_example ? '✅' : '❌'} .env.example`);
|
|
189
|
+
console.log(` ${extraFlags.has_ci ? '✅' : '❌'} CI/CD configurado`);
|
|
190
|
+
console.log(` ${extraFlags.has_docker ? '✅' : '❌'} Docker`);
|
|
191
|
+
|
|
192
|
+
// Suggestions
|
|
193
|
+
const suggestions = generateSuggestions(pkg, files, extraFlags);
|
|
194
|
+
if (suggestions.length) {
|
|
195
|
+
console.log('\n 💡 Atividades sugeridas para o Backlog:\n');
|
|
196
|
+
suggestions.forEach((s, i) =>
|
|
197
|
+
console.log(` ${i + 1}. [${s.priority.toUpperCase()}] ${s.title}`)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Save to grimoire
|
|
202
|
+
if (save || suggestions.length > 0) {
|
|
203
|
+
const grimoireDir = findGrimoireDir();
|
|
204
|
+
if (grimoireDir) {
|
|
205
|
+
// Save as memory
|
|
206
|
+
if (save) {
|
|
207
|
+
const { run: memRun } = require('./memory');
|
|
208
|
+
const stackSummary = `Stack: ${runtime}${frameworks.length ? ' + ' + frameworks.join(' + ') : ''}${tools.length ? ' · ' + tools.slice(0, 3).join(', ') : ''}`;
|
|
209
|
+
await memRun(['save', '--tag', 'stack', stackSummary]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create suggested stories
|
|
213
|
+
if (suggestions.length > 0) {
|
|
214
|
+
console.log('\n Criar atividades sugeridas no Backlog? [S/N] ');
|
|
215
|
+
if (process.stdin.isTTY) {
|
|
216
|
+
const readline = require('readline');
|
|
217
|
+
readline.emitKeypressEvents(process.stdin);
|
|
218
|
+
process.stdin.setRawMode?.(true);
|
|
219
|
+
await new Promise(resolve => {
|
|
220
|
+
process.stdin.once('keypress', (str) => {
|
|
221
|
+
process.stdin.setRawMode?.(false);
|
|
222
|
+
if ((str || '').toLowerCase() === 's') {
|
|
223
|
+
console.log('\n Criando...');
|
|
224
|
+
const { run: storyRun } = require('./story');
|
|
225
|
+
suggestions.forEach(s => storyRun(['create', s.title, '--backlog']));
|
|
226
|
+
console.log(` ✅ ${suggestions.length} atividades criadas no Backlog\n`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(' Pulado.\n');
|
|
229
|
+
}
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`\n${sep}`);
|
|
239
|
+
console.log(' grimoire dashboard ← ver kanban\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { run };
|
package/bin/commands/context.js
CHANGED
|
@@ -227,6 +227,117 @@ function regEscape(s) {
|
|
|
227
227
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ── Live Status block ──────────────────────────────────────────────────
|
|
231
|
+
const STATUS_START = '<!-- grimoire-MANAGED-START: live-status -->';
|
|
232
|
+
const STATUS_END = '<!-- grimoire-MANAGED-END: live-status -->';
|
|
233
|
+
|
|
234
|
+
function loadActiveStories(grimoireDir) {
|
|
235
|
+
const storiesDir = path.join(grimoireDir, 'stories');
|
|
236
|
+
if (!fs.existsSync(storiesDir)) return { active: [], backlog: [] };
|
|
237
|
+
const stories = fs.readdirSync(storiesDir)
|
|
238
|
+
.filter(f => f.endsWith('.json'))
|
|
239
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); } catch (_) { return null; } })
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
return {
|
|
242
|
+
active: stories.filter(s => s.status === 'in_progress'),
|
|
243
|
+
backlog: stories.filter(s => s.status === 'backlog'),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function timeSpentLabel(story) {
|
|
248
|
+
if (!story.startedAt) return '';
|
|
249
|
+
const ms = new Date() - new Date(story.startedAt);
|
|
250
|
+
const h = Math.floor(ms / 3600000);
|
|
251
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
252
|
+
return ` ⏱ ${h}h${m}min`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function loadRagStatus(grimoireDir) {
|
|
256
|
+
const cfgF = path.join(grimoireDir, 'config.yaml');
|
|
257
|
+
if (!fs.existsSync(cfgF)) return null;
|
|
258
|
+
const lines = fs.readFileSync(cfgF, 'utf8').split('\n');
|
|
259
|
+
const get = (k) => { const l = lines.find(l => l.startsWith(k + ':')); return l ? l.split(':')[1]?.trim() : null; };
|
|
260
|
+
return get('embedding_provider') || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildLiveStatusBlock(grimoireDir, entries) {
|
|
264
|
+
const now = new Date();
|
|
265
|
+
const dateStr = now.toISOString().replace('T', ' ').slice(0, 16);
|
|
266
|
+
const { active, backlog } = loadActiveStories(grimoireDir);
|
|
267
|
+
const ragProvider = loadRagStatus(grimoireDir);
|
|
268
|
+
|
|
269
|
+
// Count memories
|
|
270
|
+
const todayKey = now.toISOString().split('T')[0];
|
|
271
|
+
const todayCount = entries.filter(e => e.date === todayKey).length;
|
|
272
|
+
const totalCount = entries.length;
|
|
273
|
+
|
|
274
|
+
// Detect sprint from stories or memory tags
|
|
275
|
+
const sprintMem = entries.filter(e => /sprint/i.test(e.content)).slice(-1)[0];
|
|
276
|
+
|
|
277
|
+
const block = [STATUS_START];
|
|
278
|
+
block.push('');
|
|
279
|
+
block.push('## 🟢 Grimoire Ativo');
|
|
280
|
+
block.push('');
|
|
281
|
+
block.push('> Este bloco é auto-gerado pelo `grimoire context update` e confirma que o Grimoire Framework está ativo neste projeto.');
|
|
282
|
+
block.push('');
|
|
283
|
+
block.push('```');
|
|
284
|
+
block.push(`Data: ${dateStr}`);
|
|
285
|
+
|
|
286
|
+
if (active.length) {
|
|
287
|
+
block.push(`\nAtividades em andamento (${active.length}):`);
|
|
288
|
+
active.forEach(s => block.push(` [${s.id}] ${s.title}${timeSpentLabel(s)}`));
|
|
289
|
+
} else {
|
|
290
|
+
block.push(`\nAtividades: nenhuma em andamento`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (backlog.length) {
|
|
294
|
+
block.push(`Backlog: ${backlog.length} atividade(s) aguardando`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const ragLabel = ragProvider && ragProvider !== 'tfidf'
|
|
298
|
+
? `ativo (${ragProvider})`
|
|
299
|
+
: 'tfidf (sem embedding — rode grimoire setup rag)';
|
|
300
|
+
block.push(`\nMemória: ${totalCount} total · ${todayCount} hoje`);
|
|
301
|
+
block.push(`RAG: ${ragLabel}`);
|
|
302
|
+
block.push(`\nComandos úteis:`);
|
|
303
|
+
block.push(` grimoire → dashboard kanban`);
|
|
304
|
+
block.push(` grimoire memory search "query" → busca semântica`);
|
|
305
|
+
block.push(` grimoire context update → atualizar este bloco`);
|
|
306
|
+
block.push(` *status → verificar estado no chat`);
|
|
307
|
+
block.push('```');
|
|
308
|
+
block.push('');
|
|
309
|
+
block.push(STATUS_END);
|
|
310
|
+
return block.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function injectLiveStatus(grimoireDir, entries) {
|
|
314
|
+
const cwd = process.cwd();
|
|
315
|
+
const candidates = [
|
|
316
|
+
path.join(cwd, 'GEMINI.md'),
|
|
317
|
+
path.join(path.dirname(grimoireDir), 'GEMINI.md'),
|
|
318
|
+
];
|
|
319
|
+
const geminiPath = candidates.find(p => fs.existsSync(p));
|
|
320
|
+
if (!geminiPath) return false;
|
|
321
|
+
|
|
322
|
+
const original = fs.readFileSync(geminiPath, 'utf8');
|
|
323
|
+
const newBlock = buildLiveStatusBlock(grimoireDir, entries);
|
|
324
|
+
|
|
325
|
+
let updated;
|
|
326
|
+
if (original.includes(STATUS_START)) {
|
|
327
|
+
const re = new RegExp(`${regEscape(STATUS_START)}[\\s\\S]*?${regEscape(STATUS_END)}`, 'g');
|
|
328
|
+
updated = original.replace(re, newBlock);
|
|
329
|
+
} else {
|
|
330
|
+
// Insert at the very end (after all managed blocks)
|
|
331
|
+
updated = original.trimEnd() + '\n\n' + newBlock + '\n';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (updated !== original) {
|
|
335
|
+
fs.writeFileSync(geminiPath, updated, 'utf8');
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
230
341
|
// ── Core update logic ─────────────────────────────────────────────────────────
|
|
231
342
|
function doUpdate(grimoireDir, silent = false) {
|
|
232
343
|
const contextDir = getContextDir(grimoireDir);
|
|
@@ -255,16 +366,19 @@ function doUpdate(grimoireDir, silent = false) {
|
|
|
255
366
|
fs.writeFileSync(path.join(contextDir, 'pinned.md'), pinnedLines.join('\n'), 'utf8');
|
|
256
367
|
}
|
|
257
368
|
|
|
258
|
-
// Inject GEMINI.md
|
|
369
|
+
// Inject GEMINI.md memory-context block
|
|
259
370
|
const injected = injectIntoGeminiMd(grimoireDir, entries);
|
|
260
371
|
|
|
372
|
+
// Inject live-status block (separate, always updated)
|
|
373
|
+
const statusInjected = injectLiveStatus(grimoireDir, entries);
|
|
374
|
+
|
|
261
375
|
if (!silent) {
|
|
262
376
|
console.log('\n✅ Contexto atualizado!\n');
|
|
263
377
|
console.log(` 📄 .grimoire/context/CONTEXT.md`);
|
|
264
378
|
if (dec) console.log(` ✅ .grimoire/context/decisions.md`);
|
|
265
379
|
if (pat) console.log(` 🔧 .grimoire/context/patterns.md`);
|
|
266
380
|
if (pinned.length) console.log(` 📌 .grimoire/context/pinned.md`);
|
|
267
|
-
if (injected) console.log(`
|
|
381
|
+
if (injected || statusInjected) console.log(` 🟢 GEMINI.md — bloco live-status + memory-context atualizados`);
|
|
268
382
|
console.log(`\n ${entries.length} entradas indexadas`);
|
|
269
383
|
console.log(` grimoire context show ← ver o resultado\n`);
|
|
270
384
|
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grimoire dashboard — Kanban TUI
|
|
3
|
+
*
|
|
4
|
+
* 🎭 Design: Matisse (UX) · 🎨 Da Vinci (Dev) · 🏛️ Gaudí (Arquitetura)
|
|
5
|
+
*
|
|
6
|
+
* Exibe kanban com colunas: BACKLOG | EM ANDAMENTO | CONCLUÍDO
|
|
7
|
+
* Com wizard se nenhum projeto for detectado.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
|
|
17
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
18
|
+
function findGrimoireDir() {
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
if (fs.existsSync(path.join(cwd, '.grimoire'))) return path.join(cwd, '.grimoire');
|
|
21
|
+
if (fs.existsSync(path.join(cwd, 'grimoire', '.grimoire'))) return path.join(cwd, 'grimoire', '.grimoire');
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProjectName(grimoireDir) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = path.join(path.dirname(grimoireDir), 'package.json');
|
|
28
|
+
if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf8')).name || 'Projeto';
|
|
29
|
+
} catch (_) { }
|
|
30
|
+
return path.basename(path.dirname(grimoireDir));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadConfig(grimoireDir) {
|
|
34
|
+
const f = path.join(grimoireDir, 'config.yaml');
|
|
35
|
+
if (!fs.existsSync(f)) return {};
|
|
36
|
+
const cfg = {};
|
|
37
|
+
fs.readFileSync(f, 'utf8').split('\n').forEach(l => {
|
|
38
|
+
const m = l.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
|
|
39
|
+
if (m) cfg[m[1].trim()] = m[2].trim();
|
|
40
|
+
});
|
|
41
|
+
return cfg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadStories(grimoireDir) {
|
|
45
|
+
const storiesDir = path.join(grimoireDir, 'stories');
|
|
46
|
+
if (!fs.existsSync(storiesDir)) return [];
|
|
47
|
+
return fs.readdirSync(storiesDir)
|
|
48
|
+
.filter(f => f.endsWith('.json'))
|
|
49
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); } catch (_) { return null; } })
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Time helpers ──────────────────────────────────────────────────────────────
|
|
55
|
+
function fmtDate(iso) {
|
|
56
|
+
if (!iso) return '—';
|
|
57
|
+
return iso.slice(0, 10).split('-').slice(1).join('/'); // MM/DD
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function timeSpent(story) {
|
|
61
|
+
if (!story.startedAt) return null;
|
|
62
|
+
const end = story.completedAt ? new Date(story.completedAt) : new Date();
|
|
63
|
+
const ms = end - new Date(story.startedAt);
|
|
64
|
+
const h = Math.floor(ms / 3600000);
|
|
65
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
66
|
+
return `${h}h ${m}min`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── RAG status (quick check) ──────────────────────────────────────────────────
|
|
70
|
+
function ragStatus(config) {
|
|
71
|
+
const p = config.embedding_provider;
|
|
72
|
+
if (!p || p === 'tfidf') return null;
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Today's memory count ──────────────────────────────────────────────────────
|
|
77
|
+
function todayMemCount(grimoireDir) {
|
|
78
|
+
const today = new Date().toISOString().split('T')[0];
|
|
79
|
+
const f = path.join(grimoireDir, 'memory', 'sessions', `${today}.jsonl`);
|
|
80
|
+
if (!fs.existsSync(f)) return 0;
|
|
81
|
+
return fs.readFileSync(f, 'utf8').split('\n').filter(l => l.trim()).length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Terminal width ────────────────────────────────────────────────────────────
|
|
85
|
+
function termWidth() {
|
|
86
|
+
return Math.max(process.stdout.columns || 80, 80);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Box-drawing helpers ───────────────────────────────────────────────────────
|
|
90
|
+
const C = {
|
|
91
|
+
tl: '╔', tr: '╗', bl: '╚', br: '╝',
|
|
92
|
+
h: '═', v: '║',
|
|
93
|
+
ml: '╠', mr: '╣', mt: '╦', mb: '╩', mc: '╬',
|
|
94
|
+
sep: '─', lsep: '├', rsep: '┤',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function pad(str, len, char = ' ') {
|
|
98
|
+
const visible = stripAnsi(str).length;
|
|
99
|
+
const diff = len - visible;
|
|
100
|
+
return diff > 0 ? str + char.repeat(diff) : str;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stripAnsi(str) {
|
|
104
|
+
return String(str).replace(/\x1b\[[0-9;]*m/g, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function truncate(str, len) {
|
|
108
|
+
const clean = stripAnsi(str);
|
|
109
|
+
if (clean.length <= len) return str;
|
|
110
|
+
return clean.slice(0, len - 1) + '…';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
114
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
115
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
116
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
117
|
+
const yellow = s => `\x1b[33m${s}\x1b[0m`;
|
|
118
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
119
|
+
const red = s => `\x1b[31m${s}\x1b[0m`;
|
|
120
|
+
const magenta = s => `\x1b[35m${s}\x1b[0m`;
|
|
121
|
+
|
|
122
|
+
// ── Status icons ──────────────────────────────────────────────────────────────
|
|
123
|
+
function statusIcon(status) {
|
|
124
|
+
if (status === 'done') return green('✓');
|
|
125
|
+
if (status === 'in_progress') return yellow('●');
|
|
126
|
+
return dim('○');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Render single card ────────────────────────────────────────────────────────
|
|
130
|
+
function renderCard(story, colW) {
|
|
131
|
+
const inner = colW - 2; // minus borders
|
|
132
|
+
const lines = [];
|
|
133
|
+
|
|
134
|
+
// ID + icon
|
|
135
|
+
const idStr = bold(cyan(truncate(story.id, 8)));
|
|
136
|
+
const icon = statusIcon(story.status);
|
|
137
|
+
lines.push(` ${icon} ${idStr}`);
|
|
138
|
+
|
|
139
|
+
// Title
|
|
140
|
+
lines.push(` ${truncate(story.title, inner - 1)}`);
|
|
141
|
+
|
|
142
|
+
// Dates
|
|
143
|
+
if (story.status === 'backlog') {
|
|
144
|
+
const dead = story.deliveryDate ? `Entrega: ${fmtDate(story.deliveryDate)}` : dim('Sem prazo');
|
|
145
|
+
lines.push(` ${dim(truncate(dead, inner - 1))}`);
|
|
146
|
+
} else if (story.status === 'in_progress') {
|
|
147
|
+
const start = `Início: ${fmtDate(story.startedAt || story.createdAt)}`;
|
|
148
|
+
const dead = story.deliveryDate ? `Entrega: ${fmtDate(story.deliveryDate)}` : '';
|
|
149
|
+
lines.push(` ${dim(truncate(start, inner - 1))}`);
|
|
150
|
+
if (dead) lines.push(` ${dim(truncate(dead, inner - 1))}`);
|
|
151
|
+
const spent = timeSpent(story);
|
|
152
|
+
if (spent) lines.push(` ${dim('⏱')} ${dim(spent)}`);
|
|
153
|
+
} else {
|
|
154
|
+
// done
|
|
155
|
+
const done = story.completedAt ? `✓ ${fmtDate(story.completedAt)}` : '';
|
|
156
|
+
const spent = timeSpent(story);
|
|
157
|
+
if (done) lines.push(` ${green(truncate(done, inner - 1))}`);
|
|
158
|
+
if (spent) lines.push(` ${dim('⏱')} ${dim(spent)}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Render full kanban ────────────────────────────────────────────────────────
|
|
165
|
+
function renderKanban(stories, grimoireDir, config) {
|
|
166
|
+
const W = termWidth();
|
|
167
|
+
const colW = Math.floor((W - 4) / 3); // 3 cols, 4 borders
|
|
168
|
+
|
|
169
|
+
const backlog = stories.filter(s => s.status === 'backlog');
|
|
170
|
+
const inProgress = stories.filter(s => s.status === 'in_progress');
|
|
171
|
+
const done = stories.filter(s => s.status === 'done').slice(-5); // last 5
|
|
172
|
+
|
|
173
|
+
const output = [];
|
|
174
|
+
|
|
175
|
+
// Header
|
|
176
|
+
const projectName = getProjectName(grimoireDir);
|
|
177
|
+
const today = new Date().toISOString().split('T')[0];
|
|
178
|
+
const rag = ragStatus(config);
|
|
179
|
+
const ragBadge = rag ? cyan(` 🤖 ${rag}`) : '';
|
|
180
|
+
const headerText = bold(`🔮 Grimoire · ${projectName}`) + ` ${dim(today)}${ragBadge}`;
|
|
181
|
+
const headerInner = pad(headerText, W - 2);
|
|
182
|
+
output.push(C.tl + C.h.repeat(W - 2) + C.tr);
|
|
183
|
+
output.push(C.v + headerInner + C.v);
|
|
184
|
+
|
|
185
|
+
// Column headers separator
|
|
186
|
+
output.push(C.ml + C.h.repeat(colW) + C.mt + C.h.repeat(colW) + C.mt + C.h.repeat(W - 2 - colW * 2 - 2) + C.mr);
|
|
187
|
+
|
|
188
|
+
const col1H = bold(' 📋 BACKLOG') + dim(` (${backlog.length})`);
|
|
189
|
+
const col2H = bold(' 🔄 EM ANDAMENTO') + dim(` (${inProgress.length})`);
|
|
190
|
+
const col3H = bold(' ✅ CONCLUÍDO') + dim(` (${done.length})`);
|
|
191
|
+
const colW3 = W - 2 - colW * 2 - 2;
|
|
192
|
+
output.push(C.v + pad(col1H, colW) + C.v + pad(col2H, colW) + C.v + pad(col3H, colW3) + C.v);
|
|
193
|
+
|
|
194
|
+
// Row separator
|
|
195
|
+
output.push(C.ml + C.h.repeat(colW) + C.mc + C.h.repeat(colW) + C.mc + C.h.repeat(colW3) + C.mr);
|
|
196
|
+
|
|
197
|
+
// Cards
|
|
198
|
+
const col1Cards = backlog.map(s => renderCard(s, colW));
|
|
199
|
+
const col2Cards = inProgress.map(s => renderCard(s, colW));
|
|
200
|
+
const col3Cards = done.map(s => renderCard(s, colW3 + 2));
|
|
201
|
+
|
|
202
|
+
const maxRows = Math.max(col1Cards.flat().length + col1Cards.length,
|
|
203
|
+
col2Cards.flat().length + col2Cards.length,
|
|
204
|
+
col3Cards.flat().length + col3Cards.length, 3);
|
|
205
|
+
|
|
206
|
+
let r1 = 0, r2 = 0, r3 = 0;
|
|
207
|
+
let ci1 = 0, ci2 = 0, ci3 = 0;
|
|
208
|
+
|
|
209
|
+
for (let row = 0; row < maxRows; row++) {
|
|
210
|
+
const getCell = (cards, ci, ri) => {
|
|
211
|
+
if (ci >= cards.length) return { line: '', nextCi: ci, nextRi: ri };
|
|
212
|
+
const card = cards[ci];
|
|
213
|
+
if (ri < card.length) return { line: card[ri], nextCi: ci, nextRi: ri + 1 };
|
|
214
|
+
if (ri === card.length) return { line: dim(C.sep.repeat(Math.max(0, colW - 2))), nextCi: ci + 1, nextRi: 0 };
|
|
215
|
+
return { line: '', nextCi: ci + 1, nextRi: 0 };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const { line: l1, nextCi: nc1, nextRi: nr1 } = getCell(col1Cards, ci1, r1);
|
|
219
|
+
const { line: l2, nextCi: nc2, nextRi: nr2 } = getCell(col2Cards, ci2, r2);
|
|
220
|
+
const { line: l3, nextCi: nc3, nextRi: nr3 } = getCell(col3Cards, ci3, r3);
|
|
221
|
+
|
|
222
|
+
ci1 = nc1; r1 = nr1; ci2 = nc2; r2 = nr2; ci3 = nc3; r3 = nr3;
|
|
223
|
+
|
|
224
|
+
output.push(
|
|
225
|
+
C.v + pad(l1, colW) + C.v + pad(l2, colW) + C.v + pad(l3, colW3) + C.v
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Footer separator
|
|
230
|
+
output.push(C.ml + C.h.repeat(colW) + C.mb + C.h.repeat(colW) + C.mb + C.h.repeat(colW3) + C.mr);
|
|
231
|
+
|
|
232
|
+
// Status bar
|
|
233
|
+
const memCount = todayMemCount(grimoireDir);
|
|
234
|
+
const squad = config.default_squad || '';
|
|
235
|
+
const statusBar = dim(` 🧠 ${memCount} mem hoje`) +
|
|
236
|
+
(squad ? dim(` · squad: ${squad}`) : '') +
|
|
237
|
+
` · ${dim('[N]')}ova ${dim('[S]')}ession ${dim('[Enter]')} Detalhar ${dim('[Q]')}sair`;
|
|
238
|
+
output.push(C.v + pad(statusBar, W - 2) + C.v);
|
|
239
|
+
output.push(C.bl + C.h.repeat(W - 2) + C.br);
|
|
240
|
+
|
|
241
|
+
console.log('\n' + output.join('\n'));
|
|
242
|
+
|
|
243
|
+
return { backlog, inProgress, done: stories.filter(s => s.status === 'done') };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Interactive keys ──────────────────────────────────────────────────────────
|
|
247
|
+
function waitForKey(stories, grimoireDir) {
|
|
248
|
+
if (!process.stdin.isTTY) return;
|
|
249
|
+
|
|
250
|
+
readline.emitKeypressEvents(process.stdin);
|
|
251
|
+
process.stdin.setRawMode?.(true);
|
|
252
|
+
process.stdin.resume();
|
|
253
|
+
|
|
254
|
+
process.stdin.once('keypress', (str, key) => {
|
|
255
|
+
process.stdin.setRawMode?.(false);
|
|
256
|
+
process.stdin.pause();
|
|
257
|
+
|
|
258
|
+
const k = (key?.name || str || '').toLowerCase();
|
|
259
|
+
|
|
260
|
+
if (k === 'q' || key?.ctrl && k === 'c') {
|
|
261
|
+
console.log('');
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (k === 'n') {
|
|
266
|
+
// New story — delegate to story create wizard
|
|
267
|
+
console.log('\n📝 Criando nova atividade...');
|
|
268
|
+
console.log(' grimoire story create "Título" [--deadline YYYY-MM-DD]\n');
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (k === 's') {
|
|
273
|
+
console.log('\n🚀 Iniciando sessão...');
|
|
274
|
+
try { require('./session').run([]); }
|
|
275
|
+
catch (_) { console.log(' grimoire session start\n'); process.exit(0); }
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (k === 'return' || k === 'enter') {
|
|
280
|
+
// Show current in_progress story details
|
|
281
|
+
const active = stories.find(s => s.status === 'in_progress');
|
|
282
|
+
if (active) {
|
|
283
|
+
try { require('./story').run(['show', active.id]); }
|
|
284
|
+
catch (_) { }
|
|
285
|
+
} else {
|
|
286
|
+
console.log('\n Nenhuma atividade em andamento.\n');
|
|
287
|
+
}
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Any other key — exit
|
|
292
|
+
console.log('');
|
|
293
|
+
process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── No-project wizard ─────────────────────────────────────────────────────────
|
|
298
|
+
async function showNoProjectWizard() {
|
|
299
|
+
const cwd = process.cwd();
|
|
300
|
+
const W = Math.min(termWidth(), 50);
|
|
301
|
+
|
|
302
|
+
console.log('\n' + '╔' + '═'.repeat(W - 2) + '╗');
|
|
303
|
+
console.log('║' + pad(bold(' 🔮 Grimoire Framework'), W - 2) + '║');
|
|
304
|
+
console.log('║' + pad(dim(` ${cwd}`), W - 2) + '║');
|
|
305
|
+
console.log('╠' + '═'.repeat(W - 2) + '╣');
|
|
306
|
+
console.log('║' + pad(yellow(' ⚠️ Nenhum projeto detectado.'), W - 2) + '║');
|
|
307
|
+
console.log('║' + ' '.repeat(W - 2) + '║');
|
|
308
|
+
console.log('║' + pad(' O que você quer fazer?', W - 2) + '║');
|
|
309
|
+
console.log('║' + ' '.repeat(W - 2) + '║');
|
|
310
|
+
console.log('║' + pad(' ❯ 🆕 Novo projeto [N]', W - 2) + '║');
|
|
311
|
+
console.log('║' + pad(' 🔍 Analisar pasta atual [A]', W - 2) + '║');
|
|
312
|
+
console.log('║' + pad(' ✖ Sair [Q]', W - 2) + '║');
|
|
313
|
+
console.log('╚' + '═'.repeat(W - 2) + '╝\n');
|
|
314
|
+
|
|
315
|
+
if (!process.stdin.isTTY) return;
|
|
316
|
+
|
|
317
|
+
readline.emitKeypressEvents(process.stdin);
|
|
318
|
+
process.stdin.setRawMode?.(true);
|
|
319
|
+
process.stdin.resume();
|
|
320
|
+
|
|
321
|
+
await new Promise(resolve => {
|
|
322
|
+
process.stdin.once('keypress', (str, key) => {
|
|
323
|
+
process.stdin.setRawMode?.(false);
|
|
324
|
+
process.stdin.pause();
|
|
325
|
+
const k = (key?.name || str || '').toLowerCase();
|
|
326
|
+
|
|
327
|
+
if (k === 'n') {
|
|
328
|
+
console.log('🆕 Iniciando novo projeto...');
|
|
329
|
+
console.log(' npx grimoire-framework install\n');
|
|
330
|
+
} else if (k === 'a') {
|
|
331
|
+
console.log('🔍 Analisando pasta...');
|
|
332
|
+
try { require('./analyze').run([]); }
|
|
333
|
+
catch (_) { console.log(' grimoire analyze\n'); }
|
|
334
|
+
} else {
|
|
335
|
+
console.log('');
|
|
336
|
+
}
|
|
337
|
+
resolve();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── run ───────────────────────────────────────────────────────────────────────
|
|
343
|
+
async function run(args) {
|
|
344
|
+
const grimoireDir = findGrimoireDir();
|
|
345
|
+
|
|
346
|
+
if (!grimoireDir) {
|
|
347
|
+
await showNoProjectWizard();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const config = loadConfig(grimoireDir);
|
|
352
|
+
const stories = loadStories(grimoireDir);
|
|
353
|
+
|
|
354
|
+
const { backlog } = renderKanban(stories, grimoireDir, config);
|
|
355
|
+
waitForKey(stories, grimoireDir);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { run, renderKanban };
|
package/bin/commands/story.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* grimoire story — Story Tracking CLI
|
|
3
3
|
*
|
|
4
|
-
* grimoire story create "Título
|
|
5
|
-
* grimoire story
|
|
6
|
-
* grimoire story
|
|
7
|
-
* grimoire story
|
|
8
|
-
* grimoire story
|
|
9
|
-
* grimoire story
|
|
4
|
+
* grimoire story create "Título" Cria story (status: backlog)
|
|
5
|
+
* grimoire story create "Título" --start Cria e inicia imediatamente
|
|
6
|
+
* grimoire story create "Título" --deadline 2026-03-01
|
|
7
|
+
* grimoire story start <id> Inicia story (→ in_progress)
|
|
8
|
+
* grimoire story list Lista stories ativas
|
|
9
|
+
* grimoire story list --all Lista todas (inclui concluídas)
|
|
10
|
+
* grimoire story done <id> Marca como concluída
|
|
11
|
+
* grimoire story show <id> Detalhes + tempo gasto
|
|
12
|
+
* grimoire story delete <id> Remove story
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
'use strict';
|
|
@@ -60,6 +63,7 @@ function run(args) {
|
|
|
60
63
|
|
|
61
64
|
switch (sub) {
|
|
62
65
|
case 'create': storyCreate(rest, storiesDir, grimoireDir); break;
|
|
66
|
+
case 'start': storyStart(rest[0], storiesDir, grimoireDir); break;
|
|
63
67
|
case 'done': storyDone(rest[0], storiesDir, grimoireDir); break;
|
|
64
68
|
case 'note': storyNote(rest[0], rest.slice(1), storiesDir); break;
|
|
65
69
|
case 'delete':
|
|
@@ -74,20 +78,36 @@ function run(args) {
|
|
|
74
78
|
function storyCreate(args, storiesDir, grimoireDir) {
|
|
75
79
|
const title = args.filter(a => !a.startsWith('--')).join(' ');
|
|
76
80
|
if (!title) {
|
|
77
|
-
console.log('Usage: grimoire story create "Título
|
|
81
|
+
console.log('Usage: grimoire story create "Título" [--start] [--deadline YYYY-MM-DD]\n');
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
84
|
+
|
|
85
|
+
// Flags
|
|
86
|
+
const forceStart = args.includes('--start') || (args.some(a => a === '--backlog') === false && false);
|
|
87
|
+
const startNow = args.includes('--start');
|
|
88
|
+
const deadlineIdx = args.findIndex(a => a === '--deadline' || a.startsWith('--deadline='));
|
|
89
|
+
let deliveryDate = null;
|
|
90
|
+
if (deadlineIdx !== -1) {
|
|
91
|
+
deliveryDate = args[deadlineIdx].includes('=') ? args[deadlineIdx].split('=')[1] : args[deadlineIdx + 1];
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
const stories = loadStories(storiesDir);
|
|
81
95
|
const id = nextId(stories);
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const status = startNow ? 'in_progress' : 'backlog';
|
|
98
|
+
|
|
82
99
|
const story = {
|
|
83
100
|
id,
|
|
84
101
|
title,
|
|
85
|
-
status
|
|
86
|
-
createdAt:
|
|
87
|
-
|
|
102
|
+
status,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
startedAt: startNow ? now : null,
|
|
105
|
+
deliveryDate: deliveryDate || null,
|
|
106
|
+
completedAt: null,
|
|
88
107
|
tags: [],
|
|
89
108
|
notes: [],
|
|
90
109
|
};
|
|
110
|
+
|
|
91
111
|
const filename = path.join(storiesDir, `${id}.json`);
|
|
92
112
|
fs.writeFileSync(filename, JSON.stringify(story, null, 2), 'utf8');
|
|
93
113
|
|
|
@@ -97,14 +117,34 @@ function storyCreate(args, storiesDir, grimoireDir) {
|
|
|
97
117
|
if (fs.existsSync(metricsDir)) {
|
|
98
118
|
const today = new Date().toISOString().split('T')[0];
|
|
99
119
|
const mFile = path.join(metricsDir, `${today}.jsonl`);
|
|
100
|
-
fs.appendFileSync(mFile, JSON.stringify({ timestamp:
|
|
120
|
+
fs.appendFileSync(mFile, JSON.stringify({ timestamp: now, type: 'story_create', story: id, title }) + '\n');
|
|
101
121
|
}
|
|
102
122
|
} catch (_) { }
|
|
103
123
|
|
|
124
|
+
const statusLabel = status === 'in_progress' ? '🔄 em andamento' : '📋 backlog';
|
|
104
125
|
console.log(`\n✅ Story criada: [${id}] ${title}\n`);
|
|
105
|
-
console.log(` Status:
|
|
126
|
+
console.log(` Status: ${statusLabel}`);
|
|
127
|
+
if (deliveryDate) console.log(` Prazo: ${deliveryDate}`);
|
|
128
|
+
console.log(` Para iniciar: grimoire story start ${id}`);
|
|
106
129
|
console.log(` Para concluir: grimoire story done ${id}`);
|
|
107
|
-
console.log(`
|
|
130
|
+
console.log(` Dashboard: grimoire\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── start ─────────────────────────────────────────────────────────────────────
|
|
134
|
+
function storyStart(id, storiesDir, grimoireDir) {
|
|
135
|
+
if (!id) { console.log('Usage: grimoire story start <id>\n'); return; }
|
|
136
|
+
const f = path.join(storiesDir, `${id}.json`);
|
|
137
|
+
if (!fs.existsSync(f)) { console.error(`❌ Story ${id} não encontrada`); return; }
|
|
138
|
+
const story = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
139
|
+
if (story.status === 'in_progress') {
|
|
140
|
+
console.log(`⚠️ [${id}] já está em andamento.\n`); return;
|
|
141
|
+
}
|
|
142
|
+
story.status = 'in_progress';
|
|
143
|
+
story.startedAt = new Date().toISOString();
|
|
144
|
+
fs.writeFileSync(f, JSON.stringify(story, null, 2), 'utf8');
|
|
145
|
+
console.log(`\n🚀 [${id}] ${story.title}`);
|
|
146
|
+
console.log(` Status: 🔄 em andamento`);
|
|
147
|
+
console.log(` Iniciado: ${story.startedAt.slice(0, 10)}\n`);
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
// ── list ──────────────────────────────────────────────────────────────────────
|
package/bin/grimoire-cli.js
CHANGED
|
@@ -172,15 +172,26 @@ async function main() {
|
|
|
172
172
|
console.log(`Delegating ${command} to core logic...`);
|
|
173
173
|
require('./grimoire');
|
|
174
174
|
break;
|
|
175
|
+
case 'dashboard':
|
|
176
|
+
case 'dash':
|
|
177
|
+
require('./commands/dashboard').run(args.slice(1));
|
|
178
|
+
break;
|
|
179
|
+
case 'analyze':
|
|
180
|
+
case 'analyse':
|
|
181
|
+
require('./commands/analyze').run(args.slice(1));
|
|
182
|
+
break;
|
|
175
183
|
case '--version':
|
|
176
184
|
case '-v':
|
|
177
185
|
console.log(`Grimoire v${packageJson.version}`);
|
|
178
186
|
break;
|
|
179
187
|
case '--help':
|
|
180
188
|
case '-h':
|
|
181
|
-
default:
|
|
182
189
|
showHelp();
|
|
183
190
|
break;
|
|
191
|
+
default:
|
|
192
|
+
// No args or unknown command → show dashboard
|
|
193
|
+
require('./commands/dashboard').run([]);
|
|
194
|
+
break;
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
// Wait for update check to finish (prints banner after main command if needed)
|