@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,41 +1,39 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
- import { loadConfig } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
4
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
5
- import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
6
2
  import { registerCommandGroup } from '../descriptor/register.js';
7
3
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
8
- import { buildBoxAddDryRunPreview } from '../writeDryRun.js';
4
+ import { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } from '../writeDryRun.js';
9
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
10
6
  import { DinoxError } from '../../utils/errors.js';
11
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
12
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
8
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
13
9
  import { addBox, inspectAddBox, listBoxes } from './repo.js';
10
+ import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
14
11
  const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
15
- async function syncBeforeBoxCommand(db, offline, timeoutMs) {
16
- if (offline) {
17
- return;
18
- }
19
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
20
- await syncNoteTokenIndex(db);
12
+ async function runBoxList(context) {
13
+ await withCommandRuntime(context, async (runtime) => {
14
+ const identity = await resolveAuthIdentity(runtime.config);
15
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
16
+ printSuccess(await listBoxes(runtime.db, { userId: identity.userId }));
17
+ });
21
18
  }
22
- async function withBoxDb(context, run) {
23
- const rawConfig = await loadConfig();
24
- const config = resolveConfig(rawConfig);
25
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
26
- const offline = Boolean(context.globals.offline);
27
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
28
- try {
29
- await run({ db, config, rawConfig, offline, timeoutMs, stale });
30
- }
31
- finally {
32
- await db.close().catch(() => undefined);
33
- }
19
+ async function runBoxTree(context) {
20
+ await withCommandRuntime(context, async (runtime) => {
21
+ const identity = await resolveAuthIdentity(runtime.config);
22
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
23
+ printSuccess({
24
+ stale: commandStale(runtime),
25
+ tree: await getBoxTree(runtime.db, { userId: identity.userId }),
26
+ });
27
+ });
34
28
  }
35
- async function runBoxList(context) {
36
- await withBoxDb(context, async ({ db, offline, timeoutMs }) => {
37
- await syncBeforeBoxCommand(db, offline, timeoutMs);
38
- printSuccess(await listBoxes(db));
29
+ async function runBoxStats(context) {
30
+ await withCommandRuntime(context, async (runtime) => {
31
+ const identity = await resolveAuthIdentity(runtime.config);
32
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
33
+ printSuccess({
34
+ stale: commandStale(runtime),
35
+ ...(await getBoxStats(runtime.db, { userId: identity.userId })),
36
+ });
39
37
  });
40
38
  }
41
39
  async function runBoxAdd(context) {
@@ -43,22 +41,22 @@ async function runBoxAdd(context) {
43
41
  const optionName = typeof context.options.name === 'string' ? context.options.name : undefined;
44
42
  const name = (nameArg ?? optionName ?? '').trim();
45
43
  if (!name) {
46
- throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
44
+ throw new DinoxError('Box path is required. Use `dino box add <path>` or `dino box add --name <path>`.');
47
45
  }
48
46
  if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
49
- throw new DinoxError('Conflicting box name values from positional argument and --name option.');
47
+ throw new DinoxError('Conflicting box path values from positional argument and --name option.');
50
48
  }
51
49
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
52
- await withBoxDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
53
- const identity = await resolveAuthIdentity(rawConfig, config);
54
- await syncBeforeBoxCommand(db, offline, timeoutMs);
50
+ await withCommandRuntime(context, async (runtime) => {
51
+ const identity = await resolveAuthIdentity(runtime.config);
52
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
55
53
  const description = typeof context.options.description === 'string' ? context.options.description : undefined;
56
54
  const color = typeof context.options.color === 'string' ? context.options.color : undefined;
57
55
  if (Boolean(context.options.dryRun)) {
58
56
  printDryRunPreview(buildBoxAddDryRunPreview({
59
- config,
60
- offline,
61
- ...(await inspectAddBox(db, {
57
+ config: runtime.config,
58
+ offline: runtime.offline,
59
+ ...(await inspectAddBox(runtime.db, {
62
60
  name,
63
61
  userId: identity.userId,
64
62
  description,
@@ -67,27 +65,131 @@ async function runBoxAdd(context) {
67
65
  }));
68
66
  return;
69
67
  }
70
- const result = await addBox(db, {
68
+ const result = await addBox(runtime.db, {
71
69
  name,
72
70
  userId: identity.userId,
73
71
  description,
74
72
  color,
75
73
  });
76
- if (!offline && !config.powersync.uploadBaseUrl) {
77
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
78
- }
79
- if (!offline) {
80
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
81
- }
82
- const payload = { ok: true, stale: offline ? true : stale, ...result };
74
+ printUploadDisabledWarning(runtime);
75
+ await waitForMutationUpload(runtime);
76
+ const payload = { ok: true, stale: commandStale(runtime), ...result };
83
77
  if (structuredOutput) {
84
78
  printSuccess(payload);
85
79
  return;
86
80
  }
87
81
  printHumanReadableLine(result.id);
88
- if (stale && !offline) {
89
- printHumanReadableLine('warning: sync timed out; results may be stale');
82
+ printStaleCacheWarning(runtime, 'warning: sync timed out; results may be stale');
83
+ });
84
+ }
85
+ async function withBoxWriteRuntime(context, run) {
86
+ return withCommandRuntime(context, async (runtime) => {
87
+ const identity = await resolveAuthIdentity(runtime.config);
88
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
89
+ return run({ runtime, userId: identity.userId });
90
+ });
91
+ }
92
+ function requireArg(value, message) {
93
+ const trimmed = value?.trim() ?? '';
94
+ if (!trimmed) {
95
+ throw new DinoxError(message);
96
+ }
97
+ return trimmed;
98
+ }
99
+ async function runBoxRename(context) {
100
+ const path = requireArg(context.args.path, 'Box path is required');
101
+ const newName = requireArg(context.args.newName, 'New box name is required');
102
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
103
+ const result = await renameBox(runtime.db, { path, newName, userId }, { dryRun: Boolean(context.options.dryRun) });
104
+ if (Boolean(context.options.dryRun)) {
105
+ printDryRunPreview(buildBoxMutationDryRunPreview({
106
+ config: runtime.config,
107
+ offline: runtime.offline,
108
+ command: 'dino box rename',
109
+ action: 'rename_box',
110
+ risk: 'write',
111
+ summary: `Rename zettel box "${result.fromPath}" to "${result.toPath}".`,
112
+ inputs: { path, new_name: newName },
113
+ changes: { updated_boxes: result.updatedBoxes },
114
+ }));
115
+ return;
116
+ }
117
+ printUploadDisabledWarning(runtime);
118
+ await waitForMutationUpload(runtime);
119
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
120
+ });
121
+ }
122
+ async function runBoxMove(context) {
123
+ const path = requireArg(context.args.path, 'Box path is required');
124
+ const to = typeof context.options.to === 'string' ? context.options.to.trim() : '';
125
+ if (!to) {
126
+ throw new DinoxError('--to is required');
127
+ }
128
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
129
+ const result = await moveBox(runtime.db, { path, to, userId }, { dryRun: Boolean(context.options.dryRun) });
130
+ if (Boolean(context.options.dryRun)) {
131
+ printDryRunPreview(buildBoxMutationDryRunPreview({
132
+ config: runtime.config,
133
+ offline: runtime.offline,
134
+ command: 'dino box move',
135
+ action: 'move_box',
136
+ risk: 'write',
137
+ summary: `Move zettel box "${result.fromPath}" to "${result.toPath}".`,
138
+ inputs: { path, to },
139
+ changes: { updated_boxes: result.updatedBoxes },
140
+ }));
141
+ return;
142
+ }
143
+ printUploadDisabledWarning(runtime);
144
+ await waitForMutationUpload(runtime);
145
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
146
+ });
147
+ }
148
+ async function runBoxMerge(context) {
149
+ const from = requireArg(context.args.from, 'Source box path is required');
150
+ const to = requireArg(context.args.to, 'Target box path is required');
151
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
152
+ const result = await mergeBox(runtime.db, { from, to, userId }, { dryRun: Boolean(context.options.dryRun) });
153
+ if (Boolean(context.options.dryRun)) {
154
+ printDryRunPreview(buildBoxMutationDryRunPreview({
155
+ config: runtime.config,
156
+ offline: runtime.offline,
157
+ command: 'dino box merge',
158
+ action: 'merge_box',
159
+ risk: 'high-risk-write',
160
+ summary: `Merge zettel box subtree "${result.fromPath}" into "${result.toPath}".`,
161
+ inputs: { from, to },
162
+ changes: {
163
+ soft_deleted_box_ids: result.softDeletedBoxIds,
164
+ updated_notes: result.updatedNotes,
165
+ },
166
+ }));
167
+ return;
90
168
  }
169
+ printUploadDisabledWarning(runtime);
170
+ await waitForMutationUpload(runtime);
171
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
172
+ });
173
+ }
174
+ async function runBoxCleanup(context) {
175
+ await withCommandRuntime(context, async (runtime) => {
176
+ const identity = await resolveAuthIdentity(runtime.config);
177
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
178
+ const result = await inspectBoxCleanup(runtime.db, { userId: identity.userId });
179
+ if (Boolean(context.options.dryRun)) {
180
+ printDryRunPreview(buildBoxMutationDryRunPreview({
181
+ config: runtime.config,
182
+ offline: runtime.offline,
183
+ command: 'dino box cleanup',
184
+ action: 'cleanup_boxes',
185
+ risk: 'write',
186
+ summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
187
+ inputs: {},
188
+ targets: result.issues,
189
+ }));
190
+ return;
191
+ }
192
+ printSuccess({ stale: commandStale(runtime), ...result });
91
193
  });
92
194
  }
93
195
  export const boxCommandGroup = {
@@ -105,7 +207,7 @@ export const boxCommandGroup = {
105
207
  output: {
106
208
  type: 'structured',
107
209
  summary: 'Box list payload',
108
- fields: ['id', 'name', 'description'],
210
+ fields: ['id', 'name', 'path', 'parentId', 'description', 'color'],
109
211
  },
110
212
  examples: ['dino box list --format json'],
111
213
  },
@@ -113,10 +215,10 @@ export const boxCommandGroup = {
113
215
  },
114
216
  {
115
217
  name: 'add',
116
- description: 'Create a zettel box, restoring a deleted row when possible',
117
- arguments: [{ syntax: '[name]', description: 'Box name' }],
218
+ description: 'Create a zettel box path, restoring deleted path nodes when possible',
219
+ arguments: [{ syntax: '[path]', description: 'Box path, for example Work/Inbox' }],
118
220
  options: [
119
- { flags: '--name <string>', description: 'Box name (alternative to positional name)' },
221
+ { flags: '--name <string>', description: 'Box path (alternative to positional path)' },
120
222
  { flags: '--description <string>', description: 'Box purpose/usage description' },
121
223
  { flags: '--color <string>', description: 'Box color' },
122
224
  { flags: '--dry-run', description: 'Preview the write without executing it' },
@@ -129,12 +231,130 @@ export const boxCommandGroup = {
129
231
  output: {
130
232
  type: 'structured',
131
233
  summary: 'Box creation acknowledgement',
132
- fields: ['id', 'name', 'description', 'color', 'restored', 'stale'],
234
+ fields: ['id', 'name', 'path', 'parentId', 'description', 'color', 'restored', 'createdNodes', 'restoredNodes', 'stale'],
133
235
  },
134
- examples: ['dino box add "Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
236
+ examples: ['dino box add "Work/Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
135
237
  },
136
238
  action: runBoxAdd,
137
239
  },
240
+ {
241
+ name: 'tree',
242
+ description: 'Show zettel boxes as a hierarchy tree',
243
+ schema: {
244
+ path: 'box.tree',
245
+ risk: 'read',
246
+ auth: 'required',
247
+ globalOptions: BOX_GLOBAL_OPTIONS,
248
+ output: {
249
+ type: 'structured',
250
+ summary: 'Nested zettel box tree payload',
251
+ fields: ['tree.id', 'tree.name', 'tree.path', 'tree.parentId', 'tree.children', 'stale'],
252
+ },
253
+ examples: ['dino box tree --format json'],
254
+ },
255
+ action: runBoxTree,
256
+ },
257
+ {
258
+ name: 'stats',
259
+ description: 'Show zettel box note counts and empty-box status',
260
+ schema: {
261
+ path: 'box.stats',
262
+ risk: 'read',
263
+ auth: 'required',
264
+ globalOptions: BOX_GLOBAL_OPTIONS,
265
+ output: {
266
+ type: 'structured',
267
+ summary: 'Zettel box statistics payload',
268
+ fields: ['totalBoxes', 'emptyBoxes', 'boxes.id', 'boxes.path', 'boxes.directNoteCount', 'boxes.descendantNoteCount', 'boxes.latestNoteUpdatedAt', 'boxes.isEmpty', 'stale'],
269
+ },
270
+ examples: ['dino box stats --format json'],
271
+ },
272
+ action: runBoxStats,
273
+ },
274
+ {
275
+ name: 'rename',
276
+ description: 'Rename a zettel box and cascade descendant paths',
277
+ arguments: [
278
+ { syntax: '<path>', description: 'Existing box path' },
279
+ { syntax: '<new-name>', description: 'New leaf name' },
280
+ ],
281
+ options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
282
+ schema: {
283
+ path: 'box.rename',
284
+ risk: 'write',
285
+ auth: 'required',
286
+ globalOptions: BOX_GLOBAL_OPTIONS,
287
+ output: {
288
+ type: 'structured',
289
+ summary: 'Zettel box rename result',
290
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
291
+ },
292
+ examples: ['dino box rename "Work/Inbox" "Ideas" --format json --dry-run'],
293
+ },
294
+ action: runBoxRename,
295
+ },
296
+ {
297
+ name: 'move',
298
+ description: 'Move a zettel box under another parent and cascade descendant paths',
299
+ arguments: [{ syntax: '<path>', description: 'Existing box path' }],
300
+ options: [
301
+ { flags: '--to <parent-path>', description: 'Target parent box path', required: true, type: 'string' },
302
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
303
+ ],
304
+ schema: {
305
+ path: 'box.move',
306
+ risk: 'write',
307
+ auth: 'required',
308
+ globalOptions: BOX_GLOBAL_OPTIONS,
309
+ output: {
310
+ type: 'structured',
311
+ summary: 'Zettel box move result',
312
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
313
+ },
314
+ examples: ['dino box move "Inbox" --to "Work" --format json --dry-run'],
315
+ },
316
+ action: runBoxMove,
317
+ },
318
+ {
319
+ name: 'merge',
320
+ description: 'Merge a zettel box subtree into another box and soft-delete the source subtree',
321
+ arguments: [
322
+ { syntax: '<from>', description: 'Source box path' },
323
+ { syntax: '<to>', description: 'Target box path' },
324
+ ],
325
+ options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
326
+ schema: {
327
+ path: 'box.merge',
328
+ risk: 'high-risk-write',
329
+ auth: 'required',
330
+ globalOptions: BOX_GLOBAL_OPTIONS,
331
+ output: {
332
+ type: 'structured',
333
+ summary: 'Zettel box merge result',
334
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'stale'],
335
+ },
336
+ examples: ['dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run'],
337
+ },
338
+ action: runBoxMerge,
339
+ },
340
+ {
341
+ name: 'cleanup',
342
+ description: 'Inspect zettel box cleanup candidates',
343
+ options: [{ flags: '--dry-run', description: 'Preview cleanup candidates without writing' }],
344
+ schema: {
345
+ path: 'box.cleanup',
346
+ risk: 'read',
347
+ auth: 'required',
348
+ globalOptions: BOX_GLOBAL_OPTIONS,
349
+ output: {
350
+ type: 'structured',
351
+ summary: 'Zettel box cleanup inspection payload',
352
+ fields: ['issues', 'emptyBoxes', 'duplicatePaths', 'orphanParents', 'deletedWithActiveChildren', 'stale'],
353
+ },
354
+ examples: ['dino box cleanup --format json --dry-run'],
355
+ },
356
+ action: runBoxCleanup,
357
+ },
138
358
  ],
139
359
  };
140
360
  export function registerBoxesCommand(program) {
@@ -0,0 +1,34 @@
1
+ export declare const MAX_BOX_NAME_LENGTH = 500;
2
+ export declare const MAX_BOX_PATH_LENGTH = 2000;
3
+ export declare const MAX_BOX_DESCRIPTION_LENGTH = 200000;
4
+ export declare const MAX_BOX_COLOR_LENGTH = 200;
5
+ export type BoxListItem = {
6
+ id: string;
7
+ name: string;
8
+ path: string;
9
+ parentId: string | null;
10
+ description: string;
11
+ color: string;
12
+ };
13
+ export type MutableBoxNode = {
14
+ id: string;
15
+ name: string;
16
+ path: string;
17
+ parentId: string | null;
18
+ description: string;
19
+ color: string;
20
+ isDel: number | null;
21
+ userNo: string;
22
+ };
23
+ export type TableInfoRow = {
24
+ name: string | null;
25
+ };
26
+ export declare const REQUIRED_ZETTEL_BOX_COLUMNS: string[];
27
+ export declare const REQUIRED_NOTE_BOX_COLUMNS: string[];
28
+ export declare function canonicalizeBoxPath(value: string): string;
29
+ export declare function normalizeBoxLookupKey(value: string): string;
30
+ export declare function trimString(value: string | null | undefined): string;
31
+ export declare function toNullableId(value: string | null | undefined): string | null;
32
+ export declare function getLeafName(path: string): string;
33
+ export declare function assertMaxLength(label: string, value: string, maxLength: number): void;
34
+ export declare function parseStoredBoxIds(raw: string | null): string[];
@@ -0,0 +1,78 @@
1
+ export const MAX_BOX_NAME_LENGTH = 500;
2
+ export const MAX_BOX_PATH_LENGTH = 2000;
3
+ export const MAX_BOX_DESCRIPTION_LENGTH = 200_000;
4
+ export const MAX_BOX_COLOR_LENGTH = 200;
5
+ export const REQUIRED_ZETTEL_BOX_COLUMNS = [
6
+ 'id',
7
+ 'name',
8
+ 'path',
9
+ 'parent_id',
10
+ 'description',
11
+ 'color',
12
+ 'priority',
13
+ 'is_show',
14
+ 'cover_url',
15
+ 'created_at',
16
+ 'updated_at',
17
+ 'extra_data',
18
+ 'user_no',
19
+ 'is_del',
20
+ ];
21
+ export const REQUIRED_NOTE_BOX_COLUMNS = [
22
+ 'id',
23
+ 'zettel_boxes',
24
+ 'updated_at',
25
+ 'version',
26
+ 'is_del',
27
+ ];
28
+ export function canonicalizeBoxPath(value) {
29
+ return value
30
+ .split(/[\\/]+/g)
31
+ .map((segment) => segment.trim())
32
+ .filter(Boolean)
33
+ .join('/');
34
+ }
35
+ export function normalizeBoxLookupKey(value) {
36
+ return canonicalizeBoxPath(value).toLowerCase();
37
+ }
38
+ export function trimString(value) {
39
+ return value?.trim() ?? '';
40
+ }
41
+ export function toNullableId(value) {
42
+ const trimmed = trimString(value);
43
+ return trimmed ? trimmed : null;
44
+ }
45
+ export function getLeafName(path) {
46
+ return path.split('/').at(-1) ?? path;
47
+ }
48
+ export function assertMaxLength(label, value, maxLength) {
49
+ if (value.length > maxLength) {
50
+ throw new DinoxError(`${label} must be at most ${maxLength} characters`);
51
+ }
52
+ }
53
+ export function parseStoredBoxIds(raw) {
54
+ if (!raw) {
55
+ return [];
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (!Array.isArray(parsed)) {
60
+ return [];
61
+ }
62
+ const result = [];
63
+ const seen = new Set();
64
+ for (const value of parsed) {
65
+ const normalized = String(value).trim();
66
+ if (!normalized || seen.has(normalized)) {
67
+ continue;
68
+ }
69
+ seen.add(normalized);
70
+ result.push(normalized);
71
+ }
72
+ return result;
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ import { DinoxError } from '../../utils/errors.js';
@@ -0,0 +1,86 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { BoxListItem } from './model.js';
3
+ export type BoxTreeNode = BoxListItem & {
4
+ children: BoxTreeNode[];
5
+ };
6
+ export type BoxStatsItem = BoxListItem & {
7
+ directNoteCount: number;
8
+ descendantNoteCount: number;
9
+ latestNoteUpdatedAt: string | null;
10
+ isEmpty: boolean;
11
+ };
12
+ export type BoxStatsResult = {
13
+ totalBoxes: number;
14
+ emptyBoxes: number;
15
+ boxes: BoxStatsItem[];
16
+ };
17
+ export type BoxCleanupIssue = {
18
+ type: 'empty_box' | 'duplicate_path' | 'orphan_parent' | 'deleted_with_active_children';
19
+ id: string;
20
+ path: string;
21
+ name: string;
22
+ parentId: string | null;
23
+ detail: string;
24
+ duplicateIds?: string[];
25
+ childIds?: string[];
26
+ };
27
+ export type BoxCleanupResult = {
28
+ issues: BoxCleanupIssue[];
29
+ emptyBoxes: number;
30
+ duplicatePaths: number;
31
+ orphanParents: number;
32
+ deletedWithActiveChildren: number;
33
+ };
34
+ export type BoxRenameInput = {
35
+ path: string;
36
+ newName: string;
37
+ userId: string;
38
+ };
39
+ export type BoxMoveInput = {
40
+ path: string;
41
+ to: string;
42
+ userId: string;
43
+ };
44
+ export type BoxMergeInput = {
45
+ from: string;
46
+ to: string;
47
+ userId: string;
48
+ };
49
+ export type BoxReadInput = {
50
+ userId?: string;
51
+ };
52
+ export type BoxSubtreeChange = {
53
+ id: string;
54
+ fromPath: string;
55
+ toPath: string;
56
+ };
57
+ export type BoxRenameMoveResult = {
58
+ id: string;
59
+ fromPath: string;
60
+ toPath: string;
61
+ updatedBoxes: BoxSubtreeChange[];
62
+ };
63
+ export type BoxMergeResult = {
64
+ fromId: string;
65
+ toId: string;
66
+ fromPath: string;
67
+ toPath: string;
68
+ softDeletedBoxIds: string[];
69
+ updatedNotes: Array<{
70
+ id: string;
71
+ before: string[];
72
+ after: string[];
73
+ }>;
74
+ };
75
+ export declare function getBoxTree(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxTreeNode[]>;
76
+ export declare function getBoxStats(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxStatsResult>;
77
+ export declare function inspectBoxCleanup(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxCleanupResult>;
78
+ export declare function renameBox(db: AbstractPowerSyncDatabase, input: BoxRenameInput, options?: {
79
+ dryRun?: boolean;
80
+ }): Promise<BoxRenameMoveResult>;
81
+ export declare function moveBox(db: AbstractPowerSyncDatabase, input: BoxMoveInput, options?: {
82
+ dryRun?: boolean;
83
+ }): Promise<BoxRenameMoveResult>;
84
+ export declare function mergeBox(db: AbstractPowerSyncDatabase, input: BoxMergeInput, options?: {
85
+ dryRun?: boolean;
86
+ }): Promise<BoxMergeResult>;