@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
@@ -42,7 +42,8 @@ export async function executeSyncCommand(context, overrides = {}) {
42
42
  config,
43
43
  offline: false,
44
44
  timeoutMs: remainingTimeoutMs(),
45
- deadlineAtMs: operationDeadlineAtMs,
45
+ deadlineAtMs,
46
+ operationDeadlineAtMs,
46
47
  waitForInitialSync: false,
47
48
  expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
48
49
  cleanupFailureMode: 'throw',
@@ -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 { addTag, inspectAddTag, listTags } from './repo.js';
10
11
  import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
11
12
  const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -67,7 +68,7 @@ async function runTagAdd(context) {
67
68
  userId: identity.userId,
68
69
  emoji,
69
70
  })),
70
- }));
71
+ }), runtime);
71
72
  return;
72
73
  }
73
74
  const result = await addTag(runtime.db, {
@@ -114,7 +115,7 @@ async function runTagRename(context) {
114
115
  summary: `Rename tag "${result.fromPath}" to "${result.toPath}".`,
115
116
  inputs: { path, new_name: newName },
116
117
  changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
117
- }));
118
+ }), runtime);
118
119
  return;
119
120
  }
120
121
  await waitForMutationUpload(runtime);
@@ -139,7 +140,7 @@ async function runTagMove(context) {
139
140
  summary: `Move tag "${result.fromPath}" to "${result.toPath}".`,
140
141
  inputs: { path, to },
141
142
  changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
142
- }));
143
+ }), runtime);
143
144
  return;
144
145
  }
145
146
  await waitForMutationUpload(runtime);
@@ -149,9 +150,17 @@ async function runTagMove(context) {
149
150
  async function runTagMerge(context) {
150
151
  const from = requireArg(context.args.from, 'Source tag path is required');
151
152
  const to = requireArg(context.args.to, 'Target tag path is required');
153
+ const dryRun = Boolean(context.options.dryRun);
154
+ const expectedCount = parseExpectedCount(context.options.expectedCount);
155
+ const writeOptions = {
156
+ dryRun,
157
+ confirm: Boolean(context.options.confirm),
158
+ expectedCount,
159
+ };
160
+ assertWriteConfirmationProvided('Tag merge', writeOptions);
152
161
  await withTagWriteRuntime(context, async ({ runtime, userId }) => {
153
- const result = await mergeTag(runtime.db, { from, to, userId }, { dryRun: Boolean(context.options.dryRun) });
154
- if (Boolean(context.options.dryRun)) {
162
+ const result = await mergeTag(runtime.db, { from, to, userId }, writeOptions);
163
+ if (dryRun) {
155
164
  printDryRunPreview(buildTagMutationDryRunPreview({
156
165
  config: runtime.config,
157
166
  offline: runtime.offline,
@@ -159,12 +168,15 @@ async function runTagMerge(context) {
159
168
  action: 'merge_tag',
160
169
  risk: 'high-risk-write',
161
170
  summary: `Merge tag subtree "${result.fromPath}" into "${result.toPath}".`,
162
- inputs: { from, to },
171
+ inputs: { from, to, expected_count: expectedCount },
163
172
  changes: {
173
+ affected_count: result.affectedCount,
174
+ soft_deleted_tag_count: result.softDeletedTagIds.length,
175
+ updated_note_count: result.updatedNotes.length,
164
176
  soft_deleted_tag_ids: result.softDeletedTagIds,
165
177
  updated_notes: result.updatedNotes,
166
178
  },
167
- }));
179
+ }), runtime);
168
180
  return;
169
181
  }
170
182
  await waitForMutationUpload(runtime);
@@ -193,7 +205,7 @@ async function runTagCleanup(context) {
193
205
  summary: `Inspect tag cleanup candidates (${result.issues.length} issues).`,
194
206
  inputs: {},
195
207
  targets: result.issues,
196
- }));
208
+ }), runtime);
197
209
  return;
198
210
  }
199
211
  printSuccess({ stale: commandStale(runtime), ...result });
