@dinoxx/dinox-cli 1.0.20 → 1.0.22

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 (89) 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 +1 -2
  5. package/dist/cliTypes.d.ts +1 -0
  6. package/dist/commands/auth/index.js +11 -4
  7. package/dist/commands/boxes/index.js +274 -54
  8. package/dist/commands/boxes/model.d.ts +34 -0
  9. package/dist/commands/boxes/model.js +78 -0
  10. package/dist/commands/boxes/operations.d.ts +86 -0
  11. package/dist/commands/boxes/operations.js +415 -0
  12. package/dist/commands/boxes/repo.d.ts +13 -6
  13. package/dist/commands/boxes/repo.js +272 -149
  14. package/dist/commands/boxes/store.d.ts +22 -0
  15. package/dist/commands/boxes/store.js +143 -0
  16. package/dist/commands/graph/index.js +11 -38
  17. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  18. package/dist/commands/mcp/noteContentTools.js +390 -0
  19. package/dist/commands/mcp/server.d.ts +3 -0
  20. package/dist/commands/mcp/server.js +43 -15
  21. package/dist/commands/mcp/tools.js +3 -1
  22. package/dist/commands/mcp/types.d.ts +6 -2
  23. package/dist/commands/meta/index.js +8 -24
  24. package/dist/commands/meta/repo.d.ts +0 -3
  25. package/dist/commands/meta/repo.js +1 -4
  26. package/dist/commands/notes/actions.d.ts +2 -0
  27. package/dist/commands/notes/actions.js +149 -55
  28. package/dist/commands/notes/bulk.d.ts +34 -0
  29. package/dist/commands/notes/bulk.js +204 -0
  30. package/dist/commands/notes/contentEngine.d.ts +95 -0
  31. package/dist/commands/notes/contentEngine.js +652 -0
  32. package/dist/commands/notes/contentPatch.d.ts +84 -0
  33. package/dist/commands/notes/contentPatch.js +337 -0
  34. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  35. package/dist/commands/notes/contentPatchCommand.js +126 -0
  36. package/dist/commands/notes/descriptors.js +194 -42
  37. package/dist/commands/notes/export.d.ts +21 -0
  38. package/dist/commands/notes/export.js +224 -0
  39. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  40. package/dist/commands/notes/metadataMutation.js +127 -0
  41. package/dist/commands/notes/repo.d.ts +20 -0
  42. package/dist/commands/notes/repo.js +135 -18
  43. package/dist/commands/notes/runtime.d.ts +3 -12
  44. package/dist/commands/notes/runtime.js +4 -30
  45. package/dist/commands/notes/searchSql.js +1 -1
  46. package/dist/commands/prompt/index.js +17 -47
  47. package/dist/commands/prompt/repo.js +2 -2
  48. package/dist/commands/runtime.d.ts +34 -0
  49. package/dist/commands/runtime.js +110 -0
  50. package/dist/commands/schema/globalOptions.d.ts +1 -0
  51. package/dist/commands/schema/globalOptions.js +1 -0
  52. package/dist/commands/schema/renderSkills.js +21 -7
  53. package/dist/commands/schema/spec.d.ts +1 -2
  54. package/dist/commands/schema/spec.js +2 -3
  55. package/dist/commands/schema/types.d.ts +0 -13
  56. package/dist/commands/storage/index.js +32 -62
  57. package/dist/commands/sync.js +22 -5
  58. package/dist/commands/tags/content.d.ts +32 -0
  59. package/dist/commands/tags/content.js +243 -0
  60. package/dist/commands/tags/index.js +287 -51
  61. package/dist/commands/tags/model.d.ts +38 -0
  62. package/dist/commands/tags/model.js +100 -0
  63. package/dist/commands/tags/operations.d.ts +105 -0
  64. package/dist/commands/tags/operations.js +438 -0
  65. package/dist/commands/tags/repo.d.ts +5 -1
  66. package/dist/commands/tags/repo.js +142 -218
  67. package/dist/commands/tags/store.d.ts +28 -0
  68. package/dist/commands/tags/store.js +166 -0
  69. package/dist/commands/todo/index.js +32 -63
  70. package/dist/commands/writeDryRun.d.ts +25 -0
  71. package/dist/commands/writeDryRun.js +31 -1
  72. package/dist/config/resolve.js +2 -2
  73. package/dist/daemon/index.d.ts +2 -0
  74. package/dist/daemon/index.js +176 -58
  75. package/dist/powersync/runtime.d.ts +1 -0
  76. package/dist/powersync/runtime.js +16 -0
  77. package/dist/powersync/schema/content.d.ts +2 -0
  78. package/dist/powersync/schema/content.js +2 -0
  79. package/dist/powersync/schema/index.d.ts +2 -0
  80. package/dist/powersync/syncGate.d.ts +24 -0
  81. package/dist/powersync/syncGate.js +36 -0
  82. package/dist/skills/check.d.ts +1 -1
  83. package/dist/skills/check.js +27 -2
  84. package/dist/utils/authToken.d.ts +2 -0
  85. package/dist/utils/authToken.js +7 -2
  86. package/dist/utils/noteContentEngine.d.ts +333 -0
  87. package/dist/utils/noteContentEngine.js +740 -0
  88. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  89. package/package.json +4 -8
