@kmgeon/taskflow 0.1.3

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 (158) hide show
  1. package/README.md +374 -0
  2. package/bin/task-mcp.mjs +19 -0
  3. package/bin/task.mjs +19 -0
  4. package/docs/clean-code.md +29 -0
  5. package/docs/git.md +36 -0
  6. package/docs/guideline.md +25 -0
  7. package/docs/security.md +32 -0
  8. package/docs/step-by-step.md +29 -0
  9. package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
  10. package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
  11. package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
  12. package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
  13. package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
  14. package/docs/tdd.md +41 -0
  15. package/package.json +114 -0
  16. package/src/app/(protected)/dashboard/page.tsx +7 -0
  17. package/src/app/(protected)/layout.tsx +10 -0
  18. package/src/app/api/[[...hono]]/route.ts +13 -0
  19. package/src/app/example/page.tsx +11 -0
  20. package/src/app/favicon.ico +0 -0
  21. package/src/app/globals.css +168 -0
  22. package/src/app/layout.tsx +35 -0
  23. package/src/app/page.tsx +5 -0
  24. package/src/app/providers.tsx +57 -0
  25. package/src/backend/config/index.ts +36 -0
  26. package/src/backend/hono/app.ts +32 -0
  27. package/src/backend/hono/context.ts +38 -0
  28. package/src/backend/http/response.ts +64 -0
  29. package/src/backend/middleware/context.ts +23 -0
  30. package/src/backend/middleware/error.ts +31 -0
  31. package/src/backend/middleware/supabase.ts +23 -0
  32. package/src/backend/supabase/client.ts +17 -0
  33. package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
  34. package/src/cli/commands/advisor.ts +45 -0
  35. package/src/cli/commands/ask.ts +50 -0
  36. package/src/cli/commands/board.ts +72 -0
  37. package/src/cli/commands/init.ts +184 -0
  38. package/src/cli/commands/list.ts +138 -0
  39. package/src/cli/commands/run.ts +143 -0
  40. package/src/cli/commands/set-status.ts +50 -0
  41. package/src/cli/commands/show.ts +28 -0
  42. package/src/cli/commands/tree.ts +72 -0
  43. package/src/cli/index.ts +38 -0
  44. package/src/cli/lib/__tests__/formatter.test.ts +123 -0
  45. package/src/cli/lib/error-boundary.test.ts +135 -0
  46. package/src/cli/lib/error-boundary.ts +70 -0
  47. package/src/cli/lib/formatter.ts +764 -0
  48. package/src/cli/lib/trd.ts +33 -0
  49. package/src/cli/lib/validate.test.ts +89 -0
  50. package/src/cli/lib/validate.ts +43 -0
  51. package/src/cli/prompts/task-run.md +25 -0
  52. package/src/components/layout/AppLayout.tsx +15 -0
  53. package/src/components/layout/Sidebar.tsx +124 -0
  54. package/src/components/ui/accordion.tsx +58 -0
  55. package/src/components/ui/avatar.tsx +50 -0
  56. package/src/components/ui/badge.tsx +36 -0
  57. package/src/components/ui/button.tsx +56 -0
  58. package/src/components/ui/card.tsx +79 -0
  59. package/src/components/ui/checkbox.tsx +30 -0
  60. package/src/components/ui/dialog.tsx +122 -0
  61. package/src/components/ui/dropdown-menu.tsx +200 -0
  62. package/src/components/ui/file-upload.tsx +50 -0
  63. package/src/components/ui/form.tsx +179 -0
  64. package/src/components/ui/input.tsx +25 -0
  65. package/src/components/ui/label.tsx +26 -0
  66. package/src/components/ui/scroll-area.tsx +48 -0
  67. package/src/components/ui/select.tsx +160 -0
  68. package/src/components/ui/separator.tsx +31 -0
  69. package/src/components/ui/sheet.tsx +140 -0
  70. package/src/components/ui/textarea.tsx +22 -0
  71. package/src/components/ui/toast.tsx +129 -0
  72. package/src/components/ui/toaster.tsx +35 -0
  73. package/src/core/ai/claude-client.ts +79 -0
  74. package/src/core/claude-runner/flag-builder.ts +57 -0
  75. package/src/core/claude-runner/index.ts +2 -0
  76. package/src/core/claude-runner/spawner.ts +86 -0
  77. package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
  78. package/src/core/prd/__tests__/generator.test.ts +26 -0
  79. package/src/core/prd/__tests__/scanner.test.ts +35 -0
  80. package/src/core/prd/auto-analyzer.ts +9 -0
  81. package/src/core/prd/generator.ts +8 -0
  82. package/src/core/prd/scanner.ts +117 -0
  83. package/src/core/project/__tests__/claude-setup.test.ts +133 -0
  84. package/src/core/project/__tests__/config.test.ts +30 -0
  85. package/src/core/project/__tests__/init.test.ts +37 -0
  86. package/src/core/project/__tests__/skill-setup.test.ts +62 -0
  87. package/src/core/project/claude-setup.ts +224 -0
  88. package/src/core/project/config.ts +34 -0
  89. package/src/core/project/docs-setup.ts +26 -0
  90. package/src/core/project/docs-templates.ts +205 -0
  91. package/src/core/project/init.ts +40 -0
  92. package/src/core/project/skill-setup.ts +32 -0
  93. package/src/core/project/skill-templates.ts +277 -0
  94. package/src/core/task/index.ts +16 -0
  95. package/src/core/types.ts +58 -0
  96. package/src/features/example/backend/error.ts +9 -0
  97. package/src/features/example/backend/route.ts +52 -0
  98. package/src/features/example/backend/schema.ts +25 -0
  99. package/src/features/example/backend/service.ts +73 -0
  100. package/src/features/example/components/example-status.test.tsx +97 -0
  101. package/src/features/example/components/example-status.tsx +160 -0
  102. package/src/features/example/hooks/useExampleQuery.ts +23 -0
  103. package/src/features/example/lib/dto.test.ts +57 -0
  104. package/src/features/example/lib/dto.ts +5 -0
  105. package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
  106. package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
  107. package/src/features/kanban/backend/route.ts +55 -0
  108. package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
  109. package/src/features/kanban/backend/sse-route.ts +43 -0
  110. package/src/features/kanban/components/KanbanBoard.tsx +105 -0
  111. package/src/features/kanban/components/KanbanColumn.tsx +51 -0
  112. package/src/features/kanban/components/KanbanError.tsx +29 -0
  113. package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
  114. package/src/features/kanban/components/ProgressCard.tsx +42 -0
  115. package/src/features/kanban/components/TaskCard.tsx +76 -0
  116. package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
  117. package/src/features/kanban/hooks/useTaskSse.ts +66 -0
  118. package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
  119. package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
  120. package/src/features/kanban/lib/kanban-utils.ts +37 -0
  121. package/src/features/taskflow/constants.ts +54 -0
  122. package/src/features/taskflow/index.ts +27 -0
  123. package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
  124. package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
  125. package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
  126. package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
  127. package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
  128. package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
  129. package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
  130. package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
  131. package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
  132. package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
  133. package/src/features/taskflow/lib/advisor/db.ts +185 -0
  134. package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
  135. package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
  136. package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
  137. package/src/features/taskflow/lib/filter.ts +54 -0
  138. package/src/features/taskflow/lib/fs-utils.ts +50 -0
  139. package/src/features/taskflow/lib/graph.ts +148 -0
  140. package/src/features/taskflow/lib/index-builder.ts +42 -0
  141. package/src/features/taskflow/lib/repository.ts +168 -0
  142. package/src/features/taskflow/lib/serializer.ts +62 -0
  143. package/src/features/taskflow/lib/watcher.ts +40 -0
  144. package/src/features/taskflow/types.ts +71 -0
  145. package/src/hooks/use-toast.ts +194 -0
  146. package/src/lib/remote/api-client.ts +40 -0
  147. package/src/lib/supabase/client.ts +8 -0
  148. package/src/lib/supabase/server.ts +46 -0
  149. package/src/lib/supabase/types.ts +3 -0
  150. package/src/lib/utils.ts +6 -0
  151. package/src/mcp/index.ts +7 -0
  152. package/src/mcp/server.ts +21 -0
  153. package/src/mcp/tools/brainstorm.ts +48 -0
  154. package/src/mcp/tools/prd.ts +71 -0
  155. package/src/mcp/tools/project.ts +39 -0
  156. package/src/mcp/tools/task-status.ts +40 -0
  157. package/src/mcp/tools/task.ts +82 -0
  158. package/src/mcp/util.ts +6 -0
