@dinoxx/dinox-cli 1.0.0 → 1.0.2

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.
package/README.md CHANGED
@@ -115,6 +115,8 @@ dino auth status
115
115
  dino note search "AI"
116
116
  dino note search "AI" --days 7
117
117
  dino note search "AI" --from 2026-02-01 --to 2026-02-28
118
+ dino note search "AI" --box "Inbox"
119
+ dino note search --sql 'type = "crawl" AND zettel_boxes IN ("Inbox","Project")'
118
120
  ```
119
121
 
120
122
  创建笔记:
@@ -207,11 +209,6 @@ dino config set sync.timeoutMs 20000
207
209
  1. macOS: `~/Library/Application Support/dinox/config.json`
208
210
  2. Windows: `%APPDATA%\dinox\config.json`
209
211
 
210
- 本地数据库:
211
-
212
- 1. macOS: `~/Library/Application Support/dinox/powersync.sqlite`
213
- 2. Windows: `%LOCALAPPDATA%\dinox\powersync.sqlite`
214
-
215
212
  ## 6.2 多行命令写法不同
216
213
 
217
214
  macOS / Linux(bash/zsh)用 `\` 续行:
@@ -267,16 +264,238 @@ dino tag add "你的标签"
267
264
  dino box add "你的卡片盒"
268
265
  ```
269
266
 
270
- ## Q3:我设置了 `powersync.endpoint` 但没生效
267
+ ---
268
+
269
+ ## 8. 完整命令参数参考
270
+
271
+ ## 8.1 全局参数(适用于 CLI)
272
+
273
+ 以下参数可放在命令前后使用,例如:`dino --json auth status`。
274
+
275
+ | 参数 | 类型 | 说明 |
276
+ | --- | --- | --- |
277
+ | `--json` | flag | 输出 machine-readable YAML(仅被支持该参数的命令读取)。 |
278
+ | `--offline` | flag | 跳过 connect/sync,仅用本地缓存(仅被支持该参数的命令读取)。 |
279
+ | `--sync-timeout <ms>` | integer | 覆盖连接/同步超时毫秒,必须是正整数。 |
280
+ | `--verbose` | flag | 预留 verbose 开关。 |
281
+
282
+ ## 8.2 `auth` 命令
283
+
284
+ ### `dino auth login <authorization>`
285
+
286
+ 保存授权信息并验证连接。
287
+
288
+ | 参数 | 必填 | 说明 |
289
+ | --- | --- | --- |
290
+ | `<authorization>` | 是 | 完整 Authorization 头值,例如:`"Bearer <token>"` |
291
+
292
+ | 选项 | 必填 | 说明 |
293
+ | --- | --- | --- |
294
+ | `--no-verify` | 否 | 只保存登录信息,不做凭证交换与初始同步验证。 |
295
+
296
+ 支持全局参数:`--json`、`--sync-timeout`
297
+
298
+ ### `dino auth logout`
299
+
300
+ 清理已保存登录信息。
301
+
302
+ | 选项 | 必填 | 说明 |
303
+ | --- | --- | --- |
304
+ | `--clear-local-db` | 否 | 同时删除本地 SQLite 数据库。 |
305
+
306
+ ### `dino auth status`
307
+
308
+ 查看当前登录与同步状态。
309
+
310
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
311
+
312
+ ## 8.3 `sync` 命令
313
+
314
+ ### `dino sync`
315
+
316
+ 连接并执行同步。
317
+
318
+ 支持全局参数:`--json`、`--sync-timeout`
319
+
320
+ ## 8.4 `note` 命令
321
+
322
+ ### `dino note search [query]`
323
+
324
+ 按关键词/标签/创建时间搜索笔记。
325
+
326
+ | 参数 | 必填 | 说明 |
327
+ | --- | --- | --- |
328
+ | `[query]` | 否 | 搜索关键词。可为空。 |
329
+
330
+ | 选项 | 必填 | 说明 |
331
+ | --- | --- | --- |
332
+ | `--tags <expr>` | 否 | 标签表达式,支持 `AND` / `OR` / `NOT` 与括号。 |
333
+ | `--from <date>` | 否 | 创建时间起点,支持 `YYYY-MM-DD` 或 ISO datetime。 |
334
+ | `--to <date>` | 否 | 创建时间终点,支持 `YYYY-MM-DD` 或 ISO datetime。 |
335
+ | `--days <n>` | 否 | 最近 N 天(按 `created_at`),不能与 `--from/--to` 同时使用。 |
336
+ | `--box <string\|@file>` | 否 | 按卡片盒名称筛选(支持 JSON 数组或逗号/换行分隔);会自动解析为 box id 查询。 |
337
+ | `--sql <expr>` | 否 | SQL 风格条件表达式;仅支持字段 `id/content_md/summary/tags/zettel_boxes/created_at/type`,其中 `zettel_boxes` 按名称自动转 id。只支持只读 WHERE 条件(禁止 `INSERT/UPDATE/DELETE`、注释和多语句)。 |
338
+ | `--include-deleted` | 否 | 包含软删除笔记。 |
339
+
340
+ 支持全局参数:`--offline`、`--sync-timeout`
341
+
342
+ ### `dino note get <id>`
343
+
344
+ 按 ID 获取笔记基础信息。
345
+
346
+ | 参数 | 必填 | 说明 |
347
+ | --- | --- | --- |
348
+ | `<id>` | 是 | 笔记 ID。 |
349
+
350
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
351
+
352
+ ### `dino note detail <id>`
353
+
354
+ 按 ID 获取笔记完整详情。
355
+
356
+ | 参数 | 必填 | 说明 |
357
+ | --- | --- | --- |
358
+ | `<id>` | 是 | 笔记 ID。 |
359
+
360
+ 支持全局参数:`--offline`、`--sync-timeout`
361
+
362
+ ### `dino note create --title <string> --content <string|@file> [options]`
363
+
364
+ 创建笔记。
365
+
366
+ | 选项 | 必填 | 说明 |
367
+ | --- | --- | --- |
368
+ | `--title <string>` | 是 | 笔记标题。 |
369
+ | `--content <string|@file>` | 是 | Markdown 内容,可传 `@文件路径`。 |
370
+ | `--type <note\|crawl>` | 否 | 笔记类型,默认 `crawl`。 |
371
+ | `--tags <string\|@file>` | 否 | 标签列表(JSON 数组或逗号/换行分隔)。 |
372
+ | `--zettel_boxes <string\|@file>` | 否 | 卡片盒名称列表(JSON 数组或逗号/换行分隔)。 |
373
+
374
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
375
+
376
+ ### `dino note delete <id>`
377
+
378
+ 软删除笔记(`is_del=1`)。
379
+
380
+ | 参数 | 必填 | 说明 |
381
+ | --- | --- | --- |
382
+ | `<id>` | 是 | 笔记 ID。 |
383
+
384
+ 支持全局参数:`--json`、`--offline`、`--sync-timeout`
385
+
386
+ ## 8.5 `tag` 命令
387
+
388
+ ### `dino tag list`
389
+
390
+ 列出标签。
391
+
392
+ | 选项 | 必填 | 说明 |
393
+ | --- | --- | --- |
394
+ | `--json` | 否 | 输出 machine-readable YAML。 |
395
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
396
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
397
+
398
+ ### `dino tag add [name] [options]`
399
+
400
+ 新增标签。
401
+
402
+ | 参数 | 必填 | 说明 |
403
+ | --- | --- | --- |
404
+ | `[name]` | 否 | 标签名/路径(支持斜杠层级)。 |
405
+
406
+ | 选项 | 必填 | 说明 |
407
+ | --- | --- | --- |
408
+ | `--name <string>` | 否 | 标签名/路径,可替代位置参数 `[name]`。 |
409
+ | `--emoji <string>` | 否 | 标签 emoji。 |
410
+ | `--json` | 否 | 输出 machine-readable YAML。 |
411
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
412
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
413
+
414
+ 说明:`[name]` 与 `--name` 至少提供一个;若两者都提供且值不同会报错。
415
+
416
+ ## 8.6 `box` 命令
417
+
418
+ ### `dino box list`
419
+
420
+ 列出卡片盒。
421
+
422
+ | 选项 | 必填 | 说明 |
423
+ | --- | --- | --- |
424
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
425
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
426
+
427
+ ### `dino box add [name] [options]`
428
+
429
+ 新增卡片盒。
430
+
431
+ | 参数 | 必填 | 说明 |
432
+ | --- | --- | --- |
433
+ | `[name]` | 否 | 卡片盒名称。 |
434
+
435
+ | 选项 | 必填 | 说明 |
436
+ | --- | --- | --- |
437
+ | `--name <string>` | 否 | 卡片盒名称,可替代位置参数 `[name]`。 |
438
+ | `--description <string>` | 否 | 用途说明(可帮助 AI 路由笔记)。 |
439
+ | `--color <string>` | 否 | 卡片盒颜色。 |
440
+ | `--json` | 否 | 输出 machine-readable YAML。 |
441
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
442
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
443
+
444
+ 说明:`[name]` 与 `--name` 至少提供一个;若两者都提供且值不同会报错。
445
+
446
+ ## 8.7 `prompt` 命令
447
+
448
+ ### `dino prompt list`
449
+
450
+ 列出 Prompt(`name` 与 `cmd`)。
451
+
452
+ | 选项 | 必填 | 说明 |
453
+ | --- | --- | --- |
454
+ | `--offline` | 否 | 跳过 connect/sync,仅用本地缓存。 |
455
+ | `--sync-timeout <ms>` | 否 | 覆盖连接/同步超时毫秒。 |
456
+
457
+ ## 8.8 `config` 命令
458
+
459
+ ### `dino config get [key]`
460
+
461
+ 读取配置(输出 YAML)。
462
+
463
+ | 参数 | 必填 | 说明 |
464
+ | --- | --- | --- |
465
+ | `[key]` | 否 | 配置键(点路径);不传则返回全部配置。 |
466
+
467
+ 可读 key:
468
+
469
+ ```text
470
+ sync.timeoutMs
471
+ ```
472
+
473
+ ### `dino config set <key> <value>`
474
+
475
+ 写入配置。
476
+
477
+ | 参数 | 必填 | 说明 |
478
+ | --- | --- | --- |
479
+ | `<key>` | 是 | 配置键(点路径)。 |
480
+ | `<value>` | 是 | 配置值。 |
481
+
482
+ 可写 key:
483
+
484
+ ```text
485
+ sync.timeoutMs (必须为正整数)
486
+ ```
487
+
488
+ ## 8.9 `info` 命令
489
+
490
+ ### `dino info`
271
491
 
