@dinoxx/dinox-cli 1.0.18 → 1.0.19

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 +72 -34
  2. package/dist/cli.js +15 -1
  3. package/dist/commands/auth/index.d.ts +2 -0
  4. package/dist/commands/auth/index.js +209 -163
  5. package/dist/commands/boxes/index.d.ts +2 -0
  6. package/dist/commands/boxes/index.js +119 -75
  7. package/dist/commands/boxes/repo.d.ts +8 -0
  8. package/dist/commands/boxes/repo.js +41 -0
  9. package/dist/commands/config/index.d.ts +2 -0
  10. package/dist/commands/config/index.js +85 -36
  11. package/dist/commands/descriptor/register.d.ts +8 -0
  12. package/dist/commands/descriptor/register.js +113 -0
  13. package/dist/commands/descriptor/types.d.ts +37 -0
  14. package/dist/commands/descriptor/types.js +17 -0
  15. package/dist/commands/graph/index.d.ts +2 -0
  16. package/dist/commands/graph/index.js +143 -73
  17. package/dist/commands/info/index.d.ts +2 -0
  18. package/dist/commands/info/index.js +41 -14
  19. package/dist/commands/mcp/index.js +11 -10
  20. package/dist/commands/mcp/server.js +6 -3
  21. package/dist/commands/mcp/tools.js +30 -104
  22. package/dist/commands/mcp/types.d.ts +8 -1
  23. package/dist/commands/meta/index.d.ts +2 -0
  24. package/dist/commands/meta/index.js +64 -33
  25. package/dist/commands/meta/repo.d.ts +6 -0
  26. package/dist/commands/meta/repo.js +6 -0
  27. package/dist/commands/notes/actions.d.ts +10 -0
  28. package/dist/commands/notes/actions.js +403 -0
  29. package/dist/commands/notes/descriptors.d.ts +4 -0
  30. package/dist/commands/notes/descriptors.js +250 -0
  31. package/dist/commands/notes/dryRun.d.ts +31 -0
  32. package/dist/commands/notes/dryRun.js +130 -0
  33. package/dist/commands/notes/index.d.ts +2 -51
  34. package/dist/commands/notes/index.js +2 -581
  35. package/dist/commands/notes/repo.d.ts +9 -0
  36. package/dist/commands/notes/repo.js +56 -10
  37. package/dist/commands/notes/runtime.d.ts +28 -0
  38. package/dist/commands/notes/runtime.js +185 -0
  39. package/dist/commands/notes/searchOutput.d.ts +10 -2
  40. package/dist/commands/notes/searchOutput.js +14 -2
  41. package/dist/commands/notes/searchResponse.d.ts +51 -0
  42. package/dist/commands/notes/searchResponse.js +56 -0
  43. package/dist/commands/notes/searchSql.js +2 -0
  44. package/dist/commands/prompt/index.d.ts +2 -0
  45. package/dist/commands/prompt/index.js +122 -72
  46. package/dist/commands/prompt/repo.d.ts +7 -0
  47. package/dist/commands/prompt/repo.js +46 -0
  48. package/dist/commands/schema/commandMetadata.d.ts +2 -0
  49. package/dist/commands/schema/commandMetadata.js +18 -0
  50. package/dist/commands/schema/globalOptions.d.ts +3 -0
  51. package/dist/commands/schema/globalOptions.js +3 -0
  52. package/dist/commands/schema/index.d.ts +4 -0
  53. package/dist/commands/schema/index.js +109 -0
  54. package/dist/commands/schema/renderSkills.d.ts +6 -0
  55. package/dist/commands/schema/renderSkills.js +216 -0
  56. package/dist/commands/schema/spec.d.ts +6 -0
  57. package/dist/commands/schema/spec.js +40 -0
  58. package/dist/commands/schema/types.d.ts +47 -0
  59. package/dist/commands/schema/types.js +1 -0
  60. package/dist/commands/storage/index.d.ts +4 -0
  61. package/dist/commands/storage/index.js +292 -0
  62. package/dist/commands/storage/repo.d.ts +162 -0
  63. package/dist/commands/storage/repo.js +751 -0
  64. package/dist/commands/sync.d.ts +2 -0
  65. package/dist/commands/sync.js +41 -27
  66. package/dist/commands/tags/index.d.ts +2 -0
  67. package/dist/commands/tags/index.js +121 -79
  68. package/dist/commands/tags/repo.d.ts +7 -0
  69. package/dist/commands/tags/repo.js +52 -0
  70. package/dist/commands/todo/dryRun.d.ts +23 -0
  71. package/dist/commands/todo/dryRun.js +133 -0
  72. package/dist/commands/todo/index.d.ts +2 -0
  73. package/dist/commands/todo/index.js +285 -135
  74. package/dist/commands/todo/repo.d.ts +11 -0
  75. package/dist/commands/todo/repo.js +37 -9
  76. package/dist/commands/update.d.ts +3 -0
  77. package/dist/commands/update.js +35 -13
  78. package/dist/commands/writeDryRun.d.ts +62 -0
  79. package/dist/commands/writeDryRun.js +131 -0
  80. package/dist/constants/links.d.ts +1 -1
  81. package/dist/constants/links.js +1 -1
  82. package/dist/daemon/ensure.js +3 -2
  83. package/dist/daemon/index.js +17 -15
  84. package/dist/dinox.js +9 -13
  85. package/dist/powersync/schema/index.d.ts +27 -0
  86. package/dist/powersync/schema/index.js +2 -0
  87. package/dist/powersync/schema/note.d.ts +1 -0
  88. package/dist/powersync/schema/note.js +1 -0
  89. package/dist/powersync/schema/projects.d.ts +3 -0
  90. package/dist/powersync/schema/projects.js +3 -0
  91. package/dist/powersync/schema/storage.d.ts +24 -0
  92. package/dist/powersync/schema/storage.js +24 -0
  93. package/dist/skills/check.d.ts +6 -0
  94. package/dist/skills/check.js +254 -0
  95. package/dist/utils/bidlinkCanonicalization.d.ts +2 -0
  96. package/dist/utils/bidlinkCanonicalization.js +161 -0
  97. package/dist/utils/dryRun.d.ts +13 -0
  98. package/dist/utils/dryRun.js +69 -0
  99. package/dist/utils/output.d.ts +19 -0
  100. package/dist/utils/output.js +155 -1
  101. package/dist/utils/terminalSanitization.d.ts +2 -0
  102. package/dist/utils/terminalSanitization.js +33 -0
  103. package/dist/utils/tiptapMarkdown.d.ts +4 -1
  104. package/dist/utils/tiptapMarkdown.js +285 -113
  105. package/dist/utils/tiptapMarkdownSchema.d.ts +37 -0
  106. package/dist/utils/tiptapMarkdownSchema.js +577 -0
  107. package/dist/utils/tiptapMarkdownSchemaUtils.d.ts +74 -0
  108. package/dist/utils/tiptapMarkdownSchemaUtils.js +394 -0
  109. package/dist/utils/updateNotice.d.ts +7 -0
  110. package/dist/utils/updateNotice.js +64 -0
  111. package/package.json +25 -3
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandDescriptor } from './descriptor/types.js';
3
+ export declare const syncCommandDescriptor: CommandDescriptor;
2
4
  export declare function registerSyncCommand(program: Command): void;
