@dinoxx/dinox-cli 2.1.1 → 2.2.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.
@@ -39,11 +39,17 @@ async function executeNoteUpdateOperation(args) {
39
39
  await syncBeforeNoteCommand(runtime);
40
40
  const durability = resolveWriteDurabilityForRuntime(runtime, args.durability);
41
41
  const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
42
+ const hasTitle = typeof args.title === 'string';
42
43
  const hasTags = typeof args.tagsOption === 'string';
43
44
  const hasBoxes = typeof args.boxOption === 'string';
44
45
  const hasStarred = typeof args.starred === 'boolean';
45
- if (!hasTags && !hasBoxes && !hasStarred) {
46
- throw new DinoxError('At least one option is required: --tags, --boxes, or --starred');
46
+ if (!hasTitle && !hasTags && !hasBoxes && !hasStarred) {
47
+ throw new DinoxError('At least one option is required: --title, --tags, --boxes, or --starred');
48
+ }
49
+ if (hasTitle && ids.length !== 1) {
50
+ throw new DinoxError('--title can only be used with a single note id; run one `dino note update <id> --title` per note', {
51
+ code: 'USAGE_ERROR',
52
+ });
47
53
  }
48
54
  const requestedTags = hasTags
49
55
  ? await parseMutationListInput(args.tagsOption, '--tags')
@@ -68,6 +74,7 @@ async function executeNoteUpdateOperation(args) {
68
74
  offline,
69
75
  userId: tagUserId,
70
76
  ids,
77
+ title: args.title,
71
78
  tagsInput: tags,
72
79
  zettelBoxNames: requestedBoxNames,
73
80
  starred: hasStarred ? args.starred : undefined,
@@ -81,6 +88,7 @@ async function executeNoteUpdateOperation(args) {
81
88
  for (const targetId of ids) {
82
89
  results.push(await updateNote(db, {
83
90
  id: targetId,
91
+ title: args.title,
84
92
  tags,
85
93
  zettelBoxIds,
86
94
  starred: hasStarred ? args.starred : undefined,
@@ -98,6 +106,7 @@ async function executeNoteUpdateOperation(args) {
98
106
  results: results.map((item) => ({
99
107
  id: item.id,
100
108
  updated: item.updated,
109
+ title: item.title,
101
110
  tags: item.tags,
102
111
  boxes: item.zettelBoxIds,
103
112
  is_starred: item.is_starred,
@@ -452,6 +461,17 @@ export async function runNoteDelete({ command, options, args }) {
452
461
  printStaleCacheWarning(runtime);
453
462
  });
454
463
  }
464
+ // Titles are single-line metadata; reject input that would render as a multi-line title.
465
+ function parseNoteTitleOption(raw) {
466
+ const title = raw.trim();
467
+ if (!title) {
468
+ throw new DinoxError('--title must not be empty', { code: 'USAGE_ERROR' });
469
+ }
470
+ if (/[\u0000-\u001f\u007f]/.test(title)) {
471
+ throw new DinoxError('--title must be a single line without control characters', { code: 'USAGE_ERROR' });
472
+ }
473
+ return title;
474
+ }
455
475
  export async function runNoteUpdate({ command, options, args }) {
456
476
  const updateOptions = options;
457
477
  await executeNoteUpdateOperation({
@@ -463,6 +483,9 @@ export async function runNoteUpdate({ command, options, args }) {
463
483
  starred: typeof updateOptions.starred === 'string'
464
484
  ? parseStarredOption(updateOptions.starred, '--starred')
465
485
  : undefined,
486
+ title: typeof updateOptions.title === 'string'
487
+ ? parseNoteTitleOption(updateOptions.title)
488
+ : undefined,
466
489
  tagsOption: updateOptions.tags,
467
490
  commandName: 'dino note update',
468
491
  actionName: 'update_note',
@@ -202,10 +202,11 @@ export const noteCommandGroup = {
202
202
  },
203
203
  {
204
204
  name: 'update',
205
- description: 'Full-replace note metadata for explicit note ids',
205
+ description: 'Full-replace note metadata (title, tags, boxes, starred) for explicit note ids',
206
206
  arguments: [{ syntax: '[id]', description: 'Optional note id for single-note full replacement' }],
207
207
  options: [
208
208
  { flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
209
+ { flags: '--title <title>', description: 'Replace the note title (single note id only; content is unchanged)', type: 'string' },
209
210
  { flags: '--tags <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
210
211
  { flags: '--boxes <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
211
212
  { flags: '--starred <true|false>', description: 'Replace the starred state', type: 'string', enum: ['true', 'false'] },
@@ -224,6 +225,7 @@ export const noteCommandGroup = {
224
225
  },
225
226
  examples: [
226
227
  'dino note update <id> --tags "work,ai" --format json --dry-run',
228
+ 'dino note update <id> --title "New title" --format json --dry-run',
227
229
  'dino note update --ids @/tmp/note-ids.txt --boxes "Inbox" --starred true --format json',
228
230
  ],
229
231
  },
@@ -18,6 +18,7 @@ export declare function buildNoteUpdateDryRunPreview(args: {
18
18
  offline: boolean;
19
19
  userId?: string;
20
20
  ids: string[];
21
+ title?: string;
21
22
  tagsInput?: string[];
22
23
  zettelBoxNames?: string[];
23
24
  starred?: boolean;
@@ -67,9 +67,14 @@ export async function buildNoteUpdateDryRunPreview(args) {
67
67
  const nextBoxes = Array.isArray(args.zettelBoxNames) ? normalizeDistinctStrings(args.zettelBoxNames) : note.zettel_boxes;
68
68
  const tagsTo = nextTags ?? currentTags;
69
69
  const starredTo = typeof args.starred === 'boolean' ? args.starred : note.is_starred;
70
+ const titleFrom = note.title ?? '';
71
+ const titleTo = typeof args.title === 'string' ? args.title : titleFrom;
70
72
  return {
71
73
  id: note.id,
72
- title: note.title ?? '',
74
+ title: titleFrom,
75
+ ...(typeof args.title === 'string'
76
+ ? { title_from: titleFrom, title_to: titleTo, title_changed: titleFrom !== titleTo }
77
+ : {}),
73
78
  tags_from: currentTags,
74
79
  tags_to: tagsTo,
75
80
  tags_changed: Array.isArray(args.tagsInput) ? !sameNormalizedSet(currentTags, tagsTo) : false,
@@ -81,7 +86,7 @@ export async function buildNoteUpdateDryRunPreview(args) {
81
86
  starred_changed: typeof args.starred === 'boolean' ? note.is_starred !== starredTo : false,
82
87
  };
83
88
  });
84
- const changedCount = changes.filter((item) => item.tags_changed || item.boxes_changed || item.starred_changed).length;
89
+ const changedCount = changes.filter((item) => item.title_changed === true || item.tags_changed || item.boxes_changed || item.starred_changed).length;
85
90
  return {
86
91
  dry_run: true,
87
92
  command: args.commandName,
@@ -91,6 +96,7 @@ export async function buildNoteUpdateDryRunPreview(args) {
91
96
  warnings: buildExecutionWarnings(args.config, args.offline),
92
97
  inputs: {
93
98
  ids: args.ids,
99
+ ...(typeof args.title === 'string' ? { title: args.title } : {}),
94
100
  ...(Array.isArray(args.tagsInput) ? { tags: nextTags } : {}),
95
101
  ...(Array.isArray(args.zettelBoxNames) ? { boxes: normalizeDistinctStrings(args.zettelBoxNames) } : {}),
96
102
  ...(typeof args.starred === 'boolean' ? { starred: args.starred } : {}),
@@ -163,6 +163,7 @@ export type NotePreview = {
163
163
  };
164
164
  export type NoteUpdateStateRow = {
165
165
  id: string;
166
+ title: string | null;
166
167
  tags: string | null;
167
168
  zettel_boxes: string | null;
168
169
  is_del: number | null;
@@ -176,6 +177,7 @@ export type UpdateNoteResult = {
176
177
  id: string;
177
178
  updated: boolean;
178
179
  changed: string[];
180
+ title: string;
179
181
  tags: string[];
180
182
  zettelBoxIds: string[];
181
183
  is_starred: boolean;
@@ -16,6 +16,7 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
16
16
  }>;
17
17
  export declare function updateNote(db: AbstractPowerSyncDatabase, input: {
18
18
  id: string;
19
+ title?: string;
19
20
  tags?: string[];
20
21
  zettelBoxIds?: string[];
21
22
  starred?: boolean;
@@ -124,14 +124,15 @@ export async function updateNote(db, input) {
124
124
  if (!id) {
125
125
  throw new DinoxError('Note id is required');
126
126
  }
127
+ const shouldUpdateTitle = typeof input.title === 'string';
127
128
  const shouldUpdateTags = Array.isArray(input.tags);
128
129
  const shouldUpdateBoxes = Array.isArray(input.zettelBoxIds);
129
130
  const shouldUpdateStarred = typeof input.starred === 'boolean';
130
- if (!shouldUpdateTags && !shouldUpdateBoxes && !shouldUpdateStarred) {
131
- throw new DinoxError('At least one field is required: tags, boxes, or starred');
131
+ if (!shouldUpdateTitle && !shouldUpdateTags && !shouldUpdateBoxes && !shouldUpdateStarred) {
132
+ throw new DinoxError('At least one field is required: title, tags, boxes, or starred');
132
133
  }
133
134
  const existing = await db.getOptional(`
134
- SELECT id, tags, zettel_boxes, is_del, starred_at
135
+ SELECT id, title, tags, zettel_boxes, is_del, starred_at
135
136
  FROM c_note
136
137
  WHERE id = ?
137
138
  `, [id]);
@@ -141,20 +142,24 @@ export async function updateNote(db, input) {
141
142
  if (typeof existing.is_del === 'number' && existing.is_del !== 0) {
142
143
  throw new DinoxError(`Note has been deleted: ${id}`);
143
144
  }
145
+ const currentTitle = existing.title ?? '';
144
146
  const currentTags = parseStoredTags(existing.tags);
145
147
  const currentZettelBoxIds = parseZettelBoxIds(existing.zettel_boxes);
146
148
  const currentStarred = existing.starred_at !== null;
149
+ const nextTitle = shouldUpdateTitle ? input.title : currentTitle;
147
150
  const nextTags = shouldUpdateTags ? normalizeDistinctStrings(input.tags) : currentTags;
148
151
  const nextZettelBoxIds = shouldUpdateBoxes ? normalizeDistinctStrings(input.zettelBoxIds) : currentZettelBoxIds;
149
152
  const nextStarred = shouldUpdateStarred ? input.starred : currentStarred;
153
+ const titleChanged = shouldUpdateTitle && currentTitle !== nextTitle;
150
154
  const tagsChanged = shouldUpdateTags && !sameTagSet(currentTags, nextTags);
151
155
  const boxesChanged = shouldUpdateBoxes && !sameStringSet(currentZettelBoxIds, nextZettelBoxIds);
152
156
  const starredChanged = shouldUpdateStarred && currentStarred !== nextStarred;
153
- if (!tagsChanged && !boxesChanged && !starredChanged) {
157
+ if (!titleChanged && !tagsChanged && !boxesChanged && !starredChanged) {
154
158
  return {
155
159
  id,
156
160
  updated: false,
157
161
  changed: [],
162
+ title: currentTitle,
158
163
  tags: currentTags,
159
164
  zettelBoxIds: currentZettelBoxIds,
160
165
  is_starred: currentStarred,
@@ -162,6 +167,10 @@ export async function updateNote(db, input) {
162
167
  }
163
168
  const setParts = [];
164
169
  const params = [];
170
+ if (titleChanged) {
171
+ setParts.push('title = ?');
172
+ params.push(nextTitle);
173
+ }
165
174
  if (tagsChanged) {
166
175
  setParts.push('tags = ?');
167
176
  params.push(JSON.stringify(nextTags));
@@ -190,10 +199,12 @@ export async function updateNote(db, input) {
190
199
  id,
191
200
  updated: true,
192
201
  changed: [
202
+ ...(titleChanged ? ['title'] : []),
193
203
  ...(tagsChanged ? ['tags'] : []),
194
204
  ...(boxesChanged ? ['boxes'] : []),
195
205
  ...(starredChanged ? ['is_starred'] : []),
196
206
  ],
207
+ title: nextTitle,
197
208
  tags: nextTags,
198
209
  zettelBoxIds: nextZettelBoxIds,
199
210
  is_starred: nextStarred,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {