@dinoxx/dinox-cli 1.1.1 → 2.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.
Files changed (141) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo/read.d.ts +8 -0
  19. package/dist/commands/notes/repo/read.js +321 -0
  20. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  21. package/dist/commands/notes/repo/resolve.js +187 -0
  22. package/dist/commands/notes/repo/search.d.ts +42 -0
  23. package/dist/commands/notes/repo/search.js +513 -0
  24. package/dist/commands/notes/repo/shared.d.ts +190 -0
  25. package/dist/commands/notes/repo/shared.js +102 -0
  26. package/dist/commands/notes/repo/write.d.ts +24 -0
  27. package/dist/commands/notes/repo/write.js +251 -0
  28. package/dist/commands/notes/repo.d.ts +7 -177
  29. package/dist/commands/notes/repo.js +7 -1310
  30. package/dist/commands/notes/searchOutput.d.ts +2 -1
  31. package/dist/commands/notes/searchOutput.js +2 -1
  32. package/dist/commands/notes/searchSql.js +9 -3
  33. package/dist/commands/prompt/index.d.ts +1 -0
  34. package/dist/commands/prompt/index.js +26 -11
  35. package/dist/commands/savedViews/index.d.ts +7 -0
  36. package/dist/commands/savedViews/index.js +458 -0
  37. package/dist/commands/savedViews/model.d.ts +87 -0
  38. package/dist/commands/savedViews/model.js +306 -0
  39. package/dist/commands/savedViews/repo.d.ts +123 -0
  40. package/dist/commands/savedViews/repo.js +512 -0
  41. package/dist/commands/savedViews/semantics.d.ts +46 -0
  42. package/dist/commands/savedViews/semantics.js +738 -0
  43. package/dist/commands/schema/commandMetadata.js +4 -4
  44. package/dist/commands/schema/index.d.ts +1 -0
  45. package/dist/commands/schema/index.js +23 -2
  46. package/dist/commands/schema/renderSkills.d.ts +6 -0
  47. package/dist/commands/schema/renderSkills.js +109 -11
  48. package/dist/commands/schema/spec.js +4 -0
  49. package/dist/commands/skills/index.d.ts +80 -0
  50. package/dist/commands/skills/index.js +572 -0
  51. package/dist/commands/storage/index.js +2 -2
  52. package/dist/commands/storage/repo/configs.d.ts +30 -0
  53. package/dist/commands/storage/repo/configs.js +259 -0
  54. package/dist/commands/storage/repo/operations.d.ts +25 -0
  55. package/dist/commands/storage/repo/operations.js +337 -0
  56. package/dist/commands/storage/repo/remote.d.ts +32 -0
  57. package/dist/commands/storage/repo/remote.js +132 -0
  58. package/dist/commands/storage/repo/resources.d.ts +12 -0
  59. package/dist/commands/storage/repo/resources.js +134 -0
  60. package/dist/commands/storage/repo/shared.d.ts +181 -0
  61. package/dist/commands/storage/repo/shared.js +36 -0
  62. package/dist/commands/storage/repo/targets.d.ts +12 -0
  63. package/dist/commands/storage/repo/targets.js +178 -0
  64. package/dist/commands/storage/repo.d.ts +3 -166
  65. package/dist/commands/storage/repo.js +4 -1034
  66. package/dist/commands/sync.js +2 -1
  67. package/dist/commands/tags/index.js +30 -11
  68. package/dist/commands/tags/operations.d.ts +3 -3
  69. package/dist/commands/tags/operations.js +45 -20
  70. package/dist/commands/templates/model.d.ts +50 -0
  71. package/dist/commands/templates/model.js +239 -0
  72. package/dist/commands/todo/index.js +3 -3
  73. package/dist/commands/update.d.ts +1 -0
  74. package/dist/commands/update.js +46 -9
  75. package/dist/commands/writeConfirmation.d.ts +13 -0
  76. package/dist/commands/writeConfirmation.js +54 -0
  77. package/dist/config/resolve.js +5 -10
  78. package/dist/config/types.d.ts +3 -6
  79. package/dist/constants/links.d.ts +18 -1
  80. package/dist/constants/links.js +26 -1
  81. package/dist/contract/snapshot.d.ts +30 -0
  82. package/dist/contract/snapshot.js +114 -0
  83. package/dist/daemon/client.d.ts +2 -2
  84. package/dist/daemon/client.js +108 -54
  85. package/dist/daemon/index.d.ts +5 -3
  86. package/dist/daemon/index.js +28 -5
  87. package/dist/daemon/lifecycle.d.ts +7 -2
  88. package/dist/daemon/lifecycle.js +53 -13
  89. package/dist/daemon/pid.d.ts +1 -1
  90. package/dist/daemon/pid.js +6 -3
  91. package/dist/dinox.js +9 -1
  92. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  93. package/dist/powersync/exclusiveRuntime.js +26 -8
  94. package/dist/powersync/runtime.d.ts +3 -2
  95. package/dist/powersync/runtime.js +19 -3
  96. package/dist/powersync/schema/content.d.ts +1 -78
  97. package/dist/powersync/schema/content.js +1 -78
  98. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  99. package/dist/powersync/schema/electronCanonical.js +1154 -0
  100. package/dist/powersync/schema/index.d.ts +573 -129
  101. package/dist/powersync/schema/index.js +3 -32
  102. package/dist/powersync/schema/local.d.ts +1 -56
  103. package/dist/powersync/schema/local.js +1 -60
  104. package/dist/powersync/schema/note.d.ts +1 -35
  105. package/dist/powersync/schema/note.js +1 -35
  106. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  107. package/dist/powersync/schema/notesExtras.js +1 -71
  108. package/dist/powersync/schema/projects.d.ts +1 -104
  109. package/dist/powersync/schema/projects.js +1 -104
  110. package/dist/powersync/schema/savedViews.d.ts +1 -0
  111. package/dist/powersync/schema/savedViews.js +1 -0
  112. package/dist/powersync/schema/storage.d.ts +1 -24
  113. package/dist/powersync/schema/storage.js +1 -24
  114. package/dist/powersync/schema/tags.d.ts +1 -37
  115. package/dist/powersync/schema/tags.js +1 -37
  116. package/dist/powersync/syncGate.d.ts +20 -2
  117. package/dist/powersync/uploader.d.ts +16 -2
  118. package/dist/powersync/uploader.js +152 -158
  119. package/dist/skills/check.d.ts +1 -0
  120. package/dist/skills/check.js +538 -70
  121. package/dist/skills/commandRefs.d.ts +21 -0
  122. package/dist/skills/commandRefs.js +132 -0
  123. package/dist/skills/generate.d.ts +13 -0
  124. package/dist/skills/generate.js +80 -0
  125. package/dist/skills/registry.d.ts +13 -0
  126. package/dist/skills/registry.js +58 -0
  127. package/dist/skills/retired.d.ts +60 -0
  128. package/dist/skills/retired.js +58 -0
  129. package/dist/utils/deadline.d.ts +8 -0
  130. package/dist/utils/deadline.js +31 -0
  131. package/dist/utils/dryRun.d.ts +20 -1
  132. package/dist/utils/dryRun.js +22 -3
  133. package/dist/utils/errorCodes.d.ts +311 -0
  134. package/dist/utils/errorCodes.js +92 -0
  135. package/dist/utils/errors.d.ts +15 -2
  136. package/dist/utils/errors.js +13 -0
  137. package/dist/utils/processExit.d.ts +1 -0
  138. package/dist/utils/processExit.js +33 -0
  139. package/dist/utils/updateNotice.d.ts +1 -0
  140. package/dist/utils/updateNotice.js +1 -1
  141. package/package.json +18 -8
