@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.
- package/README.md +374 -0
- package/bin/task-mcp.mjs +19 -0
- package/bin/task.mjs +19 -0
- package/docs/clean-code.md +29 -0
- package/docs/git.md +36 -0
- package/docs/guideline.md +25 -0
- package/docs/security.md +32 -0
- package/docs/step-by-step.md +29 -0
- package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
- package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
- package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
- package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
- package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
- package/docs/tdd.md +41 -0
- package/package.json +114 -0
- package/src/app/(protected)/dashboard/page.tsx +7 -0
- package/src/app/(protected)/layout.tsx +10 -0
- package/src/app/api/[[...hono]]/route.ts +13 -0
- package/src/app/example/page.tsx +11 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +168 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +5 -0
- package/src/app/providers.tsx +57 -0
- package/src/backend/config/index.ts +36 -0
- package/src/backend/hono/app.ts +32 -0
- package/src/backend/hono/context.ts +38 -0
- package/src/backend/http/response.ts +64 -0
- package/src/backend/middleware/context.ts +23 -0
- package/src/backend/middleware/error.ts +31 -0
- package/src/backend/middleware/supabase.ts +23 -0
- package/src/backend/supabase/client.ts +17 -0
- package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
- package/src/cli/commands/advisor.ts +45 -0
- package/src/cli/commands/ask.ts +50 -0
- package/src/cli/commands/board.ts +72 -0
- package/src/cli/commands/init.ts +184 -0
- package/src/cli/commands/list.ts +138 -0
- package/src/cli/commands/run.ts +143 -0
- package/src/cli/commands/set-status.ts +50 -0
- package/src/cli/commands/show.ts +28 -0
- package/src/cli/commands/tree.ts +72 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/lib/__tests__/formatter.test.ts +123 -0
- package/src/cli/lib/error-boundary.test.ts +135 -0
- package/src/cli/lib/error-boundary.ts +70 -0
- package/src/cli/lib/formatter.ts +764 -0
- package/src/cli/lib/trd.ts +33 -0
- package/src/cli/lib/validate.test.ts +89 -0
- package/src/cli/lib/validate.ts +43 -0
- package/src/cli/prompts/task-run.md +25 -0
- package/src/components/layout/AppLayout.tsx +15 -0
- package/src/components/layout/Sidebar.tsx +124 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +50 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +129 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/core/ai/claude-client.ts +79 -0
- package/src/core/claude-runner/flag-builder.ts +57 -0
- package/src/core/claude-runner/index.ts +2 -0
- package/src/core/claude-runner/spawner.ts +86 -0
- package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
- package/src/core/prd/__tests__/generator.test.ts +26 -0
- package/src/core/prd/__tests__/scanner.test.ts +35 -0
- package/src/core/prd/auto-analyzer.ts +9 -0
- package/src/core/prd/generator.ts +8 -0
- package/src/core/prd/scanner.ts +117 -0
- package/src/core/project/__tests__/claude-setup.test.ts +133 -0
- package/src/core/project/__tests__/config.test.ts +30 -0
- package/src/core/project/__tests__/init.test.ts +37 -0
- package/src/core/project/__tests__/skill-setup.test.ts +62 -0
- package/src/core/project/claude-setup.ts +224 -0
- package/src/core/project/config.ts +34 -0
- package/src/core/project/docs-setup.ts +26 -0
- package/src/core/project/docs-templates.ts +205 -0
- package/src/core/project/init.ts +40 -0
- package/src/core/project/skill-setup.ts +32 -0
- package/src/core/project/skill-templates.ts +277 -0
- package/src/core/task/index.ts +16 -0
- package/src/core/types.ts +58 -0
- package/src/features/example/backend/error.ts +9 -0
- package/src/features/example/backend/route.ts +52 -0
- package/src/features/example/backend/schema.ts +25 -0
- package/src/features/example/backend/service.ts +73 -0
- package/src/features/example/components/example-status.test.tsx +97 -0
- package/src/features/example/components/example-status.tsx +160 -0
- package/src/features/example/hooks/useExampleQuery.ts +23 -0
- package/src/features/example/lib/dto.test.ts +57 -0
- package/src/features/example/lib/dto.ts +5 -0
- package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
- package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
- package/src/features/kanban/backend/route.ts +55 -0
- package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
- package/src/features/kanban/backend/sse-route.ts +43 -0
- package/src/features/kanban/components/KanbanBoard.tsx +105 -0
- package/src/features/kanban/components/KanbanColumn.tsx +51 -0
- package/src/features/kanban/components/KanbanError.tsx +29 -0
- package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
- package/src/features/kanban/components/ProgressCard.tsx +42 -0
- package/src/features/kanban/components/TaskCard.tsx +76 -0
- package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
- package/src/features/kanban/hooks/useTaskSse.ts +66 -0
- package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
- package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
- package/src/features/kanban/lib/kanban-utils.ts +37 -0
- package/src/features/taskflow/constants.ts +54 -0
- package/src/features/taskflow/index.ts +27 -0
- package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
- package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
- package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
- package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
- package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
- package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
- package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
- package/src/features/taskflow/lib/advisor/db.ts +185 -0
- package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
- package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
- package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
- package/src/features/taskflow/lib/filter.ts +54 -0
- package/src/features/taskflow/lib/fs-utils.ts +50 -0
- package/src/features/taskflow/lib/graph.ts +148 -0
- package/src/features/taskflow/lib/index-builder.ts +42 -0
- package/src/features/taskflow/lib/repository.ts +168 -0
- package/src/features/taskflow/lib/serializer.ts +62 -0
- package/src/features/taskflow/lib/watcher.ts +40 -0
- package/src/features/taskflow/types.ts +71 -0
- package/src/hooks/use-toast.ts +194 -0
- package/src/lib/remote/api-client.ts +40 -0
- package/src/lib/supabase/client.ts +8 -0
- package/src/lib/supabase/server.ts +46 -0
- package/src/lib/supabase/types.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/server.ts +21 -0
- package/src/mcp/tools/brainstorm.ts +48 -0
- package/src/mcp/tools/prd.ts +71 -0
- package/src/mcp/tools/project.ts +39 -0
- package/src/mcp/tools/task-status.ts +40 -0
- package/src/mcp/tools/task.ts +82 -0
- 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
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -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
|
+
});
|