@dinoxx/dinox-cli 1.0.22 → 1.1.1

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 (147) hide show
  1. package/README.md +33 -10
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +55 -14
  4. package/dist/cli.js +36 -2
  5. package/dist/commands/auth/index.d.ts +35 -0
  6. package/dist/commands/auth/index.js +382 -83
  7. package/dist/commands/boxes/index.js +2 -6
  8. package/dist/commands/boxes/model.d.ts +3 -0
  9. package/dist/commands/boxes/model.js +16 -1
  10. package/dist/commands/boxes/operations.js +18 -8
  11. package/dist/commands/boxes/repo.js +13 -11
  12. package/dist/commands/boxes/store.js +1 -1
  13. package/dist/commands/config/index.js +7 -5
  14. package/dist/commands/descriptor/register.js +9 -0
  15. package/dist/commands/doctor.d.ts +123 -0
  16. package/dist/commands/doctor.js +539 -0
  17. package/dist/commands/graph/index.d.ts +1 -1
  18. package/dist/commands/graph/index.js +35 -21
  19. package/dist/commands/graph/repo.js +92 -17
  20. package/dist/commands/meta/index.js +4 -4
  21. package/dist/commands/meta/repo.js +4 -3
  22. package/dist/commands/notes/actions.d.ts +1 -1
  23. package/dist/commands/notes/actions.js +121 -78
  24. package/dist/commands/notes/bulk.d.ts +1 -0
  25. package/dist/commands/notes/bulk.js +23 -17
  26. package/dist/commands/notes/contentEngine.js +69 -11
  27. package/dist/commands/notes/contentPatch.d.ts +4 -1
  28. package/dist/commands/notes/contentPatch.js +105 -41
  29. package/dist/commands/notes/contentPatchCommand.js +28 -11
  30. package/dist/commands/notes/descriptors.js +50 -20
  31. package/dist/commands/notes/dryRun.d.ts +1 -0
  32. package/dist/commands/notes/dryRun.js +4 -7
  33. package/dist/commands/notes/export.d.ts +6 -0
  34. package/dist/commands/notes/export.js +26 -6
  35. package/dist/commands/notes/metadataMutation.d.ts +1 -0
  36. package/dist/commands/notes/metadataMutation.js +14 -3
  37. package/dist/commands/notes/repo.d.ts +46 -4
  38. package/dist/commands/notes/repo.js +113 -28
  39. package/dist/commands/notes/runtime.d.ts +2 -1
  40. package/dist/commands/notes/runtime.js +17 -1
  41. package/dist/commands/notes/searchSql.js +13 -6
  42. package/dist/commands/notes/writeReceipt.d.ts +20 -0
  43. package/dist/commands/notes/writeReceipt.js +82 -0
  44. package/dist/commands/prompt/index.js +3 -3
  45. package/dist/commands/prompt/repo.d.ts +4 -1
  46. package/dist/commands/prompt/repo.js +17 -7
  47. package/dist/commands/runtime.d.ts +14 -2
  48. package/dist/commands/runtime.js +89 -19
  49. package/dist/commands/schema/renderSkills.js +5 -2
  50. package/dist/commands/schema/spec.js +4 -0
  51. package/dist/commands/storage/index.js +8 -2
  52. package/dist/commands/storage/repo.d.ts +4 -0
  53. package/dist/commands/storage/repo.js +356 -92
  54. package/dist/commands/sync.d.ts +23 -1
  55. package/dist/commands/sync.js +105 -60
  56. package/dist/commands/tags/content.js +9 -0
  57. package/dist/commands/tags/index.js +2 -6
  58. package/dist/commands/tags/operations.js +15 -10
  59. package/dist/commands/tags/repo.d.ts +2 -0
  60. package/dist/commands/tags/repo.js +18 -5
  61. package/dist/commands/tags/store.js +1 -1
  62. package/dist/commands/todo/contentModel.d.ts +4 -3
  63. package/dist/commands/todo/contentModel.js +78 -28
  64. package/dist/commands/todo/dryRun.d.ts +1 -0
  65. package/dist/commands/todo/dryRun.js +17 -13
  66. package/dist/commands/todo/index.d.ts +6 -0
  67. package/dist/commands/todo/index.js +89 -123
  68. package/dist/commands/todo/repo.d.ts +36 -1
  69. package/dist/commands/todo/repo.js +146 -37
  70. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  71. package/dist/commands/todo/taskIdentity.js +64 -0
  72. package/dist/commands/todo/taskModel.js +27 -17
  73. package/dist/commands/update.d.ts +13 -1
  74. package/dist/commands/update.js +135 -23
  75. package/dist/commands/writeDryRun.d.ts +3 -2
  76. package/dist/commands/writeDryRun.js +4 -3
  77. package/dist/commands/writeDurability.d.ts +5 -0
  78. package/dist/commands/writeDurability.js +22 -0
  79. package/dist/config/paths.js +6 -1
  80. package/dist/config/resolve.d.ts +2 -0
  81. package/dist/config/resolve.js +23 -9
  82. package/dist/config/store.d.ts +3 -0
  83. package/dist/config/store.js +61 -14
  84. package/dist/config/types.d.ts +4 -0
  85. package/dist/daemon/client.d.ts +26 -1
  86. package/dist/daemon/client.js +236 -12
  87. package/dist/daemon/ensure.js +74 -12
  88. package/dist/daemon/index.d.ts +42 -0
  89. package/dist/daemon/index.js +202 -52
  90. package/dist/daemon/lifecycle.d.ts +44 -0
  91. package/dist/daemon/lifecycle.js +118 -0
  92. package/dist/daemon/pid.d.ts +19 -6
  93. package/dist/daemon/pid.js +147 -13
  94. package/dist/daemon/route.d.ts +14 -0
  95. package/dist/daemon/route.js +40 -0
  96. package/dist/daemon/server.d.ts +11 -0
  97. package/dist/daemon/server.js +201 -50
  98. package/dist/daemon/types.d.ts +29 -1
  99. package/dist/daemon/types.js +1 -0
  100. package/dist/dinox.js +25 -1
  101. package/dist/powersync/connector.js +31 -16
  102. package/dist/powersync/exclusiveRuntime.d.ts +20 -0
  103. package/dist/powersync/exclusiveRuntime.js +48 -0
  104. package/dist/powersync/runtime.d.ts +25 -4
  105. package/dist/powersync/runtime.js +255 -35
  106. package/dist/powersync/syncGate.d.ts +4 -1
  107. package/dist/powersync/syncGate.js +11 -2
  108. package/dist/powersync/syncPass.d.ts +2 -0
  109. package/dist/powersync/syncPass.js +15 -4
  110. package/dist/powersync/tokenIndex.d.ts +13 -0
  111. package/dist/powersync/tokenIndex.js +216 -37
  112. package/dist/powersync/uploader.js +107 -52
  113. package/dist/utils/abort.js +0 -4
  114. package/dist/utils/asyncRelease.d.ts +2 -0
  115. package/dist/utils/asyncRelease.js +25 -0
  116. package/dist/utils/bidlinkCanonicalization.js +3 -0
  117. package/dist/utils/dryRun.d.ts +1 -1
  118. package/dist/utils/errors.d.ts +15 -1
  119. package/dist/utils/errors.js +223 -5
  120. package/dist/utils/output.d.ts +1 -0
  121. package/dist/utils/output.js +51 -15
  122. package/dist/utils/redact.js +1 -1
  123. package/dist/utils/sqliteLease.d.ts +8 -0
  124. package/dist/utils/sqliteLease.js +59 -0
  125. package/dist/utils/syncTimeout.d.ts +7 -1
  126. package/dist/utils/syncTimeout.js +8 -7
  127. package/dist/utils/terminalSanitization.js +6 -2
  128. package/dist/utils/tiptapMarkdown.js +49 -3
  129. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  130. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  131. package/dist/utils/tokenize.js +24 -2
  132. package/dist/utils/updateNotice.js +60 -12
  133. package/package.json +14 -10
  134. package/dist/commands/mcp/auth.d.ts +0 -4
  135. package/dist/commands/mcp/auth.js +0 -75
  136. package/dist/commands/mcp/index.d.ts +0 -2
  137. package/dist/commands/mcp/index.js +0 -86
  138. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  139. package/dist/commands/mcp/noteContentTools.js +0 -390
  140. package/dist/commands/mcp/server.d.ts +0 -34
  141. package/dist/commands/mcp/server.js +0 -424
  142. package/dist/commands/mcp/tools.d.ts +0 -2
  143. package/dist/commands/mcp/tools.js +0 -32
  144. package/dist/commands/mcp/types.d.ts +0 -39
  145. package/dist/commands/mcp/types.js +0 -1
  146. package/dist/utils/noteContentEngine.d.ts +0 -333
  147. package/dist/utils/noteContentEngine.js +0 -740
