@dinoxx/dinox-cli 1.1.0 → 2.0.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 (111) hide show
  1. package/README.md +48 -4
  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.d.ts +9 -0
  19. package/dist/commands/notes/repo.js +65 -15
  20. package/dist/commands/notes/searchOutput.d.ts +2 -1
  21. package/dist/commands/notes/searchOutput.js +2 -1
  22. package/dist/commands/notes/searchSql.js +9 -3
  23. package/dist/commands/prompt/index.d.ts +1 -0
  24. package/dist/commands/prompt/index.js +26 -11
  25. package/dist/commands/runtime.js +17 -6
  26. package/dist/commands/savedViews/index.d.ts +7 -0
  27. package/dist/commands/savedViews/index.js +458 -0
  28. package/dist/commands/savedViews/model.d.ts +87 -0
  29. package/dist/commands/savedViews/model.js +306 -0
  30. package/dist/commands/savedViews/repo.d.ts +123 -0
  31. package/dist/commands/savedViews/repo.js +512 -0
  32. package/dist/commands/savedViews/semantics.d.ts +46 -0
  33. package/dist/commands/savedViews/semantics.js +738 -0
  34. package/dist/commands/schema/renderSkills.d.ts +2 -0
  35. package/dist/commands/schema/renderSkills.js +69 -10
  36. package/dist/commands/schema/spec.js +4 -0
  37. package/dist/commands/skills/index.d.ts +54 -0
  38. package/dist/commands/skills/index.js +468 -0
  39. package/dist/commands/storage/index.js +2 -2
  40. package/dist/commands/sync.d.ts +23 -1
  41. package/dist/commands/sync.js +100 -58
  42. package/dist/commands/tags/index.js +30 -11
  43. package/dist/commands/tags/operations.d.ts +3 -3
  44. package/dist/commands/tags/operations.js +45 -20
  45. package/dist/commands/templates/model.d.ts +50 -0
  46. package/dist/commands/templates/model.js +239 -0
  47. package/dist/commands/todo/index.js +3 -3
  48. package/dist/commands/update.d.ts +1 -0
  49. package/dist/commands/update.js +46 -9
  50. package/dist/commands/writeConfirmation.d.ts +13 -0
  51. package/dist/commands/writeConfirmation.js +54 -0
  52. package/dist/config/resolve.js +5 -10
  53. package/dist/config/types.d.ts +3 -6
  54. package/dist/constants/links.d.ts +18 -1
  55. package/dist/constants/links.js +26 -1
  56. package/dist/daemon/client.d.ts +2 -2
  57. package/dist/daemon/client.js +106 -52
  58. package/dist/daemon/index.d.ts +5 -3
  59. package/dist/daemon/index.js +28 -5
  60. package/dist/daemon/lifecycle.d.ts +10 -3
  61. package/dist/daemon/lifecycle.js +54 -11
  62. package/dist/daemon/pid.d.ts +1 -1
  63. package/dist/daemon/pid.js +6 -3
  64. package/dist/dinox.js +9 -1
  65. package/dist/powersync/exclusiveRuntime.d.ts +2 -0
  66. package/dist/powersync/exclusiveRuntime.js +28 -4
  67. package/dist/powersync/runtime.d.ts +4 -2
  68. package/dist/powersync/runtime.js +43 -5
  69. package/dist/powersync/schema/content.d.ts +1 -78
  70. package/dist/powersync/schema/content.js +1 -78
  71. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  72. package/dist/powersync/schema/electronCanonical.js +1154 -0
  73. package/dist/powersync/schema/index.d.ts +573 -129
  74. package/dist/powersync/schema/index.js +3 -32
  75. package/dist/powersync/schema/local.d.ts +1 -56
  76. package/dist/powersync/schema/local.js +1 -60
  77. package/dist/powersync/schema/note.d.ts +1 -35
  78. package/dist/powersync/schema/note.js +1 -35
  79. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  80. package/dist/powersync/schema/notesExtras.js +1 -71
  81. package/dist/powersync/schema/projects.d.ts +1 -104
  82. package/dist/powersync/schema/projects.js +1 -104
  83. package/dist/powersync/schema/savedViews.d.ts +1 -0
  84. package/dist/powersync/schema/savedViews.js +1 -0
  85. package/dist/powersync/schema/storage.d.ts +1 -24
  86. package/dist/powersync/schema/storage.js +1 -24
  87. package/dist/powersync/schema/tags.d.ts +1 -37
  88. package/dist/powersync/schema/tags.js +1 -37
  89. package/dist/powersync/syncGate.d.ts +22 -3
  90. package/dist/powersync/syncGate.js +4 -1
  91. package/dist/powersync/syncPass.d.ts +2 -0
  92. package/dist/powersync/syncPass.js +15 -4
  93. package/dist/powersync/tokenIndex.d.ts +2 -0
  94. package/dist/powersync/tokenIndex.js +60 -18
  95. package/dist/powersync/uploader.d.ts +16 -2
  96. package/dist/powersync/uploader.js +152 -158
  97. package/dist/skills/check.d.ts +1 -0
  98. package/dist/skills/check.js +538 -70
  99. package/dist/skills/retired.d.ts +60 -0
  100. package/dist/skills/retired.js +58 -0
  101. package/dist/utils/deadline.d.ts +8 -0
  102. package/dist/utils/deadline.js +31 -0
  103. package/dist/utils/dryRun.d.ts +20 -1
  104. package/dist/utils/dryRun.js +22 -3
  105. package/dist/utils/errors.d.ts +2 -0
  106. package/dist/utils/errors.js +5 -3
  107. package/dist/utils/processExit.d.ts +1 -0
  108. package/dist/utils/processExit.js +33 -0
  109. package/dist/utils/updateNotice.d.ts +1 -0
  110. package/dist/utils/updateNotice.js +1 -1
  111. package/package.json +10 -5
