@dinoxx/dinox-cli 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +72 -139
  2. package/dist/auth/userInfo.d.ts +8 -1
  3. package/dist/auth/userInfo.js +38 -6
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +11 -4
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +43 -15
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/sync.js +4 -2
  55. package/dist/commands/tags/content.d.ts +32 -0
  56. package/dist/commands/tags/content.js +243 -0
  57. package/dist/commands/tags/index.js +285 -49
  58. package/dist/commands/tags/model.d.ts +38 -0
  59. package/dist/commands/tags/model.js +100 -0
  60. package/dist/commands/tags/operations.d.ts +105 -0
  61. package/dist/commands/tags/operations.js +438 -0
  62. package/dist/commands/tags/repo.d.ts +5 -1
  63. package/dist/commands/tags/repo.js +142 -218
  64. package/dist/commands/tags/store.d.ts +28 -0
  65. package/dist/commands/tags/store.js +166 -0
  66. package/dist/commands/todo/index.js +30 -61
  67. package/dist/commands/writeDryRun.d.ts +25 -0
  68. package/dist/commands/writeDryRun.js +31 -1
  69. package/dist/config/resolve.js +2 -2
  70. package/dist/daemon/index.d.ts +2 -0
  71. package/dist/daemon/index.js +176 -58
  72. package/dist/powersync/runtime.d.ts +1 -0
  73. package/dist/powersync/runtime.js +10 -0
  74. package/dist/powersync/schema/content.d.ts +2 -0
  75. package/dist/powersync/schema/content.js +2 -0
  76. package/dist/powersync/schema/index.d.ts +2 -0
  77. package/dist/skills/check.d.ts +1 -1
  78. package/dist/skills/check.js +27 -2
  79. package/dist/utils/authToken.d.ts +2 -0
  80. package/dist/utils/authToken.js +7 -2
  81. package/dist/utils/noteContentEngine.d.ts +333 -0
  82. package/dist/utils/noteContentEngine.js +740 -0
  83. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  84. package/package.json +4 -8
@@ -1,20 +1,40 @@
1
1
  import { generateId } from '../../utils/id.js';
2
2
  import { DinoxError } from '../../utils/errors.js';
