@dinoxx/dinox-cli 1.0.19 → 1.0.21
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 +756 -472
- package/dist/auth/userInfo.d.ts +10 -1
- package/dist/auth/userInfo.js +66 -17
- package/dist/cli.js +0 -2
- package/dist/commands/auth/index.js +29 -14
- package/dist/commands/boxes/index.js +272 -52
- package/dist/commands/boxes/model.d.ts +34 -0
- package/dist/commands/boxes/model.js +78 -0
- package/dist/commands/boxes/operations.d.ts +86 -0
- package/dist/commands/boxes/operations.js +415 -0
- package/dist/commands/boxes/repo.d.ts +13 -6
- package/dist/commands/boxes/repo.js +272 -149
- package/dist/commands/boxes/store.d.ts +22 -0
- package/dist/commands/boxes/store.js +143 -0
- package/dist/commands/graph/index.js +2 -36
- package/dist/commands/mcp/noteContentTools.d.ts +222 -0
- package/dist/commands/mcp/noteContentTools.js +390 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +45 -16
- package/dist/commands/mcp/tools.js +3 -1
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/meta/index.js +6 -22
- package/dist/commands/meta/repo.d.ts +0 -3
- package/dist/commands/meta/repo.js +1 -4
- package/dist/commands/notes/actions.d.ts +2 -0
- package/dist/commands/notes/actions.js +149 -55
- package/dist/commands/notes/bulk.d.ts +34 -0
- package/dist/commands/notes/bulk.js +204 -0
- package/dist/commands/notes/contentEngine.d.ts +95 -0
- package/dist/commands/notes/contentEngine.js +652 -0
- package/dist/commands/notes/contentPatch.d.ts +84 -0
- package/dist/commands/notes/contentPatch.js +337 -0
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +126 -0
- package/dist/commands/notes/descriptors.js +192 -40
- package/dist/commands/notes/export.d.ts +21 -0
- package/dist/commands/notes/export.js +224 -0
- package/dist/commands/notes/metadataMutation.d.ts +28 -0
- package/dist/commands/notes/metadataMutation.js +127 -0
- package/dist/commands/notes/repo.d.ts +20 -0
- package/dist/commands/notes/repo.js +135 -18
- package/dist/commands/notes/runtime.d.ts +3 -12
- package/dist/commands/notes/runtime.js +4 -30
- package/dist/commands/notes/searchSql.js +1 -1
- package/dist/commands/prompt/index.js +15 -45
- package/dist/commands/prompt/repo.js +2 -2
- package/dist/commands/runtime.d.ts +26 -0
- package/dist/commands/runtime.js +76 -0
- package/dist/commands/schema/renderSkills.js +20 -7
- package/dist/commands/schema/spec.d.ts +1 -2
- package/dist/commands/schema/spec.js +2 -3
- package/dist/commands/schema/types.d.ts +0 -13
- package/dist/commands/storage/index.js +30 -60
- package/dist/commands/storage/repo.d.ts +1 -1
- package/dist/commands/storage/repo.js +20 -1
- package/dist/commands/sync.js +4 -2
- package/dist/commands/tags/content.d.ts +32 -0
- package/dist/commands/tags/content.js +243 -0
- package/dist/commands/tags/index.js +285 -49
- package/dist/commands/tags/model.d.ts +38 -0
- package/dist/commands/tags/model.js +100 -0
- package/dist/commands/tags/operations.d.ts +105 -0
- package/dist/commands/tags/operations.js +438 -0
- package/dist/commands/tags/repo.d.ts +5 -1
- package/dist/commands/tags/repo.js +142 -218
- package/dist/commands/tags/store.d.ts +28 -0
- package/dist/commands/tags/store.js +166 -0
- package/dist/commands/todo/index.js +30 -61
- package/dist/commands/writeDryRun.d.ts +25 -0
- package/dist/commands/writeDryRun.js +31 -1
- package/dist/config/paths.d.ts +2 -1
- package/dist/config/paths.js +11 -1
- package/dist/config/resolve.js +9 -7
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +2 -1
- package/dist/powersync/runtime.js +15 -3
- package/dist/powersync/schema/content.d.ts +2 -0
- package/dist/powersync/schema/content.js +2 -0
- package/dist/powersync/schema/index.d.ts +2 -0
- package/dist/skills/check.d.ts +1 -1
- package/dist/skills/check.js +27 -2
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +7 -2
- package/dist/utils/noteContentEngine.d.ts +333 -0
- package/dist/utils/noteContentEngine.js +740 -0
- package/dist/utils/tiptapMarkdownSchema.js +31 -0
- package/package.json +4 -8
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type NoteContentBlockDescriptor, type NoteContentOutlineItem } from './contentEngine.js';
|
|
3
|
+
export type NoteContentPatchOperation = {
|
|
4
|
+
kind: 'appendSection';
|
|
5
|
+
heading: string;
|
|
6
|
+
content: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'appendToHeading';
|
|
9
|
+
headingPath: string;
|
|
10
|
+
content: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'replaceSection';
|
|
13
|
+
headingPath: string;
|
|
14
|
+
content: string;
|
|
15
|
+
allowProtectedReplace?: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'replaceBlock';
|
|
18
|
+
match: string;
|
|
19
|
+
content: string;
|
|
20
|
+
allowProtectedReplace?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type NoteContentReadResult = {
|
|
23
|
+
noteId: string;
|
|
24
|
+
title: string;
|
|
25
|
+
readToken: string;
|
|
26
|
+
readTokenExpiresAt: string;
|
|
27
|
+
base: {
|
|
28
|
+
contentHash: string;
|
|
29
|
+
updatedAt: string | null;
|
|
30
|
+
version: number | null;
|
|
31
|
+
};
|
|
32
|
+
content: string;
|
|
33
|
+
outline: NoteContentOutlineItem[];
|
|
34
|
+
blocks: NoteContentBlockDescriptor[];
|
|
35
|
+
derived: {
|
|
36
|
+
hashtags: string[];
|
|
37
|
+
images: string[];
|
|
38
|
+
bidLinks: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
type: string;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export type NoteContentPatchPlan = {
|
|
45
|
+
noteId: string;
|
|
46
|
+
title: string;
|
|
47
|
+
operation: NoteContentPatchOperation['kind'];
|
|
48
|
+
changed: boolean;
|
|
49
|
+
beforeHash: string;
|
|
50
|
+
afterHash: string;
|
|
51
|
+
beforeContent: string;
|
|
52
|
+
afterContent: string;
|
|
53
|
+
beforeBlocks: NoteContentBlockDescriptor[];
|
|
54
|
+
afterBlocks: NoteContentBlockDescriptor[];
|
|
55
|
+
derived: {
|
|
56
|
+
beforeHashtags: string[];
|
|
57
|
+
afterHashtags: string[];
|
|
58
|
+
images: string[];
|
|
59
|
+
bidLinks: Array<{
|
|
60
|
+
id: string;
|
|
61
|
+
type: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
target?: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
export type NoteContentPatchResult = Omit<NoteContentPatchPlan, 'beforeContent' | 'afterContent'> & {
|
|
67
|
+
saved: boolean;
|
|
68
|
+
};
|
|
69
|
+
export declare function planMarkdownPatch(input: {
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
contentMd: string;
|
|
73
|
+
operation: NoteContentPatchOperation;
|
|
74
|
+
}): NoteContentPatchPlan;
|
|
75
|
+
export declare function readNoteContentForPatch(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContentReadResult>;
|
|
76
|
+
export declare function planNoteContentPatch(db: AbstractPowerSyncDatabase, input: {
|
|
77
|
+
id: string;
|
|
78
|
+
operation: NoteContentPatchOperation;
|
|
79
|
+
}): Promise<NoteContentPatchPlan>;
|
|
80
|
+
export declare function applyNoteContentPatch(db: AbstractPowerSyncDatabase, input: {
|
|
81
|
+
id: string;
|
|
82
|
+
readToken: string;
|
|
83
|
+
operation: NoteContentPatchOperation;
|
|
84
|
+
}): Promise<NoteContentPatchResult>;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getDataDir } from '../../config/paths.js';
|
|
4
|
+
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
5
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
6
|
+
import { generateId } from '../../utils/id.js';
|
|
7
|
+
import { nowIsoUtc } from '../../utils/time.js';
|
|
8
|
+
import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
|
|
9
|
+
const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
10
|
+
const MAX_STORED_READ_TOKENS = 500;
|
|
11
|
+
const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
|
|
12
|
+
function normalizeVersion(value) {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
15
|
+
}
|
|
16
|
+
function canonicalTagPath(value) {
|
|
17
|
+
return value
|
|
18
|
+
.split(/[\\/]+/g)
|
|
19
|
+
.map((segment) => segment.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('/');
|
|
22
|
+
}
|
|
23
|
+
function tagPathKey(value) {
|
|
24
|
+
return canonicalTagPath(value).toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
async function loadTagReferenceLookup(db) {
|
|
27
|
+
const rows = await db.getAll(`
|
|
28
|
+
SELECT
|
|
29
|
+
TRIM(COALESCE(id, '')) AS id,
|
|
30
|
+
TRIM(COALESCE(full_path, path_key, '')) AS full_path,
|
|
31
|
+
TRIM(COALESCE(path_key, full_path, '')) AS path_key
|
|
32
|
+
FROM c_tag_node
|
|
33
|
+
WHERE TRIM(COALESCE(id, '')) <> ''
|
|
34
|
+
AND TRIM(COALESCE(full_path, path_key, '')) <> ''
|
|
35
|
+
AND (is_del IS NULL OR is_del = 0)
|
|
36
|
+
AND (is_actual IS NULL OR is_actual != 0)
|
|
37
|
+
ORDER BY
|
|
38
|
+
updated_at DESC,
|
|
39
|
+
created_at DESC,
|
|
40
|
+
id DESC
|
|
41
|
+
`, []);
|
|
42
|
+
const byId = new Map();
|
|
43
|
+
const byPathKey = new Map();
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
const id = row.id?.trim() ?? '';
|
|
46
|
+
const path = canonicalTagPath(row.full_path ?? row.path_key ?? '');
|
|
47
|
+
if (!id || !path) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
byId.set(id, path);
|
|
51
|
+
const key = tagPathKey(path);
|
|
52
|
+
if (key && !byPathKey.has(key)) {
|
|
53
|
+
byPathKey.set(key, { id, path });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { byId, byPathKey };
|
|
57
|
+
}
|
|
58
|
+
function createHashtagReferenceResolver(lookup, options) {
|
|
59
|
+
return ({ id, path }) => {
|
|
60
|
+
const pathFromId = id ? lookup.byId.get(id) : undefined;
|
|
61
|
+
const resolvedPath = canonicalTagPath(pathFromId ?? path);
|
|
62
|
+
if (!resolvedPath) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const match = lookup.byPathKey.get(tagPathKey(resolvedPath));
|
|
66
|
+
if (match) {
|
|
67
|
+
return match;
|
|
68
|
+
}
|
|
69
|
+
if (options.strictMissingPaths) {
|
|
70
|
+
throw new DinoxError(`Unknown tag path in note content: ${resolvedPath}. Create it with \`dino tag add "${resolvedPath}"\` before patching.`);
|
|
71
|
+
}
|
|
72
|
+
return { id: id || resolvedPath, path: resolvedPath };
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function tokenStorePath() {
|
|
76
|
+
return path.join(getDataDir(), TOKEN_STORE_FILE);
|
|
77
|
+
}
|
|
78
|
+
async function loadTokenStore(nowMs = Date.now()) {
|
|
79
|
+
const filePath = tokenStorePath();
|
|
80
|
+
let tokens = [];
|
|
81
|
+
try {
|
|
82
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (Array.isArray(parsed.tokens)) {
|
|
85
|
+
tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
|
|
86
|
+
typeof token.noteId === 'string' &&
|
|
87
|
+
typeof token.contentHash === 'string' &&
|
|
88
|
+
typeof token.expiresAt === 'string');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT') {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
tokens: tokens
|
|
98
|
+
.filter((token) => Date.parse(token.expiresAt) > nowMs)
|
|
99
|
+
.slice(-MAX_STORED_READ_TOKENS),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function saveTokenStore(store) {
|
|
103
|
+
const filePath = tokenStorePath();
|
|
104
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
105
|
+
const tmpPath = `${filePath}.${generateId()}.tmp`;
|
|
106
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
107
|
+
if (process.platform !== 'win32') {
|
|
108
|
+
await fs.chmod(tmpPath, 0o600).catch(() => undefined);
|
|
109
|
+
}
|
|
110
|
+
await fs.rename(tmpPath, filePath);
|
|
111
|
+
}
|
|
112
|
+
async function rememberReadToken(input) {
|
|
113
|
+
const nowMs = Date.now();
|
|
114
|
+
const store = await loadTokenStore(nowMs);
|
|
115
|
+
const expiresAt = new Date(nowMs + READ_TOKEN_TTL_MS).toISOString();
|
|
116
|
+
const token = {
|
|
117
|
+
token: generateId(),
|
|
118
|
+
noteId: input.noteId,
|
|
119
|
+
contentHash: input.contentHash,
|
|
120
|
+
updatedAt: input.updatedAt,
|
|
121
|
+
version: input.version,
|
|
122
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
123
|
+
expiresAt,
|
|
124
|
+
};
|
|
125
|
+
store.tokens.push(token);
|
|
126
|
+
store.tokens = store.tokens.slice(-MAX_STORED_READ_TOKENS);
|
|
127
|
+
await saveTokenStore(store);
|
|
128
|
+
return token;
|
|
129
|
+
}
|
|
130
|
+
async function consumeReadToken(tokenId) {
|
|
131
|
+
const normalized = tokenId.trim();
|
|
132
|
+
if (!normalized) {
|
|
133
|
+
throw new DinoxError('Missing --read-token. Run `dino note content-read <id>` before editing.');
|
|
134
|
+
}
|
|
135
|
+
const store = await loadTokenStore();
|
|
136
|
+
const token = store.tokens.find((candidate) => candidate.token === normalized);
|
|
137
|
+
if (!token) {
|
|
138
|
+
throw new DinoxError('read token is expired or unknown. Run `dino note content-read <id>` again.');
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
async function forgetReadToken(tokenId) {
|
|
143
|
+
const store = await loadTokenStore();
|
|
144
|
+
const next = store.tokens.filter((token) => token.token !== tokenId);
|
|
145
|
+
if (next.length !== store.tokens.length) {
|
|
146
|
+
await saveTokenStore({ tokens: next });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function readNoteContentRow(db, id) {
|
|
150
|
+
const noteId = id.trim();
|
|
151
|
+
if (!noteId) {
|
|
152
|
+
throw new DinoxError('Note id is required');
|
|
153
|
+
}
|
|
154
|
+
const row = await db.getOptional(`
|
|
155
|
+
SELECT id, title, content_md, content_json, content_text, updated_at, version, is_del
|
|
156
|
+
FROM c_note
|
|
157
|
+
WHERE id = ?
|
|
158
|
+
`, [noteId]);
|
|
159
|
+
if (!row) {
|
|
160
|
+
throw new DinoxError(`Note not found: ${noteId}`);
|
|
161
|
+
}
|
|
162
|
+
if (Number(row.is_del ?? 0) !== 0) {
|
|
163
|
+
throw new DinoxError(`Note has been deleted: ${noteId}`);
|
|
164
|
+
}
|
|
165
|
+
return row;
|
|
166
|
+
}
|
|
167
|
+
async function getNoteTableColumns(db) {
|
|
168
|
+
const rows = await db.getAll('PRAGMA table_info(c_note)', []);
|
|
169
|
+
return new Set(rows.map((row) => row.name?.trim().toLowerCase() ?? '').filter(Boolean));
|
|
170
|
+
}
|
|
171
|
+
async function updateNoteContentProjection(tx, input) {
|
|
172
|
+
const columns = await getNoteTableColumns(tx);
|
|
173
|
+
const setParts = [
|
|
174
|
+
'content_md = ?',
|
|
175
|
+
'content_json = ?',
|
|
176
|
+
'content_text = ?',
|
|
177
|
+
];
|
|
178
|
+
const params = [input.contentMd, input.contentJson, input.contentText];
|
|
179
|
+
if (columns.has('images')) {
|
|
180
|
+
setParts.push('images = ?');
|
|
181
|
+
params.push(JSON.stringify(input.images));
|
|
182
|
+
}
|
|
183
|
+
if (columns.has('tags')) {
|
|
184
|
+
setParts.push('tags = ?');
|
|
185
|
+
params.push(JSON.stringify(input.tags));
|
|
186
|
+
}
|
|
187
|
+
if (columns.has('bid_links')) {
|
|
188
|
+
setParts.push('bid_links = ?');
|
|
189
|
+
params.push(JSON.stringify(input.bidLinks));
|
|
190
|
+
}
|
|
191
|
+
setParts.push('updated_at = ?');
|
|
192
|
+
params.push(nowIsoUtc());
|
|
193
|
+
setParts.push('version = COALESCE(version, 0) + 1');
|
|
194
|
+
params.push(input.rowId);
|
|
195
|
+
await tx.execute(`
|
|
196
|
+
UPDATE c_note
|
|
197
|
+
SET ${setParts.join(',\n ')}
|
|
198
|
+
WHERE id = ?
|
|
199
|
+
`, params);
|
|
200
|
+
}
|
|
201
|
+
function buildPatchPlan(input) {
|
|
202
|
+
const document = parseStoredNoteDocument({
|
|
203
|
+
contentJson: input.contentJson,
|
|
204
|
+
contentMd: input.contentMd,
|
|
205
|
+
});
|
|
206
|
+
const enginePlan = applyJsonPatchToDocument(document, input.operation, {
|
|
207
|
+
resolveHashtagReference: input.resolveHashtagReference,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
noteId: input.id,
|
|
211
|
+
title: input.title,
|
|
212
|
+
operation: enginePlan.operation,
|
|
213
|
+
changed: enginePlan.changed,
|
|
214
|
+
beforeHash: enginePlan.beforeHash,
|
|
215
|
+
afterHash: enginePlan.afterHash,
|
|
216
|
+
beforeContent: enginePlan.beforeContent,
|
|
217
|
+
afterContent: enginePlan.afterContent,
|
|
218
|
+
beforeBlocks: enginePlan.beforeBlocks,
|
|
219
|
+
afterBlocks: enginePlan.afterBlocks,
|
|
220
|
+
derived: {
|
|
221
|
+
beforeHashtags: enginePlan.beforeProjection.hashtags,
|
|
222
|
+
afterHashtags: enginePlan.afterProjection.hashtags,
|
|
223
|
+
images: enginePlan.afterProjection.images,
|
|
224
|
+
bidLinks: enginePlan.afterProjection.bidLinks,
|
|
225
|
+
},
|
|
226
|
+
target: enginePlan.target,
|
|
227
|
+
afterProjection: enginePlan.afterProjection,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function planMarkdownPatch(input) {
|
|
231
|
+
const { afterProjection: _afterProjection, ...plan } = buildPatchPlan({
|
|
232
|
+
id: input.id,
|
|
233
|
+
title: input.title,
|
|
234
|
+
contentJson: null,
|
|
235
|
+
contentMd: input.contentMd,
|
|
236
|
+
operation: input.operation,
|
|
237
|
+
});
|
|
238
|
+
return plan;
|
|
239
|
+
}
|
|
240
|
+
export async function readNoteContentForPatch(db, id) {
|
|
241
|
+
const row = await readNoteContentRow(db, id);
|
|
242
|
+
const tagLookup = await loadTagReferenceLookup(db);
|
|
243
|
+
const document = parseStoredNoteDocument({
|
|
244
|
+
contentJson: row.content_json,
|
|
245
|
+
contentMd: row.content_md,
|
|
246
|
+
});
|
|
247
|
+
const projection = projectNoteContent(document, {
|
|
248
|
+
resolveHashtagReference: createHashtagReferenceResolver(tagLookup, { strictMissingPaths: false }),
|
|
249
|
+
});
|
|
250
|
+
const contentHash = getNoteContentHash(document);
|
|
251
|
+
const version = normalizeVersion(row.version);
|
|
252
|
+
const token = await rememberReadToken({
|
|
253
|
+
noteId: row.id,
|
|
254
|
+
contentHash,
|
|
255
|
+
updatedAt: row.updated_at ?? null,
|
|
256
|
+
version,
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
noteId: row.id,
|
|
260
|
+
title: row.title?.trim() ?? '',
|
|
261
|
+
readToken: token.token,
|
|
262
|
+
readTokenExpiresAt: token.expiresAt,
|
|
263
|
+
base: {
|
|
264
|
+
contentHash,
|
|
265
|
+
updatedAt: row.updated_at ?? null,
|
|
266
|
+
version,
|
|
267
|
+
},
|
|
268
|
+
content: projection.contentMd,
|
|
269
|
+
outline: buildNoteContentOutline(projection.document),
|
|
270
|
+
blocks: buildNoteContentBlocks(projection.document),
|
|
271
|
+
derived: {
|
|
272
|
+
hashtags: projection.hashtags,
|
|
273
|
+
images: projection.images,
|
|
274
|
+
bidLinks: projection.bidLinks,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export async function planNoteContentPatch(db, input) {
|
|
279
|
+
const row = await readNoteContentRow(db, input.id);
|
|
280
|
+
const tagLookup = await loadTagReferenceLookup(db);
|
|
281
|
+
const { afterProjection: _afterProjection, ...plan } = buildPatchPlan({
|
|
282
|
+
id: row.id,
|
|
283
|
+
title: row.title?.trim() ?? '',
|
|
284
|
+
contentJson: row.content_json,
|
|
285
|
+
contentMd: row.content_md,
|
|
286
|
+
operation: input.operation,
|
|
287
|
+
resolveHashtagReference: createHashtagReferenceResolver(tagLookup, { strictMissingPaths: true }),
|
|
288
|
+
});
|
|
289
|
+
return plan;
|
|
290
|
+
}
|
|
291
|
+
export async function applyNoteContentPatch(db, input) {
|
|
292
|
+
const token = await consumeReadToken(input.readToken);
|
|
293
|
+
const result = await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
294
|
+
const tagLookup = await loadTagReferenceLookup(tx);
|
|
295
|
+
const row = await readNoteContentRow(tx, input.id);
|
|
296
|
+
if (row.id !== token.noteId) {
|
|
297
|
+
throw new DinoxError('read token does not match target note. Run `dino note content-read <id>` again.');
|
|
298
|
+
}
|
|
299
|
+
const currentDocument = parseStoredNoteDocument({
|
|
300
|
+
contentJson: row.content_json,
|
|
301
|
+
contentMd: row.content_md,
|
|
302
|
+
});
|
|
303
|
+
if (token.updatedAt !== (row.updated_at ?? null) ||
|
|
304
|
+
token.version !== normalizeVersion(row.version) ||
|
|
305
|
+
token.contentHash !== getNoteContentHash(currentDocument)) {
|
|
306
|
+
await forgetReadToken(token.token);
|
|
307
|
+
throw new DinoxError('Note content changed after content-read. Run `dino note content-read <id>` again.');
|
|
308
|
+
}
|
|
309
|
+
const plan = buildPatchPlan({
|
|
310
|
+
id: row.id,
|
|
311
|
+
title: row.title?.trim() ?? '',
|
|
312
|
+
contentJson: row.content_json,
|
|
313
|
+
contentMd: row.content_md,
|
|
314
|
+
operation: input.operation,
|
|
315
|
+
resolveHashtagReference: createHashtagReferenceResolver(tagLookup, { strictMissingPaths: true }),
|
|
316
|
+
});
|
|
317
|
+
if (plan.changed) {
|
|
318
|
+
await updateNoteContentProjection(tx, {
|
|
319
|
+
rowId: row.id,
|
|
320
|
+
contentMd: plan.afterProjection.contentMd,
|
|
321
|
+
contentJson: plan.afterProjection.contentJson,
|
|
322
|
+
contentText: plan.afterProjection.contentText,
|
|
323
|
+
tags: plan.afterProjection.hashtags,
|
|
324
|
+
images: plan.afterProjection.images,
|
|
325
|
+
bidLinks: plan.afterProjection.bidLinks,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const { afterProjection: _afterProjection, ...publicPlan } = plan;
|
|
329
|
+
return publicPlan;
|
|
330
|
+
});
|
|
331
|
+
await forgetReadToken(token.token);
|
|
332
|
+
const { beforeContent: _beforeContent, afterContent: _afterContent, ...publicResult } = result;
|
|
333
|
+
return {
|
|
334
|
+
...publicResult,
|
|
335
|
+
saved: result.changed,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CommandExecutionContext } from '../descriptor/types.js';
|
|
2
|
+
export declare function runNoteContentRead({ command, options, args }: CommandExecutionContext): Promise<void>;
|
|
3
|
+
export declare function runNotePatch({ command, options, args }: CommandExecutionContext): Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
2
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
3
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
4
|
+
import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, waitForMutationUpload, } from '../runtime.js';
|
|
5
|
+
import { applyNoteContentPatch, planNoteContentPatch, readNoteContentForPatch, } from './contentPatch.js';
|
|
6
|
+
import { parseStringInput, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
7
|
+
function requireNoteId(id) {
|
|
8
|
+
const normalized = id?.trim() ?? '';
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
throw new DinoxError('Note id is required');
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
async function selectPatchOperation(options) {
|
|
15
|
+
const candidates = [
|
|
16
|
+
{ kind: 'appendSection', value: options.appendSection },
|
|
17
|
+
{ kind: 'appendToHeading', value: options.appendToHeading },
|
|
18
|
+
{ kind: 'replaceSection', value: options.replaceSection },
|
|
19
|
+
{ kind: 'replaceBlock', value: options.replaceBlock ? 'true' : undefined },
|
|
20
|
+
];
|
|
21
|
+
const selected = candidates.filter((entry) => typeof entry.value === 'string');
|
|
22
|
+
if (selected.length !== 1) {
|
|
23
|
+
throw new DinoxError('Provide exactly one patch operation: --append-section, --append-to-heading, --replace-section, or --replace-block');
|
|
24
|
+
}
|
|
25
|
+
if (typeof options.content !== 'string') {
|
|
26
|
+
throw new DinoxError('--content <string|@file> is required');
|
|
27
|
+
}
|
|
28
|
+
const content = await parseStringInput(options.content);
|
|
29
|
+
const { kind, value } = selected[0];
|
|
30
|
+
if (kind === 'appendSection') {
|
|
31
|
+
return { kind: 'appendSection', heading: value, content };
|
|
32
|
+
}
|
|
33
|
+
if (kind === 'appendToHeading') {
|
|
34
|
+
return { kind: 'appendToHeading', headingPath: value, content };
|
|
35
|
+
}
|
|
36
|
+
if (kind === 'replaceSection') {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'replaceSection',
|
|
39
|
+
headingPath: value,
|
|
40
|
+
content,
|
|
41
|
+
allowProtectedReplace: options.allowProtectedReplace === true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (typeof options.match !== 'string') {
|
|
45
|
+
throw new DinoxError('--match <string|@file> is required with --replace-block');
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
kind: 'replaceBlock',
|
|
49
|
+
match: await parseStringInput(options.match),
|
|
50
|
+
content,
|
|
51
|
+
allowProtectedReplace: options.allowProtectedReplace === true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function runNoteContentRead({ command, options, args }) {
|
|
55
|
+
const readOptions = options;
|
|
56
|
+
const id = requireNoteId(args.id);
|
|
57
|
+
await withNoteDatabase(command, async (runtime) => {
|
|
58
|
+
await syncBeforeNoteCommand(runtime);
|
|
59
|
+
const result = await readNoteContentForPatch(runtime.db, id);
|
|
60
|
+
const { content, ...rest } = result;
|
|
61
|
+
const payload = readOptions.includeContent
|
|
62
|
+
? { ...rest, content, stale: commandStale(runtime) }
|
|
63
|
+
: { ...rest, stale: commandStale(runtime) };
|
|
64
|
+
printSuccess(payload);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function runNotePatch({ command, options, args }) {
|
|
68
|
+
const patchOptions = options;
|
|
69
|
+
const id = requireNoteId(args.id);
|
|
70
|
+
const operation = await selectPatchOperation(patchOptions);
|
|
71
|
+
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
72
|
+
await withNoteDatabase(command, async (runtime) => {
|
|
73
|
+
const { db, config, offline } = runtime;
|
|
74
|
+
await syncBeforeNoteCommand(runtime);
|
|
75
|
+
if (patchOptions.dryRun) {
|
|
76
|
+
const plan = await planNoteContentPatch(db, { id, operation });
|
|
77
|
+
printDryRunPreview({
|
|
78
|
+
dry_run: true,
|
|
79
|
+
command: 'dino note patch',
|
|
80
|
+
action: 'patch_note_content',
|
|
81
|
+
risk: 'write',
|
|
82
|
+
summary: `Preview ${plan.operation} for note "${plan.title}" (${plan.noteId}); content ${plan.changed ? 'would change' : 'is unchanged'}.`,
|
|
83
|
+
warnings: [
|
|
84
|
+
'Run `dino note content-read <id> --format json` before the real edit and pass the returned readToken as --read-token.',
|
|
85
|
+
...(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
|
+
],
|
|
90
|
+
inputs: {
|
|
91
|
+
id,
|
|
92
|
+
operation: plan.operation,
|
|
93
|
+
},
|
|
94
|
+
targets: [{ id: plan.noteId, title: plan.title, ...plan.target }],
|
|
95
|
+
changes: {
|
|
96
|
+
before_hash: plan.beforeHash,
|
|
97
|
+
after_hash: plan.afterHash,
|
|
98
|
+
before_chars: plan.beforeContent.length,
|
|
99
|
+
after_chars: plan.afterContent.length,
|
|
100
|
+
changed: plan.changed,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const readToken = patchOptions.readToken?.trim() ?? '';
|
|
106
|
+
if (!readToken) {
|
|
107
|
+
throw new DinoxError('Real note content edits require --read-token from `dino note content-read <id>`.');
|
|
108
|
+
}
|
|
109
|
+
const result = await applyNoteContentPatch(db, { id, readToken, operation });
|
|
110
|
+
printUploadDisabledWarning(runtime);
|
|
111
|
+
await waitForMutationUpload(runtime);
|
|
112
|
+
const payload = {
|
|
113
|
+
ok: true,
|
|
114
|
+
...result,
|
|
115
|
+
stale: commandStale(runtime),
|
|
116
|
+
};
|
|
117
|
+
if (structuredOutput) {
|
|
118
|
+
printSuccess(payload);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
printHumanReadableLine(result.saved ? 'Patched note content' : 'No changes');
|
|
122
|
+
printHumanReadableLine(`before_hash: ${result.beforeHash}`);
|
|
123
|
+
printHumanReadableLine(`after_hash: ${result.afterHash}`);
|
|
124
|
+
printStaleCacheWarning(runtime);
|
|
125
|
+
});
|
|
126
|
+
}
|