@@ -328,7 +340,11 @@ export const tagCommandGroup = {
328
340
  { syntax: '<from>', description: 'Source tag path' },
329
341
  { syntax: '<to>', description: 'Target tag path' },
330
342
  ],
331
- options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
343
+ options: [
344
+ { flags: '--expected-count <n>', description: 'Required for real writes; must equal source tags plus notes affected', type: 'number' },
345
+ { flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
346
+ { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
347
+ ],
332
348
  schema: {
333
349
  path: 'tag.merge',
334
350
  risk: 'high-risk-write',
@@ -337,9 +353,12 @@ export const tagCommandGroup = {
337
353
  output: {
338
354
  type: 'structured',
339
355
  summary: 'Tag merge result',
340
- fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'stale'],
356
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'affectedCount', 'stale'],
341
357
  },
342
- examples: ['dino tag merge "ai" "AI" --format json --dry-run'],
358
+ examples: [
359
+ 'dino tag merge "ai" "AI" --format json --dry-run',
360
+ 'dino tag merge "ai" "AI" --expected-count 4 --confirm --format json',
361
+ ],
343
362
  },
344
363
  action: runTagMerge,
345
364
  },
@@ -1,4 +1,5 @@
1
1
  import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import { type ConfirmedWriteOptions } from '../writeConfirmation.js';
2
3
  import type { TagListItem } from './model.js';
3
4
  export type TagTreeNode = TagListItem & {
4
5
  isActual: boolean;
@@ -63,6 +64,7 @@ export type TagMergeResult = {
63
64
  before: string[];
64
65
  after: string[];
65
66
  }>;
67
+ affectedCount: number;
66
68
  };
