@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
|
@@ -4,9 +4,71 @@ import { generateId } from '../../utils/id.js';
|
|
|
4
4
|
import { nowIsoUtc } from '../../utils/time.js';
|
|
5
5
|
import { applyTaskCheckedStatus, appendTasksToDoc, buildAppendMarkdown, buildContentTextPreview, buildCreateMarkdown, buildSummaryTitle, createTodoDocFromTasks, deriveImageDetailFromContentJson, normalizeTaskTexts, patchTaskMarkdownByOrder, parseTiptapDocFromDb, } from './contentModel.js';
|
|
6
6
|
import { buildTaskKey, extractFlattenedTasks, filterTasksForTodoList, } from './taskModel.js';
|
|
7
|
+
import { isLegacyTaskId } from './taskIdentity.js';
|
|
7
8
|
export { normalizeTaskTexts } from './contentModel.js';
|
|
9
|
+
const TODO_TASK_CANDIDATE_SCAN_LIMIT = 500;
|
|
10
|
+
export function validateTodoAppendTarget(target) {
|
|
11
|
+
if (typeof target.is_del === 'number' && target.is_del !== 0) {
|
|
12
|
+
throw new DinoxError(`Note has been deleted: ${target.id}`);
|
|
13
|
+
}
|
|
14
|
+
const noteId = target.id?.trim() ?? '';
|
|
15
|
+
if (!noteId) {
|
|
16
|
+
throw new DinoxError('Note id is required');
|
|
17
|
+
}
|
|
18
|
+
const currentDoc = parseTiptapDocFromDb(target.content_json);
|
|
19
|
+
const hasMarkdownSnapshot = Boolean(target.content_md?.trim());
|
|
20
|
+
const hasDocContent = Array.isArray(currentDoc.content) && currentDoc.content.length > 0;
|
|
21
|
+
if (!hasMarkdownSnapshot && hasDocContent) {
|
|
22
|
+
throw new DinoxError('Cannot append todo: current note has non-empty content_json but empty content_md. Please repair note markdown before append.');
|
|
23
|
+
}
|
|
24
|
+
return { currentDoc, noteId, expectedVersion: normalizeVersion(target.version) };
|
|
25
|
+
}
|
|
26
|
+
function normalizeVersion(value) {
|
|
27
|
+
const parsed = Number(value ?? 0);
|
|
28
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
29
|
+
}
|
|
30
|
+
function assertSingleUpdatedRow(result, message) {
|
|
31
|
+
if (result.rows?.length !== 1) {
|
|
32
|
+
throw new DinoxError(message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Serialize parsed todo-search options into a JSON-safe wire payload. */
|
|
36
|
+
export function toTodoSearchWire(query, options) {
|
|
37
|
+
return {
|
|
38
|
+
query,
|
|
39
|
+
includeDeleted: options.includeDeleted,
|
|
40
|
+
taskStatus: options.taskStatus,
|
|
41
|
+
taskTagFilter: options.taskTagFilter,
|
|
42
|
+
limit: options.limit,
|
|
43
|
+
scanLimit: options.scanLimit,
|
|
44
|
+
startDate: options.taskDateRange?.startDate?.toISOString(),
|
|
45
|
+
endDate: options.taskDateRange?.endDate?.toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run a todo search from a wire payload. Shared by the local command path and the daemon
|
|
50
|
+
* RPC handler so both produce identical results from one implementation.
|
|
51
|
+
*/
|
|
52
|
+
export async function executeTodoSearch(db, wire) {
|
|
53
|
+
const taskDateRange = wire.startDate || wire.endDate
|
|
54
|
+
? {
|
|
55
|
+
startDate: wire.startDate ? new Date(wire.startDate) : undefined,
|
|
56
|
+
endDate: wire.endDate ? new Date(wire.endDate) : undefined,
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
return searchTodoTasks(db, wire.query, {
|
|
60
|
+
includeDeleted: wire.includeDeleted,
|
|
61
|
+
taskStatus: wire.taskStatus,
|
|
62
|
+
taskTagFilter: wire.taskTagFilter,
|
|
63
|
+
taskDateRange,
|
|
64
|
+
limit: wire.limit,
|
|
65
|
+
scanLimit: wire.scanLimit,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
8
68
|
export async function searchTodoTasks(db, query, options) {
|
|
9
|
-
const
|
|
69
|
+
const scannedRows = await loadTodoNoteRows(db, options.includeDeleted, options.scanLimit + 1);
|
|
70
|
+
const scanTruncated = scannedRows.length > options.scanLimit;
|
|
71
|
+
const noteRows = scannedRows.slice(0, options.scanLimit);
|
|
10
72
|
const allTasks = extractFlattenedTasks(noteRows);
|
|
11
73
|
const filteredTasks = filterTasksForTodoList(allTasks, query, options.taskStatus, options.taskTagFilter, options.taskDateRange);
|
|
12
74
|
const visibleTasks = filteredTasks.slice(0, options.limit);
|
|
@@ -32,10 +94,13 @@ export async function searchTodoTasks(db, query, options) {
|
|
|
32
94
|
query: query.trim(),
|
|
33
95
|
returned_count: tasks.length,
|
|
34
96
|
total_count: filteredTasks.length,
|
|
35
|
-
matched_note_count: new Set(
|
|
97
|
+
matched_note_count: new Set(filteredTasks.map((task) => task.noteId)).size,
|
|
36
98
|
limit: options.limit,
|
|
37
99
|
scan_limit: options.scanLimit,
|
|
38
|
-
|
|
100
|
+
scanned_note_count: noteRows.length,
|
|
101
|
+
scan_truncated: scanTruncated,
|
|
102
|
+
total_count_is_lower_bound: scanTruncated,
|
|
103
|
+
truncated: scanTruncated || filteredTasks.length > tasks.length,
|
|
39
104
|
applied_filters: {
|
|
40
105
|
status: options.taskStatus,
|
|
41
106
|
tags: [...options.taskTagFilter],
|
|
@@ -59,24 +124,13 @@ export async function appendTodoTasks(db, options) {
|
|
|
59
124
|
}
|
|
60
125
|
throw new DinoxError('No eligible note found for append. Try `dino todo create ...` first.');
|
|
61
126
|
}
|
|
62
|
-
|
|
63
|
-
throw new DinoxError(`Note has been deleted: ${target.id}`);
|
|
64
|
-
}
|
|
65
|
-
if (!target.id?.trim()) {
|
|
66
|
-
throw new DinoxError('Note id is required');
|
|
67
|
-
}
|
|
68
|
-
const currentDoc = parseTiptapDocFromDb(target.content_json);
|
|
69
|
-
const hasMarkdownSnapshot = Boolean(target.content_md?.trim());
|
|
70
|
-
const hasDocContent = Array.isArray(currentDoc.content) && currentDoc.content.length > 0;
|
|
71
|
-
if (!hasMarkdownSnapshot && hasDocContent) {
|
|
72
|
-
throw new DinoxError('Cannot append todo: current note has non-empty content_json but empty content_md. Please repair note markdown before append.');
|
|
73
|
-
}
|
|
127
|
+
const { currentDoc, noteId, expectedVersion } = validateTodoAppendTarget(target);
|
|
74
128
|
const nextDoc = appendTasksToDoc(currentDoc, taskTexts);
|
|
75
|
-
const imageDetail = deriveImageDetailFromContentJson(nextDoc);
|
|
129
|
+
const imageDetail = deriveImageDetailFromContentJson(nextDoc, noteId);
|
|
76
130
|
const contentMd = buildAppendMarkdown(target.content_md, taskTexts);
|
|
77
131
|
const contentText = buildContentTextPreview(contentMd);
|
|
78
132
|
const now = nowIsoUtc();
|
|
79
|
-
await db.execute(`
|
|
133
|
+
const updateResult = await db.execute(`
|
|
80
134
|
UPDATE c_note
|
|
81
135
|
SET
|
|
82
136
|
content_json = ?,
|
|
@@ -87,16 +141,20 @@ export async function appendTodoTasks(db, options) {
|
|
|
87
141
|
version = COALESCE(version, 0) + 1
|
|
88
142
|
WHERE id = ?
|
|
89
143
|
AND (is_del IS NULL OR is_del = 0)
|
|
144
|
+
AND COALESCE(version, 0) = ?
|
|
145
|
+
RETURNING id
|
|
90
146
|
`, [
|
|
91
147
|
JSON.stringify(nextDoc),
|
|
92
148
|
JSON.stringify(imageDetail),
|
|
93
149
|
contentMd,
|
|
94
150
|
contentText,
|
|
95
151
|
now,
|
|
96
|
-
|
|
152
|
+
noteId,
|
|
153
|
+
expectedVersion,
|
|
97
154
|
]);
|
|
155
|
+
assertSingleUpdatedRow(updateResult, `Note changed concurrently: ${noteId}. Re-run the todo append command.`);
|
|
98
156
|
return {
|
|
99
|
-
note_id:
|
|
157
|
+
note_id: noteId,
|
|
100
158
|
title: (target.title ?? '').trim(),
|
|
101
159
|
appended_count: taskTexts.length,
|
|
102
160
|
source,
|
|
@@ -105,12 +163,13 @@ export async function appendTodoTasks(db, options) {
|
|
|
105
163
|
export async function createTodoNote(db, options) {
|
|
106
164
|
const taskTexts = normalizeTaskTexts(options.taskTexts);
|
|
107
165
|
const title = options.title?.trim() || buildSummaryTitle(taskTexts);
|
|
166
|
+
const noteId = generateId();
|
|
108
167
|
const contentJson = createTodoDocFromTasks(taskTexts);
|
|
109
|
-
const imageDetail = deriveImageDetailFromContentJson(contentJson);
|
|
168
|
+
const imageDetail = deriveImageDetailFromContentJson(contentJson, noteId);
|
|
110
169
|
const contentMd = buildCreateMarkdown(taskTexts);
|
|
111
170
|
const contentText = buildContentTextPreview(contentMd);
|
|
112
171
|
const created = await createNote(db, {
|
|
113
|
-
id:
|
|
172
|
+
id: noteId,
|
|
114
173
|
title,
|
|
115
174
|
noteType: 'note',
|
|
116
175
|
contentMd,
|
|
@@ -132,13 +191,17 @@ export async function updateTodoTaskStatus(db, options) {
|
|
|
132
191
|
if (!taskId) {
|
|
133
192
|
throw new DinoxError('taskId is required');
|
|
134
193
|
}
|
|
135
|
-
const candidates = await loadTodoTaskCandidatesByTaskId(db, taskId);
|
|
194
|
+
const candidates = await loadTodoTaskCandidatesByTaskId(db, taskId, { noteId: options.noteId });
|
|
136
195
|
const matches = [];
|
|
137
196
|
const skippedCandidates = [];
|
|
138
197
|
for (const candidate of candidates) {
|
|
139
198
|
try {
|
|
199
|
+
const noteId = (candidate.id ?? '').trim();
|
|
200
|
+
if (!noteId) {
|
|
201
|
+
throw new DinoxError('Note id is required');
|
|
202
|
+
}
|
|
140
203
|
const doc = parseTiptapDocFromDb(candidate.content_json);
|
|
141
|
-
const applied = applyTaskCheckedStatus(doc, taskId, options.checked);
|
|
204
|
+
const applied = applyTaskCheckedStatus(doc, noteId, taskId, options.checked);
|
|
142
205
|
if (applied.matchedIndices.length === 0) {
|
|
143
206
|
continue;
|
|
144
207
|
}
|
|
@@ -190,10 +253,10 @@ export async function updateTodoTaskStatus(db, options) {
|
|
|
190
253
|
}
|
|
191
254
|
const nextContentMd = patchTaskMarkdownByOrder(match.candidate.content_md, match.applied.taskOrder, match.applied.matchedIndices, options.checked);
|
|
192
255
|
const nextContentText = buildContentTextPreview(nextContentMd);
|
|
193
|
-
const nextImageDetail = deriveImageDetailFromContentJson(match.applied.nextDoc);
|
|
256
|
+
const nextImageDetail = deriveImageDetailFromContentJson(match.applied.nextDoc, noteId);
|
|
194
257
|
const hasMarkdownChanged = nextContentMd !== (match.candidate.content_md ?? '');
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
258
|
+
const hasStatusChanged = match.applied.statusChangedCount > 0;
|
|
259
|
+
if (!hasStatusChanged && !hasMarkdownChanged) {
|
|
197
260
|
return {
|
|
198
261
|
note_id: noteId,
|
|
199
262
|
task_id: taskId,
|
|
@@ -202,7 +265,7 @@ export async function updateTodoTaskStatus(db, options) {
|
|
|
202
265
|
title: (match.candidate.title ?? '').trim(),
|
|
203
266
|
};
|
|
204
267
|
}
|
|
205
|
-
await db.execute(`
|
|
268
|
+
const updateResult = await db.execute(`
|
|
206
269
|
UPDATE c_note
|
|
207
270
|
SET
|
|
208
271
|
content_json = ?,
|
|
@@ -213,6 +276,8 @@ export async function updateTodoTaskStatus(db, options) {
|
|
|
213
276
|
version = COALESCE(version, 0) + 1
|
|
214
277
|
WHERE id = ?
|
|
215
278
|
AND (is_del IS NULL OR is_del = 0)
|
|
279
|
+
AND COALESCE(version, 0) = ?
|
|
280
|
+
RETURNING id
|
|
216
281
|
`, [
|
|
217
282
|
JSON.stringify(match.applied.nextDoc),
|
|
218
283
|
JSON.stringify(nextImageDetail),
|
|
@@ -220,7 +285,9 @@ export async function updateTodoTaskStatus(db, options) {
|
|
|
220
285
|
nextContentText,
|
|
221
286
|
nowIsoUtc(),
|
|
222
287
|
noteId,
|
|
288
|
+
normalizeVersion(match.candidate.version),
|
|
223
289
|
]);
|
|
290
|
+
assertSingleUpdatedRow(updateResult, `Note changed concurrently: ${noteId}. Re-run the todo update command.`);
|
|
224
291
|
return {
|
|
225
292
|
note_id: noteId,
|
|
226
293
|
task_id: taskId,
|
|
@@ -257,7 +324,8 @@ export async function getTodoAppendTargetById(db, noteId) {
|
|
|
257
324
|
n.title,
|
|
258
325
|
n.content_json,
|
|
259
326
|
n.content_md,
|
|
260
|
-
n.is_del
|
|
327
|
+
n.is_del,
|
|
328
|
+
n.version
|
|
261
329
|
FROM c_note n
|
|
262
330
|
WHERE n.id = ?
|
|
263
331
|
`, [noteId]);
|
|
@@ -269,7 +337,8 @@ export async function getLatestTodoAppendTarget(db) {
|
|
|
269
337
|
n.title,
|
|
270
338
|
n.content_json,
|
|
271
339
|
n.content_md,
|
|
272
|
-
n.is_del
|
|
340
|
+
n.is_del,
|
|
341
|
+
n.version
|
|
273
342
|
FROM c_note n
|
|
274
343
|
WHERE (n.is_del IS NULL OR n.is_del = 0)
|
|
275
344
|
AND n.image_detail IS NOT NULL
|
|
@@ -279,24 +348,64 @@ export async function getLatestTodoAppendTarget(db) {
|
|
|
279
348
|
LIMIT 1
|
|
280
349
|
`, []);
|
|
281
350
|
}
|
|
282
|
-
export async function loadTodoTaskCandidatesByTaskId(db, taskId) {
|
|
283
|
-
const
|
|
284
|
-
|
|
351
|
+
export async function loadTodoTaskCandidatesByTaskId(db, taskId, options = {}) {
|
|
352
|
+
const exactNoteId = options.noteId?.trim();
|
|
353
|
+
if (options.noteId !== undefined && !exactNoteId) {
|
|
354
|
+
throw new DinoxError('--note-id cannot be empty');
|
|
355
|
+
}
|
|
356
|
+
if (exactNoteId) {
|
|
357
|
+
return db.getAll(`
|
|
358
|
+
SELECT
|
|
359
|
+
n.id,
|
|
360
|
+
n.title,
|
|
361
|
+
n.content_json,
|
|
362
|
+
n.content_md,
|
|
363
|
+
n.is_del,
|
|
364
|
+
n.created_at,
|
|
365
|
+
n.version
|
|
366
|
+
FROM c_note n
|
|
367
|
+
WHERE n.id = ?
|
|
368
|
+
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
369
|
+
AND n.content_json IS NOT NULL
|
|
370
|
+
AND LENGTH(TRIM(n.content_json)) > 0
|
|
371
|
+
LIMIT 1
|
|
372
|
+
`, [exactNoteId]);
|
|
373
|
+
}
|
|
374
|
+
const pattern = isLegacyTaskId(taskId)
|
|
375
|
+
? '%taskItem%'
|
|
376
|
+
: `%${escapeLikePattern(taskId)}%`;
|
|
377
|
+
const escapeClause = isLegacyTaskId(taskId) ? '' : " ESCAPE '\\'";
|
|
378
|
+
const candidates = await db.getAll(`
|
|
285
379
|
SELECT
|
|
286
380
|
n.id,
|
|
287
381
|
n.title,
|
|
288
382
|
n.content_json,
|
|
289
383
|
n.content_md,
|
|
290
384
|
n.is_del,
|
|
291
|
-
n.created_at
|
|
385
|
+
n.created_at,
|
|
386
|
+
n.version
|
|
292
387
|
FROM c_note n
|
|
293
388
|
WHERE (n.is_del IS NULL OR n.is_del = 0)
|
|
294
389
|
AND n.content_json IS NOT NULL
|
|
295
390
|
AND LENGTH(TRIM(n.content_json)) > 0
|
|
296
|
-
AND n.content_json LIKE
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
391
|
+
AND n.content_json LIKE ?${escapeClause}
|
|
392
|
+
LIMIT ?
|
|
393
|
+
`, [pattern, TODO_TASK_CANDIDATE_SCAN_LIMIT + 1]);
|
|
394
|
+
if (candidates.length > TODO_TASK_CANDIDATE_SCAN_LIMIT) {
|
|
395
|
+
throw new DinoxError(`Task lookup reached the ${TODO_TASK_CANDIDATE_SCAN_LIMIT}-note safety limit; refusing to update because task id uniqueness could not be verified.`, {
|
|
396
|
+
code: 'TODO_TASK_LOOKUP_TRUNCATED',
|
|
397
|
+
exitCode: 5,
|
|
398
|
+
suggestion: 'Run `dino todo search <task-text> --scan-limit 5000 --format json` to get the task note_id, then retry with `--note-id <note_id>`.',
|
|
399
|
+
details: {
|
|
400
|
+
taskId,
|
|
401
|
+
candidateLimit: TODO_TASK_CANDIDATE_SCAN_LIMIT,
|
|
402
|
+
scannedCandidateCount: candidates.length,
|
|
403
|
+
scanTruncated: true,
|
|
404
|
+
},
|
|
405
|
+
recoverable: true,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return candidates;
|
|
300
409
|
}
|
|
301
410
|
function escapeLikePattern(value) {
|
|
302
411
|
return value.replace(/[\\%_]/g, (token) => `\\${token}`);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const LEGACY_TASK_ID_PREFIX = 'legacy-task-';
|
|
3
|
+
const LEGACY_TASK_ID_RE = /^legacy-task-[a-f0-9]{24}$/;
|
|
4
|
+
function normalizeChecked(value) {
|
|
5
|
+
if (value === true || value === 'true')
|
|
6
|
+
return true;
|
|
7
|
+
if (value === false || value === 'false')
|
|
8
|
+
return false;
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
function canonicalizeTaskIdentityValue(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value
|
|
14
|
+
.map((item) => canonicalizeTaskIdentityValue(item))
|
|
15
|
+
.filter((item) => item !== undefined);
|
|
16
|
+
}
|
|
17
|
+
if (!value || typeof value !== 'object') {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
const record = value;
|
|
21
|
+
if (record.type === 'taskList') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const normalized = {};
|
|
25
|
+
for (const key of Object.keys(record).sort()) {
|
|
26
|
+
if (key === 'id' || key === 'checked') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const next = canonicalizeTaskIdentityValue(record[key]);
|
|
30
|
+
if (next === undefined) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (key === 'attrs'
|
|
34
|
+
&& next
|
|
35
|
+
&& typeof next === 'object'
|
|
36
|
+
&& !Array.isArray(next)
|
|
37
|
+
&& Object.keys(next).length === 0) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
normalized[key] = next;
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
export function buildLegacyTaskId(noteId, preorderIndex, taskNode) {
|
|
45
|
+
const node = taskNode && typeof taskNode === 'object'
|
|
46
|
+
? taskNode
|
|
47
|
+
: {};
|
|
48
|
+
const signature = JSON.stringify({
|
|
49
|
+
checked: normalizeChecked(node.checked) ?? normalizeChecked(node.attrs?.checked) ?? false,
|
|
50
|
+
node: canonicalizeTaskIdentityValue(taskNode),
|
|
51
|
+
});
|
|
52
|
+
const digest = createHash('sha256')
|
|
53
|
+
.update(noteId.trim())
|
|
54
|
+
.update('\0')
|
|
55
|
+
.update(String(preorderIndex))
|
|
56
|
+
.update('\0')
|
|
57
|
+
.update(signature)
|
|
58
|
+
.digest('hex')
|
|
59
|
+
.slice(0, 24);
|
|
60
|
+
return `${LEGACY_TASK_ID_PREFIX}${digest}`;
|
|
61
|
+
}
|
|
62
|
+
export function isLegacyTaskId(value) {
|
|
63
|
+
return LEGACY_TASK_ID_RE.test(value.trim());
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildLegacyTaskId } from './taskIdentity.js';
|
|
1
2
|
const DEFAULT_TASK_TIME_DISPLAY_FORMAT = 'datetime';
|
|
2
3
|
export function buildTaskKey(noteId, taskId) {
|
|
3
4
|
return `${noteId}:${taskId}`;
|
|
@@ -14,6 +15,7 @@ export function extractFlattenedTasks(rows) {
|
|
|
14
15
|
continue;
|
|
15
16
|
}
|
|
16
17
|
const { createdAt, createdAtRaw } = parseCreatedAt(row.created_at, row.updated_at);
|
|
18
|
+
const preorderState = { nextIndex: 0 };
|
|
17
19
|
extracted.push(...extractTasksFromDetailWithHierarchy({
|
|
18
20
|
noteId,
|
|
19
21
|
noteTitle: normalizeString(row.title),
|
|
@@ -21,6 +23,7 @@ export function extractFlattenedTasks(rows) {
|
|
|
21
23
|
createdAtRaw,
|
|
22
24
|
noteTags: parseNoteTags(row.tags),
|
|
23
25
|
taskItems,
|
|
26
|
+
preorderState,
|
|
24
27
|
}));
|
|
25
28
|
}
|
|
26
29
|
extracted.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime());
|
|
@@ -233,16 +236,15 @@ function extractNestedTasks(content) {
|
|
|
233
236
|
return nested;
|
|
234
237
|
}
|
|
235
238
|
function extractTasksFromDetailWithHierarchy(input) {
|
|
236
|
-
const { noteId, noteTitle, createdAt, createdAtRaw, noteTags, taskItems, depth = 0 } = input;
|
|
239
|
+
const { noteId, noteTitle, createdAt, createdAtRaw, noteTags, taskItems, preorderState, depth = 0, } = input;
|
|
237
240
|
const result = [];
|
|
238
241
|
for (const item of taskItems) {
|
|
242
|
+
const preorderIndex = preorderState.nextIndex;
|
|
243
|
+
preorderState.nextIndex += 1;
|
|
239
244
|
const taskId = normalizeString(item.id) ||
|
|
240
245
|
normalizeString(item.attrs?.id) ||
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const checked = item.checked === true || item.checked === 'true' || item.attrs?.checked === true;
|
|
246
|
+
buildLegacyTaskId(noteId, preorderIndex, item);
|
|
247
|
+
const checked = normalizeTaskChecked(item.checked) ?? normalizeTaskChecked(item.attrs?.checked) ?? false;
|
|
246
248
|
const text = getTaskText(item).trim();
|
|
247
249
|
const tags = getTaskTags(item);
|
|
248
250
|
const { startTime, dueTime } = getTaskTimeRange(item);
|
|
@@ -253,6 +255,7 @@ function extractTasksFromDetailWithHierarchy(input) {
|
|
|
253
255
|
createdAtRaw,
|
|
254
256
|
noteTags,
|
|
255
257
|
taskItems: extractNestedTasks(item.content),
|
|
258
|
+
preorderState,
|
|
256
259
|
depth: depth + 1,
|
|
257
260
|
});
|
|
258
261
|
result.push({
|
|
@@ -302,18 +305,18 @@ function resolveTaskDateRange(taskDateRange) {
|
|
|
302
305
|
if (!taskDateRange) {
|
|
303
306
|
return null;
|
|
304
307
|
}
|
|
305
|
-
|
|
306
|
-
const endSource = taskDateRange.endDate ?? taskDateRange.startDate;
|
|
307
|
-
if (!startSource || !endSource) {
|
|
308
|
+
if (!taskDateRange.startDate && !taskDateRange.endDate) {
|
|
308
309
|
return null;
|
|
309
310
|
}
|
|
310
|
-
const start =
|
|
311
|
-
|
|
311
|
+
const start = taskDateRange.startDate
|
|
312
|
+
? new Date(taskDateRange.startDate)
|
|
313
|
+
: new Date(-8_640_000_000_000_000);
|
|
314
|
+
const end = taskDateRange.endDate
|
|
315
|
+
? new Date(taskDateRange.endDate)
|
|
316
|
+
: new Date(8_640_000_000_000_000);
|
|
312
317
|
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
313
318
|
return null;
|
|
314
319
|
}
|
|
315
|
-
start.setHours(0, 0, 0, 0);
|
|
316
|
-
end.setHours(23, 59, 59, 999);
|
|
317
320
|
return {
|
|
318
321
|
startMs: Math.min(start.getTime(), end.getTime()),
|
|
319
322
|
endMs: Math.max(start.getTime(), end.getTime()),
|
|
@@ -321,12 +324,12 @@ function resolveTaskDateRange(taskDateRange) {
|
|
|
321
324
|
}
|
|
322
325
|
function isTaskInDateRange(task, range) {
|
|
323
326
|
const dueMs = parseTaskPointMs(task.dueTime);
|
|
324
|
-
if (dueMs !== null
|
|
325
|
-
return
|
|
327
|
+
if (dueMs !== null) {
|
|
328
|
+
return dueMs >= range.startMs && dueMs <= range.endMs;
|
|
326
329
|
}
|
|
327
330
|
const startMs = parseTaskPointMs(task.startTime);
|
|
328
|
-
if (startMs !== null
|
|
329
|
-
return
|
|
331
|
+
if (startMs !== null) {
|
|
332
|
+
return startMs >= range.startMs && startMs <= range.endMs;
|
|
330
333
|
}
|
|
331
334
|
const createdAtMs = task.createdAt.getTime();
|
|
332
335
|
if (!Number.isNaN(createdAtMs) && createdAtMs >= range.startMs && createdAtMs <= range.endMs) {
|
|
@@ -347,3 +350,10 @@ function normalizeString(value) {
|
|
|
347
350
|
}
|
|
348
351
|
return value.trim();
|
|
349
352
|
}
|
|
353
|
+
function normalizeTaskChecked(value) {
|
|
354
|
+
if (value === true || value === 'true')
|
|
355
|
+
return true;
|
|
356
|
+
if (value === false || value === 'false')
|
|
357
|
+
return false;
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
@@ -4,10 +4,22 @@ type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
|
4
4
|
type LogFn = (...parts: unknown[]) => void;
|
|
5
5
|
type UpdateCommandDeps = {
|
|
6
6
|
detectPackageManager: () => PackageManager;
|
|
7
|
-
runUpdate: (pm: PackageManager
|
|
7
|
+
runUpdate: (pm: PackageManager, options: {
|
|
8
|
+
structuredOutput: boolean;
|
|
9
|
+
}) => Promise<void>;
|
|
8
10
|
getVersion: () => string;
|
|
9
11
|
log: LogFn;
|
|
10
12
|
};
|
|
13
|
+
export declare function detectPackageManager(input?: {
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
scriptPath?: string;
|
|
16
|
+
}): PackageManager;
|
|
17
|
+
type UpdateSpawnSpec = {
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
shell: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare function resolveUpdateSpawnSpec(pm: PackageManager, platform?: NodeJS.Platform): UpdateSpawnSpec;
|
|
11
23
|
export declare function createUpdateCommandDescriptor(overrides?: Partial<UpdateCommandDeps>): CommandDescriptor;
|
|
12
24
|
export declare const updateCommandDescriptor: CommandDescriptor;
|
|
13
25
|
export declare function registerUpdateCommand(program: Command, overrides?: Partial<UpdateCommandDeps>): void;
|