@@ -5,9 +5,10 @@ import { generateId } from '../../utils/id.js';
5
5
  import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
6
6
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
7
7
  import { DinoxError } from '../../utils/errors.js';
8
- import { waitForIdleDataFlow } from '../../powersync/runtime.js';
8
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, waitForMutationUpload, } from '../runtime.js';
9
9
  import { createNote, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
10
10
  import { buildNoteCreateDryRunPreview, buildNoteDeleteDryRunPreview, buildNoteUpdateDryRunPreview, } from './dryRun.js';
11
+ import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
11
12
  import { parseNoteSearchFields, projectNoteSearchFields } from './searchOutput.js';
12
13
  import { parseCreatedAtRange } from './searchTime.js';
13
14
  import { buildNoteSearchResponse, normalizeDaysForMeta } from './searchResponse.js';
@@ -39,13 +40,11 @@ function toPublicNote(note) {
39
40
  ...(zettel_boxes !== undefined ? { boxes: zettel_boxes } : {}),
40
41
  };
41
42
  }
42
- async function settleMutation(db, timeoutMs) {
43
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
44
- }
45
43
  async function executeNoteUpdateOperation(args) {
46
44
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
47
- await withNoteDatabase(args.command, async ({ db, config, offline, timeoutMs, stale }) => {
48
- await syncBeforeNoteCommand(db, offline, timeoutMs);
45
+ await withNoteDatabase(args.command, async (runtime) => {
46
+ const { db, config, offline } = runtime;
47
+ await syncBeforeNoteCommand(runtime);
49
48
  const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
50
49
  const hasTags = typeof args.tagsOption === 'string';
51
50
  const hasBoxes = typeof args.boxOption === 'string';
@@ -85,12 +84,8 @@ async function executeNoteUpdateOperation(args) {
85
84
  }));
86
85
  }
87
86
  const updatedCount = results.filter((item) => item.updated).length;
88
- if (!offline && !config.powersync.uploadBaseUrl) {
89
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
90
- }
91
- if (!offline) {
92
- await settleMutation(db, timeoutMs);
93
- }
87
+ printUploadDisabledWarning(runtime);
88
+ await waitForMutationUpload(runtime);
94
89
  const payload = {
95
90
  ok: true,
96
91
  total: results.length,
@@ -102,7 +97,7 @@ async function executeNoteUpdateOperation(args) {
102
97
  boxes: item.zettelBoxIds,
103
98
  is_starred: item.is_starred,
104
99
  })),
105
- stale: offline ? true : stale,
100
+ stale: commandStale(runtime),
106
101
  };