67
69
  export type TagSuggestResult = {
68
70
  suggestions: Array<{
@@ -98,8 +100,6 @@ export declare function renameTag(db: AbstractPowerSyncDatabase, input: TagRenam
98
100
  export declare function moveTag(db: AbstractPowerSyncDatabase, input: TagMoveInput, options?: {
99
101
  dryRun?: boolean;
100
102
  }): Promise<TagRenameMoveResult>;
101
- export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?: {
102
- dryRun?: boolean;
103
- }): Promise<TagMergeResult>;
103
+ export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?: ConfirmedWriteOptions): Promise<TagMergeResult>;
104
104
  export declare function inspectTagCleanup(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagCleanupResult>;
105
105
  export declare function suggestTagMerges(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagSuggestResult>;
@@ -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 { mergeTagReferencesInNote, renameTagReferencesInNote } from './content.js';
5
6
  import { MAX_TAG_SEGMENT_LENGTH, REQUIRED_NOTE_TAG_COLUMNS, REQUIRED_TAG_NODE_COLUMNS, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, isTagPathWithinPrefix, normalizeTagLookupKey, parseStoredTags, parentPathOf, renameTagPathWithPrefix, trimString, } from './model.js';
6
7
  import { activeTagNodes, actualActiveTagNodes, assertCanWriteTag, assertRequiredColumns, assertWritableTagNodes, descendantsOf, ensureNoTagPathConflict, getTableColumns, loadTagNodes, parentIdForPath, requireActiveTagPath, toTagListItem, } from './store.js';
@@ -297,7 +298,7 @@ export async function moveTag(db, input, options = {}) {
297
298
  updatedNotes: noteUpdates.map(({ id, before, after }) => ({ id, before, after })),
298
299
  };
299
300
  }
300
- export async function mergeTag(db, input, options = {}) {
301
+ async function buildTagMergePlan(db, input) {
301
302
  const from = assertPath(input.from, 'Source tag path');
302
303
  const to = assertPath(input.to, 'Target tag path');
303
304
  const userId = trimString(input.userId);
@@ -318,28 +319,52 @@ export async function mergeTag(db, input, options = {}) {
318
319
  .map((row) => updateNoteTagsForPrefix(row, source.fullPath, target.fullPath, 'merge', lookup))
319
320
  .filter((row) => row.id && row.changed);
320
321
  const softDeletedTagIds = sourceNodes.map((node) => node.id);
321
- if (!options.dryRun) {
322
- await withPowerSyncWriteTransaction(db, async (tx) => {
323
- const timestamp = nowIsoUtc();
324
- await applyNoteUpdates(tx, noteUpdates, timestamp);
325
- for (const tagId of softDeletedTagIds) {
326
- await tx.execute(`
327
- UPDATE c_tag_node
328
- SET is_del = 1, updated_at = ?
329
- WHERE id = ?
330
- `, [timestamp, tagId]);
331
- }
332
- });
333
- }
322
+ const affectedCount = softDeletedTagIds.length + noteUpdates.length;
334
323
  return {
335
- fromId: source.id,
336
- toId: target.id,
337
- fromPath: source.fullPath,
338
- toPath: target.fullPath,
339
- softDeletedTagIds,
340
- updatedNotes: noteUpdates.map(({ id, before, after }) => ({ id, before, after })),
324
+ result: {
325
+ fromId: source.id,
326
+ toId: target.id,
327
+ fromPath: source.fullPath,
328
+ toPath: target.fullPath,
329
+ softDeletedTagIds,
330
+ updatedNotes: noteUpdates.map(({ id, before, after }) => ({ id, before, after })),
331
+ affectedCount,
332
+ },
333
+ noteUpdates,
334
+ userId,
341
335
  };
342
336
  }
337
+ function assertTagMergeCount(plan, options) {
338
+ assertConfirmedWrite({
339
+ operation: 'Tag merge',
340
+ schemaPath: 'tag.merge',
341
+ actualCount: plan.result.affectedCount,
342
+ options,
343
+ });
344
+ }
345
+ export async function mergeTag(db, input, options = {}) {
346
+ if (options.dryRun) {
347
+ const plan = await buildTagMergePlan(db, input);
348
+ assertTagMergeCount(plan, options);
349
+ return plan.result;
350
+ }
351
+ assertWriteConfirmationProvided('Tag merge', options);
352
+ return withPowerSyncWriteTransaction(db, async (tx) => {
353
+ const plan = await buildTagMergePlan(tx, input);
354
+ assertTagMergeCount(plan, options);
355
+ const timestamp = nowIsoUtc();
356
+ await applyNoteUpdates(tx, plan.noteUpdates, timestamp);
357
+ for (const tagId of plan.result.softDeletedTagIds) {
358
+ await tx.execute(`
359
+ UPDATE c_tag_node
360
+ SET is_del = 1, updated_at = ?
361
+ WHERE id = ?
362
+ AND TRIM(COALESCE(user_id, '')) = ?
363
+ `, [timestamp, tagId, plan.userId]);
364
+ }
365
+ return plan.result;
366
+ });
367
+ }
343
368
  export async function inspectTagCleanup(db, input = {}) {
344
369
  const columns = await getTableColumns(db, 'c_tag_node');
345
370
  assertRequiredColumns('c_tag_node', columns, REQUIRED_TAG_NODE_COLUMNS);
@@ -0,0 +1,50 @@
1
+ import { type TiptapNode } from '../notes/contentEngine.js';
2
+ export declare const NOTE_TEMPLATE_PROPERTY_DEF_VERSION = 1;
3
+ export declare const MAX_NOTE_TEMPLATE_PROPERTY_KEYS = 50;
4
+ export declare const MAX_NOTE_TEMPLATE_CONTENT_LENGTH = 200000;
5
+ export type NoteTemplatePropertyDef = {
6
+ version: typeof NOTE_TEMPLATE_PROPERTY_DEF_VERSION;
7
+ keys: string[];
8
+ };
9
+ export type NoteTemplateRow = {
10
+ id: string;
11
+ user_id: string | null;
12
+ name: string | null;
13
+ content: string | null;
14
+ priority: number | null;
15
+ is_show: number | null;
16
+ extra_data: string | null;
17
+ property_def: string | null;
18
+ insert_at: string | null;
19
+ update_at: string | null;
20
+ is_del: number | null;
21
+ };
22
+ export type NoteTemplateSummary = {
23
+ id: string;
24
+ name: string;
25
+ propertyKeys: string[];
26
+ content: string;
27
+ hasContent: boolean;
28
+ isShow: boolean;
29
+ priority: number;
30
+ updateAt: string | null;
31
+ };
32
+ export type NoteTemplateTokenValues = {
33
+ date: string;
34
+ datetime: string;
35
+ time: string;
36
+ location: string;
37
+ };
38
+ export declare function isNotePropertyKeyValid(value: unknown): value is string;
39
+ export declare function validateNoteTemplatePropertyKeys(keys: readonly unknown[]): string[];
40
+ export declare function parseNoteTemplatePropertyDef(raw: string | null | undefined): NoteTemplatePropertyDef | null;
41
+ export declare function serializeNoteTemplatePropertyDef(keys: readonly string[]): string | null;
42
+ export declare function isNoteTemplateDocumentEmpty(document: TiptapNode | null | undefined): boolean;
43
+ export declare function parseNoteTemplateContent(raw: string | null | undefined): TiptapNode | null;
44
+ export declare function serializeNoteTemplateContent(document: unknown): string;
45
+ export declare function buildNoteTemplateTokenValues(now?: Date, location?: string): NoteTemplateTokenValues;
46
+ export declare function hydrateNoteTemplateDocument(document: TiptapNode, options?: {
47
+ now?: Date;
48
+ location?: string;
49
+ }): TiptapNode;
50
+ export declare function mapNoteTemplateRow(row: NoteTemplateRow): NoteTemplateSummary;
@@ -0,0 +1,239 @@
1
+ import { projectNoteContent } from '../notes/contentEngine.js';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ export const NOTE_TEMPLATE_PROPERTY_DEF_VERSION = 1;
4
+ export const MAX_NOTE_TEMPLATE_PROPERTY_KEYS = 50;
5
+ export const MAX_NOTE_TEMPLATE_CONTENT_LENGTH = 200_000;
6
+ const MAX_TIPTAP_NODE_DEPTH = 100;
7
+ const MAX_TIPTAP_NODE_COUNT = 10_000;
8
+ const NON_TEXT_TEMPLATE_NODE_TYPES = new Set([
9
+ 'image',
10
+ 'table',
11
+ 'mermaid',
12
+ 'mindgraph',
13
+ 'codeBlock',
14
+ 'horizontalRule',
15
+ 'taskList',
16
+ ]);
17
+ function formatError(message, code, details) {
18
+ return new DinoxError(message, {
19
+ code,
20
+ details,
21
+ recoverable: false,
22
+ });
23
+ }
24
+ function isPlainRecord(value) {
25
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+ export function isNotePropertyKeyValid(value) {
28
+ if (typeof value !== 'string')
29
+ return false;
30
+ const trimmed = value.trim();
31
+ if (!trimmed || trimmed !== value || trimmed.length > 64)
32
+ return false;
33
+ if (trimmed === '__proto__')
34
+ return false;
35
+ return !trimmed.includes('.') && !/[\n\r\t]/.test(trimmed);
36
+ }
37
+ export function validateNoteTemplatePropertyKeys(keys) {
38
+ if (keys.length > MAX_NOTE_TEMPLATE_PROPERTY_KEYS) {
39
+ throw formatError(`Note templates support at most ${MAX_NOTE_TEMPLATE_PROPERTY_KEYS} property keys`, 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF', { maxKeys: MAX_NOTE_TEMPLATE_PROPERTY_KEYS, receivedKeys: keys.length });
40
+ }
41
+ const seen = new Set();
42
+ const validated = [];
43
+ for (const key of keys) {
44
+ if (!isNotePropertyKeyValid(key)) {
45
+ throw formatError('Note template property_def contains an invalid property key', 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF', {
46
+ key,
47
+ });
48
+ }
49
+ if (seen.has(key)) {
50
+ throw formatError('Note template property_def contains duplicate property keys', 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF', {
51
+ key,
52
+ });
53
+ }
54
+ seen.add(key);
55
+ validated.push(key);
56
+ }
57
+ return validated;
58
+ }
59
+ export function parseNoteTemplatePropertyDef(raw) {
60
+ const value = raw?.trim() ?? '';
61
+ if (!value)
62
+ return null;
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(value);
66
+ }
67
+ catch (cause) {
68
+ throw formatError('Note template property_def must be valid JSON', 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF', {
69
+ cause: cause instanceof Error ? cause.message : String(cause),
70
+ });
71
+ }
72
+ if (!isPlainRecord(parsed)) {
73
+ throw formatError('Note template property_def must be a versioned object', 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF');
74
+ }
75
+ if (parsed.version !== NOTE_TEMPLATE_PROPERTY_DEF_VERSION) {
76
+ throw formatError(`Unsupported note template property_def version: ${String(parsed.version)}`, 'NOTE_TEMPLATE_UNSUPPORTED_PROPERTY_DEF_VERSION', { supportedVersion: NOTE_TEMPLATE_PROPERTY_DEF_VERSION, receivedVersion: parsed.version });
77
+ }
78
+ if (!Array.isArray(parsed.keys)) {
79
+ throw formatError('Note template property_def keys must be an array', 'NOTE_TEMPLATE_INVALID_PROPERTY_DEF');
80
+ }
81
+ return {
82
+ version: NOTE_TEMPLATE_PROPERTY_DEF_VERSION,
83
+ keys: validateNoteTemplatePropertyKeys(parsed.keys),
84
+ };
85
+ }
86
+ export function serializeNoteTemplatePropertyDef(keys) {
87
+ const validated = validateNoteTemplatePropertyKeys(keys);
88
+ if (validated.length === 0)
89
+ return null;
90
+ return JSON.stringify({
91
+ version: NOTE_TEMPLATE_PROPERTY_DEF_VERSION,
92
+ keys: validated,
93
+ });
94
+ }
95
+ function assertTiptapNode(value, state, depth) {
96
+ if (depth > MAX_TIPTAP_NODE_DEPTH) {
97
+ throw formatError(`Note template content exceeds the maximum node depth of ${MAX_TIPTAP_NODE_DEPTH}`, 'NOTE_TEMPLATE_INVALID_CONTENT');
98
+ }
99
+ if (!isPlainRecord(value) || typeof value.type !== 'string' || !value.type.trim()) {
100
+ throw formatError('Note template content contains an invalid Tiptap node', 'NOTE_TEMPLATE_INVALID_CONTENT');
101
+ }
102
+ state.count += 1;
103
+ if (state.count > MAX_TIPTAP_NODE_COUNT) {
104
+ throw formatError(`Note template content exceeds the maximum node count of ${MAX_TIPTAP_NODE_COUNT}`, 'NOTE_TEMPLATE_INVALID_CONTENT');
105
+ }
106
+ if (value.text !== undefined && typeof value.text !== 'string') {
107
+ throw formatError('Tiptap node text must be a string', 'NOTE_TEMPLATE_INVALID_CONTENT');
108
+ }
109
+ if (value.attrs !== undefined && !isPlainRecord(value.attrs)) {
110
+ throw formatError('Tiptap node attrs must be an object', 'NOTE_TEMPLATE_INVALID_CONTENT');
111
+ }
112
+ if (value.marks !== undefined) {
113
+ if (!Array.isArray(value.marks)) {
114
+ throw formatError('Tiptap node marks must be an array', 'NOTE_TEMPLATE_INVALID_CONTENT');
115
+ }
116
+ for (const mark of value.marks) {
117
+ if (!isPlainRecord(mark) || typeof mark.type !== 'string' || !mark.type.trim()) {
118
+ throw formatError('Tiptap node contains an invalid mark', 'NOTE_TEMPLATE_INVALID_CONTENT');
119
+ }
120
+ if (mark.attrs !== undefined && !isPlainRecord(mark.attrs)) {
121
+ throw formatError('Tiptap mark attrs must be an object', 'NOTE_TEMPLATE_INVALID_CONTENT');
122
+ }
123
+ }
124
+ }
125
+ if (value.content !== undefined) {
126
+ if (!Array.isArray(value.content)) {
127
+ throw formatError('Tiptap node content must be an array', 'NOTE_TEMPLATE_INVALID_CONTENT');
128
+ }
129
+ for (const child of value.content) {
130
+ assertTiptapNode(child, state, depth + 1);
131
+ }
132
+ }
133
+ }
134
+ function assertCanonicalTemplateDocument(value) {
135
+ assertTiptapNode(value, { count: 0 }, 1);
136
+ if (value.type !== 'doc' || !Array.isArray(value.content)) {
137
+ throw formatError('Note template content must be a Tiptap doc with a content array', 'NOTE_TEMPLATE_INVALID_CONTENT');
138
+ }
139
+ }
140
+ function extractTemplatePlainText(node) {
141
+ if (node.type === 'text')
142
+ return typeof node.text === 'string' ? node.text : '';
143
+ if (node.type === 'hardBreak')
144
+ return '\n';
145
+ return (node.content ?? []).map(extractTemplatePlainText).join('');
146
+ }
147
+ function hasNonTextTemplateContent(node) {
148
+ if (NON_TEXT_TEMPLATE_NODE_TYPES.has(node.type))
149
+ return true;
150
+ return (node.content ?? []).some(hasNonTextTemplateContent);
151
+ }
152
+ export function isNoteTemplateDocumentEmpty(document) {
153
+ if (!document)
154
+ return true;
155
+ if (extractTemplatePlainText(document).trim())
156
+ return false;
157
+ return !hasNonTextTemplateContent(document);
158
+ }
159
+ export function parseNoteTemplateContent(raw) {
160
+ const value = raw?.trim() ?? '';
161
+ if (!value)
162
+ return null;
163
+ if (value.length > MAX_NOTE_TEMPLATE_CONTENT_LENGTH) {
164
+ throw formatError(`Note template content must be at most ${MAX_NOTE_TEMPLATE_CONTENT_LENGTH} characters`, 'NOTE_TEMPLATE_INVALID_CONTENT');
165
+ }
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(value);
169
+ }
170
+ catch (cause) {
171
+ throw formatError('Note template content must be valid Tiptap JSON', 'NOTE_TEMPLATE_INVALID_CONTENT', {
172
+ cause: cause instanceof Error ? cause.message : String(cause),
173
+ });
174
+ }
175
+ assertCanonicalTemplateDocument(parsed);
176
+ return projectNoteContent(parsed).document;
177
+ }
178
+ export function serializeNoteTemplateContent(document) {
179
+ assertCanonicalTemplateDocument(document);
180
+ if (isNoteTemplateDocumentEmpty(document))
181
+ return '';
182
+ const content = projectNoteContent(document).contentJson;
183
+ if (content.length > MAX_NOTE_TEMPLATE_CONTENT_LENGTH) {
184
+ throw formatError(`Note template content must be at most ${MAX_NOTE_TEMPLATE_CONTENT_LENGTH} characters`, 'NOTE_TEMPLATE_INVALID_CONTENT');
185
+ }
186
+ return content;
187
+ }
188
+ function pad(value) {
189
+ return String(value).padStart(2, '0');
190
+ }
191
+ export function buildNoteTemplateTokenValues(now = new Date(), location = '') {
192
+ if (Number.isNaN(now.getTime())) {
193
+ throw formatError('Note template token time must be a valid date', 'NOTE_TEMPLATE_INVALID_TOKEN_CONTEXT');
194
+ }
195
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
196
+ const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
197
+ return {
198
+ date,
199
+ time,
200
+ datetime: `${date} ${time}`,
201
+ location,
202
+ };
203
+ }
204
+ function hydrateTemplateNode(node, values) {
205
+ if (node.type === 'text' && typeof node.text === 'string') {
206
+ const text = node.text
207
+ .replaceAll('{{date}}', values.date)
208
+ .replaceAll('{{datetime}}', values.datetime)
209
+ .replaceAll('{{time}}', values.time)
210
+ .replaceAll('{{location}}', values.location);
211
+ return text ? { ...node, text } : null;
212
+ }
213
+ if (!Array.isArray(node.content))
214
+ return { ...node };
215
+ const content = node.content
216
+ .map((child) => hydrateTemplateNode(child, values))
217
+ .filter((child) => child !== null);
218
+ return { ...node, content };
219
+ }
220
+ export function hydrateNoteTemplateDocument(document, options = {}) {
221
+ assertCanonicalTemplateDocument(document);
222
+ const values = buildNoteTemplateTokenValues(options.now, options.location ?? '');
223
+ return hydrateTemplateNode(document, values) ?? { type: 'doc', content: [] };
224
+ }
225
+ export function mapNoteTemplateRow(row) {
226
+ const propertyDef = parseNoteTemplatePropertyDef(row.property_def);
227
+ const document = parseNoteTemplateContent(row.content);
228
+ const content = document ? serializeNoteTemplateContent(document) : '';
229
+ return {
230
+ id: row.id,
231
+ name: (row.name ?? '').trim(),
232
+ propertyKeys: propertyDef?.keys ?? [],
233
+ content,
234
+ hasContent: Boolean(content),
235
+ isShow: Number(row.is_show ?? 0) === 1,
236
+ priority: Number.isFinite(row.priority) ? Math.trunc(Number(row.priority)) : 0,
237
+ updateAt: row.update_at ?? null,
238
+ };
239
+ }
@@ -182,7 +182,7 @@ async function runTodoAppend({ args, globals, options }) {
182
182
  noteId: typeof appendOptions.noteId === 'string' ? appendOptions.noteId : undefined,
183
183
  taskTexts,
184
184
  });
185
- printDryRunPreview(preview);
185
+ printDryRunPreview(preview, runtime);
186
186
  return;
187
187
  }
188
188
  const result = await appendTodoTasks(db, {
@@ -216,7 +216,7 @@ async function runTodoCreate({ args, globals, options }) {
216
216
  title: typeof createOptions.title === 'string' ? createOptions.title : undefined,
217
217
  taskTexts,
218
218
  });
219
- printDryRunPreview(preview);
219
+ printDryRunPreview(preview, runtime);
220
220
  return;
221
221
  }
222
222
  const identity = await resolveAuthIdentity(config);
@@ -254,7 +254,7 @@ async function runTodoUpdate({ args, globals, options }) {
254
254
  noteId: typeof updateOptions.noteId === 'string' ? updateOptions.noteId : undefined,
255
255
  checked,
256
256
  });
