@dinoxx/dinox-cli 1.0.19 → 1.0.21

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 (88) hide show
  1. package/README.md +756 -472
  2. package/dist/auth/userInfo.d.ts +10 -1
  3. package/dist/auth/userInfo.js +66 -17
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +29 -14
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +45 -16
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/storage/repo.d.ts +1 -1
  55. package/dist/commands/storage/repo.js +20 -1
  56. package/dist/commands/sync.js +4 -2
  57. package/dist/commands/tags/content.d.ts +32 -0
  58. package/dist/commands/tags/content.js +243 -0
  59. package/dist/commands/tags/index.js +285 -49
  60. package/dist/commands/tags/model.d.ts +38 -0
  61. package/dist/commands/tags/model.js +100 -0
  62. package/dist/commands/tags/operations.d.ts +105 -0
  63. package/dist/commands/tags/operations.js +438 -0
  64. package/dist/commands/tags/repo.d.ts +5 -1
  65. package/dist/commands/tags/repo.js +142 -218
  66. package/dist/commands/tags/store.d.ts +28 -0
  67. package/dist/commands/tags/store.js +166 -0
  68. package/dist/commands/todo/index.js +30 -61
  69. package/dist/commands/writeDryRun.d.ts +25 -0
  70. package/dist/commands/writeDryRun.js +31 -1
  71. package/dist/config/paths.d.ts +2 -1
  72. package/dist/config/paths.js +11 -1
  73. package/dist/config/resolve.js +9 -7
  74. package/dist/daemon/index.d.ts +2 -0
  75. package/dist/daemon/index.js +176 -58
  76. package/dist/powersync/runtime.d.ts +2 -1
  77. package/dist/powersync/runtime.js +15 -3
  78. package/dist/powersync/schema/content.d.ts +2 -0
  79. package/dist/powersync/schema/content.js +2 -0
  80. package/dist/powersync/schema/index.d.ts +2 -0
  81. package/dist/skills/check.d.ts +1 -1
  82. package/dist/skills/check.js +27 -2
  83. package/dist/utils/authToken.d.ts +2 -0
  84. package/dist/utils/authToken.js +7 -2
  85. package/dist/utils/noteContentEngine.d.ts +333 -0
  86. package/dist/utils/noteContentEngine.js +740 -0
  87. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  88. package/package.json +4 -8