@@ -1,18 +1,118 @@
1
1
  import { loadConfig } from '../config/store.js';
2
2
  import { resolveConfig } from '../config/resolve.js';
3
3
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
4
+ import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
4
5
  import { registerStandaloneCommand } from './descriptor/register.js';
5
6
  import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
6
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
7
8
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
8
- import { connectPowerSync, getStatusSnapshot } from '../powersync/runtime.js';
9
+ import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
9
10
  import { runSingleSyncPass } from '../powersync/syncPass.js';
10
11
  import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
12
+ const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
13
+ const syncCommandDependencies = {
14
+ loadConfig,
15
+ resolveConfig,
16
+ hydrateResolvedAuthIdentity,
17
+ withExclusivePowerSync,
18
+ getRequestedStructuredOutputFormat,
19
+ getStatusSnapshot,
20
+ waitForLastSyncedAfter,
21
+ runSingleSyncPass,
22
+ printHumanReadableLine,
23
+ printSuccess,
24
+ now: Date.now,
25
+ };
26
+ export async function executeSyncCommand(context, overrides = {}) {
27
+ const dependencies = { ...syncCommandDependencies, ...overrides };
28
+ const startedAtMs = dependencies.now();
29
+ const startedAtIso = new Date(startedAtMs).toISOString();
30
+ const baseConfig = dependencies.resolveConfig(await dependencies.loadConfig());
31
+ const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
32
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
33
+ const deadlineAtMs = startedAtMs + timeoutMs;
34
+ const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
35
+ const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
36
+ const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
37
+ const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
38
+ timeoutMs: remainingTimeoutMs(),
39
+ });
40
+ const strict = Boolean(context.options.strict);
41
+ const result = await dependencies.withExclusivePowerSync({
42
+ config,
43
+ offline: false,
44
+ timeoutMs: remainingTimeoutMs(),
45
+ deadlineAtMs: operationDeadlineAtMs,
46
+ waitForInitialSync: false,
47
+ expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
48
+ cleanupFailureMode: 'throw',
49
+ }, async ({ db, dbPath, stale }) => {
50
+ const recentSync = strict
51
+ ? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
52
+ : true;
53
+ const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
54
+ const status = dependencies.getStatusSnapshot(db);
55
+ const gate = evaluateSyncGate({
56
+ offline: false,
57
+ firstSyncStale: stale,
58
+ idle: syncPass.downloadIdle,
59
+ indexComplete: syncPass.tokenIndex.complete,
60
+ recentSync,
61
+ status,
62
+ });
63
+ const payload = {
64
+ ok: true,
65
+ dbPath,
66
+ stale: !gate.ok,
67
+ idle: syncPass.idle,
68
+ downloadIdle: syncPass.downloadIdle,
69
+ uploadIdle: syncPass.uploadIdle,
70
+ strict,
71
+ gate,
72
+ uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
73
+ tokenIndex: syncPass.tokenIndex,
74
+ status,
75
+ };
76
+ if (strict && !gate.ok) {
77
+ throw createSyncRequiredError(gate, status);
78
+ }
79
+ return payload;
80
+ });
81
+ const payload = {
82
+ ...result,
83
+ elapsedMs: Math.max(0, dependencies.now() - startedAtMs),
84
+ };
85
+ if (structuredOutput) {
86
+ dependencies.printSuccess(payload);
87
+ return;
88
+ }
89
+ dependencies.printHumanReadableLine(`db: ${payload.dbPath}`);
90
+ if (payload.status.lastSyncedAt) {
91
+ dependencies.printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
92
+ }
93
+ if (payload.stale) {
94
+ dependencies.printHumanReadableLine('warning: sync freshness or local index could not be fully verified');
95
+ }
96
+ if (!payload.downloadIdle) {
97
+ dependencies.printHumanReadableLine('warning: download flow did not settle before timeout');
98
+ }
99
+ if (!payload.uploadIdle) {
100
+ dependencies.printHumanReadableLine('warning: uploads are still active; downloaded data may already be current');
101
+ }
102
+ if (!payload.tokenIndex.complete) {
103
+ dependencies.printHumanReadableLine('warning: local token index did not finish before timeout');
104
+ }
105
+ dependencies.printHumanReadableLine(`tokenIndex: complete=${payload.tokenIndex.complete} scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
106
+ }
11
107
  export const syncCommandDescriptor = {
12
108
  name: 'sync',
13
109
  description: 'Connect and synchronize the local PowerSync database',
14
110
  options: [
15
- { flags: '--strict', description: 'Fail unless first sync completed, data flow is idle, and no download error exists', type: 'boolean' },
111
+ {
112
+ flags: '--strict',
113
+ description: 'Fail unless connected, a current checkpoint completes, downloads settle, and the local index finishes',
114
+ type: 'boolean',
115
+ },
16
116
  ],
17
117
  schema: {
18
118
  path: 'sync',
@@ -22,66 +122,11 @@ export const syncCommandDescriptor = {
22
122
  output: {
23
123
  type: 'structured',
24
124
  summary: 'Sync result with connection status and token index counters',
25
- fields: ['dbPath', 'stale', 'idle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status'],
125
+ fields: ['dbPath', 'stale', 'idle', 'downloadIdle', 'uploadIdle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status', 'elapsedMs'],
26
126
  },
27
- examples: ['dino sync --format json', 'dino sync --strict --sync-timeout 600000 --format json'],
28
- },
29
- action: async (context) => {
30
- const baseConfig = resolveConfig(await loadConfig());
31
- const structuredOutput = getRequestedStructuredOutputFormat() !== null;
32
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
33
- const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs });
34
- const { db, dbPath, stale } = await connectPowerSync({
35
- config,
36
- offline: false,
37
- timeoutMs,
38
- });
39
- try {
40
- const syncPass = await runSingleSyncPass(db, timeoutMs);
41
- const status = getStatusSnapshot(db);
42
- const gate = evaluateSyncGate({
43
- offline: false,
44
- firstSyncStale: stale,
45
- idle: syncPass.idle,
46
- status,
47
- });
48
- const payload = {
49
- ok: true,
50
- dbPath,
51
- stale: !gate.ok,
52
- idle: syncPass.idle,
53
- strict: Boolean(context.options.strict),
54
- gate,
55
- uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
56
- tokenIndex: syncPass.tokenIndex,
57
- status,
58
- };
59
- if (context.options.strict && !gate.ok) {
60
- throw createSyncRequiredError(gate, status);
61
- }
62
- if (structuredOutput) {
63
- printSuccess(payload);
64
- return;
65
- }
66
- printHumanReadableLine(`db: ${payload.dbPath}`);
67
- if (!payload.uploadEnabled) {
68
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset)');
69
- }
70
- if (payload.status.lastSyncedAt) {
71
- printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
72
- }
73
- if (payload.stale) {
74
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
75
- }
76
- if (!payload.idle) {
77
- printHumanReadableLine('warning: data flow did not become idle before timeout');
78
- }
79
- printHumanReadableLine(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
80
- }
81
- finally {
82
- await db.close().catch(() => undefined);
83
- }
127
+ examples: ['dino sync --sync-timeout 20000 --format json', 'dino sync --strict --sync-timeout 20000 --format json'],
84
128
  },
129
+ action: executeSyncCommand,
85
130
  };
86
131
  export function registerSyncCommand(program) {
87
132
  registerStandaloneCommand(program, syncCommandDescriptor);
@@ -1,4 +1,5 @@
1
1
  import { canonicalizeTagPath, isTagPathWithinPrefix, renameTagPathWithPrefix, } from './model.js';
2
+ import { findMarkdownLiteralRanges, isMarkdownOffsetInSortedRanges, } from '../../utils/tiptapMarkdownSyntax.js';
2
3
  function dedupeTags(values) {
3
4
  const result = [];
4
5
  const seen = new Set();
@@ -191,7 +192,15 @@ function rewriteHashtagsInMarkdown(raw, oldPrefix, newPrefix, preserveSuffix) {
191
192
  }
192
193
  let output = '';
193
194
  let changed = false;
195
+ const protectedRanges = findMarkdownLiteralRanges(raw);
194
196
  for (let index = 0; index < raw.length;) {
197
+ if (isMarkdownOffsetInSortedRanges(index, protectedRanges)) {
198
+ const range = protectedRanges.find((candidate) => index >= candidate.start && index < candidate.end);
199
+ const end = range?.end ?? index + 1;
200
+ output += raw.slice(index, end);
201
+ index = end;
202
+ continue;
203
+ }
195
204
  if (raw.charCodeAt(index) !== 0x23) {
196
205
  output += raw[index];
197
206
  index += 1;
@@ -5,7 +5,7 @@ import { buildTagAddDryRunPreview, buildTagMutationDryRunPreview } from '../writ
5
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
6
6
  import { DinoxError } from '../../utils/errors.js';
7
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
8
- import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
8
+ import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
9
9
  import { addTag, inspectAddTag, listTags } from './repo.js';
10
10
  import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
11
11
  const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -75,7 +75,6 @@ async function runTagAdd(context) {
75
75
  userId: identity.userId,
76
76
  emoji,
77
77
  });
78
- printUploadDisabledWarning(runtime);
79
78
  await waitForMutationUpload(runtime);
80
79
  const payload = { ok: true, stale: commandStale(runtime), ...result };
81
80
  if (structuredOutput) {
@@ -118,7 +117,6 @@ async function runTagRename(context) {
118
117
  }));
119
118
  return;
120
119
  }
121
- printUploadDisabledWarning(runtime);
122
120
  await waitForMutationUpload(runtime);
123
121
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
124
122
  });
@@ -144,7 +142,6 @@ async function runTagMove(context) {
144
142
  }));
145
143
  return;
146
144
  }
147
- printUploadDisabledWarning(runtime);
148
145
  await waitForMutationUpload(runtime);
149
146
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
150
147
  });
@@ -170,7 +167,6 @@ async function runTagMerge(context) {
170
167
  }));
171
168
  return;
172
169
  }
173
- printUploadDisabledWarning(runtime);
174
170
  await waitForMutationUpload(runtime);
175
171
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
176
172
  });
@@ -193,7 +189,7 @@ async function runTagCleanup(context) {
193
189
  offline: runtime.offline,
194
190
  command: 'dino tag cleanup',
195
191
  action: 'cleanup_tags',
196
- risk: 'write',
192
+ risk: 'read',
197
193
  summary: `Inspect tag cleanup candidates (${result.issues.length} issues).`,
198
194
  inputs: {},
199
195
  targets: result.issues,
@@ -2,14 +2,15 @@ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
2
2
  import { DinoxError } from '../../utils/errors.js';
3
3
  import { nowIsoUtc } from '../../utils/time.js';
4
4
  import { mergeTagReferencesInNote, renameTagReferencesInNote } from './content.js';
5
- import { MAX_TAG_PATH_LENGTH, MAX_TAG_SEGMENT_LENGTH, REQUIRED_NOTE_TAG_COLUMNS, REQUIRED_TAG_NODE_COLUMNS, assertMaxLength, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, isTagPathWithinPrefix, normalizeTagLookupKey, parseStoredTags, parentPathOf, renameTagPathWithPrefix, trimString, } from './model.js';
5
+ import { MAX_TAG_SEGMENT_LENGTH, REQUIRED_NOTE_TAG_COLUMNS, REQUIRED_TAG_NODE_COLUMNS, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, isTagPathWithinPrefix, normalizeTagLookupKey, parseStoredTags, parentPathOf, renameTagPathWithPrefix, trimString, } from './model.js';
6
6
  import { activeTagNodes, actualActiveTagNodes, assertCanWriteTag, assertRequiredColumns, assertWritableTagNodes, descendantsOf, ensureNoTagPathConflict, getTableColumns, loadTagNodes, parentIdForPath, requireActiveTagPath, toTagListItem, } from './store.js';
7
+ import { assertTagPathLimits, assertTagTextLimits } from './repo.js';
7
8
  function parseSingleSegment(value, label) {
8
9
  const normalized = canonicalizeTagPath(value);
9
10
  if (!normalized || normalized.includes('/')) {
10
11
  throw new DinoxError(`${label} must be a single path segment`);
11
12
  }
12
- assertMaxLength(label, normalized, MAX_TAG_SEGMENT_LENGTH);
13
+ assertTagTextLimits(label, normalized, MAX_TAG_SEGMENT_LENGTH);
13
14
  return normalized;
14
15
  }
15
16
  function assertPath(value, label) {
@@ -17,16 +18,20 @@ function assertPath(value, label) {
17
18
  if (!normalized) {
18
19
  throw new DinoxError(`${label} is required`);
19
20
  }
20
- assertMaxLength(label, normalized, MAX_TAG_PATH_LENGTH);
21
+ assertTagPathLimits(normalized, label);
21
22
  return normalized;
22
23
  }
23
24
  function buildSubtreeChanges(nodes, root, newRootPath) {
24
25
  const targetPath = assertPath(newRootPath, 'Target tag path');
25
- return descendantsOf(nodes, root).map((node) => ({
26
- id: node.id,
27
- fromPath: node.fullPath,
28
- toPath: renameTagPathWithPrefix(node.fullPath, root.fullPath, targetPath),
29
- }));
26
+ return descendantsOf(nodes, root).map((node) => {
27
+ const toPath = renameTagPathWithPrefix(node.fullPath, root.fullPath, targetPath);
28
+ assertTagPathLimits(toPath, 'Target tag path');
29
+ return {
30
+ id: node.id,
31
+ fromPath: node.fullPath,
32
+ toPath,
33
+ };
34
+ });
30
35
  }
31
36
  function toTreeNode(node) {
32
37
  return {
@@ -75,13 +80,13 @@ async function loadNoteTagRows(db, userId) {
75
80
  FROM c_note
76
81
  WHERE TRIM(COALESCE(id, '')) <> ''
77
82
  AND (is_del IS NULL OR is_del = 0)
78
- AND (TRIM(COALESCE(user_no, '')) = ? OR TRIM(COALESCE(user_no, '')) = '')
83
+ AND TRIM(COALESCE(user_no, '')) = ?
79
84
  `, [userId]);
