@gamflaz/opencode-todolist 1.0.12 → 1.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.
- package/README.md +61 -32
- package/dist/icons/glyph.d.ts +77 -0
- package/dist/icons/glyph.js +103 -0
- package/dist/icons/glyphs.d.ts +51 -0
- package/dist/icons/glyphs.js +56 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +41 -3
- package/dist/schemas/todo.d.ts +76 -58
- package/dist/schemas/todo.js +73 -18
- package/dist/storage/db.d.ts +71 -0
- package/dist/storage/db.js +59 -0
- package/dist/storage/migrate-json.d.ts +82 -0
- package/dist/storage/migrate-json.js +278 -0
- package/dist/storage/schema.d.ts +775 -0
- package/dist/storage/schema.js +108 -0
- package/dist/storage/todo-store.d.ts +104 -56
- package/dist/storage/todo-store.js +178 -297
- package/dist/todos/hint.d.ts +13 -2
- package/dist/todos/hint.js +16 -19
- package/dist/todos/todo-tree.d.ts +16 -3
- package/dist/todos/todo-tree.js +22 -4
- package/dist/todos/update.js +2 -2
- package/dist/tui/todo-panel.js +73 -65
- package/drizzle/20261014050406_eminent_zaran/migration.sql +49 -0
- package/drizzle/20261014050406_eminent_zaran/snapshot.json +470 -0
- package/package.json +7 -5
package/dist/schemas/todo.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
-
|
|
2
|
+
/** A top-level id only: `1`, `2`, ... — never a dotted subtask id. */
|
|
3
|
+
export declare const RootIdSchema: z.ZodNumber;
|
|
4
|
+
export declare const ChildIdSchema: z.ZodNumber;
|
|
5
|
+
export declare const TodoTargetIdSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
6
|
+
export declare const TodoChildrenItemSchema: z.ZodObject<{
|
|
3
7
|
id: z.ZodNumber;
|
|
4
8
|
content: z.ZodString;
|
|
5
9
|
status: z.ZodEnum<{
|
|
@@ -15,8 +19,7 @@ export declare const TodoChildernItemSchema: z.ZodObject<{
|
|
|
15
19
|
medium: "medium";
|
|
16
20
|
}>>;
|
|
17
21
|
note: z.ZodOptional<z.ZodString>;
|
|
18
|
-
}, z.core.$
|
|
19
|
-
export type TodoChildern = z.infer<typeof TodoChildernItemSchema>;
|
|
22
|
+
}, z.core.$strict>;
|
|
20
23
|
export declare const TodoRootItemSchema: z.ZodObject<{
|
|
21
24
|
id: z.ZodNumber;
|
|
22
25
|
content: z.ZodString;
|
|
@@ -49,9 +52,8 @@ export declare const TodoRootItemSchema: z.ZodObject<{
|
|
|
49
52
|
medium: "medium";
|
|
50
53
|
}>>;
|
|
51
54
|
note: z.ZodOptional<z.ZodString>;
|
|
52
|
-
}, z.core.$
|
|
53
|
-
}, z.core.$
|
|
54
|
-
export type TodoRootItem = z.infer<typeof TodoRootItemSchema>;
|
|
55
|
+
}, z.core.$strict>>>;
|
|
56
|
+
}, z.core.$strict>;
|
|
55
57
|
export declare const formatRootId: (id: number) => string;
|
|
56
58
|
export declare const formatChildId: (id: number) => string;
|
|
57
59
|
export declare const TodoListSchema: z.ZodArray<z.ZodObject<{
|
|
@@ -86,49 +88,43 @@ export declare const TodoListSchema: z.ZodArray<z.ZodObject<{
|
|
|
86
88
|
medium: "medium";
|
|
87
89
|
}>>;
|
|
88
90
|
note: z.ZodOptional<z.ZodString>;
|
|
89
|
-
}, z.core.$
|
|
90
|
-
}, z.core.$
|
|
91
|
-
export declare const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
cancelled: "cancelled";
|
|
97
|
-
completed: "completed";
|
|
98
|
-
error: "error";
|
|
99
|
-
in_progress: "in_progress";
|
|
100
|
-
pending: "pending";
|
|
101
|
-
}>;
|
|
102
|
-
priority: z.ZodOptional<z.ZodEnum<{
|
|
103
|
-
high: "high";
|
|
104
|
-
low: "low";
|
|
105
|
-
medium: "medium";
|
|
106
|
-
}>>;
|
|
107
|
-
note: z.ZodOptional<z.ZodString>;
|
|
108
|
-
children: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
109
|
-
id: z.ZodNumber;
|
|
110
|
-
content: z.ZodString;
|
|
111
|
-
status: z.ZodEnum<{
|
|
112
|
-
cancelled: "cancelled";
|
|
113
|
-
completed: "completed";
|
|
114
|
-
error: "error";
|
|
115
|
-
in_progress: "in_progress";
|
|
116
|
-
pending: "pending";
|
|
117
|
-
}>;
|
|
118
|
-
priority: z.ZodOptional<z.ZodEnum<{
|
|
119
|
-
high: "high";
|
|
120
|
-
low: "low";
|
|
121
|
-
medium: "medium";
|
|
122
|
-
}>>;
|
|
123
|
-
note: z.ZodOptional<z.ZodString>;
|
|
124
|
-
}, z.core.$strip>>>;
|
|
125
|
-
}, z.core.$strip>>;
|
|
126
|
-
}, z.core.$strip>;
|
|
127
|
-
export type TodoWriteInput = z.infer<typeof TodoWriteInputSchema>;
|
|
128
|
-
export declare const TodoTargetIdSchema: z.ZodNumber;
|
|
129
|
-
export declare const TodoReadOperationSchema: z.ZodObject<{
|
|
91
|
+
}, z.core.$strict>>>;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
export declare const TodoReadAllSchema: z.ZodObject<{
|
|
94
|
+
operation: z.ZodLiteral<"read">;
|
|
95
|
+
scope: z.ZodDefault<z.ZodLiteral<"all">>;
|
|
96
|
+
}, z.core.$strict>;
|
|
97
|
+
export declare const TodoReadItemSchema: z.ZodObject<{
|
|
130
98
|
operation: z.ZodLiteral<"read">;
|
|
99
|
+
scope: z.ZodLiteral<"item">;
|
|
100
|
+
target: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
131
101
|
}, z.core.$strict>;
|
|
102
|
+
export declare const TodoReadChildrenSchema: z.ZodObject<{
|
|
103
|
+
operation: z.ZodLiteral<"read">;
|
|
104
|
+
scope: z.ZodLiteral<"children">;
|
|
105
|
+
target: z.ZodNumber;
|
|
106
|
+
}, z.core.$strict>;
|
|
107
|
+
export declare const TodoReadRootSchema: z.ZodObject<{
|
|
108
|
+
operation: z.ZodLiteral<"read">;
|
|
109
|
+
scope: z.ZodLiteral<"root">;
|
|
110
|
+
target: z.ZodNumber;
|
|
111
|
+
}, z.core.$strict>;
|
|
112
|
+
export declare const TodoReadOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
113
|
+
operation: z.ZodLiteral<"read">;
|
|
114
|
+
scope: z.ZodDefault<z.ZodLiteral<"all">>;
|
|
115
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
116
|
+
operation: z.ZodLiteral<"read">;
|
|
117
|
+
scope: z.ZodLiteral<"item">;
|
|
118
|
+
target: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
119
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
120
|
+
operation: z.ZodLiteral<"read">;
|
|
121
|
+
scope: z.ZodLiteral<"children">;
|
|
122
|
+
target: z.ZodNumber;
|
|
123
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
124
|
+
operation: z.ZodLiteral<"read">;
|
|
125
|
+
scope: z.ZodLiteral<"root">;
|
|
126
|
+
target: z.ZodNumber;
|
|
127
|
+
}, z.core.$strict>], "scope">;
|
|
132
128
|
export declare const TodoWriteOperationSchema: z.ZodObject<{
|
|
133
129
|
operation: z.ZodLiteral<"write">;
|
|
134
130
|
todos: z.ZodArray<z.ZodObject<{
|
|
@@ -163,12 +159,12 @@ export declare const TodoWriteOperationSchema: z.ZodObject<{
|
|
|
163
159
|
medium: "medium";
|
|
164
160
|
}>>;
|
|
165
161
|
note: z.ZodOptional<z.ZodString>;
|
|
166
|
-
}, z.core.$
|
|
167
|
-
}, z.core.$
|
|
162
|
+
}, z.core.$strict>>>;
|
|
163
|
+
}, z.core.$strict>>;
|
|
168
164
|
}, z.core.$strict>;
|
|
169
165
|
export declare const TodoUpdateOperationSchema: z.ZodObject<{
|
|
170
166
|
operation: z.ZodLiteral<"update">;
|
|
171
|
-
id: z.ZodNumber
|
|
167
|
+
id: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
172
168
|
status: z.ZodEnum<{
|
|
173
169
|
cancelled: "cancelled";
|
|
174
170
|
completed: "completed";
|
|
@@ -184,10 +180,22 @@ export declare const TodoUpdateOperationSchema: z.ZodObject<{
|
|
|
184
180
|
}>>>;
|
|
185
181
|
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
186
182
|
}, z.core.$strict>;
|
|
187
|
-
export
|
|
188
|
-
|
|
183
|
+
export declare const TodoInputToolSchema: z.ZodDiscriminatedUnion<[z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
184
|
+
operation: z.ZodLiteral<"read">;
|
|
185
|
+
scope: z.ZodDefault<z.ZodLiteral<"all">>;
|
|
186
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
189
187
|
operation: z.ZodLiteral<"read">;
|
|
188
|
+
scope: z.ZodLiteral<"item">;
|
|
189
|
+
target: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
190
190
|
}, z.core.$strict>, z.ZodObject<{
|
|
191
|
+
operation: z.ZodLiteral<"read">;
|
|
192
|
+
scope: z.ZodLiteral<"children">;
|
|
193
|
+
target: z.ZodNumber;
|
|
194
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
195
|
+
operation: z.ZodLiteral<"read">;
|
|
196
|
+
scope: z.ZodLiteral<"root">;
|
|
197
|
+
target: z.ZodNumber;
|
|
198
|
+
}, z.core.$strict>], "scope">, z.ZodObject<{
|
|
191
199
|
operation: z.ZodLiteral<"write">;
|
|
192
200
|
todos: z.ZodArray<z.ZodObject<{
|
|
193
201
|
id: z.ZodNumber;
|
|
@@ -221,11 +229,11 @@ export declare const TodoInputToolSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
221
229
|
medium: "medium";
|
|
222
230
|
}>>;
|
|
223
231
|
note: z.ZodOptional<z.ZodString>;
|
|
224
|
-
}, z.core.$
|
|
225
|
-
}, z.core.$
|
|
232
|
+
}, z.core.$strict>>>;
|
|
233
|
+
}, z.core.$strict>>;
|
|
226
234
|
}, z.core.$strict>, z.ZodObject<{
|
|
227
235
|
operation: z.ZodLiteral<"update">;
|
|
228
|
-
id: z.ZodNumber
|
|
236
|
+
id: z.ZodUnion<readonly [z.ZodNumber, z.ZodNumber]>;
|
|
229
237
|
status: z.ZodEnum<{
|
|
230
238
|
cancelled: "cancelled";
|
|
231
239
|
completed: "completed";
|
|
@@ -241,7 +249,6 @@ export declare const TodoInputToolSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
241
249
|
}>>>;
|
|
242
250
|
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
243
251
|
}, z.core.$strict>], "operation">;
|
|
244
|
-
export type TodoInputTool = z.infer<typeof TodoInputToolSchema>;
|
|
245
252
|
export declare const TodoOutputToolSchema: z.ZodObject<{
|
|
246
253
|
todos: z.ZodArray<z.ZodObject<{
|
|
247
254
|
id: z.ZodNumber;
|
|
@@ -275,7 +282,18 @@ export declare const TodoOutputToolSchema: z.ZodObject<{
|
|
|
275
282
|
medium: "medium";
|
|
276
283
|
}>>;
|
|
277
284
|
note: z.ZodOptional<z.ZodString>;
|
|
278
|
-
}, z.core.$
|
|
279
|
-
}, z.core.$
|
|
285
|
+
}, z.core.$strict>>>;
|
|
286
|
+
}, z.core.$strict>>;
|
|
280
287
|
}, z.core.$strip>;
|
|
288
|
+
export type TodoChildren = z.infer<typeof TodoChildrenItemSchema>;
|
|
289
|
+
export type TodoRootItem = z.infer<typeof TodoRootItemSchema>;
|
|
290
|
+
export type TodoReadAllInput = z.infer<typeof TodoReadAllSchema>;
|
|
291
|
+
export type TodoReadItemInput = z.infer<typeof TodoReadItemSchema>;
|
|
292
|
+
export type TodoReadChildrenInput = z.infer<typeof TodoReadChildrenSchema>;
|
|
293
|
+
export type TodoReadRootInput = z.infer<typeof TodoReadRootSchema>;
|
|
294
|
+
export type TodoReadInput = z.infer<typeof TodoReadOperationSchema>;
|
|
295
|
+
export type TodoReadScope = TodoReadInput["scope"];
|
|
296
|
+
export type TodoWriteOperationInput = z.infer<typeof TodoWriteOperationSchema>;
|
|
297
|
+
export type TodoUpdateInput = z.infer<typeof TodoUpdateOperationSchema>;
|
|
298
|
+
export type TodoInputTool = z.infer<typeof TodoInputToolSchema>;
|
|
281
299
|
export type TodoOutputTool = z.infer<typeof TodoOutputToolSchema>;
|
package/dist/schemas/todo.js
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { PrioritySchema, TodoStatusSchema } from "./status.js";
|
|
3
|
+
/** A top-level id only: `1`, `2`, ... — never a dotted subtask id. */
|
|
4
|
+
export const RootIdSchema = z
|
|
5
|
+
.number()
|
|
6
|
+
.int()
|
|
7
|
+
.positive()
|
|
8
|
+
.describe("Unique task identifier (e.g. 1)");
|
|
3
9
|
// Child id format: <root_id>.<n> as a number -> 1.1, 2.3, etc.
|
|
4
10
|
// Only a single decimal digit (1-9) is allowed, see the note below.
|
|
5
|
-
const ChildIdSchema = z
|
|
11
|
+
export const ChildIdSchema = z
|
|
6
12
|
.number()
|
|
7
13
|
.refine((n) => /^[1-9]\d*\.[1-9]$/.test(String(n)), {
|
|
8
14
|
message: "Child id must follow the format <root_id>.<n> with n from 1 to 9, e.g. 1.1",
|
|
9
15
|
})
|
|
10
16
|
.describe("Unique subtask identifier, format <root_id>.<n> (e.g. 1.1)");
|
|
17
|
+
// Root or child id, composed from the two schemas above so the id rules
|
|
18
|
+
// live in exactly one place.
|
|
19
|
+
export const TodoTargetIdSchema = z
|
|
20
|
+
.union([RootIdSchema, ChildIdSchema])
|
|
21
|
+
.describe("Root or child todo id, e.g. 1 or 1.1");
|
|
11
22
|
// TodoItem Children SCHEMA
|
|
12
|
-
export const
|
|
23
|
+
export const TodoChildrenItemSchema = z
|
|
24
|
+
.object({
|
|
13
25
|
id: ChildIdSchema,
|
|
14
26
|
content: z.string().describe("Subtask description"),
|
|
15
27
|
status: TodoStatusSchema,
|
|
16
28
|
priority: PrioritySchema.optional(),
|
|
17
29
|
note: z.string().optional(),
|
|
18
|
-
})
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
19
32
|
// TodoItem Root SCHEMA
|
|
20
33
|
export const TodoRootItemSchema = z
|
|
21
34
|
.object({
|
|
22
|
-
id:
|
|
35
|
+
id: RootIdSchema,
|
|
23
36
|
content: z.string().describe("Main task summary"),
|
|
24
37
|
status: TodoStatusSchema,
|
|
25
38
|
priority: PrioritySchema.optional(),
|
|
26
39
|
note: z.string().optional(),
|
|
27
|
-
children: z.array(
|
|
40
|
+
children: z.array(TodoChildrenItemSchema).max(9).optional(),
|
|
28
41
|
})
|
|
42
|
+
.strict()
|
|
29
43
|
.superRefine((root, ctx) => {
|
|
30
44
|
const seen = new Set();
|
|
31
45
|
if (root.children) {
|
|
@@ -70,32 +84,73 @@ export const TodoListSchema = z
|
|
|
70
84
|
});
|
|
71
85
|
})
|
|
72
86
|
.describe("Hierarchical task list");
|
|
73
|
-
// TodoHook Write Input
|
|
74
|
-
export const TodoWriteInputSchema = z.object({
|
|
75
|
-
todos: TodoListSchema,
|
|
76
|
-
});
|
|
77
|
-
export const TodoTargetIdSchema = z
|
|
78
|
-
.number()
|
|
79
|
-
.positive()
|
|
80
|
-
.refine((id) => Number.isInteger(id) || /^[1-9]\d*\.[1-9]$/.test(String(id)), "ID must be a root number or child number such as 1 or 1.1");
|
|
81
87
|
// TodoHook Tool Schema
|
|
82
|
-
|
|
88
|
+
//
|
|
89
|
+
// `read` is a discriminated union on `scope`, so an impossible combination
|
|
90
|
+
// cannot be written at all: `target` exists only alongside `scope: "item"`,
|
|
91
|
+
// `"children"` and `"root"`, none of which is valid without one. `.strict()`
|
|
92
|
+
// on every member is what enforces that — without it
|
|
93
|
+
// `{ scope: "all", target: 1 }` would parse and silently ignore `target`.
|
|
94
|
+
//
|
|
95
|
+
// An omitted `scope` defaults to "all", so a bare `{ operation: "read" }` keeps
|
|
96
|
+
// behaving exactly as it did before this option existed. That default has to
|
|
97
|
+
// live on the discriminator itself; a member without `scope` is rejected at
|
|
98
|
+
// construction time.
|
|
99
|
+
const readOperation = z
|
|
100
|
+
.literal("read")
|
|
101
|
+
.describe("'read' to inspect current todos.");
|
|
102
|
+
export const TodoReadAllSchema = z
|
|
83
103
|
.object({
|
|
84
|
-
operation:
|
|
104
|
+
operation: readOperation,
|
|
105
|
+
scope: z
|
|
106
|
+
.literal("all")
|
|
107
|
+
.default("all")
|
|
108
|
+
.describe("Default. Return the whole list."),
|
|
85
109
|
})
|
|
86
110
|
.strict();
|
|
111
|
+
export const TodoReadItemSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
operation: readOperation,
|
|
114
|
+
scope: z.literal("item").describe("Return only the targeted item."),
|
|
115
|
+
target: TodoTargetIdSchema.describe("Root or child todo id, e.g. 1 or 1.1."),
|
|
116
|
+
})
|
|
117
|
+
.strict();
|
|
118
|
+
export const TodoReadChildrenSchema = z
|
|
119
|
+
.object({
|
|
120
|
+
operation: readOperation,
|
|
121
|
+
scope: z
|
|
122
|
+
.literal("children")
|
|
123
|
+
.describe("Return one root and its subtasks. The root is included as context."),
|
|
124
|
+
target: ChildIdSchema.describe("A subtask id such as 1.1; its root is resolved from the id."),
|
|
125
|
+
})
|
|
126
|
+
.strict();
|
|
127
|
+
export const TodoReadRootSchema = z
|
|
128
|
+
.object({
|
|
129
|
+
operation: readOperation,
|
|
130
|
+
scope: z
|
|
131
|
+
.literal("root")
|
|
132
|
+
.describe("Return exactly one root together with all of its subtasks."),
|
|
133
|
+
target: RootIdSchema.describe("Root todo id, e.g. 1."),
|
|
134
|
+
})
|
|
135
|
+
.strict();
|
|
136
|
+
export const TodoReadOperationSchema = z.discriminatedUnion("scope", [
|
|
137
|
+
TodoReadAllSchema,
|
|
138
|
+
TodoReadItemSchema,
|
|
139
|
+
TodoReadChildrenSchema,
|
|
140
|
+
TodoReadRootSchema,
|
|
141
|
+
]);
|
|
87
142
|
export const TodoWriteOperationSchema = z
|
|
88
143
|
.object({
|
|
89
144
|
operation: z
|
|
90
145
|
.literal("write")
|
|
91
|
-
.describe("'write' to
|
|
146
|
+
.describe("'write' to replace the whole list. Pass [] to clear it."),
|
|
92
147
|
todos: TodoListSchema.describe("Required for 'write'. Pass [] to clear the list."),
|
|
93
148
|
})
|
|
94
149
|
.strict();
|
|
95
150
|
export const TodoUpdateOperationSchema = z
|
|
96
151
|
.object({
|
|
97
152
|
operation: z.literal("update").describe("'update' one root or child todo."),
|
|
98
|
-
id: TodoTargetIdSchema
|
|
153
|
+
id: TodoTargetIdSchema,
|
|
99
154
|
status: TodoStatusSchema.describe("New status for the target todo."),
|
|
100
155
|
content: z.string().optional(),
|
|
101
156
|
priority: PrioritySchema.nullable().optional(),
|
|
@@ -108,5 +163,5 @@ export const TodoInputToolSchema = z.discriminatedUnion("operation", [
|
|
|
108
163
|
TodoUpdateOperationSchema,
|
|
109
164
|
]);
|
|
110
165
|
export const TodoOutputToolSchema = z.object({
|
|
111
|
-
todos:
|
|
166
|
+
todos: TodoListSchema,
|
|
112
167
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
|
+
import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
/**
|
|
4
|
+
* Database connection for todo storage.
|
|
5
|
+
*
|
|
6
|
+
* One file holds every session; rows are partitioned by `session_key`. Two
|
|
7
|
+
* separate OpenCode processes reach the same file:
|
|
8
|
+
*
|
|
9
|
+
* - the server process runs `manage_todo_list` and writes
|
|
10
|
+
* - the CLI process renders the TUI widget and reads
|
|
11
|
+
*
|
|
12
|
+
* They share no memory, so the PRAGMAs below are not decoration:
|
|
13
|
+
*
|
|
14
|
+
* - `journal_mode = WAL` lets a reader work while a writer commits
|
|
15
|
+
* - `busy_timeout` makes a concurrent writer wait instead of failing
|
|
16
|
+
* - `foreign_keys = ON` is per-connection in SQLite, so it must be set on
|
|
17
|
+
* every open or the composite parent/child constraint silently stops
|
|
18
|
+
* being enforced
|
|
19
|
+
*
|
|
20
|
+
* PRAGMAs run before `migrate()` so the migration itself benefits from WAL and
|
|
21
|
+
* enforced foreign keys.
|
|
22
|
+
*
|
|
23
|
+
* `run` is used rather than `exec`: in `bun:sqlite` they are the same call, but
|
|
24
|
+
* `exec` is deprecated in favour of `run`, and a deprecated alias on the hot
|
|
25
|
+
* path is noise in every build.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* No relations generic: this schema is queried with the core builder
|
|
29
|
+
* (`select`/`insert`/`update`/`delete`), never with relational `db.query`.
|
|
30
|
+
* Drizzle 1.0 dropped `schema` from the SQLite config for that reason, so there
|
|
31
|
+
* is nothing to thread through here.
|
|
32
|
+
*/
|
|
33
|
+
export type TodoDatabase = SQLiteBunDatabase;
|
|
34
|
+
export interface TodoDatabaseHandle {
|
|
35
|
+
/** Raw connection, for statements Drizzle does not expose (e.g. PRAGMA). */
|
|
36
|
+
readonly sqlite: BunDatabase;
|
|
37
|
+
readonly db: TodoDatabase;
|
|
38
|
+
readonly path: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Folder holding the generated migrations.
|
|
42
|
+
*
|
|
43
|
+
* `import.meta.dirname` resolves to the plugin's own directory because OpenCode
|
|
44
|
+
* loads local plugins as ordinary files from disk (not embedded in the host
|
|
45
|
+
* binary). That was verified against a standalone Bun host importing an
|
|
46
|
+
* external plugin: `import.meta.dirname` pointed at the real folder.
|
|
47
|
+
*
|
|
48
|
+
* At runtime this file lives in `dist/storage/`, so the migrations folder sits
|
|
49
|
+
* two levels up at the package root. Each migration is its own timestamped
|
|
50
|
+
* subfolder holding `migration.sql` — the layout `drizzle-kit` 1.0 emits and
|
|
51
|
+
* the migrator reads back.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveMigrationsFolder(): string;
|
|
54
|
+
export declare function openDatabase(path: string, migrationsFolder?: string): TodoDatabaseHandle;
|
|
55
|
+
/**
|
|
56
|
+
* Process-wide singleton.
|
|
57
|
+
*
|
|
58
|
+
* Opening is not free: it applies migrations and sets up WAL. The tool and the
|
|
59
|
+
* widget each call this repeatedly, so the connection is created once per
|
|
60
|
+
* process. `resetDatabaseForTests` exists for tests that need a fresh handle.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getDatabase(path: string): TodoDatabaseHandle;
|
|
63
|
+
export declare function resetDatabaseForTests(): void;
|
|
64
|
+
/**
|
|
65
|
+
* SQLite's cross-connection change counter.
|
|
66
|
+
*
|
|
67
|
+
* `PRAGMA data_version` changes when *another* connection commits, and stays
|
|
68
|
+
* put for this connection's own writes. That is exactly the signal a reader
|
|
69
|
+
* wants, so the TUI can poll it and skip work when nothing external changed.
|
|
70
|
+
*/
|
|
71
|
+
export declare function getDataVersion(sqlite: BunDatabase): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
let handle;
|
|
7
|
+
/**
|
|
8
|
+
* Folder holding the generated migrations.
|
|
9
|
+
*
|
|
10
|
+
* `import.meta.dirname` resolves to the plugin's own directory because OpenCode
|
|
11
|
+
* loads local plugins as ordinary files from disk (not embedded in the host
|
|
12
|
+
* binary). That was verified against a standalone Bun host importing an
|
|
13
|
+
* external plugin: `import.meta.dirname` pointed at the real folder.
|
|
14
|
+
*
|
|
15
|
+
* At runtime this file lives in `dist/storage/`, so the migrations folder sits
|
|
16
|
+
* two levels up at the package root. Each migration is its own timestamped
|
|
17
|
+
* subfolder holding `migration.sql` — the layout `drizzle-kit` 1.0 emits and
|
|
18
|
+
* the migrator reads back.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveMigrationsFolder() {
|
|
21
|
+
return join(import.meta.dirname, "..", "..", "drizzle");
|
|
22
|
+
}
|
|
23
|
+
export function openDatabase(path, migrationsFolder = resolveMigrationsFolder()) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
const sqlite = new BunDatabase(path, { create: true });
|
|
26
|
+
sqlite.run("PRAGMA journal_mode = WAL");
|
|
27
|
+
sqlite.run("PRAGMA synchronous = NORMAL");
|
|
28
|
+
sqlite.run("PRAGMA busy_timeout = 5000");
|
|
29
|
+
sqlite.run("PRAGMA cache_size = -64000");
|
|
30
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
31
|
+
const db = drizzle({ client: sqlite });
|
|
32
|
+
migrate(db, { migrationsFolder });
|
|
33
|
+
return { sqlite, db, path };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Process-wide singleton.
|
|
37
|
+
*
|
|
38
|
+
* Opening is not free: it applies migrations and sets up WAL. The tool and the
|
|
39
|
+
* widget each call this repeatedly, so the connection is created once per
|
|
40
|
+
* process. `resetDatabaseForTests` exists for tests that need a fresh handle.
|
|
41
|
+
*/
|
|
42
|
+
export function getDatabase(path) {
|
|
43
|
+
return (handle ??= openDatabase(path));
|
|
44
|
+
}
|
|
45
|
+
export function resetDatabaseForTests() {
|
|
46
|
+
handle?.sqlite.close();
|
|
47
|
+
handle = undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* SQLite's cross-connection change counter.
|
|
51
|
+
*
|
|
52
|
+
* `PRAGMA data_version` changes when *another* connection commits, and stays
|
|
53
|
+
* put for this connection's own writes. That is exactly the signal a reader
|
|
54
|
+
* wants, so the TUI can poll it and skip work when nothing external changed.
|
|
55
|
+
*/
|
|
56
|
+
export function getDataVersion(sqlite) {
|
|
57
|
+
const row = sqlite.query("PRAGMA data_version").get();
|
|
58
|
+
return row?.data_version ?? 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type TodoRootItem } from "../schemas/todo";
|
|
2
|
+
import type { TodoDatabaseHandle } from "./db";
|
|
3
|
+
/**
|
|
4
|
+
* One-time import of the legacy per-session JSON files into SQLite, plus the
|
|
5
|
+
* shared write primitive every caller uses.
|
|
6
|
+
*
|
|
7
|
+
* The old layout kept one file per session:
|
|
8
|
+
*
|
|
9
|
+
* <base>/s_<hash>/todos/todos-<timestamp>.json
|
|
10
|
+
* <base>/s_<hash>/todos/version
|
|
11
|
+
*
|
|
12
|
+
* Design rules:
|
|
13
|
+
*
|
|
14
|
+
* - **Never delete data.** A file is renamed to `.json.migrated` only after the
|
|
15
|
+
* transaction committed, so a crash mid-import leaves the original intact.
|
|
16
|
+
* - **Idempotent.** Importing a session that already holds rows is refused
|
|
17
|
+
* rather than merged, so a partial import cannot silently duplicate items.
|
|
18
|
+
* - **Validated.** JSON is checked with the same `TodoListSchema` the tool
|
|
19
|
+
* uses, so a corrupt legacy file fails loudly here instead of producing a
|
|
20
|
+
* half-valid database.
|
|
21
|
+
*/
|
|
22
|
+
export interface MigrationResult {
|
|
23
|
+
readonly imported: string[];
|
|
24
|
+
readonly skipped: string[];
|
|
25
|
+
readonly failed: {
|
|
26
|
+
file: string;
|
|
27
|
+
error: string;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** `todos-<ts>.json` holds the payload; `version` holds the counter. */
|
|
31
|
+
export declare function readLegacyVersion(sessionDir: string): number;
|
|
32
|
+
/** Newest payload file wins, matching the old "latest wins" behaviour. */
|
|
33
|
+
export declare function findLegacyTodoFile(sessionDir: string): string | undefined;
|
|
34
|
+
/** Parses a legacy payload, accepting the wrapped object and the bare array. */
|
|
35
|
+
export declare function parseLegacyTodos(raw: string): TodoRootItem[];
|
|
36
|
+
/**
|
|
37
|
+
* Replaces a session's whole list in one transaction.
|
|
38
|
+
*
|
|
39
|
+
* Used by both the importer and every normal write, so ordering (`position`)
|
|
40
|
+
* and atomicity are identical everywhere.
|
|
41
|
+
*
|
|
42
|
+
* `behavior: "immediate"` takes the write lock before reading, so two
|
|
43
|
+
* processes cannot both observe an old revision and then write rows that
|
|
44
|
+
* conflict. Combined with `busy_timeout` the second writer waits instead of
|
|
45
|
+
* failing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function replaceSessionTodos(handle: TodoDatabaseHandle, sessionKey: string, todos: readonly TodoRootItem[]): number;
|
|
48
|
+
/** Reads a session's revision, or 0 when the session has never been written. */
|
|
49
|
+
export declare function readRevision(handle: TodoDatabaseHandle, sessionKey: string): number;
|
|
50
|
+
/**
|
|
51
|
+
* Read-modify-write under one `IMMEDIATE` transaction.
|
|
52
|
+
*
|
|
53
|
+
* Every write goes through here so the read, the caller's transform, and the
|
|
54
|
+
* replacement cannot interleave with another process's write. Reading outside
|
|
55
|
+
* the transaction and writing inside it would allow a lost update: two
|
|
56
|
+
* processes could each read revision N, then both write revision N+1, and one
|
|
57
|
+
* change would silently disappear.
|
|
58
|
+
*
|
|
59
|
+
* The transform receives the current list and returns the next one; validation
|
|
60
|
+
* happens inside the transaction so an invalid result rolls the whole thing
|
|
61
|
+
* back without bumping the revision.
|
|
62
|
+
*/
|
|
63
|
+
export declare function mutateSessionTodos(handle: TodoDatabaseHandle, sessionKey: string, transform: (current: TodoRootItem[]) => TodoRootItem[]): {
|
|
64
|
+
todos: TodoRootItem[];
|
|
65
|
+
revision: number;
|
|
66
|
+
};
|
|
67
|
+
/** Reads a session's list in `position` order, rebuilt into the nested shape. */
|
|
68
|
+
export declare function selectTodos(executor: Pick<TodoDatabaseHandle["db"], "select">, sessionKey: string): TodoRootItem[];
|
|
69
|
+
/**
|
|
70
|
+
* Writes the full list for a session inside a transaction, without opening one.
|
|
71
|
+
*
|
|
72
|
+
* Kept separate so `mutateSessionTodos` can compose it with a read in the same
|
|
73
|
+
* transaction.
|
|
74
|
+
*/
|
|
75
|
+
export declare function writeSessionTodos(tx: Pick<TodoDatabaseHandle["db"], "select" | "insert" | "update" | "delete">, sessionKey: string, todos: readonly TodoRootItem[]): number;
|
|
76
|
+
/**
|
|
77
|
+
* Scans every session folder under `baseDir` and imports the legacy JSON.
|
|
78
|
+
*
|
|
79
|
+
* Safe to run on every startup: sessions already present in SQLite are
|
|
80
|
+
* skipped, and consumed files no longer match the payload pattern.
|
|
81
|
+
*/
|
|
82
|
+
export declare function importLegacyJson(handle: TodoDatabaseHandle, baseDir: string): MigrationResult;
|