@kozojs/cli 0.1.15 → 0.1.17

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 (2) hide show
  1. package/lib/index.js +225 -44
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -1384,7 +1384,7 @@ jobs:
1384
1384
  async function scaffoldFullstackProject(projectDir, projectName, kozoCoreDep, runtime, database, dbPort, auth, frontend, extras, template) {
1385
1385
  const hasDb = database !== "none";
1386
1386
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "routes"));
1387
- if (!hasDb) await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "data"));
1387
+ await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "data"));
1388
1388
  if (hasDb) await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "api", "src", "db"));
1389
1389
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, "apps", "web", "src", "lib"));
1390
1390
  await import_fs_extra.default.ensureDir(import_node_path.default.join(projectDir, ".vscode"));
@@ -1504,7 +1504,11 @@ const db = await createDb();
1504
1504
  const services = { db };
1505
1505
  ` : "\nconst services = {};\n";
1506
1506
  const authMiddleware = auth ? `
1507
- app.use('*', authenticateJWT({ secret: process.env.JWT_SECRET || 'change-me' }));
1507
+ // Protect resource routes with JWT (auth login route remains public)
1508
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me';
1509
+ app.use('*', authenticateJWT(JWT_SECRET, { prefix: '/api/users' }));
1510
+ app.use('*', authenticateJWT(JWT_SECRET, { prefix: '/api/posts' }));
1511
+ app.use('*', authenticateJWT(JWT_SECRET, { prefix: '/api/tasks' }));
1508
1512
  ` : "";
1509
1513
  await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "index.ts"), `import 'dotenv/config';
1510
1514
  import { createKozo } from '@kozojs/core';
@@ -1563,15 +1567,19 @@ export const tasks: z.infer<typeof TaskSchema>[] = [
1563
1567
  { id: '3', title: 'Deploy', completed: false, priority: 'low', createdAt: new Date().toISOString() },
1564
1568
  ];
1565
1569
  `);
1570
+ const authRoutesImport = auth ? `import { registerAuthRoutes } from './auth';
1571
+ ` : "";
1572
+ const authRoutesCall = auth ? ` registerAuthRoutes(app); // Public auth endpoint must register before JWT middleware
1573
+ ` : "";
1566
1574
  await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "index.ts"), `import type { Kozo } from '@kozojs/core';
1567
1575
  import { registerHealthRoutes } from './health';
1568
1576
  import { registerUserRoutes } from './users';
1569
1577
  import { registerPostRoutes } from './posts';
1570
1578
  import { registerTaskRoutes } from './tasks';
1571
1579
  import { registerToolRoutes } from './tools';
1572
-
1580
+ ${authRoutesImport}
1573
1581
  export function registerRoutes(app: Kozo) {
1574
- registerHealthRoutes(app);
1582
+ ${authRoutesCall} registerHealthRoutes(app);
1575
1583
  registerUserRoutes(app);
1576
1584
  registerPostRoutes(app);
1577
1585
  registerTaskRoutes(app);
@@ -1836,6 +1844,35 @@ export function registerToolRoutes(app: Kozo) {
1836
1844
  }));
1837
1845
  }
