popeye-cli 1.6.0 → 1.8.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.
Files changed (161) hide show
  1. package/README.md +240 -32
  2. package/cheatsheet.md +407 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +2 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +2 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/interactive.d.ts.map +1 -1
  19. package/dist/cli/interactive.js +96 -0
  20. package/dist/cli/interactive.js.map +1 -1
  21. package/dist/generators/admin-wizard.d.ts +25 -0
  22. package/dist/generators/admin-wizard.d.ts.map +1 -0
  23. package/dist/generators/admin-wizard.js +123 -0
  24. package/dist/generators/admin-wizard.js.map +1 -0
  25. package/dist/generators/all.d.ts.map +1 -1
  26. package/dist/generators/all.js +10 -3
  27. package/dist/generators/all.js.map +1 -1
  28. package/dist/generators/database.d.ts +58 -0
  29. package/dist/generators/database.d.ts.map +1 -0
  30. package/dist/generators/database.js +229 -0
  31. package/dist/generators/database.js.map +1 -0
  32. package/dist/generators/fullstack.d.ts.map +1 -1
  33. package/dist/generators/fullstack.js +23 -7
  34. package/dist/generators/fullstack.js.map +1 -1
  35. package/dist/generators/index.d.ts +2 -0
  36. package/dist/generators/index.d.ts.map +1 -1
  37. package/dist/generators/index.js +2 -0
  38. package/dist/generators/index.js.map +1 -1
  39. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  40. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  41. package/dist/generators/templates/admin-wizard-python.js +425 -0
  42. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  43. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  44. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-react.js +554 -0
  46. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  47. package/dist/generators/templates/database-docker.d.ts +23 -0
  48. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  49. package/dist/generators/templates/database-docker.js +221 -0
  50. package/dist/generators/templates/database-docker.js.map +1 -0
  51. package/dist/generators/templates/database-python.d.ts +54 -0
  52. package/dist/generators/templates/database-python.d.ts.map +1 -0
  53. package/dist/generators/templates/database-python.js +723 -0
  54. package/dist/generators/templates/database-python.js.map +1 -0
  55. package/dist/generators/templates/database-typescript.d.ts +34 -0
  56. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  57. package/dist/generators/templates/database-typescript.js +232 -0
  58. package/dist/generators/templates/database-typescript.js.map +1 -0
  59. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  60. package/dist/generators/templates/fullstack.js +29 -0
  61. package/dist/generators/templates/fullstack.js.map +1 -1
  62. package/dist/generators/templates/index.d.ts +5 -0
  63. package/dist/generators/templates/index.d.ts.map +1 -1
  64. package/dist/generators/templates/index.js +5 -0
  65. package/dist/generators/templates/index.js.map +1 -1
  66. package/dist/state/index.d.ts +10 -0
  67. package/dist/state/index.d.ts.map +1 -1
  68. package/dist/state/index.js +22 -0
  69. package/dist/state/index.js.map +1 -1
  70. package/dist/types/consensus.d.ts +3 -0
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +1 -0
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/database-runtime.d.ts +86 -0
  75. package/dist/types/database-runtime.d.ts.map +1 -0
  76. package/dist/types/database-runtime.js +61 -0
  77. package/dist/types/database-runtime.js.map +1 -0
  78. package/dist/types/database.d.ts +85 -0
  79. package/dist/types/database.d.ts.map +1 -0
  80. package/dist/types/database.js +71 -0
  81. package/dist/types/database.js.map +1 -0
  82. package/dist/types/index.d.ts +3 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +6 -0
  85. package/dist/types/index.js.map +1 -1
  86. package/dist/types/tester.d.ts +138 -0
  87. package/dist/types/tester.d.ts.map +1 -0
  88. package/dist/types/tester.js +110 -0
  89. package/dist/types/tester.js.map +1 -0
  90. package/dist/types/workflow.d.ts +166 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +14 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/db-setup-runner.d.ts +63 -0
  95. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  96. package/dist/workflow/db-setup-runner.js +336 -0
  97. package/dist/workflow/db-setup-runner.js.map +1 -0
  98. package/dist/workflow/db-state-machine.d.ts +30 -0
  99. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  100. package/dist/workflow/db-state-machine.js +51 -0
  101. package/dist/workflow/db-state-machine.js.map +1 -0
  102. package/dist/workflow/execution-mode.js +2 -2
  103. package/dist/workflow/execution-mode.js.map +1 -1
  104. package/dist/workflow/index.d.ts +3 -0
  105. package/dist/workflow/index.d.ts.map +1 -1
  106. package/dist/workflow/index.js +3 -0
  107. package/dist/workflow/index.js.map +1 -1
  108. package/dist/workflow/task-workflow.d.ts +5 -0
  109. package/dist/workflow/task-workflow.d.ts.map +1 -1
  110. package/dist/workflow/task-workflow.js +172 -6
  111. package/dist/workflow/task-workflow.js.map +1 -1
  112. package/dist/workflow/tester.d.ts +120 -0
  113. package/dist/workflow/tester.d.ts.map +1 -0
  114. package/dist/workflow/tester.js +589 -0
  115. package/dist/workflow/tester.js.map +1 -0
  116. package/dist/workflow/workflow-logger.d.ts +1 -1
  117. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  118. package/dist/workflow/workflow-logger.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/cli/commands/db.ts +281 -0
  121. package/src/cli/commands/doctor.ts +273 -0
  122. package/src/cli/commands/index.ts +2 -0
  123. package/src/cli/index.ts +4 -0
  124. package/src/cli/interactive.ts +102 -0
  125. package/src/generators/admin-wizard.ts +146 -0
  126. package/src/generators/all.ts +10 -3
  127. package/src/generators/database.ts +286 -0
  128. package/src/generators/fullstack.ts +26 -9
  129. package/src/generators/index.ts +12 -0
  130. package/src/generators/templates/admin-wizard-python.ts +431 -0
  131. package/src/generators/templates/admin-wizard-react.ts +560 -0
  132. package/src/generators/templates/database-docker.ts +227 -0
  133. package/src/generators/templates/database-python.ts +734 -0
  134. package/src/generators/templates/database-typescript.ts +238 -0
  135. package/src/generators/templates/fullstack.ts +29 -0
  136. package/src/generators/templates/index.ts +5 -0
  137. package/src/state/index.ts +29 -0
  138. package/src/types/consensus.ts +3 -0
  139. package/src/types/database-runtime.ts +69 -0
  140. package/src/types/database.ts +84 -0
  141. package/src/types/index.ts +50 -0
  142. package/src/types/tester.ts +136 -0
  143. package/src/types/workflow.ts +31 -0
  144. package/src/workflow/db-setup-runner.ts +391 -0
  145. package/src/workflow/db-state-machine.ts +58 -0
  146. package/src/workflow/execution-mode.ts +2 -2
  147. package/src/workflow/index.ts +3 -0
  148. package/src/workflow/task-workflow.ts +227 -5
  149. package/src/workflow/tester.ts +723 -0
  150. package/src/workflow/workflow-logger.ts +2 -0
  151. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  152. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  153. package/tests/generators/cross-phase-integration.test.ts +383 -0
  154. package/tests/generators/database.test.ts +456 -0
  155. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  156. package/tests/types/database-runtime.test.ts +158 -0
  157. package/tests/types/database.test.ts +187 -0
  158. package/tests/types/tester.test.ts +174 -0
  159. package/tests/workflow/db-setup-runner.test.ts +211 -0
  160. package/tests/workflow/db-state-machine.test.ts +117 -0
  161. package/tests/workflow/tester.test.ts +401 -0