@@ -0,0 +1,168 @@
1
+ @import "tailwindcss";
2
+
3
+ @plugin "@tailwindcss/typography";
4
+ @plugin "tailwindcss-animate";
5
+
6
+ @custom-variant dark (&:where(.dark, .dark *));
7
+
8
+ @utility container {
9
+ margin-inline: auto;
10
+ width: 100%;
11
+ padding-inline: 2rem;
12
+ @media (width >= 1400px) {
13
+ max-width: 1400px;
14
+ }
15
+ }
16
+
17
+ @theme {
18
+ --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
19
+ --font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
20
+
21
+ --color-border: hsl(var(--border));
22
+ --color-input: hsl(var(--input));
23
+ --color-ring: hsl(var(--ring));
24
+ --color-background: hsl(var(--background));
25
+ --color-foreground: hsl(var(--foreground));
26
+
27
+ --color-primary: hsl(var(--primary));
28
+ --color-primary-foreground: hsl(var(--primary-foreground));
29
+
30
+ --color-secondary: hsl(var(--secondary));
31
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
32
+
33
+ --color-destructive: hsl(var(--destructive));
34
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
35
+
36
+ --color-muted: hsl(var(--muted));
37
+ --color-muted-foreground: hsl(var(--muted-foreground));
38
+
39
+ --color-accent: hsl(var(--accent));
40
+ --color-accent-foreground: hsl(var(--accent-foreground));
41
+
42
+ --color-popover: hsl(var(--popover));
43
+ --color-popover-foreground: hsl(var(--popover-foreground));
44
+
45
+ --color-card: hsl(var(--card));
46
+ --color-card-foreground: hsl(var(--card-foreground));
47
+
48
+ --radius-lg: var(--radius);
49
+ --radius-md: calc(var(--radius) - 2px);
50
+ --radius-sm: calc(var(--radius) - 4px);
51
+
52
+ --animate-accordion-down: accordion-down 0.2s ease-out;
53
+ --animate-accordion-up: accordion-up 0.2s ease-out;
54
+
55
+ @keyframes accordion-down {
56
+ from {
57
+ height: 0;
58
+ }
59
+ to {
60
+ height: var(--radix-accordion-content-height);
61
+ }
62
+ }
63
+ @keyframes accordion-up {
64
+ from {
65
+ height: var(--radix-accordion-content-height);
66
+ }
67
+ to {
68
+ height: 0;
69
+ }
70
+ }
71
+ }
72
+
73
+ /*
74
+ The default border color has changed to `currentcolor` in Tailwind CSS v4,
75
+ so we've added these compatibility styles to make sure everything still
76
+ looks the same as it did with Tailwind CSS v3.
77
+
78
+ If we ever want to remove these styles, we need to add an explicit border
79
+ color utility to any element that depends on these defaults.
80
+ */
81
+ @layer base {
82
+ *,
83
+ ::after,
84
+ ::before,
85
+ ::backdrop,
86
+ ::file-selector-button {
87
+ border-color: var(--color-gray-200, currentcolor);
88
+ }
89
+ }
90
+
91
+ @layer base {
92
+ :root {
93
+ --background: 0 0% 100%;
94
+ --foreground: 222.2 84% 4.9%;
95
+ --card: 0 0% 100%;
96
+ --card-foreground: 222.2 84% 4.9%;
97
+ --popover: 0 0% 100%;
98
+ --popover-foreground: 222.2 84% 4.9%;
99
+ --primary: 222.2 47.4% 11.2%;
100
+ --primary-foreground: 210 40% 98%;
101
+ --secondary: 210 40% 96.1%;
102
+ --secondary-foreground: 222.2 47.4% 11.2%;
103
+ --muted: 210 40% 96.1%;
104
+ --muted-foreground: 215 20.2% 65.1%;
105
+ --accent: 172.2 50% 48.5%;
106
+ --accent-foreground: 210 40% 98%;
107
+ --destructive: 0 84.2% 60.2%;
108
+ --destructive-foreground: 210 40% 98%;
109
+ --border: 214.3 31.8% 91.4%;
110
+ --input: 214.3 31.8% 91.4%;
111
+ --ring: 222.2 84% 4.9%;
112
+ --radius: 0.5rem;
113
+ --chart-1: 12 76% 61%;
114
+ --chart-2: 173 58% 39%;
115
+ --chart-3: 197 37% 24%;
116
+ --chart-4: 43 74% 66%;
117
+ --chart-5: 27 87% 67%;
118
+ }
119
+
120
+ .dark {
121
+ --background: 222.2 84% 4.9%;
122
+ --foreground: 210 40% 98%;
123
+ --card: 222.2 84% 4.9%;
124
+ --card-foreground: 210 40% 98%;
125
+ --popover: 222.2 84% 4.9%;
126
+ --popover-foreground: 210 40% 98%;
127
+ --primary: 210 40% 98%;
128
+ --primary-foreground: 222.2 47.4% 11.2%;
129
+ --secondary: 217.2 32.6% 17.5%;
130
+ --secondary-foreground: 210 40% 98%;
131
+ --muted: 217.2 32.6% 17.5%;
132
+ --muted-foreground: 215 20.2% 65.1%;
133
+ --accent: 172.2 50% 48.5%;
134
+ --accent-foreground: 210 40% 98%;
135
+ --destructive: 0 62.8% 30.6%;
136
+ --destructive-foreground: 210 40% 98%;
137
+ --border: 217.2 32.6% 17.5%;
138
+ --input: 217.2 32.6% 17.5%;
139
+ --ring: 212.7 26.8% 83.9%;
140
+ --chart-1: 220 70% 50%;
141
+ --chart-2: 160 60% 45%;
142
+ --chart-3: 30 80% 55%;
143
+ --chart-4: 280 65% 60%;
144
+ --chart-5: 340 75% 55%;
145
+ }
146
+ }
147
+
148
+ @layer base {
149
+ * {
150
+ @apply border-border;
151
+ }
152
+ body {
153
+ @apply bg-background text-foreground;
154
+ }
155
+ h1 {
156
+ @apply text-4xl font-bold;
157
+ }
158
+ h2 {
159
+ @apply text-3xl font-semibold;
160
+ }
161
+ h3 {
162
+ @apply text-2xl font-semibold;
163
+ }
164
+ pre,
165
+ code {
166
+ font-family: var(--font-mono);
167
+ }
168
+ }
@@ -0,0 +1,35 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter, JetBrains_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import Providers from "./providers";
5
+
6
+ const inter = Inter({
7
+ subsets: ["latin"],
8
+ variable: "--font-inter",
9
+ });
10
+
11
+ const jetbrainsMono = JetBrains_Mono({
12
+ subsets: ["latin"],
13
+ variable: "--font-jetbrains-mono",
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "TaskFlow",
18
+ description: "AI-powered task management dashboard",
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html lang="ko" suppressHydrationWarning>
28
+ <body
29
+ className={`${inter.variable} ${jetbrainsMono.variable} antialiased font-sans`}
30
+ >
31
+ <Providers>{children}</Providers>
32
+ </body>
33
+ </html>
34
+ );
35
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function Home() {
4
+ redirect("/dashboard");
5
+ }
@@ -0,0 +1,57 @@
1
+ // In Next.js, this file would be called: app/providers.tsx
2
+ "use client";
3
+
4
+ // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
5
+ import {
6
+ isServer,
7
+ QueryClient,
8
+ QueryClientProvider,
9
+ } from "@tanstack/react-query";
10
+ import { ThemeProvider } from "next-themes";
11
+
12
+ function makeQueryClient() {
13
+ return new QueryClient({
14
+ defaultOptions: {
15
+ queries: {
16
+ // With SSR, we usually want to set some default staleTime
17
+ // above 0 to avoid refetching immediately on the client
18
+ staleTime: 60 * 1000,
19
+ },
20
+ },
21
+ });
22
+ }
23
+
24
+ let browserQueryClient: QueryClient | undefined = undefined;
25
+
26
+ function getQueryClient() {
27
+ if (isServer) {
28
+ // Server: always make a new query client
29
+ return makeQueryClient();
30
+ } else {
31
+ // Browser: make a new query client if we don't already have one
32
+ // This is very important, so we don't re-make a new client if React
33
+ // suspends during the initial render. This may not be needed if we
34
+ // have a suspense boundary BELOW the creation of the query client
35
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
36
+ return browserQueryClient;
37
+ }
38
+ }
39
+
40
+ export default function Providers({ children }: { children: React.ReactNode }) {
41
+ // NOTE: Avoid useState when initializing the query client if you don't
42
+ // have a suspense boundary between this and the code that may
43
+ // suspend because React will throw away the client on the initial
44
+ // render if it suspends and there is no boundary
45
+ const queryClient = getQueryClient();
46
+
47
+ return (
48
+ <ThemeProvider
49
+ attribute="class"
50
+ defaultTheme="system"
51
+ enableSystem
52
+ disableTransitionOnChange
53
+ >
54
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
55
+ </ThemeProvider>
56
+ );
57
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import type { AppConfig } from '@/backend/hono/context';
3
+
4
+ const envSchema = z.object({
5
+ SUPABASE_URL: z.string().url(),
6
+ SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
7
+ });
8
+
9
+ let cachedConfig: AppConfig | null = null;
10
+
11
+ export const getAppConfig = (): AppConfig => {
12
+ if (cachedConfig) {
13
+ return cachedConfig;
14
+ }
15
+
16
+ const parsed = envSchema.safeParse({
17
+ SUPABASE_URL: process.env.SUPABASE_URL,
18
+ SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
19
+ });
20
+
21
+ if (!parsed.success) {
22
+ const messages = parsed.error.issues
23
+ .map((issue) => `${issue.path.join('.') || 'config'}: ${issue.message}`)
24
+ .join('; ');
25
+ throw new Error(`Invalid backend configuration: ${messages}`);
26
+ }
27
+
28
+ cachedConfig = {
29
+ supabase: {
30
+ url: parsed.data.SUPABASE_URL,
31
+ serviceRoleKey: parsed.data.SUPABASE_SERVICE_ROLE_KEY,
32
+ },
33
+ } satisfies AppConfig;
34
+
35
+ return cachedConfig;
36
+ };
@@ -0,0 +1,32 @@
1
+ import { Hono } from 'hono';
2
+ import { errorBoundary } from '@/backend/middleware/error';
3
+ import { withAppContext } from '@/backend/middleware/context';
4
+ import { withSupabase } from '@/backend/middleware/supabase';
5
+ import { registerExampleRoutes } from '@/features/example/backend/route';
6
+ import { registerTaskRoutes } from '@/features/kanban/backend/route';
7
+ import { registerSseRoute } from '@/features/kanban/backend/sse-route';
8
+ import type { AppEnv } from '@/backend/hono/context';
9
+
10
+ let singletonApp: Hono<AppEnv> | null = null;
11
+
12
+ export const createHonoApp = () => {
13
+ if (singletonApp) {
14
+ return singletonApp;
15
+ }
16
+
17
+ const app = new Hono<AppEnv>();
18
+
19
+ // Task routes: file-based, no Supabase required
20
+ registerTaskRoutes(app);
21
+ registerSseRoute(app);
22
+
23
+ // Supabase-dependent routes (scoped middleware)
24
+ app.use('/example/*', errorBoundary());
25
+ app.use('/example/*', withAppContext());
26
+ app.use('/example/*', withSupabase());
27
+ registerExampleRoutes(app);
28
+
29
+ singletonApp = app;
30
+
31
+ return app;
32
+ };
@@ -0,0 +1,38 @@
1
+ import type { Context } from 'hono';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+
4
+ export type AppLogger = Pick<Console, 'info' | 'error' | 'warn' | 'debug'>;
5
+
6
+ export type AppConfig = {
7
+ supabase: {
8
+ url: string;
9
+ serviceRoleKey: string;
10
+ };
11
+ };
12
+
13
+ export type AppVariables = {
14
+ supabase: SupabaseClient;
15
+ logger: AppLogger;
16
+ config: AppConfig;
17
+ };
18
+
19
+ export type AppEnv = {
20
+ Variables: AppVariables;
21
+ };
22
+
23
+ export type AppContext = Context<AppEnv>;
24
+
25
+ export const contextKeys = {
26
+ supabase: 'supabase',
27
+ logger: 'logger',
28
+ config: 'config',
29
+ } as const satisfies Record<keyof AppVariables, keyof AppVariables>;
30
+
31
+ export const getSupabase = (c: AppContext) =>
32
+ c.get(contextKeys.supabase) as SupabaseClient;
33
+
34
+ export const getLogger = (c: AppContext) =>
35
+ c.get(contextKeys.logger) as AppLogger;
36
+
37
+ export const getConfig = (c: AppContext) =>
38
+ c.get(contextKeys.config) as AppConfig;
@@ -0,0 +1,64 @@
1
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
2
+ import type { AppContext } from '@/backend/hono/context';
3
+
4
+ export type SuccessResult<TData> = {
5
+ ok: true;
6
+ status: ContentfulStatusCode;
7
+ data: TData;
8
+ };
9
+
10
+ export type ErrorResult<TCode extends string, TDetails = unknown> = {
11
+ ok: false;
12
+ status: ContentfulStatusCode;
13
+ error: {
14
+ code: TCode;
15
+ message: string;
16
+ details?: TDetails;
17
+ };
18
+ };
19
+
20
+ export type HandlerResult<TData, TCode extends string, TDetails = unknown> =
21
+ | SuccessResult<TData>
22
+ | ErrorResult<TCode, TDetails>;
23
+
24
+ export const success = <TData>(
25
+ data: TData,
26
+ status: ContentfulStatusCode = 200,
27
+ ): SuccessResult<TData> => ({
28
+ ok: true,
29
+ status,
30
+ data,
31
+ });
32
+
33
+ export const failure = <TCode extends string, TDetails = unknown>(
34
+ status: ContentfulStatusCode,
35
+ code: TCode,
36
+ message: string,
37
+ details?: TDetails,
38
+ ): ErrorResult<TCode, TDetails> => ({
39
+ ok: false,
40
+ status,
41
+ error: {
42
+ code,
43
+ message,
44
+ ...(details === undefined ? {} : { details }),
45
+ },
46
+ });
47
+
48
+ export const respond = <TData, TCode extends string, TDetails = unknown>(
49
+ c: AppContext,
50
+ result: HandlerResult<TData, TCode, TDetails>,
51
+ ) => {
52
+ if (result.ok) {
53
+ return c.json(result.data, result.status);
54
+ }
55
+
56
+ const errorResult = result as ErrorResult<TCode, TDetails>;
57
+
58
+ return c.json(
59
+ {
60
+ error: errorResult.error,
61
+ },
62
+ errorResult.status,
63
+ );
64
+ };
@@ -0,0 +1,23 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { getAppConfig } from '@/backend/config';
3
+ import {
4
+ contextKeys,
5
+ type AppEnv,
6
+ type AppLogger,
7
+ } from '@/backend/hono/context';
8
+
9
+ const logger: AppLogger = {
10
+ info: (...args) => console.info(...args),
11
+ error: (...args) => console.error(...args),
12
+ warn: (...args) => console.warn(...args),
13
+ debug: (...args) => console.debug(...args),
14
+ };
15
+
16
+ export const withAppContext = () =>
17
+ createMiddleware<AppEnv>(async (c, next) => {
18
+ const config = getAppConfig();
19
+ c.set(contextKeys.logger, logger);
20
+ c.set(contextKeys.config, config);
21
+
22
+ await next();
23
+ });
@@ -0,0 +1,31 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { match, P } from 'ts-pattern';
3
+ import {
4
+ contextKeys,
5
+ type AppEnv,
6
+ type AppLogger,
7
+ } from '@/backend/hono/context';
8
+
9
+ export const errorBoundary = () =>
10
+ createMiddleware<AppEnv>(async (c, next) => {
11
+ try {
12
+ await next();
13
+ } catch (error) {
14
+ const logger = c.get(contextKeys.logger) as AppLogger | undefined;
15
+ const message = match(error)
16
+ .with(P.instanceOf(Error), (err) => err.message)
17
+ .otherwise(() => 'Unexpected error');
18
+
19
+ logger?.error?.(error);
20
+
21
+ return c.json(
22
+ {
23
+ error: {
24
+ code: 'INTERNAL_SERVER_ERROR',
25
+ message,
26
+ },
27
+ },
28
+ 500,
29
+ );
30
+ }
31
+ });
@@ -0,0 +1,23 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import {
3
+ contextKeys,
4
+ type AppEnv,
5
+ } from '@/backend/hono/context';
6
+ import { createServiceClient } from '@/backend/supabase/client';
7
+
8
+ export const withSupabase = () =>
9
+ createMiddleware<AppEnv>(async (c, next) => {
10
+ const config = c.get(
11
+ contextKeys.config,
12
+ ) as AppEnv['Variables']['config'] | undefined;
13
+
14
+ if (!config) {
15
+ throw new Error('Application configuration is not available.');
16
+ }
17
+
18
+ const client = createServiceClient(config.supabase);
19
+
20
+ c.set(contextKeys.supabase, client);
21
+
22
+ await next();
23
+ });
@@ -0,0 +1,17 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+
4
+ export type ServiceClientConfig = {
5
+ url: string;
6
+ serviceRoleKey: string;
7
+ };
8
+
9
+ export const createServiceClient = ({
10
+ url,
11
+ serviceRoleKey,
12
+ }: ServiceClientConfig): SupabaseClient =>
13
+ createClient(url, serviceRoleKey, {
14
+ auth: {
15
+ persistSession: false,
16
+ },
17
+ });