272
- 这两个配置是固定的,不允许修改:
492
+ 显示 CLI 版本信息。
273
493
 
274
- 1. `powersync.endpoint = https://powersync.dinoai.fun`
275
- 2. `powersync.uploadBaseUrl = https://dinoai.chatgo.pro`
494
+ 支持全局参数:`--json`
276
495
 
277
496
  ---
278
497
 
279
- ## 8. 查看帮助
498
+ ## 9. 查看帮助
280
499
 
281
500
  随时可以看帮助:
282
501
 
@@ -18,6 +18,12 @@ function parseSyncTimeoutMs(value) {
18
18
  }
19
19
  return Math.trunc(parsed);
20
20
  }
21
+ async function removeSqliteArtifacts(dbPath) {
22
+ const artifacts = ['', '-wal', '-shm'];
23
+ for (const suffix of artifacts) {
24
+ await fs.rm(`${dbPath}${suffix}`, { force: true }).catch(() => undefined);
25
+ }
26
+ }
21
27
  export function registerAuthCommands(program) {
22
28
  const auth = program
23
29
  .command('auth')
@@ -110,7 +116,7 @@ export function registerAuthCommands(program) {
110
116
  await saveConfig(config);
111
117
  if (options.clearLocalDb) {
112
118
  const dbPath = getPowerSyncDbPath();
113
- await fs.rm(dbPath, { force: true }).catch(() => undefined);
119
+ await removeSqliteArtifacts(dbPath);
114
120
  }
115
121
  });
116
122
  auth
@@ -1,2 +1,3 @@
1
1
  import { Command } from 'commander';
2
+ export declare function sanitizeConfigForDisplay(config: unknown): unknown;
2
3
  export declare function registerConfigCommands(program: Command): void;