107
102
  if (structuredOutput) {
108
103
  printSuccess(payload);
@@ -125,15 +120,101 @@ async function executeNoteUpdateOperation(args) {
125
120
  else {
126
121
  printHumanReadableLine(`Updated ${updatedCount}/${results.length} notes`);
127
122
  }
128
- if (stale && !offline) {
129
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
123
+ printStaleCacheWarning(runtime);
124
+ });
125
+ }
126
+ function selectMetadataOperation(options) {
127
+ const candidates = [
128
+ ['--add', options.add],
129
+ ['--remove', options.remove],
130
+ ['--replace', options.replace],
131
+ ].filter((entry) => typeof entry[1] === 'string');
132
+ if (candidates.length !== 1) {
133
+ throw new DinoxError('Provide exactly one of --add, --remove, or --replace');
134
+ }
135
+ const [optionName, rawValues] = candidates[0];
136
+ return {
137
+ operation: optionName === '--add' ? 'add' : optionName === '--remove' ? 'remove' : 'replace',
138
+ rawValues,
139
+ optionName,
140
+ };
141
+ }
142
+ async function executeNoteMetadataListOperation(args) {
143
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
144
+ await withNoteDatabase(args.command, async (runtime) => {
145
+ const { db, config, offline } = runtime;
146
+ await syncBeforeNoteCommand(runtime);
147
+ const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
148
+ const selected = selectMetadataOperation(args.options);
149
+ const requestedValues = await parseStringListInput(selected.rawValues, selected.optionName);
150
+ const values = args.field === 'tags'
151
+ ? await validateTagsForUpdate(db, requestedValues)
152
+ : await resolveZettelBoxIdsForUpdate(db, requestedValues);
153
+ const plan = await planNoteMetadataMutation(db, {
154
+ ids,
155
+ field: args.field,
156
+ operation: selected.operation,
157
+ values,
158
+ displayValues: requestedValues,
159
+ });
160
+ if (args.options.dryRun) {
161
+ printDryRunPreview({
162
+ dry_run: true,
163
+ command: args.commandName,
164
+ action: args.actionName,
165
+ risk: 'write',
166
+ summary: `Preview ${args.field} ${selected.operation} for ${plan.total} note(s); ${plan.changedCount} note(s) would change.`,
167
+ warnings: [
168
+ ...(offline ? ['Execution would run in offline mode and only use the local cache.'] : []),
169
+ ...(!offline && !config.powersync.uploadBaseUrl
170
+ ? ['Execution would be local-only because powersync.uploadBaseUrl is unset.']
171
+ : []),
172
+ ],
173
+ inputs: {
174
+ ids,
175
+ operation: selected.operation,
176
+ [args.field]: args.field === 'tags' ? values : requestedValues,
177
+ },
178
+ targets: plan.results.map((item) => ({ id: item.id, title: item.title })),
179
+ changes: plan.results.map((item) => ({
180
+ id: item.id,
181
+ title: item.title,
182
+ [`${args.field}_from`]: item.before,
183
+ [`${args.field}_to`]: item.after,
184
+ changed: item.changed,
185
+ })),
186
+ });
187
+ return;
188
+ }
189
+ await applyNoteMetadataMutation(db, plan);
190
+ printUploadDisabledWarning(runtime);
191
+ await waitForMutationUpload(runtime);
192
+ const payload = {
193
+ ok: true,
194
+ field: args.field,
195
+ operation: selected.operation,
196
+ total: plan.total,
197
+ updatedCount: plan.changedCount,
198
+ results: plan.results.map((item) => ({
199
+ id: item.id,
200
+ updated: item.changed,
201
+ [args.field]: item.after,
202
+ })),
203
+ stale: commandStale(runtime),
204
+ };
205
+ if (structuredOutput) {
206
+ printSuccess(payload);
207
+ return;
130
208
  }
209
+ printHumanReadableLine(`Updated ${plan.changedCount}/${plan.total} notes`);
210
+ printStaleCacheWarning(runtime);
131
211
  });
132
212
  }
