@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
@@ -3,14 +3,20 @@ export class DinoxError extends Error {
3
3
  details;
4
4
  code;
5
5
  suggestion;
6
+ suggestedAction;
6
7
  recoverable;
7
8
  constructor(message, options) {
8
9
  super(message);
10
+ const code = normalizeErrorCode(options?.code ?? inferDinoxErrorCode(message));
11
+ const suggestion = options?.suggestion ?? defaultSuggestionForCode(code);
9
12
  this.name = 'DinoxError';
10
- this.exitCode = options?.exitCode ?? 1;
13
+ this.exitCode = options?.exitCode ?? exitCodeForErrorCode(code);
11
14
  this.details = options?.details;
12
- this.code = options?.code;
13
- this.suggestion = options?.suggestion;
15
+ this.code = code;
16
+ this.suggestion = suggestion;
17
+ this.suggestedAction = options?.suggestedAction
18
+ ?? suggestedActionFromSuggestion(suggestion)
19
+ ?? defaultSuggestedActionForCode(code, message);
14
20
  this.recoverable = options?.recoverable ?? true;
15
21
  if (options?.cause !== undefined) {
16
22
  // Node 16+ supports Error.cause.
@@ -19,20 +25,47 @@ export class DinoxError extends Error {
19
25
  }
20
26
  }
21
27
  export function isDinoxError(error) {
22
- return Boolean(error) && typeof error === 'object' && error.name === 'DinoxError';
28
+ return Boolean(error)
29
+ && typeof error === 'object'
30
+ && error.name === 'DinoxError'
31
+ && typeof error.exitCode === 'number';
32
+ }
33
+ function serializeCause(error) {
34
+ if (!error || typeof error !== 'object' || !('cause' in error)) {
35
+ return undefined;
36
+ }
37
+ const cause = error.cause;
38
+ if (cause === undefined) {
39
+ return undefined;
40
+ }
41
+ if (cause instanceof Error) {
42
+ return {
43
+ name: cause.name,
44
+ message: cause.message,
45
+ ...(cause.stack ? { stack: cause.stack } : {}),
46
+ };
47
+ }
48
+ return {
49
+ name: 'Error',
50
+ message: String(cause),
51
+ };
23
52
  }
24
53
  export function buildStructuredErrorPayload(error, verbose) {
25
54
  const message = error instanceof Error ? error.message : String(error);
26
55
  const name = error instanceof Error ? error.name : 'Error';
27
56
  const stack = error instanceof Error ? error.stack : undefined;
28
57
  if (isDinoxError(error)) {
58
+ const cause = verbose ? serializeCause(error) : undefined;
29
59
  return {
30
60
  name,
31
- code: error.code ?? 'DINOX_ERROR',
61
+ code: error.code,
32
62
  message,
33
63
  recoverable: error.recoverable,
64
+ exit_code: error.exitCode,
34
65
  ...(error.suggestion ? { suggestion: error.suggestion } : {}),
66
+ ...(error.suggestedAction ? { suggested_action: error.suggestedAction } : {}),
35
67
  ...(error.details !== undefined ? { details: error.details } : {}),
68
+ ...(cause ? { cause } : {}),
36
69
  ...(verbose && stack ? { stack } : {}),
37
70
  };
38
71
  }
@@ -41,6 +74,191 @@ export function buildStructuredErrorPayload(error, verbose) {
41
74
  code: 'INTERNAL_ERROR',
42
75
  message,
43
76
  recoverable: false,
77
+ exit_code: 1,
78
+ suggested_action: {
79
+ command: 'dino doctor --format json',
80
+ reason: 'Inspect local auth, sync, index, daemon, and database health before retrying.',
81
+ },
44
82
  ...(verbose && stack ? { stack } : {}),
45
83
  };
46
84
  }
85
+ function normalizeErrorCode(value) {
86
+ const normalized = value.trim().toUpperCase().replace(/[^A-Z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
87
+ return normalized || 'COMMAND_FAILED';
88
+ }
89
+ function inferDinoxErrorCode(message) {
90
+ const text = message.trim();
91
+ const lower = text.toLowerCase();
92
+ if (/cache is not fully synchronized|stale|\bsync required\b|\bsync timed out\b|\bsync could not\b/.test(lower)) {
93
+ return 'SYNC_STALE';
94
+ }
95
+ if (lower.includes('not logged in')
96
+ || lower.includes('auth login')
97
+ || lower.includes('missing resolved userid')
98
+ || lower.includes('requires a logged-in user')
99
+ || lower.includes('missing backend base url')) {
100
+ return 'AUTH_REQUIRED';
101
+ }
102
+ if (text.startsWith('--')
103
+ || /^(invalid|provide|at least one|conflicting|missing|token is required|file path is required|note id is required|taskid is required|prompt name is required|prompt text is required|tag name is required|box path is required)/i.test(text)
104
+ || /\b(must be|requires|cannot be used together|expected|one of|required)\b/i.test(text)) {
105
+ return 'USAGE_ERROR';
106
+ }
107
+ if (/\b(not found|unknown|does not exist|no active|no eligible|no custom|not available)\b/i.test(text)) {
108
+ return 'NOT_FOUND';
109
+ }
110
+ if (/\b(already exists|duplicate|refusing|ambiguous|not unique|deleted|expired|changed after|does not match|belongs to a different user|cannot move|cannot merge|protected)\b/i.test(text)) {
111
+ return 'PRECONDITION_FAILED';
112
+ }
113
+ if (lower.includes('daemon')) {
114
+ return 'DAEMON_ERROR';
115
+ }
116
+ return 'COMMAND_FAILED';
117
+ }
118
+ export function exitCodeForErrorCode(code) {
119
+ const normalized = normalizeErrorCode(code);
120
+ if (normalized === 'USAGE_ERROR' || normalized === 'INVALID_ARGUMENT' || normalized.startsWith('INVALID_')) {
121
+ return 2;
122
+ }
123
+ if (normalized === 'AUTH_REQUIRED' || normalized === 'AUTH_INVALID' || normalized === 'UNAUTHENTICATED' || normalized.startsWith('AUTH_')) {
124
+ return 3;
125
+ }
126
+ if (normalized === 'SYNC_REQUIRED' || normalized === 'SYNC_STALE' || normalized === 'UPLOAD_PENDING' || normalized.startsWith('SYNC_')) {
127
+ return 4;
128
+ }
129
+ if (normalized === 'PRECONDITION_FAILED'
130
+ || normalized === 'NOT_FOUND'
131
+ || normalized === 'ALREADY_EXISTS'
132
+ || normalized === 'DAEMON_STALE'
133
+ || normalized === 'DAEMON_ERROR'
134
+ || normalized === 'DAEMON_START_FAILED'
135
+ || normalized.endsWith('_NOT_FOUND')
136
+ || normalized.endsWith('_ALREADY_EXISTS')
137
+ || normalized.endsWith('_REQUIRED')) {
138
+ return 5;
139
+ }
140
+ return 1;
141
+ }
142
+ function defaultSuggestionForCode(code) {
143
+ switch (normalizeErrorCode(code)) {
144
+ case 'AUTH_REQUIRED':
145
+ case 'AUTH_INVALID':
146
+ case 'UNAUTHENTICATED':
147
+ return 'Run `dino auth status --format json`; if auth is missing, set DINOX_TOKEN or pipe a token to `dino auth login --token-stdin` in your own terminal.';
148
+ case 'SYNC_REQUIRED':
149
+ case 'SYNC_STALE':
150
+ return 'Run `dino sync --strict --sync-timeout 20000 --format json`, then retry the command.';
151
+ case 'UPLOAD_PENDING':
152
+ return 'Run `dino doctor --fix --format json` to wait for pending uploads and inspect sync health.';
153
+ case 'DAEMON_STALE':
154
+ case 'DAEMON_ERROR':
155
+ case 'DAEMON_START_FAILED':
156
+ return 'Run `dino doctor --fix --format json` to restart a stale daemon and inspect local health.';
157
+ case 'USAGE_ERROR':
158
+ case 'INVALID_ARGUMENT':
159
+ return 'Run `dino schema --format json` to inspect valid commands, arguments, and flags.';
160
+ default: {
161
+ const normalized = normalizeErrorCode(code);
162
+ if (normalized.startsWith('AUTH_')) {
163
+ return 'Run `dino auth status --format json`; if auth is missing, set DINOX_TOKEN or pipe a token to `dino auth login --token-stdin` in your own terminal.';
164
+ }
165
+ if (normalized.startsWith('SYNC_')) {
166
+ return 'Run `dino sync --strict --sync-timeout 20000 --format json`, then retry the command.';
167
+ }
168
+ if (normalized.startsWith('DAEMON_')) {
169
+ return 'Run `dino doctor --fix --format json` to restart a stale daemon and inspect local health.';
170
+ }
171
+ return undefined;
172
+ }
173
+ }
174
+ }
175
+ function suggestedActionFromSuggestion(suggestion) {
176
+ if (!suggestion) {
177
+ return undefined;
178
+ }
179
+ const match = suggestion.match(/`(dino\s+[^`]+)`/) ?? suggestion.match(/['"](dino\s+[^'"]+)['"]/);
180
+ const command = match?.[1]?.trim();
181
+ if (!command) {
182
+ return undefined;
183
+ }
184
+ return {
185
+ command,
186
+ reason: suggestion,
187
+ };
188
+ }
189
+ function defaultSuggestedActionForCode(code, message) {
190
+ const normalized = normalizeErrorCode(code);
191
+ if (normalized === 'AUTH_REQUIRED' || normalized === 'AUTH_INVALID' || normalized === 'UNAUTHENTICATED' || normalized.startsWith('AUTH_')) {
192
+ return {
193
+ command: 'dino auth status --format json',
194
+ reason: 'Confirm whether CLI authentication is configured before retrying.',
195
+ };
196
+ }
197
+ if (normalized === 'SYNC_REQUIRED' || normalized === 'SYNC_STALE' || normalized.startsWith('SYNC_')) {
198
+ return {
199
+ command: 'dino sync --strict --sync-timeout 20000 --format json',
200
+ reason: 'Refresh the local PowerSync cache and prove freshness before retrying the read.',
201
+ };
202
+ }
203
+ if (normalized === 'UPLOAD_PENDING') {
204
+ return {
205
+ command: 'dino doctor --fix --format json',
206
+ reason: 'Drain pending uploads without discarding queued mutations, then recheck sync health.',
207
+ };
208
+ }
209
+ if (normalized.startsWith('DAEMON_') || normalized === 'DAEMON_ERROR') {
210
+ return {
211
+ command: 'dino doctor --fix --format json',
212
+ reason: 'Repair local daemon state and verify version/account freshness.',
213
+ };
214
+ }
215
+ if (normalized === 'USAGE_ERROR' || normalized === 'INVALID_ARGUMENT') {
216
+ return {
217
+ command: 'dino schema --format json',
218
+ reason: 'Inspect valid command arguments and flags before retrying.',
219
+ };
220
+ }
221
+ if (normalized === 'NOT_FOUND' || normalized.endsWith('_NOT_FOUND')) {
222
+ return defaultNotFoundAction(message);
223
+ }
224
+ return {
225
+ command: 'dino doctor --format json',
226
+ reason: 'Inspect auth, sync, index, daemon, and database health before retrying.',
227
+ };
228
+ }
229
+ function defaultNotFoundAction(message) {
230
+ if (/\bnote\b/i.test(message)) {
231
+ return {
232
+ command: 'dino note search --format json',
233
+ reason: 'Search notes to find a valid note id before retrying.',
234
+ };
235
+ }
236
+ if (/\btag\b/i.test(message)) {
237
+ return {
238
+ command: 'dino tag list --format json',
239
+ reason: 'List existing tags before retrying.',
240
+ };
241
+ }
242
+ if (/\b(box|zettel)\b/i.test(message)) {
243
+ return {
244
+ command: 'dino box list --format json',
245
+ reason: 'List existing zettel boxes before retrying.',
246
+ };
247
+ }
248
+ if (/\bstorage\b/i.test(message)) {
249
+ return {
250
+ command: 'dino storage list --format json',
251
+ reason: 'List available storage configs before retrying.',
252
+ };
253
+ }
254
+ if (/\bprompt\b/i.test(message)) {
255
+ return {
256
+ command: 'dino prompt list --format json',
257
+ reason: 'List existing prompts before retrying.',
258
+ };
259
+ }
260
+ return {
261
+ command: 'dino schema --format json',
262
+ reason: 'Inspect available command paths and retry with an existing resource.',
263
+ };
264
+ }
@@ -4,6 +4,7 @@ export declare function toYaml(value: unknown): string;
4
4
  export declare function toJson(value: unknown): string;
5
5
  export declare function printHumanReadableLine(text: string): void;
6
6
  export declare function printHumanReadableBlock(text: string): void;
7
+ export declare function printHumanReadableWarning(text: string): void;
7
8
  export declare function printHumanReadableError(text: string): void;
8
9
  export declare function resolveStructuredOutputFormat(input: {
9
10
  format?: unknown;
@@ -1,4 +1,5 @@
1
1
  import { stringify as stringifyYaml } from 'yaml';
2
+ import { DinoxError } from './errors.js';
2
3
  import { sanitizeForTerminal } from './terminalSanitization.js';
3
4
  let requestedStructuredOutputFormat = null;
4
5
  let pendingStructuredNoticeProvider = null;
@@ -23,49 +24,72 @@ export function printHumanReadableLine(text) {
23
24
  export function printHumanReadableBlock(text) {
24
25
  writeToStdout(sanitizeForTerminal(text));
25
26
  }
27
+ export function printHumanReadableWarning(text) {
28
+ writeToStderr(`${sanitizeForTerminal(text)}\n`);
29
+ }
26
30
  export function printHumanReadableError(text) {
27
31
  writeToStderr(`${sanitizeForTerminal(text)}\n`);
28
32
  }
29
33
  function normalizeStructuredOutputFormat(value) {
30
34
  if (typeof value !== 'string') {
31
- throw new Error('--format must be one of: yaml, json');
35
+ throw new DinoxError('--format must be one of: yaml, json');
32
36
  }
33
37
  const normalized = value.trim().toLowerCase();
34
38
  if (normalized === 'yaml' || normalized === 'json') {
35
39
  return normalized;
36
40
  }
37
- throw new Error('--format must be one of: yaml, json');
41
+ throw new DinoxError('--format must be one of: yaml, json');
38
42
  }
39
43
  export function resolveStructuredOutputFormat(input) {
40
- if (input.format !== undefined && input.format !== null && input.format !== '') {
44
+ const hasFormat = input.format !== undefined && input.format !== null && input.format !== '';
45
+ const hasLegacyJson = Boolean(input.json);
46
+ if (hasFormat && hasLegacyJson) {
47
+ throw new DinoxError('--json cannot be used together with --format');
48
+ }
49
+ if (hasFormat) {
41
50
  return normalizeStructuredOutputFormat(input.format);
42
51
  }
43
- if (Boolean(input.json)) {
52
+ if (hasLegacyJson) {
44
53
  return 'yaml';
45
54
  }
46
55
  return null;
47
56
  }
48
57
  export function parseStructuredOutputFormatFromArgv(argv) {
49
- let format = null;
58
+ let explicitFormat = null;
59
+ let hasLegacyJson = false;
50
60
  for (let index = 0; index < argv.length; index += 1) {
51
61
  const arg = argv[index];
52
62
  if (!arg) {
53
63
  continue;
54
64
  }
65
+ if (arg === '--') {
66
+ break;
67
+ }
55
68
  if (arg === '--json') {
56
- format = 'yaml';
69
+ hasLegacyJson = true;
57
70
  continue;
58
71
  }
59
72
  if (arg === '--format') {
60
- format = normalizeStructuredOutputFormat(argv[index + 1]);
73
+ const nextFormat = normalizeStructuredOutputFormat(argv[index + 1]);
74
+ if (explicitFormat && explicitFormat !== nextFormat) {
75
+ throw new DinoxError('--format cannot be specified with conflicting values');
76
+ }
77
+ explicitFormat = nextFormat;
61
78
  index += 1;
62
79
  continue;
63
80
  }
64
81
  if (arg.startsWith('--format=')) {
65
- format = normalizeStructuredOutputFormat(arg.slice('--format='.length));
82
+ const nextFormat = normalizeStructuredOutputFormat(arg.slice('--format='.length));
83
+ if (explicitFormat && explicitFormat !== nextFormat) {
84
+ throw new DinoxError('--format cannot be specified with conflicting values');
85
+ }
86
+ explicitFormat = nextFormat;
66
87
  }
67
88
  }
68
- return format;
89
+ if (hasLegacyJson && explicitFormat) {
90
+ throw new DinoxError('--json cannot be used together with --format');
91
+ }
92
+ return explicitFormat ?? (hasLegacyJson ? 'yaml' : null);
69
93
  }
70
94
  export function setRequestedStructuredOutputFormat(format) {
71
95
  requestedStructuredOutputFormat = format;
@@ -117,11 +141,6 @@ function deriveStructuredNotice(value) {
117
141
  message: 'Result may be stale because the local cache did not fully synchronize.',
118
142
  };
119
143
  }
120
- if (record.uploadEnabled === false) {
121
- notice.upload_disabled = {
122
- message: 'Local changes cannot upload because powersync.uploadBaseUrl is unset.',
123
- };
124
- }
125
144
  return Object.keys(notice).length > 0 ? notice : null;
126
145
  }
127
146
  function mergeStructuredNotices(base, derived) {
@@ -131,6 +150,19 @@ function mergeStructuredNotices(base, derived) {
131
150
  };
132
151
  return Object.keys(merged).length > 0 ? merged : undefined;
133
152
  }
153
+ function summarizeStructuredError(value) {
154
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
155
+ return {};
156
+ }
157
+ const record = value;
158
+ const summary = {};
159
+ for (const key of ['code', 'message', 'recoverable', 'exit_code', 'suggested_action']) {
160
+ if (record[key] !== undefined) {
161
+ summary[key] = record[key];
162
+ }
163
+ }
164
+ return summary;
165
+ }
134
166
  export function printSuccess(value, format = getRequestedStructuredOutputFormat()) {
135
167
  if (format === 'json') {
136
168
  const data = normalizeSuccessEnvelopeData(value);
@@ -149,14 +181,18 @@ export function printError(value, format = getRequestedStructuredOutputFormat())
149
181
  const notice = getPendingStructuredNotice();
150
182
  writeToStderr(toJson({
151
183
  ok: false,
184
+ ...summarizeStructuredError(value),
152
185
  error: value,
153
186
  ...(notice ? { _notice: notice } : {}),
154
187
  }));
155
188
  return;
156
189
  }
157
- writeToStdout(toYaml({
190
+ const notice = getPendingStructuredNotice();
191
+ writeToStderr(toYaml({
158
192
  ok: false,
193
+ ...summarizeStructuredError(value),
159
194
  error: value,
195
+ ...(notice ? { _notice: notice } : {}),
160
196
  }));
161
197
  }
162
198
  export function printYaml(value) {
@@ -6,5 +6,5 @@ export function redactAuthorization(authorization) {
6
6
  if (trimmed.length <= 12) {
7
7
  return '***';
8
8
  }
9
- return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
9
+ return `${trimmed.slice(0, 4)}...${trimmed.slice(-2)}`;
10
10
  }
@@ -0,0 +1,8 @@
1
+ export type SqliteLeaseOptions = {
2
+ lockPath: string;
3
+ timeoutMs: number;
4
+ maxWaitMs?: number;
5
+ pollIntervalMs?: number;
6
+ createBusyError?: () => Error;
7
+ };
8
+ export declare function acquireExclusiveSqliteLease(options: SqliteLeaseOptions): Promise<() => Promise<void>>;
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ function isSqliteBusyError(error) {
5
+ if (!error || typeof error !== 'object' || !('code' in error)) {
6
+ return false;
7
+ }
8
+ const code = String(error.code ?? '');
9
+ return code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED';
10
+ }
11
+ export async function acquireExclusiveSqliteLease(options) {
12
+ const requestedTimeout = Number.isFinite(options.timeoutMs)
13
+ ? Math.max(1, options.timeoutMs)
14
+ : 1;
15
+ const maxWaitMs = Number.isFinite(options.maxWaitMs)
16
+ ? Math.max(1, options.maxWaitMs)
17
+ : requestedTimeout;
18
+ const waitMs = Math.min(requestedTimeout, maxWaitMs);
19
+ const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
20
+ ? Math.max(1, options.pollIntervalMs)
21
+ : 50;
22
+ const deadline = Date.now() + waitMs;
23
+ await fs.mkdir(path.dirname(options.lockPath), { recursive: true, mode: 0o700 });
24
+ while (Date.now() <= deadline) {
25
+ let leaseDb = null;
26
+ try {
27
+ leaseDb = new Database(options.lockPath);
28
+ leaseDb.pragma('busy_timeout = 0');
29
+ leaseDb.exec('BEGIN EXCLUSIVE');
30
+ await fs.chmod(options.lockPath, 0o600).catch(() => undefined);
31
+ return async () => {
32
+ const current = leaseDb;
33
+ if (!current) {
34
+ return;
35
+ }
36
+ try {
37
+ if (current.inTransaction) {
38
+ current.exec('ROLLBACK');
39
+ }
40
+ current.close();
41
+ leaseDb = null;
42
+ }
43
+ catch (error) {
44
+ // Keep the handle reachable so a caller can retry instead of silently
45
+ // dropping an owner lease whose SQLite connection may still be open.
46
+ throw error;
47
+ }
48
+ };
49
+ }
50
+ catch (error) {
51
+ leaseDb?.close();
52
+ if (!isSqliteBusyError(error)) {
53
+ throw error;
54
+ }
55
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
56
+ }
57
+ }
58
+ throw options.createBusyError?.() ?? new Error(`Timed out waiting for SQLite lease: ${options.lockPath}`);
59
+ }
@@ -1 +1,7 @@
1
- export declare function parseSyncTimeoutMs(value: unknown): number | undefined;
1
+ export declare const MAX_SYNC_TIMEOUT_MS = 2147483647;
2
+ type ParseSyncTimeoutOptions = {
3
+ invalid?: 'throw' | 'undefined';
4
+ label?: string;
5
+ };
6
+ export declare function parseSyncTimeoutMs(value: unknown, options?: ParseSyncTimeoutOptions): number | undefined;
7
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { DinoxError } from './errors.js';
2
- export function parseSyncTimeoutMs(value) {
2
+ export const MAX_SYNC_TIMEOUT_MS = 2_147_483_647;
3
+ export function parseSyncTimeoutMs(value, options = {}) {
3
4
  if (value === undefined || value === null) {
4
5
  return undefined;
5
6
  }
@@ -7,13 +8,13 @@ export function parseSyncTimeoutMs(value) {
7
8
  if (!text) {
8
9
  return undefined;
9
10
  }
10
- // Commander options are typically strings; be strict and fail fast for non-integers.
11
- if (!/^\d+$/.test(text)) {
12
- throw new DinoxError('--sync-timeout must be a positive integer');
13
- }
14
11
  const parsed = Number(text);
15
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
16
- throw new DinoxError('--sync-timeout must be a positive integer');
12
+ if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed <= 0 || parsed > MAX_SYNC_TIMEOUT_MS) {
13
+ if (options.invalid === 'undefined') {
14
+ return undefined;
15
+ }
16
+ const label = options.label ?? '--sync-timeout';
17
+ throw new DinoxError(`${label} must be a positive integer no greater than ${MAX_SYNC_TIMEOUT_MS}`);
17
18
  }
18
19
  return parsed;
19
20
  }
@@ -1,6 +1,10 @@
1
- const ANSI_ESCAPE_RE = /\x1b\[[0-9;?>=!]*[a-zA-Z]|\x1b\][^\x07]*\x07/g;
1
+ const ANSI_ESCAPE_RE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\](?:(?!\x07|\x1b\\)[\s\S]){0,2048}(?:\x07|\x1b\\)/g;
2
2
  export function isDangerousUnicode(rune) {
3
3
  return ((rune >= 0x200b && rune <= 0x200d) ||
4
+ rune === 0x061c ||
5
+ rune === 0x200e ||
6
+ rune === 0x200f ||
7
+ rune === 0x2060 ||
4
8
  rune === 0xfeff ||
5
9
  (rune >= 0x202a && rune <= 0x202e) ||
6
10
  (rune >= 0x2028 && rune <= 0x2029) ||
@@ -21,7 +25,7 @@ export function sanitizeForTerminal(text) {
21
25
  result += char;
22
26
  continue;
23
27
  }
24
- if (code < 0x20 || code === 0x7f) {
28
+ if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
25
29
  continue;
26
30
  }
27
31
  if (isDangerousUnicode(code)) {
@@ -2,6 +2,7 @@ import { DinoxError } from './errors.js';
2
2
  import { generateId } from './id.js';
3
3
  import { createAudioNodeFromLink, createFileNodeFromLink, createImageNodeWithMetadata, createVideoNodeFromLink, getDinoxMarkdownManager, } from './tiptapMarkdownSchema.js';
4
4
  import { inferMimeTypeFromHref, normalizeDinoxImageAttrs, normalizeFileBlockAttrs, normalizeFrontmatterRaw, parseDinoxMarkdownComment, } from './tiptapMarkdownSchemaUtils.js';
5
+ import { findMarkdownCodeRanges, findMarkdownLiteralRanges, isMarkdownOffsetInSortedRanges, } from './tiptapMarkdownSyntax.js';
5
6
  const CONTENT_PREVIEW_MAX_LENGTH = 400;
6
7
  const MEDIA_COMMENT_PATTERN = /<!--\s*({[\s\S]*?})\s*-->/g;
7
8
  const MEDIA_COMMENT_PLACEHOLDER_PREFIX = 'DINOX_MEDIA_META_';
@@ -124,7 +125,11 @@ function validateAndCleanDocument(doc) {
124
125
  function preprocessMarkdownMediaComments(markdown) {
125
126
  const metadataByPlaceholder = new Map();
126
127
  let counter = 0;
127
- const nextMarkdown = markdown.replace(MEDIA_COMMENT_PATTERN, (rawComment) => {
128
+ const codeRanges = findMarkdownCodeRanges(markdown);
129
+ const nextMarkdown = markdown.replace(MEDIA_COMMENT_PATTERN, (rawComment, _json, offset) => {
130
+ if (isMarkdownOffsetInSortedRanges(offset, codeRanges)) {
131
+ return rawComment;
132
+ }
128
133
  const parsed = parseDinoxMarkdownComment(rawComment);
129
134
  if (!parsed) {
130
135
  return rawComment;
@@ -138,6 +143,46 @@ function preprocessMarkdownMediaComments(markdown) {
138
143
  metadataByPlaceholder,
139
144
  };
140
145
  }
146
+ function preprocessMarkdownEscapes(markdown) {
147
+ const protectedRanges = findMarkdownLiteralRanges(markdown);
148
+ const placeholderPrefix = `DINOX_ESC_${generateId().replace(/-/g, '')}_`;
149
+ const replacements = new Map();
150
+ let output = '';
151
+ for (let index = 0; index < markdown.length;) {
152
+ const next = markdown[index + 1] ?? '';
153
+ if (markdown[index] === '\\' &&
154
+ /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(next) &&
155
+ !isMarkdownOffsetInSortedRanges(index, protectedRanges)) {
156
+ const placeholder = `${placeholderPrefix}${replacements.size}__`;
157
+ replacements.set(placeholder, next);
158
+ output += placeholder;
159
+ index += 2;
160
+ continue;
161
+ }
162
+ output += markdown[index];
163
+ index += 1;
164
+ }
165
+ return { markdown: output, placeholderPrefix, replacements };
166
+ }
167
+ function restoreMarkdownEscapes(node, placeholderPrefix, replacements) {
168
+ if (replacements.size === 0) {
169
+ return node;
170
+ }
171
+ const placeholderPattern = new RegExp(`${placeholderPrefix}\\d+__`, 'g');
172
+ const restoreValue = (value) => {
173
+ if (typeof value === 'string') {
174
+ return value.replace(placeholderPattern, (placeholder) => replacements.get(placeholder) ?? placeholder);
175
+ }
176
+ if (Array.isArray(value)) {
177
+ return value.map(restoreValue);
178
+ }
179
+ if (value && typeof value === 'object') {
180
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, restoreValue(item)]));
181
+ }
182
+ return value;
183
+ };
184
+ return restoreValue(node);
185
+ }
141
186
  function extractStandaloneMarkdownLink(content) {
142
187
  if (!Array.isArray(content) || content.length !== 1) {
143
188
  return null;
@@ -308,9 +353,10 @@ export function convertMarkdownToNoteContent(markdown) {
308
353
  };
309
354
  }
310
355
  const preprocessed = preprocessMarkdownMediaComments(source);
311
- const parsed = getDinoxMarkdownManager().parse(preprocessed.markdown);
356
+ const escaped = preprocessMarkdownEscapes(preprocessed.markdown);
357
+ const parsed = getDinoxMarkdownManager().parse(escaped.markdown);
312
358
  const normalized = normalizeStandaloneMarkdownLinksToMediaBlocks(parsed, preprocessed.metadataByPlaceholder);
313
- const contentJson = validateAndCleanDocument(normalized);
359
+ const contentJson = validateAndCleanDocument(restoreMarkdownEscapes(normalized, escaped.placeholderPrefix, escaped.replacements));
314
360
  const contentText = normalizeContentText(getPreviewTextFromDocument(contentJson));
315
361
  return { contentJson, contentText };
316
362
  }
@@ -0,0 +1,9 @@
1
+ export type MarkdownSourceRange = {
2
+ start: number;
3
+ end: number;
4
+ };
5
+ export declare function escapeLiteralMarkdownText(value: string): string;
6
+ export declare function isMarkdownOffsetInRanges(offset: number, ranges: readonly MarkdownSourceRange[]): boolean;
7
+ export declare function isMarkdownOffsetInSortedRanges(offset: number, ranges: readonly MarkdownSourceRange[]): boolean;
8
+ export declare function findMarkdownCodeRanges(source: string): MarkdownSourceRange[];
9
+ export declare function findMarkdownLiteralRanges(source: string): MarkdownSourceRange[];