80
85
  }
81
86
  async function loadNoteTagStats(db, userId) {
82
87
  const normalizedUserId = trimString(userId);
83
88
  const userFilter = normalizedUserId
84
- ? `AND (TRIM(COALESCE(n.user_no, '')) = ? OR TRIM(COALESCE(n.user_no, '')) = '')`
89
+ ? `AND TRIM(COALESCE(n.user_no, '')) = ?`
85
90
  : '';
86
91
  const rows = await db.getAll(`
87
92
  SELECT
@@ -20,6 +20,8 @@ export type InspectAddTagResult = {
20
20
  restoredPaths: string[];
21
21
  emoji: string;
22
22
  };
23
+ export declare function assertTagTextLimits(label: string, value: string, maxLength: number): void;
24
+ export declare function assertTagPathLimits(path: string, label?: string): void;
23
25
  export declare function listTags(db: AbstractPowerSyncDatabase, input?: ListTagsInput): Promise<string[]>;
24
26
  export declare function addTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<AddTagResult>;
25
27
  export declare function inspectAddTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<InspectAddTagResult>;
@@ -14,6 +14,18 @@ function tagNodeMapByPath(nodes) {
14
14
  }
15
15
  return map;
16
16
  }
17
+ export function assertTagTextLimits(label, value, maxLength) {
18
+ assertMaxLength(label, value, maxLength);
19
+ if (Buffer.byteLength(value, 'utf8') > maxLength) {
20
+ throw new DinoxError(`${label} must be at most ${maxLength} UTF-8 bytes`);
21
+ }
22
+ }
23
+ export function assertTagPathLimits(path, label = 'Tag path') {
24
+ assertTagTextLimits(label, path, MAX_TAG_PATH_LENGTH);
25
+ for (const segment of path.split('/')) {
26
+ assertTagTextLimits('Tag path segment', segment, MAX_TAG_SEGMENT_LENGTH);
27
+ }
28
+ }
17
29
  function assertAddTagInput(input) {
18
30
  const userId = trimString(input.userId);
19
31
  const emoji = trimString(input.emoji);
@@ -21,15 +33,15 @@ function assertAddTagInput(input) {
21
33
  if (!tag) {
22
34
  throw new DinoxError('Tag name is required');
23
35
  }
24
- assertMaxLength('Tag path', tag, MAX_TAG_PATH_LENGTH);
25
- assertMaxLength('Tag name', getLeafName(tag), MAX_TAG_SEGMENT_LENGTH);
26
- assertMaxLength('Tag emoji', emoji, MAX_TAG_EMOJI_LENGTH);
36
+ assertTagPathLimits(tag);
37
+ assertTagTextLimits('Tag name', getLeafName(tag), MAX_TAG_SEGMENT_LENGTH);
38
+ assertTagTextLimits('Tag emoji', emoji, MAX_TAG_EMOJI_LENGTH);
27
39
  return { tag, userId, emoji };
28
40
  }
29
41
  export async function listTags(db, input = {}) {
30
42
  const userId = trimString(input.userId);
31
43
  const userFilter = userId
32
- ? `AND (TRIM(COALESCE(user_id, '')) = ? OR TRIM(COALESCE(user_id, '')) = '')`
44
+ ? `AND TRIM(COALESCE(user_id, '')) = ?`
33
45
  : '';
34
46
  const rows = await db.getAll(`
35
47
  SELECT DISTINCT TRIM(COALESCE(full_path, path_key, '')) AS tag
@@ -112,7 +124,8 @@ export async function addTag(db, input) {
112
124
  UPDATE c_tag_node
113
125
  SET ${assignments.join(', ')}
114
126
  WHERE id = ?
115
- `, params);
127
+ AND TRIM(COALESCE(user_id, '')) = ?
128
+ `, [...params, userId]);
116
129
  if (existing.isDel === 1) {
117
130
  restoredNodes += 1;
118
131
  existing.isDel = 0;
@@ -20,7 +20,7 @@ export function assertCanWriteTag(columnNames, userId, action) {
20
20
  export async function loadTagNodes(db, columnNames, options = {}) {
21
21
  const userId = trimString(options.userId);
22
22
  const userFilter = userId
23
- ? `AND (TRIM(COALESCE(user_id, '')) = ? OR TRIM(COALESCE(user_id, '')) = '')`
23
+ ? `AND TRIM(COALESCE(user_id, '')) = ?`
24
24
  : '';
25
25
  const rows = await db.getAll(`
26
26
  SELECT
@@ -14,17 +14,18 @@ export type ApplyTaskCheckedStatusResult = {
14
14
  nextDoc: JsonRecord;
15
15
  taskOrder: string[];
16
16
  matchedIndices: number[];
17
- changedCount: number;
17
+ matchedCheckedStates: boolean[];
18
+ statusChangedCount: number;
18
19
  };
19
20
  export declare function parseTiptapDocFromDb(rawContentJson: string | null): JsonRecord;
20
21
  export declare function createTodoDocFromTasks(taskTexts: string[]): JsonRecord;
21
22
  export declare function appendTasksToDoc(doc: JsonRecord, taskTexts: string[]): JsonRecord;
22
- export declare function deriveImageDetailFromContentJson(doc: JsonRecord): TodoTaskItemNode[];
23
+ export declare function deriveImageDetailFromContentJson(doc: JsonRecord, noteId: string): TodoTaskItemNode[];
23
24
  export declare function buildAppendMarkdown(existingMarkdown: string | null, taskTexts: string[]): string;
24
25
  export declare function buildCreateMarkdown(taskTexts: string[]): string;
25
26
  export declare function buildContentTextPreview(contentMd: string): string;
26
27
  export declare function buildSummaryTitle(taskTexts: string[]): string;
27
28
  export declare function normalizeTaskTexts(values: string[]): string[];
28
- export declare function applyTaskCheckedStatus(doc: JsonRecord, taskId: string, checked: boolean): ApplyTaskCheckedStatusResult;
29
+ export declare function applyTaskCheckedStatus(doc: JsonRecord, noteId: string, taskId: string, checked: boolean): ApplyTaskCheckedStatusResult;
29
30
  export declare function patchTaskMarkdownByOrder(contentMd: string | null, taskOrder: string[], matchedIndices: number[], checked: boolean): string;
30
31
  export {};
@@ -1,6 +1,8 @@
1
1
  import { DinoxError } from '../../utils/errors.js';
2
2
  import { generateId } from '../../utils/id.js';
3
3
  import { stripMarkdown } from '../../utils/text.js';
4
+ import { escapeLiteralMarkdownText, findMarkdownCodeRanges, isMarkdownOffsetInSortedRanges, } from '../../utils/tiptapMarkdownSyntax.js';
5
+ import { buildLegacyTaskId } from './taskIdentity.js';
4
6
  const CONTENT_TEXT_PREVIEW_LIMIT = 300;
5
7
  const DEFAULT_TITLE_PREFIX = '待办清单';
6
8
  export function parseTiptapDocFromDb(rawContentJson) {
@@ -46,10 +48,15 @@ export function appendTasksToDoc(doc, taskTexts) {
46
48
  });
47
49
  return nextDoc;
48
50
  }
49
- export function deriveImageDetailFromContentJson(doc) {
51
+ export function deriveImageDetailFromContentJson(doc, noteId) {
50
52
  const root = assertTiptapDoc(doc);
53
+ const normalizedNoteId = noteId.trim();
54
+ if (!normalizedNoteId) {
55
+ throw new DinoxError('Note id is required to derive todo task details');
56
+ }
51
57
  const rootContent = ensureNodeContentArray(root);
52
58
  const result = [];
59
+ const preorderState = { nextIndex: 0 };
53
60
  const nodeStack = [];
54
61
  const insideTaskStack = [];
55
62
  for (let index = rootContent.length - 1; index >= 0; index -= 1) {
@@ -66,7 +73,7 @@ export function deriveImageDetailFromContentJson(doc) {
66
73
  const type = typeof node.type === 'string' ? node.type : '';
67
74
  const content = Array.isArray(node.content) ? node.content : [];
68
75
  if (type === 'taskItem' && !insideTaskItem) {
69
- result.push(normalizeTaskItemNode(node));
76
+ result.push(normalizeTaskItemNode(node, normalizedNoteId, preorderState));
70
77
  }
71
78
  const nextInsideTaskItem = insideTaskItem || type === 'taskItem';
72
79
  for (let index = content.length - 1; index >= 0; index -= 1) {
@@ -77,9 +84,9 @@ export function deriveImageDetailFromContentJson(doc) {
77
84
  return result;
78
85
  }
79
86
  export function buildAppendMarkdown(existingMarkdown, taskTexts) {
80
- const trimmed = (existingMarkdown ?? '').trimEnd();
87
+ const trimmed = (existingMarkdown ?? '').replace(/(?:\r\n|\n|\r)+$/, '');
81
88
  const taskBlock = buildTaskMarkdown(taskTexts);
82
- if (!trimmed) {
89
+ if (!trimmed.trim()) {
83
90
  return taskBlock;
84
91
  }
85
92
  return `${trimmed}\n\n${taskBlock}`;
@@ -111,14 +118,23 @@ export function buildSummaryTitle(taskTexts) {
111
118
  }
112
119
  export function normalizeTaskTexts(values) {
113
120
  const normalized = values
114
- .map((value) => value.replace(/\s+/g, ' ').trim())
121
+ .map((value) => value
122
+ .replace(/\r\n?/g, '\n')
123
+ .split('\n')
124
+ .map((line) => line.trim())
125
+ .filter(Boolean)
126
+ .join(' '))
115
127
  .filter((value) => value.length > 0);
116
128
  if (normalized.length === 0) {
117
129
  throw new DinoxError('At least one non-empty task is required');
118
130
  }
119
131
  return normalized;
120
132
  }
121
- export function applyTaskCheckedStatus(doc, taskId, checked) {
133
+ export function applyTaskCheckedStatus(doc, noteId, taskId, checked) {
134
+ const normalizedNoteId = noteId.trim();
135
+ if (!normalizedNoteId) {
136
+ throw new DinoxError('Note id is required');
137
+ }
122
138
  const normalizedTaskId = taskId.trim();
123
139
  if (!normalizedTaskId) {
124
140
  throw new DinoxError('taskId is required');
@@ -126,7 +142,9 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
126
142
  const nextDoc = cloneJson(assertTiptapDoc(doc));
127
143
  const taskOrder = [];
128
144
  const matchedIndices = [];
129
- let changedCount = 0;
145
+ const matchedCheckedStates = [];
146
+ let statusChangedCount = 0;
147
+ let preorderIndex = 0;
130
148
  const visitNode = (value) => {
131
149
  if (!value || typeof value !== 'object') {
132
150
  return;
@@ -134,22 +152,29 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
134
152
  const node = value;
135
153
  const type = typeof node.type === 'string' ? node.type : '';
136
154
  if (type === 'taskItem') {
137
- const currentTaskId = resolveTaskId(node);
138
- if (currentTaskId) {
139
- const currentIndex = taskOrder.length;
140
- taskOrder.push(currentTaskId);
141
- if (currentTaskId === normalizedTaskId) {
142
- matchedIndices.push(currentIndex);
143
- const attrs = ensureNodeAttrs(node);
144
- const previousChecked = normalizeChecked(node.checked) ?? normalizeChecked(attrs.checked) ?? false;
145
- if (previousChecked !== checked) {
146
- changedCount += 1;
147
- }
148
- node.id = currentTaskId;
149
- node.checked = checked;
150
- attrs.id = currentTaskId;
151
- attrs.checked = checked;
155
+ const explicitTaskId = resolveTaskId(node);
156
+ const currentTaskId = explicitTaskId || buildLegacyTaskId(normalizedNoteId, preorderIndex, node);
157
+ preorderIndex += 1;
158
+ const currentIndex = taskOrder.length;
159
+ taskOrder.push(currentTaskId);
160
+ let attrs = null;
161
+ if (!explicitTaskId) {
162
+ attrs = ensureNodeAttrs(node);
163
+ node.id = currentTaskId;
164
+ attrs.id = currentTaskId;
165
+ }
166
+ if (currentTaskId === normalizedTaskId) {
167
+ matchedIndices.push(currentIndex);
168
+ attrs ??= ensureNodeAttrs(node);
169
+ const previousChecked = normalizeChecked(node.checked) ?? normalizeChecked(attrs.checked) ?? false;
170
+ matchedCheckedStates.push(previousChecked);
171
+ if (previousChecked !== checked) {
172
+ statusChangedCount += 1;
152
173
  }
174
+ node.id = currentTaskId;
175
+ node.checked = checked;
176
+ attrs.id = currentTaskId;
177
+ attrs.checked = checked;
153
178
  }
154
179
  }
155
180
  const content = Array.isArray(node.content) ? node.content : [];
@@ -162,7 +187,8 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
162
187
  nextDoc,
163
188
  taskOrder,
164
189
  matchedIndices,
165
- changedCount,
190
+ matchedCheckedStates,
191
+ statusChangedCount,
166
192
  };
167
193
  }
168
194
  export function patchTaskMarkdownByOrder(contentMd, taskOrder, matchedIndices, checked) {
@@ -174,11 +200,15 @@ export function patchTaskMarkdownByOrder(contentMd, taskOrder, matchedIndices, c
174
200
  throw new DinoxError('Cannot update task markdown: note has task entries in content_json but empty content_md.');
175
201
  }
176
202
  const lines = markdown.split('\n');
203
+ const codeRanges = findMarkdownCodeRanges(markdown);
177
204
  const checklistLineIndexes = [];
205
+ let lineOffset = 0;
178
206
  for (let index = 0; index < lines.length; index += 1) {
179
- if (TASK_MARKDOWN_LINE_RE.test(lines[index] ?? '')) {
207
+ const line = lines[index] ?? '';
208
+ if (!isMarkdownOffsetInSortedRanges(lineOffset, codeRanges) && TASK_MARKDOWN_LINE_RE.test(line)) {
180
209
  checklistLineIndexes.push(index);
181
210
  }
211
+ lineOffset += line.length + (index < lines.length - 1 ? 1 : 0);
182
212
  }
183
213
  if (checklistLineIndexes.length !== taskOrder.length) {
184
214
  throw new DinoxError(`Cannot update task markdown: checklist line count (${checklistLineIndexes.length}) does not match content_json task count (${taskOrder.length}).`);
@@ -220,7 +250,7 @@ function buildTaskItemNodes(taskTexts) {
220
250
  }
221
251
  function buildTaskMarkdown(taskTexts) {
222
252
  const normalizedTasks = normalizeTaskTexts(taskTexts);
223
- return normalizedTasks.map((task) => `- [ ] ${task}`).join('\n');
253
+ return normalizedTasks.map((task) => `- [ ] ${escapeLiteralMarkdownText(task)}`).join('\n');
224
254
  }
225
255
  function assertTiptapDoc(value) {
226
256
  if (!value || typeof value !== 'object') {
@@ -269,11 +299,13 @@ function resolveTaskId(node) {
269
299
  const attrsId = normalizeString(attrs?.id);
270
300
  return attrsId;
271
301
  }
272
- function normalizeTaskItemNode(node) {
302
+ function normalizeTaskItemNode(node, noteId, preorderState) {
273
303
  const attrsSource = isObject(node.attrs) ? node.attrs : {};
274
304
  const nodeId = normalizeString(node.id);
275
305
  const attrsId = normalizeString(attrsSource.id);
276
- const taskId = nodeId || attrsId || generateId();
306
+ const preorderIndex = preorderState.nextIndex;
307
+ preorderState.nextIndex += 1;
308
+ const taskId = nodeId || attrsId || buildLegacyTaskId(noteId, preorderIndex, node);
277
309
  const checked = normalizeChecked(node.checked) ?? normalizeChecked(attrsSource.checked) ?? false;
278
310
  return {
279
311
  type: 'taskItem',
@@ -284,9 +316,27 @@ function normalizeTaskItemNode(node) {
284
316
  id: taskId,
285
317
  checked,
286
318
  },
287
- content: Array.isArray(node.content) ? cloneJson(node.content) : [],
319
+ content: Array.isArray(node.content)
320
+ ? node.content.map((child) => normalizeTaskDetailContent(child, noteId, preorderState))
321
+ : [],
288
322
  };
289
323
  }
324
+ function normalizeTaskDetailContent(value, noteId, preorderState) {
325
+ if (Array.isArray(value)) {
326
+ return value.map((item) => normalizeTaskDetailContent(item, noteId, preorderState));
327
+ }
328
+ if (!isObject(value)) {
329
+ return value;
330
+ }
331
+ if (value.type === 'taskItem') {
332
+ return normalizeTaskItemNode(value, noteId, preorderState);
333
+ }
334
+ const normalized = {};
335
+ for (const [key, child] of Object.entries(value)) {
336
+ normalized[key] = normalizeTaskDetailContent(child, noteId, preorderState);
337
+ }
338
+ return normalized;
339
+ }
290
340
  function normalizeChecked(value) {
291
341
  if (value === true)
292
342
  return true;
@@ -19,5 +19,6 @@ export declare function buildTodoUpdateDryRunPreview(args: {
19
19
  config: ResolvedDinoxConfig;
20
20
  offline: boolean;
21
21
  taskId: string;
22
+ noteId?: string;
22
23
  checked: boolean;
23
24
  }): Promise<DryRunPreview>;