@townco/debugger 0.1.1

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.
@@ -0,0 +1,131 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@/components/ui/card";
9
+ import type { TraceDetail as TraceDetailType } from "../types";
10
+ import { LogList } from "./LogList";
11
+ import { SpanTree } from "./SpanTree";
12
+
13
+ function formatDuration(startNano: number, endNano: number): string {
14
+ const ms = (endNano - startNano) / 1_000_000;
15
+ if (ms < 1000) return `${ms.toFixed(2)}ms`;
16
+ return `${(ms / 1000).toFixed(2)}s`;
17
+ }
18
+
19
+ function formatTimestamp(nanoseconds: number): string {
20
+ return new Date(nanoseconds / 1_000_000).toLocaleString();
21
+ }
22
+
23
+ interface TraceDetailContentProps {
24
+ traceId: string;
25
+ compact?: boolean;
26
+ }
27
+
28
+ export function TraceDetailContent({
29
+ traceId,
30
+ compact = false,
31
+ }: TraceDetailContentProps) {
32
+ const [data, setData] = useState<TraceDetailType | null>(null);
33
+ const [loading, setLoading] = useState(true);
34
+ const [error, setError] = useState<string | null>(null);
35
+
36
+ useEffect(() => {
37
+ setLoading(true);
38
+ setError(null);
39
+ fetch(`/api/traces/${traceId}`)
40
+ .then((res) => {
41
+ if (!res.ok) {
42
+ if (res.status === 404) throw new Error("Trace not found");
43
+ throw new Error("Failed to fetch trace");
44
+ }
45
+ return res.json();
46
+ })
47
+ .then((data) => {
48
+ setData(data);
49
+ setLoading(false);
50
+ })
51
+ .catch((err) => {
52
+ setError(err.message);
53
+ setLoading(false);
54
+ });
55
+ }, [traceId]);
56
+
57
+ if (loading) {
58
+ return (
59
+ <div className={compact ? "p-4" : "p-8"}>
60
+ <div className="text-muted-foreground">Loading trace...</div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ if (error || !data?.trace) {
66
+ return (
67
+ <div className={compact ? "p-4" : "p-8"}>
68
+ <div className="text-red-500">Error: {error || "Trace not found"}</div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ const { trace, spans, logs } = data;
74
+
75
+ return (
76
+ <div className={compact ? "p-4" : "p-8"}>
77
+ <Card className="mb-6">
78
+ <CardHeader>
79
+ <CardTitle>{trace.first_span_name || "Unknown"}</CardTitle>
80
+ <CardDescription className="space-y-1">
81
+ <div>{trace.service_name || "Unknown service"}</div>
82
+ <div className="font-mono text-xs">{trace.trace_id}</div>
83
+ </CardDescription>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <div className="flex gap-6 text-sm">
87
+ <div>
88
+ <span className="text-muted-foreground">Duration: </span>
89
+ {formatDuration(
90
+ trace.start_time_unix_nano,
91
+ trace.end_time_unix_nano,
92
+ )}
93
+ </div>
94
+ <div>
95
+ <span className="text-muted-foreground">Spans: </span>
96
+ {spans.length}
97
+ </div>
98
+ <div>
99
+ <span className="text-muted-foreground">Logs: </span>
100
+ {logs.length}
101
+ </div>
102
+ <div>
103
+ <span className="text-muted-foreground">Started: </span>
104
+ {formatTimestamp(trace.start_time_unix_nano)}
105
+ </div>
106
+ </div>
107
+ </CardContent>
108
+ </Card>
109
+
110
+ <div className="grid gap-6 lg:grid-cols-2">
111
+ <Card>
112
+ <CardHeader>
113
+ <CardTitle className="text-lg">Spans ({spans.length})</CardTitle>
114
+ </CardHeader>
115
+ <CardContent>
116
+ <SpanTree spans={spans} />
117
+ </CardContent>
118
+ </Card>
119
+
120
+ <Card>
121
+ <CardHeader>
122
+ <CardTitle className="text-lg">Logs ({logs.length})</CardTitle>
123
+ </CardHeader>
124
+ <CardContent>
125
+ <LogList logs={logs} />
126
+ </CardContent>
127
+ </Card>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,60 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ },
37
+ );
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean;
48
+ }) {
49
+ const Comp = asChild ? Slot : "button";
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
@@ -0,0 +1,92 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardAction,
87
+ CardContent,
88
+ CardDescription,
89
+ CardFooter,
90
+ CardHeader,
91
+ CardTitle,
92
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import * as LabelPrimitive from "@radix-ui/react-label";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export { Label };
@@ -0,0 +1,187 @@
1
+ "use client";
2
+
3
+ import * as SelectPrimitive from "@radix-ui/react-select";
4
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ function Select({
10
+ ...props
11
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
12
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
13
+ }
14
+
15
+ function SelectGroup({
16
+ ...props
17
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
18
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
19
+ }
20
+
21
+ function SelectValue({
22
+ ...props
23
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
24
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
25
+ }
26
+
27
+ function SelectTrigger({
28
+ className,
29
+ size = "default",
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
33
+ size?: "sm" | "default";
34
+ }) {
35
+ return (
36
+ <SelectPrimitive.Trigger
37
+ data-slot="select-trigger"
38
+ data-size={size}
39
+ className={cn(
40
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
41
+ className,
42
+ )}
43
+ {...props}
44
+ >
45
+ {children}
46
+ <SelectPrimitive.Icon asChild>
47
+ <ChevronDownIcon className="size-4 opacity-50" />
48
+ </SelectPrimitive.Icon>
49
+ </SelectPrimitive.Trigger>
50
+ );
51
+ }
52
+
53
+ function SelectContent({
54
+ className,
55
+ children,
56
+ position = "popper",
57
+ align = "center",
58
+ ...props
59
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
60
+ return (
61
+ <SelectPrimitive.Portal>
62
+ <SelectPrimitive.Content
63
+ data-slot="select-content"
64
+ className={cn(
65
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
66
+ position === "popper" &&
67
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
68
+ className,
69
+ )}
70
+ position={position}
71
+ align={align}
72
+ {...props}
73
+ >
74
+ <SelectScrollUpButton />
75
+ <SelectPrimitive.Viewport
76
+ className={cn(
77
+ "p-1",
78
+ position === "popper" &&
79
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
80
+ )}
81
+ >
82
+ {children}
83
+ </SelectPrimitive.Viewport>
84
+ <SelectScrollDownButton />
85
+ </SelectPrimitive.Content>
86
+ </SelectPrimitive.Portal>
87
+ );
88
+ }
89
+
90
+ function SelectLabel({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
94
+ return (
95
+ <SelectPrimitive.Label
96
+ data-slot="select-label"
97
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
98
+ {...props}
99
+ />
100
+ );
101
+ }
102
+
103
+ function SelectItem({
104
+ className,
105
+ children,
106
+ ...props
107
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
108
+ return (
109
+ <SelectPrimitive.Item
110
+ data-slot="select-item"
111
+ className={cn(
112
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
113
+ className,
114
+ )}
115
+ {...props}
116
+ >
117
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
118
+ <SelectPrimitive.ItemIndicator>
119
+ <CheckIcon className="size-4" />
120
+ </SelectPrimitive.ItemIndicator>
121
+ </span>
122
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
123
+ </SelectPrimitive.Item>
124
+ );
125
+ }
126
+
127
+ function SelectSeparator({
128
+ className,
129
+ ...props
130
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
131
+ return (
132
+ <SelectPrimitive.Separator
133
+ data-slot="select-separator"
134
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
135
+ {...props}
136
+ />
137
+ );
138
+ }
139
+
140
+ function SelectScrollUpButton({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
144
+ return (
145
+ <SelectPrimitive.ScrollUpButton
146
+ data-slot="select-scroll-up-button"
147
+ className={cn(
148
+ "flex cursor-default items-center justify-center py-1",
149
+ className,
150
+ )}
151
+ {...props}
152
+ >
153
+ <ChevronUpIcon className="size-4" />
154
+ </SelectPrimitive.ScrollUpButton>
155
+ );
156
+ }
157
+
158
+ function SelectScrollDownButton({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
162
+ return (
163
+ <SelectPrimitive.ScrollDownButton
164
+ data-slot="select-scroll-down-button"
165
+ className={cn(
166
+ "flex cursor-default items-center justify-center py-1",
167
+ className,
168
+ )}
169
+ {...props}
170
+ >
171
+ <ChevronDownIcon className="size-4" />
172
+ </SelectPrimitive.ScrollDownButton>
173
+ );
174
+ }
175
+
176
+ export {
177
+ Select,
178
+ SelectContent,
179
+ SelectGroup,
180
+ SelectItem,
181
+ SelectLabel,
182
+ SelectScrollDownButton,
183
+ SelectScrollUpButton,
184
+ SelectSeparator,
185
+ SelectTrigger,
186
+ SelectValue,
187
+ };
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Textarea };
package/src/db.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { Database } from "bun:sqlite";
2
+ import type { Log, Span, Trace, TraceDetail } from "./types";
3
+
4
+ export class DebuggerDb {
5
+ private db: Database;
6
+
7
+ constructor(dbPath: string) {
8
+ this.db = new Database(dbPath, { readonly: true });
9
+ }
10
+
11
+ listTraces(limit = 50, offset = 0, sessionId?: string): Trace[] {
12
+ if (sessionId) {
13
+ return this.db
14
+ .query<Trace, [string, number, number]>(
15
+ `
16
+ SELECT DISTINCT t.trace_id, t.service_name, t.first_span_name,
17
+ t.start_time_unix_nano, t.end_time_unix_nano, t.span_count, t.created_at
18
+ FROM traces t
19
+ INNER JOIN spans s ON s.trace_id = t.trace_id
20
+ WHERE json_extract(s.attributes, '$."agent.session_id"') = ?
21
+ ORDER BY t.start_time_unix_nano DESC
22
+ LIMIT ? OFFSET ?
23
+ `,
24
+ )
25
+ .all(sessionId, limit, offset);
26
+ }
27
+
28
+ return this.db
29
+ .query<Trace, [number, number]>(
30
+ `
31
+ SELECT trace_id, service_name, first_span_name,
32
+ start_time_unix_nano, end_time_unix_nano, span_count, created_at
33
+ FROM traces
34
+ ORDER BY start_time_unix_nano DESC
35
+ LIMIT ? OFFSET ?
36
+ `,
37
+ )
38
+ .all(limit, offset);
39
+ }
40
+
41
+ getTraceById(traceId: string): TraceDetail {
42
+ const trace = this.db
43
+ .query<Trace, [string]>(`SELECT * FROM traces WHERE trace_id = ?`)
44
+ .get(traceId);
45
+
46
+ const spans = this.db
47
+ .query<Span, [string]>(
48
+ `SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time_unix_nano`,
49
+ )
50
+ .all(traceId);
51
+
52
+ const logs = this.db
53
+ .query<Log, [string]>(
54
+ `SELECT * FROM logs WHERE trace_id = ? ORDER BY timestamp_unix_nano`,
55
+ )
56
+ .all(traceId);
57
+
58
+ return { trace: trace ?? null, spans, logs };
59
+ }
60
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ declare module "*.css" {
2
+ const content: string;
3
+ export default content;
4
+ }
5
+
6
+ declare module "*.svg" {
7
+ const content: string;
8
+ export default content;
9
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * This file is the entry point for the React app, it sets up the root
3
+ * element and renders the App component to the DOM.
4
+ *
5
+ * It is included in `src/index.html`.
6
+ */
7
+
8
+ import { StrictMode } from "react";
9
+ import { createRoot } from "react-dom/client";
10
+ import { App } from "./App";
11
+
12
+ const elem = document.getElementById("root")!;
13
+ const app = (
14
+ <StrictMode>
15
+ <App />
16
+ </StrictMode>
17
+ );
18
+
19
+ if (import.meta.hot) {
20
+ // With hot module reloading, `import.meta.hot.data` is persisted.
21
+ const root = (import.meta.hot.data.root ??= createRoot(elem));
22
+ root.render(app);
23
+ } else {
24
+ // The hot module reloading API is not available in production.
25
+ createRoot(elem).render(app);
26
+ }
package/src/index.css ADDED
@@ -0,0 +1,11 @@
1
+ @import "../styles/globals.css";
2
+
3
+ @layer base {
4
+ :root {
5
+ @apply font-sans;
6
+ }
7
+
8
+ body {
9
+ @apply min-h-screen bg-background text-foreground;
10
+ }
11
+ }
package/src/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="./logo.svg" />
7
+ <title>Town Exterminator</title>
8
+ <script type="module" src="./frontend.tsx" async></script>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import {
2
+ DEFAULT_DEBUGGER_PORT,
3
+ DEFAULT_OTLP_PORT,
4
+ startDebuggerServer,
5
+ } from "./server";
6
+
7
+ const port = Number.parseInt(
8
+ process.env.PORT ?? String(DEFAULT_DEBUGGER_PORT),
9
+ 10,
10
+ );
11
+ const otlpPort = Number.parseInt(
12
+ process.env.OTLP_PORT ?? String(DEFAULT_OTLP_PORT),
13
+ 10,
14
+ );
15
+ const dbPath = process.env.DB_PATH ?? "./traces.db";
16
+
17
+ const { server, otlpServer } = startDebuggerServer({ port, otlpPort, dbPath });
18
+
19
+ console.log(`OTLP server running at ${otlpServer.url}`);
20
+ console.log(`Debugger running at ${server.url}`);
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }