@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.
@@ -1,5 +1,9 @@
1
1
  import z from "zod";
2
- export declare const TodoChildernItemSchema: z.ZodObject<{
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.$strip>;
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.$strip>>>;
53
- }, z.core.$strip>;
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.$strip>>>;
90
- }, z.core.$strip>>;
91
- export declare const TodoWriteInputSchema: z.ZodObject<{
92
- todos: z.ZodArray<z.ZodObject<{
93
- id: z.ZodNumber;
94
- content: z.ZodString;
95
- status: z.ZodEnum<{
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.$strip>>>;
167
- }, z.core.$strip>>;
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 type TodoUpdateInput = z.infer<typeof TodoUpdateOperationSchema>;
188
- export declare const TodoInputToolSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
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.$strip>>>;
225
- }, z.core.$strip>>;
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.$strip>>>;
279
- }, z.core.$strip>>;
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>;
@@ -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 TodoChildernItemSchema = z.object({
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: z.number().int().positive().describe("Unique task identifier (e.g. 1)"),
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(TodoChildernItemSchema).max(9).optional(),
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
- export const TodoReadOperationSchema = z
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: z.literal("read").describe("'read' to inspect current todos."),
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 initialize an empty list."),
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.describe("Root or child todo ID, e.g. 1 or 1.1"),
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: z.array(TodoRootItemSchema),
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;