1838
1846
  `);
1847
+ if (auth) {
1848
+ await import_fs_extra.default.writeFile(import_node_path.default.join(apiDir, "src", "routes", "auth.ts"), `import type { Kozo } from '@kozojs/core';
1849
+ import { z } from 'zod';
1850
+ import { createJWT, UnauthorizedError } from '@kozojs/auth';
1851
+
1852
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me';
1853
+
1854
+ const DEMO_USERS = [
1855
+ { email: 'admin@demo.com', password: 'admin123', role: 'admin', name: 'Admin' },
1856
+ { email: 'user@demo.com', password: 'user123', role: 'user', name: 'User' },
1857
+ ];
1858
+
1859
+ export function registerAuthRoutes(app: Kozo) {
1860
+ app.post('/api/auth/login', {
1861
+ body: z.object({ email: z.string().email(), password: z.string() }),
1862
+ }, async (c) => {
1863
+ const { email, password } = c.body;
1864
+ const user = DEMO_USERS.find(u => u.email === email && u.password === password);
1865
+ if (!user) throw new UnauthorizedError('Invalid credentials');
1866
+ const token = await createJWT(
1867
+ { email: user.email, role: user.role, name: user.name },
1868
+ JWT_SECRET,
1869
+ { expiresIn: '24h' },
1870
+ );
1871
+ return { token, user: { email: user.email, role: user.role, name: user.name } };
1872
+ });
1873
+ }
1874
+ `);
1875
+ }
1839
1876
  }
1840
1877
  async function scaffoldFullstackWeb(projectDir, projectName, frontend, auth = false) {
1841
1878
  const webDir = import_node_path.default.join(projectDir, "apps", "web");
@@ -1891,11 +1928,6 @@ export default defineConfig({
1891
1928
  port: 5173,
1892
1929
  proxy: {
1893
1930
  '/api': { target: 'http://localhost:3000', changeOrigin: true },
1894
- '/health': { target: 'http://localhost:3000', changeOrigin: true },
1895
- '/auth': { target: 'http://localhost:3000', changeOrigin: true },
1896
- '/users': { target: 'http://localhost:3000', changeOrigin: true },
1897
- '/posts': { target: 'http://localhost:3000', changeOrigin: true },
1898
- '/stats': { target: 'http://localhost:3000', changeOrigin: true },
1899
1931
  },
1900
1932
  },
1901
1933
  });
@@ -1976,6 +2008,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
1976
2008
  await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "pages", "Dashboard.tsx"), generateDashboardPage());
1977
2009
  await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "pages", "Users.tsx"), generateUsersPage());
1978
2010
  await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "pages", "Posts.tsx"), generatePostsPage());
2011
+ await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "pages", "Tasks.tsx"), generateTasksPage());
1979
2012
  if (auth) {
1980
2013
  await import_fs_extra.default.writeFile(import_node_path.default.join(webDir, "src", "pages", "Login.tsx"), generateLoginPage());
1981
2014
  }
@@ -1984,16 +2017,18 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
1984
2017
  function generateAppTsx(projectName, auth) {
1985
2018
  const authImports = auth ? `import { getToken, clearToken } from './lib/api';` : "";
1986
2019
  const loginImport = auth ? `import Login from './pages/Login';` : "";
2020
+ const queryClientImport = auth ? `import { useQueryClient } from '@tanstack/react-query';` : "";
1987
2021
  return `import { useState${auth ? ", useEffect" : ""} } from 'react';
1988
- import { useQueryClient } from '@tanstack/react-query';
1989
- import { LayoutDashboard, Users, FileText, Server, LogOut } from 'lucide-react';
2022
+ ${queryClientImport}
2023
+ import { LayoutDashboard, Users, FileText, CheckSquare, Server${auth ? ", LogOut" : ""} } from 'lucide-react';
1990
2024
  ${authImports}
1991
2025
  ${loginImport}
1992
2026
  import Dashboard from './pages/Dashboard';
1993
2027
  import UsersPage from './pages/Users';
1994
2028
  import PostsPage from './pages/Posts';
2029
+ import TasksPage from './pages/Tasks';
1995
2030
 
1996
- type Page = 'dashboard' | 'users' | 'posts';
2031
+ type Page = 'dashboard' | 'users' | 'posts' | 'tasks';
1997
2032
 
1998
2033
  export default function App() {
1999
2034
  const [page, setPage] = useState<Page>('dashboard');
@@ -2010,12 +2045,12 @@ ${auth ? ` const [token, setToken] = useState<string | null>(() => getToken());
2010
2045
  };
2011
2046
 
2012
2047
  if (!token) return <Login onLogin={handleLogin} />;
2013
- ` : ` const queryClient = useQueryClient();
2014
- `}
2048
+ ` : ""}
2015
2049
  const nav = [
2016
2050
  { id: 'dashboard' as Page, label: 'Dashboard', icon: LayoutDashboard },
2017
2051
  { id: 'users' as Page, label: 'Users', icon: Users },
2018
2052
  { id: 'posts' as Page, label: 'Posts', icon: FileText },
2053
+ { id: 'tasks' as Page, label: 'Tasks', icon: CheckSquare },
2019
2054
  ];
2020
2055
 
2021
2056
  return (
@@ -2055,6 +2090,7 @@ ${auth ? ` <button
2055
2090
  {page === 'dashboard' && <Dashboard />}
2056
2091
  {page === 'users' && <UsersPage />}
2057
2092
  {page === 'posts' && <PostsPage />}
2093
+ {page === 'tasks' && <TasksPage />}
2058
2094
  </main>
2059
2095
  </div>
2060
2096
  );