@@ -0,0 +1,560 @@
1
+ /**
2
+ * Admin Wizard React frontend templates
3
+ * Generates React components for the database setup wizard UI
4
+ */
5
+
6
+ /**
7
+ * Generate useAdminApi custom hook for authenticated API calls
8
+ *
9
+ * @returns TypeScript source for useAdminApi.ts
10
+ */
11
+ export function generateUseAdminApiHook(): string {
12
+ return `/**
13
+ * Custom hook for admin API calls with token authentication.
14
+ */
15
+
16
+ interface ApiOptions {
17
+ method?: string;
18
+ body?: unknown;
19
+ }
20
+
21
+ interface ApiResult<T = unknown> {
22
+ data: T | null;
23
+ error: string | null;
24
+ }
25
+
26
+ export function useAdminApi() {
27
+ const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
28
+ const adminToken = import.meta.env.VITE_ADMIN_TOKEN || '';
29
+
30
+ async function callApi<T = unknown>(
31
+ path: string,
32
+ options: ApiOptions = {}
33
+ ): Promise<ApiResult<T>> {
34
+ const { method = 'GET', body } = options;
35
+ try {
36
+ const response = await fetch(\`\${apiUrl}\${path}\`, {
37
+ method,
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'X-Admin-Token': adminToken,
41
+ },
42
+ body: body ? JSON.stringify(body) : undefined,
43
+ });
44
+
45
+ if (!response.ok) {
46
+ const text = await response.text();
47
+ return { data: null, error: text || \`HTTP \${response.status}\` };
48
+ }
49
+
50
+ const data = (await response.json()) as T;
51
+ return { data, error: null };
52
+ } catch (err) {
53
+ const message = err instanceof Error ? err.message : 'Unknown error';
54
+ return { data: null, error: message };
55
+ }
56
+ }
57
+
58
+ return { callApi };
59
+ }
60
+ `;
61
+ }
62
+
63
+ /**
64
+ * Generate DbStatusBanner component that polls DB status and shows setup prompt
65
+ *
66
+ * @returns TypeScript/React source for DbStatusBanner.tsx
67
+ */
68
+ export function generateDbStatusBanner(): string {
69
+ return `import { useState, useEffect } from 'react';
70
+ import { useAdminApi } from './useAdminApi';
71
+
72
+ interface DbStatus {
73
+ status: string;
74
+ mode: string;
75
+ lastError: string | null;
76
+ migrationsApplied: number;
77
+ dbUrlConfigured: boolean;
78
+ }
79
+
80
+ interface DbStatusBannerProps {
81
+ onSetupClick: () => void;
82
+ }
83
+
84
+ /**
85
+ * Banner that polls the database status on mount.
86
+ * Hidden when status is "ready". Shows an amber bar when
87
+ * the database is unconfigured or in error state.
88
+ */
89
+ export function DbStatusBanner({ onSetupClick }: DbStatusBannerProps) {
90
+ const { callApi } = useAdminApi();
91
+ const [status, setStatus] = useState<DbStatus | null>(null);
92
+ const [loading, setLoading] = useState(true);
93
+
94
+ useEffect(() => {
95
+ let cancelled = false;
96
+
97
+ async function fetchStatus() {
98
+ const result = await callApi<DbStatus>('/api/admin/db/status');
99
+ if (!cancelled) {
100
+ setStatus(result.data);
101
+ setLoading(false);
102
+ }
103
+ }
104
+
105
+ fetchStatus();
106
+ return () => { cancelled = true; };
107
+ }, []);
108
+
109
+ if (loading) return null;
110
+ if (!status) return null;
111
+ if (status.status === 'ready') return null;
112
+
113
+ return (
114
+ <div className="bg-amber-50 border-b border-amber-200 px-4 py-3 flex items-center justify-between">
115
+ <div className="flex items-center gap-2">
116
+ <span className="inline-block w-2 h-2 rounded-full bg-amber-400" />
117
+ <span className="text-amber-800 text-sm font-medium">
118
+ {status.status === 'error'
119
+ ? \`Database error: \${status.lastError || 'Unknown'}\`
120
+ : 'Database is not configured'}
121
+ </span>
122
+ </div>
123
+ <button
124
+ onClick={onSetupClick}
125
+ className="bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium px-4 py-1.5 rounded transition-colors"
126
+ >
127
+ Set up database
128
+ </button>
129
+ </div>
130
+ );
131
+ }
132
+ `;
133
+ }
134
+
135
+ /**
136
+ * Generate ConnectionForm component for testing DB URLs
137
+ *
138
+ * @returns TypeScript/React source for ConnectionForm.tsx
139
+ */
140
+ export function generateConnectionForm(): string {
141
+ return `import { useState } from 'react';
142
+ import { useAdminApi } from './useAdminApi';
143
+
144
+ interface ConnectionFormProps {
145
+ onTestSuccess: (url: string) => void;
146
+ onBack: () => void;
147
+ }
148
+
149
+ /**
150
+ * Form to enter a DATABASE_URL and test the connection.
151
+ * Calls POST /api/admin/db/test and reports success/failure.
152
+ */
153
+ export function ConnectionForm({ onTestSuccess, onBack }: ConnectionFormProps) {
154
+ const { callApi } = useAdminApi();
155
+ const [url, setUrl] = useState('');
156
+ const [testing, setTesting] = useState(false);
157
+ const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
158
+
159
+ async function handleTest() {
160
+ if (!url.trim()) return;
161
+ setTesting(true);
162
+ setResult(null);
163
+
164
+ const res = await callApi<{ success: boolean; message: string }>(
165
+ '/api/admin/db/test',
166
+ { method: 'POST', body: { database_url: url } }
167
+ );
168
+
169
+ setTesting(false);
170
+
171
+ if (res.data) {
172
+ setResult(res.data);
173
+ if (res.data.success) {
174
+ onTestSuccess(url);
175
+ }
176
+ } else {
177
+ setResult({ success: false, message: res.error || 'Connection failed' });
178
+ }
179
+ }
180
+
181
+ return (
182
+ <div className="space-y-4">
183
+ <div>
184
+ <label htmlFor="db-url" className="block text-sm font-medium text-gray-700 mb-1">
185
+ DATABASE_URL
186
+ </label>
187
+ <input
188
+ id="db-url"
189
+ type="text"
190
+ value={url}
191
+ onChange={(e) => setUrl(e.target.value)}
192
+ placeholder="postgresql://user:pass@host:5432/dbname"
193
+ className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
194
+ />
195
+ </div>
196
+
197
+ {result && (
198
+ <div className={\`text-sm p-3 rounded \${result.success ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}\`}>
199
+ {result.message}
200
+ </div>
201
+ )}
202
+
203
+ <div className="flex gap-3">
204
+ <button
205
+ onClick={onBack}
206
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
207
+ >
208
+ Back
209
+ </button>
210
+ <button
211
+ onClick={handleTest}
212
+ disabled={testing || !url.trim()}
213
+ className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
214
+ >
215
+ {testing ? 'Testing...' : 'Test Connection'}
216
+ </button>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+ `;
222
+ }
223
+
224
+ /**
225
+ * Generate MigrationProgress component that polls status during apply
226
+ *
227
+ * @returns TypeScript/React source for MigrationProgress.tsx
228
+ */
229
+ export function generateMigrationProgress(): string {
230
+ return `import { useState, useEffect, useRef } from 'react';
231
+ import { useAdminApi } from './useAdminApi';
232
+
233
+ interface StepResult {
234
+ step: string;
235
+ success: boolean;
236
+ message: string;
237
+ }
238
+
239
+ interface MigrationProgressProps {
240
+ databaseUrl: string;
241
+ onComplete: () => void;
242
+ onError: (msg: string) => void;
243
+ }
244
+
245
+ /**
246
+ * Runs POST /api/admin/db/apply then polls GET /api/admin/db/status
247
+ * every 2 seconds while in "applying" state.
248
+ * Shows a step-by-step progress list.
249
+ */
250
+ export function MigrationProgress({ databaseUrl, onComplete, onError }: MigrationProgressProps) {
251
+ const { callApi } = useAdminApi();
252
+ const [steps, setSteps] = useState<StepResult[]>([]);
253
+ const [applying, setApplying] = useState(true);
254
+ const started = useRef(false);
255
+
256
+ useEffect(() => {
257
+ if (started.current) return;
258
+ started.current = true;
259
+
260
+ async function runApply() {
261
+ const res = await callApi<{ steps: StepResult[]; status: string }>(
262
+ '/api/admin/db/apply',
263
+ { method: 'POST', body: { database_url: databaseUrl } }
264
+ );
265
+
266
+ if (res.data) {
267
+ setSteps(res.data.steps);
268
+ if (res.data.status === 'ready') {
269
+ setApplying(false);
270
+ onComplete();
271
+ } else if (res.data.status === 'error') {
272
+ setApplying(false);
273
+ const failedStep = res.data.steps.find((s) => !s.success);
274
+ onError(failedStep?.message || 'Setup failed');
275
+ }
276
+ } else {
277
+ setApplying(false);
278
+ onError(res.error || 'Failed to apply setup');
279
+ }
280
+ }
281
+
282
+ runApply();
283
+ }, [databaseUrl]);
284
+
285
+ useEffect(() => {
286
+ if (!applying) return;
287
+
288
+ const interval = setInterval(async () => {
289
+ const res = await callApi<{ status: string }>('/api/admin/db/status');
290
+ if (res.data) {
291
+ if (res.data.status === 'ready') {
292
+ setApplying(false);
293
+ onComplete();
294
+ } else if (res.data.status === 'error') {
295
+ setApplying(false);
296
+ }
297
+ }
298
+ }, 2000);
299
+
300
+ return () => clearInterval(interval);
301
+ }, [applying]);
302
+
303
+ return (
304
+ <div className="space-y-3">
305
+ <h3 className="text-sm font-semibold text-gray-700">Applying Setup</h3>
306
+ <ul className="space-y-2">
307
+ {steps.map((step, i) => (
308
+ <li key={i} className="flex items-center gap-2 text-sm">
309
+ <span className={\`inline-block w-4 h-4 rounded-full flex items-center justify-center text-xs \${
310
+ step.success ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'
311
+ }\`}>
312
+ {step.success ? '\\u2713' : '\\u2717'}
313
+ </span>
314
+ <span className={step.success ? 'text-gray-700' : 'text-red-700'}>
315
+ {step.message}
316
+ </span>
317
+ </li>
318
+ ))}
319
+ {applying && (
320
+ <li className="flex items-center gap-2 text-sm text-gray-500">
321
+ <span className="inline-block w-4 h-4 rounded-full bg-blue-100 animate-pulse" />
322
+ Running...
323
+ </li>
324
+ )}
325
+ </ul>
326
+ </div>
327
+ );
328
+ }
329
+ `;
330
+ }
331
+
332
+ /**
333
+ * Generate DbSetupStepper component - multi-step wizard overlay
334
+ *
335
+ * @returns TypeScript/React source for DbSetupStepper.tsx
336
+ */
337
+ export function generateDbSetupStepper(): string {
338
+ return `import { useState } from 'react';
339
+ import { ConnectionForm } from './ConnectionForm';
340
+ import { MigrationProgress } from './MigrationProgress';
341
+
342
+ type WizardStep = 'choose' | 'credentials' | 'apply' | 'ready';
343
+
344
+ interface DbSetupStepperProps {
345
+ onClose: () => void;
346
+ }
347
+
348
+ const STEP_LABELS: Record<WizardStep, string> = {
349
+ choose: 'Setup Mode',
350
+ credentials: 'Connection',
351
+ apply: 'Applying',
352
+ ready: 'Complete',
353
+ };
354
+
355
+ const STEP_ORDER: WizardStep[] = ['choose', 'credentials', 'apply', 'ready'];
356
+
357
+ /**
358
+ * Multi-step database setup wizard rendered as a modal overlay.
359
+ * State machine: choose -> credentials -> apply -> ready
360
+ */
361
+ export function DbSetupStepper({ onClose }: DbSetupStepperProps) {
362
+ const [step, setStep] = useState<WizardStep>('choose');
363
+ const [dbUrl, setDbUrl] = useState('');
364
+ const [errorMsg, setErrorMsg] = useState('');
365
+
366
+ const currentIndex = STEP_ORDER.indexOf(step);
367
+
368
+ return (
369
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
370
+ {/* Backdrop */}
371
+ <div className="absolute inset-0 bg-black/50" onClick={onClose} />
372
+
373
+ {/* Panel */}
374
+ <div className="relative bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6">
375
+ {/* Close button */}
376
+ <button
377
+ onClick={onClose}
378
+ className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 text-xl leading-none"
379
+ aria-label="Close"
380
+ >
381
+ \\u00d7
382
+ </button>
383
+
384
+ <h2 className="text-lg font-bold text-gray-900 mb-4">Database Setup</h2>
385
+
386
+ {/* Step indicator */}
387
+ <div className="flex gap-1 mb-6">
388
+ {STEP_ORDER.map((s, i) => (
389
+ <div key={s} className="flex-1">
390
+ <div className={\`h-1.5 rounded-full \${
391
+ i <= currentIndex ? 'bg-blue-500' : 'bg-gray-200'
392
+ }\`} />
393
+ <span className={\`block text-xs mt-1 \${
394
+ i === currentIndex ? 'text-blue-600 font-medium' : 'text-gray-400'
395
+ }\`}>
396
+ {STEP_LABELS[s]}
397
+ </span>
398
+ </div>
399
+ ))}
400
+ </div>
401
+
402
+ {/* Step content */}
403
+ {step === 'choose' && (
404
+ <div className="space-y-4">
405
+ <p className="text-sm text-gray-600">
406
+ Configure your PostgreSQL database to enable data persistence.
407
+ </p>
408
+ <button
409
+ onClick={() => setStep('credentials')}
410
+ className="w-full px-4 py-3 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
411
+ >
412
+ Enter connection details
413
+ </button>
414
+ </div>
415
+ )}
416
+
417
+ {step === 'credentials' && (
418
+ <ConnectionForm
419
+ onTestSuccess={(url) => {
420
+ setDbUrl(url);
421
+ setStep('apply');
422
+ }}
423
+ onBack={() => setStep('choose')}
424
+ />
425
+ )}
426
+
427
+ {step === 'apply' && (
428
+ <MigrationProgress
429
+ databaseUrl={dbUrl}
430
+ onComplete={() => setStep('ready')}
431
+ onError={(msg) => setErrorMsg(msg)}
432
+ />
433
+ )}
434
+
435
+ {step === 'ready' && (
436
+ <div className="text-center space-y-4">
437
+ <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100">
438
+ <span className="text-green-600 text-2xl">\\u2713</span>
439
+ </div>
440
+ <p className="text-gray-700 font-medium">Database is ready!</p>
441
+ <button
442
+ onClick={onClose}
443
+ className="px-6 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
444
+ >
445
+ Done
446
+ </button>
447
+ </div>
448
+ )}
449
+
450
+ {errorMsg && step === 'apply' && (
451
+ <div className="mt-4 p-3 bg-red-50 text-red-700 text-sm rounded">
452
+ {errorMsg}
453
+ </div>
454
+ )}
455
+ </div>
456
+ </div>
457
+ );
458
+ }
459
+ `;
460
+ }
461
+
462
+ /**
463
+ * Generate admin barrel export index
464
+ *
465
+ * @returns TypeScript source for admin/index.ts
466
+ */
467
+ export function generateAdminIndex(): string {
468
+ return `/**
469
+ * Admin wizard components barrel export.
470
+ */
471
+
472
+ export { DbStatusBanner } from './DbStatusBanner';
473
+ export { DbSetupStepper } from './DbSetupStepper';
474
+ `;
475
+ }
476
+
477
+ /**
478
+ * Generate App.tsx that includes DbStatusBanner and DbSetupStepper
479
+ *
480
+ * @param projectName - Human-readable project name
481
+ * @returns TypeScript/React source for App.tsx
482
+ */
483
+ export function generateAppTsxWithAdmin(projectName: string): string {
484
+ return `import { useState, useEffect } from 'react';
485
+ import { DbStatusBanner } from './admin';
486
+ import { DbSetupStepper } from './admin';
487
+
488
+ interface HealthStatus {
489
+ status: string;
490
+ message: string;
491
+ }
492
+
493
+ function App() {
494
+ const [health, setHealth] = useState<HealthStatus | null>(null);
495
+ const [loading, setLoading] = useState(true);
496
+ const [error, setError] = useState<string | null>(null);
497
+ const [showWizard, setShowWizard] = useState(false);
498
+
499
+ useEffect(() => {
500
+ const checkHealth = async () => {
501
+ try {
502
+ const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
503
+ const response = await fetch(\`\${apiUrl}/health\`);
504
+ if (response.ok) {
505
+ const data = await response.json();
506
+ setHealth(data);
507
+ } else {
508
+ setError('Backend not responding');
509
+ }
510
+ } catch (err) {
511
+ setError('Failed to connect to backend');
512
+ } finally {
513
+ setLoading(false);
514
+ }
515
+ };
516
+
517
+ checkHealth();
518
+ }, []);
519
+
520
+ return (
521
+ <div className="min-h-screen flex flex-col">
522
+ <DbStatusBanner onSetupClick={() => setShowWizard(true)} />
523
+
524
+ <div className="flex-1 flex items-center justify-center">
525
+ <div className="text-center p-8">
526
+ <h1 className="text-4xl font-bold text-primary-600 mb-4">
527
+ ${projectName}
528
+ </h1>
529
+ <p className="text-gray-600 mb-8">
530
+ Fullstack application with React + FastAPI
531
+ </p>
532
+
533
+ <div className="bg-white rounded-lg shadow-md p-6">
534
+ <h2 className="text-lg font-semibold mb-2">Backend Status</h2>
535
+ {loading && (
536
+ <p className="text-gray-500">Checking...</p>
537
+ )}
538
+ {error && (
539
+ <p className="text-red-500">{error}</p>
540
+ )}
541
+ {health && (
542
+ <div className="text-green-500">
543
+ <p>Status: {health.status}</p>
544
+ <p className="text-sm text-gray-500">{health.message}</p>
545
+ </div>
546
+ )}
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ {showWizard && (
552
+ <DbSetupStepper onClose={() => setShowWizard(false)} />
553
+ )}
554
+ </div>
555
+ );
556
+ }
557
+
558
+ export default App;
559
+ `;
560
+ }