@@ -1,18 +1,30 @@
1
1
  import { loadConfig } from '../config/store.js';
2
2
  import { resolveConfig } from '../config/resolve.js';
3
- import { printYaml } from '../utils/output.js';
3
+ import { registerStandaloneCommand } from './descriptor/register.js';
4
+ import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
5
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
4
6
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
5
7
  import { connectPowerSync, getStatusSnapshot } from '../powersync/runtime.js';
6
8
  import { runSingleSyncPass } from '../powersync/syncPass.js';
7
- export function registerSyncCommand(program) {
8
- program
9
- .command('sync')
10
- .description('Connect and synchronize local PowerSync database')
11
- .action(async (_options, command) => {
9
+ export const syncCommandDescriptor = {
10
+ name: 'sync',
11
+ description: 'Connect and synchronize the local PowerSync database',
12
+ schema: {
13
+ path: 'sync',
14
+ risk: 'write',
15
+ auth: 'required',
16
+ globalOptions: [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
17
+ output: {
18
+ type: 'structured',
19
+ summary: 'Sync result with connection status and token index counters',
20
+ fields: ['dbPath', 'stale', 'idle', 'uploadEnabled', 'tokenIndex', 'status'],
21
+ },
22
+ examples: ['dino sync --format json', 'dino sync --sync-timeout 600000 --format json'],
23
+ },
24
+ action: async (context) => {
12
25
  const config = resolveConfig(await loadConfig());
13
- const globals = command.optsWithGlobals?.() ?? {};
14
- const jsonOutput = Boolean(globals.json);
15
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
26
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
27
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
16
28
  const { db, dbPath, stale } = await connectPowerSync({
17
29
  config,
18
30
  offline: false,
@@ -30,28 +42,30 @@ export function registerSyncCommand(program) {
30
42
  tokenIndex: syncPass.tokenIndex,
31
43
  status,
32
44
  };
33
- if (jsonOutput) {
34
- printYaml(payload);
45
+ if (structuredOutput) {
46
+ printSuccess(payload);
47
+ return;
48
+ }
49
+ printHumanReadableLine(`db: ${payload.dbPath}`);
50
+ if (!payload.uploadEnabled) {
51
+ printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset)');
52
+ }
53
+ if (payload.status.lastSyncedAt) {
54
+ printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
35
55
  }
36
- else {
37
- console.log(`db: ${payload.dbPath}`);
38
- if (!payload.uploadEnabled) {
39
- console.log('warning: upload disabled (powersync.uploadBaseUrl is unset)');
40
- }
41
- if (payload.status.lastSyncedAt) {
42
- console.log(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
43
- }
44
- if (payload.stale) {
45
- console.log('warning: sync timed out; local cache may be stale');
46
- }
47
- if (!payload.idle) {
48
- console.log('warning: data flow did not become idle before timeout');
49
- }
50
- console.log(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
56
+ if (payload.stale) {
57
+ printHumanReadableLine('warning: sync timed out; local cache may be stale');
51
58
  }
59
+ if (!payload.idle) {
60
+ printHumanReadableLine('warning: data flow did not become idle before timeout');
61
+ }
62
+ printHumanReadableLine(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
52
63
  }
53
64
  finally {
54
65
  await db.close().catch(() => undefined);
55
66
  }
56
- });
67
+ },
68
+ };
69
+ export function registerSyncCommand(program) {
70
+ registerStandaloneCommand(program, syncCommandDescriptor);
57
71
  }
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ export declare const tagCommandGroup: CommandGroupDescriptor;
2
4
  export declare function registerTagsCommand(program: Command): void;
@@ -1,12 +1,17 @@
1
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
1
2
  import { loadConfig } from '../../config/store.js';
2
3
  import { resolveConfig } from '../../config/resolve.js';
3
- import { DinoxError } from '../../utils/errors.js';
4
- import { printYaml } from '../../utils/output.js';
5
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
6
4
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
7
5
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
8
- import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
- import { addTag, listTags } from './repo.js';
6
+ import { registerCommandGroup } from '../descriptor/register.js';
7
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
8
+ import { buildTagAddDryRunPreview } from '../writeDryRun.js';
9
+ import { printDryRunPreview } from '../../utils/dryRun.js';
10
+ import { DinoxError } from '../../utils/errors.js';
11
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
12
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
13
+ import { addTag, inspectAddTag, listTags } from './repo.js';
14
+ const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
10
15
  async function syncBeforeTagCommand(db, offline, timeoutMs) {
11
16
  if (offline) {
12
17
  return;
@@ -14,98 +19,135 @@ async function syncBeforeTagCommand(db, offline, timeoutMs) {
14
19
  await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
15
20
  await syncNoteTokenIndex(db);
16
21
  }
17
- async function runTagListCommand(options, command) {
18
- const globals = command.optsWithGlobals?.() ?? {};
19
- const offline = Boolean(options.offline ?? globals.offline);
20
- const jsonOutput = Boolean(options.json ?? globals.json);
21
- const config = resolveConfig(await loadConfig());
22
- const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
22
+ async function withTagDb(context, run) {
23
+ const rawConfig = await loadConfig();
24
+ const config = resolveConfig(rawConfig);
25
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
26
+ const offline = Boolean(context.globals.offline);
23
27
  const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
24
28
  try {
29
+ await run({ db, config, rawConfig, offline, timeoutMs, stale });
30
+ }
31
+ finally {
32
+ await db.close().catch(() => undefined);
33
+ }
34
+ }
35
+ async function runTagList(context) {
36
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
37
+ await withTagDb(context, async ({ db, offline, timeoutMs, stale }) => {
25
38
  await syncBeforeTagCommand(db, offline, timeoutMs);
26
39
  const tags = await listTags(db);
27
- if (jsonOutput) {
28
- printYaml({ stale, count: tags.length, tags });
40
+ if (structuredOutput) {
41
+ printSuccess({ stale, count: tags.length, tags });
29
42
  return;
30
43
  }
31
44
  if (stale && !offline) {
32
- console.log('warning: sync timed out; results may be stale');
45
+ printHumanReadableLine('warning: sync timed out; results may be stale');
33
46
  }
34
47
  if (tags.length === 0) {
35
- console.log('No tags.');
48
+ printHumanReadableLine('No tags.');
36
49
  return;
37
50
  }
38
51
  for (const tag of tags) {
39
- console.log(tag);
52
+ printHumanReadableLine(tag);
40
53
  }
54
+ });
55
+ }
56
+ async function runTagAdd(context) {
57
+ const nameArg = context.args.name;
58
+ const optionName = typeof context.options.name === 'string' ? context.options.name : undefined;
59
+ const name = (nameArg ?? optionName ?? '').trim();
60
+ if (!name) {
61
+ throw new DinoxError('Tag name is required. Use `dino tag add <name>` or `dino tag add --name <name>`.');
41
62
  }
42
- finally {
43
- await db.close().catch(() => undefined);
63
+ if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
64
+ throw new DinoxError('Conflicting tag name values from positional argument and --name option.');
44
65
  }
45
- }
46
- export function registerTagsCommand(program) {
47
- const tag = program.command('tag').description('Manage tags');
48
- tag
49
- .command('list')
50
- .description('List tags from c_tag_node')
51
- .option('--json', 'output machine-readable YAML')
52
- .option('--offline', 'skip connect/sync and only use local cache')
53
- .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
54
- .action(runTagListCommand);
55
- tag
56
- .command('add')
57
- .description('Create a new tag in c_tag_node')
58
- .argument('[name]', 'tag name/path (supports slash hierarchy)')
59
- .option('--name <string>', 'tag name/path (alternative to positional name)')
60
- .option('--emoji <string>', 'tag emoji')
61
- .option('--json', 'output machine-readable YAML')
62
- .option('--offline', 'skip connect/sync and only use local cache')
63
- .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
64
- .action(async (nameArg, options, command) => {
65
- const globals = command.optsWithGlobals?.() ?? {};
66
- const offline = Boolean(options.offline ?? globals.offline);
67
- const jsonOutput = Boolean(options.json ?? globals.json);
68
- const optionName = typeof options.name === 'string' ? options.name : undefined;
69
- const name = (nameArg ?? optionName ?? '').trim();
70
- if (!name) {
71
- throw new DinoxError('Tag name is required. Use `dino tag add <name>` or `dino tag add --name <name>`.');
66
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
67
+ await withTagDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
68
+ const identity = await resolveAuthIdentity(rawConfig, config);
69
+ await syncBeforeTagCommand(db, offline, timeoutMs);
70
+ const emoji = typeof context.options.emoji === 'string' ? context.options.emoji : undefined;
71
+ if (Boolean(context.options.dryRun)) {
72
+ printDryRunPreview(buildTagAddDryRunPreview({
73
+ config,
74
+ offline,
75
+ ...(await inspectAddTag(db, {
76
+ name,
77
+ userId: identity.userId,
78
+ emoji,
79
+ })),
80
+ }));
81
+ return;
72
82
  }
73
- if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
74
- throw new DinoxError('Conflicting tag name values from positional argument and --name option.');
83
+ const result = await addTag(db, {
84
+ name,
85
+ userId: identity.userId,
86
+ emoji,
87
+ });
88
+ if (!offline && !config.powersync.uploadBaseUrl) {
89
+ printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
75
90
  }
76
- const rawConfig = await loadConfig();
77
- const config = resolveConfig(rawConfig);
78
- const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
79
- const identity = await resolveAuthIdentity(rawConfig, config);
80
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
81
- try {
82
- await syncBeforeTagCommand(db, offline, timeoutMs);
83
- const result = await addTag(db, {
84
- name,
85
- userId: identity.userId,
86
- emoji: typeof options.emoji === 'string' ? options.emoji : undefined,
87
- });
88
- if (!offline && !config.powersync.uploadBaseUrl) {
89
- console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
90
- }
91
- if (!offline) {
92
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
93
- }
94
- const payload = { ok: true, stale: offline ? true : stale, ...result };
95
- if (jsonOutput) {
96
- printYaml(payload);
97
- return;
98
- }
99
- console.log(result.id);
100
- if (stale && !offline) {
101
- console.log('warning: sync timed out; results may be stale');
102
- }
91
+ if (!offline) {
92
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
103
93
  }
104
- finally {
105
- await db.close().catch(() => undefined);
94
+ const payload = { ok: true, stale: offline ? true : stale, ...result };
95
+ if (structuredOutput) {
96
+ printSuccess(payload);
97
+ return;
98
+ }
99
+ printHumanReadableLine(result.id);
100
+ if (stale && !offline) {
101
+ printHumanReadableLine('warning: sync timed out; results may be stale');
106
102
  }
107
103
  });
108
- tag.action((_options, command) => {
109
- command.outputHelp();
110
- });
104
+ }
105
+ export const tagCommandGroup = {
106
+ name: 'tag',
107
+ description: 'Manage tags',
108
+ commands: [
109
+ {
110
+ name: 'list',
111
+ description: 'List all tags from c_tag_node',
112
+ schema: {
113
+ path: 'tag.list',
114
+ risk: 'read',
115
+ auth: 'required',
116
+ globalOptions: TAG_GLOBAL_OPTIONS,
117
+ output: {
118
+ type: 'structured',
119
+ summary: 'Tag list payload',
120
+ fields: ['count', 'tags', 'stale'],
121
+ },
122
+ examples: ['dino tag list --format json'],
123
+ },
124
+ action: runTagList,
125
+ },
126
+ {
127
+ name: 'add',
128
+ description: 'Create a tag path, restoring deleted nodes when possible',
129
+ arguments: [{ syntax: '[name]', description: 'Tag name/path (supports slash hierarchy)' }],
130
+ options: [
131
+ { flags: '--name <string>', description: 'Tag name/path (alternative to positional name)' },
132
+ { flags: '--emoji <string>', description: 'Tag emoji' },
133
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
134
+ ],
135
+ schema: {
136
+ path: 'tag.add',
137
+ risk: 'write',
138
+ auth: 'required',
139
+ globalOptions: TAG_GLOBAL_OPTIONS,
140
+ output: {
141
+ type: 'structured',
142
+ summary: 'Tag creation acknowledgement',
143
+ fields: ['id', 'tag', 'createdNodes', 'restoredNodes', 'stale'],
144
+ },
145
+ examples: ['dino tag add "work/projects" --format json --dry-run', 'dino tag add "work/projects" --emoji "🏷️" --format json'],
146
+ },
147
+ action: runTagAdd,
148
+ },
149
+ ],
150
+ };
151
+ export function registerTagsCommand(program) {
152
+ registerCommandGroup(program, tagCommandGroup);
111
153
  }
@@ -10,5 +10,12 @@ export type AddTagResult = {
10
10
  createdNodes: number;
11
11
  restoredNodes: number;
12
12
  };
13
+ export type InspectAddTagResult = {
14
+ tag: string;
15
+ createdPaths: string[];
16
+ restoredPaths: string[];
17
+ emoji: string;
18
+ };
13
19
  export declare function listTags(db: AbstractPowerSyncDatabase): Promise<string[]>;
14
20
  export declare function addTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<AddTagResult>;
21
+ export declare function inspectAddTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<InspectAddTagResult>;
@@ -245,3 +245,55 @@ export async function addTag(db, input) {
245
245
  restoredNodes,
246
246
  };
247
247
  }
248
+ export async function inspectAddTag(db, input) {
249
+ const userId = input.userId.trim();
250
+ const emoji = typeof input.emoji === 'string' ? input.emoji.trim() : '';
251
+ const tag = canonicalizeTagPath(input.name);
252
+ if (!tag) {
253
+ throw new DinoxError('Tag name is required');
254
+ }
255
+ const columnNames = await getTagNodeColumns(db);
256
+ if (!columnNames.has('id')) {
257
+ throw new DinoxError('c_tag_node table is missing required column: id');
258
+ }
259
+ if (!columnNames.has('full_path') && !columnNames.has('path_key')) {
260
+ throw new DinoxError('c_tag_node table is missing required column: full_path or path_key');
261
+ }
262
+ const requiredUserColumns = ['user_id'].filter((column) => columnNames.has(column));
263
+ if (requiredUserColumns.length > 0 && !userId) {
264
+ throw new DinoxError(`c_tag_node insert requires ${requiredUserColumns.join(', ')}. Missing persisted userId. Run \`dino auth login\` first.`);
265
+ }
266
+ const nodesByPath = await loadTagNodes(db, columnNames);
267
+ const existingLeaf = nodesByPath.get(tag);
268
+ if (existingLeaf && existingLeaf.isDel !== 1 && existingLeaf.isActual === 1) {
269
+ throw new DinoxError(`Tag already exists: ${tag}`, {
270
+ details: {
271
+ type: 'tag_exists',
272
+ tag,
273
+ id: existingLeaf.id,
274
+ question: 'This tag already exists. Do you want to keep using the existing tag?',
275
+ },
276
+ });
277
+ }
278
+ const segments = tag.split('/');
279
+ const createdPaths = [];
280
+ const restoredPaths = [];
281
+ for (let i = 0; i < segments.length; i += 1) {
282
+ const path = segments.slice(0, i + 1).join('/');
283
+ const isLeaf = i === segments.length - 1;
284
+ const existing = nodesByPath.get(path);
285
+ if (!existing) {
286
+ createdPaths.push(path);
287
+ continue;
288
+ }
289
+ if (existing.isDel === 1 || (isLeaf && existing.isActual !== 1)) {
290
+ restoredPaths.push(path);
291
+ }
292
+ }
293
+ return {
294
+ tag,
295
+ createdPaths,
296
+ restoredPaths,
297
+ emoji,
298
+ };
299
+ }
@@ -0,0 +1,23 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ import type { ResolvedDinoxConfig } from '../../config/types.js';
3
+ import type { DryRunPreview } from '../../utils/dryRun.js';
4
+ export declare function buildTodoAppendDryRunPreview(args: {
5
+ db: AbstractPowerSyncDatabase;
6
+ config: ResolvedDinoxConfig;
7
+ offline: boolean;
8
+ noteId?: string;
9
+ taskTexts: string[];
10
+ }): Promise<DryRunPreview>;
11
+ export declare function buildTodoCreateDryRunPreview(args: {
12
+ config: ResolvedDinoxConfig;
13
+ offline: boolean;
14
+ title?: string;
15
+ taskTexts: string[];
16
+ }): Promise<DryRunPreview>;
17
+ export declare function buildTodoUpdateDryRunPreview(args: {
18
+ db: AbstractPowerSyncDatabase;
19
+ config: ResolvedDinoxConfig;
20
+ offline: boolean;
21
+ taskId: string;
22
+ checked: boolean;
23
+ }): Promise<DryRunPreview>;
@@ -0,0 +1,133 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ import { applyTaskCheckedStatus, buildSummaryTitle, normalizeTaskTexts, parseTiptapDocFromDb, } from './contentModel.js';
3
+ import { getLatestTodoAppendTarget, getTodoAppendTargetById, loadTodoTaskCandidatesByTaskId, } from './repo.js';
4
+ function buildExecutionWarnings(config, offline) {
5
+ const warnings = [];
6
+ if (offline) {
7
+ warnings.push('Execution would run in offline mode and only use the local cache.');
8
+ }
9
+ if (!offline && !config.powersync.uploadBaseUrl) {
10
+ warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
11
+ }
12
+ return warnings;
13
+ }
14
+ export async function buildTodoAppendDryRunPreview(args) {
15
+ const normalizedTaskTexts = normalizeTaskTexts(args.taskTexts);
16
+ const explicitNoteId = typeof args.noteId === 'string' ? args.noteId.trim() : '';
17
+ const source = explicitNoteId ? 'explicit' : 'latest_non_empty_image_detail';
18
+ const target = explicitNoteId
19
+ ? await getTodoAppendTargetById(args.db, explicitNoteId)
20
+ : await getLatestTodoAppendTarget(args.db);
21
+ if (!target) {
22
+ if (explicitNoteId) {
23
+ throw new DinoxError(`Note not found: ${explicitNoteId}`);
24
+ }
25
+ throw new DinoxError('No eligible note found for append. Try `dino todo create ...` first.');
26
+ }
27
+ return {
28
+ dry_run: true,
29
+ command: 'dino todo append',
30
+ action: 'append_todo_tasks',
31
+ risk: 'write',
32
+ summary: `Append ${normalizedTaskTexts.length} task(s) to note "${(target.title ?? '').trim()}".`,
33
+ warnings: buildExecutionWarnings(args.config, args.offline),
34
+ inputs: {
35
+ note_id: explicitNoteId || null,
36
+ source,
37
+ task_count: normalizedTaskTexts.length,
38
+ task_texts: normalizedTaskTexts,
39
+ },
40
+ targets: [
41
+ {
42
+ id: target.id,
43
+ title: (target.title ?? '').trim(),
44
+ source,
45
+ },
46
+ ],
47
+ changes: {
48
+ appended_count: normalizedTaskTexts.length,
49
+ task_texts: normalizedTaskTexts,
50
+ },
51
+ };
52
+ }
53
+ export async function buildTodoCreateDryRunPreview(args) {
54
+ const normalizedTaskTexts = normalizeTaskTexts(args.taskTexts);
55
+ const resolvedTitle = args.title?.trim() || buildSummaryTitle(normalizedTaskTexts);
56
+ return {
57
+ dry_run: true,
58
+ command: 'dino todo create',
59
+ action: 'create_todo_note',
60
+ risk: 'write',
61
+ summary: `Create a new todo note titled "${resolvedTitle}" with ${normalizedTaskTexts.length} task(s).`,
62
+ warnings: buildExecutionWarnings(args.config, args.offline),
63
+ inputs: {
64
+ title: resolvedTitle,
65
+ task_count: normalizedTaskTexts.length,
66
+ task_texts: normalizedTaskTexts,
67
+ },
68
+ changes: {
69
+ title: resolvedTitle,
70
+ created_count: normalizedTaskTexts.length,
71
+ task_texts: normalizedTaskTexts,
72
+ },
73
+ };
74
+ }
75
+ export async function buildTodoUpdateDryRunPreview(args) {
76
+ const taskId = args.taskId.trim();
77
+ if (!taskId) {
78
+ throw new DinoxError('taskId is required');
79
+ }
80
+ const candidates = await loadTodoTaskCandidatesByTaskId(args.db, taskId);
81
+ const matches = [];
82
+ for (const candidate of candidates) {
83
+ try {
84
+ const doc = parseTiptapDocFromDb(candidate.content_json);
85
+ const applied = applyTaskCheckedStatus(doc, taskId, args.checked);
86
+ if (applied.matchedIndices.length === 0) {
87
+ continue;
88
+ }
89
+ const noteId = (candidate.id ?? '').trim();
90
+ if (!noteId) {
91
+ continue;
92
+ }
93
+ matches.push({
94
+ noteId,
95
+ title: (candidate.title ?? '').trim(),
96
+ checked: applied.changedCount > 0 ? !args.checked : args.checked,
97
+ });
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ }
103
+ if (matches.length === 0) {
104
+ throw new DinoxError(`Task not found: ${taskId}`);
105
+ }
106
+ if (matches.length > 1) {
107
+ throw new DinoxError(`Task id is not unique; found in multiple notes: ${matches.map((item) => item.noteId).join(', ')}`);
108
+ }
109
+ const match = matches[0];
110
+ return {
111
+ dry_run: true,
112
+ command: 'dino todo update',
113
+ action: 'update_todo_status',
114
+ risk: 'write',
115
+ summary: `Mark task ${taskId} as ${args.checked ? 'completed' : 'uncompleted'} in note "${match.title}".`,
116
+ warnings: buildExecutionWarnings(args.config, args.offline),
117
+ inputs: {
118
+ task_id: taskId,
119
+ status_to: args.checked ? 'completed' : 'uncompleted',
120
+ },
121
+ targets: [
122
+ {
123
+ note_id: match.noteId,
124
+ title: match.title,
125
+ },
126
+ ],
127
+ changes: {
128
+ checked_from: match.checked,
129
+ checked_to: args.checked,
130
+ updated: match.checked !== args.checked,
131
+ },
132
+ };
133
+ }
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ export declare const todoCommandGroup: CommandGroupDescriptor;
2
4
  export declare function registerTodoCommands(program: Command): void;