@dinoxx/dinox-cli 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -139
- package/dist/auth/userInfo.d.ts +8 -1
- package/dist/auth/userInfo.js +38 -6
- package/dist/cli.js +1 -2
- package/dist/cliTypes.d.ts +1 -0
- package/dist/commands/auth/index.js +11 -4
- package/dist/commands/boxes/index.js +274 -54
- package/dist/commands/boxes/model.d.ts +34 -0
- package/dist/commands/boxes/model.js +78 -0
- package/dist/commands/boxes/operations.d.ts +86 -0
- package/dist/commands/boxes/operations.js +415 -0
- package/dist/commands/boxes/repo.d.ts +13 -6
- package/dist/commands/boxes/repo.js +272 -149
- package/dist/commands/boxes/store.d.ts +22 -0
- package/dist/commands/boxes/store.js +143 -0
- package/dist/commands/graph/index.js +11 -38
- package/dist/commands/mcp/noteContentTools.d.ts +222 -0
- package/dist/commands/mcp/noteContentTools.js +390 -0
- package/dist/commands/mcp/server.d.ts +3 -0
- package/dist/commands/mcp/server.js +43 -15
- package/dist/commands/mcp/tools.js +3 -1
- package/dist/commands/mcp/types.d.ts +6 -2
- package/dist/commands/meta/index.js +8 -24
- package/dist/commands/meta/repo.d.ts +0 -3
- package/dist/commands/meta/repo.js +1 -4
- package/dist/commands/notes/actions.d.ts +2 -0
- package/dist/commands/notes/actions.js +149 -55
- package/dist/commands/notes/bulk.d.ts +34 -0
- package/dist/commands/notes/bulk.js +204 -0
- package/dist/commands/notes/contentEngine.d.ts +95 -0
- package/dist/commands/notes/contentEngine.js +652 -0
- package/dist/commands/notes/contentPatch.d.ts +84 -0
- package/dist/commands/notes/contentPatch.js +337 -0
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +126 -0
- package/dist/commands/notes/descriptors.js +194 -42
- package/dist/commands/notes/export.d.ts +21 -0
- package/dist/commands/notes/export.js +224 -0
- package/dist/commands/notes/metadataMutation.d.ts +28 -0
- package/dist/commands/notes/metadataMutation.js +127 -0
- package/dist/commands/notes/repo.d.ts +20 -0
- package/dist/commands/notes/repo.js +135 -18
- package/dist/commands/notes/runtime.d.ts +3 -12
- package/dist/commands/notes/runtime.js +4 -30
- package/dist/commands/notes/searchSql.js +1 -1
- package/dist/commands/prompt/index.js +17 -47
- package/dist/commands/prompt/repo.js +2 -2
- package/dist/commands/runtime.d.ts +34 -0
- package/dist/commands/runtime.js +110 -0
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +21 -7
- package/dist/commands/schema/spec.d.ts +1 -2
- package/dist/commands/schema/spec.js +2 -3
- package/dist/commands/schema/types.d.ts +0 -13
- package/dist/commands/storage/index.js +32 -62
- package/dist/commands/sync.js +22 -5
- package/dist/commands/tags/content.d.ts +32 -0
- package/dist/commands/tags/content.js +243 -0
- package/dist/commands/tags/index.js +287 -51
- package/dist/commands/tags/model.d.ts +38 -0
- package/dist/commands/tags/model.js +100 -0
- package/dist/commands/tags/operations.d.ts +105 -0
- package/dist/commands/tags/operations.js +438 -0
- package/dist/commands/tags/repo.d.ts +5 -1
- package/dist/commands/tags/repo.js +142 -218
- package/dist/commands/tags/store.d.ts +28 -0
- package/dist/commands/tags/store.js +166 -0
- package/dist/commands/todo/index.js +32 -63
- package/dist/commands/writeDryRun.d.ts +25 -0
- package/dist/commands/writeDryRun.js +31 -1
- package/dist/config/resolve.js +2 -2
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +176 -58
- package/dist/powersync/runtime.d.ts +1 -0
- package/dist/powersync/runtime.js +16 -0
- package/dist/powersync/schema/content.d.ts +2 -0
- package/dist/powersync/schema/content.js +2 -0
- package/dist/powersync/schema/index.d.ts +2 -0
- package/dist/powersync/syncGate.d.ts +24 -0
- package/dist/powersync/syncGate.js +36 -0
- package/dist/skills/check.d.ts +1 -1
- package/dist/skills/check.js +27 -2
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +7 -2
- package/dist/utils/noteContentEngine.d.ts +333 -0
- package/dist/utils/noteContentEngine.js +740 -0
- package/dist/utils/tiptapMarkdownSchema.js +31 -0
- package/package.json +4 -8
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
2
|
-
import { loadConfig } from '../../config/store.js';
|
|
3
|
-
import { resolveConfig } from '../../config/resolve.js';
|
|
4
|
-
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
5
|
-
import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
|
|
6
2
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
7
|
-
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
8
|
-
import { buildBoxAddDryRunPreview } from '../writeDryRun.js';
|
|
3
|
+
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
4
|
+
import { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } from '../writeDryRun.js';
|
|
9
5
|
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
10
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
11
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
12
|
-
import {
|
|
8
|
+
import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
13
9
|
import { addBox, inspectAddBox, listBoxes } from './repo.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
|
|
11
|
+
const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
12
|
+
async function runBoxList(context) {
|
|
13
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
14
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
15
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
16
|
+
printSuccess(await listBoxes(runtime.db, { userId: identity.userId }));
|
|
17
|
+
});
|
|
21
18
|
}
|
|
22
|
-
async function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
await db.close().catch(() => undefined);
|
|
33
|
-
}
|
|
19
|
+
async function runBoxTree(context) {
|
|
20
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
21
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
22
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
23
|
+
printSuccess({
|
|
24
|
+
stale: commandStale(runtime),
|
|
25
|
+
tree: await getBoxTree(runtime.db, { userId: identity.userId }),
|
|
26
|
+
});
|
|
27
|
+
});
|
|
34
28
|
}
|
|
35
|
-
async function
|
|
36
|
-
await
|
|
37
|
-
await
|
|
38
|
-
|
|
29
|
+
async function runBoxStats(context) {
|
|
30
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
31
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
32
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
33
|
+
printSuccess({
|
|
34
|
+
stale: commandStale(runtime),
|
|
35
|
+
...(await getBoxStats(runtime.db, { userId: identity.userId })),
|
|
36
|
+
});
|
|
39
37
|
});
|
|
40
38
|
}
|
|
41
39
|
async function runBoxAdd(context) {
|
|
@@ -43,22 +41,22 @@ async function runBoxAdd(context) {
|
|
|
43
41
|
const optionName = typeof context.options.name === 'string' ? context.options.name : undefined;
|
|
44
42
|
const name = (nameArg ?? optionName ?? '').trim();
|
|
45
43
|
if (!name) {
|
|
46
|
-
throw new DinoxError('Box
|
|
44
|
+
throw new DinoxError('Box path is required. Use `dino box add <path>` or `dino box add --name <path>`.');
|
|
47
45
|
}
|
|
48
46
|
if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
|
|
49
|
-
throw new DinoxError('Conflicting box
|
|
47
|
+
throw new DinoxError('Conflicting box path values from positional argument and --name option.');
|
|
50
48
|
}
|
|
51
49
|
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
52
|
-
await
|
|
53
|
-
const identity = await resolveAuthIdentity(
|
|
54
|
-
await
|
|
50
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
51
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
52
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
55
53
|
const description = typeof context.options.description === 'string' ? context.options.description : undefined;
|
|
56
54
|
const color = typeof context.options.color === 'string' ? context.options.color : undefined;
|
|
57
55
|
if (Boolean(context.options.dryRun)) {
|
|
58
56
|
printDryRunPreview(buildBoxAddDryRunPreview({
|
|
59
|
-
config,
|
|
60
|
-
offline,
|
|
61
|
-
...(await inspectAddBox(db, {
|
|
57
|
+
config: runtime.config,
|
|
58
|
+
offline: runtime.offline,
|
|
59
|
+
...(await inspectAddBox(runtime.db, {
|
|
62
60
|
name,
|
|
63
61
|
userId: identity.userId,
|
|
64
62
|
description,
|
|
@@ -67,27 +65,131 @@ async function runBoxAdd(context) {
|
|
|
67
65
|
}));
|
|
68
66
|
return;
|
|
69
67
|
}
|
|
70
|
-
const result = await addBox(db, {
|
|
68
|
+
const result = await addBox(runtime.db, {
|
|
71
69
|
name,
|
|
72
70
|
userId: identity.userId,
|
|
73
71
|
description,
|
|
74
72
|
color,
|
|
75
73
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
if (!offline) {
|
|
80
|
-
await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
|
|
81
|
-
}
|
|
82
|
-
const payload = { ok: true, stale: offline ? true : stale, ...result };
|
|
74
|
+
printUploadDisabledWarning(runtime);
|
|
75
|
+
await waitForMutationUpload(runtime);
|
|
76
|
+
const payload = { ok: true, stale: commandStale(runtime), ...result };
|
|
83
77
|
if (structuredOutput) {
|
|
84
78
|
printSuccess(payload);
|
|
85
79
|
return;
|
|
86
80
|
}
|
|
87
81
|
printHumanReadableLine(result.id);
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
printStaleCacheWarning(runtime, 'warning: sync timed out; results may be stale');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function withBoxWriteRuntime(context, run) {
|
|
86
|
+
return withCommandRuntime(context, async (runtime) => {
|
|
87
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
88
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
89
|
+
return run({ runtime, userId: identity.userId });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function requireArg(value, message) {
|
|
93
|
+
const trimmed = value?.trim() ?? '';
|
|
94
|
+
if (!trimmed) {
|
|
95
|
+
throw new DinoxError(message);
|
|
96
|
+
}
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
async function runBoxRename(context) {
|
|
100
|
+
const path = requireArg(context.args.path, 'Box path is required');
|
|
101
|
+
const newName = requireArg(context.args.newName, 'New box name is required');
|
|
102
|
+
await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
|
|
103
|
+
const result = await renameBox(runtime.db, { path, newName, userId }, { dryRun: Boolean(context.options.dryRun) });
|
|
104
|
+
if (Boolean(context.options.dryRun)) {
|
|
105
|
+
printDryRunPreview(buildBoxMutationDryRunPreview({
|
|
106
|
+
config: runtime.config,
|
|
107
|
+
offline: runtime.offline,
|
|
108
|
+
command: 'dino box rename',
|
|
109
|
+
action: 'rename_box',
|
|
110
|
+
risk: 'write',
|
|
111
|
+
summary: `Rename zettel box "${result.fromPath}" to "${result.toPath}".`,
|
|
112
|
+
inputs: { path, new_name: newName },
|
|
113
|
+
changes: { updated_boxes: result.updatedBoxes },
|
|
114
|
+
}));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
printUploadDisabledWarning(runtime);
|
|
118
|
+
await waitForMutationUpload(runtime);
|
|
119
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function runBoxMove(context) {
|
|
123
|
+
const path = requireArg(context.args.path, 'Box path is required');
|
|
124
|
+
const to = typeof context.options.to === 'string' ? context.options.to.trim() : '';
|
|
125
|
+
if (!to) {
|
|
126
|
+
throw new DinoxError('--to is required');
|
|
127
|
+
}
|
|
128
|
+
await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
|
|
129
|
+
const result = await moveBox(runtime.db, { path, to, userId }, { dryRun: Boolean(context.options.dryRun) });
|
|
130
|
+
if (Boolean(context.options.dryRun)) {
|
|
131
|
+
printDryRunPreview(buildBoxMutationDryRunPreview({
|
|
132
|
+
config: runtime.config,
|
|
133
|
+
offline: runtime.offline,
|
|
134
|
+
command: 'dino box move',
|
|
135
|
+
action: 'move_box',
|
|
136
|
+
risk: 'write',
|
|
137
|
+
summary: `Move zettel box "${result.fromPath}" to "${result.toPath}".`,
|
|
138
|
+
inputs: { path, to },
|
|
139
|
+
changes: { updated_boxes: result.updatedBoxes },
|
|
140
|
+
}));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
printUploadDisabledWarning(runtime);
|
|
144
|
+
await waitForMutationUpload(runtime);
|
|
145
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function runBoxMerge(context) {
|
|
149
|
+
const from = requireArg(context.args.from, 'Source box path is required');
|
|
150
|
+
const to = requireArg(context.args.to, 'Target box path is required');
|
|
151
|
+
await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
|
|
152
|
+
const result = await mergeBox(runtime.db, { from, to, userId }, { dryRun: Boolean(context.options.dryRun) });
|
|
153
|
+
if (Boolean(context.options.dryRun)) {
|
|
154
|
+
printDryRunPreview(buildBoxMutationDryRunPreview({
|
|
155
|
+
config: runtime.config,
|
|
156
|
+
offline: runtime.offline,
|
|
157
|
+
command: 'dino box merge',
|
|
158
|
+
action: 'merge_box',
|
|
159
|
+
risk: 'high-risk-write',
|
|
160
|
+
summary: `Merge zettel box subtree "${result.fromPath}" into "${result.toPath}".`,
|
|
161
|
+
inputs: { from, to },
|
|
162
|
+
changes: {
|
|
163
|
+
soft_deleted_box_ids: result.softDeletedBoxIds,
|
|
164
|
+
updated_notes: result.updatedNotes,
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
return;
|
|
90
168
|
}
|
|
169
|
+
printUploadDisabledWarning(runtime);
|
|
170
|
+
await waitForMutationUpload(runtime);
|
|
171
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function runBoxCleanup(context) {
|
|
175
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
176
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
177
|
+
await syncBeforeCommand(runtime, { syncTokenIndex: true });
|
|
178
|
+
const result = await inspectBoxCleanup(runtime.db, { userId: identity.userId });
|
|
179
|
+
if (Boolean(context.options.dryRun)) {
|
|
180
|
+
printDryRunPreview(buildBoxMutationDryRunPreview({
|
|
181
|
+
config: runtime.config,
|
|
182
|
+
offline: runtime.offline,
|
|
183
|
+
command: 'dino box cleanup',
|
|
184
|
+
action: 'cleanup_boxes',
|
|
185
|
+
risk: 'write',
|
|
186
|
+
summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
|
|
187
|
+
inputs: {},
|
|
188
|
+
targets: result.issues,
|
|
189
|
+
}));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
printSuccess({ stale: commandStale(runtime), ...result });
|
|
91
193
|
});
|
|
92
194
|
}
|
|
93
195
|
export const boxCommandGroup = {
|
|
@@ -105,7 +207,7 @@ export const boxCommandGroup = {
|
|
|
105
207
|
output: {
|
|
106
208
|
type: 'structured',
|
|
107
209
|
summary: 'Box list payload',
|
|
108
|
-
fields: ['id', 'name', 'description'],
|
|
210
|
+
fields: ['id', 'name', 'path', 'parentId', 'description', 'color'],
|
|
109
211
|
},
|
|
110
212
|
examples: ['dino box list --format json'],
|
|
111
213
|
},
|
|
@@ -113,10 +215,10 @@ export const boxCommandGroup = {
|
|
|
113
215
|
},
|
|
114
216
|
{
|
|
115
217
|
name: 'add',
|
|
116
|
-
description: 'Create a zettel box, restoring
|
|
117
|
-
arguments: [{ syntax: '[
|
|
218
|
+
description: 'Create a zettel box path, restoring deleted path nodes when possible',
|
|
219
|
+
arguments: [{ syntax: '[path]', description: 'Box path, for example Work/Inbox' }],
|
|
118
220
|
options: [
|
|
119
|
-
{ flags: '--name <string>', description: 'Box
|
|
221
|
+
{ flags: '--name <string>', description: 'Box path (alternative to positional path)' },
|
|
120
222
|
{ flags: '--description <string>', description: 'Box purpose/usage description' },
|
|
121
223
|
{ flags: '--color <string>', description: 'Box color' },
|
|
122
224
|
{ flags: '--dry-run', description: 'Preview the write without executing it' },
|
|
@@ -129,12 +231,130 @@ export const boxCommandGroup = {
|
|
|
129
231
|
output: {
|
|
130
232
|
type: 'structured',
|
|
131
233
|
summary: 'Box creation acknowledgement',
|
|
132
|
-
fields: ['id', 'name', 'description', 'color', 'restored', 'stale'],
|
|
234
|
+
fields: ['id', 'name', 'path', 'parentId', 'description', 'color', 'restored', 'createdNodes', 'restoredNodes', 'stale'],
|
|
133
235
|
},
|
|
134
|
-
examples: ['dino box add "Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
|
|
236
|
+
examples: ['dino box add "Work/Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
|
|
135
237
|
},
|
|
136
238
|
action: runBoxAdd,
|
|
137
239
|
},
|
|
240
|
+
{
|
|
241
|
+
name: 'tree',
|
|
242
|
+
description: 'Show zettel boxes as a hierarchy tree',
|
|
243
|
+
schema: {
|
|
244
|
+
path: 'box.tree',
|
|
245
|
+
risk: 'read',
|
|
246
|
+
auth: 'required',
|
|
247
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
248
|
+
output: {
|
|
249
|
+
type: 'structured',
|
|
250
|
+
summary: 'Nested zettel box tree payload',
|
|
251
|
+
fields: ['tree.id', 'tree.name', 'tree.path', 'tree.parentId', 'tree.children', 'stale'],
|
|
252
|
+
},
|
|
253
|
+
examples: ['dino box tree --format json'],
|
|
254
|
+
},
|
|
255
|
+
action: runBoxTree,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'stats',
|
|
259
|
+
description: 'Show zettel box note counts and empty-box status',
|
|
260
|
+
schema: {
|
|
261
|
+
path: 'box.stats',
|
|
262
|
+
risk: 'read',
|
|
263
|
+
auth: 'required',
|
|
264
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
265
|
+
output: {
|
|
266
|
+
type: 'structured',
|
|
267
|
+
summary: 'Zettel box statistics payload',
|
|
268
|
+
fields: ['totalBoxes', 'emptyBoxes', 'boxes.id', 'boxes.path', 'boxes.directNoteCount', 'boxes.descendantNoteCount', 'boxes.latestNoteUpdatedAt', 'boxes.isEmpty', 'stale'],
|
|
269
|
+
},
|
|
270
|
+
examples: ['dino box stats --format json'],
|
|
271
|
+
},
|
|
272
|
+
action: runBoxStats,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'rename',
|
|
276
|
+
description: 'Rename a zettel box and cascade descendant paths',
|
|
277
|
+
arguments: [
|
|
278
|
+
{ syntax: '<path>', description: 'Existing box path' },
|
|
279
|
+
{ syntax: '<new-name>', description: 'New leaf name' },
|
|
280
|
+
],
|
|
281
|
+
options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
|
|
282
|
+
schema: {
|
|
283
|
+
path: 'box.rename',
|
|
284
|
+
risk: 'write',
|
|
285
|
+
auth: 'required',
|
|
286
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
287
|
+
output: {
|
|
288
|
+
type: 'structured',
|
|
289
|
+
summary: 'Zettel box rename result',
|
|
290
|
+
fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
|
|
291
|
+
},
|
|
292
|
+
examples: ['dino box rename "Work/Inbox" "Ideas" --format json --dry-run'],
|
|
293
|
+
},
|
|
294
|
+
action: runBoxRename,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'move',
|
|
298
|
+
description: 'Move a zettel box under another parent and cascade descendant paths',
|
|
299
|
+
arguments: [{ syntax: '<path>', description: 'Existing box path' }],
|
|
300
|
+
options: [
|
|
301
|
+
{ flags: '--to <parent-path>', description: 'Target parent box path', required: true, type: 'string' },
|
|
302
|
+
{ flags: '--dry-run', description: 'Preview the write without executing it' },
|
|
303
|
+
],
|
|
304
|
+
schema: {
|
|
305
|
+
path: 'box.move',
|
|
306
|
+
risk: 'write',
|
|
307
|
+
auth: 'required',
|
|
308
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
309
|
+
output: {
|
|
310
|
+
type: 'structured',
|
|
311
|
+
summary: 'Zettel box move result',
|
|
312
|
+
fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
|
|
313
|
+
},
|
|
314
|
+
examples: ['dino box move "Inbox" --to "Work" --format json --dry-run'],
|
|
315
|
+
},
|
|
316
|
+
action: runBoxMove,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'merge',
|
|
320
|
+
description: 'Merge a zettel box subtree into another box and soft-delete the source subtree',
|
|
321
|
+
arguments: [
|
|
322
|
+
{ syntax: '<from>', description: 'Source box path' },
|
|
323
|
+
{ syntax: '<to>', description: 'Target box path' },
|
|
324
|
+
],
|
|
325
|
+
options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
|
|
326
|
+
schema: {
|
|
327
|
+
path: 'box.merge',
|
|
328
|
+
risk: 'high-risk-write',
|
|
329
|
+
auth: 'required',
|
|
330
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
331
|
+
output: {
|
|
332
|
+
type: 'structured',
|
|
333
|
+
summary: 'Zettel box merge result',
|
|
334
|
+
fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'stale'],
|
|
335
|
+
},
|
|
336
|
+
examples: ['dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run'],
|
|
337
|
+
},
|
|
338
|
+
action: runBoxMerge,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'cleanup',
|
|
342
|
+
description: 'Inspect zettel box cleanup candidates',
|
|
343
|
+
options: [{ flags: '--dry-run', description: 'Preview cleanup candidates without writing' }],
|
|
344
|
+
schema: {
|
|
345
|
+
path: 'box.cleanup',
|
|
346
|
+
risk: 'read',
|
|
347
|
+
auth: 'required',
|
|
348
|
+
globalOptions: BOX_GLOBAL_OPTIONS,
|
|
349
|
+
output: {
|
|
350
|
+
type: 'structured',
|
|
351
|
+
summary: 'Zettel box cleanup inspection payload',
|
|
352
|
+
fields: ['issues', 'emptyBoxes', 'duplicatePaths', 'orphanParents', 'deletedWithActiveChildren', 'stale'],
|
|
353
|
+
},
|
|
354
|
+
examples: ['dino box cleanup --format json --dry-run'],
|
|
355
|
+
},
|
|
356
|
+
action: runBoxCleanup,
|
|
357
|
+
},
|
|
138
358
|
],
|
|
139
359
|
};
|
|
140
360
|
export function registerBoxesCommand(program) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const MAX_BOX_NAME_LENGTH = 500;
|
|
2
|
+
export declare const MAX_BOX_PATH_LENGTH = 2000;
|
|
3
|
+
export declare const MAX_BOX_DESCRIPTION_LENGTH = 200000;
|
|
4
|
+
export declare const MAX_BOX_COLOR_LENGTH = 200;
|
|
5
|
+
export type BoxListItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
parentId: string | null;
|
|
10
|
+
description: string;
|
|
11
|
+
color: string;
|
|
12
|
+
};
|
|
13
|
+
export type MutableBoxNode = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
path: string;
|
|
17
|
+
parentId: string | null;
|
|
18
|
+
description: string;
|
|
19
|
+
color: string;
|
|
20
|
+
isDel: number | null;
|
|
21
|
+
userNo: string;
|
|
22
|
+
};
|
|
23
|
+
export type TableInfoRow = {
|
|
24
|
+
name: string | null;
|
|
25
|
+
};
|
|
26
|
+
export declare const REQUIRED_ZETTEL_BOX_COLUMNS: string[];
|
|
27
|
+
export declare const REQUIRED_NOTE_BOX_COLUMNS: string[];
|
|
28
|
+
export declare function canonicalizeBoxPath(value: string): string;
|
|
29
|
+
export declare function normalizeBoxLookupKey(value: string): string;
|
|
30
|
+
export declare function trimString(value: string | null | undefined): string;
|
|
31
|
+
export declare function toNullableId(value: string | null | undefined): string | null;
|
|
32
|
+
export declare function getLeafName(path: string): string;
|
|
33
|
+
export declare function assertMaxLength(label: string, value: string, maxLength: number): void;
|
|
34
|
+
export declare function parseStoredBoxIds(raw: string | null): string[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const MAX_BOX_NAME_LENGTH = 500;
|
|
2
|
+
export const MAX_BOX_PATH_LENGTH = 2000;
|
|
3
|
+
export const MAX_BOX_DESCRIPTION_LENGTH = 200_000;
|
|
4
|
+
export const MAX_BOX_COLOR_LENGTH = 200;
|
|
5
|
+
export const REQUIRED_ZETTEL_BOX_COLUMNS = [
|
|
6
|
+
'id',
|
|
7
|
+
'name',
|
|
8
|
+
'path',
|
|
9
|
+
'parent_id',
|
|
10
|
+
'description',
|
|
11
|
+
'color',
|
|
12
|
+
'priority',
|
|
13
|
+
'is_show',
|
|
14
|
+
'cover_url',
|
|
15
|
+
'created_at',
|
|
16
|
+
'updated_at',
|
|
17
|
+
'extra_data',
|
|
18
|
+
'user_no',
|
|
19
|
+
'is_del',
|
|
20
|
+
];
|
|
21
|
+
export const REQUIRED_NOTE_BOX_COLUMNS = [
|
|
22
|
+
'id',
|
|
23
|
+
'zettel_boxes',
|
|
24
|
+
'updated_at',
|
|
25
|
+
'version',
|
|
26
|
+
'is_del',
|
|
27
|
+
];
|
|
28
|
+
export function canonicalizeBoxPath(value) {
|
|
29
|
+
return value
|
|
30
|
+
.split(/[\\/]+/g)
|
|
31
|
+
.map((segment) => segment.trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join('/');
|
|
34
|
+
}
|
|
35
|
+
export function normalizeBoxLookupKey(value) {
|
|
36
|
+
return canonicalizeBoxPath(value).toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
export function trimString(value) {
|
|
39
|
+
return value?.trim() ?? '';
|
|
40
|
+
}
|
|
41
|
+
export function toNullableId(value) {
|
|
42
|
+
const trimmed = trimString(value);
|
|
43
|
+
return trimmed ? trimmed : null;
|
|
44
|
+
}
|
|
45
|
+
export function getLeafName(path) {
|
|
46
|
+
return path.split('/').at(-1) ?? path;
|
|
47
|
+
}
|
|
48
|
+
export function assertMaxLength(label, value, maxLength) {
|
|
49
|
+
if (value.length > maxLength) {
|
|
50
|
+
throw new DinoxError(`${label} must be at most ${maxLength} characters`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function parseStoredBoxIds(raw) {
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (!Array.isArray(parsed)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const result = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const value of parsed) {
|
|
65
|
+
const normalized = String(value).trim();
|
|
66
|
+
if (!normalized || seen.has(normalized)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
seen.add(normalized);
|
|
70
|
+
result.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import type { BoxListItem } from './model.js';
|
|
3
|
+
export type BoxTreeNode = BoxListItem & {
|
|
4
|
+
children: BoxTreeNode[];
|
|
5
|
+
};
|
|
6
|
+
export type BoxStatsItem = BoxListItem & {
|
|
7
|
+
directNoteCount: number;
|
|
8
|
+
descendantNoteCount: number;
|
|
9
|
+
latestNoteUpdatedAt: string | null;
|
|
10
|
+
isEmpty: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type BoxStatsResult = {
|
|
13
|
+
totalBoxes: number;
|
|
14
|
+
emptyBoxes: number;
|
|
15
|
+
boxes: BoxStatsItem[];
|
|
16
|
+
};
|
|
17
|
+
export type BoxCleanupIssue = {
|
|
18
|
+
type: 'empty_box' | 'duplicate_path' | 'orphan_parent' | 'deleted_with_active_children';
|
|
19
|
+
id: string;
|
|
20
|
+
path: string;
|
|
21
|
+
name: string;
|
|
22
|
+
parentId: string | null;
|
|
23
|
+
detail: string;
|
|
24
|
+
duplicateIds?: string[];
|
|
25
|
+
childIds?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type BoxCleanupResult = {
|
|
28
|
+
issues: BoxCleanupIssue[];
|
|
29
|
+
emptyBoxes: number;
|
|
30
|
+
duplicatePaths: number;
|
|
31
|
+
orphanParents: number;
|
|
32
|
+
deletedWithActiveChildren: number;
|
|
33
|
+
};
|
|
34
|
+
export type BoxRenameInput = {
|
|
35
|
+
path: string;
|
|
36
|
+
newName: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
};
|
|
39
|
+
export type BoxMoveInput = {
|
|
40
|
+
path: string;
|
|
41
|
+
to: string;
|
|
42
|
+
userId: string;
|
|
43
|
+
};
|
|
44
|
+
export type BoxMergeInput = {
|
|
45
|
+
from: string;
|
|
46
|
+
to: string;
|
|
47
|
+
userId: string;
|
|
48
|
+
};
|
|
49
|
+
export type BoxReadInput = {
|
|
50
|
+
userId?: string;
|
|
51
|
+
};
|
|
52
|
+
export type BoxSubtreeChange = {
|
|
53
|
+
id: string;
|
|
54
|
+
fromPath: string;
|
|
55
|
+
toPath: string;
|
|
56
|
+
};
|
|
57
|
+
export type BoxRenameMoveResult = {
|
|
58
|
+
id: string;
|
|
59
|
+
fromPath: string;
|
|
60
|
+
toPath: string;
|
|
61
|
+
updatedBoxes: BoxSubtreeChange[];
|
|
62
|
+
};
|
|
63
|
+
export type BoxMergeResult = {
|
|
64
|
+
fromId: string;
|
|
65
|
+
toId: string;
|
|
66
|
+
fromPath: string;
|
|
67
|
+
toPath: string;
|
|
68
|
+
softDeletedBoxIds: string[];
|
|
69
|
+
updatedNotes: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
before: string[];
|
|
72
|
+
after: string[];
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
export declare function getBoxTree(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxTreeNode[]>;
|
|
76
|
+
export declare function getBoxStats(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxStatsResult>;
|
|
77
|
+
export declare function inspectBoxCleanup(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxCleanupResult>;
|
|
78
|
+
export declare function renameBox(db: AbstractPowerSyncDatabase, input: BoxRenameInput, options?: {
|
|
79
|
+
dryRun?: boolean;
|
|
80
|
+
}): Promise<BoxRenameMoveResult>;
|
|
81
|
+
export declare function moveBox(db: AbstractPowerSyncDatabase, input: BoxMoveInput, options?: {
|
|
82
|
+
dryRun?: boolean;
|
|
83
|
+
}): Promise<BoxRenameMoveResult>;
|
|
84
|
+
export declare function mergeBox(db: AbstractPowerSyncDatabase, input: BoxMergeInput, options?: {
|
|
85
|
+
dryRun?: boolean;
|
|
86
|
+
}): Promise<BoxMergeResult>;
|