133
213
  export async function runNoteSearch({ command, options, args }) {
134
214
  const searchOptions = options;
135
- await withNoteDatabase(command, async ({ db, offline, timeoutMs, stale }) => {
136
- await syncBeforeNoteCommand(db, offline, timeoutMs);
215
+ await withNoteDatabase(command, async (runtime) => {
216
+ const { db } = runtime;
217
+ await syncBeforeNoteCommand(runtime);
137
218
  const keyword = args.query?.trim() ?? '';
138
219
  const tagsExpression = typeof searchOptions.tags === 'string' ? searchOptions.tags.trim() : '';
139
220
  const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
@@ -167,7 +248,7 @@ export async function runNoteSearch({ command, options, args }) {
167
248
  query: keyword,
168
249
  notes: projectNoteSearchFields(rows.slice(offset, offset + limit), fields),
169
250
  totalCount: rows.length,
170
- stale: offline ? true : stale,
251
+ stale: commandStale(runtime),
171
252
  includeDeleted: Boolean(searchOptions.includeDeleted),
172
253
  limit,
173
254
  offset,
@@ -191,8 +272,9 @@ export async function runNoteGet({ command, options, args }) {
191
272
  throw new DinoxError('Note id is required');
192
273
  }
193
274
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
194
- await withNoteDatabase(command, async ({ db, offline, timeoutMs, stale }) => {
195
- await syncBeforeNoteCommand(db, offline, timeoutMs);
275
+ await withNoteDatabase(command, async (runtime) => {
276
+ const { db } = runtime;
277
+ await syncBeforeNoteCommand(runtime);
196
278
  if (getOptions.contextOnly) {
197
279
  const context = await getNoteContext(db, id);
198
280
  if (!context) {
@@ -202,7 +284,7 @@ export async function runNoteGet({ command, options, args }) {
202
284
  recoverable: true,
203
285
  });
204
286
  }
205
- printSuccess({ stale: offline ? true : stale, note: toPublicNoteContext(context) });
287
+ printSuccess({ stale: commandStale(runtime), note: toPublicNoteContext(context) });
206
288
  return;
207
289
  }
208
290
  const note = await getNote(db, id);
@@ -214,12 +296,10 @@ export async function runNoteGet({ command, options, args }) {
214
296
  });
215
297
  }
216
298
  if (structuredOutput) {
217
- printSuccess({ stale, note: toPublicNote(note) });
299
+ printSuccess({ stale: commandStale(runtime), note: toPublicNote(note) });
218
300
  return;
219
301
  }
220
- if (stale && !offline) {
221
- printHumanReadableLine('warning: sync timed out; result may be stale');
222
- }
302
+ printStaleCacheWarning(runtime, 'warning: sync timed out; result may be stale');
223
303
  printHumanReadableLine(`${note.id}`);
224
304
  printHumanReadableLine(`title: ${note.title ?? ''}`);
225
305
  if (note.updated_at) {
@@ -239,8 +319,9 @@ export async function runNotePreview({ command, options, args }) {
239
319
  if (!id) {
240
320
  throw new DinoxError('Note id is required');
241
321
  }
242
- await withNoteDatabase(command, async ({ db, offline, timeoutMs, stale }) => {
243
- await syncBeforeNoteCommand(db, offline, timeoutMs);
322
+ await withNoteDatabase(command, async (runtime) => {
323
+ const { db } = runtime;
324
+ await syncBeforeNoteCommand(runtime);
244
325
  const preview = await getNotePreview(db, id, parsePreviewLineCount(previewOptions.lines));
245
326
  if (!preview) {
246
327
  throw new DinoxError(`Note not found: ${id}`, {
@@ -249,13 +330,14 @@ export async function runNotePreview({ command, options, args }) {
249
330
  recoverable: true,
250
331
  });
251
332
  }
252
- printSuccess({ stale: offline ? true : stale, note: preview });
333
+ printSuccess({ stale: commandStale(runtime), note: preview });
253
334
  });
254
335
  }
255
336
  export async function runNoteDetail({ command, options, args }) {
256
337
  const detailOptions = options;
257
- await withNoteDatabase(command, async ({ db, offline, timeoutMs }) => {
258
- await syncBeforeNoteCommand(db, offline, timeoutMs);
338
+ await withNoteDatabase(command, async (runtime) => {
339
+ const { db } = runtime;
340
+ await syncBeforeNoteCommand(runtime);
259
341
  const ids = await parseTargetNoteIds(args.id, detailOptions.ids, 'detail');
260
342
  const notes = await getNoteDetails(db, ids);
261
343
  const foundIds = new Set(notes.map((note) => note.id));
@@ -270,9 +352,10 @@ export async function runNoteDetail({ command, options, args }) {
270
352
  export async function runNoteCreate({ command, options }) {
271
353
  const createOptions = options;
272
354
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
273
- await withNoteDatabase(command, async ({ db, rawConfig, config, offline, timeoutMs, stale }) => {
274
- await syncBeforeNoteCommand(db, offline, timeoutMs);
275
- const identity = await resolveAuthIdentity(rawConfig, config);
355
+ await withNoteDatabase(command, async (runtime) => {
356
+ const { db, config, offline } = runtime;
357
+ await syncBeforeNoteCommand(runtime);
358
+ const identity = await resolveAuthIdentity(config);
276
359
  const contentMd = await parseStringInput(String(createOptions.content));
277
360
  const canonicalContentMd = await canonicalizeBidlinksInMarkdown(db, contentMd, identity.userId);
278
361
  const { contentJson, contentText } = convertMarkdownToNoteContent(canonicalContentMd);
@@ -309,21 +392,15 @@ export async function runNoteCreate({ command, options }) {
309
392
  zettelBoxIds: await resolveZettelBoxIdsForCreate(db, boxNames),
310
393
  userId: identity.userId,
311
394
  });
312
- if (!offline && !config.powersync.uploadBaseUrl) {
313
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
314
- }
315
- if (!offline) {
316
- await settleMutation(db, timeoutMs);
317
- }
318
- const payload = { ok: true, id, stale: offline ? true : stale };
395
+ printUploadDisabledWarning(runtime);
396
+ await waitForMutationUpload(runtime);
397
+ const payload = { ok: true, id, stale: commandStale(runtime) };
319
398
  if (structuredOutput) {
320
399
  printSuccess(payload);
321
400
  return;
322
401
  }
323
402
  printHumanReadableLine(id);
324
- if (stale && !offline) {
325
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
326
- }
403
+ printStaleCacheWarning(runtime);
327
404
  });
328
405
  }
329
406
  export async function runNoteDelete({ command, options, args }) {
@@ -333,28 +410,23 @@ export async function runNoteDelete({ command, options, args }) {
333
410
  throw new DinoxError('Note id is required');
334
411
  }
335
412
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
336
- await withNoteDatabase(command, async ({ db, config, offline, timeoutMs, stale }) => {
337
- await syncBeforeNoteCommand(db, offline, timeoutMs);
413
+ await withNoteDatabase(command, async (runtime) => {
414
+ const { db, config, offline } = runtime;
415
+ await syncBeforeNoteCommand(runtime);
338
416
  if (Boolean(deleteOptions.dryRun)) {
339
417
  printDryRunPreview(await buildNoteDeleteDryRunPreview({ db, config, offline, id }));
340
418
  return;
341
419
  }
342
420
  await softDeleteNote(db, id);
343
- if (!offline && !config.powersync.uploadBaseUrl) {
344
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
345
- }
346
- if (!offline) {
347
- await settleMutation(db, timeoutMs);
348
- }
349
- const payload = { ok: true, id, stale: offline ? true : stale };
421
+ printUploadDisabledWarning(runtime);
422
+ await waitForMutationUpload(runtime);
423
+ const payload = { ok: true, id, stale: commandStale(runtime) };
350
424
  if (structuredOutput) {
351
425
  printSuccess(payload);
352
426
  return;
353
427
  }
354
428
  printHumanReadableLine('OK');
355
- if (stale && !offline) {
356
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
357
- }
429
+ printStaleCacheWarning(runtime);
358
430
  });
359
431
  }
360
432
  export async function runNoteUpdate({ command, options, args }) {
@@ -373,6 +445,28 @@ export async function runNoteUpdate({ command, options, args }) {
373
445
  actionName: 'update_note',
374
446
  });
375
447
  }
448
+ export async function runNoteTag({ command, options, args }) {
449
+ await executeNoteMetadataListOperation({
450
+ command,
451
+ id: args.id,
452
+ idsOption: options.ids,
453
+ options: options,
454
+ field: 'tags',
455
+ commandName: 'dino note tag',
456
+ actionName: 'mutate_note_tags',
457
+ });
458
+ }
459
+ export async function runNoteMove({ command, options, args }) {
460
+ await executeNoteMetadataListOperation({
461
+ command,
462
+ id: args.id,
463
+ idsOption: options.ids,
464
+ options: options,
465
+ field: 'boxes',
466
+ commandName: 'dino note move',
467
+ actionName: 'mutate_note_boxes',
468
+ });
469
+ }
376
470
  export async function runNoteStar({ command, options, args }) {
377
471
  const starOptions = options;
378
472
  await executeNoteUpdateOperation({
@@ -0,0 +1,34 @@
1
+ import type { CommandExecutionContext } from '../descriptor/types.js';
2
+ import type { NoteMetadataListField, NoteMetadataOperation } from './metadataMutation.js';
3
+ type NoteBulkOptions = {
4
+ tags?: string;
5
+ from?: string;
6
+ to?: string;
7
+ days?: string;
8
+ starred?: string;
9
+ boxes?: string;
10
+ all?: boolean;
11
+ tagAdd?: string;
12
+ tagRemove?: string;
13
+ tagReplace?: string;
14
+ boxAdd?: string;
15
+ boxRemove?: string;
16
+ boxReplace?: string;
17
+ expectedCount?: string;
18
+ confirm?: boolean;
19
+ dryRun?: boolean;
20
+ };
21
+ type NoteBulkMutationSelection = {
22
+ field: NoteMetadataListField;
23
+ operation: NoteMetadataOperation;
24
+ rawValues: string;
25
+ optionName: '--tag-add' | '--tag-remove' | '--tag-replace' | '--box-add' | '--box-remove' | '--box-replace';
26
+ };
27
+ export declare function selectBulkMutation(options: NoteBulkOptions): NoteBulkMutationSelection;
28
+ export declare function parseExpectedCount(value: unknown): number | null;
29
+ export declare function assertBulkTargetFilter(input: {
30
+ query: string;
31
+ options: NoteBulkOptions;
32
+ }): void;
33
+ export declare function runNoteBulk({ command, options, args }: CommandExecutionContext): Promise<void>;
34
+ export {};
@@ -0,0 +1,204 @@
1
+ import { printDryRunPreview } from '../../utils/dryRun.js';
2
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
3
+ import { DinoxError } from '../../utils/errors.js';
4
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, waitForMutationUpload, } from '../runtime.js';
5
+ import { resolveZettelBoxIdsForBulk, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForBulk, } from './repo.js';
6
+ import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
7
+ import { parseCreatedAtRange } from './searchTime.js';
8
+ import { normalizeDaysForMeta, } from './searchResponse.js';
9
+ import { parseSearchListInput, parseStarredOption, parseStringListInput, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
10
+ const BULK_PREVIEW_LIMIT = 100;
11
+ export function selectBulkMutation(options) {
12
+ const candidates = [
13
+ { optionName: '--tag-add', field: 'tags', operation: 'add', rawValues: options.tagAdd },
14
+ { optionName: '--tag-remove', field: 'tags', operation: 'remove', rawValues: options.tagRemove },
15
+ { optionName: '--tag-replace', field: 'tags', operation: 'replace', rawValues: options.tagReplace },
16
+ { optionName: '--box-add', field: 'boxes', operation: 'add', rawValues: options.boxAdd },
17
+ { optionName: '--box-remove', field: 'boxes', operation: 'remove', rawValues: options.boxRemove },
18
+ { optionName: '--box-replace', field: 'boxes', operation: 'replace', rawValues: options.boxReplace },
19
+ ];
20
+ const selected = candidates.filter((entry) => typeof entry.rawValues === 'string');
21
+ if (selected.length !== 1) {
22
+ throw new DinoxError('Provide exactly one bulk mutation option: --tag-add, --tag-remove, --tag-replace, --box-add, --box-remove, or --box-replace');
23
+ }
24
+ return selected[0];
25
+ }
26
+ export function parseExpectedCount(value) {
27
+ if (value === undefined || value === null || value === '') {
28
+ return null;
29
+ }
30
+ if (typeof value !== 'string') {
31
+ throw new DinoxError('--expected-count must be a non-negative integer');
32
+ }
33
+ const trimmed = value.trim();
34
+ if (!/^\d+$/.test(trimmed)) {
35
+ throw new DinoxError('--expected-count must be a non-negative integer');
36
+ }
37
+ const parsed = Number(trimmed);
38
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
39
+ throw new DinoxError('--expected-count must be a non-negative integer');
40
+ }
41
+ return parsed;
42
+ }
43
+ function hasConcreteTargetFilter(input) {
44
+ const { query, options } = input;
45
+ return Boolean(query ||
46
+ options.tags ||
47
+ options.from ||
48
+ options.to ||
49
+ options.days ||
50
+ options.starred ||
51
+ options.boxes);
52
+ }
53
+ export function assertBulkTargetFilter(input) {
54
+ const hasFilter = hasConcreteTargetFilter(input);
55
+ if (input.options.all && hasFilter) {
56
+ throw new DinoxError('--all cannot be combined with other target filters');
57
+ }
58
+ if (!input.options.all && !hasFilter) {
59
+ throw new DinoxError('Provide at least one target filter or --all for note bulk');
60
+ }
61
+ }
62
+ function buildExecutionWarnings(input) {
63
+ const warnings = [];
64
+ if (input.offline) {
65
+ warnings.push('Execution would run in offline mode and only use the local cache.');
66
+ }
67
+ if (!input.offline && !input.uploadEnabled) {
68
+ warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
69
+ }
70
+ if (input.total > BULK_PREVIEW_LIMIT) {
71
+ warnings.push(`Preview is truncated to the first ${BULK_PREVIEW_LIMIT} matched notes.`);
72
+ }
73
+ return warnings;
74
+ }
75
+ function previewItems(items) {
76
+ return items.slice(0, BULK_PREVIEW_LIMIT);
77
+ }
78
+ export async function runNoteBulk({ command, options, args }) {
79
+ const bulkOptions = options;
80
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
81
+ await withNoteDatabase(command, async (runtime) => {
82
+ const { db, config, offline } = runtime;
83
+ await syncBeforeNoteCommand(runtime);
84
+ const query = args.query?.trim() ?? '';
85
+ assertBulkTargetFilter({ query, options: bulkOptions });
86
+ const selected = selectBulkMutation(bulkOptions);
87
+ const requestedValues = await parseStringListInput(selected.rawValues, selected.optionName);
88
+ const values = selected.field === 'tags'
89
+ ? await validateTagsForBulk(db, requestedValues)
90
+ : await resolveZettelBoxIdsForBulk(db, requestedValues);
91
+ const tagsExpression = typeof bulkOptions.tags === 'string' ? bulkOptions.tags.trim() : '';
92
+ const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
93
+ from: bulkOptions.from,
94
+ to: bulkOptions.to,
95
+ days: bulkOptions.days,
96
+ });
97
+ const starred = typeof bulkOptions.starred === 'string'
98
+ ? parseStarredOption(bulkOptions.starred, '--starred')
99
+ : undefined;
100
+ const boxInput = typeof bulkOptions.boxes === 'string' ? bulkOptions.boxes : '';
101
+ const parsedBoxInput = boxInput
102
+ ? await parseSearchListInput(boxInput, '--boxes')
103
+ : { values: [], isExplicitEmptyArray: false };
104
+ const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, parsedBoxInput.values);
105
+ const matches = await searchNotes(db, query, {
106
+ includeDeleted: false,
107
+ tagsExpression: tagsExpression || undefined,
108
+ createdAtFrom,
109
+ createdAtTo,
110
+ starred,
111
+ zettelBoxIds,
112
+ matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
113
+ });
114
+ const ids = matches.map((note) => note.id);
115
+ if (ids.length === 0) {
116
+ throw new DinoxError('No active notes matched the bulk filters');
117
+ }
118
+ const plan = await planNoteMetadataMutation(db, {
119
+ ids,
120
+ field: selected.field,
121
+ operation: selected.operation,
122
+ values,
123
+ displayValues: requestedValues,
124
+ });
125
+ const expectedCount = parseExpectedCount(bulkOptions.expectedCount);
126
+ const filterMeta = {
127
+ query,
128
+ tags_expression: tagsExpression || null,
129
+ created_at_range: createdAtFrom || createdAtTo
130
+ ? { from: createdAtFrom ?? null, to: createdAtTo ?? null }
131
+ : null,
132
+ days: normalizeDaysForMeta(bulkOptions.days) ?? null,
133
+ starred: typeof starred === 'boolean' ? starred : null,
134
+ box_names: parsedBoxInput.values,
135
+ box_ids: zettelBoxIds,
136
+ match_empty_boxes: parsedBoxInput.isExplicitEmptyArray,
137
+ all: Boolean(bulkOptions.all),
138
+ };
139
+ if (bulkOptions.dryRun) {
140
+ printDryRunPreview({
141
+ dry_run: true,
142
+ command: 'dino note bulk',
143
+ action: 'bulk_mutate_note_metadata',
144
+ risk: 'high-risk-write',
145
+ summary: `Preview bulk ${selected.field} ${selected.operation} for ${plan.total} matched note(s); ${plan.changedCount} note(s) would change.`,
146
+ warnings: buildExecutionWarnings({
147
+ offline,
148
+ uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
149
+ total: plan.total,
150
+ }),
151
+ inputs: {
152
+ filters: filterMeta,
153
+ operation: selected.operation,
154
+ field: selected.field,
155
+ [selected.field]: selected.field === 'tags' ? values : requestedValues,
156
+ expected_count: expectedCount,
157
+ preview_limit: BULK_PREVIEW_LIMIT,
158
+ preview_truncated: plan.total > BULK_PREVIEW_LIMIT,
159
+ },
160
+ targets: previewItems(plan.results).map((item) => ({ id: item.id, title: item.title })),
161
+ changes: previewItems(plan.results).map((item) => ({
162
+ id: item.id,
163
+ title: item.title,
164
+ [`${selected.field}_from`]: item.before,
165
+ [`${selected.field}_to`]: item.after,
166
+ changed: item.changed,
167
+ })),
168
+ });
169
+ return;
170
+ }
171
+ if (!bulkOptions.confirm) {
172
+ throw new DinoxError('Bulk writes require --confirm');
173
+ }
174
+ if (expectedCount === null) {
175
+ throw new DinoxError('Bulk writes require --expected-count <n>');
176
+ }
177
+ if (expectedCount !== plan.total) {
178
+ throw new DinoxError(`--expected-count ${expectedCount} does not match current target count ${plan.total}`);
179
+ }
180
+ await applyNoteMetadataMutation(db, plan);
181
+ printUploadDisabledWarning(runtime);
182
+ await waitForMutationUpload(runtime);
183
+ const payload = {
184
+ ok: true,
185
+ field: selected.field,
186
+ operation: selected.operation,
187
+ total: plan.total,
188
+ updatedCount: plan.changedCount,
189
+ filters: filterMeta,
190
+ results: plan.results.map((item) => ({
191
+ id: item.id,
192
+ updated: item.changed,
193
+ [selected.field]: item.after,
194
+ })),
195
+ stale: commandStale(runtime),
196
+ };
197
+ if (structuredOutput) {
198
+ printSuccess(payload);
199
+ return;
200
+ }
201
+ printHumanReadableLine(`Updated ${plan.changedCount}/${plan.total} notes`);
202
+ printStaleCacheWarning(runtime);
203
+ });
204
+ }
@@ -0,0 +1,95 @@
1
+ export type TiptapNode = {
2
+ type: string;
3
+ attrs?: Record<string, unknown>;
4
+ content?: TiptapNode[];
5
+ marks?: Array<Record<string, unknown>>;
6
+ text?: string;
7
+ [key: string]: unknown;
8
+ };
9
+ export type NoteContentEditMode = 'text' | 'inlineAtom' | 'atomicBlock' | 'container' | 'unsupported';
10
+ export type NoteContentBlockDescriptor = {
11
+ anchorId: string;
12
+ hasStableAnchor: boolean;
13
+ nodeIndex: number;
14
+ nodeRange: string;
15
+ type: string;
16
+ editMode: NoteContentEditMode;
17
+ level: number | null;
18
+ text: string;
19
+ textPreview: string;
20
+ textHash: string;
21
+ nodeHash: string;
22
+ charCount: number;
23
+ attrs?: Record<string, unknown>;
24
+ };
25
+ export type NoteContentOutlineItem = {
26
+ path: string;
27
+ level: number;
28
+ nodeRange: string;
29
+ contentHash: string;
30
+ };
31
+ export type NoteContentProjection = {
32
+ document: TiptapNode;
33
+ contentJson: string;
34
+ contentMd: string;
35
+ contentText: string;
36
+ hashtags: string[];
37
+ images: string[];
38
+ bidLinks: Array<{
39
+ id: string;
40
+ type: string;
41
+ }>;
42
+ };
43
+ export type HashtagReferenceResolver = (input: {
44
+ id: string;
45
+ path: string;
46
+ }) => {
47
+ id: string;
48
+ path: string;
49
+ } | null;
50
+ export type NoteContentProjectionOptions = {
51
+ resolveHashtagReference?: HashtagReferenceResolver;
52
+ };
53
+ export type JsonPatchOperation = {
54
+ kind: 'appendSection';
55
+ heading: string;
56
+ content: string;
57
+ } | {
58
+ kind: 'appendToHeading';
59
+ headingPath: string;
60
+ content: string;
61
+ } | {
62
+ kind: 'replaceSection';
63
+ headingPath: string;
64
+ content: string;
65
+ allowProtectedReplace?: boolean;
66
+ } | {
67
+ kind: 'replaceBlock';
68
+ match: string;
69
+ content: string;
70
+ allowProtectedReplace?: boolean;
71
+ };
72
+ export type JsonPatchPlan = {
73
+ operation: JsonPatchOperation['kind'];
74
+ changed: boolean;
75
+ beforeHash: string;
76
+ afterHash: string;
77
+ beforeContent: string;
78
+ afterContent: string;
79
+ beforeBlocks: NoteContentBlockDescriptor[];
80
+ afterBlocks: NoteContentBlockDescriptor[];
81
+ beforeProjection: NoteContentProjection;
82
+ afterProjection: NoteContentProjection;
83
+ target?: Record<string, unknown>;
84
+ };
85
+ export declare function parseStoredNoteDocument(input: {
86
+ contentJson: string | null;
87
+ contentMd: string | null;
88
+ }): TiptapNode;
89
+ export declare function buildNoteContentBlocks(document: TiptapNode): NoteContentBlockDescriptor[];
90
+ export declare function buildNoteContentOutline(document: TiptapNode): NoteContentOutlineItem[];
91
+ export declare function getNoteContentHash(document: TiptapNode): string;
92
+ export declare function serializeNoteContentMarkdown(document: TiptapNode): string;
93
+ export declare function projectNoteContent(document: TiptapNode, options?: NoteContentProjectionOptions): NoteContentProjection;
94
+ export declare function applyJsonPatchToDocument(document: TiptapNode, operation: JsonPatchOperation, options?: NoteContentProjectionOptions): JsonPatchPlan;
95
+ export declare function withStableBlockAnchors(document: TiptapNode): TiptapNode;