@dinoxx/dinox-cli 1.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 (90) hide show
  1. package/README.md +294 -0
  2. package/dist/auth/userInfo.d.ts +14 -0
  3. package/dist/auth/userInfo.js +115 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +32 -0
  6. package/dist/cliTypes.d.ts +6 -0
  7. package/dist/cliTypes.js +1 -0
  8. package/dist/commands/auth/index.d.ts +2 -0
  9. package/dist/commands/auth/index.js +193 -0
  10. package/dist/commands/boxes/index.d.ts +2 -0
  11. package/dist/commands/boxes/index.js +107 -0
  12. package/dist/commands/boxes/repo.d.ts +21 -0
  13. package/dist/commands/boxes/repo.js +154 -0
  14. package/dist/commands/config/index.d.ts +2 -0
  15. package/dist/commands/config/index.js +67 -0
  16. package/dist/commands/info/index.d.ts +2 -0
  17. package/dist/commands/info/index.js +20 -0
  18. package/dist/commands/notes/index.d.ts +2 -0
  19. package/dist/commands/notes/index.js +271 -0
  20. package/dist/commands/notes/repo.d.ts +70 -0
  21. package/dist/commands/notes/repo.js +674 -0
  22. package/dist/commands/notes/searchTime.d.ts +9 -0
  23. package/dist/commands/notes/searchTime.js +85 -0
  24. package/dist/commands/prompt/index.d.ts +2 -0
  25. package/dist/commands/prompt/index.js +51 -0
  26. package/dist/commands/prompt/repo.d.ts +6 -0
  27. package/dist/commands/prompt/repo.js +18 -0
  28. package/dist/commands/sync.d.ts +2 -0
  29. package/dist/commands/sync.js +68 -0
  30. package/dist/commands/tags/index.d.ts +2 -0
  31. package/dist/commands/tags/index.js +120 -0
  32. package/dist/commands/tags/repo.d.ts +14 -0
  33. package/dist/commands/tags/repo.js +247 -0
  34. package/dist/config/keys.d.ts +9 -0
  35. package/dist/config/keys.js +17 -0
  36. package/dist/config/paths.d.ts +4 -0
  37. package/dist/config/paths.js +39 -0
  38. package/dist/config/resolve.d.ts +2 -0
  39. package/dist/config/resolve.js +56 -0
  40. package/dist/config/serviceEndpoints.d.ts +3 -0
  41. package/dist/config/serviceEndpoints.js +3 -0
  42. package/dist/config/store.d.ts +5 -0
  43. package/dist/config/store.js +87 -0
  44. package/dist/config/types.d.ts +51 -0
  45. package/dist/config/types.js +1 -0
  46. package/dist/dinox.d.ts +2 -0
  47. package/dist/dinox.js +50 -0
  48. package/dist/powersync/connector.d.ts +21 -0
  49. package/dist/powersync/connector.js +58 -0
  50. package/dist/powersync/runtime.d.ts +37 -0
  51. package/dist/powersync/runtime.js +107 -0
  52. package/dist/powersync/schema/content.d.ts +76 -0
  53. package/dist/powersync/schema/content.js +76 -0
  54. package/dist/powersync/schema/index.d.ts +371 -0
  55. package/dist/powersync/schema/index.js +35 -0
  56. package/dist/powersync/schema/local.d.ts +68 -0
  57. package/dist/powersync/schema/local.js +83 -0
  58. package/dist/powersync/schema/note.d.ts +34 -0
  59. package/dist/powersync/schema/note.js +34 -0
  60. package/dist/powersync/schema/notesExtras.d.ts +62 -0
  61. package/dist/powersync/schema/notesExtras.js +71 -0
  62. package/dist/powersync/schema/projects.d.ts +101 -0
  63. package/dist/powersync/schema/projects.js +101 -0
  64. package/dist/powersync/schema/tags.d.ts +37 -0
  65. package/dist/powersync/schema/tags.js +37 -0
  66. package/dist/powersync/tokenIndex.d.ts +17 -0
  67. package/dist/powersync/tokenIndex.js +202 -0
  68. package/dist/powersync/uploader.d.ts +7 -0
  69. package/dist/powersync/uploader.js +134 -0
  70. package/dist/utils/argValue.d.ts +1 -0
  71. package/dist/utils/argValue.js +17 -0
  72. package/dist/utils/errors.d.ts +10 -0
  73. package/dist/utils/errors.js +17 -0
  74. package/dist/utils/id.d.ts +1 -0
  75. package/dist/utils/id.js +4 -0
  76. package/dist/utils/output.d.ts +2 -0
  77. package/dist/utils/output.js +10 -0
  78. package/dist/utils/redact.d.ts +1 -0
  79. package/dist/utils/redact.js +10 -0
  80. package/dist/utils/text.d.ts +1 -0
  81. package/dist/utils/text.js +35 -0
  82. package/dist/utils/time.d.ts +1 -0
  83. package/dist/utils/time.js +3 -0
  84. package/dist/utils/tiptapMarkdown.d.ts +6 -0
  85. package/dist/utils/tiptapMarkdown.js +149 -0
  86. package/dist/utils/tokenize.d.ts +1 -0
  87. package/dist/utils/tokenize.js +56 -0
  88. package/dist/utils/version.d.ts +1 -0
  89. package/dist/utils/version.js +21 -0
  90. package/package.json +63 -0
