@dinoxx/dinox-cli 1.0.22 → 1.1.1

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 (147) hide show
  1. package/README.md +33 -10
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +55 -14
  4. package/dist/cli.js +36 -2
  5. package/dist/commands/auth/index.d.ts +35 -0
  6. package/dist/commands/auth/index.js +382 -83
  7. package/dist/commands/boxes/index.js +2 -6
  8. package/dist/commands/boxes/model.d.ts +3 -0
  9. package/dist/commands/boxes/model.js +16 -1
  10. package/dist/commands/boxes/operations.js +18 -8
  11. package/dist/commands/boxes/repo.js +13 -11
  12. package/dist/commands/boxes/store.js +1 -1
  13. package/dist/commands/config/index.js +7 -5
  14. package/dist/commands/descriptor/register.js +9 -0
  15. package/dist/commands/doctor.d.ts +123 -0
  16. package/dist/commands/doctor.js +539 -0
  17. package/dist/commands/graph/index.d.ts +1 -1
  18. package/dist/commands/graph/index.js +35 -21
  19. package/dist/commands/graph/repo.js +92 -17
  20. package/dist/commands/meta/index.js +4 -4
  21. package/dist/commands/meta/repo.js +4 -3
  22. package/dist/commands/notes/actions.d.ts +1 -1
  23. package/dist/commands/notes/actions.js +121 -78
  24. package/dist/commands/notes/bulk.d.ts +1 -0
  25. package/dist/commands/notes/bulk.js +23 -17
  26. package/dist/commands/notes/contentEngine.js +69 -11
  27. package/dist/commands/notes/contentPatch.d.ts +4 -1
  28. package/dist/commands/notes/contentPatch.js +105 -41
  29. package/dist/commands/notes/contentPatchCommand.js +28 -11
  30. package/dist/commands/notes/descriptors.js +50 -20
  31. package/dist/commands/notes/dryRun.d.ts +1 -0
  32. package/dist/commands/notes/dryRun.js +4 -7
  33. package/dist/commands/notes/export.d.ts +6 -0
  34. package/dist/commands/notes/export.js +26 -6
  35. package/dist/commands/notes/metadataMutation.d.ts +1 -0
  36. package/dist/commands/notes/metadataMutation.js +14 -3
  37. package/dist/commands/notes/repo.d.ts +46 -4
  38. package/dist/commands/notes/repo.js +113 -28
  39. package/dist/commands/notes/runtime.d.ts +2 -1
  40. package/dist/commands/notes/runtime.js +17 -1
  41. package/dist/commands/notes/searchSql.js +13 -6
  42. package/dist/commands/notes/writeReceipt.d.ts +20 -0
  43. package/dist/commands/notes/writeReceipt.js +82 -0
  44. package/dist/commands/prompt/index.js +3 -3
  45. package/dist/commands/prompt/repo.d.ts +4 -1
  46. package/dist/commands/prompt/repo.js +17 -7
  47. package/dist/commands/runtime.d.ts +14 -2
  48. package/dist/commands/runtime.js +89 -19
  49. package/dist/commands/schema/renderSkills.js +5 -2
  50. package/dist/commands/schema/spec.js +4 -0
  51. package/dist/commands/storage/index.js +8 -2
  52. package/dist/commands/storage/repo.d.ts +4 -0
  53. package/dist/commands/storage/repo.js +356 -92
  54. package/dist/commands/sync.d.ts +23 -1
  55. package/dist/commands/sync.js +105 -60
  56. package/dist/commands/tags/content.js +9 -0
  57. package/dist/commands/tags/index.js +2 -6
  58. package/dist/commands/tags/operations.js +15 -10
  59. package/dist/commands/tags/repo.d.ts +2 -0
  60. package/dist/commands/tags/repo.js +18 -5
  61. package/dist/commands/tags/store.js +1 -1
  62. package/dist/commands/todo/contentModel.d.ts +4 -3
  63. package/dist/commands/todo/contentModel.js +78 -28
  64. package/dist/commands/todo/dryRun.d.ts +1 -0
  65. package/dist/commands/todo/dryRun.js +17 -13
  66. package/dist/commands/todo/index.d.ts +6 -0
  67. package/dist/commands/todo/index.js +89 -123
  68. package/dist/commands/todo/repo.d.ts +36 -1
  69. package/dist/commands/todo/repo.js +146 -37
  70. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  71. package/dist/commands/todo/taskIdentity.js +64 -0
  72. package/dist/commands/todo/taskModel.js +27 -17
  73. package/dist/commands/update.d.ts +13 -1
  74. package/dist/commands/update.js +135 -23
  75. package/dist/commands/writeDryRun.d.ts +3 -2
  76. package/dist/commands/writeDryRun.js +4 -3
  77. package/dist/commands/writeDurability.d.ts +5 -0
  78. package/dist/commands/writeDurability.js +22 -0
  79. package/dist/config/paths.js +6 -1
  80. package/dist/config/resolve.d.ts +2 -0
  81. package/dist/config/resolve.js +23 -9
  82. package/dist/config/store.d.ts +3 -0
  83. package/dist/config/store.js +61 -14
  84. package/dist/config/types.d.ts +4 -0
  85. package/dist/daemon/client.d.ts +26 -1
  86. package/dist/daemon/client.js +236 -12
  87. package/dist/daemon/ensure.js +74 -12
  88. package/dist/daemon/index.d.ts +42 -0
  89. package/dist/daemon/index.js +202 -52
  90. package/dist/daemon/lifecycle.d.ts +44 -0
  91. package/dist/daemon/lifecycle.js +118 -0
  92. package/dist/daemon/pid.d.ts +19 -6
  93. package/dist/daemon/pid.js +147 -13
  94. package/dist/daemon/route.d.ts +14 -0
  95. package/dist/daemon/route.js +40 -0
  96. package/dist/daemon/server.d.ts +11 -0
  97. package/dist/daemon/server.js +201 -50
  98. package/dist/daemon/types.d.ts +29 -1
  99. package/dist/daemon/types.js +1 -0
  100. package/dist/dinox.js +25 -1
  101. package/dist/powersync/connector.js +31 -16
  102. package/dist/powersync/exclusiveRuntime.d.ts +20 -0
  103. package/dist/powersync/exclusiveRuntime.js +48 -0
  104. package/dist/powersync/runtime.d.ts +25 -4
  105. package/dist/powersync/runtime.js +255 -35
  106. package/dist/powersync/syncGate.d.ts +4 -1
  107. package/dist/powersync/syncGate.js +11 -2
  108. package/dist/powersync/syncPass.d.ts +2 -0
  109. package/dist/powersync/syncPass.js +15 -4
  110. package/dist/powersync/tokenIndex.d.ts +13 -0
  111. package/dist/powersync/tokenIndex.js +216 -37
  112. package/dist/powersync/uploader.js +107 -52
  113. package/dist/utils/abort.js +0 -4
  114. package/dist/utils/asyncRelease.d.ts +2 -0
  115. package/dist/utils/asyncRelease.js +25 -0
  116. package/dist/utils/bidlinkCanonicalization.js +3 -0
  117. package/dist/utils/dryRun.d.ts +1 -1
  118. package/dist/utils/errors.d.ts +15 -1
  119. package/dist/utils/errors.js +223 -5
  120. package/dist/utils/output.d.ts +1 -0
  121. package/dist/utils/output.js +51 -15
  122. package/dist/utils/redact.js +1 -1
  123. package/dist/utils/sqliteLease.d.ts +8 -0
  124. package/dist/utils/sqliteLease.js +59 -0
  125. package/dist/utils/syncTimeout.d.ts +7 -1
  126. package/dist/utils/syncTimeout.js +8 -7
  127. package/dist/utils/terminalSanitization.js +6 -2
  128. package/dist/utils/tiptapMarkdown.js +49 -3
  129. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  130. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  131. package/dist/utils/tokenize.js +24 -2
  132. package/dist/utils/updateNotice.js +60 -12
  133. package/package.json +14 -10
  134. package/dist/commands/mcp/auth.d.ts +0 -4
  135. package/dist/commands/mcp/auth.js +0 -75
  136. package/dist/commands/mcp/index.d.ts +0 -2
  137. package/dist/commands/mcp/index.js +0 -86
  138. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  139. package/dist/commands/mcp/noteContentTools.js +0 -390
  140. package/dist/commands/mcp/server.d.ts +0 -34
  141. package/dist/commands/mcp/server.js +0 -424
  142. package/dist/commands/mcp/tools.d.ts +0 -2
  143. package/dist/commands/mcp/tools.js +0 -32
  144. package/dist/commands/mcp/types.d.ts +0 -39
  145. package/dist/commands/mcp/types.js +0 -1
  146. package/dist/utils/noteContentEngine.d.ts +0 -333
  147. package/dist/utils/noteContentEngine.js +0 -740
