@dinoxx/dinox-cli 1.0.22 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +29 -9
  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 +74 -15
  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.js +16 -12
  55. package/dist/commands/tags/content.js +9 -0
  56. package/dist/commands/tags/index.js +2 -6
  57. package/dist/commands/tags/operations.js +15 -10
  58. package/dist/commands/tags/repo.d.ts +2 -0
  59. package/dist/commands/tags/repo.js +18 -5
  60. package/dist/commands/tags/store.js +1 -1
  61. package/dist/commands/todo/contentModel.d.ts +4 -3
  62. package/dist/commands/todo/contentModel.js +78 -28
  63. package/dist/commands/todo/dryRun.d.ts +1 -0
  64. package/dist/commands/todo/dryRun.js +17 -13
  65. package/dist/commands/todo/index.d.ts +6 -0
  66. package/dist/commands/todo/index.js +89 -123
  67. package/dist/commands/todo/repo.d.ts +36 -1
  68. package/dist/commands/todo/repo.js +146 -37
  69. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  70. package/dist/commands/todo/taskIdentity.js +64 -0
  71. package/dist/commands/todo/taskModel.js +27 -17
  72. package/dist/commands/update.d.ts +13 -1
  73. package/dist/commands/update.js +135 -23
  74. package/dist/commands/writeDryRun.d.ts +3 -2
  75. package/dist/commands/writeDryRun.js +4 -3
  76. package/dist/commands/writeDurability.d.ts +5 -0
  77. package/dist/commands/writeDurability.js +22 -0
  78. package/dist/config/paths.js +6 -1
  79. package/dist/config/resolve.d.ts +2 -0
  80. package/dist/config/resolve.js +23 -9
  81. package/dist/config/store.d.ts +3 -0
  82. package/dist/config/store.js +61 -14
  83. package/dist/config/types.d.ts +4 -0
  84. package/dist/daemon/client.d.ts +26 -1
  85. package/dist/daemon/client.js +236 -12
  86. package/dist/daemon/ensure.js +74 -12
  87. package/dist/daemon/index.d.ts +42 -0
  88. package/dist/daemon/index.js +202 -52
  89. package/dist/daemon/lifecycle.d.ts +42 -0
  90. package/dist/daemon/lifecycle.js +115 -0
  91. package/dist/daemon/pid.d.ts +19 -6
  92. package/dist/daemon/pid.js +147 -13
  93. package/dist/daemon/route.d.ts +14 -0
  94. package/dist/daemon/route.js +40 -0
  95. package/dist/daemon/server.d.ts +11 -0
  96. package/dist/daemon/server.js +201 -50
  97. package/dist/daemon/types.d.ts +29 -1
  98. package/dist/daemon/types.js +1 -0
  99. package/dist/dinox.js +25 -1
  100. package/dist/powersync/connector.js +31 -16
  101. package/dist/powersync/exclusiveRuntime.d.ts +19 -0
  102. package/dist/powersync/exclusiveRuntime.js +42 -0
  103. package/dist/powersync/runtime.d.ts +24 -4
  104. package/dist/powersync/runtime.js +234 -36
  105. package/dist/powersync/syncGate.d.ts +3 -1
  106. package/dist/powersync/syncGate.js +7 -1
  107. package/dist/powersync/tokenIndex.d.ts +11 -0
  108. package/dist/powersync/tokenIndex.js +159 -22
  109. package/dist/powersync/uploader.js +107 -52
  110. package/dist/utils/abort.js +0 -4
  111. package/dist/utils/asyncRelease.d.ts +2 -0
  112. package/dist/utils/asyncRelease.js +25 -0
  113. package/dist/utils/bidlinkCanonicalization.js +3 -0
  114. package/dist/utils/dryRun.d.ts +1 -1
  115. package/dist/utils/errors.d.ts +15 -1
  116. package/dist/utils/errors.js +223 -5
  117. package/dist/utils/output.d.ts +1 -0
  118. package/dist/utils/output.js +51 -15
  119. package/dist/utils/redact.js +1 -1
  120. package/dist/utils/sqliteLease.d.ts +8 -0
  121. package/dist/utils/sqliteLease.js +59 -0
  122. package/dist/utils/syncTimeout.d.ts +7 -1
  123. package/dist/utils/syncTimeout.js +8 -7
  124. package/dist/utils/terminalSanitization.js +6 -2
  125. package/dist/utils/tiptapMarkdown.js +49 -3
  126. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  127. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  128. package/dist/utils/tokenize.js +24 -2
  129. package/dist/utils/updateNotice.js +60 -12
  130. package/package.json +12 -10
  131. package/dist/commands/mcp/auth.d.ts +0 -4
  132. package/dist/commands/mcp/auth.js +0 -75
  133. package/dist/commands/mcp/index.d.ts +0 -2
  134. package/dist/commands/mcp/index.js +0 -86
  135. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  136. package/dist/commands/mcp/noteContentTools.js +0 -390
  137. package/dist/commands/mcp/server.d.ts +0 -34
  138. package/dist/commands/mcp/server.js +0 -424
  139. package/dist/commands/mcp/tools.d.ts +0 -2
  140. package/dist/commands/mcp/tools.js +0 -32
  141. package/dist/commands/mcp/types.d.ts +0 -39
  142. package/dist/commands/mcp/types.js +0 -1
  143. package/dist/utils/noteContentEngine.d.ts +0 -333
  144. package/dist/utils/noteContentEngine.js +0 -740
