@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.
- package/README.md +31 -0
- package/dependencies-matrix.json +205 -50
- package/dist/bin/commands/agent-setup.js +2 -2
- package/dist/bin/commands/build.js +6 -6
- package/dist/bin/commands/bump.js +495 -70
- package/dist/bin/commands/cacheout.js +6 -6
- package/dist/bin/commands/coach.js +6 -6
- package/dist/bin/commands/create-app.js +24 -16
- package/dist/bin/commands/create-project.js +114 -18
- package/dist/bin/commands/db.js +142136 -0
- package/dist/bin/commands/deploy.js +354 -126
- package/dist/bin/commands/dev.js +6 -6
- package/dist/bin/commands/doctor.js +140 -33
- package/dist/bin/commands/emu.js +6 -6
- package/dist/bin/commands/format.js +6 -6
- package/dist/bin/commands/get-demo.js +11 -6
- package/dist/bin/commands/make-admin.js +14210 -13770
- package/dist/bin/commands/preview.js +6 -6
- package/dist/bin/commands/seed.js +142426 -0
- package/dist/bin/commands/setup-cicd.js +8904 -0
- package/dist/bin/commands/setup.js +259 -212
- package/dist/bin/commands/staging.js +361 -127
- package/dist/bin/commands/sync-secrets.js +55 -33
- package/dist/bin/commands/type-check.js +16 -10
- package/dist/bin/commands/wai.js +6 -6
- package/dist/bin/dndev.js +194 -188
- package/dist/bin/donotdev.js +139 -189
- package/dist/index.js +468 -144
- package/package.json +1 -1
- package/templates/app-demo/.env.example +1 -0
- package/templates/{root-consumer → app-demo}/entities/ExampleEntity.ts.example +15 -9
- package/templates/app-demo/index.html.example +1 -1
- package/templates/app-demo/public/apple-touch-icon.png.example +0 -0
- package/templates/app-demo/public/favicon.svg.example +1 -0
- package/templates/app-demo/public/icon-192x192.png.example +0 -0
- package/templates/app-demo/public/icon-512x512.png.example +0 -0
- package/templates/app-demo/src/App.tsx.example +3 -1
- package/templates/app-demo/src/config/app.ts.example +1 -0
- package/templates/app-demo/src/entities/booking.ts.example +75 -0
- package/templates/app-demo/src/entities/onboarding.ts.example +160 -0
- package/templates/app-demo/src/entities/product.ts.example +12 -0
- package/templates/app-demo/src/entities/quote.ts.example +70 -0
- package/templates/app-demo/src/pages/ChangelogPage.tsx.example +28 -1
- package/templates/app-demo/src/pages/ConditionalFormPage.tsx.example +88 -0
- package/templates/app-demo/src/pages/DashboardPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/HomePage.tsx.example +355 -2
- package/templates/app-demo/src/pages/OnboardingPage.tsx.example +47 -0
- package/templates/app-demo/src/pages/PricingPage.tsx.example +28 -1
- package/templates/app-demo/src/pages/ProductsPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/ProfilePage.tsx.example +2 -0
- package/templates/app-demo/src/pages/SettingsPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/ShowcaseDetailPage.tsx.example +22 -16
- package/templates/app-demo/src/pages/ShowcasePage.tsx.example +3 -1
- package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +147 -51
- package/templates/app-demo/src/pages/components/ComponentsData.tsx.example +103 -21
- package/templates/app-demo/src/pages/components/componentConfig.ts.example +139 -59
- package/templates/app-demo/src/pages/legal/LegalPage.tsx.example +12 -1
- package/templates/app-demo/src/pages/legal/PrivacyPage.tsx.example +10 -1
- package/templates/app-demo/src/pages/legal/TermsPage.tsx.example +10 -1
- package/templates/app-demo/src/themes.css.example +289 -77
- package/templates/app-demo/stats.html.example +4949 -0
- package/templates/app-dndev/index.html.example +164 -0
- package/templates/app-dndev/public/logo.svg.example +1 -0
- package/templates/app-dndev/public/manifest.json.example +10 -0
- package/templates/app-dndev/src/App.tsx.example +35 -0
- package/templates/app-dndev/src/components/CockpitLayout.css.example +181 -0
- package/templates/app-dndev/src/components/CockpitLayout.tsx.example +209 -0
- package/templates/app-dndev/src/components/Kanban.css.example +385 -0
- package/templates/app-dndev/src/components/ModeToggle.tsx.example +32 -0
- package/templates/app-dndev/src/components/OverlaySlot.tsx.example +68 -0
- package/templates/app-dndev/src/components/TerminalPanel.css.example +228 -0
- package/templates/app-dndev/src/components/TerminalPanel.tsx.example +714 -0
- package/templates/app-dndev/src/components/markdown-prose.css.example +49 -0
- package/templates/app-dndev/src/components/phases/CaptainLog.tsx.example +107 -0
- package/templates/app-dndev/src/components/phases/ContextTabs.tsx.example +352 -0
- package/templates/app-dndev/src/components/phases/PhaseCard.tsx.example +126 -0
- package/templates/app-dndev/src/components/phases/PhaseDetail.tsx.example +147 -0
- package/templates/app-dndev/src/components/phases/ReviewPanel.tsx.example +115 -0
- package/templates/app-dndev/src/components/phases/phaseData.ts.example +366 -0
- package/templates/app-dndev/src/config/app.ts.example +103 -0
- package/templates/app-dndev/src/config/commands.ts.example +171 -0
- package/templates/app-dndev/src/config/legal.ts.example +170 -0
- package/templates/app-dndev/src/config/providers.ts.example +7 -0
- package/templates/app-dndev/src/globals.css.example +10 -0
- package/templates/app-dndev/src/hooks/useDndevFile.ts.example +144 -0
- package/templates/app-dndev/src/main.tsx.example +21 -0
- package/templates/app-dndev/src/pages/BoardPage.tsx.example +640 -0
- package/templates/app-dndev/src/pages/GrillPage.tsx.example +658 -0
- package/templates/app-dndev/src/pages/HomePage.tsx.example +347 -0
- package/templates/app-dndev/src/pages/NotFoundPage.tsx.example +33 -0
- package/templates/app-dndev/src/pages/PhasesPage.tsx.example +137 -0
- package/templates/app-dndev/src/pages/SettingsPage.tsx.example +64 -0
- package/templates/app-dndev/src/pages/legal/LegalNoticePage.tsx.example +75 -0
- package/templates/app-dndev/src/pages/legal/PrivacyPage.tsx.example +69 -0
- package/templates/app-dndev/src/pages/legal/TermsPage.tsx.example +71 -0
- package/templates/app-dndev/src/stores/dndevStore.ts.example +386 -0
- package/templates/app-dndev/src/themes.css.example +161 -0
- package/templates/app-dndev/terminal-sidecar.cjs.example +341 -0
- package/templates/app-dndev/tsconfig.json.example +9 -0
- package/templates/app-dndev/vite.config.ts.example +24 -0
- package/templates/app-vite/index.html.example +1 -1
- package/templates/functions-supabase/supabase/functions/.env.example +0 -2
- package/templates/root-consumer/.claude/commands/grill.md.example +86 -8
- package/templates/root-consumer/.dndev.secrets.example +32 -0
- package/templates/root-consumer/.gitignore.example +3 -0
- package/templates/root-consumer/AI.md.example +4 -0
- package/templates/root-consumer/entities/index.ts.example +2 -5
- package/templates/root-consumer/guides/dndev/COMPONENTS_ATOMIC.md.example +4 -0
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +23 -20
- package/templates/root-consumer/guides/dndev/INDEX.md.example +1 -0
- package/templates/root-consumer/guides/dndev/SETUP_BILLING.md.example +3 -7
- package/templates/root-consumer/guides/dndev/SETUP_CICD.md.example +115 -0
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +41 -0
- package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +13 -18
- package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +17 -12
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +185 -251
- package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +26 -8
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +66 -49
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +6 -5
- package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +9 -9
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +1 -1
- package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +7 -6
- package/templates/root-consumer/guides/wai-way/context_map.json.example +51 -20
- package/templates/root-consumer/guides/wai-way/hld_template.md.example +138 -0
- package/templates/root-consumer/guides/wai-way/lld_template.md.example +103 -0
- package/templates/root-consumer/guides/wai-way/prd_template.md.example +140 -0
- /package/templates/{root-consumer → app-demo}/entities/Contact.ts.example +0 -0
- /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
|
+
}
|