@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.
- package/README.md +72 -139
- package/dist/auth/userInfo.d.ts +8 -1
- package/dist/auth/userInfo.js +38 -6
- package/dist/cli.js +1 -2
- package/dist/cliTypes.d.ts +1 -0
- package/dist/commands/auth/index.js +11 -4
- package/dist/commands/boxes/index.js +274 -54
- package/dist/commands/boxes/model.d.ts +34 -0
- package/dist/commands/boxes/model.js +78 -0
- package/dist/commands/boxes/operations.d.ts +86 -0
- package/dist/commands/boxes/operations.js +415 -0
- package/dist/commands/boxes/repo.d.ts +13 -6
- package/dist/commands/boxes/repo.js +272 -149
- package/dist/commands/boxes/store.d.ts +22 -0
- package/dist/commands/boxes/store.js +143 -0
- package/dist/commands/graph/index.js +11 -38
- package/dist/commands/mcp/noteContentTools.d.ts +222 -0
- package/dist/commands/mcp/noteContentTools.js +390 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +43 -15
- package/dist/commands/mcp/tools.js +3 -1
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/meta/index.js +8 -24
- package/dist/commands/meta/repo.d.ts +0 -3
- package/dist/commands/meta/repo.js +1 -4
- package/dist/commands/notes/actions.d.ts +2 -0
- package/dist/commands/notes/actions.js +149 -55
- package/dist/commands/notes/bulk.d.ts +34 -0
- package/dist/commands/notes/bulk.js +204 -0
- package/dist/commands/notes/contentEngine.d.ts +95 -0
- package/dist/commands/notes/contentEngine.js +652 -0
- package/dist/commands/notes/contentPatch.d.ts +84 -0
- package/dist/commands/notes/contentPatch.js +337 -0
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +126 -0
- package/dist/commands/notes/descriptors.js +194 -42
- package/dist/commands/notes/export.d.ts +21 -0
- package/dist/commands/notes/export.js +224 -0
- package/dist/commands/notes/metadataMutation.d.ts +28 -0
- package/dist/commands/notes/metadataMutation.js +127 -0
- package/dist/commands/notes/repo.d.ts +20 -0
- package/dist/commands/notes/repo.js +135 -18
- package/dist/commands/notes/runtime.d.ts +3 -12
- package/dist/commands/notes/runtime.js +4 -30
- package/dist/commands/notes/searchSql.js +1 -1
- package/dist/commands/prompt/index.js +17 -47
- package/dist/commands/prompt/repo.js +2 -2
- package/dist/commands/runtime.d.ts +34 -0
- package/dist/commands/runtime.js +110 -0
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +21 -7
- package/dist/commands/schema/spec.d.ts +1 -2
- package/dist/commands/schema/spec.js +2 -3
- package/dist/commands/schema/types.d.ts +0 -13
- package/dist/commands/storage/index.js +32 -62
- package/dist/commands/sync.js +22 -5
- package/dist/commands/tags/content.d.ts +32 -0
- package/dist/commands/tags/content.js +243 -0
- package/dist/commands/tags/index.js +287 -51
- package/dist/commands/tags/model.d.ts +38 -0
- package/dist/commands/tags/model.js +100 -0
- package/dist/commands/tags/operations.d.ts +105 -0
- package/dist/commands/tags/operations.js +438 -0
- package/dist/commands/tags/repo.d.ts +5 -1
- package/dist/commands/tags/repo.js +142 -218
- package/dist/commands/tags/store.d.ts +28 -0
- package/dist/commands/tags/store.js +166 -0
- package/dist/commands/todo/index.js +32 -63
- package/dist/commands/writeDryRun.d.ts +25 -0
- package/dist/commands/writeDryRun.js +31 -1
- package/dist/config/resolve.js +2 -2
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +1 -0
- package/dist/powersync/runtime.js +16 -0
- package/dist/powersync/schema/content.d.ts +2 -0
- package/dist/powersync/schema/content.js +2 -0
- package/dist/powersync/schema/index.d.ts +2 -0
- package/dist/powersync/syncGate.d.ts +24 -0
- package/dist/powersync/syncGate.js +36 -0
- package/dist/skills/check.d.ts +1 -1
- package/dist/skills/check.js +27 -2
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +7 -2
- package/dist/utils/noteContentEngine.d.ts +333 -0
- package/dist/utils/noteContentEngine.js +740 -0
- package/dist/utils/tiptapMarkdownSchema.js +31 -0
- 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 {
|
|
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 (
|
|
48
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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:
|
|
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
|
-
|
|
129
|
-
|
|
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 (
|
|
136
|
-
|
|
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:
|
|
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 (
|
|
195
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
243
|
-
|
|
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:
|
|
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 (
|
|
258
|
-
|
|
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 (
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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 (
|
|
337
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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;
|