@dinoxx/dinox-cli 2.1.1 → 2.3.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 (68) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/commands/domain/options.d.ts +10 -0
  3. package/dist/commands/domain/options.js +75 -0
  4. package/dist/commands/domain/run.d.ts +24 -0
  5. package/dist/commands/domain/run.js +137 -0
  6. package/dist/commands/notes/actions.js +72 -20
  7. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  8. package/dist/commands/notes/createFromTemplate.js +58 -0
  9. package/dist/commands/notes/descriptors.js +12 -5
  10. package/dist/commands/notes/dryRun.d.ts +1 -0
  11. package/dist/commands/notes/dryRun.js +8 -2
  12. package/dist/commands/notes/repo/read.js +4 -23
  13. package/dist/commands/notes/repo/shared.d.ts +2 -0
  14. package/dist/commands/notes/repo/write.d.ts +3 -0
  15. package/dist/commands/notes/repo/write.js +16 -5
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +29 -5
  28. package/dist/commands/schema/spec.js +4 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/skills/registry.js +7 -0
  36. package/dist/utils/errorCodes.d.ts +212 -0
  37. package/dist/utils/errorCodes.js +53 -0
  38. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  39. package/dist/vendor/electron/domain/note-templates.js +232 -0
  40. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  41. package/dist/vendor/electron/domain/properties.js +1538 -0
  42. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  43. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  44. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  45. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  46. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  47. package/dist/vendor/electron/domain/saved-views.js +1117 -0
  48. package/dist/vendor/electron/rpc.d.ts +22 -0
  49. package/dist/vendor/electron/rpc.js +27 -0
  50. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  51. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  52. package/dist/vendor/electron/shared/external-types.d.ts +2 -0
  53. package/dist/vendor/electron/shared/external-types.js +1 -0
  54. package/dist/vendor/electron/shared/index.d.ts +9 -0
  55. package/dist/vendor/electron/shared/index.js +7 -0
  56. package/dist/vendor/electron/shared/note-properties.d.ts +312 -0
  57. package/dist/vendor/electron/shared/note-properties.js +982 -0
  58. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  59. package/dist/vendor/electron/shared/note-templates.js +84 -0
  60. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1710 -0
  61. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  62. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  63. package/dist/vendor/electron/shared/saved-views.js +981 -0
  64. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  65. package/dist/vendor/electron/sql-utils.js +32 -0
  66. package/package.json +6 -3
  67. package/dist/commands/savedViews/semantics.d.ts +0 -46
  68. package/dist/commands/savedViews/semantics.js +0 -738
@@ -1,95 +1,65 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
- import { readStringOrFile } from '../../utils/argValue.js';
3
2
  import { printDryRunPreview } from '../../utils/dryRun.js';
4
3
  import { DinoxError } from '../../utils/errors.js';
5
4
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
6
5
  import { registerCommandGroup } from '../descriptor/register.js';
6
+ import { assertExclusive, parseIntegerOption, parseNonNegativeIntegerOption, parsePositiveIntegerOption, parseTimeZoneOption, readJsonOption, requiredArgument, } from '../domain/options.js';
7
7
  import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
8
8
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS, } from '../schema/globalOptions.js';
9
9
  import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
10
- import { validateSavedViewConfig, validateSavedViewFilter, } from './model.js';
11
- import { countSavedViewRows, createSavedView, deleteSavedView, getSavedView, getSavedViewFieldCatalog, inspectCreateSavedView, inspectDeleteSavedView, inspectUpdateSavedView, listSavedViews, querySavedView, updateSavedView, } from './repo.js';
10
+ import { SAVED_VIEW_LAYOUTS, validateSavedViewConfig, validateSavedViewFilter, } from './model.js';
11
+ import { countSavedViewRows, createSavedView, deleteSavedView, getNoteDetailSections, getSavedView, getSavedViewAnalytics, listSavedViewFields, listSavedViews, previewCreateSavedView, previewDeleteSavedView, previewUpdateSavedView, querySavedView, updateSavedView, } from './repo.js';
12
12
  const VIEW_GLOBAL_OPTIONS = [
13
13
  ...FORMAT_OPTIONS,
14
14
  ...OFFLINE_OPTIONS,
15
15
  ...REQUIRE_SYNC_OPTIONS,
16
16
  ...SYNC_TIMEOUT_OPTIONS,
17
17
  ];