package/README.md CHANGED
@@ -341,9 +341,11 @@ dino note get <note-id>
341
341
  #### 只拿轻量上下文(适合 AI / 脚本)
342
342
 
343
343
  ```bash
344
- dino note get <note-id> --context-only --format json
344
+ dino note get <note-id> --format json
345
345
  ```
346
346
 
347
+ `note get` 在 2.0 中始终是轻量读取;`--context-only` 仅保留为兼容参数,不会改变输出。
348
+
347
349
  #### 预览正文前几行
348
350
 
349
351
  ```bash
@@ -450,11 +452,14 @@ dino note bulk --days 30 --boxes "Work/Inbox" --box-replace "Archive/Review" --e
450
452
  #### `note patch`:结构化编辑正文
451
453
 
452
454
  ```bash
455
+ dino note patch <note-id> --append-section "复盘" --content @./section.md --format json --dry-run
453
456
  dino note content-read <note-id> --format json
454
- dino note patch <note-id> --append-section "复盘" --content @./section.md --read-token <token> --format json
457
+ dino note patch <note-id> --append-section "复盘" --content @./section.md --read-token @/tmp/dinox-read-token-<random> --format json
455
458
  ```
456
459
 
457
- `note patch` 只用于正文结构化编辑。真实写入前必须先 `content-read`,并传回 `readToken`;token 短期有效(当前为 30 分钟,以 `readTokenExpiresAt` 为准),绑定生成它时当前账号对应的本地 PowerSync 数据库作用域,不能在切换账号或本地数据库后复用。token 也是单次使用的,任何真实 patch 尝试都会先消费它,失败后重试必须重新执行 `content-read`。替换媒体、表格、未知节点等受保护 block 时,还需要显式 `--allow-protected-replace`。
460
+ `note patch` 只用于正文结构化编辑。先 dry-run,再在真实写入前执行 `content-read` 并传回 `readToken`。默认 `content-read` 只返回 block 类型、范围、hash 和字符数,不返回 block 文本、预览或 attributes,同时返回 `readTokenScope: "structural-metadata"`。只有用户授权全文读取时才添加 `--include-content`;此时返回 `readTokenScope: "full-content"`。替换媒体、表格、容器、未知节点等受保护 block 时,必须使用已审阅全文后取得的 `full-content` token,并显式添加 `--allow-protected-replace`;默认 scope 会被拒绝。
461
+
462
+ token 短期有效(当前为 30 分钟,以 `readTokenExpiresAt` 为准),绑定生成它时当前账号对应的本地 PowerSync 数据库作用域,不能在切换账号或本地数据库后复用。token 也是单次使用的,任何真实 patch 尝试都会先消费它,失败后重试必须重新执行 `content-read`。Agent 应把 token 写入唯一、仅 owner 可读写(mode `0600`)的临时文件,通过 `--read-token @<file>` 传入,并在这一次真实 patch 尝试后立即删除;不要把 token 放进命令参数、日志、笔记或可复用文件。
458
463
 
459
464
  #### 收藏状态
460
465
 
@@ -567,6 +572,19 @@ dino tag add --name "工作/项目A" --emoji "🧠"
567
572
  - 支持层级标签,如 `工作/项目A`
568
573
  - 已删除但同名的标签,CLI 会尽量做恢复而不是重复新建
569
574
 
575
+ #### 查看、调整和合并标签层级
576
+
577
+ ```bash
578
+ dino tag tree --format json
579
+ dino tag stats --format json
580
+ dino tag rename "工作/旧项目" "新项目" --format json --dry-run
581
+ dino tag move "工作/新项目" --to "归档" --format json --dry-run
582
+ dino tag merge "重复标签" "规范标签" --format json --dry-run
583
+ dino tag merge "重复标签" "规范标签" --expected-count <affected-count> --confirm --format json
584
+ ```
585
+
586
+ `tag merge` 会更新笔记引用并软删除来源子树。真实写入必须使用新鲜 dry-run 返回的 `data.changes.affected_count`,同时提供 `--expected-count` 和 `--confirm`。
587
+
570
588
  ### 5.8 管理卡片盒
571
589
 
572
590
  #### 查看全部卡片盒
@@ -587,6 +605,19 @@ dino box add --name "Inbox" --description "用于存放待整理的想法和资
587
605
  - `--description` 很值得写
588
606
  - 这段说明会帮助 AI 和你自己更准确地理解这个卡片盒的用途
589
607
 
608
+ #### 查看、调整和合并卡片盒层级
609
+
610
+ ```bash
611
+ dino box tree --format json
612
+ dino box stats --format json
613
+ dino box rename "工作/Inbox" "待整理" --format json --dry-run
614
+ dino box move "工作/待整理" --to "归档" --format json --dry-run
615
+ dino box merge "工作/重复" "工作/规范" --format json --dry-run
616
+ dino box merge "工作/重复" "工作/规范" --expected-count <affected-count> --confirm --format json
617
+ ```
618
+
619
+ `box merge` 会更新笔记归属并软删除来源子树,同样要求新鲜 dry-run、`--expected-count` 和 `--confirm`。
620
+
590
621
  ### 5.9 管理 Prompt
591
622
 
592
623
  #### 查看全部 Prompt
@@ -599,6 +630,7 @@ dino prompt list
599
630
 
600
631
  ```bash
