@donotdev/cli 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +31 -0
  2. package/dependencies-matrix.json +205 -50
  3. package/dist/bin/commands/agent-setup.js +2 -2
  4. package/dist/bin/commands/build.js +6 -6
  5. package/dist/bin/commands/bump.js +495 -70
  6. package/dist/bin/commands/cacheout.js +6 -6
  7. package/dist/bin/commands/coach.js +6 -6
  8. package/dist/bin/commands/create-app.js +24 -16
  9. package/dist/bin/commands/create-project.js +114 -18
  10. package/dist/bin/commands/db.js +142136 -0
  11. package/dist/bin/commands/deploy.js +354 -126
  12. package/dist/bin/commands/dev.js +6 -6
  13. package/dist/bin/commands/doctor.js +140 -33
  14. package/dist/bin/commands/emu.js +6 -6
  15. package/dist/bin/commands/format.js +6 -6
  16. package/dist/bin/commands/get-demo.js +11 -6
  17. package/dist/bin/commands/make-admin.js +14210 -13770
  18. package/dist/bin/commands/preview.js +6 -6
  19. package/dist/bin/commands/seed.js +142426 -0
  20. package/dist/bin/commands/setup-cicd.js +8904 -0
  21. package/dist/bin/commands/setup.js +259 -212
  22. package/dist/bin/commands/staging.js +361 -127
  23. package/dist/bin/commands/sync-secrets.js +55 -33
  24. package/dist/bin/commands/type-check.js +16 -10
  25. package/dist/bin/commands/wai.js +6 -6
  26. package/dist/bin/dndev.js +194 -188
  27. package/dist/bin/donotdev.js +139 -189
  28. package/dist/index.js +468 -144
  29. package/package.json +1 -1
  30. package/templates/app-demo/.env.example +1 -0
  31. package/templates/{root-consumer → app-demo}/entities/ExampleEntity.ts.example +15 -9
  32. package/templates/app-demo/index.html.example +1 -1
  33. package/templates/app-demo/public/apple-touch-icon.png.example +0 -0
  34. package/templates/app-demo/public/favicon.svg.example +1 -0
  35. package/templates/app-demo/public/icon-192x192.png.example +0 -0
  36. package/templates/app-demo/public/icon-512x512.png.example +0 -0
  37. package/templates/app-demo/src/App.tsx.example +3 -1
  38. package/templates/app-demo/src/config/app.ts.example +1 -0
  39. package/templates/app-demo/src/entities/booking.ts.example +75 -0
  40. package/templates/app-demo/src/entities/onboarding.ts.example +160 -0
  41. package/templates/app-demo/src/entities/product.ts.example +12 -0
  42. package/templates/app-demo/src/entities/quote.ts.example +70 -0
  43. package/templates/app-demo/src/pages/ChangelogPage.tsx.example +28 -1
  44. package/templates/app-demo/src/pages/ConditionalFormPage.tsx.example +88 -0
  45. package/templates/app-demo/src/pages/DashboardPage.tsx.example +2 -0
  46. package/templates/app-demo/src/pages/HomePage.tsx.example +355 -2
  47. package/templates/app-demo/src/pages/OnboardingPage.tsx.example +47 -0
  48. package/templates/app-demo/src/pages/PricingPage.tsx.example +28 -1
  49. package/templates/app-demo/src/pages/ProductsPage.tsx.example +2 -0
  50. package/templates/app-demo/src/pages/ProfilePage.tsx.example +2 -0
  51. package/templates/app-demo/src/pages/SettingsPage.tsx.example +2 -0
  52. package/templates/app-demo/src/pages/ShowcaseDetailPage.tsx.example +22 -16
  53. package/templates/app-demo/src/pages/ShowcasePage.tsx.example +3 -1
  54. package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +147 -51
  55. package/templates/app-demo/src/pages/components/ComponentsData.tsx.example +103 -21
  56. package/templates/app-demo/src/pages/components/componentConfig.ts.example +139 -59
  57. package/templates/app-demo/src/pages/legal/LegalPage.tsx.example +12 -1
  58. package/templates/app-demo/src/pages/legal/PrivacyPage.tsx.example +10 -1
  59. package/templates/app-demo/src/pages/legal/TermsPage.tsx.example +10 -1
  60. package/templates/app-demo/src/themes.css.example +289 -77
  61. package/templates/app-demo/stats.html.example +4949 -0
  62. package/templates/app-dndev/index.html.example +164 -0
  63. package/templates/app-dndev/public/logo.svg.example +1 -0
  64. package/templates/app-dndev/public/manifest.json.example +10 -0
  65. package/templates/app-dndev/src/App.tsx.example +35 -0
  66. package/templates/app-dndev/src/components/CockpitLayout.css.example +181 -0
  67. package/templates/app-dndev/src/components/CockpitLayout.tsx.example +209 -0
  68. package/templates/app-dndev/src/components/Kanban.css.example +385 -0
  69. package/templates/app-dndev/src/components/ModeToggle.tsx.example +32 -0
  70. package/templates/app-dndev/src/components/OverlaySlot.tsx.example +68 -0
  71. package/templates/app-dndev/src/components/TerminalPanel.css.example +228 -0
  72. package/templates/app-dndev/src/components/TerminalPanel.tsx.example +714 -0
  73. package/templates/app-dndev/src/components/markdown-prose.css.example +49 -0
  74. package/templates/app-dndev/src/components/phases/CaptainLog.tsx.example +107 -0
  75. package/templates/app-dndev/src/components/phases/ContextTabs.tsx.example +352 -0
  76. package/templates/app-dndev/src/components/phases/PhaseCard.tsx.example +126 -0
  77. package/templates/app-dndev/src/components/phases/PhaseDetail.tsx.example +147 -0
  78. package/templates/app-dndev/src/components/phases/ReviewPanel.tsx.example +115 -0
  79. package/templates/app-dndev/src/components/phases/phaseData.ts.example +366 -0
  80. package/templates/app-dndev/src/config/app.ts.example +103 -0
  81. package/templates/app-dndev/src/config/commands.ts.example +171 -0
  82. package/templates/app-dndev/src/config/legal.ts.example +170 -0
  83. package/templates/app-dndev/src/config/providers.ts.example +7 -0
  84. package/templates/app-dndev/src/globals.css.example +10 -0
  85. package/templates/app-dndev/src/hooks/useDndevFile.ts.example +144 -0
  86. package/templates/app-dndev/src/main.tsx.example +21 -0
  87. package/templates/app-dndev/src/pages/BoardPage.tsx.example +640 -0
  88. package/templates/app-dndev/src/pages/GrillPage.tsx.example +658 -0
  89. package/templates/app-dndev/src/pages/HomePage.tsx.example +347 -0
  90. package/templates/app-dndev/src/pages/NotFoundPage.tsx.example +33 -0
  91. package/templates/app-dndev/src/pages/PhasesPage.tsx.example +137 -0
  92. package/templates/app-dndev/src/pages/SettingsPage.tsx.example +64 -0
  93. package/templates/app-dndev/src/pages/legal/LegalNoticePage.tsx.example +75 -0
  94. package/templates/app-dndev/src/pages/legal/PrivacyPage.tsx.example +69 -0
  95. package/templates/app-dndev/src/pages/legal/TermsPage.tsx.example +71 -0
  96. package/templates/app-dndev/src/stores/dndevStore.ts.example +386 -0
  97. package/templates/app-dndev/src/themes.css.example +161 -0
  98. package/templates/app-dndev/terminal-sidecar.cjs.example +341 -0
  99. package/templates/app-dndev/tsconfig.json.example +9 -0
  100. package/templates/app-dndev/vite.config.ts.example +24 -0
  101. package/templates/app-vite/index.html.example +1 -1
  102. package/templates/functions-supabase/supabase/functions/.env.example +0 -2
  103. package/templates/root-consumer/.claude/commands/grill.md.example +86 -8
  104. package/templates/root-consumer/.dndev.secrets.example +32 -0
  105. package/templates/root-consumer/.gitignore.example +3 -0
  106. package/templates/root-consumer/AI.md.example +4 -0
  107. package/templates/root-consumer/entities/index.ts.example +2 -5
  108. package/templates/root-consumer/guides/dndev/COMPONENTS_ATOMIC.md.example +4 -0
  109. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +23 -20
  110. package/templates/root-consumer/guides/dndev/INDEX.md.example +1 -0
  111. package/templates/root-consumer/guides/dndev/SETUP_BILLING.md.example +3 -7
  112. package/templates/root-consumer/guides/dndev/SETUP_CICD.md.example +115 -0
  113. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +41 -0
  114. package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +13 -18
  115. package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +17 -12
  116. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +185 -251
  117. package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +26 -8
  118. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +66 -49
  119. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +6 -5
  120. package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +9 -9
  121. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +1 -1
  122. package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +7 -6
  123. package/templates/root-consumer/guides/wai-way/context_map.json.example +51 -20
  124. package/templates/root-consumer/guides/wai-way/hld_template.md.example +138 -0
  125. package/templates/root-consumer/guides/wai-way/lld_template.md.example +103 -0
  126. package/templates/root-consumer/guides/wai-way/prd_template.md.example +140 -0
  127. /package/templates/{root-consumer → app-demo}/entities/Contact.ts.example +0 -0
  128. /package/templates/{root-consumer → app-demo}/entities/demo.ts.example +0 -0