@@ -1,10 +1,9 @@
1
1
  import { isConfigKey, isFixedConfigKey, isReadableConfigKey } from '../../config/keys.js';
2
- import { getConfigValue, loadConfig, setConfigValue } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
2
+ import { getConfigValue, setConfigValue } from '../../config/store.js';
4
3
  import { redactAuthorization } from '../../utils/redact.js';
5
4
  import { DinoxError } from '../../utils/errors.js';
6
5
  import { printYaml } from '../../utils/output.js';
7
- function redactConfigForDisplay(config) {
6
+ export function sanitizeConfigForDisplay(config) {
8
7
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
9
8
  return config;
10
9
  }
@@ -13,16 +12,9 @@ function redactConfigForDisplay(config) {
13
12
  if (auth && typeof auth === 'object') {
14
13
  auth.authorization = redactAuthorization(auth.authorization);
15
14
  }
15
+ delete clone.powersync;
16
16
  return clone;
17
17
  }
18
- function getByPath(value, keyPath) {
19
- return keyPath.split('.').reduce((current, segment) => {
20
- if (!current || typeof current !== 'object') {
21
- return undefined;
22
- }
23
- return current[segment];
24
- }, value);
25
- }
26
18
  export function registerConfigCommands(program) {
27
19
  const configCmd = program.command('config').description('Read and write Dinox configuration');
28
20
  configCmd
@@ -33,13 +25,8 @@ export function registerConfigCommands(program) {
33
25
  if (key && !isReadableConfigKey(key)) {
34
26
  throw new DinoxError(`Unknown config key: ${key}`);
35
27
  }
36
- if (key && isFixedConfigKey(key)) {
37
- const resolved = resolveConfig(await loadConfig());
38
- printYaml(getByPath(resolved, key));
39
- return;
40
- }
41
28
  const value = await getConfigValue(key);
42
- const redacted = key ? value : redactConfigForDisplay(value);
29
+ const redacted = key ? value : sanitizeConfigForDisplay(value);
43
30
  printYaml(redacted);
44
31
  });
45
32
  configCmd
@@ -8,7 +8,7 @@ import { printYaml } from '../../utils/output.js';
8
8
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
9
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
10
10
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
11
- import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
11
+ import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
12
12
  import { parseCreatedAtRange } from './searchTime.js';
13
13
  function parseSyncTimeoutMs(value) {
14
14
  if (typeof value !== 'string') {
@@ -78,6 +78,8 @@ export function registerNoteCommands(program) {
78
78
  .option('--from <date>', 'created_at start (YYYY-MM-DD or ISO datetime)')
79
79
  .option('--to <date>', 'created_at end (YYYY-MM-DD or ISO datetime)')
80
80
  .option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
81
+ .option('--box <string|@file>', 'zettel box names (JSON array or comma/newline-separated)')
82
+ .option('--sql <expr>', 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type')
81
83
  .option('--include-deleted', 'include soft-deleted notes')
82
84
  .action(async (query, options, command) => {
83
85
  const globals = command.optsWithGlobals?.() ?? {};
@@ -94,11 +96,17 @@ export function registerNoteCommands(program) {
94
96
  to: options.to,
95
97
  days: options.days,
96
98
  });
99
+ const boxInput = typeof options.box === 'string' ? options.box : '';
100
+ const zettelBoxNames = boxInput ? await parseStringListInput(boxInput, '--box') : [];
101
+ const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, zettelBoxNames);
102
+ const sqlExpression = typeof options.sql === 'string' ? options.sql.trim() : '';
97
103
  const rows = await searchNotes(db, keyword, {
98
104
  includeDeleted: Boolean(options.includeDeleted),
99
105
  tagsExpression: tagsExpression || undefined,
100
106
  createdAtFrom,
101
107
  createdAtTo,
108
+ zettelBoxIds,
109
+ sqlExpression: sqlExpression || undefined,
102
110
  });
103
111
  printYaml(rows);
104
112
  }
@@ -18,6 +18,8 @@ type SearchNotesOptions = {
18
18
  tagsExpression?: string;
19
19
  createdAtFrom?: string;
20
20
  createdAtTo?: string;
21
+ zettelBoxIds?: string[];
22
+ sqlExpression?: string;
21
23
  };
22
24
  export type SearchedNote = {
23
25
  id: string;
@@ -66,5 +68,6 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
66
68
  }>;
67
69
  export declare function validateTagsForCreate(db: AbstractPowerSyncDatabase, requestedTags: string[]): Promise<string[]>;
68
70
  export declare function resolveZettelBoxIdsForCreate(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
71
+ export declare function resolveZettelBoxIdsForSearch(db: AbstractPowerSyncDatabase, requestedNames: string[]): Promise<string[]>;
69
72
  export declare function softDeleteNote(db: AbstractPowerSyncDatabase, id: string): Promise<void>;
70
73
  export {};
@@ -3,16 +3,23 @@ import { nowIsoUtc } from '../../utils/time.js';
3
3
  import { tokenizeWithJieba } from '../../utils/tokenize.js';
4
4
  import { listBoxes } from '../boxes/repo.js';
5
5
  import { listTags } from '../tags/repo.js';
6
+ import { buildNoteSearchSqlFilter } from './searchSql.js';
6
7
  export async function searchNotes(db, query, options) {
7
8
  const normalizedQuery = query.trim();
8
9
  const tagFilter = buildTagFilter(options.tagsExpression, 'n');
9
10
  const createdAtFilter = buildCreatedAtFilter(options.createdAtFrom, options.createdAtTo, 'n');
11
+ const zettelBoxFilter = buildZettelBoxFilter(options.zettelBoxIds, 'n');
12
+ const sqlFilter = await buildNoteSearchSqlFilter({
13
+ expression: options.sqlExpression,
14
+ noteAlias: 'n',
15
+ resolveZettelBoxNames: (names) => resolveZettelBoxIdsForSearch(db, names),
16
+ });
10
17
  let rows = [];
11
18
  if (!options.includeDeleted) {
12
19
  const ftsMatch = buildFtsMatchQuery(normalizedQuery);
13
20
  if (ftsMatch) {
14
21
  try {
15
- rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter);
22
+ rows = await searchNotesWithFts(db, ftsMatch, tagFilter, createdAtFilter, zettelBoxFilter, sqlFilter);
16
23
  return enrichSearchRows(db, rows);
17
24
  }
18
25
  catch (error) {
@@ -53,6 +60,14 @@ export async function searchNotes(db, query, options) {
53
60
  whereClauses.push(createdAtFilter.sql);
54
61
  params.push(...createdAtFilter.params);
55
62
  }
63
+ if (zettelBoxFilter) {
64
+ whereClauses.push(zettelBoxFilter.sql);
65
+ params.push(...zettelBoxFilter.params);
66
+ }
67
+ if (sqlFilter) {
68
+ whereClauses.push(sqlFilter.sql);
69
+ params.push(...sqlFilter.params);
70
+ }
56
71
  rows = await db.getAll(`
57
72
  SELECT
58
73
  n.id,
@@ -69,7 +84,7 @@ export async function searchNotes(db, query, options) {
69
84
  `, params);
70
85
  return enrichSearchRows(db, rows);
71
86
  }
72
- async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
87
+ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter, zettelBoxFilter, sqlFilter) {
73
88
  const whereClauses = ['(n.is_del IS NULL OR n.is_del = 0)', 'note_local_fts MATCH ?'];
74
89
  const params = [matchQuery];
75
90
  if (tagFilter) {
@@ -80,6 +95,14 @@ async function searchNotesWithFts(db, matchQuery, tagFilter, createdAtFilter) {
80
95
  whereClauses.push(createdAtFilter.sql);
81
96
  params.push(...createdAtFilter.params);
82
97
  }
98
+ if (zettelBoxFilter) {
99
+ whereClauses.push(zettelBoxFilter.sql);
100
+ params.push(...zettelBoxFilter.params);
101
+ }
102
+ if (sqlFilter) {
103
+ whereClauses.push(sqlFilter.sql);
104
+ params.push(...sqlFilter.params);
105
+ }
83
106
  return db.getAll(`
84
107
  SELECT
85
108
  n.id,
@@ -214,6 +237,30 @@ function buildCreatedAtFilter(createdAtFrom, createdAtTo, noteAlias) {
214
237
  params,
215
238
  };
216
239
  }
240
+ function buildZettelBoxFilter(zettelBoxIds, noteAlias) {
241
+ const normalizedIds = Array.from(new Set((zettelBoxIds ?? [])
242
+ .map((id) => id.trim())
243
+ .filter((id) => id.length > 0)));
244
+ if (normalizedIds.length === 0) {
245
+ return null;
246
+ }
247
+ const placeholders = normalizedIds.map(() => '?').join(', ');
248
+ return {
249
+ sql: `
250
+ EXISTS (
251
+ SELECT 1
252
+ FROM json_each(
253
+ CASE
254
+ WHEN json_valid(COALESCE(${noteAlias}.zettel_boxes, '')) THEN ${noteAlias}.zettel_boxes
255
+ ELSE '[]'
256
+ END
257
+ ) box_item
258
+ WHERE TRIM(CAST(box_item.value AS TEXT)) IN (${placeholders})
259
+ )
260
+ `,
261
+ params: normalizedIds,
262
+ };
263
+ }
217
264
  function parseTagExpression(expression) {
218
265
  const tokens = tokenizeTagExpression(expression);
219
266
  if (tokens.length === 0) {
@@ -537,6 +584,13 @@ export async function createNote(db, input) {
537
584
  function normalizeLookupKey(value) {
538
585
  return value.trim().toLowerCase();
539
586
  }
587
+ function buildZettelBoxRetryQuestion(intent, type) {
588
+ const command = intent === 'search' ? 'dino note search' : 'dino note create';
589
+ if (type === 'ambiguous') {
590
+ return `Do you want to specify unique box names (or IDs) and retry \`${command}\`?`;
591
+ }
592
+ return `Do you want to add these missing boxes and retry \`${command}\`?`;
593
+ }
540
594
  export async function validateTagsForCreate(db, requestedTags) {
541
595
  if (requestedTags.length === 0) {
542
596
  return [];
@@ -580,6 +634,12 @@ export async function validateTagsForCreate(db, requestedTags) {
580
634
  return resolved;
581
635
  }
582
636
  export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
637
+ return resolveZettelBoxIdsByName(db, requestedNames, 'create');
638
+ }
639
+ export async function resolveZettelBoxIdsForSearch(db, requestedNames) {
640
+ return resolveZettelBoxIdsByName(db, requestedNames, 'search');
641
+ }
642
+ async function resolveZettelBoxIdsByName(db, requestedNames, intent) {
583
643
  if (requestedNames.length === 0) {
584
644
  return [];
585
645
  }
@@ -642,7 +702,7 @@ export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
642
702
  type: 'ambiguous_zettel_boxes',
643
703
  ambiguous,
644
704
  availableNamesSample: availableNames.slice(0, 20),
645
- question: 'Do you want to specify unique box names (or IDs) and retry `dino note create`?',
705
+ question: buildZettelBoxRetryQuestion(intent, 'ambiguous'),
646
706
  },
647
707
  });
648
708
  }
@@ -653,7 +713,7 @@ export async function resolveZettelBoxIdsForCreate(db, requestedNames) {
653
713
  missing,
654
714
  availableCount: availableNames.length,
655
715
  availableNamesSample: availableNames.slice(0, 20),
656
- question: 'Do you want to add these missing boxes and retry `dino note create`?',
716
+ question: buildZettelBoxRetryQuestion(intent, 'missing'),
657
717
  },
658
718
  });
659
719
  }
@@ -0,0 +1,11 @@
1
+ export type SqlFilter = {
2
+ sql: string;
3
+ params: string[];
4
+ };
5
+ type BuildNoteSearchSqlFilterOptions = {
6
+ expression?: string;
7
+ noteAlias: string;
8
+ resolveZettelBoxNames: (names: string[]) => Promise<string[]>;
9
+ };
10
+ export declare function buildNoteSearchSqlFilter(options: BuildNoteSearchSqlFilterOptions): Promise<SqlFilter | null>;
11
+ export {};
@@ -0,0 +1,599 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ const SEARCH_FIELDS = new Set([
3
+ 'id',
4
+ 'content_md',
5
+ 'summary',
6
+ 'tags',
7
+ 'zettel_boxes',
8
+ 'created_at',
9
+ 'type',
10
+ ]);
11
+ const FORBIDDEN_SQL_KEYWORDS = [
12
+ 'insert',
13
+ 'update',
14
+ 'delete',
15
+ 'drop',
16
+ 'alter',
17
+ 'create',
18
+ 'replace',
19
+ 'truncate',
20
+ 'attach',
21
+ 'detach',
22
+ 'pragma',
23
+ 'vacuum',
24
+ 'begin',
25
+ 'commit',
26
+ 'rollback',
27
+ ];
28
+ const SCALAR_FIELD_COLUMNS = {
29
+ id: 'id',
30
+ content_md: 'content_md',
31
+ summary: 'summary',
32
+ created_at: 'created_at',
33
+ type: 'type',
34
+ };
35
+ export async function buildNoteSearchSqlFilter(options) {
36
+ const expression = options.expression?.trim() ?? '';
37
+ if (!expression) {
38
+ return null;
39
+ }
40
+ try {
41
+ assertReadOnlySearchExpression(expression);
42
+ const ast = parseSearchSqlExpression(expression);
43
+ const zettelBoxIdByName = await buildZettelBoxIdLookup(ast, options.resolveZettelBoxNames);
44
+ const params = [];
45
+ const sql = compileNode(ast, options.noteAlias, params, zettelBoxIdByName);
46
+ return {
47
+ sql: `(${sql})`,
48
+ params,
49
+ };
50
+ }
51
+ catch (error) {
52
+ if (error instanceof DinoxError) {
53
+ throw error;
54
+ }
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new DinoxError(`Invalid --sql expression: ${message}`);
57
+ }
58
+ }
59
+ function assertReadOnlySearchExpression(expression) {
60
+ const unquoted = maskQuotedSegments(expression).toLowerCase();
61
+ if (unquoted.includes(';')) {
62
+ throw new DinoxError('Invalid --sql expression: only read-only WHERE-style conditions are allowed (semicolon is not permitted)');
63
+ }
64
+ if (unquoted.includes('--') || unquoted.includes('/*') || unquoted.includes('*/')) {
65
+ throw new DinoxError('Invalid --sql expression: SQL comments are not allowed; only read-only WHERE-style conditions are supported');
66
+ }
67
+ for (const keyword of FORBIDDEN_SQL_KEYWORDS) {
68
+ const pattern = new RegExp(`\\b${keyword}\\b`, 'i');
69
+ if (pattern.test(unquoted)) {
70
+ throw new DinoxError(`Invalid --sql expression: keyword '${keyword.toUpperCase()}' is not allowed (read-only search only)`);
71
+ }
72
+ }
73
+ }
74
+ function maskQuotedSegments(expression) {
75
+ let result = '';
76
+ let quote = null;
77
+ for (let i = 0; i < expression.length; i += 1) {
78
+ const ch = expression[i];
79
+ if (!quote) {
80
+ if (ch === '"' || ch === "'") {
81
+ quote = ch;
82
+ result += ' ';
83
+ }
84
+ else {
85
+ result += ch;
86
+ }
87
+ continue;
88
+ }
89
+ result += ' ';
90
+ if (ch === '\\') {
91
+ if (i + 1 < expression.length) {
92
+ result += ' ';
93
+ i += 1;
94
+ }
95
+ continue;
96
+ }
97
+ if (ch !== quote) {
98
+ continue;
99
+ }
100
+ if (expression[i + 1] === quote) {
101
+ result += ' ';
102
+ i += 1;
103
+ continue;
104
+ }
105
+ quote = null;
106
+ }
107
+ return result;
108
+ }
109
+ function parseSearchSqlExpression(expression) {
110
+ const tokens = tokenizeSearchSqlExpression(expression);
111
+ if (tokens.length === 0) {
112
+ throw new Error('Expression is empty');
113
+ }
114
+ let index = 0;
115
+ const peek = () => tokens[index] ?? null;
116
+ const consume = (expected) => {
117
+ const token = peek();
118
+ if (!token) {
119
+ throw new Error('Unexpected end of expression');
120
+ }
121
+ if (expected && token.type !== expected) {
122
+ throw new Error(`Expected ${expected} at position ${token.position + 1}`);
123
+ }
124
+ index += 1;
125
+ return token;
126
+ };
127
+ const match = (type) => {
128
+ const token = peek();
129
+ if (!token || token.type !== type) {
130
+ return false;
131
+ }
132
+ index += 1;
133
+ return true;
134
+ };
135
+ const parseValue = () => {
136
+ const token = peek();
137
+ if (!token) {
138
+ throw new Error('Unexpected end of expression while reading a value');
139
+ }
140
+ if (token.type !== 'WORD' && token.type !== 'STRING') {
141
+ throw new Error(`Expected value at position ${token.position + 1}`);
142
+ }
143
+ consume(token.type);
144
+ const value = token.value.trim();
145
+ if (!value) {
146
+ throw new Error(`Empty value at position ${token.position + 1}`);
147
+ }
148
+ return value;
149
+ };
150
+ const parseValueList = () => {
151
+ const open = consume('LPAREN');
152
+ const values = [];
153
+ while (true) {
154
+ values.push(parseValue());
155
+ if (match('COMMA')) {
156
+ continue;
157
+ }
158
+ if (match('RPAREN')) {
159
+ break;
160
+ }
161
+ const next = peek();
162
+ const nextPos = next ? next.position + 1 : open.position + 1;
163
+ throw new Error(`Expected ',' or ')' at position ${nextPos}`);
164
+ }
165
+ if (values.length === 0) {
166
+ throw new Error(`IN list cannot be empty at position ${open.position + 1}`);
167
+ }
168
+ return values;
169
+ };
170
+ const parseCondition = () => {
171
+ const fieldToken = peek();
172
+ if (!fieldToken) {
173
+ throw new Error('Unexpected end of expression');
174
+ }
175
+ if (fieldToken.type !== 'WORD') {
176
+ throw new Error(`Expected field name at position ${fieldToken.position + 1}`);
177
+ }
178
+ consume('WORD');
179
+ const field = normalizeSearchField(fieldToken.value);
180
+ if (!field) {
181
+ throw new Error(`Unsupported field '${fieldToken.value}' at position ${fieldToken.position + 1}`);
182
+ }
183
+ if (match('NOT')) {
184
+ if (match('IN')) {
185
+ return {
186
+ kind: 'in',
187
+ field,
188
+ not: true,
189
+ values: parseValueList(),
190
+ };
191
+ }
192
+ if (match('LIKE')) {
193
+ return {
194
+ kind: 'comparison',
195
+ field,
196
+ operator: 'NOT LIKE',
197
+ value: parseValue(),
198
+ };
199
+ }
200
+ const token = peek();
201
+ const pos = token ? token.position + 1 : fieldToken.position + 1;
202
+ throw new Error(`Expected IN or LIKE after NOT at position ${pos}`);
203
+ }
204
+ if (match('IN')) {
205
+ return {
206
+ kind: 'in',
207
+ field,
208
+ not: false,
209
+ values: parseValueList(),
210
+ };
211
+ }
212
+ if (match('LIKE')) {
213
+ return {
214
+ kind: 'comparison',
215
+ field,
216
+ operator: 'LIKE',
217
+ value: parseValue(),
218
+ };
219
+ }
220
+ const operatorToken = peek();
221
+ if (!operatorToken || operatorToken.type !== 'OP') {
222
+ const pos = operatorToken ? operatorToken.position + 1 : fieldToken.position + 1;
223
+ throw new Error(`Expected comparison operator at position ${pos}`);
224
+ }
225
+ consume('OP');
226
+ return {
227
+ kind: 'comparison',
228
+ field,
229
+ operator: normalizeComparisonOperator(operatorToken.value, operatorToken.position),
230
+ value: parseValue(),
231
+ };
232
+ };
233
+ const parsePrimary = () => {
234
+ const token = peek();
235
+ if (!token) {
236
+ throw new Error('Unexpected end of expression');
237
+ }
238
+ if (token.type === 'LPAREN') {
239
+ consume('LPAREN');
240
+ const node = parseOr();
241
+ const close = peek();
242
+ if (!close || close.type !== 'RPAREN') {
243
+ throw new Error(`Missing ')' for '(' at position ${token.position + 1}`);
244
+ }
245
+ consume('RPAREN');
246
+ return node;
247
+ }
248
+ return parseCondition();
249
+ };
250
+ const parseUnary = () => {
251
+ if (match('NOT')) {
252
+ return { kind: 'not', child: parseUnary() };
253
+ }
254
+ return parsePrimary();
255
+ };
256
+ const parseAnd = () => {
257
+ let node = parseUnary();
258
+ while (match('AND')) {
259
+ node = { kind: 'and', left: node, right: parseUnary() };
260
+ }
261
+ return node;
262
+ };
263
+ const parseOr = () => {
264
+ let node = parseAnd();
265
+ while (match('OR')) {
266
+ node = { kind: 'or', left: node, right: parseAnd() };
267
+ }
268
+ return node;
269
+ };
270
+ const ast = parseOr();
271
+ const extra = peek();
272
+ if (extra) {
273
+ throw new Error(`Unexpected token '${extra.value}' at position ${extra.position + 1}`);
274
+ }
275
+ return ast;
276
+ }
277
+ function tokenizeSearchSqlExpression(expression) {
278
+ const tokens = [];
279
+ let i = 0;
280
+ while (i < expression.length) {
281
+ const ch = expression[i];
282
+ if (/\s/.test(ch)) {
283
+ i += 1;
284
+ continue;
285
+ }
286
+ if (ch === '(') {
287
+ tokens.push({ type: 'LPAREN', value: ch, position: i });
288
+ i += 1;
289
+ continue;
290
+ }
291
+ if (ch === ')') {
292
+ tokens.push({ type: 'RPAREN', value: ch, position: i });
293
+ i += 1;
294
+ continue;
295
+ }
296
+ if (ch === ',') {
297
+ tokens.push({ type: 'COMMA', value: ch, position: i });
298
+ i += 1;
299
+ continue;
300
+ }
301
+ if (ch === '\'' || ch === '"') {
302
+ const quoted = readQuotedValue(expression, i);
303
+ tokens.push({ type: 'STRING', value: quoted.value, position: i });
304
+ i = quoted.next;
305
+ continue;
306
+ }
307
+ const op = readOperator(expression, i);
308
+ if (op) {
309
+ tokens.push({ type: 'OP', value: op.value, position: i });
310
+ i = op.next;
311
+ continue;
312
+ }
313
+ let j = i;
314
+ while (j < expression.length) {
315
+ const next = expression[j];
316
+ if (/\s/.test(next) || next === '(' || next === ')' || next === ',' || /[<>=!]/.test(next)) {
317
+ break;
318
+ }
319
+ j += 1;
320
+ }
321
+ const raw = expression.slice(i, j);
322
+ if (!raw) {
323
+ throw new Error(`Unsupported token '${ch}' at position ${i + 1}`);
324
+ }
325
+ const upper = raw.toUpperCase();
326
+ if (upper === 'AND' || upper === 'OR' || upper === 'NOT' || upper === 'IN' || upper === 'LIKE') {
327
+ tokens.push({ type: upper, value: raw, position: i });
328
+ }
329
+ else {
330
+ tokens.push({ type: 'WORD', value: raw, position: i });
331
+ }
332
+ i = j;
333
+ }
334
+ return tokens;
335
+ }
336
+ function readOperator(expression, start) {
337
+ const twoChars = expression.slice(start, start + 2);
338
+ if (twoChars === '<=' || twoChars === '>=' || twoChars === '<>' || twoChars === '!=') {
339
+ return {
340
+ value: twoChars,
341
+ next: start + 2,
342
+ };
343
+ }
344
+ const oneChar = expression[start];
345
+ if (oneChar === '=' || oneChar === '<' || oneChar === '>') {
346
+ return {
347
+ value: oneChar,
348
+ next: start + 1,
349
+ };
350
+ }
351
+ return null;
352
+ }
353
+ function readQuotedValue(expression, start) {
354
+ const quote = expression[start];
355
+ let i = start + 1;
356
+ let value = '';
357
+ while (i < expression.length) {
358
+ const ch = expression[i];
359
+ if (ch === '\\') {
360
+ if (i + 1 >= expression.length) {
361
+ throw new Error(`Invalid escape at position ${i + 1}`);
362
+ }
363
+ value += expression[i + 1];
364
+ i += 2;
365
+ continue;
366
+ }
367
+ if (ch === quote) {
368
+ if (expression[i + 1] === quote) {
369
+ value += quote;
370
+ i += 2;
371
+ continue;
372
+ }
373
+ return {
374
+ value,
375
+ next: i + 1,
376
+ };
377
+ }
378
+ value += ch;
379
+ i += 1;
380
+ }
381
+ throw new Error(`Unclosed quote at position ${start + 1}`);
382
+ }
383
+ function normalizeSearchField(value) {
384
+ const normalized = value.trim().toLowerCase();
385
+ if (SEARCH_FIELDS.has(normalized)) {
386
+ return normalized;
387
+ }
388
+ return null;
389
+ }
390
+ function normalizeComparisonOperator(raw, position) {
391
+ if (raw === '=') {
392
+ return '=';
393
+ }
394
+ if (raw === '!=' || raw === '<>') {
395
+ return '!=';
396
+ }
397
+ if (raw === '>') {
398
+ return '>';
399
+ }
400
+ if (raw === '>=') {
401
+ return '>=';
402
+ }
403
+ if (raw === '<') {
404
+ return '<';
405
+ }
406
+ if (raw === '<=') {
407
+ return '<=';
408
+ }
409
+ throw new Error(`Unsupported operator '${raw}' at position ${position + 1}`);
410
+ }
411
+ function normalizeLookupKey(value) {
412
+ return value.trim().toLowerCase();
413
+ }
414
+ async function buildZettelBoxIdLookup(ast, resolveZettelBoxNames) {
415
+ const rawValues = collectZettelBoxValues(ast)
416
+ .map((value) => value.trim())
417
+ .filter((value) => value.length > 0);
418
+ if (rawValues.length === 0) {
419
+ return new Map();
420
+ }
421
+ const uniqueNames = [];
422
+ const seen = new Set();
423
+ for (const raw of rawValues) {
424
+ const key = normalizeLookupKey(raw);
425
+ if (seen.has(key)) {
426
+ continue;
427
+ }
428
+ seen.add(key);
429
+ uniqueNames.push(raw);
430
+ }
431
+ const ids = await resolveZettelBoxNames(uniqueNames);
432
+ if (ids.length !== uniqueNames.length) {
433
+ throw new DinoxError('Failed to resolve zettel box names in --sql expression');
434
+ }
435
+ const lookup = new Map();
436
+ for (let i = 0; i < uniqueNames.length; i += 1) {
437
+ lookup.set(normalizeLookupKey(uniqueNames[i]), ids[i]);
438
+ }
439
+ return lookup;
440
+ }
441
+ function collectZettelBoxValues(node) {
442
+ if (node.kind === 'comparison') {
443
+ if (node.field === 'zettel_boxes') {
444
+ return [node.value];
445
+ }
446
+ return [];
447
+ }
448
+ if (node.kind === 'in') {
449
+ if (node.field === 'zettel_boxes') {
450
+ return node.values;
451
+ }
452
+ return [];
453
+ }
454
+ if (node.kind === 'not') {
455
+ return collectZettelBoxValues(node.child);
456
+ }
457
+ return [...collectZettelBoxValues(node.left), ...collectZettelBoxValues(node.right)];
458
+ }
459
+ function compileNode(node, noteAlias, params, zettelBoxIdByName) {
460
+ if (node.kind === 'comparison') {
461
+ return compileComparison(node, noteAlias, params, zettelBoxIdByName);
462
+ }
463
+ if (node.kind === 'in') {
464
+ return compileInCondition(node, noteAlias, params, zettelBoxIdByName);
465
+ }
466
+ if (node.kind === 'not') {
467
+ return `NOT (${compileNode(node.child, noteAlias, params, zettelBoxIdByName)})`;
468
+ }
469
+ if (node.kind === 'and') {
470
+ return `(${compileNode(node.left, noteAlias, params, zettelBoxIdByName)}) AND (${compileNode(node.right, noteAlias, params, zettelBoxIdByName)})`;
471
+ }
472
+ return `(${compileNode(node.left, noteAlias, params, zettelBoxIdByName)}) OR (${compileNode(node.right, noteAlias, params, zettelBoxIdByName)})`;
473
+ }
474
+ function compileComparison(node, noteAlias, params, zettelBoxIdByName) {
475
+ if (node.field === 'tags') {
476
+ if (node.operator !== '=' && node.operator !== '!=') {
477
+ throw new Error(`Operator '${node.operator}' is not supported for field 'tags'`);
478
+ }
479
+ const value = normalizeLookupKey(node.value);
480
+ if (!value) {
481
+ throw new Error('tags condition value cannot be empty');
482
+ }
483
+ params.push(value);
484
+ const existsSql = `
485
+ EXISTS (
486
+ SELECT 1
487
+ FROM json_each(
488
+ CASE
489
+ WHEN json_valid(COALESCE(${noteAlias}.tags, '')) THEN ${noteAlias}.tags
490
+ ELSE '[]'
491
+ END
492
+ ) tag_item
493
+ WHERE LOWER(TRIM(CAST(tag_item.value AS TEXT))) = ?
494
+ )
495
+ `;
496
+ return node.operator === '!=' ? `NOT (${existsSql})` : existsSql;
497
+ }
498
+ if (node.field === 'zettel_boxes') {
499
+ if (node.operator !== '=' && node.operator !== '!=') {
500
+ throw new Error(`Operator '${node.operator}' is not supported for field 'zettel_boxes'`);
501
+ }
502
+ const resolvedId = zettelBoxIdByName.get(normalizeLookupKey(node.value));
503
+ if (!resolvedId) {
504
+ throw new DinoxError(`Unknown zettel box name in --sql expression: ${node.value}`);
505
+ }
506
+ params.push(resolvedId);
507
+ const existsSql = `
508
+ EXISTS (
509
+ SELECT 1
510
+ FROM json_each(
511
+ CASE
512
+ WHEN json_valid(COALESCE(${noteAlias}.zettel_boxes, '')) THEN ${noteAlias}.zettel_boxes
513
+ ELSE '[]'
514
+ END
515
+ ) box_item
516
+ WHERE TRIM(CAST(box_item.value AS TEXT)) = ?
517
+ )
518
+ `;
519
+ return node.operator === '!=' ? `NOT (${existsSql})` : existsSql;
520
+ }
521
+ const column = `${noteAlias}.${SCALAR_FIELD_COLUMNS[node.field]}`;
522
+ params.push(node.value);
523
+ if (node.operator === 'LIKE' || node.operator === 'NOT LIKE') {
524
+ return `(${column} IS NOT NULL AND ${column} ${node.operator} ?)`;
525
+ }
526
+ return `(${column} ${node.operator} ?)`;
527
+ }
528
+ function compileInCondition(node, noteAlias, params, zettelBoxIdByName) {
529
+ if (node.values.length === 0) {
530
+ throw new Error('IN list cannot be empty');
531
+ }
532
+ if (node.field === 'tags') {
533
+ const normalized = uniqueNonEmptyValues(node.values.map((value) => normalizeLookupKey(value)));
534
+ if (normalized.length === 0) {
535
+ throw new Error('tags IN list cannot be empty');
536
+ }
537
+ const placeholders = normalized.map(() => '?').join(', ');
538
+ params.push(...normalized);
539
+ const existsSql = `
540
+ EXISTS (
541
+ SELECT 1
542
+ FROM json_each(
543
+ CASE
544
+ WHEN json_valid(COALESCE(${noteAlias}.tags, '')) THEN ${noteAlias}.tags
545
+ ELSE '[]'
546
+ END
547
+ ) tag_item
548
+ WHERE LOWER(TRIM(CAST(tag_item.value AS TEXT))) IN (${placeholders})
549
+ )
550
+ `;
551
+ return node.not ? `NOT (${existsSql})` : existsSql;
552
+ }
553
+ if (node.field === 'zettel_boxes') {
554
+ const ids = uniqueNonEmptyValues(node.values
555
+ .map((value) => zettelBoxIdByName.get(normalizeLookupKey(value)) ?? '')
556
+ .filter((value) => value.length > 0));
557
+ if (ids.length === 0) {
558
+ throw new Error('zettel_boxes IN list cannot be empty');
559
+ }
560
+ const placeholders = ids.map(() => '?').join(', ');
561
+ params.push(...ids);
562
+ const existsSql = `
563
+ EXISTS (
564
+ SELECT 1
565
+ FROM json_each(
566
+ CASE
567
+ WHEN json_valid(COALESCE(${noteAlias}.zettel_boxes, '')) THEN ${noteAlias}.zettel_boxes
568
+ ELSE '[]'
569
+ END
570
+ ) box_item
571
+ WHERE TRIM(CAST(box_item.value AS TEXT)) IN (${placeholders})
572
+ )
573
+ `;
574
+ return node.not ? `NOT (${existsSql})` : existsSql;
575
+ }
576
+ const values = uniqueNonEmptyValues(node.values.map((value) => value.trim()));
577
+ if (values.length === 0) {
578
+ throw new Error('IN list cannot be empty');
579
+ }
580
+ const column = `${noteAlias}.${SCALAR_FIELD_COLUMNS[node.field]}`;
581
+ const placeholders = values.map(() => '?').join(', ');
582
+ params.push(...values);
583
+ if (node.not) {
584
+ return `(${column} NOT IN (${placeholders}))`;
585
+ }
586
+ return `(${column} IN (${placeholders}))`;
587
+ }
588
+ function uniqueNonEmptyValues(values) {
589
+ const result = [];
590
+ const seen = new Set();
591
+ for (const value of values) {
592
+ if (!value || seen.has(value)) {
593
+ continue;
594
+ }
595
+ seen.add(value);
596
+ result.push(value);
597
+ }
598
+ return result;
599
+ }
@@ -1,6 +1,6 @@
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"];
1
+ export declare const CONFIG_KEYS: readonly ["sync.timeoutMs"];
2
+ export declare const FIXED_CONFIG_KEYS: readonly [];
3
+ export declare const READABLE_CONFIG_KEYS: readonly ["sync.timeoutMs"];
4
4
  export type ConfigKey = (typeof CONFIG_KEYS)[number];
5
5
  export type FixedConfigKey = (typeof FIXED_CONFIG_KEYS)[number];
6
6
  export type ReadableConfigKey = (typeof READABLE_CONFIG_KEYS)[number];
@@ -1,10 +1,7 @@
1
1
  export const CONFIG_KEYS = [
2
- 'powersync.tokenEndpoint',
3
- 'powersync.uploadV4Path',
4
- 'powersync.uploadV2Path',
5
2
  'sync.timeoutMs',
6
3
  ];
7
- export const FIXED_CONFIG_KEYS = ['powersync.endpoint', 'powersync.uploadBaseUrl'];
4
+ export const FIXED_CONFIG_KEYS = [];
8
5
  export const READABLE_CONFIG_KEYS = [...CONFIG_KEYS, ...FIXED_CONFIG_KEYS];
9
6
  export function isConfigKey(value) {
10
7
  return CONFIG_KEYS.includes(value);
@@ -1,5 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
3
4
  function resolveConfigHome() {
4
5
  if (process.env.DINOX_CONFIG_DIR) {
5
6
  return process.env.DINOX_CONFIG_DIR;
@@ -35,5 +36,5 @@ export function getDataDir() {
35
36
  return path.join(resolveDataHome(), 'dinox');
36
37
  }
37
38
  export function getPowerSyncDbPath() {
38
- return path.join(getDataDir(), 'powersync.sqlite');
39
+ return path.join(getDataDir(), POWERSYNC_DB_FILENAME);
39
40
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {
7
7
  "build": "tsc -p tsconfig.json && node scripts/add-shebang.mjs",
8
8
  "dev": "tsx src/dinox.ts",
9
9
  "prepublishOnly": "npm run build",
10
+ "publish:npm": "sh scripts/publish-npm.sh",
11
+ "publish:npm:dry-run": "sh scripts/publish-npm.sh --dry-run",
10
12
  "start": "node dist/dinox.js",
11
13
  "test": "node --test --import tsx test/**/*.test.ts"
12
14
  },