601
632
  dino prompt add --name "周报助手" --prompt "请基于本周笔记输出一份简洁周报"
633
+ dino prompt add --name "周报助手" --prompt @./weekly-prompt.txt
602
634
  ```
603
635
 
604
636
  ### 5.10 查看版本与配置
@@ -933,6 +965,15 @@ dino todo search --status uncompleted --format json
933
965
 
934
966
  `--format json` 会返回稳定的结构化 envelope,适合被脚本、自动化任务和 AI Agent 解析。
935
967
 
968
+ 2.0 对几个读命令统一了 freshness envelope,升级脚本时需要迁移:
969
+
970
+ - `dino note get` 只返回轻量 `data.note`,不再默认暴露正文。
971
+ - `dino note detail` 始终返回 `data.stale`、`data.count` 和 `data.notes`,即使只查询一条笔记。
972
+ - `dino prompt list` 从 `data.prompts` 读取,`dino box list` 从 `data.boxes` 读取;两者同时返回 `data.stale` 和 `data.count`。
973
+ - 所有 structured dry-run 都会返回 `data.stale` 和 `data.sync.gate`,用于判断预览是否建立在新鲜数据上。
974
+
975
+ Agent 调用在线命令时,只要 schema 提供 `--sync-timeout`,建议传入有界预算(例如 `20000`),并把宿主工具超时设为高 5–10 秒。宿主先超时但没有收到 structured envelope 时,结果是未知,不要直接重试写入;先检查新鲜度或写入后置条件。
976
+
936
977
  ### 7.2 先读取命令 schema
937
978
 
938
979
  不确定某个命令支持哪些参数、输出哪些字段时,先读取 schema:
package/dist/cli.js CHANGED
@@ -10,6 +10,8 @@ import { registerMetaCommands } from './commands/meta/index.js';
10
10
  import { registerNoteCommands } from './commands/notes/index.js';
11
11
  import { registerPromptCommands } from './commands/prompt/index.js';
12
12
  import { registerSchemaCommands } from './commands/schema/index.js';
13
+ import { registerSavedViewCommands } from './commands/savedViews/index.js';
14
+ import { registerSkillsCommands } from './commands/skills/index.js';
13
15
  import { registerSyncCommand } from './commands/sync.js';
14
16
  import { registerStorageCommands } from './commands/storage/index.js';
15
17
  import { registerUpdateCommand } from './commands/update.js';
@@ -82,7 +84,9 @@ export function createCli() {
82
84
  registerSyncCommand(program);
83
85
  registerUpdateCommand(program);
84
86
  registerPromptCommands(program);
87
+ registerSavedViewCommands(program);
85
88
  registerSchemaCommands(program);
89
+ registerSkillsCommands(program);
86
90
  registerStorageCommands(program);
87
91
  registerTagsCommand(program);
88
92
  registerTodoCommands(program);
@@ -430,8 +430,7 @@ async function runAuthStatus(context) {
430
430
  endpoint: resolved.powersync.endpoint ?? null,
431
431
  tokenEndpoint: resolved.powersync.tokenEndpoint ?? null,
432
432
  uploadBaseUrl: resolved.powersync.uploadBaseUrl ?? null,
433
- uploadV4Path: resolved.powersync.uploadV4Path,
434
- uploadV2Path: resolved.powersync.uploadV2Path,
433
+ uploadPath: resolved.powersync.uploadPath,
435
434
  },
436
435
  dbPath,
437
436
  };
@@ -6,6 +6,7 @@ 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
8
  import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
9
+ import { assertWriteConfirmationProvided, parseExpectedCount } from '../writeConfirmation.js';
9
10
  import { addBox, inspectAddBox, listBoxes } from './repo.js';
10
11
  import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
11
12
  const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -13,7 +14,12 @@ async function runBoxList(context) {
13
14
  await withCommandRuntime(context, async (runtime) => {
14
15
  const identity = await resolveAuthIdentity(runtime.config);
15
16
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
16
- printSuccess(await listBoxes(runtime.db, { userId: identity.userId }));
17
+ const boxes = await listBoxes(runtime.db, { userId: identity.userId });
18
+ printSuccess({
19
+ stale: commandStale(runtime),
20
+ count: boxes.length,
21
+ boxes,
22
+ });
17
23
  });
18
24
  }
19
25
  async function runBoxTree(context) {
@@ -62,7 +68,7 @@ async function runBoxAdd(context) {
62
68
  description,
63
69
  color,
64
70
  })),
65
- }));
71
+ }), runtime);
66
72
  return;
67
73
  }
68
74
  const result = await addBox(runtime.db, {
@@ -110,7 +116,7 @@ async function runBoxRename(context) {
110
116
  summary: `Rename zettel box "${result.fromPath}" to "${result.toPath}".`,
111
117
  inputs: { path, new_name: newName },
112
118
  changes: { updated_boxes: result.updatedBoxes },
113
- }));
119
+ }), runtime);
114
120
  return;
115
121
  }
116
122
  await waitForMutationUpload(runtime);
@@ -135,7 +141,7 @@ async function runBoxMove(context) {
135
141
  summary: `Move zettel box "${result.fromPath}" to "${result.toPath}".`,
136
142
  inputs: { path, to },
137
143
  changes: { updated_boxes: result.updatedBoxes },
138
- }));
144
+ }), runtime);
139
145
  return;
140
146
  }
141
147
  await waitForMutationUpload(runtime);
@@ -145,9 +151,17 @@ async function runBoxMove(context) {
145
151
  async function runBoxMerge(context) {
146
152
  const from = requireArg(context.args.from, 'Source box path is required');
147
153
  const to = requireArg(context.args.to, 'Target box path is required');
154
+ const dryRun = Boolean(context.options.dryRun);
155
+ const expectedCount = parseExpectedCount(context.options.expectedCount);
156
+ const writeOptions = {
157
+ dryRun,
158
+ confirm: Boolean(context.options.confirm),
159
+ expectedCount,
160
+ };
161
+ assertWriteConfirmationProvided('Box merge', writeOptions);
148
162
  await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
149
- const result = await mergeBox(runtime.db, { from, to, userId }, { dryRun: Boolean(context.options.dryRun) });
150
- if (Boolean(context.options.dryRun)) {
163
+ const result = await mergeBox(runtime.db, { from, to, userId }, writeOptions);
164
+ if (dryRun) {
151
165
  printDryRunPreview(buildBoxMutationDryRunPreview({
152
166
  config: runtime.config,
153
167
  offline: runtime.offline,
@@ -155,12 +169,15 @@ async function runBoxMerge(context) {
155
169
  action: 'merge_box',
156
170
  risk: 'high-risk-write',
157
171
  summary: `Merge zettel box subtree "${result.fromPath}" into "${result.toPath}".`,
158
- inputs: { from, to },
172
+ inputs: { from, to, expected_count: expectedCount },
159
173
  changes: {
174
+ affected_count: result.affectedCount,
175
+ soft_deleted_box_count: result.softDeletedBoxIds.length,
176
+ updated_note_count: result.updatedNotes.length,
160
177
  soft_deleted_box_ids: result.softDeletedBoxIds,
161
178
  updated_notes: result.updatedNotes,
162
179
  },
163
- }));
180
+ }), runtime);
164
181
  return;
165
182
  }
166
183
  await waitForMutationUpload(runtime);
@@ -182,7 +199,7 @@ async function runBoxCleanup(context) {
182
199
  summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
183
200
  inputs: {},
184
201
  targets: result.issues,
185
- }));
202
+ }), runtime);
186
203
  return;
187
204
  }
188
205
  printSuccess({ stale: commandStale(runtime), ...result });
@@ -203,7 +220,7 @@ export const boxCommandGroup = {
203
220
  output: {
204
221
  type: 'structured',
205
222
  summary: 'Box list payload',
206
- fields: ['id', 'name', 'path', 'parentId', 'description', 'color'],
223
+ fields: ['stale', 'count', 'boxes.id', 'boxes.name', 'boxes.path', 'boxes.parentId', 'boxes.description', 'boxes.color'],
207
224
  },
208
225
  examples: ['dino box list --format json'],
209
226
  },
@@ -318,7 +335,11 @@ export const boxCommandGroup = {
318
335
  { syntax: '<from>', description: 'Source box path' },
319
336
  { syntax: '<to>', description: 'Target box path' },
320
337
  ],
321
- options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
338
+ options: [
339
+ { flags: '--expected-count <n>', description: 'Required for real writes; must equal source boxes plus notes affected', type: 'number' },
340
+ { flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
341
+ { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
342
+ ],
322
343
  schema: {
323
344
  path: 'box.merge',
324
345
  risk: 'high-risk-write',
@@ -327,9 +348,12 @@ export const boxCommandGroup = {
327
348
  output: {
328
349
  type: 'structured',
329
350
  summary: 'Zettel box merge result',
330
- fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'stale'],
351
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'affectedCount', 'stale'],
331
352
  },
332
- examples: ['dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run'],
353
+ examples: [
354
+ 'dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run',
355
+ 'dino box merge "Work/Inbox" "Work/Archive" --expected-count 6 --confirm --format json',
356
+ ],
333
357
  },
334
358
  action: runBoxMerge,
335
359
  },
@@ -1,4 +1,5 @@
1
1
  import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import { type ConfirmedWriteOptions } from '../writeConfirmation.js';
2
3
  import type { BoxListItem } from './model.js';
3
4
  export type BoxTreeNode = BoxListItem & {
4
5
  children: BoxTreeNode[];
@@ -71,6 +72,7 @@ export type BoxMergeResult = {
71
72
  before: string[];
72
73
  after: string[];
73
74
  }>;
75
+ affectedCount: number;
74
76
  };
75
77
  export declare function getBoxTree(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxTreeNode[]>;
76
78
  export declare function getBoxStats(db: AbstractPowerSyncDatabase, input?: BoxReadInput): Promise<BoxStatsResult>;
@@ -81,6 +83,4 @@ export declare function renameBox(db: AbstractPowerSyncDatabase, input: BoxRenam
81
83
  export declare function moveBox(db: AbstractPowerSyncDatabase, input: BoxMoveInput, options?: {
82
84
  dryRun?: boolean;
83
85
  }): Promise<BoxRenameMoveResult>;
84
- export declare function mergeBox(db: AbstractPowerSyncDatabase, input: BoxMergeInput, options?: {
85
- dryRun?: boolean;
86
- }): Promise<BoxMergeResult>;
86
+ export declare function mergeBox(db: AbstractPowerSyncDatabase, input: BoxMergeInput, options?: ConfirmedWriteOptions): Promise<BoxMergeResult>;
@@ -1,6 +1,7 @@
1
1
  import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
2
2
  import { DinoxError } from '../../utils/errors.js';
3
3
  import { nowIsoUtc } from '../../utils/time.js';
4
+ import { assertConfirmedWrite, assertWriteConfirmationProvided, } from '../writeConfirmation.js';
4
5
  import { MAX_BOX_NAME_LENGTH, REQUIRED_NOTE_BOX_COLUMNS, assertBoxPathLimits, assertTextLimits, canonicalizeBoxPath, getLeafName, normalizeBoxLookupKey, parseStoredBoxIds, trimString, } from './model.js';
5
6
  import { activeBoxNodes, activeNodeByPath, assertCanWriteBox, assertRequiredColumns, assertWritableBoxNodes, descendantsOf, ensureNoPathConflict, getTableColumns, isActiveBoxNode, isDescendantBoxPath, loadAllBoxNodes, requireActivePath, toBoxListItem, } from './store.js';
6
7
  function buildSubtreeChanges(args) {
@@ -369,7 +370,7 @@ function sameIds(left, right) {
369
370
  }
370
371
  return left.every((value, index) => value === right[index]);
371
372
  }
372
- export async function mergeBox(db, input, options = {}) {
373
+ async function buildBoxMergePlan(db, input) {
373
374
  const parsed = parseMergeInput(input);
374
375
  const boxColumns = await getTableColumns(db, 'c_zettel_box');
375
376
  const noteColumns = await getTableColumns(db, 'c_note');
@@ -393,33 +394,55 @@ export async function mergeBox(db, input, options = {}) {
393
394
  })
394
395
  .filter((row) => row.id && !sameIds(row.before, row.after));
395
396
  const softDeletedBoxIds = sourceNodes.map((node) => node.id);
396
- if (!options.dryRun) {
397
- await withPowerSyncWriteTransaction(db, async (tx) => {
398
- const timestamp = nowIsoUtc();
399
- for (const note of updatedNotes) {
400
- await tx.execute(`
401
- UPDATE c_note
402
- SET zettel_boxes = ?, updated_at = ?, version = COALESCE(version, 0) + 1
403
- WHERE id = ?
404
- AND TRIM(COALESCE(user_no, '')) = ?
405
- `, [JSON.stringify(note.after), timestamp, note.id, parsed.userId]);
406
- }
407
- for (const boxId of softDeletedBoxIds) {
408
- await tx.execute(`
409
- UPDATE c_zettel_box
410
- SET is_del = 1, updated_at = ?
411
- WHERE id = ?
412
- AND TRIM(COALESCE(user_no, '')) = ?
413
- `, [timestamp, boxId, parsed.userId]);
414
- }
415
- });
416
- }
397
+ const affectedCount = softDeletedBoxIds.length + updatedNotes.length;
417
398
  return {
418
- fromId: source.id,
419
- toId: target.id,
420
- fromPath: source.path,
421
- toPath: target.path,
422
- softDeletedBoxIds,
423
- updatedNotes,
399
+ result: {
400
+ fromId: source.id,
401
+ toId: target.id,
402
+ fromPath: source.path,
403
+ toPath: target.path,
404
+ softDeletedBoxIds,
405
+ updatedNotes,
406
+ affectedCount,
407
+ },
408
+ userId: parsed.userId,
424
409
  };
425
410
  }
411
+ function assertBoxMergeCount(plan, options) {
412
+ assertConfirmedWrite({
413
+ operation: 'Box merge',
414
+ schemaPath: 'box.merge',
415
+ actualCount: plan.result.affectedCount,
416
+ options,
417
+ });
418
+ }
419
+ export async function mergeBox(db, input, options = {}) {
420
+ if (options.dryRun) {
421
+ const plan = await buildBoxMergePlan(db, input);
422
+ assertBoxMergeCount(plan, options);
423
+ return plan.result;
424
+ }
425
+ assertWriteConfirmationProvided('Box merge', options);
426
+ return withPowerSyncWriteTransaction(db, async (tx) => {
427
+ const plan = await buildBoxMergePlan(tx, input);
428
+ assertBoxMergeCount(plan, options);
429
+ const timestamp = nowIsoUtc();
430
+ for (const note of plan.result.updatedNotes) {
431
+ await tx.execute(`
432
+ UPDATE c_note
433
+ SET zettel_boxes = ?, updated_at = ?, version = COALESCE(version, 0) + 1
434
+ WHERE id = ?
435
+ AND TRIM(COALESCE(user_no, '')) = ?
436
+ `, [JSON.stringify(note.after), timestamp, note.id, plan.userId]);
437
+ }
438
+ for (const boxId of plan.result.softDeletedBoxIds) {
439
+ await tx.execute(`
440
+ UPDATE c_zettel_box
441
+ SET is_del = 1, updated_at = ?
442
+ WHERE id = ?
443
+ AND TRIM(COALESCE(user_no, '')) = ?
444
+ `, [timestamp, boxId, plan.userId]);
445
+ }
446
+ return plan.result;
447
+ });
448
+ }
@@ -1,12 +1,13 @@
1
1
  import { registerStandaloneCommand } from '../descriptor/register.js';
2
2
  import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
3
- import { SKILLS_REPO_URL } from '../../constants/links.js';
3
+ import { getSkillsReleaseMetadata } from '../../constants/links.js';
4
+ import { RETIRED_SKILLS, RETIRED_SKILLS_CLEANUP, formatRetiredSkillsSummary, } from '../../skills/retired.js';
4
5
  import { clearPendingStructuredNoticeProvider, getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, setPendingStructuredNoticeProvider, } from '../../utils/output.js';
5
6
  import { getUpdateNotice } from '../../utils/updateNotice.js';
6
7
  import { getPackageVersion } from '../../utils/version.js';
7
8
  export const infoCommandDescriptor = {
8
9
  name: 'info',
9
- description: 'Show CLI version and bundled skills location',
10
+ description: 'Show CLI version and its matching standalone skills release',
10
11
  schema: {
11
12
  path: 'info',
12
13
  risk: 'read',
@@ -14,17 +15,36 @@ export const infoCommandDescriptor = {
14
15
  globalOptions: FORMAT_OPTIONS,
15
16
  output: {
16
17
  type: 'structured',
17
- summary: 'CLI version metadata',
18
- fields: ['name', 'version', 'skills_repo'],
18
+ summary: 'CLI version and matching standalone skills release metadata',
19
+ fields: [
20
+ 'name',
21
+ 'version',
22
+ 'skills_repo',
23
+ 'skills_version',
24
+ 'skills_tag',
25
+ 'skills_release',
26
+ 'skills_installer',
27
+ 'skills_install_command',
28
+ 'retired_skills',
29
+ 'retired_skills_cleanup',
30
+ ],
19
31
  },
20
32
  examples: ['dino info', 'dino info --format json'],
21
33
  },
22
34
  action: async (_context) => {
23
35
  const version = getPackageVersion();
36
+ const skills = getSkillsReleaseMetadata(version);
24
37
  const payload = {
25
38
  name: 'dino',
26
39
  version,
27
- skills_repo: SKILLS_REPO_URL,
40
+ skills_repo: skills.repository,
41
+ skills_version: skills.version,
42
+ skills_tag: skills.tag,
43
+ skills_release: skills.release,
44
+ skills_installer: skills.installer,
45
+ skills_install_command: skills.installCommand,
46
+ retired_skills: RETIRED_SKILLS,
47
+ retired_skills_cleanup: RETIRED_SKILLS_CLEANUP,
28
48
  };
29
49
  if (getRequestedStructuredOutputFormat()) {
30
50
  const updateNotice = await getUpdateNotice(version);
@@ -43,6 +63,11 @@ export const infoCommandDescriptor = {
43
63
  }
44
64
  printHumanReadableLine(payload.version);
45
65
  printHumanReadableLine(`skills repo: ${payload.skills_repo}`);
66
+ printHumanReadableLine(`skills release: ${payload.skills_release}`);
67
+ printHumanReadableLine(`skills installer: ${payload.skills_installer.name}@${payload.skills_installer.version}`);
68
+ printHumanReadableLine(`skills install: ${payload.skills_install_command}`);
69
+ printHumanReadableLine(`retired skills: ${formatRetiredSkillsSummary()}`);
70
+ printHumanReadableLine(`retired skills cleanup: ${RETIRED_SKILLS_CLEANUP.guidance}`);
46
71
  },
47
72
  };
48
73
  export function registerInfoCommand(program) {
@@ -33,7 +33,7 @@ export function getMetaSchema() {
33
33
  version: 1,
34
34
  entities: {
35
35
  note_search_result: {
36
- fields: ['id', 'title', 'summary', 'tags', 'created_at', 'boxes', 'is_starred'],
36
+ fields: ['id', 'title', 'summary', 'tags', 'created_at', 'starred_at', 'boxes', 'is_starred'],
37
37
  meta_fields: [
38
38
  'query',
39
39
  'returned_count',
@@ -55,6 +55,7 @@ export function getMetaSchema() {
55
55
  'created_at',
56
56
  'updated_at',
57
57
  'is_del',
58
+ 'starred_at',
58
59
  'is_starred',
59
60
  'boxes',
60
61
  'links.backlinks',
@@ -6,7 +6,7 @@ import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
6
6
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
7
7
  import { DinoxError } from '../../utils/errors.js';
8
8
  import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime, waitForMutationUpload, } from '../runtime.js';
9
- import { createNote, executeNoteSearch, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForUpdate, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
9
+ import { createNote, executeNoteSearch, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForUpdate, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
10
10
  import { routeRead } from '../../daemon/route.js';
11
11
  import { buildNoteCreateDryRunPreview, buildNoteDeleteDryRunPreview, buildNoteUpdateDryRunPreview, } from './dryRun.js';
12
12
  import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
@@ -32,16 +32,6 @@ function toPublicNoteDetail(note) {
32
32
  boxes: zettel_boxes,
33
33
  };
34
34
  }
35
- function toPublicNote(note) {
36
- if (!note) {
37
- return null;
38
- }
39
- const { zettel_boxes, ...rest } = note;
40
- return {
41
- ...rest,
42
- ...(zettel_boxes !== undefined ? { boxes: zettel_boxes } : {}),
43
- };
44
- }
45
35
  async function executeNoteUpdateOperation(args) {
46
36
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
47
37
  await withNoteDatabase(args.command, async (runtime) => {
@@ -84,7 +74,7 @@ async function executeNoteUpdateOperation(args) {
84
74
  commandName: args.commandName ?? 'dino note update',
85
75
  action: args.actionName ?? 'update_note',
86
76
  });
87
- printDryRunPreview(preview);
77
+ printDryRunPreview(preview, runtime);
88
78
  return;
89
79
  }
90
80
  const results = [];
@@ -199,7 +189,7 @@ async function executeNoteMetadataListOperation(args) {
199
189
  [`${args.field}_to`]: item.after,
200
190
  changed: item.changed,
201
191
  })),
202
- });
192
+ }, runtime);
203
193
  return;
204
194
  }
205
195
  await applyNoteMetadataMutation(db, plan);
@@ -299,40 +289,29 @@ export async function runNoteGet({ command, options, args }) {
299
289
  await withNoteDatabase(command, async (runtime) => {
300
290
  const { db } = runtime;
301
291
  await syncBeforeNoteCommand(runtime);
302
- if (getOptions.contextOnly) {
303
- const context = await getNoteContext(db, id);
304
- if (!context) {
305
- throw new DinoxError(`Note not found: ${id}`, {
306
- code: 'NOTE_NOT_FOUND',
307
- suggestion: "Run 'dino note search <keyword>' to locate the note id.",
308
- recoverable: true,
309
- });
310
- }
311
- printSuccess({ stale: commandStale(runtime), note: toPublicNoteContext(context) });
312
- return;
313
- }
314
- const note = await getNote(db, id);
315
- if (!note) {
292
+ const context = await getNoteContext(db, id);
293
+ if (!context) {
316
294
  throw new DinoxError(`Note not found: ${id}`, {
317
295
  code: 'NOTE_NOT_FOUND',
318
296
  suggestion: "Run 'dino note search <keyword>' to locate the note id.",
319
297
  recoverable: true,
320
298
  });
321
299
  }
322
- if (structuredOutput) {
323
- printSuccess({ stale: commandStale(runtime), note: toPublicNote(note) });
300
+ const note = toPublicNoteContext(context);
301
+ if (structuredOutput || getOptions.contextOnly) {
302
+ printSuccess({ stale: commandStale(runtime), note });
324
303
  return;
325
304
  }
326
305
  printStaleCacheWarning(runtime, 'warning: sync timed out; result may be stale');
327
- printHumanReadableLine(`${note.id}`);
328
- printHumanReadableLine(`title: ${note.title ?? ''}`);
306
+ printHumanReadableLine(String(note.id));
307
+ printHumanReadableLine(`title: ${String(note.title ?? '')}`);
329
308
  if (note.updated_at) {
330
- printHumanReadableLine(`updated_at: ${note.updated_at}`);
309
+ printHumanReadableLine(`updated_at: ${String(note.updated_at)}`);
331
310
  }
332
- if (note.is_starred) {
311
+ if (note.is_starred === true) {
333
312
  printHumanReadableLine('is_starred: 1');
334
313
  }
335
- if (note.is_del) {
314
+ if (typeof note.is_del === 'number' && note.is_del !== 0) {
336
315
  printHumanReadableLine('is_del: 1');
337
316
  }
338
317
  });
@@ -370,7 +349,11 @@ export async function runNoteDetail({ command, options, args }) {
370
349
  throw new DinoxError(ids.length === 1 ? `Note not found: ${missing[0]}` : `Notes not found: ${missing.join(', ')}`);
371
350
  }
372
351
  const publicNotes = notes.map((note) => toPublicNoteDetail(note));
373
- printSuccess(publicNotes.length === 1 ? publicNotes[0] : publicNotes);
352
+ printSuccess({
353
+ stale: commandStale(runtime),
354
+ count: publicNotes.length,
355
+ notes: publicNotes,
356
+ });
374
357
  });
375
358
  }
376
359
  export async function runNoteCreate({ command, options }) {
@@ -402,7 +385,7 @@ export async function runNoteCreate({ command, options }) {
402
385
  contentMd,
403
386
  tagsInput,
404
387
  zettelBoxNames: boxNames,
405
- }));
388
+ }), runtime);
406
389
  return;
407
390
  }
408
391
  const id = generateId();
@@ -447,7 +430,7 @@ export async function runNoteDelete({ command, options, args }) {
447
430
  await syncBeforeNoteCommand(runtime);
448
431
  const durability = resolveWriteDurabilityForRuntime(runtime, deleteOptions.durability);
449
432
  if (Boolean(deleteOptions.dryRun)) {
450
- printDryRunPreview(await buildNoteDeleteDryRunPreview({ db, config, offline, id }));
433
+ printDryRunPreview(await buildNoteDeleteDryRunPreview({ db, config, offline, id }), runtime);
451
434
  return;
452
435
  }
453
436
  await softDeleteNote(db, id);
@@ -1,5 +1,7 @@
1
+ import { parseExpectedCount } from '../writeConfirmation.js';
1
2
  import type { CommandExecutionContext } from '../descriptor/types.js';
2
3
  import type { NoteMetadataListField, NoteMetadataOperation } from './metadataMutation.js';
4
+ export { parseExpectedCount };
3
5
  type NoteBulkOptions = {
4
6
  tags?: string;
5
7
  from?: string;
@@ -26,10 +28,8 @@ type NoteBulkMutationSelection = {
26
28
  optionName: '--tag-add' | '--tag-remove' | '--tag-replace' | '--box-add' | '--box-remove' | '--box-replace';
27
29
  };
28
30
  export declare function selectBulkMutation(options: NoteBulkOptions): NoteBulkMutationSelection;
29
- export declare function parseExpectedCount(value: unknown): number | null;
30
31
  export declare function assertBulkTargetFilter(input: {
31
32
  query: string;
32
33
  options: NoteBulkOptions;
33
34
  }): void;
34
35
  export declare function runNoteBulk({ command, options, args }: CommandExecutionContext): Promise<void>;
35
- export {};
@@ -3,12 +3,14 @@ import { printDryRunPreview } from '../../utils/dryRun.js';
3
3
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
4
4
  import { DinoxError } from '../../utils/errors.js';
5
5
  import { commandStale, printStaleCacheWarning, resolveWriteDurabilityForRuntime, waitForMutationUpload, } from '../runtime.js';
6
+ import { parseExpectedCount } from '../writeConfirmation.js';
6
7
  import { resolveZettelBoxIdsForBulk, resolveZettelBoxIdsForSearch, searchNotes, validateTagsForBulk, } from './repo.js';
7
8
  import { applyNoteMetadataMutation, planNoteMetadataMutation, } from './metadataMutation.js';
8
9
  import { parseCreatedAtRange } from './searchTime.js';
9
10
  import { normalizeDaysForMeta, } from './searchResponse.js';
10
11
  import { parseMutationListInput, parseSearchListInput, parseStarredOption, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
11
12
  import { buildBatchWriteReceiptFields, readNoteRevisionReceipts, } from './writeReceipt.js';
13
+ export { parseExpectedCount };
12
14
  const BULK_PREVIEW_LIMIT = 100;
13
15
  export function selectBulkMutation(options) {
14
16
  const candidates = [
@@ -25,23 +27,6 @@ export function selectBulkMutation(options) {
25
27
  }
26
28
  return selected[0];
27
29
  }
28
- export function parseExpectedCount(value) {
29
- if (value === undefined || value === null || value === '') {
30
- return null;
31
- }
32
- if (typeof value !== 'string') {
33
- throw new DinoxError('--expected-count must be a non-negative integer');
34
- }
35
- const trimmed = value.trim();
36
- if (!/^\d+$/.test(trimmed)) {
37
- throw new DinoxError('--expected-count must be a non-negative integer');
38
- }
39
- const parsed = Number(trimmed);
40
- if (!Number.isSafeInteger(parsed) || parsed < 0) {
41
- throw new DinoxError('--expected-count must be a non-negative integer');
42
- }
43
- return parsed;
44
- }
45
30
  function hasConcreteTargetFilter(input) {
46
31
  const { query, options } = input;
47
32
  const hasValue = (value) => typeof value === 'string' && value.trim().length > 0;
@@ -169,7 +154,7 @@ export async function runNoteBulk({ command, options, args }) {
169
154
  [`${selected.field}_to`]: item.after,
170
155
  changed: item.changed,
171
156
  })),
172
- });
157
+ }, runtime);
173
158
  return;
174
159
  }
175
160
  if (!bulkOptions.confirm) {