18
- function parseIntegerOption(value) {
19
- const normalized = value.trim();
20
- if (!/^-?\d+$/.test(normalized)) {
21
- throw new DinoxError('Expected an integer', { code: 'USAGE_ERROR' });
22
- }
23
- const parsed = Number(normalized);
24
- if (!Number.isSafeInteger(parsed)) {
25
- throw new DinoxError('Expected a safe integer', { code: 'USAGE_ERROR' });
26
- }
27
- return parsed;
28
- }
29
- function parsePositiveIntegerOption(value) {
30
- const parsed = parseIntegerOption(value);
31
- if (parsed <= 0)
32
- throw new DinoxError('Expected a positive integer', { code: 'USAGE_ERROR' });
33
- return parsed;
34
- }
35
- function parseNonNegativeIntegerOption(value) {
36
- const parsed = parseIntegerOption(value);
37
- if (parsed < 0)
38
- throw new DinoxError('Expected a non-negative integer', { code: 'USAGE_ERROR' });
39
- return parsed;
40
- }
41
- async function readJsonOption(value, label) {
42
- let raw;
43
- try {
44
- raw = await readStringOrFile(value);
45
- }
46
- catch (error) {
47
- throw new DinoxError(`Failed to read ${label}`, { code: 'USAGE_ERROR', cause: error });
48
- }
49
- try {
50
- return JSON.parse(raw);
51
- }
52
- catch (error) {
53
- throw new DinoxError(`${label} must be valid JSON`, {
54
- code: 'USAGE_ERROR',
55
- cause: error,
56
- suggestion: `Pass inline JSON or use @file. See \`dino schema view.create --format json\`.`,
57
- });
58
- }
59
- }
18
+ const TIME_ZONE_OPTION = {
19
+ flags: '--time-zone <iana>',
20
+ description: 'IANA time zone for dynamic date filters (isToday, isThisWeek, withinLastDays); defaults to the system zone',
21
+ parser: parseTimeZoneOption,
22
+ type: 'string',
23
+ };
24
+ const LAYOUT_OPTION_DESCRIPTION = `View layout (${SAVED_VIEW_LAYOUTS.join(', ')}); overrides config.layout`;
60
25
  export async function parseSavedViewFilterOption(value) {
61
26
  if (value === undefined)
62
27
  return undefined;
63
- if (typeof value !== 'string' || !value.trim()) {
64
- throw new DinoxError('--filter requires inline JSON or @file', { code: 'USAGE_ERROR' });
65
- }
66
- return validateSavedViewFilter(await readJsonOption(value, 'Saved view filter'));
28
+ return validateSavedViewFilter(await readJsonOption(value, '--filter', 'view.create'));
67
29
  }
