@leanspec/ui 0.2.3 → 0.2.4
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/dist/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for date formatting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import dayjs from 'dayjs';
|
|
6
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
7
|
+
|
|
8
|
+
dayjs.extend(relativeTime);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a date as relative time (e.g., "2 days ago")
|
|
12
|
+
*/
|
|
13
|
+
export function formatRelativeTime(date: Date | string | number | null | undefined): string {
|
|
14
|
+
if (!date) return 'Unknown';
|
|
15
|
+
return dayjs(date).fromNow();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a date in a readable format (e.g., "Nov 12, 2025")
|
|
20
|
+
*/
|
|
21
|
+
export function formatDate(date: Date | string | number | null | undefined): string {
|
|
22
|
+
if (!date) return 'Unknown';
|
|
23
|
+
return dayjs(date).format('MMM D, YYYY');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a date with time (e.g., "Nov 12, 2025 10:30 AM")
|
|
28
|
+
*/
|
|
29
|
+
export function formatDateTime(date: Date | string | number | null | undefined): string {
|
|
30
|
+
if (!date) return 'Unknown';
|
|
31
|
+
return dayjs(date).format('MMM D, YYYY h:mm A');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format duration between two dates in a human-readable format
|
|
36
|
+
*/
|
|
37
|
+
export function formatDuration(
|
|
38
|
+
start: Date | string | number | null | undefined,
|
|
39
|
+
end: Date | string | number | null | undefined
|
|
40
|
+
): string {
|
|
41
|
+
if (!start || !end) return '';
|
|
42
|
+
|
|
43
|
+
const startDate = dayjs(start);
|
|
44
|
+
const endDate = dayjs(end);
|
|
45
|
+
const diffMs = endDate.diff(startDate);
|
|
46
|
+
|
|
47
|
+
if (diffMs < 0) return '';
|
|
48
|
+
|
|
49
|
+
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
50
|
+
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
51
|
+
|
|
52
|
+
if (days === 0 && hours === 0) {
|
|
53
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
54
|
+
if (minutes === 0) return '< 1m';
|
|
55
|
+
return `${minutes}m`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (days === 0) {
|
|
59
|
+
return `${hours}h`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (days < 30) {
|
|
63
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const months = Math.floor(days / 30);
|
|
67
|
+
const remainingDays = days % 30;
|
|
68
|
+
|
|
69
|
+
if (months < 12) {
|
|
70
|
+
return remainingDays > 0 ? `${months}mo ${remainingDays}d` : `${months}mo`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const years = Math.floor(months / 12);
|
|
74
|
+
const remainingMonths = months % 12;
|
|
75
|
+
return remainingMonths > 0 ? `${years}y ${remainingMonths}mo` : `${years}y`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection and client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import * as schema from './schema';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
// Lazy database initialization
|
|
11
|
+
let _db: ReturnType<typeof drizzle> | null = null;
|
|
12
|
+
let _sqlite: Database.Database | null = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get or create database connection
|
|
16
|
+
* Only initializes when first accessed (lazy loading)
|
|
17
|
+
*/
|
|
18
|
+
export function getDb() {
|
|
19
|
+
if (!_db) {
|
|
20
|
+
// Database path - use local SQLite for development
|
|
21
|
+
const dbPath = process.env.DATABASE_PATH || join(process.cwd(), 'leanspec.db');
|
|
22
|
+
|
|
23
|
+
// Create SQLite database connection
|
|
24
|
+
_sqlite = new Database(dbPath);
|
|
25
|
+
|
|
26
|
+
// Create Drizzle client
|
|
27
|
+
_db = drizzle(_sqlite, { schema });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return _db;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Export schema for use in queries
|
|
34
|
+
export { schema };
|
|
35
|
+
|
|
36
|
+
// Legacy export for backward compatibility (will be lazy)
|
|
37
|
+
export const db = new Proxy({} as ReturnType<typeof drizzle>, {
|
|
38
|
+
get(_target, prop, receiver) {
|
|
39
|
+
const realDb = getDb();
|
|
40
|
+
return Reflect.get(realDb as object, prop, receiver);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run database migrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
6
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
const dbPath = process.env.DATABASE_PATH || join(process.cwd(), 'leanspec.db');
|
|
11
|
+
const sqlite = new Database(dbPath);
|
|
12
|
+
const db = drizzle(sqlite);
|
|
13
|
+
|
|
14
|
+
console.log('Running migrations...');
|
|
15
|
+
migrate(db, { migrationsFolder: './drizzle' });
|
|
16
|
+
console.log('Migrations complete!');
|
|
17
|
+
|
|
18
|
+
sqlite.close();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database queries for projects and specs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { db, schema } from './index';
|
|
6
|
+
import { eq, desc } from 'drizzle-orm';
|
|
7
|
+
import { detectSubSpecs } from '../sub-specs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
// Projects
|
|
11
|
+
export async function getProjects() {
|
|
12
|
+
return await db.select().from(schema.projects).orderBy(desc(schema.projects.isFeatured), schema.projects.displayName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getProjectById(id: string) {
|
|
16
|
+
const results = await db.select().from(schema.projects).where(eq(schema.projects.id, id)).limit(1);
|
|
17
|
+
return results[0] || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function getFeaturedProjects() {
|
|
21
|
+
return await db.select().from(schema.projects).where(eq(schema.projects.isFeatured, true));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Specs
|
|
25
|
+
export async function getSpecs() {
|
|
26
|
+
const results = await db.select().from(schema.specs).orderBy(schema.specs.specNumber);
|
|
27
|
+
return results.map(spec => ({
|
|
28
|
+
...spec,
|
|
29
|
+
tags: spec.tags ? JSON.parse(spec.tags) : null,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getSpecById(id: string) {
|
|
34
|
+
// Support both uuid and spec number (e.g., "035" or "35")
|
|
35
|
+
let results;
|
|
36
|
+
|
|
37
|
+
// Check if id is numeric (spec number)
|
|
38
|
+
const specNum = parseInt(id, 10);
|
|
39
|
+
if (!isNaN(specNum)) {
|
|
40
|
+
results = await db.select().from(schema.specs).where(eq(schema.specs.specNumber, specNum)).limit(1);
|
|
41
|
+
} else {
|
|
42
|
+
// Fallback to uuid lookup
|
|
43
|
+
results = await db.select().from(schema.specs).where(eq(schema.specs.id, id)).limit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (results.length === 0) return null;
|
|
47
|
+
const spec = results[0];
|
|
48
|
+
|
|
49
|
+
// Detect sub-specs from filesystem
|
|
50
|
+
const specDirPath = join(process.cwd(), '../../specs', spec.filePath.replace('/README.md', '').replace('specs/', ''));
|
|
51
|
+
const subSpecs = detectSubSpecs(specDirPath);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...spec,
|
|
55
|
+
tags: spec.tags ? JSON.parse(spec.tags) : null,
|
|
56
|
+
subSpecs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getSpecsByProjectId(projectId: string) {
|
|
61
|
+
const results = await db.select().from(schema.specs).where(eq(schema.specs.projectId, projectId)).orderBy(schema.specs.specNumber);
|
|
62
|
+
return results.map(spec => ({
|
|
63
|
+
...spec,
|
|
64
|
+
tags: spec.tags ? JSON.parse(spec.tags) : null,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getSpecsByStatus(status: 'planned' | 'in-progress' | 'complete' | 'archived') {
|
|
69
|
+
const results = await db.select().from(schema.specs).where(eq(schema.specs.status, status)).orderBy(schema.specs.specNumber);
|
|
70
|
+
return results.map(spec => ({
|
|
71
|
+
...spec,
|
|
72
|
+
tags: spec.tags ? JSON.parse(spec.tags) : null,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Stats
|
|
77
|
+
export interface StatsResult {
|
|
78
|
+
totalProjects: number;
|
|
79
|
+
totalSpecs: number;
|
|
80
|
+
specsByStatus: { status: string; count: number }[];
|
|
81
|
+
specsByPriority: { priority: string; count: number }[];
|
|
82
|
+
completionRate: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getStats(): Promise<StatsResult> {
|
|
86
|
+
const [projects, specs] = await Promise.all([
|
|
87
|
+
db.select().from(schema.projects),
|
|
88
|
+
db.select().from(schema.specs),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// Count by status
|
|
92
|
+
const statusCounts = new Map<string, number>();
|
|
93
|
+
const priorityCounts = new Map<string, number>();
|
|
94
|
+
|
|
95
|
+
for (const spec of specs) {
|
|
96
|
+
if (spec.status) {
|
|
97
|
+
statusCounts.set(spec.status, (statusCounts.get(spec.status) || 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
if (spec.priority) {
|
|
100
|
+
priorityCounts.set(spec.priority, (priorityCounts.get(spec.priority) || 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const completeCount = statusCounts.get('complete') || 0;
|
|
105
|
+
const completionRate = specs.length > 0 ? (completeCount / specs.length) * 100 : 0;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
totalProjects: projects.length,
|
|
109
|
+
totalSpecs: specs.length,
|
|
110
|
+
specsByStatus: Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count })),
|
|
111
|
+
specsByPriority: Array.from(priorityCounts.entries()).map(([priority, count]) => ({ priority, count })),
|
|
112
|
+
completionRate: Math.round(completionRate * 10) / 10,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema for LeanSpec Web
|
|
3
|
+
* Using Drizzle ORM with SQLite (development) / PostgreSQL (production)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
7
|
+
import { relations } from 'drizzle-orm';
|
|
8
|
+
|
|
9
|
+
// Projects table - GitHub repositories using LeanSpec
|
|
10
|
+
export const projects = sqliteTable('projects', {
|
|
11
|
+
id: text('id').primaryKey(),
|
|
12
|
+
githubOwner: text('github_owner').notNull(),
|
|
13
|
+
githubRepo: text('github_repo').notNull(),
|
|
14
|
+
displayName: text('display_name'),
|
|
15
|
+
description: text('description'),
|
|
16
|
+
homepageUrl: text('homepage_url'),
|
|
17
|
+
stars: integer('stars').default(0),
|
|
18
|
+
isPublic: integer('is_public', { mode: 'boolean' }).default(true),
|
|
19
|
+
isFeatured: integer('is_featured', { mode: 'boolean' }).default(false),
|
|
20
|
+
lastSyncedAt: integer('last_synced_at', { mode: 'timestamp' }),
|
|
21
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
22
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Specs table - Cached specification content from GitHub
|
|
26
|
+
export const specs = sqliteTable('specs', {
|
|
27
|
+
id: text('id').primaryKey(),
|
|
28
|
+
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
|
29
|
+
specNumber: integer('spec_number'),
|
|
30
|
+
specName: text('spec_name').notNull(),
|
|
31
|
+
title: text('title'),
|
|
32
|
+
status: text('status', {
|
|
33
|
+
enum: ['planned', 'in-progress', 'complete', 'archived']
|
|
34
|
+
}),
|
|
35
|
+
priority: text('priority', {
|
|
36
|
+
enum: ['low', 'medium', 'high', 'critical']
|
|
37
|
+
}),
|
|
38
|
+
tags: text('tags'), // JSON string array
|
|
39
|
+
assignee: text('assignee'),
|
|
40
|
+
contentMd: text('content_md').notNull(), // Full markdown content
|
|
41
|
+
contentHtml: text('content_html'), // Pre-rendered HTML (optional optimization)
|
|
42
|
+
createdAt: integer('created_at', { mode: 'timestamp' }),
|
|
43
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }),
|
|
44
|
+
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
45
|
+
filePath: text('file_path').notNull(), // Path in repo
|
|
46
|
+
githubUrl: text('github_url'), // Direct GitHub file link
|
|
47
|
+
syncedAt: integer('synced_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
48
|
+
}, (table) => ({
|
|
49
|
+
// Unique constraint on projectId + specNumber (prevent duplicates within a project)
|
|
50
|
+
uniqueSpecNumber: uniqueIndex('unique_spec_number').on(table.projectId, table.specNumber),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Spec relationships table - Tracks dependencies between specs
|
|
54
|
+
export const specRelationships = sqliteTable('spec_relationships', {
|
|
55
|
+
id: text('id').primaryKey(),
|
|
56
|
+
specId: text('spec_id').notNull().references(() => specs.id, { onDelete: 'cascade' }),
|
|
57
|
+
relatedSpecId: text('related_spec_id').notNull().references(() => specs.id, { onDelete: 'cascade' }),
|
|
58
|
+
relationshipType: text('relationship_type', {
|
|
59
|
+
enum: ['depends_on', 'related']
|
|
60
|
+
}).notNull(),
|
|
61
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Sync logs table - Audit trail for GitHub sync operations
|
|
65
|
+
export const syncLogs = sqliteTable('sync_logs', {
|
|
66
|
+
id: text('id').primaryKey(),
|
|
67
|
+
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
|
68
|
+
status: text('status', {
|
|
69
|
+
enum: ['pending', 'running', 'success', 'failed']
|
|
70
|
+
}).notNull(),
|
|
71
|
+
specsAdded: integer('specs_added').default(0),
|
|
72
|
+
specsUpdated: integer('specs_updated').default(0),
|
|
73
|
+
specsDeleted: integer('specs_deleted').default(0),
|
|
74
|
+
errorMessage: text('error_message'),
|
|
75
|
+
startedAt: integer('started_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
|
76
|
+
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
77
|
+
durationMs: integer('duration_ms'),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Define relations
|
|
81
|
+
export const projectsRelations = relations(projects, ({ many }) => ({
|
|
82
|
+
specs: many(specs),
|
|
83
|
+
syncLogs: many(syncLogs),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
export const specsRelations = relations(specs, ({ one, many }) => ({
|
|
87
|
+
project: one(projects, {
|
|
88
|
+
fields: [specs.projectId],
|
|
89
|
+
references: [projects.id],
|
|
90
|
+
}),
|
|
91
|
+
dependencies: many(specRelationships, { relationName: 'spec' }),
|
|
92
|
+
dependents: many(specRelationships, { relationName: 'relatedSpec' }),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
export const specRelationshipsRelations = relations(specRelationships, ({ one }) => ({
|
|
96
|
+
spec: one(specs, {
|
|
97
|
+
fields: [specRelationships.specId],
|
|
98
|
+
references: [specs.id],
|
|
99
|
+
relationName: 'spec',
|
|
100
|
+
}),
|
|
101
|
+
relatedSpec: one(specs, {
|
|
102
|
+
fields: [specRelationships.relatedSpecId],
|
|
103
|
+
references: [specs.id],
|
|
104
|
+
relationName: 'relatedSpec',
|
|
105
|
+
}),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
export const syncLogsRelations = relations(syncLogs, ({ one }) => ({
|
|
109
|
+
project: one(projects, {
|
|
110
|
+
fields: [syncLogs.projectId],
|
|
111
|
+
references: [projects.id],
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Export types
|
|
116
|
+
export type Project = typeof projects.$inferSelect;
|
|
117
|
+
export type NewProject = typeof projects.$inferInsert;
|
|
118
|
+
export type Spec = typeof specs.$inferSelect;
|
|
119
|
+
export type NewSpec = typeof specs.$inferInsert;
|
|
120
|
+
export type SpecRelationship = typeof specRelationships.$inferSelect;
|
|
121
|
+
export type NewSpecRelationship = typeof specRelationships.$inferInsert;
|
|
122
|
+
export type SyncLog = typeof syncLogs.$inferSelect;
|
|
123
|
+
export type NewSyncLog = typeof syncLogs.$inferInsert;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed database with LeanSpec's own specs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { db, schema } from './index';
|
|
6
|
+
import matter from 'gray-matter';
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { eq } from 'drizzle-orm';
|
|
11
|
+
|
|
12
|
+
// Path to specs directory (relative to monorepo root)
|
|
13
|
+
const SPECS_DIR = join(process.cwd(), '../../specs');
|
|
14
|
+
|
|
15
|
+
type Frontmatter = {
|
|
16
|
+
title?: string;
|
|
17
|
+
status?: 'planned' | 'in-progress' | 'complete' | 'archived';
|
|
18
|
+
priority?: 'low' | 'medium' | 'high' | 'critical';
|
|
19
|
+
tags?: string[];
|
|
20
|
+
assignee?: string;
|
|
21
|
+
created?: string;
|
|
22
|
+
created_at?: string;
|
|
23
|
+
updated?: string;
|
|
24
|
+
updated_at?: string;
|
|
25
|
+
completed?: string;
|
|
26
|
+
completed_at?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface ParsedSpec {
|
|
30
|
+
number: number | null;
|
|
31
|
+
name: string;
|
|
32
|
+
frontmatter: Frontmatter;
|
|
33
|
+
content: string;
|
|
34
|
+
filePath: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseSpecDirectory(dirPath: string): ParsedSpec | null {
|
|
38
|
+
try {
|
|
39
|
+
const readmePath = join(dirPath, 'README.md');
|
|
40
|
+
const content = readFileSync(readmePath, 'utf-8');
|
|
41
|
+
const { data: frontmatter, content: markdownContent } = matter(content);
|
|
42
|
+
|
|
43
|
+
// Extract spec number and name from directory name
|
|
44
|
+
const dirName = dirPath.split('/').pop() || '';
|
|
45
|
+
const match = dirName.match(/^(\d+)-(.+)$/);
|
|
46
|
+
const specNumber = match ? parseInt(match[1], 10) : null;
|
|
47
|
+
const specName = match ? match[2] : dirName;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
number: specNumber,
|
|
51
|
+
name: specName,
|
|
52
|
+
frontmatter: frontmatter as Frontmatter,
|
|
53
|
+
content: markdownContent, // Use the parsed content without frontmatter
|
|
54
|
+
filePath: `specs/${dirName}/README.md`,
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Failed to parse spec ${dirPath}:`, error);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getAllSpecs(): ParsedSpec[] {
|
|
63
|
+
const specs: ParsedSpec[] = [];
|
|
64
|
+
const entries = readdirSync(SPECS_DIR);
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const entryPath = join(SPECS_DIR, entry);
|
|
68
|
+
const stat = statSync(entryPath);
|
|
69
|
+
|
|
70
|
+
// Skip archived specs and non-directories
|
|
71
|
+
if (!stat.isDirectory() || entry === 'archived') {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const spec = parseSpecDirectory(entryPath);
|
|
76
|
+
if (spec) {
|
|
77
|
+
specs.push(spec);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return specs.sort((a, b) => (a.number || 0) - (b.number || 0));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function seed() {
|
|
85
|
+
console.log('Seeding database with LeanSpec specs...');
|
|
86
|
+
|
|
87
|
+
// Clear existing data for LeanSpec project (idempotent seed)
|
|
88
|
+
console.log('Clearing existing LeanSpec data...');
|
|
89
|
+
const existingProjects = await db
|
|
90
|
+
.select()
|
|
91
|
+
.from(schema.projects)
|
|
92
|
+
.where(eq(schema.projects.githubRepo, 'lean-spec'));
|
|
93
|
+
|
|
94
|
+
for (const project of existingProjects) {
|
|
95
|
+
// Cascade delete will remove associated specs
|
|
96
|
+
await db.delete(schema.projects).where(eq(schema.projects.id, project.id));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create LeanSpec project
|
|
100
|
+
const projectId = randomUUID();
|
|
101
|
+
await db.insert(schema.projects).values({
|
|
102
|
+
id: projectId,
|
|
103
|
+
githubOwner: 'codervisor',
|
|
104
|
+
githubRepo: 'lean-spec',
|
|
105
|
+
displayName: 'LeanSpec',
|
|
106
|
+
description: 'Lightweight spec methodology for AI-powered development',
|
|
107
|
+
homepageUrl: 'https://lean-spec.dev',
|
|
108
|
+
stars: 0,
|
|
109
|
+
isPublic: true,
|
|
110
|
+
isFeatured: true,
|
|
111
|
+
lastSyncedAt: new Date(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log(`Created project: ${projectId}`);
|
|
115
|
+
|
|
116
|
+
// Load and insert specs
|
|
117
|
+
const specs = getAllSpecs();
|
|
118
|
+
console.log(`Found ${specs.length} specs`);
|
|
119
|
+
|
|
120
|
+
for (const spec of specs) {
|
|
121
|
+
const specId = randomUUID();
|
|
122
|
+
const fm = spec.frontmatter;
|
|
123
|
+
const parseDate = (value?: string) => (value ? new Date(value) : null);
|
|
124
|
+
const created = fm.created_at ?? fm.created;
|
|
125
|
+
const updated = fm.updated_at ?? fm.updated;
|
|
126
|
+
const completed = fm.completed_at ?? fm.completed;
|
|
127
|
+
|
|
128
|
+
await db.insert(schema.specs).values({
|
|
129
|
+
id: specId,
|
|
130
|
+
projectId,
|
|
131
|
+
specNumber: spec.number,
|
|
132
|
+
specName: spec.name,
|
|
133
|
+
title: fm.title || spec.name,
|
|
134
|
+
status: fm.status || 'planned',
|
|
135
|
+
priority: fm.priority || 'medium',
|
|
136
|
+
tags: fm.tags ? JSON.stringify(fm.tags) : null,
|
|
137
|
+
assignee: fm.assignee || null,
|
|
138
|
+
contentMd: spec.content,
|
|
139
|
+
createdAt: parseDate(created),
|
|
140
|
+
updatedAt: parseDate(updated),
|
|
141
|
+
completedAt: parseDate(completed),
|
|
142
|
+
filePath: spec.filePath,
|
|
143
|
+
githubUrl: `https://github.com/codervisor/lean-spec/blob/main/${spec.filePath}`,
|
|
144
|
+
syncedAt: new Date(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log(`Inserted spec ${spec.number}: ${spec.name}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log('Seed complete!');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
seed().catch((error) => {
|
|
154
|
+
console.error('Seed failed:', error);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|