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