@@ -0,0 +1,76 @@
1
+ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
2
+ import { loadConfig } from '../config/store.js';
3
+ import { resolveConfig } from '../config/resolve.js';
4
+ import { connectPowerSync, waitForIdleDataFlow } from '../powersync/runtime.js';
5
+ import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
6
+ import { printHumanReadableLine } from '../utils/output.js';
7
+ import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
8
+ const PRE_SYNC_TIMEOUT_MS = 10_000;
9
+ const POST_WRITE_IDLE_TIMEOUT_MS = 5_000;
10
+ function isCommandExecutionContext(source) {
11
+ return typeof source === 'object' && source !== null && 'globals' in source;
12
+ }
13
+ function isCommanderCommand(source) {
14
+ return typeof source.optsWithGlobals === 'function';
15
+ }
16
+ function resolveGlobals(source) {
17
+ if (isCommandExecutionContext(source)) {
18
+ return source.globals;
19
+ }
20
+ if (isCommanderCommand(source)) {
21
+ return (source.optsWithGlobals?.() ?? {});
22
+ }
23
+ return source;
24
+ }
25
+ export function commandStale(runtime) {
26
+ return runtime.offline ? true : runtime.stale;
27
+ }
28
+ export async function withCommandRuntime(source, run) {
29
+ const globals = resolveGlobals(source);
30
+ const rawConfig = await loadConfig();
31
+ const baseConfig = resolveConfig(rawConfig);
32
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
33
+ const offline = Boolean(globals.offline);
34
+ const { config } = offline
35
+ ? { config: baseConfig }
36
+ : await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs });
37
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
38
+ try {
39
+ return await run({
40
+ db,
41
+ config,
42
+ rawConfig,
43
+ offline,
44
+ timeoutMs,
45
+ stale,
46
+ });
47
+ }
48
+ finally {
49
+ await db.close().catch(() => undefined);
50
+ }
51
+ }
52
+ export async function syncBeforeCommand(runtime, options = {}) {
53
+ if (runtime.offline) {
54
+ return;
55
+ }
56
+ await waitForIdleDataFlow(runtime.db, Math.min(runtime.timeoutMs, options.timeoutMs ?? PRE_SYNC_TIMEOUT_MS));
57
+ if (options.syncTokenIndex) {
58
+ await syncNoteTokenIndex(runtime.db);
59
+ }
60
+ }
61
+ export async function waitForMutationUpload(runtime, options = {}) {
62
+ if (runtime.offline) {
63
+ return;
64
+ }
65
+ await waitForIdleDataFlow(runtime.db, Math.min(runtime.timeoutMs, options.timeoutMs ?? POST_WRITE_IDLE_TIMEOUT_MS));
66
+ }
67
+ export function printUploadDisabledWarning(runtime, message = 'warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now') {
68
+ if (!runtime.offline && !runtime.config.powersync.uploadBaseUrl) {
69
+ printHumanReadableLine(message);
70
+ }
71
+ }
72
+ export function printStaleCacheWarning(runtime, message = 'warning: sync timed out; local cache may be stale') {
73
+ if (runtime.stale && !runtime.offline) {
74
+ printHumanReadableLine(message);
75
+ }
76
+ }
@@ -3,13 +3,14 @@ import path from 'node:path';
3
3
  import { listCommandSchemas } from './spec.js';
