grimoire-framework 1.6.0 → 1.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.
@@ -7,8 +7,8 @@
7
7
  # - SHA256 hashes for change detection
8
8
  # - File types for categorization
9
9
  #
10
- version: 1.6.0
11
- generated_at: "2026-02-22T17:57:17.499Z"
10
+ version: 1.7.0
11
+ generated_at: "2026-02-22T18:11:23.264Z"
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 };
@@ -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 };
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * grimoire story — Story Tracking CLI
3
3
  *
4
- * grimoire story create "Título da story" Cria nova story
5
- * grimoire story list Lista stories ativas
6
- * grimoire story list --all Lista todas (inclui concluídas)
7
- * grimoire story done <id> Marca story como concluída
8
- * grimoire story show <id> Detalhes da story
9
- * grimoire story delete <id> Remove 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 da story"\n');
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: 'in_progress',
86
- createdAt: new Date().toISOString(),
87
- doneAt: null,
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: new Date().toISOString(), type: 'story_create', story: id, title }) + '\n');
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: 🔄 em andamento`);
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(` Para ver: grimoire story show ${id}\n`);
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 ──────────────────────────────────────────────────────────────────────
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grimoire-framework",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Grimoire: AI-Orchestrated System for Full Stack Development - Core Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"