@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
@@ -1,5 +1,5 @@
1
1
  import type { SearchedNote } from './repo.js';
2
- export declare const NOTE_SEARCH_FIELDS: readonly ["id", "title", "summary", "tags", "created_at", "boxes", "is_starred"];
2
+ export declare const NOTE_SEARCH_FIELDS: readonly ["id", "title", "summary", "tags", "created_at", "starred_at", "boxes", "is_starred"];
3
3
  export type NoteSearchField = (typeof NOTE_SEARCH_FIELDS)[number];
4
4
  export type NoteSearchResultItem = {
5
5
  id?: string;
@@ -7,6 +7,7 @@ export type NoteSearchResultItem = {
7
7
  summary?: string;
8
8
  tags?: string[];
9
9
  created_at?: string | null;
10
+ starred_at?: string | null;
10
11
  is_starred?: boolean;
11
12
  boxes?: string[];
12
13
  };
@@ -1,5 +1,5 @@
1
1
  import { DinoxError } from '../../utils/errors.js';
2
- export const NOTE_SEARCH_FIELDS = ['id', 'title', 'summary', 'tags', 'created_at', 'boxes', 'is_starred'];
2
+ export const NOTE_SEARCH_FIELDS = ['id', 'title', 'summary', 'tags', 'created_at', 'starred_at', 'boxes', 'is_starred'];
3
3
  const NOTE_SEARCH_FIELD_SET = new Set(NOTE_SEARCH_FIELDS);
4
4
  export function parseNoteSearchFields(value, optionName) {
5
5
  if (value === undefined || value === null || value === '') {
@@ -35,6 +35,7 @@ export function projectNoteSearchFields(notes, fields) {
35
35
  summary: note.summary,
36
36
  tags: note.tags,
37
37
  created_at: note.created_at,
38
+ starred_at: note.starred_at,
38
39
  is_starred: note.is_starred,
39
40
  boxes: note.zettel_boxes,
40
41
  }));
@@ -32,8 +32,14 @@ const SCALAR_FIELD_COLUMNS = {
32
32
  summary: 'summary',
33
33
  created_at: 'created_at',
34
34
  type: 'type',
35
- is_starred: 'is_starred',
35
+ is_starred: 'starred_at',
36
36
  };
37
+ function scalarFieldSql(field, noteAlias) {
38
+ if (field === 'is_starred') {
39
+ return `(CASE WHEN ${noteAlias}.starred_at IS NOT NULL THEN 1 ELSE 0 END)`;
40
+ }
41
+ return `${noteAlias}.${SCALAR_FIELD_COLUMNS[field]}`;
42
+ }
37
43
  export async function buildNoteSearchSqlFilter(options) {
38
44
  const expression = options.expression?.trim() ?? '';
39
45
  if (!expression) {
@@ -527,7 +533,7 @@ function compileComparison(node, noteAlias, params, zettelBoxIdByName) {
527
533
  `;
528
534
  return node.operator === '!=' ? `NOT (${existsSql})` : existsSql;
529
535
  }
530
- const column = `${noteAlias}.${SCALAR_FIELD_COLUMNS[node.field]}`;
536
+ const column = scalarFieldSql(node.field, noteAlias);
531
537
  params.push(node.value);
532
538
  if (node.operator === 'LIKE' || node.operator === 'NOT LIKE') {
533
539
  return `(${column} IS NOT NULL AND ${column} ${node.operator} ?)`;
@@ -586,7 +592,7 @@ function compileInCondition(node, noteAlias, params, zettelBoxIdByName) {
586
592
  if (values.length === 0) {
587
593
  throw new Error('IN list cannot be empty');
588
594
  }
589
- const column = `${noteAlias}.${SCALAR_FIELD_COLUMNS[node.field]}`;
595
+ const column = scalarFieldSql(node.field, noteAlias);
590
596
  const placeholders = values.map(() => '?').join(', ');
591
597
  params.push(...values);
592
598
  if (node.not) {
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ export declare function parsePromptTextInput(value: unknown): Promise<string>;
3
4
  export declare const promptCommandGroup: CommandGroupDescriptor;
4
5
  export declare function registerPromptCommands(program: Command): void;
@@ -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
  },
@@ -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
+ }