@@ -0,0 +1,658 @@
1
+ /**
2
+ * @fileoverview Grill Review — triage findings from grill-report.json
3
+ *
4
+ * DataTable-based view with batch actions and per-item triage.
5
+ * Fix/Discuss/Fix Source inject prompts into AI agent terminal.
6
+ * Reject saves reason to grill-report.json + grill-ignore.json.
7
+ */
8
+
9
+ import { useEffect, useState } from 'react';
10
+ import {
11
+ ShieldCheck,
12
+ AlertTriangle,
13
+ AlertCircle,
14
+ Info,
15
+ Wrench,
16
+ X,
17
+ MessageCircle,
18
+ FileCode,
19
+ CheckCircle2,
20
+ Flame,
21
+ } from 'lucide-react';
22
+
23
+ import {
24
+ Section,
25
+ Card,
26
+ Code,
27
+ Stack,
28
+ Text,
29
+ Button,
30
+ Badge,
31
+ Input,
32
+ DataTable,
33
+ Grid,
34
+ Sheet,
35
+ ToggleGroup,
36
+ } from '@donotdev/components';
37
+ import type { TableColumn } from '@donotdev/components';
38
+ import type { PageMeta } from '@donotdev/core';
39
+ import { PageContainer } from '@donotdev/ui';
40
+
41
+ import { useDndevFile, writeDndevFile } from '../hooks/useDndevFile';
42
+ import { useDoNotDashStore } from '../stores/dndevStore';
43
+
44
+ export const meta: PageMeta = {
45
+ icon: <ShieldCheck />,
46
+ title: 'Grill',
47
+ };
48
+
49
+ // ============================================================================
50
+ // TYPES
51
+ // ============================================================================
52
+
53
+ type FindingStatus = 'pending' | 'fixing' | 'fixed' | 'rejected' | 'discussing' | 'fixing-source';
54
+
55
+ interface GrillItem {
56
+ id: number;
57
+ severity: 'blocker' | 'warn' | 'note';
58
+ file: string;
59
+ line: number;
60
+ issue: string;
61
+ context?: string;
62
+ category: string;
63
+ status: FindingStatus;
64
+ comment?: string;
65
+ }
66
+
67
+ interface GrillReport {
68
+ generated: string;
69
+ target: string;
70
+ verdict: 'DO NOT SHIP' | 'SHIP' | 'READY' | 'CONDITIONAL';
71
+ items: GrillItem[];
72
+ lastReviewed?: string;
73
+ }
74
+
75
+ interface IgnoreRule {
76
+ issue: string;
77
+ category: string;
78
+ reason: string;
79
+ date: string;
80
+ }
81
+
82
+ // ============================================================================
83
+ // CONSTANTS
84
+ // ============================================================================
85
+
86
+ const SEVERITY_ICON: Record<GrillItem['severity'], typeof AlertCircle> = {
87
+ blocker: AlertCircle,
88
+ warn: AlertTriangle,
89
+ note: Info,
90
+ };
91
+
92
+ const SEVERITY_VARIANT: Record<GrillItem['severity'], 'destructive' | 'warning' | 'primary'> = {
93
+ blocker: 'destructive',
94
+ warn: 'warning',
95
+ note: 'primary',
96
+ };
97
+
98
+ const STATUS_VARIANT: Record<FindingStatus, 'muted' | 'warning' | 'destructive' | 'accent' | 'success'> = {
99
+ pending: 'muted',
100
+ fixing: 'warning',
101
+ fixed: 'success',
102
+ rejected: 'destructive',
103
+ discussing: 'accent',
104
+ 'fixing-source': 'warning',
105
+ };
106
+
107
+ const VERDICT_VARIANT: Record<string, 'destructive' | 'success' | 'warning' | 'default'> = {
108
+ 'DO NOT SHIP': 'destructive',
109
+ SHIP: 'success',
110
+ READY: 'success',
111
+ CONDITIONAL: 'warning',
112
+ };
113
+
114
+ const FILTER_ITEMS = [
115
+ { value: 'all', label: 'All' },
116
+ { value: 'pending', label: 'Pending' },
117
+ { value: 'fixing', label: 'Fixing' },
118
+ { value: 'fixed', label: 'Fixed' },
119
+ { value: 'rejected', label: 'Rejected' },
120
+ ];
121
+
122
+ // ============================================================================
123
+ // EXPANDED ROW — context + actions for a single finding
124
+ // ============================================================================
125
+
126
+ function FindingActions({
127
+ item,
128
+ onAction,
129
+ }: {
130
+ item: GrillItem;
131
+ onAction: (id: number, action: 'fix' | 'reject' | 'discuss' | 'fix-source' | 'reopen' | 'mark-fixed', reason?: string) => void;
132
+ }) {
133
+ const [rejectMode, setRejectMode] = useState(false);
134
+ const [rejectReason, setRejectReason] = useState('');
135
+
136
+ function handleRejectSubmit() {
137
+ if (!rejectReason.trim()) return;
138
+ onAction(item.id, 'reject', rejectReason.trim());
139
+ setRejectMode(false);
140
+ setRejectReason('');
141
+ }
142
+
143
+ const actionable = item.status === 'pending' || item.status === 'discussing';
144
+
145
+ if (rejectMode) {
146
+ return (
147
+ <Stack direction="row" gap="tight" align="center">
148
+ <Input
149
+ value={rejectReason}
150
+ onChange={(e) => setRejectReason(e.target.value)}
151
+ placeholder="Why is this wrong?"
152
+ onKeyDown={(e) => { if (e.key === 'Enter') handleRejectSubmit(); }}
153
+ autoFocus
154
+ />
155
+ <Button variant="destructive" display="compact" onClick={handleRejectSubmit} disabled={!rejectReason.trim()}>
156
+ Reject
157
+ </Button>
158
+ <Button variant="ghost" display="compact" onClick={() => setRejectMode(false)}>
159
+ Cancel
160
+ </Button>
161
+ </Stack>
162
+ );
163
+ }
164
+
165
+ if (actionable) {
166
+ return (
167
+ <Stack direction="row" gap="tight">
168
+ <Button variant="default" display="compact" icon={Wrench} onClick={() => onAction(item.id, 'fix')}>
169
+ Fix
170
+ </Button>
171
+ <Button variant="destructive" display="compact" icon={X} onClick={() => setRejectMode(true)}>
172
+ Reject
173
+ </Button>
174
+ <Button variant="ghost" display="compact" icon={MessageCircle} onClick={() => onAction(item.id, 'discuss')}>
175
+ Discuss
176
+ </Button>
177
+ <Button variant="ghost" display="compact" icon={FileCode} onClick={() => onAction(item.id, 'fix-source')}>
178
+ Fix Source
179
+ </Button>
180
+ </Stack>
181
+ );
182
+ }
183
+
184
+ // Non-actionable (fixing, fixed, rejected, fixing-source) — show status + actions
185
+ return (
186
+ <Stack direction="row" gap="tight" align="center">
187
+ <Badge variant={STATUS_VARIANT[item.status]}>{item.status}</Badge>
188
+ {item.comment && <Text level="caption" variant="muted">{item.comment}</Text>}
189
+ {(item.status === 'fixing' || item.status === 'fixing-source') && (
190
+ <Button variant="default" display="compact" icon={CheckCircle2} onClick={() => onAction(item.id, 'mark-fixed')}>
191
+ Mark Fixed
192
+ </Button>
193
+ )}
194
+ <Button variant="ghost" display="compact" onClick={() => onAction(item.id, 'reopen')}>
195
+ Reopen
196
+ </Button>
197
+ </Stack>
198
+ );
199
+ }
200
+
201
+ // ============================================================================
202
+ // CODE PREVIEW — fetch file content for read-only display
203
+ // ============================================================================
204
+
205
+ /** Fetch a source file and return lines array. Cached per path. */
206
+ function useSourceFile(path: string | null): { lines: string[]; loading: boolean } {
207
+ const [lines, setLines] = useState<string[]>([]);
208
+ const [loading, setLoading] = useState(false);
209
+
210
+ useEffect(() => {
211
+ if (!path) { setLines([]); return; }
212
+ let cancelled = false;
213
+ setLoading(true);
214
+ fetch(`/api/dndev/file?path=${encodeURIComponent(path)}`)
215
+ .then((res) => res.ok ? res.json() : null)
216
+ .then((json) => {
217
+ if (cancelled) return;
218
+ const content = json?.content ?? '';
219
+ setLines(typeof content === 'string' ? content.split('\n') : []);
220
+ })
221
+ .catch(() => { if (!cancelled) setLines([]); })
222
+ .finally(() => { if (!cancelled) setLoading(false); });
223
+ return () => { cancelled = true; };
224
+ }, [path]);
225
+
226
+ return { lines, loading };
227
+ }
228
+
229
+ /** How many lines of context to show around the finding line */
230
+ const CODE_CONTEXT_LINES = 15;
231
+
232
+ /** Guess language from file extension */
233
+ function guessLanguage(file: string): string {
234
+ const ext = file.split('.').pop()?.toLowerCase() ?? '';
235
+ const map: Record<string, string> = {
236
+ ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
237
+ css: 'css', json: 'json', md: 'markdown', html: 'html',
238
+ vue: 'vue', py: 'python', rs: 'rust', go: 'go',
239
+ };
240
+ return map[ext] ?? 'typescript';
241
+ }
242
+
243
+ function FindingSheet({
244
+ item,
245
+ open,
246
+ onOpenChange,
247
+ onAction,
248
+ }: {
249
+ item: GrillItem | null;
250
+ open: boolean;
251
+ onOpenChange: (open: boolean) => void;
252
+ onAction: (id: number, action: 'fix' | 'reject' | 'discuss' | 'fix-source' | 'reopen' | 'mark-fixed', reason?: string) => void;
253
+ }) {
254
+ const { lines, loading: fileLoading } = useSourceFile(open && item ? item.file : null);
255
+
256
+ if (!item) return null;
257
+
258
+ // Extract lines around the finding
259
+ const targetLine = item.line;
260
+ const startLine = Math.max(0, targetLine - CODE_CONTEXT_LINES - 1);
261
+ const endLine = Math.min(lines.length, targetLine + CODE_CONTEXT_LINES);
262
+ const codeSnippet = lines.slice(startLine, endLine).join('\n');
263
+ const lang = guessLanguage(item.file);
264
+
265
+ const SeverityIcon = SEVERITY_ICON[item.severity];
266
+
267
+ return (
268
+ <Sheet
269
+ open={open}
270
+ onOpenChange={onOpenChange}
271
+ title={
272
+ <Stack direction="row" align="center" gap="tight">
273
+ <Badge variant={SEVERITY_VARIANT[item.severity]}>
274
+ <SeverityIcon size={12} /> {item.severity}
275
+ </Badge>
276
+ <Badge variant="muted">{item.category}</Badge>
277
+ </Stack>
278
+ }
279
+ description={item.issue}
280
+ side="right"
281
+ footer={
282
+ <FindingActions
283
+ item={item}
284
+ onAction={(id, action, reason) => {
285
+ onAction(id, action, reason);
286
+ if (action !== 'reopen') onOpenChange(false);
287
+ }}
288
+ />
289
+ }
290
+ >
291
+ <Stack gap="tight">
292
+ {/* Location */}
293
+ <Text as="code" variant="code" level="caption">{item.file}:{item.line}</Text>
294
+
295
+ {/* Code preview with Shiki syntax highlighting */}
296
+ {fileLoading ? (
297
+ <Text level="caption" variant="muted">Loading source...</Text>
298
+ ) : lines.length > 0 ? (
299
+ <Code language={lang} compact showCopyButton={false}>
300
+ {codeSnippet}
301
+ </Code>
302
+ ) : (
303
+ <Text level="caption" variant="muted">Could not load source file.</Text>
304
+ )}
305
+ </Stack>
306
+ </Sheet>
307
+ );
308
+ }
309
+
310
+ // ============================================================================
311
+ // START GRILL — collapsible trigger section
312
+ // ============================================================================
313
+
314
+ function StartGrill() {
315
+ const [target, setTarget] = useState('');
316
+
317
+ function handleRun() {
318
+ const cmd = target.trim() ? `/grill ${target.trim()}` : '/grill';
319
+ useDoNotDashStore.getState().injectPrompt(cmd, { mode: 'ai-agent' });
320
+ }
321
+
322
+ return (
323
+ <Grid cols="1fr auto" gap="tight" align="end">
324
+ <Input
325
+ placeholder="Target — e.g. packages/core, src/pages (blank = all)"
326
+ value={target}
327
+ onChange={(e) => setTarget(e.target.value)}
328
+ onKeyDown={(e) => { if (e.key === 'Enter') handleRun(); }}
329
+ />
330
+ <Button icon={Flame} variant="default" onClick={handleRun}>
331
+ Grill
332
+ </Button>
333
+ </Grid>
334
+ );
335
+ }
336
+
337
+ // ============================================================================
338
+ // PAGE
339
+ // ============================================================================
340
+
341
+ const DEFAULT_REPORT: GrillReport = {
342
+ generated: '',
343
+ target: '',
344
+ verdict: 'READY',
345
+ items: [],
346
+ };
347
+
348
+ export default function GrillPage() {
349
+ const { data: report, loading, reload } = useDndevFile<GrillReport>(
350
+ '.dndev/grill-report.json',
351
+ (raw) => raw as GrillReport,
352
+ { fallback: DEFAULT_REPORT },
353
+ );
354
+
355
+ const [filter, setFilter] = useState('all');
356
+ const [expandedId, setExpandedId] = useState<number | null>(null);
357
+ const data = report ?? DEFAULT_REPORT;
358
+ const hasReport = data.items.length > 0;
359
+
360
+ // ---- Action handler ----
361
+
362
+ async function handleAction(id: number, action: 'fix' | 'reject' | 'discuss' | 'fix-source' | 'reopen' | 'mark-fixed', reason?: string) {
363
+ if (!report) return;
364
+ const item = report.items.find((i) => i.id === id);
365
+ if (!item) return;
366
+
367
+ const statusMap: Record<string, FindingStatus> = {
368
+ fix: 'fixing', 'mark-fixed': 'fixed', reject: 'rejected', discuss: 'discussing', 'fix-source': 'fixing-source', reopen: 'pending',
369
+ };
370
+
371
+ const updated: GrillReport = {
372
+ ...report,
373
+ lastReviewed: new Date().toISOString(),
374
+ items: report.items.map((i) =>
375
+ i.id === id
376
+ ? { ...i, status: statusMap[action]!, comment: action === 'reject' ? reason : i.comment }
377
+ : i,
378
+ ),
379
+ };
380
+ await writeDndevFile('.dndev/grill-report.json', updated);
381
+
382
+ if (action === 'fix') {
383
+ const prompt = [
384
+ `Fix this grill finding in ${item.file}:${item.line}:`,
385
+ item.issue,
386
+ item.context ? `Context:\n${item.context}` : '',
387
+ 'Please fix this issue.',
388
+ ].filter(Boolean).join('\n');
389
+ useDoNotDashStore.getState().injectPrompt(prompt, { mode: 'ai-agent' });
390
+ }
391
+
392
+ if (action === 'discuss') {
393
+ const prompt = [
394
+ `Explain this grill finding in detail:`,
395
+ `File: ${item.file}:${item.line}`,
396
+ `Issue: ${item.issue}`,
397
+ `Category: ${item.category}`,
398
+ item.context ? `Context:\n${item.context}` : '',
399
+ 'Is this a real issue? What would be the correct fix?',
400
+ ].filter(Boolean).join('\n');
401
+ useDoNotDashStore.getState().injectPrompt(prompt, { mode: 'ai-agent' });
402
+ }
403
+
404
+ if (action === 'fix-source') {
405
+ const prompt = [
406
+ `Update the JSDoc or conventions for "${item.category}" so this finding never flags again:`,
407
+ `Issue: ${item.issue}`,
408
+ `File: ${item.file}:${item.line}`,
409
+ 'The convention rule or JSDoc is wrong/incomplete. Fix the source of truth, not the code.',
410
+ ].filter(Boolean).join('\n');
411
+ useDoNotDashStore.getState().injectPrompt(prompt, { mode: 'ai-agent' });
412
+ }
413
+
414
+ if (action === 'reject' && reason) {
415
+ try {
416
+ const ignoreRes = await fetch('/api/dndev/file?path=.dndev/grill-ignore.json');
417
+ const ignoreData: IgnoreRule[] = ignoreRes.ok
418
+ ? ((await ignoreRes.json()).content ?? [])
419
+ : [];
420
+
421
+ const rule: IgnoreRule = {
422
+ issue: item.issue,
423
+ category: item.category,
424
+ reason,
425
+ date: new Date().toISOString(),
426
+ };
427
+
428
+ await writeDndevFile('.dndev/grill-ignore.json', [...(Array.isArray(ignoreData) ? ignoreData : []), rule]);
429
+ } catch (err) {
430
+ console.error('[grill] failed to write ignore rule — rejection saved but rule lost:', err);
431
+ }
432
+ }
433
+
434
+ await reload();
435
+ }
436
+
437
+ // ---- Batch actions ----
438
+
439
+ async function handleBatchFix(severity?: GrillItem['severity']) {
440
+ if (!report) return;
441
+ const pending = report.items.filter((i) =>
442
+ i.status === 'pending' && (!severity || i.severity === severity),
443
+ );
444
+ if (pending.length === 0) return;
445
+
446
+ const updated: GrillReport = {
447
+ ...report,
448
+ lastReviewed: new Date().toISOString(),
449
+ items: report.items.map((i) =>
450
+ i.status === 'pending' && (!severity || i.severity === severity)
451
+ ? { ...i, status: 'fixing' as FindingStatus }
452
+ : i,
453
+ ),
454
+ };
455
+ await writeDndevFile('.dndev/grill-report.json', updated);
456
+
457
+ const prompt = pending.map((item) => [
458
+ `Fix this grill finding in ${item.file}:${item.line}:`,
459
+ item.issue,
460
+ item.context ? `Context:\n${item.context}` : '',
461
+ 'Please fix this issue.',
462
+ ].filter(Boolean).join('\n')).join('\n\n---\n\n');
463
+
464
+ useDoNotDashStore.getState().injectPrompt(prompt, { mode: 'ai-agent' });
465
+ await reload();
466
+ }
467
+
468
+ // ---- Stats ----
469
+
470
+ const items = data.items;
471
+ const stats = {
472
+ blocker: items.filter((i) => i.severity === 'blocker').length,
473
+ warn: items.filter((i) => i.severity === 'warn').length,
474
+ note: items.filter((i) => i.severity === 'note').length,
475
+ pending: items.filter((i) => i.status === 'pending').length,
476
+ total: items.length,
477
+ };
478
+
479
+ // ---- Filter ----
480
+
481
+ const filteredItems = filter === 'all'
482
+ ? items
483
+ : items.filter((i) => i.status === filter);
484
+
485
+ // ---- Table columns ----
486
+
487
+ const columns: TableColumn<GrillItem>[] = [
488
+ {
489
+ key: 'severity',
490
+ title: '',
491
+ width: 40,
492
+ align: 'center',
493
+ render: (_v, row) => {
494
+ const Icon = SEVERITY_ICON[row.severity];
495
+ return (
496
+ <Badge variant={SEVERITY_VARIANT[row.severity]} as="span">
497
+ <Icon size={12} />
498
+ </Badge>
499
+ );
500
+ },
501
+ },
502
+ {
503
+ key: 'location',
504
+ title: 'Location',
505
+ width: 220,
506
+ align: 'start',
507
+ render: (_v, row) => (
508
+ <Text as="code" variant="code" level="caption">
509
+ {row.file.replace(/^apps\/dndev\//, '')}:{row.line}
510
+ </Text>
511
+ ),
512
+ },
513
+ {
514
+ key: 'issue',
515
+ title: 'Issue',
516
+ dataIndex: 'issue',
517
+ align: 'start',
518
+ render: (_v, row) => (
519
+ <Text level="small">{row.issue}</Text>
520
+ ),
521
+ },
522
+ {
523
+ key: 'category',
524
+ title: 'Cat',
525
+ width: 100,
526
+ align: 'start',
527
+ render: (_v, row) => <Badge variant="muted">{row.category}</Badge>,
528
+ },
529
+ {
530
+ key: 'status',
531
+ title: 'Status',
532
+ width: 100,
533
+ align: 'start',
534
+ render: (_v, row) => (
535
+ <Badge variant={STATUS_VARIANT[row.status]}>{row.status}</Badge>
536
+ ),
537
+ },
538
+ ];
539
+
540
+ // ---- Render ----
541
+
542
+ if (loading) {
543
+ return (
544
+ <PageContainer>
545
+ <Section title="Grill Review">
546
+ <Stack align="center"><Text level="small" variant="muted">Loading report...</Text></Stack>
547
+ </Section>
548
+ </PageContainer>
549
+ );
550
+ }
551
+
552
+ return (
553
+ <PageContainer>
554
+ <Section title="Grill Review">
555
+ <Stack gap="tight">
556
+ <StartGrill />
557
+
558
+ {hasReport ? (
559
+ <Stack gap="tight">
560
+ {/* Verdict bar + batch actions */}
561
+ <Card>
562
+ <Stack direction="row" align="center" justify="between">
563
+ <Stack direction="row" align="center" gap="tight">
564
+ <Badge variant={VERDICT_VARIANT[data.verdict] ?? 'default'}>{data.verdict}</Badge>
565
+ <Text level="caption" variant="muted">{data.target}</Text>
566
+ </Stack>
567
+ <Stack direction="row" align="center" gap="tight">
568
+ {stats.pending > 0 && (
569
+ <Button
570
+ variant="default"
571
+ display="compact"
572
+ icon={Wrench}
573
+ onClick={() => handleBatchFix()}
574
+ >
575
+ Fix all ({stats.pending})
576
+ </Button>
577
+ )}
578
+ {stats.blocker > 0 && stats.pending !== stats.blocker && (
579
+ <Button
580
+ variant="destructive"
581
+ display="compact"
582
+ icon={AlertCircle}
583
+ onClick={() => handleBatchFix('blocker')}
584
+ >
585
+ Fix blockers
586
+ </Button>
587
+ )}
588
+ <ToggleGroup
589
+ type="single"
590
+ size="sm"
591
+ variant="outline"
592
+ value={filter}
593
+ onValueChange={(v) => { if (v) setFilter(v); }}
594
+ items={FILTER_ITEMS}
595
+ />
596
+ </Stack>
597
+ </Stack>
598
+ </Card>
599
+
600
+ {/* Stats */}
601
+ <Grid cols={[2, 2, 4, 4]} gap="tight">
602
+ <Card variant="destructive">
603
+ <Stack direction="row" align="center" gap="tight">
604
+ <AlertCircle size={14} />
605
+ <Text level="caption" weight="semibold">{stats.blocker} blockers</Text>
606
+ </Stack>
607
+ </Card>
608
+ <Card variant="warning">
609
+ <Stack direction="row" align="center" gap="tight">
610
+ <AlertTriangle size={14} />
611
+ <Text level="caption" weight="semibold">{stats.warn} warnings</Text>
612
+ </Stack>
613
+ </Card>
614
+ <Card variant="primary">
615
+ <Stack direction="row" align="center" gap="tight">
616
+ <Info size={14} />
617
+ <Text level="caption" weight="semibold">{stats.note} notes</Text>
618
+ </Stack>
619
+ </Card>
620
+ <Card variant="muted">
621
+ <Stack direction="row" align="center" gap="tight">
622
+ <CheckCircle2 size={14} />
623
+ <Text level="caption" weight="semibold">{stats.pending}/{stats.total} pending</Text>
624
+ </Stack>
625
+ </Card>
626
+ </Grid>
627
+
628
+ {/* Findings table */}
629
+ <DataTable<GrillItem>
630
+ data={filteredItems}
631
+ columns={columns}
632
+ onRowClick={(row) => setExpandedId(row.id)}
633
+ />
634
+
635
+ {/* Finding detail sheet with code preview + actions */}
636
+ <FindingSheet
637
+ item={expandedId !== null ? items.find((i) => i.id === expandedId) ?? null : null}
638
+ open={expandedId !== null}
639
+ onOpenChange={(open) => { if (!open) setExpandedId(null); }}
640
+ onAction={async (id, action, reason) => { await handleAction(id, action, reason); }}
641
+ />
642
+
643
+ {filteredItems.length === 0 && (
644
+ <Card variant="muted">
645
+ <Text level="small" variant="muted">No findings match the current filter.</Text>
646
+ </Card>
647
+ )}
648
+ </Stack>
649
+ ) : (
650
+ <Card variant="muted">
651
+ <Text level="small" variant="muted">No findings yet — run a grill to get started.</Text>
652
+ </Card>
653
+ )}
654
+ </Stack>
655
+ </Section>
656
+ </PageContainer>
657
+ );
658
+ }