68
30
  export async function parseSavedViewConfigOption(value) {
69
31
  if (value === undefined)
70
32
  return undefined;
71
- if (typeof value !== 'string' || !value.trim()) {
72
- throw new DinoxError('--config requires inline JSON or @file', { code: 'USAGE_ERROR' });
33
+ return validateSavedViewConfig(await readJsonOption(value, '--config', 'view.create'));
34
+ }
35
+ function parseLayoutOption(value) {
36
+ const normalized = value.trim();
37
+ if (!SAVED_VIEW_LAYOUTS.includes(normalized)) {
38
+ throw new DinoxError(`--layout must be one of ${SAVED_VIEW_LAYOUTS.join(', ')}`, { code: 'USAGE_ERROR' });
73
39
  }
74
- return validateSavedViewConfig(await readJsonOption(value, 'Saved view config'));
40
+ return normalized;
75
41
  }
76
- function requiredViewId(context) {
77
- const id = context.args.id?.trim();
78
- if (!id)
79
- throw new DinoxError('Saved view id is required', { code: 'USAGE_ERROR' });
80
- return id;
42
+ function inspectionOutput(inspection) {
43
+ return {
44
+ view: inspection.view,
45
+ valid: inspection.valid,
46
+ ...(inspection.valid ? {} : { error: inspection.error }),
47
+ };
81
48
  }
82
- function assertExclusiveOptions(options) {
83
- const conflicts = [
84
- [Boolean(options.emoji !== undefined && options.clearEmoji), '--emoji cannot be used with --clear-emoji'],
85
- [Boolean(options.templateId !== undefined && options.clearTemplate), '--template-id cannot be used with --clear-template'],
86
- [Boolean(options.groupId !== undefined && options.clearGroup), '--group-id cannot be used with --clear-group'],
87
- [Boolean(options.sortRank !== undefined && options.clearSortRank), '--sort-rank cannot be used with --clear-sort-rank'],
88
- [Boolean(options.pinnedAt !== undefined && options.unpin), '--pinned-at cannot be used with --unpin'],
89
- ];
90
- const conflict = conflicts.find(([active]) => active);
91
- if (conflict)
92
- throw new DinoxError(conflict[1], { code: 'USAGE_ERROR' });
49
+ function printPreview(command, action, summary, preview, runtime) {
50
+ const target = preview.before?.view ?? preview.after?.view;
51
+ printDryRunPreview({
52
+ dry_run: true,
53
+ command,
54
+ action,
55
+ risk: 'write',
56
+ summary,
57
+ ...(target ? { targets: [{ id: target.id, name: target.name }] } : {}),
58
+ changes: {
59
+ before: preview.before ? inspectionOutput(preview.before) : null,
60
+ after: preview.after ? inspectionOutput(preview.after) : null,
61
+ },
62
+ }, runtime);
93
63
  }
94
64
  async function runViewList(context) {
95
65
  const limit = context.options.limit;
@@ -102,7 +72,7 @@ async function runViewList(context) {
102
72
  return;
103
73
  }
104
74
  for (const view of views) {
105
- printHumanReadableLine(`${view.id}\t${view.emoji ?? ''}\t${view.name}${view.valid ? '' : '\t[invalid]'}`);
75
+ printHumanReadableLine(`${view.id}\t${view.emoji ?? ''}\t${view.layout ?? ''}\t${view.name}${view.valid ? '' : '\t[invalid]'}`);
106
76
  }
107
77
  if (views.length === 0)
108
78
  printHumanReadableLine('No saved views found.');
@@ -110,19 +80,19 @@ async function runViewList(context) {
110
80
  });
111
81
  }
112
82
  async function runViewGet(context) {
113
- const id = requiredViewId(context);
83
+ const id = requiredArgument(context.args.id, 'Saved view id');
114
84
  await withCommandRuntime(context, async (runtime) => {
115
85
  const identity = await resolveAuthIdentity(runtime.config);
116
86
  await syncBeforeCommand(runtime);
117
- const view = await getSavedView(runtime.db, { id, userId: identity.userId });
118
- printSuccess({ ok: true, stale: commandStale(runtime), view });
87
+ const inspection = await getSavedView(runtime.db, { id, userId: identity.userId });
88
+ printSuccess({ ok: true, stale: commandStale(runtime), ...inspectionOutput(inspection) });
119
89
  });
120
90
  }
121
91
  async function runViewFields(context) {
122
92
  await withCommandRuntime(context, async (runtime) => {
123
93
  const identity = await resolveAuthIdentity(runtime.config);
124
94
  await syncBeforeCommand(runtime);
125
- const fields = await getSavedViewFieldCatalog(runtime.db, identity.userId);
95
+ const fields = await listSavedViewFields(runtime.db, identity.userId);
126
96
  if (getRequestedStructuredOutputFormat()) {
127
97
  printSuccess({ ok: true, stale: commandStale(runtime), count: fields.length, fields });
128
98
  return;
@@ -134,9 +104,10 @@ async function runViewFields(context) {
134
104
  });
135
105
  }
136
106
  async function runViewQuery(context) {
137
- const viewId = requiredViewId(context);
107
+ const viewId = requiredArgument(context.args.id, 'Saved view id');
138
108
  const limit = context.options.limit;
139
109
  const offset = context.options.offset;
110
+ const timeZone = context.options.timeZone;
140
111
  await withCommandRuntime(context, async (runtime) => {
141
112
  const identity = await resolveAuthIdentity(runtime.config);
142
113
  await syncBeforeCommand(runtime);
@@ -145,6 +116,7 @@ async function runViewQuery(context) {
145
116
  userId: identity.userId,
146
117
  limit,
147
118
  offset,
119
+ timeZone,
148
120
  });
149
121
  if (getRequestedStructuredOutputFormat()) {
150
122
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
@@ -158,11 +130,12 @@ async function runViewQuery(context) {
158
130
  });
159
131
  }
160
132
  async function runViewCount(context) {
161
- const viewId = requiredViewId(context);
133
+ const viewId = requiredArgument(context.args.id, 'Saved view id');
134
+ const timeZone = context.options.timeZone;
162
135
  await withCommandRuntime(context, async (runtime) => {
163
136
  const identity = await resolveAuthIdentity(runtime.config);
164
137
  await syncBeforeCommand(runtime);
165
- const totalCount = await countSavedViewRows(runtime.db, { viewId, userId: identity.userId });
138
+ const totalCount = await countSavedViewRows(runtime.db, { viewId, userId: identity.userId, timeZone });
166
139
  if (getRequestedStructuredOutputFormat()) {
167
140
  printSuccess({ ok: true, stale: commandStale(runtime), viewId, totalCount });
168
141
  return;
@@ -171,134 +144,159 @@ async function runViewCount(context) {
171
144
  printStaleCacheWarning(runtime);
172
145
  });
173
146
  }
174
- async function runViewCreate(context) {
175
- const options = context.options;
176
- const name = options.name?.trim() ?? '';
177
- if (!name)
178
- throw new DinoxError('Saved view name is required. Use --name <string>.', { code: 'USAGE_ERROR' });
179
- const [filter, config] = await Promise.all([
180
- parseSavedViewFilterOption(options.filter),
181
- parseSavedViewConfigOption(options.config),
182
- ]);
147
+ async function runViewAnalytics(context) {
148
+ const viewId = requiredArgument(context.args.id, 'Saved view id');
149
+ const timeZone = context.options.timeZone;
183
150
  await withCommandRuntime(context, async (runtime) => {
184
151
  const identity = await resolveAuthIdentity(runtime.config);
185
152
  await syncBeforeCommand(runtime);
186
- const input = {
187
- userId: identity.userId,
188
- name,
189
- emoji: options.emoji,
190
- filter,
191
- config,
192
- templateId: options.templateId,
193
- groupId: options.groupId,
194
- sortRank: options.sortRank,
195
- pinnedAt: options.pinnedAt,
196
- };
197
- if (options.dryRun) {
198
- const plan = await inspectCreateSavedView(runtime.db, input);
199
- printDryRunPreview({
200
- dry_run: true,
201
- command: 'dino view create',
202
- action: 'create_saved_view',
203
- risk: 'write',
204
- summary: `Create saved view "${name}".`,
205
- inputs: { name, ...(filter ? { filter } : {}), ...(config ? { config } : {}) },
206
- changes: plan.changes,
207
- }, runtime);
153
+ const analytics = await getSavedViewAnalytics(runtime.db, { viewId, userId: identity.userId, timeZone });
154
+ if (getRequestedStructuredOutputFormat()) {
155
+ printSuccess({ ok: true, stale: commandStale(runtime), viewId, ...analytics });
208
156
  return;
209
157
  }
210
- const view = await createSavedView(runtime.db, input);
211
- const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
158
+ for (const stat of analytics.stats) {
159
+ printHumanReadableLine(`stat\t${stat.id}\t${stat.unavailable ? 'unavailable' : String(stat.value)}`);
160
+ }
161
+ for (const chart of analytics.charts) {
162
+ printHumanReadableLine(`chart\t${chart.id}\t${chart.unavailable ? 'unavailable' : `${chart.rows.length} rows`}`);
163
+ }
164
+ for (const item of analytics.summary) {
165
+ printHumanReadableLine(`summary\t${item.column}\t${item.aggregation}\t${item.unavailable ? 'unavailable' : String(item.value)}`);
166
+ }
167
+ if (analytics.stats.length + analytics.charts.length + analytics.summary.length === 0) {
168
+ printHumanReadableLine('This view has no stats, charts, or summary configured.');
169
+ }
170
+ printStaleCacheWarning(runtime);
171
+ });
172
+ }
173
+ async function runViewNoteDetail(context) {
174
+ const noteId = requiredArgument(context.args.noteId, 'Note id');
175
+ const limit = context.options.limit;
176
+ const timeZone = context.options.timeZone;
177
+ await withCommandRuntime(context, async (runtime) => {
178
+ const identity = await resolveAuthIdentity(runtime.config);
179
+ await syncBeforeCommand(runtime);
180
+ const sections = await getNoteDetailSections(runtime.db, { noteId, userId: identity.userId, limit, timeZone });
212
181
  if (getRequestedStructuredOutputFormat()) {
213
- printSuccess({ ok: true, stale: commandStale(runtime), view, ...receipt });
182
+ printSuccess({ ok: true, stale: commandStale(runtime), noteId, count: sections.length, sections });
214
183
  return;
215
184
  }
216
- printHumanReadableLine(view.id);
185
+ for (const section of sections) {
186
+ printHumanReadableLine(`${section.viewId}\t${section.emoji ?? ''}\t${section.name}\t${section.totalCount} notes`);
187
+ for (const row of section.rows)
188
+ printHumanReadableLine(` ${row.id}\t${row.title ?? ''}`);
189
+ }
190
+ if (sections.length === 0)
191
+ printHumanReadableLine('No saved view embeds this note.');
217
192
  printStaleCacheWarning(runtime);
218
193
  });
219
194
  }
220
- async function runViewUpdate(context) {
221
- const id = requiredViewId(context);
222
- const options = context.options;
223
- assertExclusiveOptions(options);
195
+ async function readWriteInput(options) {
224
196
  const [filter, config] = await Promise.all([
225
197
  parseSavedViewFilterOption(options.filter),
226
198
  parseSavedViewConfigOption(options.config),
227
199
  ]);
228
- const inputPatch = {
200
+ return {
229
201
  ...(options.name === undefined ? {} : { name: options.name }),
230
202
  ...(options.clearEmoji ? { emoji: null } : options.emoji === undefined ? {} : { emoji: options.emoji }),
231
203
  ...(filter === undefined ? {} : { filter }),
232
204
  ...(config === undefined ? {} : { config }),
205
+ ...(options.layout === undefined ? {} : { layout: parseLayoutOption(options.layout) }),
233
206
  ...(options.clearTemplate ? { templateId: null } : options.templateId === undefined ? {} : { templateId: options.templateId }),
234
207
  ...(options.clearGroup ? { groupId: null } : options.groupId === undefined ? {} : { groupId: options.groupId }),
235
208
  ...(options.clearSortRank ? { sortRank: null } : options.sortRank === undefined ? {} : { sortRank: options.sortRank }),
236
209
  ...(options.unpin ? { pinnedAt: null } : options.pinnedAt === undefined ? {} : { pinnedAt: options.pinnedAt }),
237
210
  };
211
+ }
212
+ async function runViewCreate(context) {
213
+ const options = context.options;
214
+ const name = options.name?.trim() ?? '';
215
+ if (!name)
216
+ throw new DinoxError('Saved view name is required. Use --name <string>.', { code: 'USAGE_ERROR' });
217
+ const patch = await readWriteInput(options);
238
218
  await withCommandRuntime(context, async (runtime) => {
239
219
  const identity = await resolveAuthIdentity(runtime.config);
240
220
  await syncBeforeCommand(runtime);
241
- const input = { id, userId: identity.userId, ...inputPatch };
221
+ const input = { ...patch, userId: identity.userId, name };
242
222
  if (options.dryRun) {
243
- const plan = await inspectUpdateSavedView(runtime.db, input);
244
- printDryRunPreview({
245
- dry_run: true,
246
- command: 'dino view update',
247
- action: 'update_saved_view',
248
- risk: 'write',
249
- summary: `Update saved view "${plan.target?.name ?? id}".`,
250
- targets: plan.target ? [{ id: plan.target.id, name: plan.target.name }] : undefined,
251
- changes: plan.changes,
252
- }, runtime);
223
+ const preview = await previewCreateSavedView(runtime.db, input);
224
+ printPreview('dino view create', 'create_saved_view', `Create saved view "${name}".`, preview, runtime);
253
225
  return;
254
226
  }
255
- const view = await updateSavedView(runtime.db, input);
227
+ const inspection = await createSavedView(runtime.db, input);
256
228
  const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
257
229
  if (getRequestedStructuredOutputFormat()) {
258
- printSuccess({ ok: true, stale: commandStale(runtime), view, ...receipt });
230
+ printSuccess({ ok: true, stale: commandStale(runtime), ...inspectionOutput(inspection), ...receipt });
259
231
  return;
260
232
  }
261
- printHumanReadableLine(view.id);
233
+ printHumanReadableLine(inspection.view.id);
234
+ printStaleCacheWarning(runtime);
235
+ });
236
+ }
237
+ async function runViewUpdate(context) {
238
+ const id = requiredArgument(context.args.id, 'Saved view id');
239
+ const options = context.options;
240
+ assertExclusive([
241
+ [options.emoji !== undefined && Boolean(options.clearEmoji), '--emoji cannot be used with --clear-emoji'],
242
+ [options.templateId !== undefined && Boolean(options.clearTemplate), '--template-id cannot be used with --clear-template'],
243
+ [options.groupId !== undefined && Boolean(options.clearGroup), '--group-id cannot be used with --clear-group'],
244
+ [options.sortRank !== undefined && Boolean(options.clearSortRank), '--sort-rank cannot be used with --clear-sort-rank'],
245
+ [options.pinnedAt !== undefined && Boolean(options.unpin), '--pinned-at cannot be used with --unpin'],
246
+ ]);
247
+ const patch = await readWriteInput(options);
248
+ if (Object.keys(patch).length === 0) {
249
+ throw new DinoxError('At least one saved-view field must be updated', { code: 'USAGE_ERROR' });
250
+ }
251
+ await withCommandRuntime(context, async (runtime) => {
252
+ const identity = await resolveAuthIdentity(runtime.config);
253
+ await syncBeforeCommand(runtime);
254
+ const input = { ...patch, id, userId: identity.userId };
255
+ if (options.dryRun) {
256
+ const preview = await previewUpdateSavedView(runtime.db, input);
257
+ printPreview('dino view update', 'update_saved_view', `Update saved view "${preview.before?.view.name ?? id}".`, preview, runtime);
258
+ return;
259
+ }
260
+ const inspection = await updateSavedView(runtime.db, input);
261
+ const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
262
+ if (getRequestedStructuredOutputFormat()) {
263
+ printSuccess({ ok: true, stale: commandStale(runtime), ...inspectionOutput(inspection), ...receipt });
264
+ return;
265
+ }
266
+ printHumanReadableLine(inspection.view.id);
262
267
  printStaleCacheWarning(runtime);
263
268
  });
264
269
  }
265
270
  async function runViewDelete(context) {
266
- const id = requiredViewId(context);
271
+ const id = requiredArgument(context.args.id, 'Saved view id');
267
272
  const options = context.options;
268
273
  await withCommandRuntime(context, async (runtime) => {
269
274
  const identity = await resolveAuthIdentity(runtime.config);
270
275
  await syncBeforeCommand(runtime);
271
276
  const input = { id, userId: identity.userId };
272
277
  if (options.dryRun) {
273
- const plan = await inspectDeleteSavedView(runtime.db, input);
274
- printDryRunPreview({
275
- dry_run: true,
276
- command: 'dino view delete',
277
- action: 'delete_saved_view',
278
- risk: 'write',
279
- summary: `Soft-delete saved view "${plan.target?.name ?? id}".`,
280
- targets: plan.target ? [{ id: plan.target.id, name: plan.target.name }] : undefined,
281
- changes: plan.changes,
282
- }, runtime);
278
+ const preview = await previewDeleteSavedView(runtime.db, input);
279
+ printPreview('dino view delete', 'delete_saved_view', `Soft-delete saved view "${preview.before?.view.name ?? id}".`, preview, runtime);
283
280
  return;
284
281
  }
285
- const view = await deleteSavedView(runtime.db, input);
282
+ const inspection = await deleteSavedView(runtime.db, input);
286
283
  const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
287
284
  if (getRequestedStructuredOutputFormat()) {
288
- printSuccess({ ok: true, stale: commandStale(runtime), deleted: true, view, ...receipt });
285
+ printSuccess({ ok: true, stale: commandStale(runtime), deleted: true, view: inspection.view, ...receipt });
289
286
  return;
290
287
  }
291
- printHumanReadableLine(`Deleted ${view.id}`);
288
+ printHumanReadableLine(`Deleted ${inspection.view.id}`);
292
289
  printStaleCacheWarning(runtime);
293
290
  });
294
291
  }
292
+ const WRITE_OUTPUT_FIELDS = ['view', 'valid', 'error', 'durability', 'upload_queue_remaining', 'stale'];
295
293
  export const savedViewCommandGroup = {
296
294
  name: 'view',
297
- description: 'Manage and query saved table views',
295
+ description: 'Manage and query saved views (table, list, card) and their analytics',
298
296
  commands: [
299
297
  {
300
298
  name: 'list',
301
- description: 'List active saved table views',
299
+ description: 'List active saved views',
302
300
  options: [
303
301
  { flags: '--limit <n>', description: 'Maximum returned views (1-5000)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 200 },
304
302
  ],
@@ -307,28 +305,28 @@ export const savedViewCommandGroup = {
307
305
  risk: 'read',
308
306
  auth: 'required',
309
307
  globalOptions: VIEW_GLOBAL_OPTIONS,
310
- output: { type: 'structured', summary: 'Saved view catalog', fields: ['stale', 'count', 'views'] },
308
+ output: { type: 'structured', summary: 'Saved view catalog in desktop sidebar order; invalid rows are listed with valid=false', fields: ['stale', 'count', 'views'] },
311
309
  examples: ['dino view list --format json'],
312
310
  },
313
311
  action: runViewList,
314
312
  },
315
313
  {
316
314
  name: 'get',
317
- description: 'Get one saved view definition',
315
+ description: 'Get one saved view definition (stored rows the CLI cannot parse return valid=false with the raw JSON)',
318
316
  arguments: [{ syntax: '<id>', description: 'Saved view id' }],
319
317
  schema: {
320
318
  path: 'view.get',
321
319
  risk: 'read',
322
320
  auth: 'required',
323
321
  globalOptions: VIEW_GLOBAL_OPTIONS,
324
- output: { type: 'structured', summary: 'Canonical saved view definition', fields: ['stale', 'view'] },
322
+ output: { type: 'structured', summary: 'Saved view definition with validity', fields: ['stale', 'view', 'valid', 'error'] },
325
323
  examples: ['dino view get <id> --format json'],
326
324
  },
327
325
  action: runViewGet,
328
326
  },
329
327
  {
330
328
  name: 'fields',
331
- description: 'List system and property fields available to saved views',
329
+ description: 'List system and property fields with their filter operators, sortability, and option ids',
332
330
  schema: {
333
331
  path: 'view.fields',
334
332
  risk: 'read',
@@ -346,14 +344,18 @@ export const savedViewCommandGroup = {
346
344
  options: [
347
345
  { flags: '--limit <n>', description: 'Maximum returned notes (1-500)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 100 },
348
346
  { flags: '--offset <n>', description: 'Result offset for pagination', parser: parseNonNegativeIntegerOption, type: 'number', defaultValue: 0 },
347
+ TIME_ZONE_OPTION,
349
348
  ],
350
349
  schema: {
351
350
  path: 'view.query',
352
351
  risk: 'read',
353
352
  auth: 'required',
354
353
  globalOptions: VIEW_GLOBAL_OPTIONS,
355
- output: { type: 'structured', summary: 'Saved view page with projected configured columns', fields: ['stale', 'view', 'columns', 'totalCount', 'offset', 'limit', 'nextOffset', 'hasMore', 'rows'] },
356
- examples: ['dino view query <id> --limit 50 --offset 0 --format json'],
354
+ output: { type: 'structured', summary: 'Saved view page with projected configured and computed columns', fields: ['stale', 'view', 'columns', 'totalCount', 'offset', 'limit', 'nextOffset', 'hasMore', 'rows'] },
355
+ examples: [
356
+ 'dino view query <id> --limit 50 --offset 0 --format json',
357
+ 'dino view query <id> --time-zone Asia/Shanghai --format json',
358
+ ],
357
359
  },
358
360
  action: runViewQuery,
359
361
  },
@@ -361,6 +363,7 @@ export const savedViewCommandGroup = {
361
363
  name: 'count',
362
364
  description: 'Count notes matching a saved view',
363
365
  arguments: [{ syntax: '<id>', description: 'Saved view id' }],
366
+ options: [TIME_ZONE_OPTION],
364
367
  schema: {
365
368
  path: 'view.count',
366
369
  risk: 'read',
@@ -371,44 +374,79 @@ export const savedViewCommandGroup = {
371
374
  },
372
375
  action: runViewCount,
373
376
  },
377
+ {
378
+ name: 'analytics',
379
+ description: 'Compute the stats, charts, and summary row configured on a saved view',
380
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
381
+ options: [TIME_ZONE_OPTION],
382
+ schema: {
383
+ path: 'view.analytics',
384
+ risk: 'read',
385
+ auth: 'required',
386
+ globalOptions: VIEW_GLOBAL_OPTIONS,
387
+ output: { type: 'structured', summary: 'Aggregates over the full view scope (not the visible page)', fields: ['stale', 'viewId', 'stats', 'charts', 'summary', 'config'] },
388
+ examples: ['dino view analytics <id> --format json'],
389
+ },
390
+ action: runViewAnalytics,
391
+ },
392
+ {
393
+ name: 'note-detail',
394
+ description: 'List saved views embedded on a note (views with a noteDetail rule, $self bound to the note)',
395
+ arguments: [{ syntax: '<noteId>', description: 'Host note id' }],
396
+ options: [
397
+ { flags: '--limit <n>', description: 'Maximum rows per section (1-100)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 6 },
398
+ TIME_ZONE_OPTION,
399
+ ],
400
+ schema: {
401
+ path: 'view.note-detail',
402
+ risk: 'read',
403
+ auth: 'required',
404
+ globalOptions: VIEW_GLOBAL_OPTIONS,
405
+ output: { type: 'structured', summary: 'Note-detail sections with projected rows', fields: ['stale', 'noteId', 'count', 'sections'] },
406
+ examples: ['dino view note-detail <noteId> --format json'],
407
+ },
408
+ action: runViewNoteDetail,
409
+ },
374
410
  {
375
411
  name: 'create',
376
- description: 'Create a canonical saved table view',
412
+ description: 'Create a saved view',
377
413
  options: [
378
414
  { flags: '--name <string>', description: 'Saved view name', required: true, type: 'string' },
379
415
  { flags: '--emoji <string>', description: 'Optional emoji or short icon text', type: 'string' },
380
- { flags: '--filter <json|@file>', description: 'Canonical version 1 saved view filter JSON; defaults to no filter', type: 'string' },
381
- { flags: '--config <json|@file>', description: 'Canonical version 1 saved view config JSON; defaults to title + updated_at', type: 'string' },
416
+ { flags: '--filter <json|@file>', description: 'Saved view filter JSON (version 1-3); defaults to no filter', type: 'string' },
417
+ { flags: '--config <json|@file>', description: 'Saved view config JSON (columns, sort, layout, noteDetail, stats, charts, computedColumns, summary); defaults to title + updated_at', type: 'string' },
418
+ { flags: '--layout <layout>', description: LAYOUT_OPTION_DESCRIPTION, type: 'string', enum: [...SAVED_VIEW_LAYOUTS] },
382
419
  { flags: '--template-id <id>', description: 'Optional note template id associated with the view', type: 'string' },
383
420
  { flags: '--group-id <id>', description: 'Optional saved view group id', type: 'string' },
384
- { flags: '--sort-rank <n>', description: 'Explicit catalog sort rank', parser: parseIntegerOption, type: 'number' },
421
+ { flags: '--sort-rank <n>', description: 'Explicit catalog sort rank (defaults to the end of the sidebar)', parser: parseIntegerOption, type: 'number' },
385
422
  { flags: '--pinned-at <timestamp>', description: 'Pinned timestamp', type: 'string' },
386
423
  WRITE_DURABILITY_OPTION,
387
- { flags: '--dry-run', description: 'Validate and preview the view without writing', type: 'boolean' },
424
+ { flags: '--dry-run', description: 'Validate and preview the stored view without writing', type: 'boolean' },
388
425
  ],
389
426
  schema: {
390
427
  path: 'view.create',
391
428
  risk: 'write',
392
429
  auth: 'required',
393
430
  globalOptions: VIEW_GLOBAL_OPTIONS,
394
- output: { type: 'structured', summary: 'Created saved view definition or dry-run plan', fields: ['view', 'durability', 'upload_queue_remaining', 'stale'] },
431
+ output: { type: 'structured', summary: 'Created saved view definition or dry-run plan', fields: WRITE_OUTPUT_FIELDS },
395
432
  examples: [
396
433
  'dino view create --name "Recent notes" --format json --dry-run',
397
- 'dino view create --name "Active projects" --filter @filter.json --config @config.json --format json',
434
+ 'dino view create --name "Reading list" --layout card --filter @filter.json --config @config.json --format json',
398
435
  ],
399
436
  },
400
437
  action: runViewCreate,
401
438
  },
402
439
  {
403
440
  name: 'update',
404
- description: 'Update a canonical saved table view',
441
+ description: 'Update a saved view; fields you do not pass are left as stored',
405
442
  arguments: [{ syntax: '<id>', description: 'Saved view id' }],
406
443
  options: [
407
444
  { flags: '--name <string>', description: 'Replace the saved view name', type: 'string' },
408
445
  { flags: '--emoji <string>', description: 'Replace emoji or short icon text', type: 'string' },
409
446
  { flags: '--clear-emoji', description: 'Clear the saved view emoji', type: 'boolean' },
410
- { flags: '--filter <json|@file>', description: 'Replace the canonical version 1 filter JSON', type: 'string' },
411
- { flags: '--config <json|@file>', description: 'Replace the canonical version 1 config JSON', type: 'string' },
447
+ { flags: '--filter <json|@file>', description: 'Replace the filter JSON (version 1-3)', type: 'string' },
448
+ { flags: '--config <json|@file>', description: 'Replace the whole config JSON', type: 'string' },
449
+ { flags: '--layout <layout>', description: LAYOUT_OPTION_DESCRIPTION, type: 'string', enum: [...SAVED_VIEW_LAYOUTS] },
412
450
  { flags: '--template-id <id>', description: 'Replace the associated note template id', type: 'string' },
413
451
  { flags: '--clear-template', description: 'Clear the associated note template id', type: 'boolean' },
414
452
  { flags: '--group-id <id>', description: 'Replace the saved view group id', type: 'string' },
@@ -418,16 +456,17 @@ export const savedViewCommandGroup = {
418
456
  { flags: '--pinned-at <timestamp>', description: 'Replace the pinned timestamp', type: 'string' },
419
457
  { flags: '--unpin', description: 'Clear the pinned timestamp', type: 'boolean' },
420
458
  WRITE_DURABILITY_OPTION,
421
- { flags: '--dry-run', description: 'Validate and preview the update without writing', type: 'boolean' },
459
+ { flags: '--dry-run', description: 'Validate and preview the stored view without writing', type: 'boolean' },
422
460
  ],
423
461
  schema: {
424
462
  path: 'view.update',
425
463
  risk: 'write',
426
464
  auth: 'required',
427
465
  globalOptions: VIEW_GLOBAL_OPTIONS,
428
- output: { type: 'structured', summary: 'Updated saved view definition or dry-run plan', fields: ['view', 'durability', 'upload_queue_remaining', 'stale'] },
466
+ output: { type: 'structured', summary: 'Updated saved view definition or dry-run plan', fields: WRITE_OUTPUT_FIELDS },
429
467
  examples: [
430
468
  'dino view update <id> --filter @filter.json --format json --dry-run',
469
+ 'dino view update <id> --layout list --format json',
431
470
  'dino view update <id> --name "Priority work" --unpin --format json',
432
471
  ],
433
472
  },