@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,5 +1,6 @@
1
1
  import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
2
  export type NoteTokenIndexResult = {
3
+ complete: boolean;
3
4
  scanned: number;
4
5
  reindexed: number;
5
6
  skipped: number;
@@ -10,8 +11,20 @@ export type NoteTokenIndexResult = {
10
11
  id: string;
11
12
  };
12
13
  };
14
+ export type NoteTokenIndexInspection = {
15
+ notes: number;
16
+ ftsRows: number;
17
+ metaRows: number;
18
+ missingRows: number;
19
+ extraRows: number;
20
+ drift: number;
21
+ ftsAvailable: boolean;
22
+ };
13
23
  export declare function syncNoteTokenIndex(db: AbstractPowerSyncDatabase, options?: {
14
24
  batchSize?: number;
15
25
  nowIso?: () => string;
16
26
  tokenize?: (text: string, limit?: number) => string[];
27
+ signal?: AbortSignal;
17
28
  }): Promise<NoteTokenIndexResult>;
29
+ export declare function inspectNoteTokenIndex(db: AbstractPowerSyncDatabase): Promise<NoteTokenIndexInspection>;
30
+ export declare function rebuildNoteTokenIndex(db: AbstractPowerSyncDatabase, options?: Parameters<typeof syncNoteTokenIndex>[1]): Promise<NoteTokenIndexResult>;
@@ -9,22 +9,27 @@ export async function syncNoteTokenIndex(db, options) {
9
9
  const batchSize = Math.max(1, Math.trunc(options?.batchSize ?? DEFAULT_BATCH_SIZE));
10
10
  const nowIso = options?.nowIso ?? nowIsoUtc;
11
11
  const tokenize = options?.tokenize ?? tokenizeWithJieba;
12
+ const signal = options?.signal;
13
+ if (signal?.aborted) {
14
+ return createIndexResult('', '', false);
15
+ }
12
16
  await ensureFtsTable(db);
17
+ if (signal?.aborted) {
18
+ return createIndexResult('', '', false);
19
+ }
13
20
  const state = await ensureIndexState(db, nowIso);
14
21
  let cursorUpdatedAt = state.cursor_updated_at ?? '';
15
22
  let cursorId = state.cursor_id ?? '';
16
- const result = {
17
- scanned: 0,
18
- reindexed: 0,
19
- skipped: 0,
20
- removed: 0,
21
- ftsRowsUpserted: 0,
22
- cursor: {
23
- updatedAt: cursorUpdatedAt,
24
- id: cursorId,
25
- },
26
- };
23
+ const result = createIndexResult(cursorUpdatedAt, cursorId, true);
24
+ if (signal?.aborted) {
25
+ result.complete = false;
26
+ return result;
27
+ }
27
28
  while (true) {
29
+ if (signal?.aborted) {
30
+ result.complete = false;
31
+ return result;
32
+ }
28
33
  const rows = await db.getAll(`
29
34
  SELECT
30
35
  id,
@@ -50,43 +55,194 @@ export async function syncNoteTokenIndex(db, options) {
50
55
  break;
51
56
  }
52
57
  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
- }
58
+ if (signal?.aborted) {
59
+ result.complete = false;
60
+ await updateIndexCursor(db, cursorUpdatedAt, cursorId, nowIso);
61
+ result.cursor = { updatedAt: cursorUpdatedAt, id: cursorId };
62
+ return result;
74
63
  }
64
+ const sortKey = await indexNoteRow(db, row, result, nowIso, tokenize);
75
65
  cursorUpdatedAt = sortKey;
76
66
  cursorId = row.id;
77
67
  }
78
- await db.execute(`
79
- UPDATE note_local_index_state
80
- SET cursor_updated_at = ?, cursor_id = ?, updated_at = ?
81
- WHERE id = ?
82
- `, [cursorUpdatedAt, cursorId, nowIso(), INDEX_STATE_ID]);
68
+ await updateIndexCursor(db, cursorUpdatedAt, cursorId, nowIso);
69
+ result.cursor = { updatedAt: cursorUpdatedAt, id: cursorId };
83
70
  }
71
+ result.complete = await reconcileSkippedRows(db, result, batchSize, nowIso, tokenize, signal);
84
72
  result.cursor = {
85
73
  updatedAt: cursorUpdatedAt,
86
74
  id: cursorId,
87
75
  };
88
76
  return result;
89
77
  }
78
+ function createIndexResult(cursorUpdatedAt, cursorId, complete) {
79
+ return {
80
+ complete,
81
+ scanned: 0,
82
+ reindexed: 0,
83
+ skipped: 0,
84
+ removed: 0,
85
+ ftsRowsUpserted: 0,
86
+ cursor: {
87
+ updatedAt: cursorUpdatedAt,
88
+ id: cursorId,
89
+ },
90
+ };
91
+ }
92
+ export async function inspectNoteTokenIndex(db) {
93
+ const notes = await countRows(db, `
94
+ SELECT COUNT(*) AS count
95
+ FROM c_note
96
+ WHERE is_del IS NULL OR is_del = 0
97
+ `);
98
+ const metaRows = await countRowsIfAvailable(db, `
99
+ SELECT COUNT(*) AS count
100
+ FROM note_local_index_meta
101
+ `);
102
+ const ftsRows = await countRowsIfAvailable(db, `
103
+ SELECT COUNT(DISTINCT note_id) AS count
104
+ FROM ${NOTE_LOCAL_FTS_TABLE}
105
+ `);
106
+ if (ftsRows === null) {
107
+ return {
108
+ notes,
109
+ ftsRows: 0,
110
+ metaRows: metaRows ?? 0,
111
+ missingRows: notes,
112
+ extraRows: 0,
113
+ drift: notes,
114
+ ftsAvailable: false,
115
+ };
116
+ }
117
+ const missingRows = await countRows(db, `
118
+ SELECT COUNT(*) AS count
119
+ FROM c_note n
120
+ LEFT JOIN ${NOTE_LOCAL_FTS_TABLE} f ON f.note_id = n.id
121
+ WHERE (n.is_del IS NULL OR n.is_del = 0)
122
+ AND f.note_id IS NULL
123
+ `);
124
+ const extraRows = await countRows(db, `
125
+ SELECT COUNT(DISTINCT f.note_id) AS count
126
+ FROM ${NOTE_LOCAL_FTS_TABLE} f
127
+ LEFT JOIN c_note n
128
+ ON n.id = f.note_id
129
+ AND (n.is_del IS NULL OR n.is_del = 0)
130
+ WHERE n.id IS NULL
131
+ `);
132
+ return {
133
+ notes,
134
+ ftsRows,
135
+ metaRows: metaRows ?? 0,
136
+ missingRows,
137
+ extraRows,
138
+ drift: missingRows + extraRows,
139
+ ftsAvailable: true,
140
+ };
141
+ }
142
+ export async function rebuildNoteTokenIndex(db, options) {
143
+ await ensureFtsTable(db);
144
+ await db.execute(`DELETE FROM ${NOTE_LOCAL_FTS_TABLE}`, []);
145
+ await db.execute('DELETE FROM note_local_index_meta', []);
146
+ await db.execute('DELETE FROM note_local_index_state WHERE id = ?', [INDEX_STATE_ID]);
147
+ return syncNoteTokenIndex(db, options);
148
+ }
149
+ async function indexNoteRow(db, row, result, nowIso, tokenize) {
150
+ result.scanned += 1;
151
+ const sortKey = noteSortKey(row);
152
+ const bodySource = selectIndexBodyText(row);
153
+ const sourceHash = createSourceHash(row, bodySource);
154
+ const deleted = isDeleted(row.is_del);
155
+ const meta = await db.getOptional(`
156
+ SELECT source_hash
157
+ FROM note_local_index_meta
158
+ WHERE id = ?
159
+ `, [row.id]);
160
+ if (meta?.source_hash === sourceHash) {
161
+ await db.execute(`
162
+ UPDATE note_local_index_meta
163
+ SET note_updated_at = ?, is_del = ?, indexed_at = ?
164
+ WHERE id = ?
165
+ `, [sortKey, deleted ? 1 : 0, nowIso(), row.id]);
166
+ result.skipped += 1;
167
+ return sortKey;
168
+ }
169
+ const changed = await reindexOneNoteFts(db, row, sourceHash, bodySource, nowIso, tokenize);
170
+ if (changed.upserted) {
171
+ result.reindexed += 1;
172
+ result.ftsRowsUpserted += 1;
173
+ }
174
+ else {
175
+ result.removed += 1;
176
+ }
177
+ return sortKey;
178
+ }
179
+ async function reconcileSkippedRows(db, result, batchSize, nowIso, tokenize, signal) {
180
+ while (true) {
181
+ if (signal?.aborted) {
182
+ return false;
183
+ }
184
+ const rows = await db.getAll(`
185
+ SELECT
186
+ n.id,
187
+ n.title,
188
+ n.content_md,
189
+ n.content_text,
190
+ n.tags,
191
+ n.created_at,
192
+ n.updated_at,
193
+ n.is_del,
194
+ n.version
195
+ FROM c_note n
196
+ LEFT JOIN note_local_index_meta m ON m.id = n.id
197
+ WHERE
198
+ m.id IS NULL
199
+ OR COALESCE(m.note_updated_at, '') <> COALESCE(n.updated_at, n.created_at, '')
200
+ OR COALESCE(m.is_del, 0) <> CASE WHEN n.is_del IS NOT NULL AND n.is_del <> 0 THEN 1 ELSE 0 END
201
+ ORDER BY COALESCE(n.updated_at, n.created_at, '') ASC, n.id ASC
202
+ LIMIT ?
203
+ `, [batchSize]);
204
+ if (rows.length === 0) {
205
+ break;
206
+ }
207
+ for (const row of rows) {
208
+ if (signal?.aborted) {
209
+ return false;
210
+ }
211
+ await indexNoteRow(db, row, result, nowIso, tokenize);
212
+ }
213
+ }
214
+ while (true) {
215
+ if (signal?.aborted) {
216
+ return false;
217
+ }
218
+ const rows = await db.getAll(`
219
+ SELECT m.id
220
+ FROM note_local_index_meta m
221
+ LEFT JOIN c_note n ON n.id = m.id
222
+ WHERE n.id IS NULL
223
+ LIMIT ?
224
+ `, [batchSize]);
225
+ if (rows.length === 0) {
226
+ break;
227
+ }
228
+ for (const row of rows) {
229
+ if (signal?.aborted) {
230
+ return false;
231
+ }
232
+ await db.execute(`DELETE FROM ${NOTE_LOCAL_FTS_TABLE} WHERE note_id = ?`, [row.id]);
233
+ await db.execute('DELETE FROM note_local_index_meta WHERE id = ?', [row.id]);
234
+ result.removed += 1;
235
+ }
236
+ }
237
+ return true;
238
+ }
239
+ async function updateIndexCursor(db, cursorUpdatedAt, cursorId, nowIso) {
240
+ await db.execute(`
241
+ UPDATE note_local_index_state
242
+ SET cursor_updated_at = ?, cursor_id = ?, updated_at = ?
243
+ WHERE id = ?
244
+ `, [cursorUpdatedAt, cursorId, nowIso(), INDEX_STATE_ID]);
245
+ }
90
246
  async function ensureFtsTable(db) {
91
247
  try {
92
248
  await db.execute(`
@@ -105,6 +261,29 @@ async function ensureFtsTable(db) {
105
261
  throw new Error(`Failed to initialize FTS5 table (${NOTE_LOCAL_FTS_TABLE}): ${message}`);
106
262
  }
107
263
  }
264
+ function isMissingLocalIndexTableError(error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ return message.toLowerCase().includes('no such table');
267
+ }
268
+ function normalizeCount(value) {
269
+ const parsed = Number(value ?? 0);
270
+ return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0;
271
+ }
272
+ async function countRows(db, sql, params = []) {
273
+ const row = await db.getOptional(sql, params);
274
+ return normalizeCount(row?.count);
275
+ }
276
+ async function countRowsIfAvailable(db, sql, params = []) {
277
+ try {
278
+ return await countRows(db, sql, params);
279
+ }
280
+ catch (error) {
281
+ if (isMissingLocalIndexTableError(error)) {
282
+ return null;
283
+ }
284
+ throw error;
285
+ }
286
+ }
108
287
  async function ensureIndexState(db, nowIso) {
109
288
  const state = await db.getOptional(`
110
289
  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;