@@ -1,41 +1,121 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
2
3
  import { registerStandaloneCommand } from './descriptor/register.js';
4
+ import { FORMAT_OPTIONS } from './schema/globalOptions.js';
3
5
  import { SKILLS_REPO_URL } from '../constants/links.js';
4
- import { printHumanReadableLine } from '../utils/output.js';
6
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
7
+ import { DinoxError } from '../utils/errors.js';
5
8
  import { getPackageVersion } from '../utils/version.js';
6
9
  const PACKAGE_NAME = '@dinoxx/dinox-cli';
10
+ const MAX_CAPTURED_UPDATE_OUTPUT_BYTES = 64 * 1024;
7
11
  function logHumanReadable(...parts) {
8
12
  printHumanReadableLine(parts.map((part) => String(part)).join(' '));
9
13
  }
10
- function detectPackageManager() {
11
- const scriptPath = process.argv[1] ?? '';
12
- if (scriptPath.includes('/.bun/'))
14
+ function packageManagerFromHint(value) {
15
+ const hint = value?.trim().toLowerCase().replaceAll('\\', '/');
16
+ if (!hint)
17
+ return null;
18
+ if (hint.startsWith('bun/') || hint.includes('/.bun/') || /\/bun(?:\.exe)?$/.test(hint))
13
19
  return 'bun';
14
- if (scriptPath.includes('/pnpm/'))
20
+ if (hint.startsWith('pnpm/') || hint.includes('/.pnpm/') || /\/pnpm(?:\.c?js|\.cmd|\.exe)?$/.test(hint))
15
21
  return 'pnpm';
16
- if (scriptPath.includes('/.yarn/') || scriptPath.includes('/yarn/global'))
22
+ if (hint.startsWith('yarn/') || hint.includes('/.yarn/') || hint.includes('/yarn/global') || /\/yarn(?:\.c?js|\.cmd|\.exe)?$/.test(hint))
17
23
  return 'yarn';
24
+ if (hint.startsWith('npm/') || /\/npm(?:-cli\.js|\.cmd|\.exe)?$/.test(hint))
25
+ return 'npm';
26
+ return null;
27
+ }
28
+ export function detectPackageManager(input = {}) {
29
+ const env = input.env ?? process.env;
30
+ const scriptPath = input.scriptPath ?? process.argv[1] ?? '';
31
+ const hints = [env.npm_config_user_agent, env.npm_execpath, scriptPath];
32
+ try {
33
+ if (scriptPath)
34
+ hints.push(realpathSync(scriptPath));
35
+ }
36
+ catch {
37
+ // The entrypoint may disappear while a global package is being replaced.
38
+ }
39
+ for (const hint of hints) {
40
+ const detected = packageManagerFromHint(hint);
41
+ if (detected)
42
+ return detected;
43
+ }
18
44
  return 'npm';
19
45
  }
20
46
  const PM_COMMANDS = {
21
- npm: { bin: 'npm', args: ['update', '-g', PACKAGE_NAME] },
22
- pnpm: { bin: 'pnpm', args: ['update', '--global', PACKAGE_NAME] },
23
- yarn: { bin: 'yarn', args: ['global', 'upgrade', PACKAGE_NAME] },
24
- bun: { bin: 'bun', args: ['add', '--global', PACKAGE_NAME] },
47
+ npm: { bin: 'npm', args: ['install', '--global', `${PACKAGE_NAME}@latest`] },
48
+ pnpm: { bin: 'pnpm', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
49
+ yarn: { bin: 'yarn', args: ['global', 'add', `${PACKAGE_NAME}@latest`] },
50
+ bun: { bin: 'bun', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
25
51
  };
26
- function runUpdate(pm) {
52
+ export function resolveUpdateSpawnSpec(pm, platform = process.platform) {
27
53
  const { bin, args } = PM_COMMANDS[pm];
54
+ const usesWindowsCommandShim = platform === 'win32' && bin !== 'bun';
55
+ if (usesWindowsCommandShim) {
56
+ return {
57
+ command: [`${bin}.cmd`, ...args].join(' '),
58
+ args: [],
59
+ shell: true,
60
+ };
61
+ }
62
+ return {
63
+ command: bin,
64
+ args: [...args],
65
+ shell: false,
66
+ };
67
+ }
68
+ function appendBoundedOutput(current, chunk) {
69
+ if (Buffer.byteLength(current, 'utf8') >= MAX_CAPTURED_UPDATE_OUTPUT_BYTES) {
70
+ return current;
71
+ }
72
+ const remaining = MAX_CAPTURED_UPDATE_OUTPUT_BYTES - Buffer.byteLength(current, 'utf8');
73
+ return current + chunk.subarray(0, remaining).toString('utf8');
74
+ }
75
+ function runUpdate(pm, options) {
76
+ const { command, args, shell } = resolveUpdateSpawnSpec(pm);
77
+ const invocation = [command, ...args].join(' ');
28
78
  return new Promise((resolve, reject) => {
29
- const child = spawn(bin, args, { stdio: 'inherit', shell: true });
79
+ let stdout = '';
80
+ let stderr = '';
81
+ const child = spawn(command, args, {
82
+ shell,
83
+ stdio: options.structuredOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
84
+ });
85
+ if (options.structuredOutput) {
86
+ child.stdout?.on('data', (chunk) => {
87
+ stdout = appendBoundedOutput(stdout, chunk);
88
+ });
89
+ child.stderr?.on('data', (chunk) => {
90
+ stderr = appendBoundedOutput(stderr, chunk);
91
+ });
92
+ }
30
93
  child.on('close', (code) => {
31
94
  if (code === 0) {
32
95
  resolve();
33
96
  }
34
97
  else {
35
- reject(new Error(`${bin} exited with code ${code}`));
98
+ reject(new DinoxError(`${command} exited with code ${code ?? 'unknown'}`, {
99
+ code: 'UPDATE_FAILED',
100
+ details: {
101
+ packageManager: pm,
102
+ exitCode: code,
103
+ ...(stdout.trim() ? { stdout: stdout.trim() } : {}),
104
+ ...(stderr.trim() ? { stderr: stderr.trim() } : {}),
105
+ },
106
+ suggestion: `Run ${invocation} manually to inspect the failure.`,
107
+ recoverable: true,
108
+ }));
36
109
  }
37
110
  });
38
- child.on('error', reject);
111
+ child.on('error', (error) => {
112
+ reject(new DinoxError(`Failed to start ${command}: ${error.message}`, {
113
+ code: 'UPDATE_FAILED',
114
+ cause: error,
115
+ suggestion: `Verify that ${command} is installed and available on PATH.`,
116
+ recoverable: true,
117
+ }));
118
+ });
39
119
  });
40
120
  }
41
121
  function resolveDeps(overrides) {
@@ -51,28 +131,60 @@ function printSkillsReminder(log) {
51
131
  log(`skills repo: ${SKILLS_REPO_URL}`);
52
132
  log('ai reminder: review and update local Dinox skills after CLI updates (especially dino-dinox and dino-update-cli).');
53
133
  }
134
+ function parsePackageManagerOverride(value) {
135
+ if (value === undefined || value === null || value === '') {
136
+ return null;
137
+ }
138
+ if (value === 'npm' || value === 'pnpm' || value === 'yarn' || value === 'bun') {
139
+ return value;
140
+ }
141
+ throw new DinoxError('--package-manager must be one of: npm, pnpm, yarn, bun', {
142
+ code: 'USAGE_ERROR',
143
+ recoverable: true,
144
+ });
145
+ }
54
146
  export function createUpdateCommandDescriptor(overrides = {}) {
55
147
  const deps = resolveDeps(overrides);
56
148
  return {
57
149
  name: 'update',
58
150
  description: `Update ${PACKAGE_NAME} to the latest version`,
151
+ options: [{
152
+ flags: '--package-manager <manager>',
153
+ description: 'Override package manager detection',
154
+ type: 'string',
155
+ enum: ['npm', 'pnpm', 'yarn', 'bun'],
156
+ }],
59
157
  schema: {
60
158
  path: 'update',
61
159
  risk: 'write',
62
160
  auth: 'none',
63
- globalOptions: [],
161
+ globalOptions: FORMAT_OPTIONS,
64
162
  output: {
65
163
  type: 'structured',
66
- summary: 'Human-readable updater progress and bundled skills reminder',
67
- fields: ['current version log', 'package manager log', 'skills repo reminder'],
164
+ summary: 'Updater result and bundled skills reminder',
165
+ fields: ['previousVersion', 'packageManager', 'package', 'target', 'skillsRepo'],
68
166
  },
69
- examples: ['dino update'],
167
+ examples: ['dino update', 'dino update --package-manager pnpm --format json'],
70
168
  },
71
- action: async () => {
72
- const pm = deps.detectPackageManager();
73
- deps.log(`current version: ${deps.getVersion()}`);
74
- deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
75
- await deps.runUpdate(pm);
169
+ action: async (context) => {
170
+ const pm = parsePackageManagerOverride(context.options.packageManager) ?? deps.detectPackageManager();
171
+ const previousVersion = deps.getVersion();
172
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
173
+ if (!structuredOutput) {
174
+ deps.log(`current version: ${previousVersion}`);
175
+ deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
176
+ }
177
+ await deps.runUpdate(pm, { structuredOutput });
178
+ if (structuredOutput) {
179
+ printSuccess({
180
+ previousVersion,
181
+ packageManager: pm,
182
+ package: PACKAGE_NAME,
183
+ target: 'latest',
184
+ skillsRepo: SKILLS_REPO_URL,
185
+ });
186
+ return;
187
+ }
76
188
  deps.log('done');
77
189
  printSkillsReminder(deps.log);
78
190
  },
@@ -14,7 +14,7 @@ export declare function buildTagMutationDryRunPreview(args: {
14
14
  command: string;
15
15
  action: 'rename_tag' | 'move_tag' | 'merge_tag' | 'cleanup_tags';
16
16
  summary: string;
17
- risk: 'write' | 'high-risk-write';
17
+ risk: 'read' | 'write' | 'high-risk-write';
18
18
  inputs: Record<string, unknown>;
19
19
  changes?: Record<string, unknown> | Array<Record<string, unknown>>;
20
20
  targets?: Array<Record<string, unknown>>;
@@ -37,7 +37,7 @@ export declare function buildBoxMutationDryRunPreview(args: {
37
37
  command: string;
38
38
  action: 'rename_box' | 'move_box' | 'merge_box' | 'cleanup_boxes';
39
39
  summary: string;
40
- risk: 'write' | 'high-risk-write';
40
+ risk: 'read' | 'write' | 'high-risk-write';
41
41
  inputs: Record<string, unknown>;
42
42
  changes?: Record<string, unknown> | Array<Record<string, unknown>>;
43
43
  targets?: Array<Record<string, unknown>>;
@@ -68,6 +68,7 @@ export declare function buildStorageUploadDryRunPreview(args: {
68
68
  storageKey: string;
69
69
  storageUrl: string;
70
70
  selectionSource: 'storage_id' | 'active' | 'single_available_config';
71
+ overwrite: boolean;
71
72
  }): DryRunPreview;
72
73
  export declare function buildStorageTestDryRunPreview(args: {
73
74
  config: ResolvedDinoxConfig;
@@ -3,9 +3,6 @@ function buildExecutionWarnings(config, offline) {
3
3
  if (offline) {
4
4
  warnings.push('Execution would run in offline mode and only use the local cache.');
5
5
  }
6
- if (!offline && !config.powersync.uploadBaseUrl) {
7
- warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
8
- }
9
6
  return warnings;
10
7
  }
11
8
  export function buildTagAddDryRunPreview(args) {
@@ -105,6 +102,9 @@ export function buildStorageUploadDryRunPreview(args) {
105
102
  if (args.storage.isPrivate) {
106
103
  warnings.push('The target bucket is private; the final storageUrl may require signed read access.');
107
104
  }
105
+ if (args.overwrite) {
106
+ warnings.push('Existing objects at the explicit key and derived thumbnail key may be replaced.');
107
+ }
108
108
  return {
109
109
  dry_run: true,
110
110
  command: 'dino storage upload',
@@ -117,6 +117,7 @@ export function buildStorageUploadDryRunPreview(args) {
117
117
  category: args.category,
118
118
  size_bytes: args.sizeBytes,
119
119
  selection_source: args.selectionSource,
120
+ overwrite: args.overwrite,
120
121
  },
121
122
  changes: {
122
123
  storage_config_id: args.storage.id,
@@ -0,0 +1,5 @@
1
+ import type { CommandOptionDescriptor } from './descriptor/types.js';
2
+ export declare const WRITE_DURABILITY_VALUES: readonly ["local", "uploaded"];
3
+ export type WriteDurability = typeof WRITE_DURABILITY_VALUES[number];
4
+ export declare const WRITE_DURABILITY_OPTION: CommandOptionDescriptor;
5
+ export declare function parseWriteDurability(value: unknown, optionName?: string): WriteDurability;
@@ -0,0 +1,22 @@
1
+ import { DinoxError } from '../utils/errors.js';
2
+ export const WRITE_DURABILITY_VALUES = ['local', 'uploaded'];
3
+ export const WRITE_DURABILITY_OPTION = {
4
+ flags: '--durability <local|uploaded>',
5
+ description: 'Required write durability before success: local saves to the local DB; uploaded waits for the PowerSync upload queue to drain',
6
+ type: 'string',
7
+ defaultValue: 'local',
8
+ enum: [...WRITE_DURABILITY_VALUES],
9
+ };
10
+ export function parseWriteDurability(value, optionName = '--durability') {
11
+ if (value === undefined || value === null || value === '') {
12
+ return 'local';
13
+ }
14
+ if (typeof value !== 'string') {
15
+ throw new DinoxError(`${optionName} must be one of: local, uploaded`, { code: 'USAGE_ERROR' });
16
+ }
17
+ const normalized = value.trim().toLowerCase();
18
+ if (normalized === 'local' || normalized === 'uploaded') {
19
+ return normalized;
20
+ }
21
+ throw new DinoxError(`${optionName} must be one of: local, uploaded`, { code: 'USAGE_ERROR' });
22
+ }
@@ -2,7 +2,12 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
4
4
  function normalizeUserPathSegment(userId) {
5
- return encodeURIComponent(userId.trim());
5
+ const normalized = userId.trim();
6
+ const encoded = encodeURIComponent(normalized);
7
+ if (encoded === '.' || encoded === '..') {
8
+ return `user-${Buffer.from(normalized, 'utf8').toString('hex')}`;
9
+ }
10
+ return encoded;
6
11
  }
7
12
  function resolveConfigHome() {
8
13
  if (process.env.DINOX_CONFIG_DIR) {
@@ -1,2 +1,4 @@
1
1
  import type { DinoxConfig, ResolvedDinoxConfig } from './types.js';
2
+ export declare function fingerprintAuthToken(authorization: string | undefined): string | undefined;
3
+ export declare function fingerprintDaemonRuntimeConfig(config: ResolvedDinoxConfig): string;
2
4
  export declare function resolveConfig(raw: DinoxConfig): ResolvedDinoxConfig;
@@ -1,5 +1,7 @@
1
+ import crypto from 'node:crypto';
1
2
  import { POWERSYNC_DEFAULT_ENDPOINT, POWERSYNC_DEFAULT_TOKEN_ENDPOINT, POWERSYNC_DEFAULT_UPLOAD_BASE_URL, } from './serviceEndpoints.js';
2
3
  import { normalizeAuthToken, readEnvAuthToken } from '../utils/authToken.js';
4
+ import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
3
5
  const DEFAULT_UPLOAD_V4_PATH = '/powersync/v4/uploadData';
4
6
  const DEFAULT_UPLOAD_V2_PATH = '/powersync/v2/uploadData';
5
7
  const DEFAULT_SYNC_TIMEOUT_MS = 300_000;
@@ -10,15 +12,26 @@ function nonEmptyString(value) {
10
12
  const trimmed = value.trim();
11
13
  return trimmed.length > 0 ? trimmed : undefined;
12
14
  }
13
- function parsePositiveInt(value) {
14
- if (typeof value !== 'string' && typeof value !== 'number') {
15
+ export function fingerprintAuthToken(authorization) {
16
+ if (!authorization) {
15
17
  return undefined;
16
18
  }
17
- const parsed = Number(value);
18
- if (!Number.isFinite(parsed) || parsed <= 0) {
19
- return undefined;
20
- }
21
- return Math.trunc(parsed);
19
+ return `sha256:${crypto.createHash('sha256').update(authorization, 'utf8').digest('hex')}`;
20
+ }
21
+ export function fingerprintDaemonRuntimeConfig(config) {
22
+ const payload = JSON.stringify({
23
+ powersync: {
24
+ endpoint: config.powersync.endpoint,
25
+ tokenEndpoint: config.powersync.tokenEndpoint,
26
+ uploadBaseUrl: config.powersync.uploadBaseUrl,
27
+ uploadV4Path: config.powersync.uploadV4Path,
28
+ uploadV2Path: config.powersync.uploadV2Path,
29
+ },
30
+ sync: {
31
+ timeoutMs: config.sync.timeoutMs,
32
+ },
33
+ });
34
+ return `sha256:${crypto.createHash('sha256').update(payload, 'utf8').digest('hex')}`;
22
35
  }
23
36
  export function resolveConfig(raw) {
24
37
  const fileAuth = normalizeAuthToken(raw.auth?.authorization);
@@ -29,17 +42,18 @@ export function resolveConfig(raw) {
29
42
  const fileTokenEndpoint = nonEmptyString(raw.powersync?.tokenEndpoint);
30
43
  const fileUploadV4Path = nonEmptyString(raw.powersync?.uploadV4Path);
31
44
  const fileUploadV2Path = nonEmptyString(raw.powersync?.uploadV2Path);
32
- const fileTimeoutMs = parsePositiveInt(raw.sync?.timeoutMs);
45
+ const fileTimeoutMs = parseSyncTimeoutMs(raw.sync?.timeoutMs, { invalid: 'undefined' });
33
46
  const envAuth = readEnvAuthToken();
34
47
  const envTokenEndpoint = nonEmptyString(process.env.DINOX_POWERSYNC_TOKEN_ENDPOINT) ?? nonEmptyString(process.env.POWERSYNC_TOKEN_ENDPOINT);
35
48
  const envUploadV4Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V4_PATH);
36
49
  const envUploadV2Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V2_PATH);
37
- const envTimeoutMs = parsePositiveInt(process.env.DINOX_SYNC_TIMEOUT_MS);
50
+ const envTimeoutMs = parseSyncTimeoutMs(process.env.DINOX_SYNC_TIMEOUT_MS, { invalid: 'undefined' });
38
51
  const effectiveAuth = envAuth ?? fileAuth;
39
52
  const canReusePersistedIdentity = !envAuth || envAuth === fileAuth;
40
53
  return {
41
54
  auth: {
42
55
  authorization: effectiveAuth,
56
+ tokenFingerprint: fingerprintAuthToken(effectiveAuth),
43
57
  userId: canReusePersistedIdentity ? fileUserId : undefined,
44
58
  username: canReusePersistedIdentity ? fileUsername : undefined,
45
59
  email: canReusePersistedIdentity ? fileEmail : undefined,
@@ -1,5 +1,8 @@
1
1
  import type { DinoxConfig } from './types.js';
2
2
  export declare function loadConfig(): Promise<DinoxConfig>;
3
3
  export declare function saveConfig(config: DinoxConfig): Promise<void>;
4
+ export declare function updateConfig(update: (current: DinoxConfig) => DinoxConfig | Promise<DinoxConfig>, options?: {
5
+ timeoutMs?: number;
6
+ }): Promise<DinoxConfig>;
4
7
  export declare function setConfigValue(keyPath: string, value: unknown): Promise<void>;
5
8
  export declare function getConfigValue(keyPath?: string): Promise<unknown>;
@@ -3,6 +3,9 @@ import path from 'node:path';
3
3
  import { getConfigDir, getConfigFilePath } from './paths.js';
4
4
  import { generateId } from '../utils/id.js';
5
5
  import { DinoxError } from '../utils/errors.js';
6
+ import { acquireExclusiveSqliteLease } from '../utils/sqliteLease.js';
7
+ import { withAsyncRelease } from '../utils/asyncRelease.js';
8
+ const CONFIG_WRITE_LOCK_TIMEOUT_MS = 10_000;
6
9
  function isPlainObject(value) {
7
10
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
11
  }
@@ -25,6 +28,24 @@ async function ensureConfigDir() {
25
28
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
26
29
  return dir;
27
30
  }
31
+ function getConfigWriteLockPath() {
32
+ return `${getConfigFilePath()}.write.sqlite`;
33
+ }
34
+ async function acquireConfigWriteLock(timeoutMs = CONFIG_WRITE_LOCK_TIMEOUT_MS) {
35
+ return acquireExclusiveSqliteLease({
36
+ lockPath: getConfigWriteLockPath(),
37
+ timeoutMs,
38
+ maxWaitMs: CONFIG_WRITE_LOCK_TIMEOUT_MS,
39
+ createBusyError: () => new DinoxError('Another Dinox process is updating the configuration.', {
40
+ code: 'CONFIG_WRITE_BUSY',
41
+ suggestion: 'Wait for the other Dinox command to finish, then retry.',
42
+ recoverable: true,
43
+ }),
44
+ });
45
+ }
46
+ async function withConfigWriteLock(run, timeoutMs = CONFIG_WRITE_LOCK_TIMEOUT_MS) {
47
+ return withAsyncRelease(() => acquireConfigWriteLock(timeoutMs), run);
48
+ }
28
49
  export async function loadConfig() {
29
50
  const filePath = getConfigFilePath();
30
51
  try {
@@ -39,27 +60,52 @@ export async function loadConfig() {
39
60
  if (error?.code === 'ENOENT') {
40
61
  return {};
41
62
  }
63
+ if (error instanceof SyntaxError) {
64
+ throw new DinoxError(`Invalid config JSON at ${filePath}: ${error.message}`, {
65
+ code: 'CONFIG_INVALID_JSON',
66
+ cause: error,
67
+ });
68
+ }
42
69
  throw error;
43
70
  }
44
71
  }
45
- export async function saveConfig(config) {
72
+ async function writeConfigFile(config) {
46
73
  const dir = await ensureConfigDir();
47
74
  const filePath = getConfigFilePath();
48
75
  const tmpPath = path.join(dir, `config.${generateId()}.tmp`);
49
76
  const payload = `${JSON.stringify(config, null, 2)}\n`;
50
- // Write config atomically to avoid partial reads.
51
- const handle = await fs.open(tmpPath, 'wx', 0o600);
52
77
  try {
53
- await handle.writeFile(payload, 'utf8');
54
- }
55
- finally {
56
- await handle.close();
78
+ // Write config atomically to avoid partial reads.
79
+ const handle = await fs.open(tmpPath, 'wx', 0o600);
80
+ try {
81
+ await handle.writeFile(payload, 'utf8');
82
+ }
83
+ finally {
84
+ await handle.close();
85
+ }
86
+ if (process.platform !== 'win32') {
87
+ // Best-effort: keep the config private.
88
+ await fs.chmod(tmpPath, 0o600).catch(() => undefined);
89
+ }
90
+ await fs.rename(tmpPath, filePath);
57
91
  }
58
- if (process.platform !== 'win32') {
59
- // Best-effort: keep the config private.
60
- await fs.chmod(tmpPath, 0o600).catch(() => undefined);
92
+ catch (error) {
93
+ await fs.rm(tmpPath, { force: true }).catch(() => undefined);
94
+ throw error;
61
95
  }
62
- await fs.rename(tmpPath, filePath);
96
+ }
97
+ export async function saveConfig(config) {
98
+ await withConfigWriteLock(async () => {
99
+ await writeConfigFile(config);
100
+ });
101
+ }
102
+ export async function updateConfig(update, options = {}) {
103
+ return withConfigWriteLock(async () => {
104
+ const current = await loadConfig();
105
+ const next = await update(current);
106
+ await writeConfigFile(next);
107
+ return next;
108
+ }, options.timeoutMs);
63
109
  }
64
110
  function setDeepValue(target, keyPath, value) {
65
111
  const segments = splitKeyPath(keyPath);
@@ -89,9 +135,10 @@ function getDeepValue(target, keyPath) {
89
135
  return cursor;
90
136
  }
91
137
  export async function setConfigValue(keyPath, value) {
92
- const config = await loadConfig();
93
- setDeepValue(config, keyPath, value);
94
- await saveConfig(config);
138
+ await updateConfig((config) => {
139
+ setDeepValue(config, keyPath, value);
140
+ return config;
141
+ });
95
142
  }
96
143
  export async function getConfigValue(keyPath) {
97
144
  const config = await loadConfig();
@@ -11,6 +11,8 @@ export type DinoxConfig = {
11
11
  username?: string | null;
12
12
  email?: string | null;
13
13
  userInfoSyncedAt?: string;
14
+ /** Internal CAS token changed by every persisted login/logout transition. */
15
+ stateRevision?: string;
14
16
  };
15
17
  powersync?: {
16
18
  /** PowerSync sync service endpoint (downloads/subscriptions). */
@@ -32,6 +34,8 @@ export type DinoxConfig = {
32
34
  export type ResolvedDinoxConfig = {
33
35
  auth: {
34
36
  authorization?: string;
37
+ /** Runtime-only SHA-256 fingerprint used to bind long-lived processes to the active token. */
38
+ tokenFingerprint?: string;
35
39
  userId?: string;
36
40
  username?: string | null;
37
41
  email?: string | null;
@@ -1,2 +1,27 @@
1
- export declare function sendDaemonRequest(method: string, params?: unknown): Promise<unknown>;
1
+ import type { DaemonPingResult, DaemonShutdownResult } from './types.js';
2
+ export declare function sendDaemonRequest(method: string, params?: unknown, requestOptions?: {
3
+ timeoutMs?: number;
4
+ }): Promise<unknown>;
2
5
  export declare function isDaemonRunning(): Promise<boolean>;
6
+ /**
7
+ * Ping the daemon and return its handshake payload (version + connected identity),
8
+ * or null when no daemon is reachable. Callers use this to detect a stale daemon
9
+ * (wrong version or wrong account) rather than only "running / not running".
10
+ */
11
+ export declare function getDaemonInfo(): Promise<DaemonPingResult | null>;
12
+ /**
13
+ * Whether a reachable daemon matches the current CLI version and signed-in account.
14
+ * A mismatch means the daemon was started by an older build or a different user and
15
+ * must be restarted before its cached data can be trusted.
16
+ */
17
+ export declare function isDaemonFresh(info: DaemonPingResult, expectedVersion: string, expectedUserId: string | null, expectedTokenFingerprint: string | null, expectedRuntimeConfigFingerprint: string): boolean;
18
+ export declare function isLegacyDaemonInfo(info: DaemonPingResult): boolean;
19
+ export declare function shutdownLegacyDaemon(initialInfo: DaemonPingResult, timeoutMs?: number): Promise<boolean>;
20
+ /** Poll until the daemon answers a ping or the timeout elapses. */
21
+ export declare function waitForDaemonReady(timeoutMs?: number, intervalMs?: number): Promise<boolean>;
22
+ /** Poll until the daemon answers with the expected authenticated identity. */
23
+ export declare function waitForFreshDaemon(expectedVersion: string, expectedUserId: string | null, expectedTokenFingerprint: string | null, expectedRuntimeConfigFingerprint: string, timeoutMs?: number, intervalMs?: number): Promise<boolean>;
24
+ /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
25
+ export declare function requestDaemonShutdown(expectedInstanceId: string, timeoutMs?: number): Promise<DaemonShutdownResult>;
26
+ /** Stop a reachable daemon before another process acquires the exclusive PowerSync owner lease. */
27
+ export declare function shutdownDaemonForExclusiveAccess(timeoutMs?: number, expectedInstanceId?: string): Promise<boolean>;