3
- function normalizeName(value) {
4
- return value.trim();
3
+ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
4
+ import { MAX_BOX_COLOR_LENGTH, MAX_BOX_DESCRIPTION_LENGTH, MAX_BOX_NAME_LENGTH, MAX_BOX_PATH_LENGTH, assertMaxLength, canonicalizeBoxPath, getLeafName, normalizeBoxLookupKey, toNullableId, trimString, } from './model.js';
5
+ import { assertCanWriteBox, getTableColumns } from './store.js';
6
+ export { canonicalizeBoxPath } from './model.js';
7
+ function normalizeLookupKey(value) {
8
+ return normalizeBoxLookupKey(value);
5
9
  }
6
- function normalizeLookup(value) {
7
- return normalizeName(value).toLowerCase();
8
- }
9
- async function getBoxColumns(db) {
10
- const rows = await db.getAll('PRAGMA table_info(c_zettel_box)', []);
11
- return new Set(rows.map((row) => row.name).filter(Boolean));
10
+ function parseAddBoxInput(input) {
11
+ const userId = input.userId.trim();
12
+ const path = canonicalizeBoxPath(input.name);
13
+ if (!path) {
14
+ throw new DinoxError('Box path is required');
15
+ }
16
+ const name = getLeafName(path);
17
+ const description = trimString(input.description);
18
+ const color = trimString(input.color);
19
+ assertMaxLength('Box name', name, MAX_BOX_NAME_LENGTH);
20
+ assertMaxLength('Box path', path, MAX_BOX_PATH_LENGTH);
21
+ assertMaxLength('Box description', description, MAX_BOX_DESCRIPTION_LENGTH);
22
+ assertMaxLength('Box color', color, MAX_BOX_COLOR_LENGTH);
23
+ return {
24
+ userId,
25
+ path,
26
+ name,
27
+ description,
28
+ color,
29
+ };
12
30
  }
13
- async function listBoxRowsForWrite(db) {
14
- return db.getAll(`
31
+ async function loadBoxNodesByPath(db) {
32
+ const rows = await db.getAll(`
15
33
  SELECT
16
34
  TRIM(COALESCE(id, '')) AS id,
17
35
  TRIM(COALESCE(name, '')) AS name,
36
+ TRIM(COALESCE(path, name, '')) AS path,
37
+ TRIM(COALESCE(parent_id, '')) AS parent_id,
18
38
  TRIM(COALESCE(description, '')) AS description,
19
39
  TRIM(COALESCE(color, '')) AS color,
20
40
  is_del,
@@ -27,169 +47,272 @@ async function listBoxRowsForWrite(db) {
27
47
  created_at DESC,
28
48
  id DESC
29
49
  `, []);
50
+ const nodesByPath = new Map();
51
+ for (const row of rows) {
52
+ const id = trimString(row.id);
53
+ if (!id) {
54
+ continue;
55
+ }
56
+ const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
57
+ if (!path) {
58
+ continue;
59
+ }
60
+ const key = normalizeLookupKey(path);
61
+ if (nodesByPath.has(key)) {
62
+ continue;
63
+ }
64
+ nodesByPath.set(key, {
65
+ id,
66
+ name: trimString(row.name) || getLeafName(path),
67
+ path,
68
+ parentId: toNullableId(row.parent_id),
69
+ description: trimString(row.description),
70
+ color: trimString(row.color),
71
+ isDel: typeof row.is_del === 'number' ? row.is_del : null,
72
+ userNo: trimString(row.user_no),
73
+ });
74
+ }
75
+ return nodesByPath;
30
76
  }
31
- export async function listBoxes(db) {
77
+ export async function listBoxes(db, input = {}) {
78
+ const userId = trimString(input.userId);
79
+ const userFilter = userId
80
+ ? `AND (TRIM(COALESCE(user_no, '')) = ? OR TRIM(COALESCE(user_no, '')) = '')`
81
+ : '';
32
82
  const rows = await db.getAll(`
33
83
  SELECT DISTINCT
34
84
  TRIM(COALESCE(id, '')) AS id,
35
85
  TRIM(COALESCE(name, '')) AS name,
36
- TRIM(COALESCE(description, '')) AS description
86
+ TRIM(COALESCE(path, name, '')) AS path,
87
+ TRIM(COALESCE(parent_id, '')) AS parent_id,
88
+ TRIM(COALESCE(description, '')) AS description,
89
+ TRIM(COALESCE(color, '')) AS color
37
90
  FROM c_zettel_box
38
91
  WHERE (is_del IS NULL OR is_del = 0)
39
92
  AND TRIM(COALESCE(id, '')) <> ''
40
- ORDER BY name COLLATE NOCASE ASC, id ASC
41
- `, []);
93
+ ${userFilter}
94
+ ORDER BY path COLLATE NOCASE ASC, id ASC
95
+ `, userId ? [userId] : []);
42
96
  return rows
43
- .map((row) => ({
44
- id: row.id?.trim() ?? '',
45
- name: row.name?.trim() ?? '',
46
- description: row.description?.trim() ?? '',
47
- }))
48
- .filter((row) => row.id.length > 0);
97
+ .map((row) => {
98
+ const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
99
+ return {
100
+ id: trimString(row.id),
101
+ name: trimString(row.name) || (path ? getLeafName(path) : ''),
102
+ path,
103
+ parentId: toNullableId(row.parent_id),
104
+ description: trimString(row.description),
105
+ color: trimString(row.color),
106
+ };
107
+ })
108
+ .filter((row) => row.id.length > 0 && row.path.length > 0);
49
109
  }
50
- export async function addBox(db, input) {
51
- const userId = input.userId.trim();
52
- const name = normalizeName(input.name);
53
- if (!name) {
54
- throw new DinoxError('Box name is required');
110
+ function buildBoxExistsError(path, ids) {
111
+ return new DinoxError(`Zettel box already exists: ${path}`, {
112
+ details: {
113
+ type: 'box_exists',
114
+ name: getLeafName(path),
115
+ path,
116
+ ids,
117
+ question: 'This box already exists. Do you want to keep using the existing box?',
118
+ },
119
+ });
120
+ }
121
+ function setExistingValue(assignments, params, node, field, column, value) {
122
+ if (node[field] === value) {
123
+ return;
55
124
  }
56
- const description = normalizeName(input.description ?? '');
57
- const color = normalizeName(input.color ?? '');
58
- const lookup = normalizeLookup(name);
59
- const columnNames = await getBoxColumns(db);
60
- const requiredColumns = ['id', 'name', 'user_no'];
61
- const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
62
- if (missingRequired.length > 0) {
63
- throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
125
+ assignments.push(`${column} = ?`);
126
+ params.push(value);
127
+ node[field] = value;
128
+ }
129
+ async function restoreOrUpdateExistingBox(args) {
130
+ const { db, node, name, path, parentId, description, color, userId, timestamp, isLeaf } = args;
131
+ const assignments = [];
132
+ const params = [];
133
+ const wasDeleted = node.isDel === 1;
134
+ if (wasDeleted) {
135
+ assignments.push('is_del = ?');
136
+ params.push(0);
137
+ node.isDel = 0;
64
138
  }
65
- if (!userId) {
66
- throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
139
+ setExistingValue(assignments, params, node, 'name', 'name', name);
140
+ setExistingValue(assignments, params, node, 'path', 'path', path);
141
+ setExistingValue(assignments, params, node, 'parentId', 'parent_id', parentId);
142
+ if (!node.userNo) {
143
+ setExistingValue(assignments, params, node, 'userNo', 'user_no', userId);
67
144
  }
68
- const rows = await listBoxRowsForWrite(db);
69
- const sameNameRows = rows.filter((row) => normalizeLookup(row.name ?? '') === lookup);
70
- const activeRows = sameNameRows.filter((row) => row.is_del !== 1);
71
- if (activeRows.length > 0) {
72
- const ids = activeRows.map((row) => row.id?.trim() ?? '').filter(Boolean);
73
- throw new DinoxError(`Zettel box already exists: ${name}`, {
74
- details: {
75
- type: 'box_exists',
76
- name,
77
- ids,
78
- question: 'This box already exists. Do you want to keep using the existing box?',
79
- },
80
- });
145
+ if (isLeaf && description) {
146
+ setExistingValue(assignments, params, node, 'description', 'description', description);
147
+ }
148
+ if (isLeaf && color) {
149
+ setExistingValue(assignments, params, node, 'color', 'color', color);
150
+ }
151
+ if (assignments.length === 0) {
152
+ return false;
153
+ }
154
+ assignments.unshift('updated_at = ?');
155
+ params.unshift(timestamp);
156
+ params.push(node.id);
157
+ await db.execute(`
158
+ UPDATE c_zettel_box
159
+ SET ${assignments.join(', ')}
160
+ WHERE id = ?
161
+ `, params);
162
+ return wasDeleted;
163
+ }
164
+ async function insertBoxNode(args) {
165
+ const { db, id, name, path, parentId, description, color, userId, timestamp } = args;
166
+ await db.execute(`
167
+ INSERT INTO c_zettel_box (
168
+ id,
169
+ name,
170
+ path,
171
+ parent_id,
172
+ description,
173
+ color,
174
+ priority,
175
+ is_show,
176
+ cover_url,
177
+ created_at,
178
+ updated_at,
179
+ extra_data,
180
+ user_no,
181
+ is_del
182
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, 1, '', ?, ?, '{}', ?, 0)
183
+ `, [id, name, path, parentId, description, color, timestamp, timestamp, userId]);
184
+ }
185
+ export async function addBox(db, input) {
186
+ const parsed = parseAddBoxInput(input);
187
+ const columnNames = await getTableColumns(db, 'c_zettel_box');
188
+ assertCanWriteBox(columnNames, parsed.userId, 'insert');
189
+ const nodesByPath = await loadBoxNodesByPath(db);
190
+ const existingLeaf = nodesByPath.get(normalizeLookupKey(parsed.path));
191
+ if (existingLeaf && existingLeaf.isDel !== 1) {
192
+ throw buildBoxExistsError(parsed.path, [existingLeaf.id]);
81
193
  }
82
194
  const timestamp = new Date().toISOString();
83
- const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
84
- if (deletedRow?.id) {
85
- const setParts = [];
86
- const params = [];
87
- const setValue = (column, value) => {
88
- if (!columnNames.has(column)) {
89
- return;
195
+ const segments = parsed.path.split('/');
196
+ let parentId = null;
197
+ let leafId = '';
198
+ let leafParentId = null;
199
+ let leafDescription = '';
200
+ let leafColor = '';
201
+ let createdNodes = 0;
202
+ let restoredNodes = 0;
203
+ let restoredLeaf = false;
204
+ await withPowerSyncWriteTransaction(db, async (tx) => {
205
+ for (let index = 0; index < segments.length; index += 1) {
206
+ const path = segments.slice(0, index + 1).join('/');
207
+ const key = normalizeLookupKey(path);
208
+ const name = segments[index];
209
+ const isLeaf = index === segments.length - 1;
210
+ const existing = nodesByPath.get(key);
211
+ if (existing) {
212
+ const restored = await restoreOrUpdateExistingBox({
213
+ db: tx,
214
+ node: existing,
215
+ name,
216
+ path,
217
+ parentId,
218
+ description: parsed.description,
219
+ color: parsed.color,
220
+ userId: parsed.userId,
221
+ timestamp,
222
+ isLeaf,
223
+ });
224
+ if (restored) {
225
+ restoredNodes += 1;
226
+ restoredLeaf = isLeaf;
227
+ }
228
+ parentId = existing.id;
229
+ if (isLeaf) {
230
+ leafId = existing.id;
231
+ leafParentId = existing.parentId;
232
+ leafDescription = existing.description;
233
+ leafColor = existing.color;
234
+ }
235
+ continue;
236
+ }
237
+ const id = generateId();
238
+ await insertBoxNode({
239
+ db: tx,
240
+ id,
241
+ name,
242
+ path,
243
+ parentId,
244
+ description: isLeaf && parsed.description ? parsed.description : null,
245
+ color: isLeaf && parsed.color ? parsed.color : null,
246
+ userId: parsed.userId,
247
+ timestamp,
248
+ });
249
+ const node = {
250
+ id,
251
+ name,
252
+ path,
253
+ parentId,
254
+ description: isLeaf ? parsed.description : '',
255
+ color: isLeaf ? parsed.color : '',
256
+ isDel: 0,
257
+ userNo: parsed.userId,
258
+ };
259
+ nodesByPath.set(key, node);
260
+ createdNodes += 1;
261
+ parentId = id;
262
+ if (isLeaf) {
263
+ leafId = id;
264
+ leafParentId = node.parentId;
265
+ leafDescription = node.description;
266
+ leafColor = node.color;
90
267
  }
91
- setParts.push(`${column} = ?`);
92
- params.push(value);
93
- };
94
- setValue('name', name);
95
- setValue('is_del', 0);
96
- setValue('updated_at', timestamp);
97
- if (description) {
98
- setValue('description', description);
99
- }
100
- if (color) {
101
- setValue('color', color);
102
- }
103
- setValue('user_no', userId);
104
- if (setParts.length > 0) {
105
- params.push(deletedRow.id);
106
- await db.execute(`
107
- UPDATE c_zettel_box
108
- SET ${setParts.join(', ')}
109
- WHERE id = ?
110
- `, params);
111
268
  }
112
- return {
113
- id: deletedRow.id,
114
- name,
115
- description: description || (deletedRow.description?.trim() ?? ''),
116
- color: color || (deletedRow.color?.trim() ?? ''),
117
- restored: true,
118
- };
269
+ });
270
+ if (!leafId) {
271
+ throw new DinoxError(`Failed to create zettel box: ${parsed.path}`);
119
272
  }
120
- const id = generateId();
121
- const insertEntries = [];
122
- const pushInsert = (column, value) => {
123
- if (!columnNames.has(column)) {
124
- return;
125
- }
126
- insertEntries.push([column, value]);
127
- };
128
- pushInsert('id', id);
129
- pushInsert('name', name);
130
- pushInsert('description', description || null);
131
- pushInsert('color', color || null);
132
- pushInsert('priority', 0);
133
- pushInsert('is_show', 1);
134
- pushInsert('cover_url', '');
135
- pushInsert('created_at', timestamp);
136
- pushInsert('updated_at', timestamp);
137
- pushInsert('extra_data', '{}');
138
- pushInsert('user_no', userId);
139
- pushInsert('is_del', 0);
140
- const insertColumns = insertEntries.map(([column]) => column);
141
- const placeholders = insertColumns.map(() => '?').join(', ');
142
- const insertParams = insertEntries.map(([, value]) => value);
143
- await db.execute(`
144
- INSERT INTO c_zettel_box (${insertColumns.join(', ')})
145
- VALUES (${placeholders})
146
- `, insertParams);
147
273
  return {
148
- id,
149
- name,
150
- description,
151
- color,
152
- restored: false,
274
+ id: leafId,
275
+ name: parsed.name,
276
+ path: parsed.path,
277
+ parentId: leafParentId,
278
+ description: leafDescription,
279
+ color: leafColor,
280
+ restored: restoredLeaf,
281
+ createdNodes,
282
+ restoredNodes,
153
283
  };
154
284
  }
155
285
  export async function inspectAddBox(db, input) {
156
- const userId = input.userId.trim();
157
- const name = normalizeName(input.name);
158
- if (!name) {
159
- throw new DinoxError('Box name is required');
160
- }
161
- const description = normalizeName(input.description ?? '');
162
- const color = normalizeName(input.color ?? '');
163
- const lookup = normalizeLookup(name);
164
- const columnNames = await getBoxColumns(db);
165
- const requiredColumns = ['id', 'name', 'user_no'];
166
- const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
167
- if (missingRequired.length > 0) {
168
- throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
169
- }
170
- if (!userId) {
171
- throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
286
+ const parsed = parseAddBoxInput(input);
287
+ const columnNames = await getTableColumns(db, 'c_zettel_box');
288
+ assertCanWriteBox(columnNames, parsed.userId, 'insert');
289
+ const nodesByPath = await loadBoxNodesByPath(db);
290
+ const existingLeaf = nodesByPath.get(normalizeLookupKey(parsed.path));
291
+ if (existingLeaf && existingLeaf.isDel !== 1) {
292
+ throw buildBoxExistsError(parsed.path, [existingLeaf.id]);
172
293
  }
173
- const rows = await listBoxRowsForWrite(db);
174
- const sameNameRows = rows.filter((row) => normalizeLookup(row.name ?? '') === lookup);
175
- const activeRows = sameNameRows.filter((row) => row.is_del !== 1);
176
- if (activeRows.length > 0) {
177
- const ids = activeRows.map((row) => row.id?.trim() ?? '').filter(Boolean);
178
- throw new DinoxError(`Zettel box already exists: ${name}`, {
179
- details: {
180
- type: 'box_exists',
181
- name,
182
- ids,
183
- question: 'This box already exists. Do you want to keep using the existing box?',
184
- },
185
- });
294
+ const segments = parsed.path.split('/');
295
+ const createdPaths = [];
296
+ const restoredPaths = [];
297
+ for (let index = 0; index < segments.length; index += 1) {
298
+ const path = segments.slice(0, index + 1).join('/');
299
+ const existing = nodesByPath.get(normalizeLookupKey(path));
300
+ if (!existing) {
301
+ createdPaths.push(path);
302
+ continue;
303
+ }
304
+ if (existing.isDel === 1) {
305
+ restoredPaths.push(path);
306
+ }
186
307
  }
187
- const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
188
308
  return {
189
- name,
190
- description,
191
- color,
192
- restored: Boolean(deletedRow?.id),
193
- ...(deletedRow?.id ? { existingId: deletedRow.id } : {}),
309
+ name: parsed.name,
310
+ path: parsed.path,
311
+ description: parsed.description,
312
+ color: parsed.color,
313
+ restored: existingLeaf?.isDel === 1,
314
+ createdPaths,
315
+ restoredPaths,
316
+ ...(existingLeaf?.id ? { existingId: existingLeaf.id } : {}),
194
317
  };
195
318
  }
@@ -0,0 +1,22 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { BoxListItem, MutableBoxNode } from './model.js';
3
+ type BoxTableName = 'c_zettel_box' | 'c_note';
4
+ export declare function getTableColumns(db: AbstractPowerSyncDatabase, table: BoxTableName): Promise<Set<string>>;
5
+ export declare function assertRequiredColumns(table: string, columnNames: Set<string>, required: string[]): void;
6
+ export declare function assertCanWriteBox(columnNames: Set<string>, userId: string, action: 'insert' | 'update'): void;
7
+ export declare function loadAllBoxNodes(db: AbstractPowerSyncDatabase, options?: {
8
+ userId?: string;
9
+ }): Promise<MutableBoxNode[]>;
10
+ export declare function isActiveBoxNode(node: MutableBoxNode): boolean;
11
+ export declare function toBoxListItem(node: MutableBoxNode): BoxListItem;
12
+ export declare function activeBoxNodes(nodes: MutableBoxNode[]): MutableBoxNode[];
13
+ export declare function activeNodeByPath(nodes: MutableBoxNode[]): Map<string, MutableBoxNode>;
14
+ export declare function requireActivePath(nodes: MutableBoxNode[], rawPath: string, label: string): MutableBoxNode;
15
+ export declare function isDescendantBoxPath(candidatePath: string, rootPath: string): boolean;
16
+ export declare function descendantsOf(nodes: MutableBoxNode[], root: MutableBoxNode, includeRoot?: boolean): MutableBoxNode[];
17
+ export declare function ensureNoPathConflict(nodes: MutableBoxNode[], changes: Array<{
18
+ id: string;
19
+ toPath: string;
20
+ }>): void;
21
+ export declare function assertWritableBoxNodes(nodes: MutableBoxNode[], ids: Set<string>, userId: string): void;
22
+ export {};
@@ -0,0 +1,143 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { REQUIRED_ZETTEL_BOX_COLUMNS, canonicalizeBoxPath, getLeafName, normalizeBoxLookupKey, toNullableId, trimString, } from './model.js';
3
+ export async function getTableColumns(db, table) {
4
+ const sql = table === 'c_zettel_box' ? 'PRAGMA table_info(c_zettel_box)' : 'PRAGMA table_info(c_note)';
5
+ const rows = await db.getAll(sql, []);
6
+ return new Set(rows.map((row) => trimString(row.name)).filter(Boolean));
7
+ }
8
+ export function assertRequiredColumns(table, columnNames, required) {
9
+ const missing = required.filter((column) => !columnNames.has(column));
10
+ if (missing.length > 0) {
11
+ throw new DinoxError(`${table} table is missing required columns: ${missing.join(', ')}`);
12
+ }
13
+ }
14
+ export function assertCanWriteBox(columnNames, userId, action) {
15
+ assertRequiredColumns('c_zettel_box', columnNames, REQUIRED_ZETTEL_BOX_COLUMNS);
16
+ if (!userId.trim()) {
17
+ throw new DinoxError(`c_zettel_box ${action} requires user_no. Missing resolved userId. Run \`dino auth login\` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.`);
18
+ }
19
+ }
20
+ export async function loadAllBoxNodes(db, options = {}) {
21
+ const userId = trimString(options.userId);
22
+ const userFilter = userId
23
+ ? `AND (TRIM(COALESCE(user_no, '')) = ? OR TRIM(COALESCE(user_no, '')) = '')`
24
+ : '';
25
+ const rows = await db.getAll(`
26
+ SELECT
27
+ TRIM(COALESCE(id, '')) AS id,
28
+ TRIM(COALESCE(name, '')) AS name,
29
+ TRIM(COALESCE(path, name, '')) AS path,
30
+ TRIM(COALESCE(parent_id, '')) AS parent_id,
31
+ TRIM(COALESCE(description, '')) AS description,
32
+ TRIM(COALESCE(color, '')) AS color,
33
+ is_del,
34
+ TRIM(COALESCE(user_no, '')) AS user_no
35
+ FROM c_zettel_box
36
+ WHERE TRIM(COALESCE(id, '')) <> ''
37
+ ${userFilter}
38
+ ORDER BY path COLLATE NOCASE ASC, id ASC
39
+ `, userId ? [userId] : []);
40
+ return rows
41
+ .map((row) => {
42
+ const path = canonicalizeBoxPath(row.path ?? row.name ?? '');
43
+ return {
44
+ id: trimString(row.id),
45
+ name: trimString(row.name) || (path ? getLeafName(path) : ''),
46
+ path,
47
+ parentId: toNullableId(row.parent_id),
48
+ description: trimString(row.description),
49
+ color: trimString(row.color),
50
+ isDel: typeof row.is_del === 'number' ? row.is_del : null,
51
+ userNo: trimString(row.user_no),
52
+ };
53
+ })
54
+ .filter((row) => row.id && row.path);
55
+ }
56
+ export function isActiveBoxNode(node) {
57
+ return node.isDel === null || node.isDel === 0;
58
+ }
59
+ export function toBoxListItem(node) {
60
+ return {
61
+ id: node.id,
62
+ name: node.name,
63
+ path: node.path,
64
+ parentId: node.parentId,
65
+ description: node.description,
66
+ color: node.color,
67
+ };
68
+ }
69
+ export function activeBoxNodes(nodes) {
70
+ return nodes.filter(isActiveBoxNode);
71
+ }
72
+ export function activeNodeByPath(nodes) {
73
+ const map = new Map();
74
+ for (const node of activeBoxNodes(nodes)) {
75
+ const key = normalizeBoxLookupKey(node.path);
76
+ if (!map.has(key)) {
77
+ map.set(key, node);
78
+ }
79
+ }
80
+ return map;
81
+ }
82
+ export function requireActivePath(nodes, rawPath, label) {
83
+ const path = canonicalizeBoxPath(rawPath);
84
+ if (!path) {
85
+ throw new DinoxError(`${label} is required`);
86
+ }
87
+ const node = activeNodeByPath(nodes).get(normalizeBoxLookupKey(path));
88
+ if (!node) {
89
+ throw new DinoxError(`Unknown zettel box path: ${path}`);
90
+ }
91
+ return node;
92
+ }
93
+ export function isDescendantBoxPath(candidatePath, rootPath) {
94
+ const candidateKey = normalizeBoxLookupKey(candidatePath);
95
+ const rootKey = normalizeBoxLookupKey(rootPath);
96
+ return candidateKey.startsWith(`${rootKey}/`);
97
+ }
98
+ export function descendantsOf(nodes, root, includeRoot = true) {
99
+ return activeBoxNodes(nodes)
100
+ .filter((node) => {
101
+ if (includeRoot && node.id === root.id) {
102
+ return true;
103
+ }
104
+ return isDescendantBoxPath(node.path, root.path);
105
+ })
106
+ .sort((left, right) => left.path.localeCompare(right.path));
107
+ }
108
+ export function ensureNoPathConflict(nodes, changes) {
109
+ const changedIds = new Set(changes.map((change) => change.id));
110
+ const activePaths = new Map();
111
+ for (const node of activeBoxNodes(nodes)) {
112
+ if (changedIds.has(node.id)) {
113
+ continue;
114
+ }
115
+ activePaths.set(normalizeBoxLookupKey(node.path), node);
116
+ }
117
+ const seenNext = new Set();
118
+ for (const change of changes) {
119
+ const key = normalizeBoxLookupKey(change.toPath);
120
+ const existing = activePaths.get(key);
121
+ if (existing) {
122
+ throw new DinoxError(`Target zettel box path already exists: ${change.toPath}`, {
123
+ details: {
124
+ type: 'box_path_conflict',
125
+ path: change.toPath,
126
+ existingId: existing.id,
127
+ },
128
+ });
129
+ }
130
+ if (seenNext.has(key)) {
131
+ throw new DinoxError(`Duplicate target zettel box path in operation: ${change.toPath}`);
132
+ }
133
+ seenNext.add(key);
134
+ }
135
+ }
136
+ export function assertWritableBoxNodes(nodes, ids, userId) {
137
+ const foreign = nodes
138
+ .filter((node) => ids.has(node.id))
139
+ .filter((node) => node.userNo && node.userNo !== userId);
140
+ if (foreign.length > 0) {
141
+ throw new DinoxError(`Zettel boxes belong to a different user: ${foreign.map((node) => node.path).join(', ')}`);
142
+ }
143
+ }