@@ -1,31 +1,43 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { readStringOrFile } from '../../utils/argValue.js';
3
+ import { printDryRunPreview } from '../../utils/dryRun.js';
4
+ import { DinoxError } from '../../utils/errors.js';
2
5
  import { registerCommandGroup } from '../descriptor/register.js';
3
6
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
4
7
  import { buildPromptAddDryRunPreview } from '../writeDryRun.js';
5
- import { printDryRunPreview } from '../../utils/dryRun.js';
6
- import { DinoxError } from '../../utils/errors.js';
7
8
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
8
9
  import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
9
10
  import { addPrompt, inspectAddPrompt, listPrompts } from './repo.js';
10
11
  const PROMPT_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
12
+ export async function parsePromptTextInput(value) {
13
+ if (typeof value !== 'string' || !value) {
14
+ return '';
15
+ }
16
+ return (await readStringOrFile(value)).trim();
17
+ }
11
18
  async function runPromptList(context) {
12
19
  await withCommandRuntime(context, async (runtime) => {
13
20
  const identity = await resolveAuthIdentity(runtime.config);
14
21
  await syncBeforeCommand(runtime, { syncTokenIndex: true });
15
- printSuccess((await listPrompts(runtime.db, { userId: identity.userId })).map((prompt) => ({
22
+ const prompts = (await listPrompts(runtime.db, { userId: identity.userId })).map((prompt) => ({
16
23
  name: prompt.name,
17
24
  prompt: prompt.cmd,
18
- })));
25
+ }));
26
+ printSuccess({
27
+ stale: commandStale(runtime),
28
+ count: prompts.length,
29
+ prompts,
30
+ });
19
31
  });
20
32
  }
21
33
  async function runPromptAdd(context) {
22
34
  const name = typeof context.options.name === 'string' ? context.options.name.trim() : '';
23
- const cmd = typeof context.options.prompt === 'string' ? context.options.prompt.trim() : '';
35
+ const cmd = await parsePromptTextInput(context.options.prompt);
24
36
  if (!name) {
25
- throw new DinoxError('Prompt name is required. Use `dino prompt add --name <name> --prompt <text>`.');
37
+ throw new DinoxError('Prompt name is required. Use `dino prompt add --name <name> --prompt <string|@file>`.');
26
38
  }
27
39
  if (!cmd) {
28
- throw new DinoxError('Prompt text is required. Use `dino prompt add --name <name> --prompt <text>`.');
40
+ throw new DinoxError('Prompt text is required. Use `dino prompt add --name <name> --prompt <string|@file>`.');
29
41
  }
30
42
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
31
43
  await withCommandRuntime(context, async (runtime) => {
@@ -40,7 +52,7 @@ async function runPromptAdd(context) {
40
52
  cmd,
41
53
  userId: identity.userId,
42
54
  })),
43
- }));
55
+ }), runtime);
44
56
  return;