4
4
  const SECTION_ORDER = [
5
5
  { title: 'Auth', paths: ['auth.login', 'auth.logout', 'auth.status'] },
6
+ { title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop'] },
6
7
  { title: 'Sync', paths: ['sync'] },
7
8
  { title: 'Schema', paths: ['schema'] },
8
9
  { title: 'Update CLI', paths: ['update'] },
9
- { title: 'Notes', paths: ['note.search', 'note.get', 'note.preview', 'note.detail', 'note.create', 'note.update', 'note.star', 'note.unstar', 'note.delete'] },
10
+ { title: 'Notes', paths: ['note.search', 'note.get', 'note.preview', 'note.detail', 'note.export', 'note.content-read', 'note.create', 'note.update', 'note.tag', 'note.move', 'note.patch', 'note.bulk', 'note.star', 'note.unstar', 'note.delete'] },
10
11
  { title: 'Todo', paths: ['todo.search', 'todo.append', 'todo.create', 'todo.update'] },
11
- { title: 'Tags', paths: ['tag.list', 'tag.add'] },
12
- { title: 'Card Boxes (Zettel Boxes)', paths: ['box.list', 'box.add'] },
12
+ { title: 'Tags', paths: ['tag.list', 'tag.tree', 'tag.stats', 'tag.add', 'tag.rename', 'tag.move', 'tag.merge', 'tag.suggest', 'tag.cleanup'] },
13
+ { title: 'Card Boxes (Zettel Boxes)', paths: ['box.list', 'box.add', 'box.tree', 'box.stats', 'box.rename', 'box.move', 'box.merge', 'box.cleanup'] },
13
14
  { title: 'Prompts', paths: ['prompt.list', 'prompt.add'] },
14
15
  { title: 'Storage', paths: ['storage.list', 'storage.test', 'storage.upload', 'storage.stats'] },
15
16
  { title: 'Config', paths: ['config.get', 'config.set'] },
@@ -52,18 +53,23 @@ const GENERATED_COMMAND_SECTIONS = {
52
53
  },
53
54
  'skill:dino-manage-tags': {
54
55
  title: 'Command Reference',
55
- paths: ['tag.list', 'tag.add'],
56
+ paths: ['tag.list', 'tag.tree', 'tag.stats', 'tag.add', 'tag.rename', 'tag.move', 'tag.merge', 'tag.suggest', 'tag.cleanup'],
56
57
  intro: 'Use these commands as the canonical Dinox CLI interface for tag management.',
57
58
  notes: [
58
59
  'For writes, run the same command with `--dry-run` first.',
60
+ '`tag rename` and `tag move` cascade descendant paths and note tag references inside one transaction.',
61
+ '`tag merge` remaps note references into the target tag and soft-deletes the source subtree.',
62
+ 'Inline `hashtag` nodes keep `attrs.id` as `c_tag_node.id`; `attrs.label` stores the canonical path.',
59
63
  ],
60
64
  },
61
65
  'skill:dino-manage-boxes': {
62
66
  title: 'Command Reference',
63
- paths: ['box.list', 'box.add'],
67
+ paths: ['box.list', 'box.add', 'box.tree', 'box.stats', 'box.rename', 'box.move', 'box.merge', 'box.cleanup'],
64
68
  intro: 'Use these commands as the canonical Dinox CLI interface for box management.',
65
69
  notes: [
66
70
  'For writes, run the same command with `--dry-run` first.',
71
+ '`box rename` and `box move` cascade descendant paths inside one transaction.',
72
+ '`box merge` remaps note references into the target box and soft-deletes the source subtree.',
67
73
  ],
68
74
  },
69
75
  'skill:dino-storage': {
@@ -77,11 +83,13 @@ const GENERATED_COMMAND_SECTIONS = {
77
83
  },
78
84
  'ref:dino-note:search-and-read': {
79
85
  title: 'Command Surface',
80
- paths: ['note.search', 'note.get', 'note.preview', 'note.detail'],
86
+ paths: ['note.search', 'note.get', 'note.preview', 'note.detail', 'note.export', 'note.content-read'],
81
87
  intro: 'Use these generated commands as the canonical interfaces for note search and read workflows.',
82
88
  notes: [
83
89
  'Use `--boxes` for public box filters.',
84
90
  'Prefer `--fields id,title,summary,tags,created_at,boxes,is_starred` when the user only needs search metadata.',
91
+ 'Use `note export` for backup or migration; Markdown exports include frontmatter, JSON exports preserve note metadata and content JSON.',
92
+ 'Use `note content-read` immediately before `note patch`; the returned readToken, content hash, block index, outline, and resolved hashtag paths are the required edit context.',
85
93
  '`--sql` remains storage-oriented and still uses the field name `zettel_boxes`.',
86
94
  ],
87
95
  },
@@ -97,10 +105,15 @@ const GENERATED_COMMAND_SECTIONS = {
97
105
  },
98
106
  'ref:dino-note:update-and-delete': {
99
107
  title: 'Command Surface',
100
- paths: ['note.update', 'note.star', 'note.unstar', 'note.delete'],
108
+ paths: ['note.update', 'note.tag', 'note.move', 'note.patch', 'note.bulk', 'note.star', 'note.unstar', 'note.delete'],
101
109
  intro: 'Use these generated commands as the canonical interfaces for note mutation workflows.',
102
110
  notes: [
103
111
  '`--tags` and `--boxes` are full-replacement inputs.',
112
+ '`note update` is for explicit note ids and full metadata replacement. It is not an append command.',
113
+ '`note tag` and `note move` are explicit-id incremental organizers: add/remove preserves existing values, replace overwrites the whole list.',
114
+ '`note move` changes zettel box membership only; it does not move files or note content.',
115
+ '`note patch` is only for structured content edits. Run `note content-read` immediately before it, pass `--read-token` for real writes, and use `--allow-protected-replace` only after reviewing protected blocks.',
116
+ '`note bulk` is for filter-based batch metadata organization; it never accepts `--sql`, and real writes require `--confirm --expected-count <n>`.',
104
117
  'Prefer `note star` / `note unstar` for pure starring changes, and `note update` when multiple fields change together.',
105
118
  'Run the same command with `--dry-run --format json` first.',
106
119
  ],
@@ -1,6 +1,5 @@
1
1
  import type { CommandSchema } from './types.js';
2
- export type { CommandSchema, CommandSchemaAuth, CommandSchemaMcp, CommandSchemaMcpProperty, CommandSchemaOutput, CommandSchemaRisk, } from './types.js';
2
+ export type { CommandSchema, CommandSchemaAuth, CommandSchemaOutput, CommandSchemaRisk, } from './types.js';
3
3
  export declare const COMMAND_SCHEMAS: CommandSchema[];
4
4
  export declare function listCommandSchemas(): CommandSchema[];
5
5
  export declare function findCommandSchema(path: string): CommandSchema | null;
6
- export declare function listMcpCommandSchemas(): CommandSchema[];
@@ -3,6 +3,7 @@ import { boxCommandGroup } from '../boxes/index.js';
3
3
  import { commandSchemasFromGroupDescriptor } from '../descriptor/register.js';
4
4
  import { standaloneCommandSchemaFromDescriptor } from '../descriptor/register.js';
5
5
  import { configCommandGroup } from '../config/index.js';
6
+ import { daemonCommandGroup } from '../../daemon/index.js';
6
7
  import { graphCommandGroup } from '../graph/index.js';
7
8
  import { infoCommandDescriptor } from '../info/index.js';
8
9
  import { noteCommandGroup } from '../notes/index.js';
@@ -16,6 +17,7 @@ import { updateCommandDescriptor } from '../update.js';
16
17
  export const COMMAND_SCHEMAS = [
17
18
  ...commandSchemasFromGroupDescriptor(authCommandGroup),
18
19
  ...commandSchemasFromGroupDescriptor(configCommandGroup),
20
+ ...commandSchemasFromGroupDescriptor(daemonCommandGroup),
19
21
  standaloneCommandSchemaFromDescriptor(syncCommandDescriptor),
20
22
  standaloneCommandSchemaFromDescriptor({ ...schemaCommandMetadata, action: async () => undefined }),
21
23
  standaloneCommandSchemaFromDescriptor(infoCommandDescriptor),
@@ -35,6 +37,3 @@ export function listCommandSchemas() {
35
37
  export function findCommandSchema(path) {
36
38
  return COMMAND_SCHEMA_MAP.get(path) ?? null;
37
39
  }
38
- export function listMcpCommandSchemas() {
39
- return COMMAND_SCHEMAS.filter((schema) => schema.mcp);
40
- }
@@ -20,18 +20,6 @@ export type CommandSchemaOutput = {
20
20
  fields?: string[];
21
21
  metaFields?: string[];
22
22
  };
23
- export type CommandSchemaMcpProperty = {
24
- name: string;
25
- type: 'string' | 'number' | 'boolean' | 'string[]';
26
- description: string;
27
- required?: boolean;
28
- enum?: string[];
29
- };
30
- export type CommandSchemaMcp = {
31
- toolName: string;
32
- description: string;
33
- properties: CommandSchemaMcpProperty[];
34
- };
35
23
  export type CommandSchema = {
36
24
  path: string;
37
25
  command: string;
@@ -42,6 +30,5 @@ export type CommandSchema = {
42
30
  arguments?: CommandSchemaArgument[];
43
31
  options?: CommandSchemaOption[];
44
32
  output: CommandSchemaOutput;
45
- mcp?: CommandSchemaMcp;
46
33
  examples: string[];
47
34
  };
@@ -1,42 +1,20 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
- import { loadConfig } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
4
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
5
2
  import { registerCommandGroup } from '../descriptor/register.js';
6
3
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
7
4
  import { buildStorageTestDryRunPreview, buildStorageUploadDryRunPreview } from '../writeDryRun.js';
8
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
9
6
  import { DinoxError } from '../../utils/errors.js';
10
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
11
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
8
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
12
9
  import { getStorageStats, inspectStorageTest, inspectStorageUpload, listStorageConfigs, parseStorageUploadCategory, testStorageUpload, uploadFileToStorage, } from './repo.js';
13
10
  const STORAGE_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
14
- async function syncBeforeStorageCommand(db, offline, timeoutMs) {
15
- if (offline) {
16
- return;
17
- }
18
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
19
- }
20
- async function withStorageDb(context, run) {
21
- const rawConfig = await loadConfig();
22
- const config = resolveConfig(rawConfig);
23
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
24
- const offline = Boolean(context.globals.offline);
25
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
26
- try {
27
- await run({ db, config, rawConfig, offline, timeoutMs, stale });
28
- }
29
- finally {
30
- await db.close().catch(() => undefined);
31
- }
32
- }
33
11
  async function runStorageList(context) {
34
- await withStorageDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
35
- const identity = await resolveAuthIdentity(rawConfig, config);
36
- await syncBeforeStorageCommand(db, offline, timeoutMs);
12
+ await withCommandRuntime(context, async (runtime) => {
13
+ const identity = await resolveAuthIdentity(runtime.config);
14
+ await syncBeforeCommand(runtime);
37
15
  printSuccess({
38
- stale: offline ? true : stale,
39
- ...(await listStorageConfigs(db, identity.userId)),
16
+ stale: commandStale(runtime),
17
+ ...(await listStorageConfigs(runtime.db, identity.userId)),
40
18
  });
41
19
  });
42
20
  }
@@ -48,9 +26,10 @@ async function runStorageUpload(context) {
48
26
  const options = context.options;
49
27
  const category = parseStorageUploadCategory(options.category ?? 'files');
50
28
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
51
- await withStorageDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
52
- const identity = await resolveAuthIdentity(rawConfig, config);
53
- await syncBeforeStorageCommand(db, offline, timeoutMs);
29
+ await withCommandRuntime(context, async (runtime) => {
30
+ const { db } = runtime;
31
+ const identity = await resolveAuthIdentity(runtime.config);
32
+ await syncBeforeCommand(runtime);
54
33
  const plan = await inspectStorageUpload(db, {
55
34
  userId: identity.userId,
56
35
  filePath,
@@ -60,8 +39,8 @@ async function runStorageUpload(context) {
60
39
  });
61
40
  if (options.dryRun) {
62
41
  printDryRunPreview(buildStorageUploadDryRunPreview({
63
- config,
64
- offline,
42
+ config: runtime.config,
43
+ offline: runtime.offline,
65
44
  filePath: plan.filePath,
66
45
  fileName: plan.fileName,
67
46
  sizeBytes: plan.sizeBytes,
@@ -80,15 +59,11 @@ async function runStorageUpload(context) {
80
59
  storageId: options.storageId,
81
60
  key: options.key,
82
61
  });
83
- if (!offline && !config.powersync.uploadBaseUrl) {
84
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); resource metadata is local-only for now');
85
- }
86
- if (!offline) {
87
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
88
- }
62
+ printUploadDisabledWarning(runtime, 'warning: upload disabled (powersync.uploadBaseUrl is unset); resource metadata is local-only for now');
63
+ await waitForMutationUpload(runtime);
89
64
  const payload = {
90
65
  ok: true,
91
- stale: offline ? true : stale,
66
+ stale: commandStale(runtime),
92
67
  ...result,
93
68
  };
94
69
  if (structuredOutput) {
@@ -104,9 +79,7 @@ async function runStorageUpload(context) {
104
79
  if (result.thumbnailUrl) {
105
80
  printHumanReadableLine(`- thumbnail: ${result.thumbnailUrl}`);
106
81
  }
107
- if (stale && !offline) {
108
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
109
- }
82
+ printStaleCacheWarning(runtime);
110
83
  });
111
84
  }
112
85
  function formatBytes(bytes) {
@@ -123,17 +96,18 @@ function formatBytes(bytes) {
123
96
  async function runStorageTest(context) {
124
97
  const options = context.options;
125
98
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
126
- await withStorageDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
127
- const identity = await resolveAuthIdentity(rawConfig, config);
128
- await syncBeforeStorageCommand(db, offline, timeoutMs);
99
+ await withCommandRuntime(context, async (runtime) => {
100
+ const { db } = runtime;
101
+ const identity = await resolveAuthIdentity(runtime.config);
102
+ await syncBeforeCommand(runtime);
129
103
  const plan = await inspectStorageTest(db, {
130
104
  userId: identity.userId,
131
105
  storageId: options.storageId,
132
106
  });
133
107
  if (options.dryRun) {
134
108
  printDryRunPreview(buildStorageTestDryRunPreview({
135
- config,
136
- offline,
109
+ config: runtime.config,
110
+ offline: runtime.offline,
137
111
  storage: plan.storage,
138
112
  storageKey: plan.storageKey,
139
113
  storageUrl: plan.storageUrl,
@@ -148,7 +122,7 @@ async function runStorageTest(context) {
148
122
  });
149
123
  const payload = {
150
124
  ok: true,
151
- stale: offline ? true : stale,
125
+ stale: commandStale(runtime),
152
126
  ...result,
153
127
  };
154
128
  if (structuredOutput) {
@@ -162,20 +136,18 @@ async function runStorageTest(context) {
162
136
  if (result.isPrivate) {
163
137
  printHumanReadableLine('- note: the target bucket is private; direct reads may still require signed access.');
164
138
  }
165
- if (stale && !offline) {
166
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
167
- }
139
+ printStaleCacheWarning(runtime);
168
140
  });
169
141
  }
170
142
  async function runStorageStats(context) {
171
143
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
172
- await withStorageDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
173
- const identity = await resolveAuthIdentity(rawConfig, config);
174
- await syncBeforeStorageCommand(db, offline, timeoutMs);
175
- const stats = await getStorageStats(db, identity.userId);
144
+ await withCommandRuntime(context, async (runtime) => {
145
+ const identity = await resolveAuthIdentity(runtime.config);
146
+ await syncBeforeCommand(runtime);
147
+ const stats = await getStorageStats(runtime.db, identity.userId);
176
148
  const payload = {
177
149
  ok: true,
178
- stale: offline ? true : stale,
150
+ stale: commandStale(runtime),
179
151
  ...stats,
180
152
  };
181
153
  if (structuredOutput) {
@@ -191,9 +163,7 @@ async function runStorageStats(context) {
191
163
  printHumanReadableLine(`- ${entry.label}: ${entry.fileCount} files, ${formatBytes(entry.totalBytes)}`);
192
164
  }
193
165
  }
194
- if (stale && !offline) {
195
- printHumanReadableLine('warning: sync timed out; local cache may be stale');
196
- }
166
+ printStaleCacheWarning(runtime);
197
167
  });
198
168
  }
199
169
  export const storageCommandGroup = {
@@ -151,7 +151,7 @@ export declare function inspectStorageTest(db: AbstractPowerSyncDatabase, input:
151
151
  export declare function testStorageUpload(db: AbstractPowerSyncDatabase, input: {
152
152
  userId: string;
153
153
  storageId?: string;
154
- }, deps?: Pick<UploadDeps, 'uploadBytes'>): Promise<StorageTestResult>;
154
+ }, deps?: Pick<UploadDeps, 'deleteObject' | 'uploadBytes'>): Promise<StorageTestResult>;
155
155
  export declare function uploadFileToStorage(db: AbstractPowerSyncDatabase, input: {
156
156
  userId: string;
157
157
  filePath: string;
@@ -515,12 +515,13 @@ export async function getStorageStats(db, userId) {
515
515
  COUNT(1) AS file_count
516
516
  FROM c_resource
517
517
  WHERE (is_del IS NULL OR is_del = 0)
518
+ AND TRIM(COALESCE(user_id, '')) = ?
518
519
  AND TRIM(COALESCE(storage_provider, '')) <> ''
519
520
  AND TRIM(COALESCE(storage_provider, '')) <> 'local'
520
521
  AND TRIM(COALESCE(status, '')) = 'uploaded'
521
522
  GROUP BY storage_provider, bucket
522
523
  ORDER BY total_bytes DESC, provider ASC, bucket ASC
523
- `, []);
524
+ `, [userId]);
524
525
  const configs = await listStoredCustomStorageConfigs(db, userId);
525
526
  const configsByKey = new Map();
526
527
  const configsByProvider = new Map();
@@ -639,6 +640,7 @@ export async function testStorageUpload(db, input, deps) {
639
640
  throw new DinoxError(`Storage config not found: ${plan.storage.id}`);
640
641
  }
641
642
  const uploadBytes = deps?.uploadBytes ?? defaultUploadBytes;
643
+ const deleteObject = deps?.deleteObject ?? defaultDeleteObject;
642
644
  const payload = Buffer.from(`dinox-cli storage test\nprovider=${storage.provider}\nuser=${input.userId}\ncreated_at=${new Date().toISOString()}\n`, 'utf8');
643
645
  await uploadBytes({
644
646
  body: payload,
@@ -647,6 +649,23 @@ export async function testStorageUpload(db, input, deps) {
647
649
  contentType: plan.contentType,
648
650
  storage,
649
651
  });
652
+ try {
653
+ await deleteObject({
654
+ bucket: storage.bucket,
655
+ key: plan.storageKey,
656
+ storage,
657
+ });
658
+ }
659
+ catch (error) {
660
+ throw new DinoxError('Storage test uploaded the temporary object but failed to clean it up.', {
661
+ cause: error,
662
+ code: 'STORAGE_TEST_CLEANUP_FAILED',
663
+ details: {
664
+ storageKey: plan.storageKey,
665
+ bucket: storage.bucket,
666
+ },
667
+ });
668
+ }
650
669
  return {
651
670
  storageConfigId: storage.id,
652
671
  aliasName: storage.aliasName,
@@ -1,5 +1,6 @@
1
1
  import { loadConfig } from '../config/store.js';
2
2
  import { resolveConfig } from '../config/resolve.js';
3
+ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
3
4
  import { registerStandaloneCommand } from './descriptor/register.js';
4
5
  import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
5
6
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
@@ -22,9 +23,10 @@ export const syncCommandDescriptor = {
22
23
  examples: ['dino sync --format json', 'dino sync --sync-timeout 600000 --format json'],
23
24
  },
24
25
  action: async (context) => {
25
- const config = resolveConfig(await loadConfig());
26
+ const baseConfig = resolveConfig(await loadConfig());
26
27
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
27
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
28
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
29
+ const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs });
28
30
  const { db, dbPath, stale } = await connectPowerSync({
29
31
  config,
30
32
  offline: false,
@@ -0,0 +1,32 @@
1
+ export type NoteTagContentUpdate = {
2
+ tagsChanged: boolean;
3
+ contentJsonChanged: boolean;
4
+ contentMdChanged: boolean;
5
+ contentTextChanged: boolean;
6
+ tags: string[];
7
+ contentJson: string | null;
8
+ contentMd: string | null;
9
+ contentText: string | null;
10
+ };
11
+ export type TagReferenceLookup = {
12
+ idByPath: ReadonlyMap<string, string>;
13
+ pathById: ReadonlyMap<string, string>;
14
+ };
15
+ export declare function renameTagReferencesInNote(args: {
16
+ tags: string[];
17
+ contentJson: string | null;
18
+ contentMd: string | null;
19
+ contentText: string | null;
20
+ oldPrefix: string;
21
+ newPrefix: string;
22
+ lookup?: TagReferenceLookup;
23
+ }): NoteTagContentUpdate;
24
+ export declare function mergeTagReferencesInNote(args: {
25
+ tags: string[];
26
+ contentJson: string | null;
27
+ contentMd: string | null;
28
+ contentText: string | null;
29
+ oldPrefix: string;
30
+ newPrefix: string;
31
+ lookup?: TagReferenceLookup;
32
+ }): NoteTagContentUpdate;