@mg21st/dev-assist 1.0.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 (65) hide show
  1. package/.eslintrc.json +17 -0
  2. package/.github/workflows/ci.yml +42 -0
  3. package/.github/workflows/docs.yml +49 -0
  4. package/.github/workflows/publish.yml +49 -0
  5. package/README.md +117 -0
  6. package/bin/dev-assist.js +4 -0
  7. package/dev-assist.config.js +10 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +133 -0
  11. package/dist/cli/wizard.d.ts +5 -0
  12. package/dist/cli/wizard.d.ts.map +1 -0
  13. package/dist/cli/wizard.js +66 -0
  14. package/dist/config.d.ts +3 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +62 -0
  17. package/dist/generators/docsGenerator.d.ts +15 -0
  18. package/dist/generators/docsGenerator.d.ts.map +1 -0
  19. package/dist/generators/docsGenerator.js +186 -0
  20. package/dist/generators/testGenerator.d.ts +12 -0
  21. package/dist/generators/testGenerator.d.ts.map +1 -0
  22. package/dist/generators/testGenerator.js +185 -0
  23. package/dist/parser/astParser.d.ts +7 -0
  24. package/dist/parser/astParser.d.ts.map +1 -0
  25. package/dist/parser/astParser.js +194 -0
  26. package/dist/server/index.d.ts +5 -0
  27. package/dist/server/index.d.ts.map +1 -0
  28. package/dist/server/index.js +247 -0
  29. package/dist/shared/types.d.ts +77 -0
  30. package/dist/shared/types.d.ts.map +1 -0
  31. package/dist/shared/types.js +3 -0
  32. package/docs/_config.yml +22 -0
  33. package/docs/api-reference.md +173 -0
  34. package/docs/architecture.md +90 -0
  35. package/docs/configuration.md +52 -0
  36. package/docs/contributing.md +101 -0
  37. package/docs/index.md +50 -0
  38. package/docs/installation.md +95 -0
  39. package/docs/usage.md +107 -0
  40. package/package.json +58 -0
  41. package/src/cli/index.ts +108 -0
  42. package/src/cli/wizard.ts +63 -0
  43. package/src/config.ts +29 -0
  44. package/src/generators/docsGenerator.ts +192 -0
  45. package/src/generators/testGenerator.ts +174 -0
  46. package/src/parser/astParser.ts +172 -0
  47. package/src/server/index.ts +238 -0
  48. package/src/shared/types.ts +83 -0
  49. package/tsconfig.build.json +8 -0
  50. package/tsconfig.json +19 -0
  51. package/ui/index.html +13 -0
  52. package/ui/package-lock.json +3086 -0
  53. package/ui/package.json +31 -0
  54. package/ui/postcss.config.js +6 -0
  55. package/ui/src/App.tsx +36 -0
  56. package/ui/src/components/ApiDocsTab.tsx +184 -0
  57. package/ui/src/components/ApiTestingTab.tsx +363 -0
  58. package/ui/src/components/Dashboard.tsx +128 -0
  59. package/ui/src/components/Layout.tsx +76 -0
  60. package/ui/src/components/TestsTab.tsx +149 -0
  61. package/ui/src/main.tsx +10 -0
  62. package/ui/src/styles/index.css +41 -0
  63. package/ui/tailwind.config.js +20 -0
  64. package/ui/tsconfig.json +19 -0
  65. package/ui/vite.config.ts +19 -0