@@ -50,28 +50,7 @@ export async function syncNoteTokenIndex(db, options) {
50
50
  break;
51
51
  }
52
52
  for (const row of rows) {
53
- result.scanned += 1;
54
- const sortKey = noteSortKey(row);
55
- const bodySource = selectIndexBodyText(row);
56
- const sourceHash = createSourceHash(row, bodySource);
57
- const meta = await db.getOptional(`
58
- SELECT source_hash
59
- FROM note_local_index_meta
60
- WHERE id = ?
61
- `, [row.id]);
62
- if (meta?.source_hash === sourceHash) {
63
- result.skipped += 1;
64
- }
65
- else {
66
- const changed = await reindexOneNoteFts(db, row, sourceHash, bodySource, nowIso, tokenize);
67
- if (changed.upserted) {
68
- result.reindexed += 1;
69
- result.ftsRowsUpserted += 1;
70
- }
71
- else {
72
- result.removed += 1;
73
- }
74
- }
53
+ const sortKey = await indexNoteRow(db, row, result, nowIso, tokenize);
75
54
  cursorUpdatedAt = sortKey;
76
55
  cursorId = row.id;
77
56
  }
@@ -81,12 +60,147 @@ export async function syncNoteTokenIndex(db, options) {
81
60
  WHERE id = ?
82
61
  `, [cursorUpdatedAt, cursorId, nowIso(), INDEX_STATE_ID]);
83
62
  }
63
+ await reconcileSkippedRows(db, result, batchSize, nowIso, tokenize);
84
64
  result.cursor = {
85
65
  updatedAt: cursorUpdatedAt,
86
66
  id: cursorId,
87
67
  };
88
68
  return result;
89
69
  }
70
+ export async function inspectNoteTokenIndex(db) {
71
+ const notes = await countRows(db, `
72
+ SELECT COUNT(*) AS count
73
+ FROM c_note
74
+ WHERE is_del IS NULL OR is_del = 0
75
+ `);
76
+ const metaRows = await countRowsIfAvailable(db, `
77
+ SELECT COUNT(*) AS count
78
+ FROM note_local_index_meta
79
+ `);
80
+ const ftsRows = await countRowsIfAvailable(db, `
81
+ SELECT COUNT(DISTINCT note_id) AS count
82
+ FROM ${NOTE_LOCAL_FTS_TABLE}
83
+ `);
84
+ if (ftsRows === null) {
85
+ return {
86
+ notes,
87
+ ftsRows: 0,
88
+ metaRows: metaRows ?? 0,
89
+ missingRows: notes,
90
+ extraRows: 0,
91
+ drift: notes,
92
+ ftsAvailable: false,
93
+ };
94
+ }
95
+ const missingRows = await countRows(db, `
96
+ SELECT COUNT(*) AS count
97
+ FROM c_note n
98
+ LEFT JOIN ${NOTE_LOCAL_FTS_TABLE} f ON f.note_id = n.id
99
+ WHERE (n.is_del IS NULL OR n.is_del = 0)
100
+ AND f.note_id IS NULL
101
+ `);
102
+ const extraRows = await countRows(db, `
103
+ SELECT COUNT(DISTINCT f.note_id) AS count
104
+ FROM ${NOTE_LOCAL_FTS_TABLE} f
105
+ LEFT JOIN c_note n
106
+ ON n.id = f.note_id
107
+ AND (n.is_del IS NULL OR n.is_del = 0)
108
+ WHERE n.id IS NULL
109
+ `);
110
+ return {
111
+ notes,
112
+ ftsRows,
113
+ metaRows: metaRows ?? 0,
114
+ missingRows,
115
+ extraRows,
116
+ drift: missingRows + extraRows,
117
+ ftsAvailable: true,
118
+ };
119
+ }
120
+ export async function rebuildNoteTokenIndex(db, options) {
121
+ await ensureFtsTable(db);
122
+ await db.execute(`DELETE FROM ${NOTE_LOCAL_FTS_TABLE}`, []);
123
+ await db.execute('DELETE FROM note_local_index_meta', []);
124
+ await db.execute('DELETE FROM note_local_index_state WHERE id = ?', [INDEX_STATE_ID]);
125
+ return syncNoteTokenIndex(db, options);
126
+ }
127
+ async function indexNoteRow(db, row, result, nowIso, tokenize) {
128
+ result.scanned += 1;
129
+ const sortKey = noteSortKey(row);
130
+ const bodySource = selectIndexBodyText(row);
131
+ const sourceHash = createSourceHash(row, bodySource);
132
+ const deleted = isDeleted(row.is_del);
133
+ const meta = await db.getOptional(`
134
+ SELECT source_hash
135
+ FROM note_local_index_meta
136
+ WHERE id = ?
137
+ `, [row.id]);
138
+ if (meta?.source_hash === sourceHash) {
139
+ await db.execute(`
140
+ UPDATE note_local_index_meta
141
+ SET note_updated_at = ?, is_del = ?, indexed_at = ?
142
+ WHERE id = ?
143
+ `, [sortKey, deleted ? 1 : 0, nowIso(), row.id]);
144
+ result.skipped += 1;
145
+ return sortKey;
146
+ }
147
+ const changed = await reindexOneNoteFts(db, row, sourceHash, bodySource, nowIso, tokenize);
148
+ if (changed.upserted) {
149
+ result.reindexed += 1;
150
+ result.ftsRowsUpserted += 1;
151
+ }
152
+ else {
153
+ result.removed += 1;
154
+ }
155
+ return sortKey;
156
+ }
157
+ async function reconcileSkippedRows(db, result, batchSize, nowIso, tokenize) {
158
+ while (true) {
159
+ const rows = await db.getAll(`
160
+ SELECT
161
+ n.id,
162
+ n.title,
163
+ n.content_md,
164
+ n.content_text,
165
+ n.tags,
166
+ n.created_at,
167
+ n.updated_at,
168
+ n.is_del,
169
+ n.version
170
+ FROM c_note n
171
+ LEFT JOIN note_local_index_meta m ON m.id = n.id
172
+ WHERE
173
+ m.id IS NULL
174
+ OR COALESCE(m.note_updated_at, '') <> COALESCE(n.updated_at, n.created_at, '')
175
+ OR COALESCE(m.is_del, 0) <> CASE WHEN n.is_del IS NOT NULL AND n.is_del <> 0 THEN 1 ELSE 0 END
176
+ ORDER BY COALESCE(n.updated_at, n.created_at, '') ASC, n.id ASC
177
+ LIMIT ?
178
+ `, [batchSize]);
179
+ if (rows.length === 0) {
180
+ break;
181
+ }
182
+ for (const row of rows) {
183
+ await indexNoteRow(db, row, result, nowIso, tokenize);
184
+ }
185
+ }
186
+ while (true) {
187
+ const rows = await db.getAll(`
188
+ SELECT m.id
189
+ FROM note_local_index_meta m
190
+ LEFT JOIN c_note n ON n.id = m.id
191
+ WHERE n.id IS NULL
192
+ LIMIT ?
193
+ `, [batchSize]);
194
+ if (rows.length === 0) {
195
+ break;
196
+ }
197
+ for (const row of rows) {
198
+ await db.execute(`DELETE FROM ${NOTE_LOCAL_FTS_TABLE} WHERE note_id = ?`, [row.id]);
199
+ await db.execute('DELETE FROM note_local_index_meta WHERE id = ?', [row.id]);
200
+ result.removed += 1;
201
+ }
202
+ }
203
+ }
90
204
  async function ensureFtsTable(db) {
91
205
  try {
92
206
  await db.execute(`
@@ -105,6 +219,29 @@ async function ensureFtsTable(db) {
105
219
  throw new Error(`Failed to initialize FTS5 table (${NOTE_LOCAL_FTS_TABLE}): ${message}`);
106
220
  }
107
221
  }
