@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.
Files changed (187) hide show
  1. package/dist/standalone/packages/web/.next/BUILD_ID +1 -1
  2. package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
  3. package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
  4. package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  5. package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  6. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
  12. package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
  13. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
  19. package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
  20. package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
  21. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
  22. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
  23. package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
  24. package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
  25. package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
  26. package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
  27. package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
  28. package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
  29. package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
  30. package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
  31. package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
  32. package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
  33. package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  34. package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
  35. package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
  36. package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
  37. package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
  38. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
  39. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
  40. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
  41. package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
  42. package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
  43. package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
  44. package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
  45. package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
  46. package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
  47. package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
  48. package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  49. package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
  51. package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
  52. package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
  53. package/dist/standalone/packages/web/components.json +20 -0
  54. package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
  55. package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
  56. package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
  57. package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
  58. package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
  59. package/dist/standalone/packages/web/drizzle.config.ts +10 -0
  60. package/dist/standalone/packages/web/eslint.config.mjs +18 -0
  61. package/dist/standalone/packages/web/next.config.ts +7 -0
  62. package/dist/standalone/packages/web/package.json +1 -1
  63. package/dist/standalone/packages/web/postcss.config.mjs +8 -0
  64. package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
  65. package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
  66. package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
  67. package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
  68. package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
  69. package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
  70. package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
  71. package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
  72. package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
  73. package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
  74. package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
  75. package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
  76. package/dist/standalone/packages/web/src/app/error.tsx +43 -0
  77. package/dist/standalone/packages/web/src/app/globals.css +531 -0
  78. package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
  79. package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
  80. package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
  81. package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
  82. package/dist/standalone/packages/web/src/app/page.tsx +18 -0
  83. package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
  84. package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
  85. package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
  86. package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
  87. package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
  88. package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
  89. package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
  90. package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
  91. package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
  92. package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
  93. package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
  94. package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
  95. package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
  96. package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
  97. package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
  98. package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
  99. package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
  100. package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
  101. package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
  102. package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
  103. package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
  104. package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
  105. package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
  106. package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
  107. package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
  108. package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
  109. package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
  110. package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
  111. package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
  112. package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
  113. package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
  114. package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
  115. package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
  116. package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
  117. package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
  118. package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
  119. package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
  120. package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
  121. package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
  122. package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
  123. package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
  124. package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
  125. package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
  126. package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
  127. package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
  128. package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
  129. package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
  130. package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
  131. package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
  132. package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
  133. package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
  134. package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
  135. package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
  136. package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
  137. package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
  138. package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
  139. package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
  140. package/dist/standalone/packages/web/src/types/specs.ts +18 -0
  141. package/dist/standalone/packages/web/tailwind.config.ts +58 -0
  142. package/dist/standalone/packages/web/tsconfig.json +34 -0
  143. package/dist/standalone/packages/web/vitest.config.ts +14 -0
  144. package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
  145. package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
  146. package/package.json +5 -3
  147. package/dist/BUILD_ID +0 -1
  148. package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
  149. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
  150. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
  151. /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
  152. /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
  153. /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
  154. /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
  155. /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
  156. /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
  157. /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
  158. /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
  159. /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
  160. /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
  161. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
  162. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
  163. /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
  164. /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
  165. /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
  166. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
  167. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
  168. /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
  169. /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
  170. /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
  171. /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
  172. /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
  173. /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
  174. /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
  175. /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
  176. /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
  177. /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
  178. /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
  179. /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
  180. /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
  181. /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
  182. /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
  183. /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
  184. /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
  185. /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
  186. /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
  187. /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
+ });