@dinoxx/dinox-cli 1.0.22 → 1.1.0
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 +29 -9
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +74 -15
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.js +16 -12
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +42 -0
- package/dist/daemon/lifecycle.js +115 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +19 -0
- package/dist/powersync/exclusiveRuntime.js +42 -0
- package/dist/powersync/runtime.d.ts +24 -4
- package/dist/powersync/runtime.js +234 -36
- package/dist/powersync/syncGate.d.ts +3 -1
- package/dist/powersync/syncGate.js +7 -1
- package/dist/powersync/tokenIndex.d.ts +11 -0
- package/dist/powersync/tokenIndex.js +159 -22
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +12 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
1
2
|
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
2
3
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
3
4
|
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
-
import { commandStale, printStaleCacheWarning,
|
|
5
|
+
import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime, waitForMutationUpload, } from '../runtime.js';
|
|
5
6
|
import { resolveZettelBoxIdsForBulk, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForBulk, } from './repo.js';
|
|
6
7
|
import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
|
|
7
8
|
import { parseCreatedAtRange } from './searchTime.js';
|
|
8
9
|
import { normalizeDaysForMeta, } from './searchResponse.js';
|
|
9
|
-
import { parseSearchListInput, parseStarredOption,
|
|
10
|
+
import { parseMutationListInput, parseSearchListInput, parseStarredOption, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
11
|
+
import { buildBatchWriteReceiptFields, readNoteRevisionReceipts, } from './writeReceipt.js';
|
|
10
12
|
const BULK_PREVIEW_LIMIT = 100;
|
|
11
13
|
export function selectBulkMutation(options) {
|
|
12
14
|
const candidates = [
|
|
@@ -42,13 +44,14 @@ export function parseExpectedCount(value) {
|
|
|
42
44
|
}
|
|
43
45
|
function hasConcreteTargetFilter(input) {
|
|
44
46
|
const { query, options } = input;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
options.
|
|
48
|
-
options.
|
|
49
|
-
options.
|
|
50
|
-
options.
|
|
51
|
-
options.
|
|
47
|
+
const hasValue = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
48
|
+
return Boolean(hasValue(query) ||
|
|
49
|
+
hasValue(options.tags) ||
|
|
50
|
+
hasValue(options.from) ||
|
|
51
|
+
hasValue(options.to) ||
|
|
52
|
+
hasValue(options.days) ||
|
|
53
|
+
hasValue(options.starred) ||
|
|
54
|
+
hasValue(options.boxes));
|
|
52
55
|
}
|
|
53
56
|
export function assertBulkTargetFilter(input) {
|
|
54
57
|
const hasFilter = hasConcreteTargetFilter(input);
|
|
@@ -64,9 +67,6 @@ function buildExecutionWarnings(input) {
|
|
|
64
67
|
if (input.offline) {
|
|
65
68
|
warnings.push('Execution would run in offline mode and only use the local cache.');
|
|
66
69
|
}
|
|
67
|
-
if (!input.offline && !input.uploadEnabled) {
|
|
68
|
-
warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
|
|
69
|
-
}
|
|
70
70
|
if (input.total > BULK_PREVIEW_LIMIT) {
|
|
71
71
|
warnings.push(`Preview is truncated to the first ${BULK_PREVIEW_LIMIT} matched notes.`);
|
|
72
72
|
}
|
|
@@ -81,12 +81,16 @@ export async function runNoteBulk({ command, options, args }) {
|
|
|
81
81
|
await withNoteDatabase(command, async (runtime) => {
|
|
82
82
|
const { db, config, offline } = runtime;
|
|
83
83
|
await syncBeforeNoteCommand(runtime);
|
|
84
|
+
const durability = resolveWriteDurabilityForRuntime(runtime, bulkOptions.durability);
|
|
84
85
|
const query = args.query?.trim() ?? '';
|
|
85
86
|
assertBulkTargetFilter({ query, options: bulkOptions });
|
|
86
87
|
const selected = selectBulkMutation(bulkOptions);
|
|
87
|
-
const requestedValues = await
|
|
88
|
+
const requestedValues = await parseMutationListInput(selected.rawValues, selected.optionName);
|
|
89
|
+
const tagUserId = selected.field === 'tags'
|
|
90
|
+
? (await resolveAuthIdentity(config)).userId
|
|
91
|
+
: undefined;
|
|
88
92
|
const values = selected.field === 'tags'
|
|
89
|
-
? await validateTagsForBulk(db, requestedValues)
|
|
93
|
+
? await validateTagsForBulk(db, requestedValues, tagUserId)
|
|
90
94
|
: await resolveZettelBoxIdsForBulk(db, requestedValues);
|
|
91
95
|
const tagsExpression = typeof bulkOptions.tags === 'string' ? bulkOptions.tags.trim() : '';
|
|
92
96
|
const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
|
|
@@ -178,8 +182,9 @@ export async function runNoteBulk({ command, options, args }) {
|
|
|
178
182
|
throw new DinoxError(`--expected-count ${expectedCount} does not match current target count ${plan.total}`);
|
|
179
183
|
}
|
|
180
184
|
await applyNoteMetadataMutation(db, plan);
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
const upload = await waitForMutationUpload(runtime, { durability });
|
|
186
|
+
const stale = commandStale(runtime);
|
|
187
|
+
const revisions = await readNoteRevisionReceipts(db, plan.results.map((item) => item.id), new Map(plan.results.map((item) => [item.id, item.changed ? [selected.field] : []])));
|
|
183
188
|
const payload = {
|
|
184
189
|
ok: true,
|
|
185
190
|
field: selected.field,
|
|
@@ -187,12 +192,13 @@ export async function runNoteBulk({ command, options, args }) {
|
|
|
187
192
|
total: plan.total,
|
|
188
193
|
updatedCount: plan.changedCount,
|
|
189
194
|
filters: filterMeta,
|
|
195
|
+
...buildBatchWriteReceiptFields(upload, stale),
|
|
190
196
|
results: plan.results.map((item) => ({
|
|
191
197
|
id: item.id,
|
|
192
198
|
updated: item.changed,
|
|
193
199
|
[selected.field]: item.after,
|
|
200
|
+
...revisions.get(item.id),
|
|
194
201
|
})),
|
|
195
|
-
stale: commandStale(runtime),
|
|
196
202
|
};
|
|
197
203
|
if (structuredOutput) {
|
|
198
204
|
printSuccess(payload);
|
|
@@ -2,6 +2,8 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
3
|
import { generateId } from '../../utils/id.js';
|
|
4
4
|
import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
|
|
5
|
+
import { normalizeStoredImageWidthPercent } from '../../utils/tiptapMarkdownSchemaUtils.js';
|
|
6
|
+
import { escapeLiteralMarkdownText } from '../../utils/tiptapMarkdownSyntax.js';
|
|
5
7
|
const CONTENT_TEXT_MAX_LENGTH = 300;
|
|
6
8
|
const MAX_REPLACE_BLOCK_RANGE = 50;
|
|
7
9
|
const ANCHOR_ATTRIBUTE_KEYS = ['anchorKey', 'data-toc-id', 'id', 'uniqueId'];
|
|
@@ -208,11 +210,18 @@ function findSerializedBlockRanges(nodes, match) {
|
|
|
208
210
|
return ranges;
|
|
209
211
|
}
|
|
210
212
|
function assertProtectedReplacementAllowed(nodes, allowed) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
+
const protectedTypes = new Set();
|
|
214
|
+
for (const node of nodes) {
|
|
215
|
+
visit(node, (candidate) => {
|
|
216
|
+
if (isProtectedBlock(candidate.type)) {
|
|
217
|
+
protectedTypes.add(candidate.type);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!protectedTypes.size || allowed) {
|
|
213
222
|
return;
|
|
214
223
|
}
|
|
215
|
-
const types = Array.from(
|
|
224
|
+
const types = Array.from(protectedTypes).join(', ');
|
|
216
225
|
throw new DinoxError(`Target includes protected block(s): ${types}. Re-run with --allow-protected-replace after content-read if this replacement is intentional.`);
|
|
217
226
|
}
|
|
218
227
|
function buildHeadingRanges(document) {
|
|
@@ -425,7 +434,7 @@ function blockToMarkdown(node, depth) {
|
|
|
425
434
|
case 'heading': return `${'#'.repeat(normalizeHeadingLevel(node.attrs?.level))} ${text}`.trim();
|
|
426
435
|
case 'paragraph': return text;
|
|
427
436
|
case 'blockquote': return childBlocks(node).map((child) => blockToMarkdown(child, depth)).join('\n').split('\n').map((line) => `> ${line}`).join('\n').trim();
|
|
428
|
-
case 'codeBlock': return
|
|
437
|
+
case 'codeBlock': return fencedMarkdown(stringAttr(node, 'language') || stringAttr(node, 'lang'), extractPlainText(node));
|
|
429
438
|
case 'bulletList': return listToMarkdown(node, depth, '-');
|
|
430
439
|
case 'orderedList': return listToMarkdown(node, depth, '1.');
|
|
431
440
|
case 'taskList': return taskListToMarkdown(node, depth);
|
|
@@ -462,8 +471,10 @@ function inlineMarkdown(nodes) {
|
|
|
462
471
|
}).join('');
|
|
463
472
|
}
|
|
464
473
|
function markedText(node) {
|
|
465
|
-
|
|
466
|
-
|
|
474
|
+
const marks = Array.isArray(node.marks) ? node.marks : [];
|
|
475
|
+
const codeMark = marks.find((mark) => mark?.type === 'code');
|
|
476
|
+
let text = codeMark ? inlineCodeMarkdown(node.text ?? '') : escapeLiteralMarkdownText(node.text ?? '');
|
|
477
|
+
for (const mark of marks) {
|
|
467
478
|
if (mark?.type === 'bold')
|
|
468
479
|
text = `**${text}**`;
|
|
469
480
|
if (mark?.type === 'italic')
|
|
@@ -471,12 +482,14 @@ function markedText(node) {
|
|
|
471
482
|
if (mark?.type === 'strike')
|
|
472
483
|
text = `~~${text}~~`;
|
|
473
484
|
if (mark?.type === 'code')
|
|
474
|
-
|
|
485
|
+
continue;
|
|
475
486
|
if (mark?.type === 'link') {
|
|
476
487
|
const href = stringFromAttrs(mark.attrs, 'href');
|
|
477
488
|
const title = stringFromAttrs(mark.attrs, 'title');
|
|
478
|
-
if (href)
|
|
479
|
-
|
|
489
|
+
if (href) {
|
|
490
|
+
const serializedTitle = title ? ` "${escapeMarkdownLinkTitle(title)}"` : '';
|
|
491
|
+
text = `[${text}](${markdownLinkDestination(href)}${serializedTitle})`;
|
|
492
|
+
}
|
|
480
493
|
}
|
|
481
494
|
}
|
|
482
495
|
return text;
|
|
@@ -515,7 +528,34 @@ function imageToMarkdown(node) {
|
|
|
515
528
|
const src = stringAttr(node, 'src');
|
|
516
529
|
const alt = stringAttr(node, 'alt') || stringAttr(node, 'title') || inlineMarkdown(node.content ?? []);
|
|
517
530
|
const caption = stringAttr(node, 'caption') || inlineMarkdown(node.content ?? []);
|
|
518
|
-
|
|
531
|
+
const image = src
|
|
532
|
+
? `})${imageMetadataComment(node)}`
|
|
533
|
+
: placeholderMarkdown('image', node);
|
|
534
|
+
return [image, caption && caption !== alt ? caption : ''].filter(Boolean).join('\n');
|
|
535
|
+
}
|
|
536
|
+
function markdownLinkDestination(value) {
|
|
537
|
+
const safe = value.replace(/[\u0000-\u001F\u007F<>\\]/g, (character) => {
|
|
538
|
+
return `%${character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`;
|
|
539
|
+
});
|
|
540
|
+
return `<${safe}>`;
|
|
541
|
+
}
|
|
542
|
+
function escapeMarkdownLinkTitle(value) {
|
|
543
|
+
return value
|
|
544
|
+
.replace(/\\/g, '\\\\')
|
|
545
|
+
.replace(/"/g, '\\"')
|
|
546
|
+
.replace(/\r?\n/g, ' ');
|
|
547
|
+
}
|
|
548
|
+
function imageMetadataComment(node) {
|
|
549
|
+
const resourceId = stringAttr(node, 'resourceId');
|
|
550
|
+
const widthPercent = normalizeStoredImageWidthPercent(node.attrs?.widthPercent);
|
|
551
|
+
if (!resourceId && widthPercent == null) {
|
|
552
|
+
return '';
|
|
553
|
+
}
|
|
554
|
+
return `<!-- ${JSON.stringify({
|
|
555
|
+
kind: 'image',
|
|
556
|
+
...(resourceId ? { resourceId } : {}),
|
|
557
|
+
...(widthPercent == null ? {} : { widthPercent }),
|
|
558
|
+
})} -->`;
|
|
519
559
|
}
|
|
520
560
|
function atomicReferenceMarkdown(node) {
|
|
521
561
|
if (node.type === 'video' || node.type === 'audio' || node.type === 'file' || node.type === 'bookmark' || node.type === 'iframe' || node.type === 'audiosegment' || node.type === 'pdfAnnotation') {
|
|
@@ -524,7 +564,25 @@ function atomicReferenceMarkdown(node) {
|
|
|
524
564
|
return '';
|
|
525
565
|
}
|
|
526
566
|
function fencedAttrMarkdown(language, text) {
|
|
527
|
-
return text ?
|
|
567
|
+
return text ? fencedMarkdown(language, text) : `[${language}]`;
|
|
568
|
+
}
|
|
569
|
+
function longestBacktickRun(value) {
|
|
570
|
+
let longest = 0;
|
|
571
|
+
for (const match of value.matchAll(/`+/g)) {
|
|
572
|
+
longest = Math.max(longest, match[0].length);
|
|
573
|
+
}
|
|
574
|
+
return longest;
|
|
575
|
+
}
|
|
576
|
+
function fencedMarkdown(language, value) {
|
|
577
|
+
const fence = '`'.repeat(Math.max(3, longestBacktickRun(value) + 1));
|
|
578
|
+
const safeLanguage = language.replace(/[`\r\n]/g, '').trim();
|
|
579
|
+
return `${fence}${safeLanguage}\n${value}${value.endsWith('\n') ? '' : '\n'}${fence}`;
|
|
580
|
+
}
|
|
581
|
+
function inlineCodeMarkdown(value) {
|
|
582
|
+
const fence = '`'.repeat(Math.max(1, longestBacktickRun(value) + 1));
|
|
583
|
+
const needsPadding = value.startsWith('`') || value.endsWith('`') ||
|
|
584
|
+
(value.startsWith(' ') && value.endsWith(' ') && value.trim().length > 0);
|
|
585
|
+
return `${fence}${needsPadding ? ` ${value} ` : value}${fence}`;
|
|
528
586
|
}
|
|
529
587
|
function placeholderMarkdown(type, node, label) {
|
|
530
588
|
const attrs = [
|
|
@@ -72,13 +72,16 @@ export declare function planMarkdownPatch(input: {
|
|
|
72
72
|
contentMd: string;
|
|
73
73
|
operation: NoteContentPatchOperation;
|
|
74
74
|
}): NoteContentPatchPlan;
|
|
75
|
-
export declare function readNoteContentForPatch(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContentReadResult>;
|
|
75
|
+
export declare function readNoteContentForPatch(db: AbstractPowerSyncDatabase, id: string, databaseScope: string, userId: string): Promise<NoteContentReadResult>;
|
|
76
76
|
export declare function planNoteContentPatch(db: AbstractPowerSyncDatabase, input: {
|
|
77
77
|
id: string;
|
|
78
|
+
userId: string;
|
|
78
79
|
operation: NoteContentPatchOperation;
|
|
79
80
|
}): Promise<NoteContentPatchPlan>;
|
|
80
81
|
export declare function applyNoteContentPatch(db: AbstractPowerSyncDatabase, input: {
|
|
81
82
|
id: string;
|
|
83
|
+
databaseScope: string;
|
|
84
|
+
userId: string;
|
|
82
85
|
readToken: string;
|
|
83
86
|
operation: NoteContentPatchOperation;
|
|
84
87
|
}): Promise<NoteContentPatchResult>;
|
|
@@ -1,18 +1,31 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getDataDir } from '../../config/paths.js';
|
|
4
5
|
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
5
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
6
7
|
import { generateId } from '../../utils/id.js';
|
|
8
|
+
import { acquireExclusiveSqliteLease } from '../../utils/sqliteLease.js';
|
|
9
|
+
import { withAsyncRelease } from '../../utils/asyncRelease.js';
|
|
7
10
|
import { nowIsoUtc } from '../../utils/time.js';
|
|
11
|
+
import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
|
|
8
12
|
import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
|
|
13
|
+
import { parseStoredTags } from './repo.js';
|
|
9
14
|
const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
10
15
|
const MAX_STORED_READ_TOKENS = 500;
|
|
11
16
|
const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
|
|
17
|
+
const TOKEN_STORE_LOCK_TIMEOUT_MS = 5_000;
|
|
12
18
|
function normalizeVersion(value) {
|
|
13
19
|
const parsed = Number(value);
|
|
14
20
|
return Number.isFinite(parsed) ? parsed : null;
|
|
15
21
|
}
|
|
22
|
+
function fingerprintDatabaseScope(value) {
|
|
23
|
+
const normalized = value.trim();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
throw new DinoxError('Database scope is required for note content read tokens.');
|
|
26
|
+
}
|
|
27
|
+
return `sha256:${crypto.createHash('sha256').update(normalized, 'utf8').digest('hex')}`;
|
|
28
|
+
}
|
|
16
29
|
function canonicalTagPath(value) {
|
|
17
30
|
return value
|
|
18
31
|
.split(/[\\/]+/g)
|
|
@@ -23,7 +36,11 @@ function canonicalTagPath(value) {
|
|
|
23
36
|
function tagPathKey(value) {
|
|
24
37
|
return canonicalTagPath(value).toLowerCase();
|
|
25
38
|
}
|
|
26
|
-
async function loadTagReferenceLookup(db) {
|
|
39
|
+
async function loadTagReferenceLookup(db, userId) {
|
|
40
|
+
const normalizedUserId = userId.trim();
|
|
41
|
+
if (!normalizedUserId) {
|
|
42
|
+
throw new DinoxError('Note content tag resolution requires the current userId. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
|
|
43
|
+
}
|
|
27
44
|
const rows = await db.getAll(`
|
|
28
45
|
SELECT
|
|
29
46
|
TRIM(COALESCE(id, '')) AS id,
|
|
@@ -34,11 +51,12 @@ async function loadTagReferenceLookup(db) {
|
|
|
34
51
|
AND TRIM(COALESCE(full_path, path_key, '')) <> ''
|
|
35
52
|
AND (is_del IS NULL OR is_del = 0)
|
|
36
53
|
AND (is_actual IS NULL OR is_actual != 0)
|
|
54
|
+
AND TRIM(COALESCE(user_id, '')) = ?
|
|
37
55
|
ORDER BY
|
|
38
56
|
updated_at DESC,
|
|
39
57
|
created_at DESC,
|
|
40
58
|
id DESC
|
|
41
|
-
`, []);
|
|
59
|
+
`, [normalizedUserId]);
|
|
42
60
|
const byId = new Map();
|
|
43
61
|
const byPathKey = new Map();
|
|
44
62
|
for (const row of rows) {
|
|
@@ -75,6 +93,20 @@ function createHashtagReferenceResolver(lookup, options) {
|
|
|
75
93
|
function tokenStorePath() {
|
|
76
94
|
return path.join(getDataDir(), TOKEN_STORE_FILE);
|
|
77
95
|
}
|
|
96
|
+
function tokenStoreLockPath() {
|
|
97
|
+
return `${tokenStorePath()}.lock.sqlite`;
|
|
98
|
+
}
|
|
99
|
+
async function waitForTokenStoreLock() {
|
|
100
|
+
return acquireExclusiveSqliteLease({
|
|
101
|
+
lockPath: tokenStoreLockPath(),
|
|
102
|
+
timeoutMs: TOKEN_STORE_LOCK_TIMEOUT_MS,
|
|
103
|
+
pollIntervalMs: 20,
|
|
104
|
+
createBusyError: () => new DinoxError('Timed out waiting for the note content token store lock. Retry the command.'),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function withTokenStoreLock(run) {
|
|
108
|
+
return withAsyncRelease(waitForTokenStoreLock, run);
|
|
109
|
+
}
|
|
78
110
|
async function loadTokenStore(nowMs = Date.now()) {
|
|
79
111
|
const filePath = tokenStorePath();
|
|
80
112
|
let tokens = [];
|
|
@@ -83,6 +115,7 @@ async function loadTokenStore(nowMs = Date.now()) {
|
|
|
83
115
|
const parsed = JSON.parse(raw);
|
|
84
116
|
if (Array.isArray(parsed.tokens)) {
|
|
85
117
|
tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
|
|
118
|
+
typeof token.databaseScopeFingerprint === 'string' &&
|
|
86
119
|
typeof token.noteId === 'string' &&
|
|
87
120
|
typeof token.contentHash === 'string' &&
|
|
88
121
|
typeof token.expiresAt === 'string');
|
|
@@ -110,41 +143,42 @@ async function saveTokenStore(store) {
|
|
|
110
143
|
await fs.rename(tmpPath, filePath);
|
|
111
144
|
}
|
|
112
145
|
async function rememberReadToken(input) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
token
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
146
|
+
return withTokenStoreLock(async () => {
|
|
147
|
+
const nowMs = Date.now();
|
|
148
|
+
const store = await loadTokenStore(nowMs);
|
|
149
|
+
const expiresAt = new Date(nowMs + READ_TOKEN_TTL_MS).toISOString();
|
|
150
|
+
const token = {
|
|
151
|
+
token: generateId(),
|
|
152
|
+
databaseScopeFingerprint: fingerprintDatabaseScope(input.databaseScope),
|
|
153
|
+
noteId: input.noteId,
|
|
154
|
+
contentHash: input.contentHash,
|
|
155
|
+
updatedAt: input.updatedAt,
|
|
156
|
+
version: input.version,
|
|
157
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
158
|
+
expiresAt,
|
|
159
|
+
};
|
|
160
|
+
store.tokens.push(token);
|
|
161
|
+
store.tokens = store.tokens.slice(-MAX_STORED_READ_TOKENS);
|
|
162
|
+
await saveTokenStore(store);
|
|
163
|
+
return token;
|
|
164
|
+
});
|
|
129
165
|
}
|
|
130
166
|
async function consumeReadToken(tokenId) {
|
|
131
167
|
const normalized = tokenId.trim();
|
|
132
168
|
if (!normalized) {
|
|
133
169
|
throw new DinoxError('Missing --read-token. Run `dino note content-read <id>` before editing.');
|
|
134
170
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await saveTokenStore({ tokens: next });
|
|
147
|
-
}
|
|
171
|
+
return withTokenStoreLock(async () => {
|
|
172
|
+
const store = await loadTokenStore();
|
|
173
|
+
const token = store.tokens.find((candidate) => candidate.token === normalized);
|
|
174
|
+
if (!token) {
|
|
175
|
+
throw new DinoxError('read token is expired, unknown, or already used. Run `dino note content-read <id>` again.');
|
|
176
|
+
}
|
|
177
|
+
await saveTokenStore({
|
|
178
|
+
tokens: store.tokens.filter((candidate) => candidate.token !== normalized),
|
|
179
|
+
});
|
|
180
|
+
return token;
|
|
181
|
+
});
|
|
148
182
|
}
|
|
149
183
|
async function readNoteContentRow(db, id) {
|
|
150
184
|
const noteId = id.trim();
|
|
@@ -152,7 +186,7 @@ async function readNoteContentRow(db, id) {
|
|
|
152
186
|
throw new DinoxError('Note id is required');
|
|
153
187
|
}
|
|
154
188
|
const row = await db.getOptional(`
|
|
155
|
-
SELECT id, title, content_md, content_json, content_text, updated_at, version, is_del
|
|
189
|
+
SELECT id, title, content_md, content_json, content_text, tags, updated_at, version, is_del
|
|
156
190
|
FROM c_note
|
|
157
191
|
WHERE id = ?
|
|
158
192
|
`, [noteId]);
|
|
@@ -180,6 +214,10 @@ async function updateNoteContentProjection(tx, input) {
|
|
|
180
214
|
setParts.push('images = ?');
|
|
181
215
|
params.push(JSON.stringify(input.images));
|
|
182
216
|
}
|
|
217
|
+
if (columns.has('image_detail')) {
|
|
218
|
+
setParts.push('image_detail = ?');
|
|
219
|
+
params.push(JSON.stringify(input.imageDetail));
|
|
220
|
+
}
|
|
183
221
|
if (columns.has('tags')) {
|
|
184
222
|
setParts.push('tags = ?');
|
|
185
223
|
params.push(JSON.stringify(input.tags));
|
|
@@ -191,12 +229,33 @@ async function updateNoteContentProjection(tx, input) {
|
|
|
191
229
|
setParts.push('updated_at = ?');
|
|
192
230
|
params.push(nowIsoUtc());
|
|
193
231
|
setParts.push('version = COALESCE(version, 0) + 1');
|
|
194
|
-
params.push(input.rowId);
|
|
195
|
-
await tx.execute(`
|
|
232
|
+
params.push(input.rowId, input.expectedVersion);
|
|
233
|
+
const result = await tx.execute(`
|
|
196
234
|
UPDATE c_note
|
|
197
235
|
SET ${setParts.join(',\n ')}
|
|
198
236
|
WHERE id = ?
|
|
237
|
+
AND COALESCE(version, 0) = ?
|
|
238
|
+
AND (is_del IS NULL OR is_del = 0)
|
|
239
|
+
RETURNING id
|
|
199
240
|
`, params);
|
|
241
|
+
if (result.rows?.length !== 1) {
|
|
242
|
+
throw new DinoxError('Note content changed while applying the patch. Run `dino note content-read <id>` again.');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function mergeProjectedTags(storedTags, beforeHashtags, afterHashtags) {
|
|
246
|
+
const contentTagKeys = new Set(beforeHashtags.map((tag) => tag.trim().toLowerCase()).filter(Boolean));
|
|
247
|
+
const result = storedTags.filter((tag) => !contentTagKeys.has(tag.trim().toLowerCase()));
|
|
248
|
+
const seen = new Set(result.map((tag) => tag.trim().toLowerCase()).filter(Boolean));
|
|
249
|
+
for (const tag of afterHashtags) {
|
|
250
|
+
const normalized = tag.trim();
|
|
251
|
+
const key = normalized.toLowerCase();
|
|
252
|
+
if (!normalized || seen.has(key)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
result.push(normalized);
|
|
256
|
+
seen.add(key);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
200
259
|
}
|
|
201
260
|
function buildPatchPlan(input) {
|
|
202
261
|
const document = parseStoredNoteDocument({
|
|
@@ -237,9 +296,9 @@ export function planMarkdownPatch(input) {
|
|
|
237
296
|
});
|
|
238
297
|
return plan;
|
|
239
298
|
}
|
|
240
|
-
export async function readNoteContentForPatch(db, id) {
|
|
299
|
+
export async function readNoteContentForPatch(db, id, databaseScope, userId) {
|
|
241
300
|
const row = await readNoteContentRow(db, id);
|
|
242
|
-
const tagLookup = await loadTagReferenceLookup(db);
|
|
301
|
+
const tagLookup = await loadTagReferenceLookup(db, userId);
|
|
243
302
|
const document = parseStoredNoteDocument({
|
|
244
303
|
contentJson: row.content_json,
|
|
245
304
|
contentMd: row.content_md,
|
|
@@ -250,6 +309,7 @@ export async function readNoteContentForPatch(db, id) {
|
|
|
250
309
|
const contentHash = getNoteContentHash(document);
|
|
251
310
|
const version = normalizeVersion(row.version);
|
|
252
311
|
const token = await rememberReadToken({
|
|
312
|
+
databaseScope,
|
|
253
313
|
noteId: row.id,
|
|
254
314
|
contentHash,
|
|
255
315
|
updatedAt: row.updated_at ?? null,
|
|
@@ -277,7 +337,7 @@ export async function readNoteContentForPatch(db, id) {
|
|
|
277
337
|
}
|
|
278
338
|
export async function planNoteContentPatch(db, input) {
|
|
279
339
|
const row = await readNoteContentRow(db, input.id);
|
|
280
|
-
const tagLookup = await loadTagReferenceLookup(db);
|
|
340
|
+
const tagLookup = await loadTagReferenceLookup(db, input.userId);
|
|
281
341
|
const { afterProjection: _afterProjection, ...plan } = buildPatchPlan({
|
|
282
342
|
id: row.id,
|
|
283
343
|
title: row.title?.trim() ?? '',
|
|
@@ -290,8 +350,11 @@ export async function planNoteContentPatch(db, input) {
|
|
|
290
350
|
}
|
|
291
351
|
export async function applyNoteContentPatch(db, input) {
|
|
292
352
|
const token = await consumeReadToken(input.readToken);
|
|
353
|
+
if (token.databaseScopeFingerprint !== fingerprintDatabaseScope(input.databaseScope)) {
|
|
354
|
+
throw new DinoxError('read token was issued for a different local account database. Run `dino note content-read <id>` again.');
|
|
355
|
+
}
|
|
293
356
|
const result = await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
294
|
-
const tagLookup = await loadTagReferenceLookup(tx);
|
|
357
|
+
const tagLookup = await loadTagReferenceLookup(tx, input.userId);
|
|
295
358
|
const row = await readNoteContentRow(tx, input.id);
|
|
296
359
|
if (row.id !== token.noteId) {
|
|
297
360
|
throw new DinoxError('read token does not match target note. Run `dino note content-read <id>` again.');
|
|
@@ -303,7 +366,6 @@ export async function applyNoteContentPatch(db, input) {
|
|
|
303
366
|
if (token.updatedAt !== (row.updated_at ?? null) ||
|
|
304
367
|
token.version !== normalizeVersion(row.version) ||
|
|
305
368
|
token.contentHash !== getNoteContentHash(currentDocument)) {
|
|
306
|
-
await forgetReadToken(token.token);
|
|
307
369
|
throw new DinoxError('Note content changed after content-read. Run `dino note content-read <id>` again.');
|
|
308
370
|
}
|
|
309
371
|
const plan = buildPatchPlan({
|
|
@@ -315,20 +377,22 @@ export async function applyNoteContentPatch(db, input) {
|
|
|
315
377
|
resolveHashtagReference: createHashtagReferenceResolver(tagLookup, { strictMissingPaths: true }),
|
|
316
378
|
});
|
|
317
379
|
if (plan.changed) {
|
|
380
|
+
const nextTags = mergeProjectedTags(parseStoredTags(row.tags), plan.derived.beforeHashtags, plan.derived.afterHashtags);
|
|
318
381
|
await updateNoteContentProjection(tx, {
|
|
319
382
|
rowId: row.id,
|
|
320
383
|
contentMd: plan.afterProjection.contentMd,
|
|
321
384
|
contentJson: plan.afterProjection.contentJson,
|
|
322
385
|
contentText: plan.afterProjection.contentText,
|
|
323
|
-
tags:
|
|
386
|
+
tags: nextTags,
|
|
324
387
|
images: plan.afterProjection.images,
|
|
325
388
|
bidLinks: plan.afterProjection.bidLinks,
|
|
389
|
+
imageDetail: deriveImageDetailFromContentJson(plan.afterProjection.document, row.id),
|
|
390
|
+
expectedVersion: normalizeVersion(row.version) ?? 0,
|
|
326
391
|
});
|
|
327
392
|
}
|
|
328
393
|
const { afterProjection: _afterProjection, ...publicPlan } = plan;
|
|
329
394
|
return publicPlan;
|
|
330
395
|
});
|
|
331
|
-
await forgetReadToken(token.token);
|
|
332
396
|
const { beforeContent: _beforeContent, afterContent: _afterContent, ...publicResult } = result;
|
|
333
397
|
return {
|
|
334
398
|
...publicResult,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
2
|
+
import { getPowerSyncDbPath } from '../../config/paths.js';
|
|
1
3
|
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
2
4
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
5
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
4
|
-
import { commandStale, printStaleCacheWarning,
|
|
6
|
+
import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime, waitForMutationUpload, } from '../runtime.js';
|
|
7
|
+
import { readNoteWriteReceipt } from './writeReceipt.js';
|
|
5
8
|
import { applyNoteContentPatch, planNoteContentPatch, readNoteContentForPatch, } from './contentPatch.js';
|
|
6
9
|
import { parseStringInput, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
7
10
|
function requireNoteId(id) {
|
|
@@ -56,7 +59,8 @@ export async function runNoteContentRead({ command, options, args }) {
|
|
|
56
59
|
const id = requireNoteId(args.id);
|
|
57
60
|
await withNoteDatabase(command, async (runtime) => {
|
|
58
61
|
await syncBeforeNoteCommand(runtime);
|
|
59
|
-
const
|
|
62
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
63
|
+
const result = await readNoteContentForPatch(runtime.db, id, getPowerSyncDbPath(identity.userId), identity.userId);
|
|
60
64
|
const { content, ...rest } = result;
|
|
61
65
|
const payload = readOptions.includeContent
|
|
62
66
|
? { ...rest, content, stale: commandStale(runtime) }
|
|
@@ -72,8 +76,10 @@ export async function runNotePatch({ command, options, args }) {
|
|
|
72
76
|
await withNoteDatabase(command, async (runtime) => {
|
|
73
77
|
const { db, config, offline } = runtime;
|
|
74
78
|
await syncBeforeNoteCommand(runtime);
|
|
79
|
+
const identity = await resolveAuthIdentity(config);
|
|
80
|
+
const durability = resolveWriteDurabilityForRuntime(runtime, patchOptions.durability);
|
|
75
81
|
if (patchOptions.dryRun) {
|
|
76
|
-
const plan = await planNoteContentPatch(db, { id, operation });
|
|
82
|
+
const plan = await planNoteContentPatch(db, { id, userId: identity.userId, operation });
|
|
77
83
|
printDryRunPreview({
|
|
78
84
|
dry_run: true,
|
|
79
85
|
command: 'dino note patch',
|
|
@@ -83,9 +89,6 @@ export async function runNotePatch({ command, options, args }) {
|
|
|
83
89
|
warnings: [
|
|
84
90
|
'Run `dino note content-read <id> --format json` before the real edit and pass the returned readToken as --read-token.',
|
|
85
91
|
...(offline ? ['Execution would run in offline mode and only use the local cache.'] : []),
|
|
86
|
-
...(!offline && !config.powersync.uploadBaseUrl
|
|
87
|
-
? ['Execution would be local-only because powersync.uploadBaseUrl is unset.']
|
|
88
|
-
: []),
|
|
89
92
|
],
|
|
90
93
|
inputs: {
|
|
91
94
|
id,
|
|
@@ -106,13 +109,27 @@ export async function runNotePatch({ command, options, args }) {
|
|
|
106
109
|
if (!readToken) {
|
|
107
110
|
throw new DinoxError('Real note content edits require --read-token from `dino note content-read <id>`.');
|
|
108
111
|
}
|
|
109
|
-
const result = await applyNoteContentPatch(db, {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const result = await applyNoteContentPatch(db, {
|
|
113
|
+
id,
|
|
114
|
+
databaseScope: getPowerSyncDbPath(identity.userId),
|
|
115
|
+
userId: identity.userId,
|
|
116
|
+
readToken,
|
|
117
|
+
operation,
|
|
118
|
+
});
|
|
119
|
+
const upload = await waitForMutationUpload(runtime, { durability });
|
|
120
|
+
const receipt = await readNoteWriteReceipt(db, id, {
|
|
121
|
+
changed: result.saved
|
|
122
|
+
? ['content_md', 'content_json', 'content_text', 'images', 'image_detail', 'tags', 'bid_links']
|
|
123
|
+
: [],
|
|
124
|
+
upload,
|
|
125
|
+
stale: commandStale(runtime),
|
|
126
|
+
});
|
|
127
|
+
const { changed: contentChanged, ...publicResult } = result;
|
|
112
128
|
const payload = {
|
|
113
129
|
ok: true,
|
|
114
|
-
...
|
|
115
|
-
|
|
130
|
+
...publicResult,
|
|
131
|
+
content_changed: contentChanged,
|
|
132
|
+
...receipt,
|
|
116
133
|
};
|
|
117
134
|
if (structuredOutput) {
|
|
118
135
|
printSuccess(payload);
|