@@ -0,0 +1,128 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import axios from 'axios';
3
+ import { FileCode2, FunctionSquare, Route, TestTube2, RefreshCw } from 'lucide-react';
4
+
5
+ interface Summary {
6
+ totalFiles: number;
7
+ totalFunctions: number;
8
+ totalRoutes: number;
9
+ totalTests: number;
10
+ generatedAt: string;
11
+ }
12
+
13
+ interface StatCardProps {
14
+ label: string;
15
+ value: number;
16
+ icon: React.ReactNode;
17
+ color: string;
18
+ }
19
+
20
+ function StatCard({ label, value, icon, color }: StatCardProps) {
21
+ return (
22
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center gap-4">
23
+ <div className={`p-3 rounded-lg ${color}`}>
24
+ {icon}
25
+ </div>
26
+ <div>
27
+ <p className="text-2xl font-bold">{value}</p>
28
+ <p className="text-sm text-gray-500 dark:text-gray-400">{label}</p>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ export function Dashboard() {
35
+ const [summary, setSummary] = useState<Summary | null>(null);
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState<string | null>(null);
38
+
39
+ const fetchSummary = useCallback(async () => {
40
+ try {
41
+ setLoading(true);
42
+ setError(null);
43
+ const { data } = await axios.get<Summary>('/api/summary');
44
+ setSummary(data);
45
+ } catch {
46
+ setError('Failed to fetch project summary. Make sure the server is running.');
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }, []);
51
+
52
+ useEffect(() => { fetchSummary(); }, [fetchSummary]);
53
+
54
+ return (
55
+ <div className="space-y-6">
56
+ <div className="flex items-center justify-between">
57
+ <div>
58
+ <h1 className="text-2xl font-bold">Dashboard</h1>
59
+ <p className="text-gray-500 dark:text-gray-400 text-sm mt-1">Project overview and statistics</p>
60
+ </div>
61
+ <button
62
+ onClick={fetchSummary}
63
+ className="flex items-center gap-2 px-4 py-2 bg-sky-500 text-white rounded-lg hover:bg-sky-600 transition-colors text-sm font-medium"
64
+ >
65
+ <RefreshCw size={15} className={loading ? 'animate-spin' : ''} />
66
+ Refresh
67
+ </button>
68
+ </div>
69
+
70
+ {error && (
71
+ <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
72
+ {error}
73
+ </div>
74
+ )}
75
+
76
+ {loading && !summary ? (
77
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
78
+ {[...Array(4)].map((_, i) => (
79
+ <div key={i} className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700 h-24 animate-pulse" />
80
+ ))}
81
+ </div>
82
+ ) : summary ? (
83
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
84
+ <StatCard label="Files Scanned" value={summary.totalFiles} icon={<FileCode2 size={20} className="text-blue-500" />} color="bg-blue-50 dark:bg-blue-900/30" />
85
+ <StatCard label="Functions Found" value={summary.totalFunctions} icon={<FunctionSquare size={20} className="text-purple-500" />} color="bg-purple-50 dark:bg-purple-900/30" />
86
+ <StatCard label="API Routes" value={summary.totalRoutes} icon={<Route size={20} className="text-green-500" />} color="bg-green-50 dark:bg-green-900/30" />
87
+ <StatCard label="Tests Generated" value={summary.totalTests} icon={<TestTube2 size={20} className="text-orange-500" />} color="bg-orange-50 dark:bg-orange-900/30" />
88
+ </div>
89
+ ) : null}
90
+
91
+ {summary && (
92
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
93
+ <h2 className="font-semibold mb-3">Project Information</h2>
94
+ <div className="space-y-2 text-sm">
95
+ <div className="flex justify-between">
96
+ <span className="text-gray-500 dark:text-gray-400">Last scanned</span>
97
+ <span>{new Date(summary.generatedAt).toLocaleString()}</span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
104
+ <h2 className="font-semibold mb-3">Quick Actions</h2>
105
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
106
+ <button
107
+ onClick={() => axios.post('/api/generate/tests', {}).then(fetchSummary)}
108
+ className="p-3 border-2 border-dashed border-sky-300 dark:border-sky-700 rounded-lg text-sky-600 dark:text-sky-400 hover:border-sky-500 hover:bg-sky-50 dark:hover:bg-sky-900/20 transition-colors text-sm font-medium"
109
+ >
110
+ 🧪 Generate Tests
111
+ </button>
112
+ <button
113
+ onClick={() => axios.post('/api/generate/docs', {}).then(fetchSummary)}
114
+ className="p-3 border-2 border-dashed border-green-300 dark:border-green-700 rounded-lg text-green-600 dark:text-green-400 hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors text-sm font-medium"
115
+ >
116
+ 📚 Generate API Docs
117
+ </button>
118
+ <button
119
+ onClick={fetchSummary}
120
+ className="p-3 border-2 border-dashed border-purple-300 dark:border-purple-700 rounded-lg text-purple-600 dark:text-purple-400 hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-colors text-sm font-medium"
121
+ >
122
+ 🔄 Re-scan Project
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { TabId } from '../App';
3
+ import {
4
+ LayoutDashboard,
5
+ TestTube2,
6
+ BookOpen,
7
+ Zap,
8
+ Moon,
9
+ Sun,
10
+ Terminal
11
+ } from 'lucide-react';
12
+
13
+ interface LayoutProps {
14
+ children: React.ReactNode;
15
+ activeTab: TabId;
16
+ onTabChange: (tab: TabId) => void;
17
+ darkMode: boolean;
18
+ onToggleDark: () => void;
19
+ }
20
+
21
+ const tabs = [
22
+ { id: 'dashboard' as TabId, label: 'Dashboard', icon: LayoutDashboard },
23
+ { id: 'tests' as TabId, label: 'Tests', icon: TestTube2 },
24
+ { id: 'docs' as TabId, label: 'API Docs', icon: BookOpen },
25
+ { id: 'testing' as TabId, label: 'API Testing', icon: Zap },
26
+ ];
27
+
28
+ export function Layout({ children, activeTab, onTabChange, darkMode, onToggleDark }: LayoutProps) {
29
+ return (
30
+ <div className={`min-h-screen flex flex-col ${darkMode ? 'dark bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
31
+ <header className={`h-14 flex items-center justify-between px-6 border-b ${darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'} shadow-sm`}>
32
+ <div className="flex items-center gap-3">
33
+ <Terminal className="text-sky-500" size={22} />
34
+ <span className="font-bold text-lg tracking-tight">DevAssist</span>
35
+ <span className={`text-xs px-2 py-0.5 rounded-full ${darkMode ? 'bg-sky-900 text-sky-300' : 'bg-sky-100 text-sky-700'}`}>v1.0.0</span>
36
+ </div>
37
+ <button
38
+ onClick={onToggleDark}
39
+ className={`p-2 rounded-lg transition-colors ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
40
+ title="Toggle dark mode"
41
+ >
42
+ {darkMode ? <Sun size={18} /> : <Moon size={18} />}
43
+ </button>
44
+ </header>
45
+
46
+ <div className="flex flex-1">
47
+ <aside className={`w-56 flex-shrink-0 border-r ${darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
48
+ <nav className="p-3 space-y-1 pt-4">
49
+ {tabs.map(({ id, label, icon: Icon }) => (
50
+ <button
51
+ key={id}
52
+ onClick={() => onTabChange(id)}
53
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
54
+ activeTab === id
55
+ ? 'bg-sky-500 text-white shadow-md'
56
+ : darkMode
57
+ ? 'text-gray-300 hover:bg-gray-700'
58
+ : 'text-gray-600 hover:bg-gray-100'
59
+ }`}
60
+ >
61
+ <Icon size={17} />
62
+ {label}
63
+ </button>
64
+ ))}
65
+ </nav>
66
+ </aside>
67
+
68
+ <main className="flex-1 overflow-auto p-6">
69
+ <div className="fade-in">
70
+ {children}
71
+ </div>
72
+ </main>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,149 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import axios from 'axios';
3
+ import { TestTube2, Download, RefreshCw, FileCode2, ChevronRight, Loader } from 'lucide-react';
4
+
5
+ interface TestFile {
6
+ filePath: string;
7
+ content: string;
8
+ }
9
+
10
+ export function TestsTab() {
11
+ const [tests, setTests] = useState<TestFile[]>([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [generating, setGenerating] = useState(false);
14
+ const [selectedTest, setSelectedTest] = useState<TestFile | null>(null);
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ const fetchTests = useCallback(async () => {
18
+ try {
19
+ setLoading(true);
20
+ const { data } = await axios.get<TestFile[]>('/api/tests');
21
+ setTests(data);
22
+ if (data.length > 0 && !selectedTest) setSelectedTest(data[0]);
23
+ } catch {
24
+ setError('Failed to fetch tests');
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ }, [selectedTest]);
29
+
30
+ const generateTests = async () => {
31
+ try {
32
+ setGenerating(true);
33
+ setError(null);
34
+ await axios.post('/api/generate/tests', {});
35
+ await fetchTests();
36
+ } catch {
37
+ setError('Failed to generate tests');
38
+ } finally {
39
+ setGenerating(false);
40
+ }
41
+ };
42
+
43
+ const downloadTest = (test: TestFile) => {
44
+ const blob = new Blob([test.content], { type: 'text/plain' });
45
+ const url = URL.createObjectURL(blob);
46
+ const a = document.createElement('a');
47
+ a.href = url;
48
+ a.download = test.filePath.split('/').pop() || 'test.ts';
49
+ a.click();
50
+ URL.revokeObjectURL(url);
51
+ };
52
+
53
+ useEffect(() => { fetchTests(); }, [fetchTests]);
54
+
55
+ return (
56
+ <div className="space-y-4">
57
+ <div className="flex items-center justify-between">
58
+ <div>
59
+ <h1 className="text-2xl font-bold">Generated Tests</h1>
60
+ <p className="text-gray-500 dark:text-gray-400 text-sm mt-1">{tests.length} test files</p>
61
+ </div>
62
+ <div className="flex gap-2">
63
+ <button
64
+ onClick={fetchTests}
65
+ className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm"
66
+ >
67
+ <RefreshCw size={14} />
68
+ Refresh
69
+ </button>
70
+ <button
71
+ onClick={generateTests}
72
+ disabled={generating}
73
+ className="flex items-center gap-2 px-4 py-2 bg-sky-500 text-white rounded-lg hover:bg-sky-600 disabled:opacity-50 transition-colors text-sm font-medium"
74
+ >
75
+ {generating ? <Loader size={14} className="animate-spin" /> : <TestTube2 size={14} />}
76
+ {generating ? 'Generating...' : 'Generate Tests'}
77
+ </button>
78
+ </div>
79
+ </div>
80
+
81
+ {error && (
82
+ <div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
83
+ {error}
84
+ </div>
85
+ )}
86
+
87
+ {loading ? (
88
+ <div className="flex items-center justify-center h-64">
89
+ <Loader className="animate-spin text-sky-500" size={32} />
90
+ </div>
91
+ ) : tests.length === 0 ? (
92
+ <div className="flex flex-col items-center justify-center h-64 text-gray-500 dark:text-gray-400">
93
+ <TestTube2 size={48} className="mb-4 opacity-30" />
94
+ <p className="text-lg font-medium">No tests generated yet</p>
95
+ <p className="text-sm mt-1">Click "Generate Tests" to create test stubs</p>
96
+ </div>
97
+ ) : (
98
+ <div className="grid grid-cols-3 gap-4 h-[calc(100vh-200px)]">
99
+ <div className="col-span-1 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-auto">
100
+ <div className="p-3 border-b border-gray-200 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400">
101
+ Test Files
102
+ </div>
103
+ <div className="divide-y divide-gray-100 dark:divide-gray-700">
104
+ {tests.map((test) => (
105
+ <button
106
+ key={test.filePath}
107
+ onClick={() => setSelectedTest(test)}
108
+ className={`w-full flex items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors ${
109
+ selectedTest?.filePath === test.filePath
110
+ ? 'bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300'
111
+ : 'hover:bg-gray-50 dark:hover:bg-gray-700'
112
+ }`}
113
+ >
114
+ <FileCode2 size={14} className="flex-shrink-0" />
115
+ <span className="truncate">{test.filePath.split('/').pop()}</span>
116
+ <ChevronRight size={12} className="ml-auto flex-shrink-0" />
117
+ </button>
118
+ ))}
119
+ </div>
120
+ </div>
121
+
122
+ <div className="col-span-2 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 flex flex-col">
123
+ {selectedTest ? (
124
+ <>
125
+ <div className="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
126
+ <span className="text-sm font-medium text-gray-600 dark:text-gray-300">{selectedTest.filePath}</span>
127
+ <button
128
+ onClick={() => downloadTest(selectedTest)}
129
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
130
+ >
131
+ <Download size={12} />
132
+ Download
133
+ </button>
134
+ </div>
135
+ <pre className="flex-1 overflow-auto p-4 text-xs font-mono text-gray-800 dark:text-gray-200 scrollbar-thin">
136
+ <code>{selectedTest.content}</code>
137
+ </pre>
138
+ </>
139
+ ) : (
140
+ <div className="flex items-center justify-center h-full text-gray-400">
141
+ Select a test file to view
142
+ </div>
143
+ )}
144
+ </div>
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles/index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
@@ -0,0 +1,41 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ body {
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
13
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
14
+ }
15
+
16
+ pre, code {
17
+ font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace;
18
+ }
19
+
20
+ .scrollbar-thin::-webkit-scrollbar {
21
+ width: 6px;
22
+ height: 6px;
23
+ }
24
+
25
+ .scrollbar-thin::-webkit-scrollbar-track {
26
+ background: transparent;
27
+ }
28
+
29
+ .scrollbar-thin::-webkit-scrollbar-thumb {
30
+ background: #4b5563;
31
+ border-radius: 3px;
32
+ }
33
+
34
+ .fade-in {
35
+ animation: fadeIn 0.3s ease-in-out;
36
+ }
37
+
38
+ @keyframes fadeIn {
39
+ from { opacity: 0; transform: translateY(4px); }
40
+ to { opacity: 1; transform: translateY(0); }
41
+ }
@@ -0,0 +1,20 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+ darkMode: 'class',
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ primary: {
9
+ 50: '#f0f9ff',
10
+ 100: '#e0f2fe',
11
+ 500: '#0ea5e9',
12
+ 600: '#0284c7',
13
+ 700: '#0369a1',
14
+ 900: '#0c4a6e',
15
+ },
16
+ },
17
+ },
18
+ },
19
+ plugins: [],
20
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": false
17
+ },
18
+ "include": ["src"]
19
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ outDir: 'dist',
8
+ emptyOutDir: true,
9
+ },
10
+ server: {
11
+ port: 5173,
12
+ proxy: {
13
+ '/api': {
14
+ target: 'http://localhost:3000',
15
+ changeOrigin: true,
16
+ },
17
+ },
18
+ },
19
+ });