@@ -2071,8 +2107,8 @@ interface Props {
2071
2107
  }
2072
2108
 
2073
2109
  export default function Login({ onLogin }: Props) {
2074
- const [email, setEmail] = useState('admin@kozo.app');
2075
- const [password, setPassword] = useState('password123');
2110
+ const [email, setEmail] = useState('admin@demo.com');
2111
+ const [password, setPassword] = useState('admin123');
2076
2112
  const [error, setError] = useState('');
2077
2113
  const [loading, setLoading] = useState(false);
2078
2114
 
@@ -2081,7 +2117,7 @@ export default function Login({ onLogin }: Props) {
2081
2117
  setError('');
2082
2118
  setLoading(true);
2083
2119
  try {
2084
- const res = await apiFetch<{ token: string }>('/auth/login', {
2120
+ const res = await apiFetch<{ token: string }>('/api/auth/login', {
2085
2121
  method: 'POST',
2086
2122
  body: JSON.stringify({ email, password }),
2087
2123
  });
@@ -2149,15 +2185,15 @@ export default function Login({ onLogin }: Props) {
2149
2185
  function generateDashboardPage() {
2150
2186
  return `import { useQuery } from '@tanstack/react-query';
2151
2187
  import { apiFetch } from '../lib/api';
2152
- import { Users, FileText, Activity, Zap } from 'lucide-react';
2188
+ import { Users, FileText, CheckSquare, Zap } from 'lucide-react';
2153
2189
 
2190
+ interface Health { status: string; uptime: number; version: string; }
2154
2191
  interface Stats {
2155
- users?: { total: number; admins?: number; regular?: number };
2156
- posts?: { total: number; published?: number; drafts?: number };
2157
- performance?: { uptime: number; memoryUsage?: { heapUsed: number } };
2158
- // fullstack format
2159
- timestamp?: string;
2160
- version?: string;
2192
+ users: number;
2193
+ posts: number;
2194
+ tasks: number;
2195
+ publishedPosts: number;
2196
+ completedTasks: number;
2161
2197
  }
2162
2198
 
2163
2199
  function StatCard({ label, value, sub, icon: Icon, color }: {
@@ -2183,15 +2219,15 @@ function StatCard({ label, value, sub, icon: Icon, color }: {
2183
2219
  export default function Dashboard() {
2184
2220
  const { data: health } = useQuery({
2185
2221
  queryKey: ['health'],
2186
- queryFn: () => apiFetch<{ status: string; uptime: number; version: string }>('/health').then(r => r.data),
2222
+ queryFn: () => apiFetch<Health>('/api/health').then(r => r.data),
2187
2223
  });
2188
2224
 
2189
2225
  const { data: stats, isLoading } = useQuery({
2190
2226
  queryKey: ['stats'],
2191
- queryFn: () => apiFetch<Stats>('/stats').then(r => r.data),
2227
+ queryFn: () => apiFetch<Stats>('/api/stats').then(r => r.data),
2192
2228
  });
2193
2229
 
2194
- const uptime = health?.uptime ?? stats?.performance?.uptime;
2230
+ const uptime = health?.uptime;
2195
2231
  const uptimeStr = uptime !== undefined
2196
2232
  ? uptime > 3600 ? \`\${Math.floor(uptime / 3600)}h \${Math.floor((uptime % 3600) / 60)}m\`
2197
2233
  : uptime > 60 ? \`\${Math.floor(uptime / 60)}m \${Math.floor(uptime % 60)}s\`
@@ -2211,24 +2247,24 @@ export default function Dashboard() {
2211
2247
  <div className="grid grid-cols-2 gap-4 mb-8">
2212
2248
  <StatCard
2213
2249
  label="Users"
2214
- value={stats?.users?.total ?? '\u2014'}
2215
- sub={stats?.users ? \`\${stats.users.admins ?? 0} admins \xB7 \${stats.users.regular ?? 0} regular\` : undefined}
2250
+ value={stats?.users ?? '\u2014'}
2216
2251
  icon={Users} color="bg-blue-600"
2217
2252
  />
2218
2253
  <StatCard
2219
2254
  label="Posts"
2220
- value={stats?.posts?.total ?? '\u2014'}
2221
- sub={stats?.posts ? \`\${stats.posts.published ?? 0} published \xB7 \${stats.posts.drafts ?? 0} drafts\` : undefined}
2255
+ value={stats?.posts ?? '\u2014'}
2256
+ sub={stats ? \`\${stats.publishedPosts} published\` : undefined}
2222
2257
  icon={FileText} color="bg-purple-600"
2223
2258
  />
2224
2259
  <StatCard
2225
- label="Uptime"
2226
- value={uptimeStr}
2227
- icon={Activity} color="bg-emerald-600"
2260
+ label="Tasks"
2261
+ value={stats?.tasks ?? '\u2014'}
2262
+ sub={stats ? \`\${stats.completedTasks} completed\` : undefined}
2263
+ icon={CheckSquare} color="bg-emerald-600"
2228
2264
  />
2229
2265
  <StatCard
2230
- label="API Status"
2231
- value={health?.status === 'ok' ? '\u2713 Online' : '? Unknown'}
2266
+ label="Uptime"
2267
+ value={uptimeStr}
2232
2268
  sub={health?.version ? \`v\${health.version}\` : undefined}
2233
2269
  icon={Zap} color="bg-amber-600"
2234
2270
  />
@@ -2236,10 +2272,10 @@ export default function Dashboard() {
2236
2272
  )}
2237
2273
 
2238
2274
  <div className="bg-slate-800 rounded-xl border border-slate-700 p-4">
2239
- <h3 className="text-sm font-medium text-slate-300 mb-3">Quick API Test</h3>
2275
+ <h3 className="text-sm font-medium text-slate-300 mb-3">API Status</h3>
2240
2276
  <div className="flex items-center gap-2">
2241
2277
  <div className="flex-1 px-3 py-2 bg-slate-900 rounded-lg text-sm text-slate-400 font-mono">
2242
- GET /health
2278
+ GET /api/health
2243
2279
  </div>
2244
2280
  <span className={\`px-2 py-0.5 rounded text-xs font-medium \${
2245
2281
  health?.status === 'ok' ? 'bg-emerald-900 text-emerald-300' : 'bg-slate-700 text-slate-400'
@@ -2276,7 +2312,7 @@ export default function UsersPage() {
2276
2312
  const { data: users = [], isLoading } = useQuery({
2277
2313
  queryKey: ['users'],
2278
2314
  queryFn: async () => {
2279
- const res = await apiFetch<User[] | { users: User[] }>('/users');
2315
+ const res = await apiFetch<User[] | { users: User[] }>('/api/users');
2280
2316
  return Array.isArray(res.data) ? res.data : res.data.users ?? [];
2281
2317
  },
2282
2318
  });
@@ -2285,7 +2321,7 @@ export default function UsersPage() {
2285
2321
  if (!form.name || !form.email) { setError('Name and email required'); return; }
2286
2322
  setLoading(true); setError('');
2287
2323
  try {
2288
- const res = await apiFetch('/users', { method: 'POST', body: JSON.stringify(form) });
2324
+ const res = await apiFetch('/api/users', { method: 'POST', body: JSON.stringify(form) });
2289
2325
  if (res.status >= 400) throw new Error('Failed');
2290
2326
  setForm({ name: '', email: '' });
2291
2327
  queryClient.invalidateQueries({ queryKey: ['users'] });
@@ -2295,7 +2331,7 @@ export default function UsersPage() {
2295
2331
 
2296
2332
  const deleteUser = async (id: string | number) => {
2297
2333
  setLoading(true);
2298
- await apiFetch(\`/users/\${id}\`, { method: 'DELETE' });
2334
+ await apiFetch(\`/api/users/\${id}\`, { method: 'DELETE' });
2299
2335
  queryClient.invalidateQueries({ queryKey: ['users'] });
2300
2336
  setLoading(false);
2301
2337
  };
@@ -2410,7 +2446,7 @@ export default function PostsPage() {
2410
2446
  const { data: posts = [], isLoading } = useQuery({
2411
2447
  queryKey: ['posts'],
2412
2448
  queryFn: async () => {
2413
- const res = await apiFetch<Post[] | { posts: Post[] }>('/posts');
2449
+ const res = await apiFetch<Post[] | { posts: Post[] }>('/api/posts');
2414
2450
  return Array.isArray(res.data) ? res.data : res.data.posts ?? [];
2415
2451
  },
2416
2452
  });
@@ -2419,7 +2455,7 @@ export default function PostsPage() {
2419
2455
  if (!form.title) { setError('Title required'); return; }
2420
2456
  setLoading(true); setError('');
2421
2457
  try {
2422
- const res = await apiFetch('/posts', { method: 'POST', body: JSON.stringify(form) });
2458
+ const res = await apiFetch('/api/posts', { method: 'POST', body: JSON.stringify(form) });
2423
2459
  if (res.status >= 400) throw new Error('Failed');
2424
2460
  setForm({ title: '', content: '', published: false });
2425
2461
  queryClient.invalidateQueries({ queryKey: ['posts'] });
@@ -2429,7 +2465,7 @@ export default function PostsPage() {
2429
2465
 
2430
2466
  const deletePost = async (id: string | number) => {
2431
2467
  setLoading(true);
2432
- await apiFetch(\`/posts/\${id}\`, { method: 'DELETE' });
2468
+ await apiFetch(\`/api/posts/\${id}\`, { method: 'DELETE' });
2433
2469
  queryClient.invalidateQueries({ queryKey: ['posts'] });
2434
2470
  setLoading(false);
2435
2471
  };
@@ -2524,6 +2560,151 @@ export default function PostsPage() {
2524
2560
  }
2525
2561
  `;
2526
2562
  }
2563
+ function generateTasksPage() {
2564
+ return `import { useState } from 'react';
2565
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2566
+ import { apiFetch } from '../lib/api';
2567
+ import { Plus, Trash2, Loader2, CheckCircle2, Circle } from 'lucide-react';
2568
+
2569
+ interface Task {
2570
+ id: string | number;
2571
+ title: string;
2572
+ completed: boolean;
2573
+ priority: 'low' | 'medium' | 'high';
2574
+ createdAt?: string;
2575
+ }
2576
+
2577
+ const priorityColor: Record<string, string> = {
2578
+ high: 'bg-red-900 text-red-300',
2579
+ medium: 'bg-amber-900 text-amber-300',
2580
+ low: 'bg-slate-700 text-slate-300',
2581
+ };
2582
+
2583
+ export default function TasksPage() {
2584
+ const queryClient = useQueryClient();
2585
+ const [form, setForm] = useState({ title: '', priority: 'medium' as 'low' | 'medium' | 'high' });
2586
+ const [loading, setLoading] = useState(false);
2587
+ const [error, setError] = useState('');
2588
+
2589
+ const { data: tasks = [], isLoading } = useQuery({
2590
+ queryKey: ['tasks'],
2591
+ queryFn: async () => {
2592
+ const res = await apiFetch<Task[] | { tasks: Task[] }>('/api/tasks');
2593
+ return Array.isArray(res.data) ? res.data : res.data.tasks ?? [];
2594
+ },
2595
+ });
2596
+
2597
+ const createTask = async () => {
2598
+ if (!form.title) { setError('Title required'); return; }
2599
+ setLoading(true); setError('');
2600
+ try {
2601
+ const res = await apiFetch('/api/tasks', { method: 'POST', body: JSON.stringify(form) });
2602
+ if (res.status >= 400) throw new Error('Failed');
2603
+ setForm({ title: '', priority: 'medium' });
2604
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
2605
+ } catch { setError('Failed to create task'); }
2606
+ finally { setLoading(false); }
2607
+ };
2608
+
2609
+ const toggleTask = async (id: string | number) => {
2610
+ await apiFetch(\`/api/tasks/\${id}/toggle\`, { method: 'PATCH' });
2611
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
2612
+ };
2613
+
2614
+ const deleteTask = async (id: string | number) => {
2615
+ setLoading(true);
2616
+ await apiFetch(\`/api/tasks/\${id}\`, { method: 'DELETE' });
2617
+ queryClient.invalidateQueries({ queryKey: ['tasks'] });
2618
+ setLoading(false);
2619
+ };
2620
+
2621
+ const done = tasks.filter(t => t.completed).length;
2622
+
2623
+ return (
2624
+ <div>
2625
+ <div className="mb-6">
2626
+ <h1 className="text-2xl font-bold text-slate-100">Tasks</h1>
2627
+ <p className="text-slate-400 text-sm mt-1">{done}/{tasks.length} completed</p>
2628
+ </div>
2629
+
2630
+ {/* Create form */}
2631
+ <div className="bg-slate-800 rounded-xl border border-slate-700 p-4 mb-6">
2632
+ <h3 className="text-sm font-medium text-slate-300 mb-3">New Task</h3>
2633
+ <div className="flex gap-3">
2634
+ <input
2635
+ placeholder="Task title"
2636
+ value={form.title}
2637
+ onChange={e => setForm({ ...form, title: e.target.value })}
2638
+ onKeyDown={e => { if (e.key === 'Enter') { void createTask(); } }}
2639
+ className="flex-1 px-3 py-2 bg-slate-900 border border-slate-600 rounded-lg text-sm focus:border-blue-500 focus:outline-none"
2640
+ />
2641
+ <select
2642
+ value={form.priority}
2643
+ onChange={e => setForm({ ...form, priority: e.target.value as 'low' | 'medium' | 'high' })}
2644
+ className="px-3 py-2 bg-slate-900 border border-slate-600 rounded-lg text-sm focus:border-blue-500 focus:outline-none"
2645
+ >
2646
+ <option value="low">Low</option>
2647
+ <option value="medium">Medium</option>
2648
+ <option value="high">High</option>
2649
+ </select>
2650
+ <button
2651
+ onClick={() => { void createTask(); }}
2652
+ disabled={loading}
2653
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-lg text-sm font-medium transition flex items-center gap-2"
2654
+ >
2655
+ {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
2656
+ Add
2657
+ </button>
2658
+ </div>
2659
+ {error && <p className="text-red-400 text-xs mt-2">{error}</p>}
2660
+ </div>
2661
+
2662
+ {/* Tasks list */}
2663
+ <div className="space-y-2">
2664
+ {isLoading ? (
2665
+ <div className="text-center text-slate-400 text-sm py-8">Loading tasks...</div>
2666
+ ) : tasks.length === 0 ? (
2667
+ <div className="text-center text-slate-400 text-sm py-8">No tasks yet. Add one above.</div>
2668
+ ) : (
2669
+ tasks.map((task) => (
2670
+ <div
2671
+ key={task.id}
2672
+ className={\`flex items-center justify-between p-3 rounded-xl border transition \${
2673
+ task.completed ? 'bg-slate-800/50 border-slate-700/50' : 'bg-slate-800 border-slate-700'
2674
+ }\`}
2675
+ >
2676
+ <div className="flex items-center gap-3 flex-1 min-w-0">
2677
+ <button
2678
+ onClick={() => { void toggleTask(task.id); }}
2679
+ className="flex-shrink-0 text-slate-400 hover:text-blue-400 transition"
2680
+ >
2681
+ {task.completed
2682
+ ? <CheckCircle2 className="w-5 h-5 text-emerald-400" />
2683
+ : <Circle className="w-5 h-5" />
2684
+ }
2685
+ </button>
2686
+ <span className={\`text-sm font-medium truncate \${task.completed ? 'line-through text-slate-500' : 'text-slate-200'}\`}>
2687
+ {task.title}
2688
+ </span>
2689
+ <span className={\`flex-shrink-0 px-2 py-0.5 rounded-full text-xs font-medium \${priorityColor[task.priority] ?? ''}\`}>
2690
+ {task.priority}
2691
+ </span>
2692
+ </div>
2693
+ <button
2694
+ onClick={() => { void deleteTask(task.id); }}
2695
+ className="ml-3 p-1.5 text-slate-500 hover:text-red-400 transition rounded flex-shrink-0"
2696
+ >
2697
+ <Trash2 className="w-4 h-4" />
2698
+ </button>
2699
+ </div>
2700
+ ))
2701
+ )}
2702
+ </div>
2703
+ </div>
2704
+ );
2705
+ }
2706
+ `;
2707
+ }
2527
2708
  async function scaffoldFullstackReadme(projectDir, projectName) {
2528
2709
  const readme = `# ${projectName}
2529
2710
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kozojs/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "CLI to scaffold new Kozo Framework projects - The next-gen TypeScript Backend Framework",
5
5
  "bin": {
6
6
  "create-kozo": "./lib/index.js",