45
57
  }
46
58
  const result = await addPrompt(runtime.db, {
@@ -80,7 +92,7 @@ export const promptCommandGroup = {
80
92
  output: {
81
93
  type: 'structured',
82
94
  summary: 'Prompt list payload',
83
- fields: ['name', 'prompt'],
95
+ fields: ['stale', 'count', 'prompts.name', 'prompts.prompt'],
84
96
  },
85
97
  examples: ['dino prompt list --format json'],
86
98
  },
@@ -91,7 +103,7 @@ export const promptCommandGroup = {
91
103
  description: 'Create a prompt template, restoring a deleted prompt when possible',
92
104
  options: [
93
105
  { flags: '--name <string>', description: 'Prompt name', required: true, type: 'string' },
94
- { flags: '--prompt <string>', description: 'Prompt text', required: true, type: 'string' },
106
+ { flags: '--prompt <string|@file>', description: 'Prompt text or @file containing prompt text', required: true, type: 'string' },
95
107
  { flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
96
108
  ],
97
109
  schema: {
@@ -104,7 +116,10 @@ export const promptCommandGroup = {
104
116
  summary: 'Prompt creation acknowledgement',
105
117
  fields: ['id', 'name', 'prompt', 'restored', 'stale'],
106
118
  },
107
- examples: ['dino prompt add --name "周报助手" --prompt "请基于本周笔记输出一份简洁周报" --format json --dry-run'],
119
+ examples: [
120
+ 'dino prompt add --name "周报助手" --prompt "请基于本周笔记输出一份简洁周报" --format json --dry-run',
121
+ 'dino prompt add --name "周报助手" --prompt @/tmp/weekly-prompt.txt --format json --dry-run',
122
+ ],
108
123
  },
109
124
  action: runPromptAdd,
110
125
  },
@@ -2,9 +2,10 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
2
2
  import { loadConfig } from '../config/store.js';
3
3
  import { resolveConfig } from '../config/resolve.js';
4
4
  import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
5
- import { getStatusSnapshot, waitForIdleDataFlow, waitForLastSyncedAfter, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
5
+ import { getStatusSnapshot, waitForDownloadIdle, waitForLastSyncedAfter, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
6
6
  import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
7
7
  import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
8
+ import { createTimeoutSignal } from '../utils/abort.js';
8
9
  import { DinoxError } from '../utils/errors.js';
9
10
  import { printHumanReadableWarning } from '../utils/output.js';
10
11
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
@@ -94,20 +95,30 @@ export async function syncBeforeCommand(runtime, options = {}) {
94
95
  }
95
96
  return;
96
97
  }
97
- const idle = await waitForIdleDataFlow(runtime.db, Math.min(runtime.timeoutMs, options.timeoutMs ?? PRE_SYNC_TIMEOUT_MS));
98
- runtime.sync.preSyncIdle = idle;
98
+ const timeoutMs = Math.min(runtime.timeoutMs, options.timeoutMs ?? PRE_SYNC_TIMEOUT_MS);
99
+ const deadlineAtMs = Date.now() + timeoutMs;
100
+ const remainingTimeoutMs = () => Math.max(0, deadlineAtMs - Date.now());
99
101
  const recentSync = runtime.requireSync
100
- ? await waitForLastSyncedAfter(runtime.db, runtime.sync.startedAtIso, Math.min(runtime.timeoutMs, options.timeoutMs ?? PRE_SYNC_TIMEOUT_MS))
102
+ ? await waitForLastSyncedAfter(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
101
103
  : true;
102
104
  runtime.sync.recentSync = recentSync;
105
+ const downloadIdle = await waitForDownloadIdle(runtime.db, Math.max(1, remainingTimeoutMs()));
106
+ runtime.sync.preSyncIdle = downloadIdle;
107
+ let indexComplete;
103
108
  if (options.syncTokenIndex) {
104
- await syncNoteTokenIndex(runtime.db);
109
+ const indexTimeoutMs = remainingTimeoutMs();
110
+ const timeout = indexTimeoutMs > 0 ? createTimeoutSignal(indexTimeoutMs) : null;
111
+ const tokenIndex = await syncNoteTokenIndex(runtime.db, {
112
+ signal: timeout?.signal ?? AbortSignal.abort(),
113
+ }).finally(() => timeout?.cancel());
114
+ indexComplete = tokenIndex.complete;
105
115
  }
106
116
  const status = getStatusSnapshot(runtime.db);
107
117
  const gate = evaluateSyncGate({
108
118
  offline: runtime.offline,
109
119
  firstSyncStale: runtime.sync.firstSyncStale,
110
- idle,
120
+ idle: downloadIdle,
121
+ indexComplete,
111
122
  recentSync,
112
123
  status,
113
124
  });
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ import { type SavedViewConfig, type SavedViewFilter } from './model.js';
4
+ export declare function parseSavedViewFilterOption(value: unknown): Promise<SavedViewFilter | undefined>;
5
+ export declare function parseSavedViewConfigOption(value: unknown): Promise<SavedViewConfig | undefined>;
6
+ export declare const savedViewCommandGroup: CommandGroupDescriptor;
7
+ export declare function registerSavedViewCommands(program: Command): void;
@@ -0,0 +1,458 @@
1
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
+ import { readStringOrFile } from '../../utils/argValue.js';
3
+ import { printDryRunPreview } from '../../utils/dryRun.js';
4
+ import { DinoxError } from '../../utils/errors.js';
5
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
6
+ import { registerCommandGroup } from '../descriptor/register.js';
7
+ import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
8
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS, } from '../schema/globalOptions.js';
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';
12
+ const VIEW_GLOBAL_OPTIONS = [
13
+ ...FORMAT_OPTIONS,
14
+ ...OFFLINE_OPTIONS,
15
+ ...REQUIRE_SYNC_OPTIONS,
16
+ ...SYNC_TIMEOUT_OPTIONS,
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
+ }
60
+ export async function parseSavedViewFilterOption(value) {
61
+ if (value === undefined)
62
+ 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'));
67
+ }
68
+ export async function parseSavedViewConfigOption(value) {
69
+ if (value === undefined)
70
+ return undefined;
71
+ if (typeof value !== 'string' || !value.trim()) {
72
+ throw new DinoxError('--config requires inline JSON or @file', { code: 'USAGE_ERROR' });
73
+ }
74
+ return validateSavedViewConfig(await readJsonOption(value, 'Saved view config'));
75
+ }
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;
81
+ }
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' });
93
+ }
94
+ async function runViewList(context) {
95
+ const limit = context.options.limit;
96
+ await withCommandRuntime(context, async (runtime) => {
97
+ const identity = await resolveAuthIdentity(runtime.config);
98
+ await syncBeforeCommand(runtime);
99
+ const views = await listSavedViews(runtime.db, { userId: identity.userId, limit });
100
+ if (getRequestedStructuredOutputFormat()) {
101
+ printSuccess({ ok: true, stale: commandStale(runtime), count: views.length, views });
102
+ return;
103
+ }
104
+ for (const view of views) {
105
+ printHumanReadableLine(`${view.id}\t${view.emoji ?? ''}\t${view.name}${view.valid ? '' : '\t[invalid]'}`);
106
+ }
107
+ if (views.length === 0)
108
+ printHumanReadableLine('No saved views found.');
109
+ printStaleCacheWarning(runtime);
110
+ });
111
+ }
112
+ async function runViewGet(context) {
113
+ const id = requiredViewId(context);
114
+ await withCommandRuntime(context, async (runtime) => {
115
+ const identity = await resolveAuthIdentity(runtime.config);
116
+ await syncBeforeCommand(runtime);
117
+ const view = await getSavedView(runtime.db, { id, userId: identity.userId });
118
+ printSuccess({ ok: true, stale: commandStale(runtime), view });
119
+ });
120
+ }
121
+ async function runViewFields(context) {
122
+ await withCommandRuntime(context, async (runtime) => {
123
+ const identity = await resolveAuthIdentity(runtime.config);
124
+ await syncBeforeCommand(runtime);
125
+ const fields = await getSavedViewFieldCatalog(runtime.db, identity.userId);
126
+ if (getRequestedStructuredOutputFormat()) {
127
+ printSuccess({ ok: true, stale: commandStale(runtime), count: fields.length, fields });
128
+ return;
129
+ }
130
+ for (const field of fields) {
131
+ printHumanReadableLine(`${field.field}\t${field.propertyType}\t${field.sortable ? 'sortable' : 'not-sortable'}\t${field.available ? 'available' : 'unavailable'}\t${field.name}`);
132
+ }
133
+ printStaleCacheWarning(runtime);
134
+ });
135
+ }
136
+ async function runViewQuery(context) {
137
+ const viewId = requiredViewId(context);
138
+ const limit = context.options.limit;
139
+ const offset = context.options.offset;
140
+ await withCommandRuntime(context, async (runtime) => {
141
+ const identity = await resolveAuthIdentity(runtime.config);
142
+ await syncBeforeCommand(runtime);
143
+ const result = await querySavedView(runtime.db, {
144
+ viewId,
145
+ userId: identity.userId,
146
+ limit,
147
+ offset,
148
+ });
149
+ if (getRequestedStructuredOutputFormat()) {
150
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
151
+ return;
152
+ }
153
+ for (const row of result.rows) {
154
+ printHumanReadableLine(`${row.id}\t${row.title ?? ''}`);
155
+ }
156
+ printHumanReadableLine(`Showing ${result.rows.length} of ${result.totalCount} matching notes.`);
157
+ printStaleCacheWarning(runtime);
158
+ });
159
+ }
160
+ async function runViewCount(context) {
161
+ const viewId = requiredViewId(context);
162
+ await withCommandRuntime(context, async (runtime) => {
163
+ const identity = await resolveAuthIdentity(runtime.config);
164
+ await syncBeforeCommand(runtime);
165
+ const totalCount = await countSavedViewRows(runtime.db, { viewId, userId: identity.userId });
166
+ if (getRequestedStructuredOutputFormat()) {
167
+ printSuccess({ ok: true, stale: commandStale(runtime), viewId, totalCount });
168
+ return;
169
+ }
170
+ printHumanReadableLine(String(totalCount));
171
+ printStaleCacheWarning(runtime);
172
+ });
173
+ }
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
+ ]);
183
+ await withCommandRuntime(context, async (runtime) => {
184
+ const identity = await resolveAuthIdentity(runtime.config);
185
+ 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);
208
+ return;
209
+ }
210
+ const view = await createSavedView(runtime.db, input);
211
+ const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
212
+ if (getRequestedStructuredOutputFormat()) {
213
+ printSuccess({ ok: true, stale: commandStale(runtime), view, ...receipt });
214
+ return;
215
+ }
216
+ printHumanReadableLine(view.id);
217
+ printStaleCacheWarning(runtime);
218
+ });
219
+ }
220
+ async function runViewUpdate(context) {
221
+ const id = requiredViewId(context);
222
+ const options = context.options;
223
+ assertExclusiveOptions(options);
224
+ const [filter, config] = await Promise.all([
225
+ parseSavedViewFilterOption(options.filter),
226
+ parseSavedViewConfigOption(options.config),
227
+ ]);
228
+ const inputPatch = {
229
+ ...(options.name === undefined ? {} : { name: options.name }),
230
+ ...(options.clearEmoji ? { emoji: null } : options.emoji === undefined ? {} : { emoji: options.emoji }),
231
+ ...(filter === undefined ? {} : { filter }),
232
+ ...(config === undefined ? {} : { config }),
233
+ ...(options.clearTemplate ? { templateId: null } : options.templateId === undefined ? {} : { templateId: options.templateId }),
234
+ ...(options.clearGroup ? { groupId: null } : options.groupId === undefined ? {} : { groupId: options.groupId }),
235
+ ...(options.clearSortRank ? { sortRank: null } : options.sortRank === undefined ? {} : { sortRank: options.sortRank }),
236
+ ...(options.unpin ? { pinnedAt: null } : options.pinnedAt === undefined ? {} : { pinnedAt: options.pinnedAt }),
237
+ };
238
+ await withCommandRuntime(context, async (runtime) => {
239
+ const identity = await resolveAuthIdentity(runtime.config);
240
+ await syncBeforeCommand(runtime);
241
+ const input = { id, userId: identity.userId, ...inputPatch };
242
+ 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);
253
+ return;
254
+ }
255
+ const view = await updateSavedView(runtime.db, input);
256
+ const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
257
+ if (getRequestedStructuredOutputFormat()) {
258
+ printSuccess({ ok: true, stale: commandStale(runtime), view, ...receipt });
259
+ return;
260
+ }
261
+ printHumanReadableLine(view.id);
262
+ printStaleCacheWarning(runtime);
263
+ });
264
+ }
265
+ async function runViewDelete(context) {
266
+ const id = requiredViewId(context);
267
+ const options = context.options;
268
+ await withCommandRuntime(context, async (runtime) => {
269
+ const identity = await resolveAuthIdentity(runtime.config);
270
+ await syncBeforeCommand(runtime);
271
+ const input = { id, userId: identity.userId };
272
+ 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);
283
+ return;
284
+ }
285
+ const view = await deleteSavedView(runtime.db, input);
286
+ const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
287
+ if (getRequestedStructuredOutputFormat()) {
288
+ printSuccess({ ok: true, stale: commandStale(runtime), deleted: true, view, ...receipt });
289
+ return;
290
+ }
291
+ printHumanReadableLine(`Deleted ${view.id}`);
292
+ printStaleCacheWarning(runtime);
293
+ });
294
+ }
295
+ export const savedViewCommandGroup = {
296
+ name: 'view',
297
+ description: 'Manage and query saved table views',
298
+ commands: [
299
+ {
300
+ name: 'list',
301
+ description: 'List active saved table views',
302
+ options: [
303
+ { flags: '--limit <n>', description: 'Maximum returned views (1-5000)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 200 },
304
+ ],
305
+ schema: {
306
+ path: 'view.list',
307
+ risk: 'read',
308
+ auth: 'required',
309
+ globalOptions: VIEW_GLOBAL_OPTIONS,
310
+ output: { type: 'structured', summary: 'Saved view catalog', fields: ['stale', 'count', 'views'] },
311
+ examples: ['dino view list --format json'],
312
+ },
313
+ action: runViewList,
314
+ },
315
+ {
316
+ name: 'get',
317
+ description: 'Get one saved view definition',
318
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
319
+ schema: {
320
+ path: 'view.get',
321
+ risk: 'read',
322
+ auth: 'required',
323
+ globalOptions: VIEW_GLOBAL_OPTIONS,
324
+ output: { type: 'structured', summary: 'Canonical saved view definition', fields: ['stale', 'view'] },
325
+ examples: ['dino view get <id> --format json'],
326
+ },
327
+ action: runViewGet,
328
+ },
329
+ {
330
+ name: 'fields',
331
+ description: 'List system and property fields available to saved views',
332
+ schema: {
333
+ path: 'view.fields',
334
+ risk: 'read',
335
+ auth: 'required',
336
+ globalOptions: VIEW_GLOBAL_OPTIONS,
337
+ output: { type: 'structured', summary: 'Saved view field catalog and filter semantics', fields: ['stale', 'count', 'fields'] },
338
+ examples: ['dino view fields --format json'],
339
+ },
340
+ action: runViewFields,
341
+ },
342
+ {
343
+ name: 'query',
344
+ description: 'Query notes through a saved view filter and sort definition',
345
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
346
+ options: [
347
+ { flags: '--limit <n>', description: 'Maximum returned notes (1-500)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 100 },
348
+ { flags: '--offset <n>', description: 'Result offset for pagination', parser: parseNonNegativeIntegerOption, type: 'number', defaultValue: 0 },
349
+ ],
350
+ schema: {
351
+ path: 'view.query',
352
+ risk: 'read',
353
+ auth: 'required',
354
+ 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'],
357
+ },
358
+ action: runViewQuery,
359
+ },
360
+ {
361
+ name: 'count',
362
+ description: 'Count notes matching a saved view',
363
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
364
+ schema: {
365
+ path: 'view.count',
366
+ risk: 'read',
367
+ auth: 'required',
368
+ globalOptions: VIEW_GLOBAL_OPTIONS,
369
+ output: { type: 'structured', summary: 'Saved view matching note count', fields: ['stale', 'viewId', 'totalCount'] },
370
+ examples: ['dino view count <id> --format json'],
371
+ },
372
+ action: runViewCount,
373
+ },
374
+ {
375
+ name: 'create',
376
+ description: 'Create a canonical saved table view',
377
+ options: [
378
+ { flags: '--name <string>', description: 'Saved view name', required: true, type: 'string' },
379
+ { 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' },
382
+ { flags: '--template-id <id>', description: 'Optional note template id associated with the view', type: 'string' },
383
+ { 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' },
385
+ { flags: '--pinned-at <timestamp>', description: 'Pinned timestamp', type: 'string' },
386
+ WRITE_DURABILITY_OPTION,
387
+ { flags: '--dry-run', description: 'Validate and preview the view without writing', type: 'boolean' },
388
+ ],
389
+ schema: {
390
+ path: 'view.create',
391
+ risk: 'write',
392
+ auth: 'required',
393
+ globalOptions: VIEW_GLOBAL_OPTIONS,
394
+ output: { type: 'structured', summary: 'Created saved view definition or dry-run plan', fields: ['view', 'durability', 'upload_queue_remaining', 'stale'] },
395
+ examples: [
396
+ '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',
398
+ ],
399
+ },
400
+ action: runViewCreate,
401
+ },
402
+ {
403
+ name: 'update',
404
+ description: 'Update a canonical saved table view',
405
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
406
+ options: [
407
+ { flags: '--name <string>', description: 'Replace the saved view name', type: 'string' },
408
+ { flags: '--emoji <string>', description: 'Replace emoji or short icon text', type: 'string' },
409
+ { 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' },
412
+ { flags: '--template-id <id>', description: 'Replace the associated note template id', type: 'string' },
413
+ { flags: '--clear-template', description: 'Clear the associated note template id', type: 'boolean' },
414
+ { flags: '--group-id <id>', description: 'Replace the saved view group id', type: 'string' },
415
+ { flags: '--clear-group', description: 'Clear the saved view group id', type: 'boolean' },
416
+ { flags: '--sort-rank <n>', description: 'Replace the catalog sort rank', parser: parseIntegerOption, type: 'number' },
417
+ { flags: '--clear-sort-rank', description: 'Clear the catalog sort rank', type: 'boolean' },
418
+ { flags: '--pinned-at <timestamp>', description: 'Replace the pinned timestamp', type: 'string' },
419
+ { flags: '--unpin', description: 'Clear the pinned timestamp', type: 'boolean' },
420
+ WRITE_DURABILITY_OPTION,
421
+ { flags: '--dry-run', description: 'Validate and preview the update without writing', type: 'boolean' },
422
+ ],
423
+ schema: {
424
+ path: 'view.update',
425
+ risk: 'write',
426
+ auth: 'required',
427
+ globalOptions: VIEW_GLOBAL_OPTIONS,
428
+ output: { type: 'structured', summary: 'Updated saved view definition or dry-run plan', fields: ['view', 'durability', 'upload_queue_remaining', 'stale'] },
429
+ examples: [
430
+ 'dino view update <id> --filter @filter.json --format json --dry-run',
431
+ 'dino view update <id> --name "Priority work" --unpin --format json',
432
+ ],
433
+ },
434
+ action: runViewUpdate,
435
+ },
436
+ {
437
+ name: 'delete',
438
+ description: 'Soft-delete a saved view',
439
+ arguments: [{ syntax: '<id>', description: 'Saved view id' }],
440
+ options: [
441
+ WRITE_DURABILITY_OPTION,
442
+ { flags: '--dry-run', description: 'Preview the soft delete without writing', type: 'boolean' },
443
+ ],
444
+ schema: {
445
+ path: 'view.delete',
446
+ risk: 'write',
447
+ auth: 'required',
448
+ globalOptions: VIEW_GLOBAL_OPTIONS,
449
+ output: { type: 'structured', summary: 'Saved view soft-delete acknowledgement or dry-run plan', fields: ['deleted', 'view', 'durability', 'upload_queue_remaining', 'stale'] },
450
+ examples: ['dino view delete <id> --format json --dry-run'],
451
+ },
452
+ action: runViewDelete,
453
+ },
454
+ ],
455
+ };
456
+ export function registerSavedViewCommands(program) {
457
+ registerCommandGroup(program, savedViewCommandGroup);
458
+ }