@@ -0,0 +1,85 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
3
+ const DAY_MS = 24 * 60 * 60 * 1000;
4
+ function parseUtcDateOnly(value) {
5
+ const match = value.match(DATE_ONLY_RE);
6
+ if (!match) {
7
+ return null;
8
+ }
9
+ const year = Number(match[1]);
10
+ const month = Number(match[2]);
11
+ const day = Number(match[3]);
12
+ const utc = new Date(Date.UTC(year, month - 1, day));
13
+ if (utc.getUTCFullYear() !== year ||
14
+ utc.getUTCMonth() !== month - 1 ||
15
+ utc.getUTCDate() !== day) {
16
+ return null;
17
+ }
18
+ return utc;
19
+ }
20
+ function parseDateInput(value, optionName) {
21
+ if (typeof value !== 'string') {
22
+ return undefined;
23
+ }
24
+ const raw = value.trim();
25
+ if (!raw) {
26
+ throw new DinoxError(`${optionName} requires a non-empty date value`);
27
+ }
28
+ const isDateOnly = DATE_ONLY_RE.test(raw);
29
+ const dateOnly = parseUtcDateOnly(raw);
30
+ if (dateOnly) {
31
+ if (optionName === '--from') {
32
+ return dateOnly.toISOString();
33
+ }
34
+ return new Date(dateOnly.getTime() + DAY_MS - 1).toISOString();
35
+ }
36
+ if (isDateOnly) {
37
+ throw new DinoxError(`${optionName} must be a valid date (YYYY-MM-DD or ISO datetime)`);
38
+ }
39
+ const parsed = new Date(raw);
40
+ if (Number.isNaN(parsed.getTime())) {
41
+ throw new DinoxError(`${optionName} must be a valid date (YYYY-MM-DD or ISO datetime)`);
42
+ }
43
+ return parsed.toISOString();
44
+ }
45
+ function parseDaysInput(value) {
46
+ if (value === undefined || value === null || value === '') {
47
+ return undefined;
48
+ }
49
+ if (typeof value !== 'string') {
50
+ throw new DinoxError('--days must be a positive integer');
51
+ }
52
+ const trimmed = value.trim();
53
+ if (!/^\d+$/.test(trimmed)) {
54
+ throw new DinoxError('--days must be a positive integer');
55
+ }
56
+ const parsed = Number(trimmed);
57
+ if (!Number.isFinite(parsed) || parsed <= 0) {
58
+ throw new DinoxError('--days must be a positive integer');
59
+ }
60
+ return Math.trunc(parsed);
61
+ }
62
+ export function parseCreatedAtRange(input, now = new Date()) {
63
+ const from = parseDateInput(input.from, '--from');
64
+ const to = parseDateInput(input.to, '--to');
65
+ const days = parseDaysInput(input.days);
66
+ if (days !== undefined && (from || to)) {
67
+ throw new DinoxError('--days cannot be used together with --from/--to');
68
+ }
69
+ if (days !== undefined) {
70
+ const nowIso = now.toISOString();
71
+ const todayUtcStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0);
72
+ const fromUtc = new Date(todayUtcStart - (days - 1) * DAY_MS).toISOString();
73
+ return {
74
+ createdAtFrom: fromUtc,
75
+ createdAtTo: nowIso,
76
+ };
77
+ }
78
+ if (from && to && from > to) {
79
+ throw new DinoxError('--from must be earlier than or equal to --to');
80
+ }
81
+ return {
82
+ createdAtFrom: from,
83
+ createdAtTo: to,
84
+ };
85
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPromptCommands(program: Command): void;
@@ -0,0 +1,51 @@
1
+ import { loadConfig } from '../../config/store.js';
2
+ import { resolveConfig } from '../../config/resolve.js';
3
+ import { DinoxError } from '../../utils/errors.js';
4
+ import { printYaml } from '../../utils/output.js';
5
+ import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
+ import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
+ import { listPrompts } from './repo.js';
8
+ function parseSyncTimeoutMs(value) {
9
+ if (typeof value !== 'string') {
10
+ return undefined;
11
+ }
12
+ const parsed = Number(value);
13
+ if (!Number.isFinite(parsed) || parsed <= 0) {
14
+ throw new DinoxError('--sync-timeout must be a positive integer');
15
+ }
16
+ return Math.trunc(parsed);
17
+ }
18
+ async function syncBeforePromptCommand(db, offline, timeoutMs) {
19
+ if (offline) {
20
+ return;
21
+ }
22
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
23
+ await syncNoteTokenIndex(db);
24
+ }
25
+ async function runPromptListCommand(options, command) {
26
+ const globals = command.optsWithGlobals?.() ?? {};
27
+ const offline = Boolean(options.offline ?? globals.offline);
28
+ const config = resolveConfig(await loadConfig());
29
+ const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
30
+ const { db } = await connectPowerSync({ config, offline, timeoutMs });
31
+ try {
32
+ await syncBeforePromptCommand(db, offline, timeoutMs);
33
+ const prompts = await listPrompts(db);
34
+ printYaml(prompts);
35
+ }
36
+ finally {
37
+ await db.close().catch(() => undefined);
38
+ }
39
+ }
40
+ export function registerPromptCommands(program) {
41
+ const prompt = program.command('prompt').description('Manage prompts');
42
+ prompt
43
+ .command('list')
44
+ .description('List prompts from c_cmd (name and cmd)')
45
+ .option('--offline', 'skip connect/sync and only use local cache')
46
+ .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
47
+ .action(runPromptListCommand);
48
+ prompt.action((_options, command) => {
49
+ command.outputHelp();
50
+ });
51
+ }
@@ -0,0 +1,6 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ export type PromptListItem = {
3
+ name: string;
4
+ cmd: string;
5
+ };
6
+ export declare function listPrompts(db: AbstractPowerSyncDatabase): Promise<PromptListItem[]>;
@@ -0,0 +1,18 @@
1
+ export async function listPrompts(db) {
2
+ const rows = await db.getAll(`
3
+ SELECT DISTINCT
4
+ TRIM(COALESCE(name, '')) AS name,
5
+ TRIM(COALESCE(cmd, '')) AS cmd
6
+ FROM c_cmd
7
+ WHERE (is_del IS NULL OR is_del = 0)
8
+ AND TRIM(COALESCE(name, '')) <> ''
9
+ AND TRIM(COALESCE(cmd, '')) <> ''
10
+ ORDER BY COALESCE(priority, 0) DESC, name COLLATE NOCASE ASC, cmd COLLATE NOCASE ASC
11
+ `, []);
12
+ return rows
13
+ .map((row) => ({
14
+ name: row.name?.trim() ?? '',
15
+ cmd: row.cmd?.trim() ?? '',
16
+ }))
17
+ .filter((row) => row.name.length > 0 && row.cmd.length > 0);
18
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSyncCommand(program: Command): void;
@@ -0,0 +1,68 @@
1
+ import { loadConfig } from '../config/store.js';
2
+ import { resolveConfig } from '../config/resolve.js';
3
+ import { DinoxError } from '../utils/errors.js';
4
+ import { printYaml } from '../utils/output.js';
5
+ import { connectPowerSync, getStatusSnapshot, waitForIdleDataFlow } from '../powersync/runtime.js';
6
+ import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
7
+ function parseSyncTimeoutMs(value) {
8
+ if (typeof value !== 'string') {
9
+ return undefined;
10
+ }
11
+ const parsed = Number(value);
12
+ if (!Number.isFinite(parsed) || parsed <= 0) {
13
+ throw new DinoxError('--sync-timeout must be a positive integer');
14
+ }
15
+ return Math.trunc(parsed);
16
+ }
17
+ export function registerSyncCommand(program) {
18
+ program
19
+ .command('sync')
20
+ .description('Connect and synchronize local PowerSync database')
21
+ .action(async (_options, command) => {
22
+ const config = resolveConfig(await loadConfig());
23
+ const globals = command.optsWithGlobals?.() ?? {};
24
+ const jsonOutput = Boolean(globals.json);
25
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
26
+ const { db, dbPath, stale } = await connectPowerSync({
27
+ config,
28
+ offline: false,
29
+ timeoutMs,
30
+ });
31
+ try {
32
+ const idle = await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
33
+ const tokenIndex = await syncNoteTokenIndex(db);
34
+ const status = getStatusSnapshot(db);
35
+ const payload = {
36
+ ok: true,
37
+ dbPath,
38
+ stale,
39
+ idle,
40
+ uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
41
+ tokenIndex,
42
+ status,
43
+ };
44
+ if (jsonOutput) {
45
+ printYaml(payload);
46
+ }
47
+ else {
48
+ console.log(`db: ${payload.dbPath}`);
49
+ if (!payload.uploadEnabled) {
50
+ console.log('warning: upload disabled (powersync.uploadBaseUrl is unset)');
51
+ }
52
+ if (payload.status.lastSyncedAt) {
53
+ console.log(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
54
+ }
55
+ if (payload.stale) {
56
+ console.log('warning: sync timed out; local cache may be stale');
57
+ }
58
+ if (!payload.idle) {
59
+ console.log('warning: data flow did not become idle before timeout');
60
+ }
61
+ console.log(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
62
+ }
63
+ }
64
+ finally {
65
+ await db.close().catch(() => undefined);
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerTagsCommand(program: Command): void;
@@ -0,0 +1,120 @@
1
+ import { loadConfig } from '../../config/store.js';
2
+ import { resolveConfig } from '../../config/resolve.js';
3
+ import { DinoxError } from '../../utils/errors.js';
4
+ import { printYaml } from '../../utils/output.js';
5
+ import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
6
+ import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
7
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
8
+ import { addTag, listTags } from './repo.js';
9
+ function parseSyncTimeoutMs(value) {
10
+ if (typeof value !== 'string') {
11
+ return undefined;
12
+ }
13
+ const parsed = Number(value);
14
+ if (!Number.isFinite(parsed) || parsed <= 0) {
15
+ throw new DinoxError('--sync-timeout must be a positive integer');
16
+ }
17
+ return Math.trunc(parsed);
18
+ }
19
+ async function syncBeforeTagCommand(db, offline, timeoutMs) {
20
+ if (offline) {
21
+ return;
22
+ }
23
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
24
+ await syncNoteTokenIndex(db);
25
+ }
26
+ async function runTagListCommand(options, command) {
27
+ const globals = command.optsWithGlobals?.() ?? {};
28
+ const offline = Boolean(options.offline ?? globals.offline);
29
+ const jsonOutput = Boolean(options.json ?? globals.json);
30
+ const config = resolveConfig(await loadConfig());
31
+ const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
32
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
33
+ try {
34
+ await syncBeforeTagCommand(db, offline, timeoutMs);
35
+ const tags = await listTags(db);
36
+ if (jsonOutput) {
37
+ printYaml({ stale, count: tags.length, tags });
38
+ return;
39
+ }
40
+ if (stale && !offline) {
41
+ console.log('warning: sync timed out; results may be stale');
42
+ }
43
+ if (tags.length === 0) {
44
+ console.log('No tags.');
45
+ return;
46
+ }
47
+ for (const tag of tags) {
48
+ console.log(tag);
49
+ }
50
+ }
51
+ finally {
52
+ await db.close().catch(() => undefined);
53
+ }
54
+ }
55
+ export function registerTagsCommand(program) {
56
+ const tag = program.command('tag').description('Manage tags');
57
+ tag
58
+ .command('list')
59
+ .description('List tags from c_tag_node')
60
+ .option('--json', 'output machine-readable YAML')
61
+ .option('--offline', 'skip connect/sync and only use local cache')
62
+ .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
63
+ .action(runTagListCommand);
64
+ tag
65
+ .command('add')
66
+ .description('Create a new tag in c_tag_node')
67
+ .argument('[name]', 'tag name/path (supports slash hierarchy)')
68
+ .option('--name <string>', 'tag name/path (alternative to positional name)')
69
+ .option('--emoji <string>', 'tag emoji')
70
+ .option('--json', 'output machine-readable YAML')
71
+ .option('--offline', 'skip connect/sync and only use local cache')
72
+ .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
73
+ .action(async (nameArg, options, command) => {
74
+ const globals = command.optsWithGlobals?.() ?? {};
75
+ const offline = Boolean(options.offline ?? globals.offline);
76
+ const jsonOutput = Boolean(options.json ?? globals.json);
77
+ const optionName = typeof options.name === 'string' ? options.name : undefined;
78
+ const name = (nameArg ?? optionName ?? '').trim();
79
+ if (!name) {
80
+ throw new DinoxError('Tag name is required. Use `dino tag add <name>` or `dino tag add --name <name>`.');
81
+ }
82
+ if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
83
+ throw new DinoxError('Conflicting tag name values from positional argument and --name option.');
84
+ }
85
+ const rawConfig = await loadConfig();
86
+ const config = resolveConfig(rawConfig);
87
+ const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
88
+ const identity = await resolveAuthIdentity(rawConfig, config);
89
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
90
+ try {
91
+ await syncBeforeTagCommand(db, offline, timeoutMs);
92
+ const result = await addTag(db, {
93
+ name,
94
+ userId: identity.userId,
95
+ emoji: typeof options.emoji === 'string' ? options.emoji : undefined,
96
+ });
97
+ if (!offline && !config.powersync.uploadBaseUrl) {
98
+ console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
99
+ }
100
+ if (!offline) {
101
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
102
+ }
103
+ const payload = { ok: true, stale: offline ? true : stale, ...result };
104
+ if (jsonOutput) {
105
+ printYaml(payload);
106
+ return;
107
+ }
108
+ console.log(result.id);
109
+ if (stale && !offline) {
110
+ console.log('warning: sync timed out; results may be stale');
111
+ }
112
+ }
113
+ finally {
114
+ await db.close().catch(() => undefined);
115
+ }
116
+ });
117
+ tag.action((_options, command) => {
118
+ command.outputHelp();
119
+ });
120
+ }
@@ -0,0 +1,14 @@
1
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
+ export type AddTagInput = {
3
+ name: string;
4
+ userId: string;
5
+ emoji?: string;
6
+ };
7
+ export type AddTagResult = {
8
+ id: string;
9
+ tag: string;
10
+ createdNodes: number;
11
+ restoredNodes: number;
12
+ };
13
+ export declare function listTags(db: AbstractPowerSyncDatabase): Promise<string[]>;
14
+ export declare function addTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<AddTagResult>;
@@ -0,0 +1,247 @@
1
+ import { generateId } from '../../utils/id.js';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ function canonicalizeTagPath(value) {
4
+ return value
5
+ .split(/[\\/]+/g)
6
+ .map((segment) => segment.trim())
7
+ .filter(Boolean)
8
+ .join('/');
9
+ }
10
+ async function getTagNodeColumns(db) {
11
+ const rows = await db.getAll('PRAGMA table_info(c_tag_node)', []);
12
+ return new Set(rows.map((row) => row.name).filter(Boolean));
13
+ }
14
+ async function loadTagNodes(db, columnNames) {
15
+ const fullPathExpr = columnNames.has('full_path')
16
+ ? (columnNames.has('path_key')
17
+ ? `TRIM(COALESCE(full_path, path_key, ''))`
18
+ : `TRIM(COALESCE(full_path, ''))`)
19
+ : `TRIM(COALESCE(path_key, ''))`;
20
+ const pathKeyExpr = columnNames.has('path_key')
21
+ ? (columnNames.has('full_path')
22
+ ? `TRIM(COALESCE(path_key, full_path, ''))`
23
+ : `TRIM(COALESCE(path_key, ''))`)
24
+ : `TRIM(COALESCE(full_path, ''))`;
25
+ const userIdExpr = columnNames.has('user_id')
26
+ ? `TRIM(COALESCE(user_id, ''))`
27
+ : `''`;
28
+ const emojiExpr = columnNames.has('emoji')
29
+ ? `TRIM(COALESCE(emoji, ''))`
30
+ : `''`;
31
+ const parentIdExpr = columnNames.has('parent_id') ? 'parent_id' : 'NULL';
32
+ const depthExpr = columnNames.has('depth') ? 'depth' : 'NULL';
33
+ const isActualExpr = columnNames.has('is_actual') ? 'is_actual' : 'NULL';
34
+ const isDelExpr = columnNames.has('is_del') ? 'is_del' : 'NULL';
35
+ const updatedOrderExpr = columnNames.has('updated_at') ? 'updated_at DESC,' : '';
36
+ const createdOrderExpr = columnNames.has('created_at') ? 'created_at DESC,' : '';
37
+ const rows = await db.getAll(`
38
+ SELECT
39
+ TRIM(COALESCE(id, '')) AS id,
40
+ ${fullPathExpr} AS full_path,
41
+ ${pathKeyExpr} AS path_key,
42
+ ${parentIdExpr} AS parent_id,
43
+ ${depthExpr} AS depth,
44
+ ${isActualExpr} AS is_actual,
45
+ ${isDelExpr} AS is_del,
46
+ ${userIdExpr} AS user_id,
47
+ ${emojiExpr} AS emoji
48
+ FROM c_tag_node
49
+ WHERE ${fullPathExpr} <> ''
50
+ ORDER BY
51
+ CASE WHEN is_del IS NULL OR is_del = 0 THEN 0 ELSE 1 END ASC,
52
+ CASE WHEN is_actual IS NULL THEN 0 ELSE is_actual END DESC,
53
+ ${updatedOrderExpr}
54
+ ${createdOrderExpr}
55
+ id DESC
56
+ `, []);
57
+ const nodes = new Map();
58
+ for (const row of rows) {
59
+ const id = row.id?.trim() ?? '';
60
+ if (!id) {
61
+ continue;
62
+ }
63
+ const fullPath = canonicalizeTagPath(row.full_path ?? row.path_key ?? '');
64
+ if (!fullPath) {
65
+ continue;
66
+ }
67
+ if (nodes.has(fullPath)) {
68
+ continue;
69
+ }
70
+ nodes.set(fullPath, {
71
+ id,
72
+ fullPath,
73
+ pathKey: canonicalizeTagPath(row.path_key ?? fullPath) || fullPath,
74
+ parentId: row.parent_id ?? null,
75
+ depth: typeof row.depth === 'number' ? row.depth : null,
76
+ isActual: typeof row.is_actual === 'number' ? row.is_actual : null,
77
+ isDel: typeof row.is_del === 'number' ? row.is_del : null,
78
+ userId: row.user_id?.trim() ?? '',
79
+ emoji: row.emoji?.trim() ?? '',
80
+ });
81
+ }
82
+ return nodes;
83
+ }
84
+ export async function listTags(db) {
85
+ const rows = await db.getAll(`
86
+ SELECT DISTINCT TRIM(COALESCE(full_path, path_key, '')) AS tag
87
+ FROM c_tag_node
88
+ WHERE (is_del IS NULL OR is_del = 0)
89
+ AND (is_actual IS NULL OR is_actual != 0)
90
+ AND TRIM(COALESCE(full_path, path_key, '')) <> ''
91
+ ORDER BY tag COLLATE NOCASE ASC
92
+ `, []);
93
+ return rows.map((row) => row.tag?.trim() ?? '').filter(Boolean);
94
+ }
95
+ export async function addTag(db, input) {
96
+ const userId = input.userId.trim();
97
+ const emoji = typeof input.emoji === 'string' ? input.emoji.trim() : '';
98
+ const tag = canonicalizeTagPath(input.name);
99
+ if (!tag) {
100
+ throw new DinoxError('Tag name is required');
101
+ }
102
+ const columnNames = await getTagNodeColumns(db);
103
+ if (!columnNames.has('id')) {
104
+ throw new DinoxError('c_tag_node table is missing required column: id');
105
+ }
106
+ if (!columnNames.has('full_path') && !columnNames.has('path_key')) {
107
+ throw new DinoxError('c_tag_node table is missing required column: full_path or path_key');
108
+ }
109
+ const requiredUserColumns = ['user_id'].filter((column) => columnNames.has(column));
110
+ if (requiredUserColumns.length > 0 && !userId) {
111
+ throw new DinoxError(`c_tag_node insert requires ${requiredUserColumns.join(', ')}. Missing persisted userId. Run \`dino auth login\` first.`);
112
+ }
113
+ const nodesByPath = await loadTagNodes(db, columnNames);
114
+ const existingLeaf = nodesByPath.get(tag);
115
+ if (existingLeaf && existingLeaf.isDel !== 1 && existingLeaf.isActual === 1) {
116
+ throw new DinoxError(`Tag already exists: ${tag}`, {
117
+ details: {
118
+ type: 'tag_exists',
119
+ tag,
120
+ id: existingLeaf.id,
121
+ question: 'This tag already exists. Do you want to keep using the existing tag?',
122
+ },
123
+ });
124
+ }
125
+ const timestamp = new Date().toISOString();
126
+ const segments = tag.split('/');
127
+ let createdNodes = 0;
128
+ let restoredNodes = 0;
129
+ let parentId = null;
130
+ let leafId = '';
131
+ for (let i = 0; i < segments.length; i += 1) {
132
+ const path = segments.slice(0, i + 1).join('/');
133
+ const isLeaf = i === segments.length - 1;
134
+ const existing = nodesByPath.get(path);
135
+ if (existing) {
136
+ const assignments = [];
137
+ const params = [];
138
+ const setValue = (column, value) => {
139
+ if (!columnNames.has(column)) {
140
+ return;
141
+ }
142
+ assignments.push(`${column} = ?`);
143
+ params.push(value);
144
+ };
145
+ if (existing.isDel === 1) {
146
+ setValue('is_del', 0);
147
+ }
148
+ if (isLeaf && existing.isActual !== 1) {
149
+ setValue('is_actual', 1);
150
+ }
151
+ if (columnNames.has('updated_at')) {
152
+ setValue('updated_at', timestamp);
153
+ }
154
+ if (columnNames.has('user_id') && !existing.userId) {
155
+ setValue('user_id', userId);
156
+ }
157
+ if (columnNames.has('parent_id') && existing.parentId == null && parentId !== null) {
158
+ setValue('parent_id', parentId);
159
+ }
160
+ if (columnNames.has('depth') && existing.depth == null) {
161
+ setValue('depth', i);
162
+ }
163
+ if (columnNames.has('full_path') && existing.fullPath !== path) {
164
+ setValue('full_path', path);
165
+ }
166
+ if (columnNames.has('path_key') && existing.pathKey !== path) {
167
+ setValue('path_key', path);
168
+ }
169
+ if (isLeaf && emoji && columnNames.has('emoji') && existing.emoji !== emoji) {
170
+ setValue('emoji', emoji);
171
+ }
172
+ if (assignments.length > 0) {
173
+ params.push(existing.id);
174
+ await db.execute(`
175
+ UPDATE c_tag_node
176
+ SET ${assignments.join(', ')}
177
+ WHERE id = ?
178
+ `, params);
179
+ if (existing.isDel === 1) {
180
+ restoredNodes += 1;
181
+ existing.isDel = 0;
182
+ }
183
+ if (isLeaf && existing.isActual !== 1) {
184
+ existing.isActual = 1;
185
+ }
186
+ }
187
+ parentId = existing.id;
188
+ if (isLeaf) {
189
+ leafId = existing.id;
190
+ }
191
+ continue;
192
+ }
193
+ const id = generateId();
194
+ const insertEntries = [];
195
+ const pushInsert = (column, value) => {
196
+ if (!columnNames.has(column)) {
197
+ return;
198
+ }
199
+ insertEntries.push([column, value]);
200
+ };
201
+ pushInsert('id', id);
202
+ pushInsert('user_id', userId);
203
+ pushInsert('emoji', isLeaf && emoji ? emoji : null);
204
+ pushInsert('full_path', path);
205
+ pushInsert('path_key', path);
206
+ pushInsert('parent_id', parentId);
207
+ pushInsert('depth', i);
208
+ pushInsert('is_actual', isLeaf ? 1 : 0);
209
+ pushInsert('is_del', 0);
210
+ pushInsert('created_at', timestamp);
211
+ pushInsert('updated_at', timestamp);
212
+ pushInsert('meta', '{}');
213
+ const insertColumns = insertEntries.map(([column]) => column);
214
+ const placeholders = insertColumns.map(() => '?').join(', ');
215
+ const insertParams = insertEntries.map(([, value]) => value);
216
+ await db.execute(`
217
+ INSERT INTO c_tag_node (${insertColumns.join(', ')})
218
+ VALUES (${placeholders})
219
+ `, insertParams);
220
+ const node = {
221
+ id,
222
+ fullPath: path,
223
+ pathKey: path,
224
+ parentId,
225
+ depth: i,
226
+ isActual: isLeaf ? 1 : 0,
227
+ isDel: 0,
228
+ userId,
229
+ emoji: isLeaf && emoji ? emoji : '',
230
+ };
231
+ nodesByPath.set(path, node);
232
+ createdNodes += 1;
233
+ parentId = id;
234
+ if (isLeaf) {
235
+ leafId = id;
236
+ }
237
+ }
238
+ if (!leafId) {
239
+ throw new DinoxError(`Failed to create tag: ${tag}`);
240
+ }
241
+ return {
242
+ id: leafId,
243
+ tag,
244
+ createdNodes,
245
+ restoredNodes,
246
+ };
247
+ }
@@ -0,0 +1,9 @@
1
+ export declare const CONFIG_KEYS: readonly ["powersync.tokenEndpoint", "powersync.uploadV4Path", "powersync.uploadV2Path", "sync.timeoutMs"];
2
+ export declare const FIXED_CONFIG_KEYS: readonly ["powersync.endpoint", "powersync.uploadBaseUrl"];
3
+ export declare const READABLE_CONFIG_KEYS: readonly ["powersync.tokenEndpoint", "powersync.uploadV4Path", "powersync.uploadV2Path", "sync.timeoutMs", "powersync.endpoint", "powersync.uploadBaseUrl"];
4
+ export type ConfigKey = (typeof CONFIG_KEYS)[number];
5
+ export type FixedConfigKey = (typeof FIXED_CONFIG_KEYS)[number];
6
+ export type ReadableConfigKey = (typeof READABLE_CONFIG_KEYS)[number];
7
+ export declare function isConfigKey(value: string): value is ConfigKey;
8
+ export declare function isFixedConfigKey(value: string): value is FixedConfigKey;
9
+ export declare function isReadableConfigKey(value: string): value is ReadableConfigKey;
@@ -0,0 +1,17 @@
1
+ export const CONFIG_KEYS = [
2
+ 'powersync.tokenEndpoint',
3
+ 'powersync.uploadV4Path',
4
+ 'powersync.uploadV2Path',
5
+ 'sync.timeoutMs',
6
+ ];
7
+ export const FIXED_CONFIG_KEYS = ['powersync.endpoint', 'powersync.uploadBaseUrl'];
8
+ export const READABLE_CONFIG_KEYS = [...CONFIG_KEYS, ...FIXED_CONFIG_KEYS];
9
+ export function isConfigKey(value) {
10
+ return CONFIG_KEYS.includes(value);
11
+ }
12
+ export function isFixedConfigKey(value) {
13
+ return FIXED_CONFIG_KEYS.includes(value);
14
+ }
15
+ export function isReadableConfigKey(value) {
16
+ return READABLE_CONFIG_KEYS.includes(value);
17
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getConfigDir(): string;
2
+ export declare function getConfigFilePath(): string;
3
+ export declare function getDataDir(): string;
4
+ export declare function getPowerSyncDbPath(): string;