222
+ function isMissingLocalIndexTableError(error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ return message.toLowerCase().includes('no such table');
225
+ }
226
+ function normalizeCount(value) {
227
+ const parsed = Number(value ?? 0);
228
+ return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0;
229
+ }
230
+ async function countRows(db, sql, params = []) {
231
+ const row = await db.getOptional(sql, params);
232
+ return normalizeCount(row?.count);
233
+ }
234
+ async function countRowsIfAvailable(db, sql, params = []) {
235
+ try {
236
+ return await countRows(db, sql, params);
237
+ }
238
+ catch (error) {
239
+ if (isMissingLocalIndexTableError(error)) {
240
+ return null;
241
+ }
242
+ throw error;
243
+ }
244
+ }
108
245
  async function ensureIndexState(db, nowIso) {
109
246
  const state = await db.getOptional(`
110
247
  SELECT cursor_updated_at, cursor_id
@@ -1,16 +1,21 @@
1
1
  import { UpdateType } from '@powersync/common';
2
2
  import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
3
3
  import { toAuthorizationHeaderValue } from '../utils/authToken.js';
4
+ import { sanitizeForTerminal } from '../utils/terminalSanitization.js';
4
5
  const FATAL_RESPONSE_CODES = [/^22...$/, /^23...$/, /^42501$/];
6
+ const legacyUploadTargets = new Set();
5
7
  function shouldFallbackToLegacy(status) {
6
8
  return status === 404 || status === 405;
7
9
  }
8
10
  function normalizeBaseUrl(baseUrl) {
9
11
  return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
10
12
  }
13
+ function legacyUploadTargetKey(baseUrl, uploadV4Path) {
14
+ return `${normalizeBaseUrl(baseUrl)}${uploadV4Path}`;
15
+ }
11
16
  async function readErrorBody(response) {
12
17
  const contentType = response.headers.get('content-type') ?? '';
13
- const text = await response.text().catch(() => '');
18
+ const text = await response.text();
14
19
  if (contentType.includes('application/json')) {
15
20
  try {
16
21
  const json = JSON.parse(text);
@@ -43,17 +48,60 @@ function isFatalUploadError(error) {
43
48
  if (!error || typeof error !== 'object') {
44
49
  return false;
45
50
  }
46
- const code = error.code;
47
- return typeof code === 'string' && FATAL_RESPONSE_CODES.some(regex => regex.test(code));
51
+ const uploadError = error;
52
+ if (uploadError.fatal === true) {
53
+ return true;
54
+ }
55
+ return typeof uploadError.code === 'string'
56
+ && FATAL_RESPONSE_CODES.some(regex => regex.test(uploadError.code));
57
+ }
58
+ function createUnsupportedCrudOperationError(op) {
59
+ const error = new Error(`Unsupported PowerSync CRUD operation: ${String(op.op)} (${String(op.table)}:${String(op.id ?? '')})`);
60
+ error.name = 'PowerSyncUploadError';
61
+ error.code = 'UNSUPPORTED_CRUD_OPERATION';
62
+ error.fatal = true;
63
+ return error;
64
+ }
65
+ function summarizeCrudOperation(op) {
66
+ const opName = UpdateType[op.op] ?? String(op.op);
67
+ return {
68
+ id: String(op.id ?? ''),
69
+ op: opName,
70
+ table: String(op.table ?? ''),
71
+ };
72
+ }
73
+ function attachFatalContext(error, transaction, failedIndex) {
74
+ if (!isFatalUploadError(error)) {
75
+ return;
76
+ }
77
+ error.fatalContext = {
78
+ failedIndex,
79
+ operations: transaction.crud.map((op) => summarizeCrudOperation(op)),
80
+ };
81
+ }
82
+ function logFatalUploadFailure(error) {
83
+ const context = error.fatalContext;
84
+ if (!context) {
85
+ process.stderr.write(`${sanitizeForTerminal(`warning: fatal PowerSync upload error (${error.code ?? error.status ?? 'unknown'}); queued operations were retained.`)}\n`);
86
+ return;
87
+ }
88
+ const failedOperation = context.operations[context.failedIndex];
89
+ const failedOperationText = failedOperation
90
+ ? `${failedOperation.op}:${failedOperation.table}:${failedOperation.id || '(no-id)'}`
91
+ : `index ${context.failedIndex}`;
92
+ const succeededBeforeFailure = Math.min(context.failedIndex, context.operations.length);
93
+ const replayWarning = succeededBeforeFailure > 0
94
+ ? ` ${succeededBeforeFailure} earlier operation(s) already returned success and may be sent again on retry; the legacy upload endpoints do not provide a transaction-level acknowledgement.`
95
+ : '';
96
+ process.stderr.write(`${sanitizeForTerminal(`warning: fatal PowerSync upload error (${error.code ?? error.status ?? 'unknown'}) at ${failedOperationText}; PowerSync retained the entire ${context.operations.length}-operation transaction.${replayWarning}`)}\n`);
48
97
  }
49
98
  async function dispatchMutation(options) {
50
99
  const timeout = typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
51
100
  ? createTimeoutSignal(options.timeoutMs)
52
101
  : null;
53
102
  const requestSignal = mergeAbortSignals([options.signal, timeout?.signal]);
54
- let response;
55
103
  try {
56
- response = await fetch(options.url, {
104
+ const response = await fetch(options.url, {
57
105
  method: options.method,
58
106
  headers: {
59
107
  Authorization: toAuthorizationHeaderValue(options.authorization),
@@ -62,6 +110,19 @@ async function dispatchMutation(options) {
62
110
  body: JSON.stringify(options.payload),
63
111
  signal: requestSignal,
64
112
  });
113
+ if (response.ok) {
114
+ await response.arrayBuffer();
115
+ return;
116
+ }
117
+ const body = await readErrorBody(response);
118
+ throw createUploadError({
119
+ status: response.status,
120
+ method: options.method,
121
+ url: options.url,
122
+ code: body.code,
123
+ message: body.message,
124
+ text: body.text,
125
+ });
65
126
  }
66
127
  catch (error) {
67
128
  if (timeout?.signal.aborted && !options.signal?.aborted) {
@@ -75,57 +136,50 @@ async function dispatchMutation(options) {
75
136
  finally {
76
137
  timeout?.cancel();
77
138
  }
78
- if (response.ok) {
79
- return;
80
- }
81
- const body = await readErrorBody(response);
82
- throw createUploadError({
83
- status: response.status,
84
- method: options.method,
85
- url: options.url,
86
- code: body.code,
87
- message: body.message,
88
- text: body.text,
89
- });
90
139
  }
91
140
  async function uploadTransaction(args) {
92
141
  const baseUrl = normalizeBaseUrl(args.uploadBaseUrl);
93
142
  const primaryUrl = `${baseUrl}${args.uploadV4Path}`;
94
143
  const fallbackUrl = `${baseUrl}${args.uploadV2Path}`;
95
- for (const op of args.transaction.crud) {
144
+ const primaryKey = legacyUploadTargetKey(baseUrl, args.uploadV4Path);
145
+ const dispatchPutOrPatch = async (method, payload) => {
146
+ if (legacyUploadTargets.has(primaryKey)) {
147
+ await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
148
+ return;
149
+ }
150
+ try {
151
+ await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method, payload, timeoutMs: args.timeoutMs });
152
+ }
153
+ catch (error) {
154
+ if (shouldFallbackToLegacy(error?.status)) {
155
+ legacyUploadTargets.add(primaryKey);
156
+ await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
157
+ return;
158
+ }
159
+ throw error;
160
+ }
161
+ };
162
+ for (let index = 0; index < args.transaction.crud.length; index += 1) {
163
+ const op = args.transaction.crud[index];
96
164
  const record = { table: op.table, data: { ...(op.opData ?? {}), id: op.id } };
97
- switch (op.op) {
98
- case UpdateType.PUT:
99
- try {
100
- await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PUT', payload: record, timeoutMs: args.timeoutMs });
101
- }
102
- catch (error) {
103
- if (shouldFallbackToLegacy(error?.status)) {
104
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PUT', payload: record, timeoutMs: args.timeoutMs });
105
- }
106
- else {
107
- throw error;
108
- }
109
- }
110
- break;
111
- case UpdateType.PATCH:
112
- try {
113
- await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PATCH', payload: record, timeoutMs: args.timeoutMs });
114
- }
115
- catch (error) {
116
- if (shouldFallbackToLegacy(error?.status)) {
117
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PATCH', payload: record, timeoutMs: args.timeoutMs });
118
- }
119
- else {
120
- throw error;
121
- }
122
- }
123
- break;
124
- case UpdateType.DELETE:
125
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record, timeoutMs: args.timeoutMs });
126
- break;
127
- default:
128
- break;
165
+ try {
166
+ switch (op.op) {
167
+ case UpdateType.PUT:
168
+ await dispatchPutOrPatch('PUT', record);
169
+ break;
170
+ case UpdateType.PATCH:
171
+ await dispatchPutOrPatch('PATCH', record);
172
+ break;
173
+ case UpdateType.DELETE:
174
+ await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record, timeoutMs: args.timeoutMs });
175
+ break;
176
+ default:
177
+ throw createUnsupportedCrudOperationError(op);
178
+ }
179
+ }
180
+ catch (error) {
181
+ attachFatalContext(error, args.transaction, index);
182
+ throw error;
129
183
  }
130
184
  }
131
185
  }
@@ -148,8 +202,9 @@ export function createPowerSyncUploadHandler(options) {
148
202
  }
149
203
  catch (error) {
150
204
  if (isFatalUploadError(error)) {
151
- await transaction.complete();
152
- return;
205
+ // v4/v2 acknowledge individual operations, so completing here would discard
206
+ // the failed suffix; retaining the transaction can replay an accepted prefix.
207
+ logFatalUploadFailure(error);
153
208
  }
154
209
  throw error;
155
210
  }
@@ -1,10 +1,6 @@
1
1
  export function createTimeoutSignal(timeoutMs) {
2
2
  const controller = new AbortController();
3
3
  const timer = setTimeout(() => controller.abort(), timeoutMs);
4
- // Best-effort: don't keep the process alive solely for the timeout.
5
- if (typeof timer.unref === 'function') {
6
- timer.unref();
7
- }
8
4
  return {
9
5
  signal: controller.signal,
10
6
  cancel: () => clearTimeout(timer),
@@ -0,0 +1,2 @@
1
+ export type AsyncRelease = () => Promise<void>;
2
+ export declare function withAsyncRelease<T>(acquire: () => Promise<AsyncRelease>, run: () => Promise<T>): Promise<T>;
@@ -0,0 +1,25 @@
1
+ export async function withAsyncRelease(acquire, run) {
2
+ const release = await acquire();
3
+ let operationFailed = false;
4
+ let operationError;
5
+ let value;
6
+ try {
7
+ value = await run();
8
+ }
9
+ catch (error) {
10
+ operationFailed = true;
11
+ operationError = error;
12
+ }
13
+ try {
14
+ await release();
15
+ }
16
+ catch (error) {
17
+ if (!operationFailed) {
18
+ throw error;
19
+ }
20
+ }
21
+ if (operationFailed) {
22
+ throw operationError;
23
+ }
24
+ return value;
25
+ }
@@ -151,6 +151,9 @@ export async function canonicalizeBidlinksInMarkdown(db, markdown, userId) {
151
151
  if (!resolvedId) {
152
152
  continue;
153
153
  }
154
+ if (nextMarkdown.slice(occurrence.startOffset, occurrence.endOffset) !== occurrence.raw) {
155
+ continue;
156
+ }
154
157
  const replacement = `[[${escapeBidlinkValue(occurrence.label)}|${escapeBidlinkValue(resolvedId)}]]`;
155
158
  nextMarkdown =
156
159
  nextMarkdown.slice(0, occurrence.startOffset) +
@@ -2,7 +2,7 @@ export type DryRunPreview = {
2
2
  dry_run: true;
3
3
  command: string;
4
4
  action: string;
5
- risk: 'write' | 'high-risk-write';
5
+ risk: 'read' | 'write' | 'high-risk-write';
6
6
  summary: string;
7
7
  warnings?: string[];
8
8
  inputs?: Record<string, unknown>;
@@ -4,24 +4,38 @@ export type DinoxErrorOptions = {
4
4
  details?: unknown;
5
5
  code?: string;
6
6
  suggestion?: string;
7
+ suggestedAction?: SuggestedAction;
7
8
  recoverable?: boolean;
8
9
  };
10
+ export type SuggestedAction = {
11
+ command: string;
12
+ reason: string;
13
+ };
9
14
  export type StructuredErrorPayload = {
10
15
  name: string;
11
16
  code: string;
12
17
  message: string;
13
18
  recoverable: boolean;
19
+ exit_code: number;
20
+ cause?: {
21
+ name: string;
22
+ message: string;
23
+ stack?: string;
24
+ };
14
25
  suggestion?: string;
26
+ suggested_action?: SuggestedAction;
15
27
  details?: unknown;
16
28
  stack?: string;
17
29
  };
18
30
  export declare class DinoxError extends Error {
19
31
  readonly exitCode: number;
20
32
  readonly details?: unknown;
21
- readonly code?: string;
33
+ readonly code: string;
22
34
  readonly suggestion?: string;
35
+ readonly suggestedAction?: SuggestedAction;
23
36
  readonly recoverable: boolean;
24
37
  constructor(message: string, options?: DinoxErrorOptions);
25
38
  }
26
39
  export declare function isDinoxError(error: unknown): error is DinoxError;
27
40
  export declare function buildStructuredErrorPayload(error: unknown, verbose: boolean): StructuredErrorPayload;
41
+ export declare function exitCodeForErrorCode(code: string): number;