257
- printDryRunPreview(preview);
257
+ printDryRunPreview(preview, runtime);
258
258
  return;
259
259
  }
260
260
  const result = await updateTodoTaskStatus(db, {
@@ -8,6 +8,7 @@ type UpdateCommandDeps = {
8
8
  structuredOutput: boolean;
9
9
  }) => Promise<void>;
10
10
  getVersion: () => string;
11
+ getLatestVersion: () => Promise<string | null>;
11
12
  log: LogFn;
12
13
  };
13
14
  export declare function detectPackageManager(input?: {
@@ -2,9 +2,11 @@ import { spawn } from 'node:child_process';
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { registerStandaloneCommand } from './descriptor/register.js';
4
4
  import { FORMAT_OPTIONS } from './schema/globalOptions.js';
5
- import { SKILLS_REPO_URL } from '../constants/links.js';
5
+ import { getSkillsReleaseMetadata } from '../constants/links.js';
6
+ import { RETIRED_SKILLS, RETIRED_SKILLS_CLEANUP, formatRetiredSkillsSummary, } from '../skills/retired.js';
6
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
7
8
  import { DinoxError } from '../utils/errors.js';
9
+ import { fetchLatestPublishedVersion } from '../utils/updateNotice.js';
8
10
  import { getPackageVersion } from '../utils/version.js';
9
11
  const PACKAGE_NAME = '@dinoxx/dinox-cli';
10
12
  const MAX_CAPTURED_UPDATE_OUTPUT_BYTES = 64 * 1024;
@@ -123,13 +125,20 @@ function resolveDeps(overrides) {
123
125
  detectPackageManager,
124
126
  runUpdate,
125
127
  getVersion: getPackageVersion,
128
+ getLatestVersion: fetchLatestPublishedVersion,
126
129
  log: logHumanReadable,
127
130
  ...overrides,
128
131
  };
129
132
  }
130
- function printSkillsReminder(log) {
131
- log(`skills repo: ${SKILLS_REPO_URL}`);
132
- log('ai reminder: review and update local Dinox skills after CLI updates (especially dino-dinox and dino-update-cli).');
133
+ function printSkillsReminder(log, version) {
134
+ const skills = getSkillsReleaseMetadata(version);
135
+ log(`skills repo: ${skills.repository}`);
136
+ log(`skills release: ${skills.release}`);
137
+ log(`skills installer: ${skills.installer.name}@${skills.installer.version}`);
138
+ log(`skills install: ${skills.installCommand}`);
139
+ log(`ai reminder: update local Dinox skills to ${skills.tag} after this CLI update.`);
140
+ log(`retired skills: ${formatRetiredSkillsSummary()}`);
141
+ log(`retired skills cleanup: ${RETIRED_SKILLS_CLEANUP.guidance}`);
133
142
  }
134
143
  function parsePackageManagerOverride(value) {
135
144
  if (value === undefined || value === null || value === '') {
@@ -161,32 +170,60 @@ export function createUpdateCommandDescriptor(overrides = {}) {
161
170
  globalOptions: FORMAT_OPTIONS,
162
171
  output: {
163
172
  type: 'structured',
164
- summary: 'Updater result and bundled skills reminder',
165
- fields: ['previousVersion', 'packageManager', 'package', 'target', 'skillsRepo'],
173
+ summary: 'Updater result and matching standalone skills release metadata',
174
+ fields: [
175
+ 'previousVersion',
176
+ 'version',
177
+ 'packageManager',
178
+ 'package',
179
+ 'target',
180
+ 'skillsRepo',
181
+ 'skillsVersion',
182
+ 'skillsTag',
183
+ 'skillsRelease',
184
+ 'skillsInstaller',
185
+ 'skillsInstallCommand',
186
+ 'retiredSkills',
187
+ 'retiredSkillsCleanup',
188
+ ],
166
189
  },
167
190
  examples: ['dino update', 'dino update --package-manager pnpm --format json'],
168
191
  },
169
192
  action: async (context) => {
170
193
  const pm = parsePackageManagerOverride(context.options.packageManager) ?? deps.detectPackageManager();
171
194
  const previousVersion = deps.getVersion();
195
+ const targetVersion = await deps.getLatestVersion();
172
196
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
173
197
  if (!structuredOutput) {
174
198
  deps.log(`current version: ${previousVersion}`);
175
199
  deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
176
200
  }
177
201
  await deps.runUpdate(pm, { structuredOutput });
202
+ const observedVersion = deps.getVersion();
203
+ const version = observedVersion !== previousVersion
204
+ ? observedVersion
205
+ : targetVersion ?? observedVersion;
206
+ const skills = getSkillsReleaseMetadata(version);
178
207
  if (structuredOutput) {
179
208
  printSuccess({
180
209
  previousVersion,
210
+ version,
181
211
  packageManager: pm,
182
212
  package: PACKAGE_NAME,
183
213
  target: 'latest',
184
- skillsRepo: SKILLS_REPO_URL,
214
+ skillsRepo: skills.repository,
215
+ skillsVersion: skills.version,
216
+ skillsTag: skills.tag,
217
+ skillsRelease: skills.release,
218
+ skillsInstaller: skills.installer,
219
+ skillsInstallCommand: skills.installCommand,
220
+ retiredSkills: RETIRED_SKILLS,
221
+ retiredSkillsCleanup: RETIRED_SKILLS_CLEANUP,
185
222
  });
186
223
  return;
187
224
  }
188
- deps.log('done');
189
- printSkillsReminder(deps.log);
225
+ deps.log(`done: ${version}`);
226
+ printSkillsReminder(deps.log, version);
190
227
  },
191
228
  };
192
229
  }