@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,27 +1,239 @@
1
1
  import fs from 'node:fs/promises';
2
- import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, } from '../../auth/userInfo.js';
2
+ import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, persistedAuthStateMatches, rotatePersistedAuthStateRevision, } from '../../auth/userInfo.js';
3
3
  import { getLegacyPowerSyncDbPath, getPowerSyncDbPath } from '../../config/paths.js';
4
- import { loadConfig, saveConfig } from '../../config/store.js';
4
+ import { loadConfig, updateConfig } from '../../config/store.js';
5
5
  import { resolveConfig } from '../../config/resolve.js';
6
+ import { getDaemonInfo } from '../../daemon/client.js';
7
+ import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
8
+ import { stopDaemonLocked } from '../../daemon/index.js';
9
+ import { acquireDaemonStartLock } from '../../daemon/pid.js';
6
10
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
7
- import { connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
11
+ import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
12
+ import { acquirePowerSyncOwnerLease, closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, } from '../../powersync/runtime.js';
8
13
  import { runSingleSyncPass } from '../../powersync/syncPass.js';
9
14
  import { registerCommandGroup } from '../descriptor/register.js';
10
15
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
11
16
  import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
12
17
  import { DinoxError } from '../../utils/errors.js';
13
- import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
18
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableWarning, printSuccess, } from '../../utils/output.js';
14
19
  import { redactAuthorization } from '../../utils/redact.js';
20
+ import { withAsyncRelease } from '../../utils/asyncRelease.js';
15
21
  import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
16
22
  const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
17
23
  const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
24
+ const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
25
+ async function withDaemonStartLock(run) {
26
+ return withAsyncRelease(() => acquireDaemonStartLock(), run);
27
+ }
18
28
  async function removeSqliteArtifacts(dbPath) {
19
- for (const suffix of ['', '-wal', '-shm']) {
20
- await fs.rm(`${dbPath}${suffix}`, { force: true }).catch(() => undefined);
29
+ for (const suffix of ['', '-wal', '-shm', '-journal']) {
30
+ await fs.rm(`${dbPath}${suffix}`, { force: true });
31
+ }
32
+ }
33
+ function createPartialLogoutError(input) {
34
+ const original = input.failure.error instanceof DinoxError ? input.failure.error : null;
35
+ return new DinoxError('Saved credentials were cleared, but logout cleanup could not be completed safely.', {
36
+ code: original?.code ?? 'CACHE_CLEANUP_FAILED',
37
+ cause: input.failure.error,
38
+ details: {
39
+ persistedCredentialsCleared: true,
40
+ environmentTokenActive: input.environmentTokenActive,
41
+ cleanupPhase: input.failure.phase,
42
+ cachePaths: [...input.paths],
43
+ },
44
+ suggestion: original?.suggestion
45
+ ?? 'Stop other Dinox processes, then retry `dino auth logout --clear-local-db`.',
46
+ recoverable: true,
47
+ });
48
+ }
49
+ function createOwnerLeaseReleaseError(input) {
50
+ const cause = input.errors.length === 1
51
+ ? input.errors[0]
52
+ : new AggregateError(input.errors, 'Multiple PowerSync owner leases failed to release.');
53
+ return new DinoxError('Saved credentials were cleared, but local database ownership could not be released cleanly.', {
54
+ code: 'CACHE_CLEANUP_FAILED',
55
+ cause,
56
+ details: {
57
+ persistedCredentialsCleared: true,
58
+ environmentTokenActive: input.environmentTokenActive,
59
+ cleanupPhase: 'owner-release',
60
+ cachePaths: [...input.paths],
61
+ releaseErrorCount: input.errors.length,
62
+ },
63
+ suggestion: 'Exit other Dinox processes, then retry the command before opening the affected local database again.',
64
+ recoverable: true,
65
+ });
66
+ }
67
+ export async function releaseOwnerLeases(releases) {
68
+ const errors = [];
69
+ for (const release of [...releases].reverse()) {
70
+ try {
71
+ await release();
72
+ }
73
+ catch (error) {
74
+ errors.push(error);
75
+ }
21
76
  }
77
+ return errors;
78
+ }
79
+ async function readAuthTokenFromStdin() {
80
+ if (process.stdin.isTTY) {
81
+ throw new DinoxError('--token-stdin requires piped stdin', {
82
+ code: 'USAGE_ERROR',
83
+ suggestion: 'Pipe the token from a password manager or secret store into `dino auth login --token-stdin`.',
84
+ });
85
+ }
86
+ const chunks = [];
87
+ let totalBytes = 0;
88
+ for await (const chunk of process.stdin) {
89
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
90
+ totalBytes += buffer.length;
91
+ if (totalBytes > MAX_STDIN_TOKEN_BYTES) {
92
+ throw new DinoxError('Token from stdin is too large');
93
+ }
94
+ chunks.push(buffer);
95
+ }
96
+ return Buffer.concat(chunks).toString('utf8');
97
+ }
98
+ const authLoginDependencies = {
99
+ acquireDaemonStartLock,
100
+ loadConfig,
101
+ resolveConfig,
102
+ fetchCredentials: fetchCredentialsWithAuthorization,
103
+ fetchUserInfo: fetchUserInfoWithAuthorization,
104
+ suspendDaemon: suspendDaemonForExclusiveAccessLocked,
105
+ connectPowerSync,
106
+ runSingleSyncPass,
107
+ getStatusSnapshot,
108
+ closePowerSyncAndReleaseOwner,
109
+ updateConfig,
110
+ restoreDaemon: restoreSuspendedDaemonLocked,
111
+ printWarning: printHumanReadableWarning,
112
+ };
113
+ function authLoginErrorMessage(error) {
114
+ return error instanceof Error ? error.message : String(error);
115
+ }
116
+ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
117
+ const dependencies = { ...authLoginDependencies, ...overrides };
118
+ const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
119
+ let previousConfig = null;
120
+ let suspended = null;
121
+ let restoreAllowed = true;
122
+ let lifecycleLockReleaseError;
123
+ let outcome;
124
+ try {
125
+ previousConfig = await dependencies.loadConfig();
126
+ const previousResolved = dependencies.resolveConfig(previousConfig);
127
+ const pendingConfig = buildPendingAuthConfig(previousConfig, rawToken);
128
+ const resolved = dependencies.resolveConfig(pendingConfig);
129
+ const timeoutMs = parseSyncTimeoutMs(syncTimeout) ?? resolved.sync.timeoutMs;
130
+ await dependencies.fetchCredentials({
131
+ authorization: rawToken,
132
+ endpoint: resolved.powersync.endpoint,
133
+ credentialsUrl: resolved.powersync.tokenEndpoint,
134
+ timeoutMs,
135
+ });
136
+ const identity = await dependencies.fetchUserInfo({
137
+ authorization: rawToken,
138
+ config: resolved,
139
+ timeoutMs,
140
+ });
141
+ const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
142
+ const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
143
+ suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
144
+ const { db, dbPath, stale, releaseOwner } = await dependencies.connectPowerSync({
145
+ config: verifiedResolved,
146
+ offline: false,
147
+ timeoutMs,
148
+ });
149
+ let syncOutcome;
150
+ try {
151
+ await dependencies.runSingleSyncPass(db, timeoutMs);
152
+ syncOutcome = {
153
+ ok: true,
154
+ lastSyncedAt: dependencies.getStatusSnapshot(db).lastSyncedAt,
155
+ };
156
+ }
157
+ catch (error) {
158
+ syncOutcome = { ok: false, error };
159
+ }
160
+ let cleanupError;
161
+ try {
162
+ await dependencies.closePowerSyncAndReleaseOwner(db, releaseOwner);
163
+ }
164
+ catch (error) {
165
+ cleanupError = error;
166
+ restoreAllowed = false;
167
+ dependencies.printWarning(`warning: failed to close the login verification runtime safely: ${authLoginErrorMessage(error)}`);
168
+ }
169
+ if (!syncOutcome.ok) {
170
+ throw syncOutcome.error;
171
+ }
172
+ if (cleanupError) {
173
+ throw cleanupError;
174
+ }
175
+ await dependencies.updateConfig((current) => {
176
+ if (!persistedAuthStateMatches(current, previousConfig)) {
177
+ throw new DinoxError('Authentication changed while login was being verified.', {
178
+ code: 'AUTH_STATE_CHANGED',
179
+ suggestion: 'Retry `dino auth login` if you still want to replace the current login.',
180
+ recoverable: true,
181
+ });
182
+ }
183
+ return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
184
+ }, { timeoutMs });
185
+ outcome = {
186
+ ok: true,
187
+ value: {
188
+ ok: true,
189
+ verified: true,
190
+ endpoint: verifiedResolved.powersync.endpoint,
191
+ dbPath,
192
+ stale,
193
+ lastSyncedAt: syncOutcome.lastSyncedAt,
194
+ userId: identity.userId,
195
+ },
196
+ };
197
+ }
198
+ catch (error) {
199
+ outcome = { ok: false, error };
200
+ if (previousConfig && suspended && restoreAllowed) {
201
+ try {
202
+ const current = await dependencies.loadConfig();
203
+ if (persistedAuthStateMatches(current, previousConfig)) {
204
+ await dependencies.restoreDaemon(suspended);
205
+ }
206
+ }
207
+ catch (restoreError) {
208
+ dependencies.printWarning(`warning: failed to restore daemon after login failure: ${authLoginErrorMessage(restoreError)}`);
209
+ }
210
+ }
211
+ }
212
+ finally {
213
+ try {
214
+ await releaseStartLock();
215
+ }
216
+ catch (error) {
217
+ lifecycleLockReleaseError = error;
218
+ dependencies.printWarning(`warning: failed to release daemon lifecycle lock: ${authLoginErrorMessage(error)}`);
219
+ }
220
+ }
221
+ if (!outcome.ok) {
222
+ throw outcome.error;
223
+ }
224
+ if (lifecycleLockReleaseError) {
225
+ throw lifecycleLockReleaseError;
226
+ }
227
+ return outcome.value;
22
228
  }
23
229
  async function runAuthLogin(context) {
24
- const token = context.args.token;
230
+ const options = context.options;
231
+ if (context.args.token && options.tokenStdin) {
232
+ throw new DinoxError('Provide the token either as an argument or with --token-stdin, not both', {
233
+ code: 'USAGE_ERROR',
234
+ });
235
+ }
236
+ const token = options.tokenStdin ? await readAuthTokenFromStdin() : context.args.token;
25
237
  const rawToken = normalizeAuthToken(token);
26
238
  if (!rawToken) {
27
239
  throw new DinoxError('Token is required');
@@ -30,83 +242,172 @@ async function runAuthLogin(context) {
30
242
  if (envToken && envToken !== rawToken) {
31
243
  throw new DinoxError('DINOX_TOKEN is set and differs from the login token. Unset DINOX_TOKEN before running `dino auth login`, or use the same token.');
32
244
  }
33
- const previousConfig = await loadConfig();
34
- const pendingConfig = buildPendingAuthConfig(previousConfig, rawToken);
35
- const resolved = resolveConfig(pendingConfig);
36
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? resolved.sync.timeoutMs;
37
245
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
38
- await fetchCredentialsWithAuthorization({
39
- authorization: rawToken,
40
- endpoint: resolved.powersync.endpoint,
41
- credentialsUrl: resolved.powersync.tokenEndpoint,
42
- timeoutMs,
43
- });
44
- const identity = await fetchUserInfoWithAuthorization({
45
- authorization: rawToken,
46
- config: resolved,
47
- timeoutMs,
48
- });
49
- const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
50
- const verifiedResolved = resolveConfig(verifiedConfig);
51
- const { db, dbPath, stale } = await connectPowerSync({
52
- config: verifiedResolved,
53
- offline: false,
54
- timeoutMs,
55
- });
246
+ const result = await performAuthLogin(rawToken, context.globals.syncTimeout);
247
+ if (structuredOutput) {
248
+ printSuccess(result);
249
+ return;
250
+ }
251
+ printHumanReadableLine('Login verified.');
252
+ printHumanReadableLine(`- endpoint: ${result.endpoint}`);
253
+ printHumanReadableLine(`- db: ${result.dbPath}`);
254
+ printHumanReadableLine(`- userId: ${result.userId}`);
255
+ if (result.lastSyncedAt) {
256
+ printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
257
+ }
258
+ if (result.stale) {
259
+ printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
260
+ }
261
+ }
262
+ async function runAuthLogoutLocked(options) {
263
+ const config = await loadConfig();
264
+ const resolved = resolveConfig(config);
265
+ const envTokenActive = Boolean(readEnvAuthToken());
266
+ const daemonInfo = await getDaemonInfo();
267
+ let initiallyResolvedUserId = resolved.auth.userId;
268
+ if (!initiallyResolvedUserId
269
+ && daemonInfo?.authenticated
270
+ && daemonInfo.tokenFingerprint === (resolved.auth.tokenFingerprint ?? null)) {
271
+ initiallyResolvedUserId = daemonInfo.userId ?? undefined;
272
+ }
273
+ let daemonStopped = false;
274
+ let cleanupFailure = null;
56
275
  try {
57
- await runSingleSyncPass(db, timeoutMs);
58
- await saveConfig(verifiedConfig);
59
- const status = getStatusSnapshot(db);
60
- const result = {
61
- ok: true,
62
- verified: true,
63
- endpoint: verifiedResolved.powersync.endpoint,
64
- dbPath,
65
- stale,
66
- lastSyncedAt: status.lastSyncedAt,
67
- userId: identity.userId,
68
- };
69
- if (structuredOutput) {
70
- printSuccess(result);
71
- return;
276
+ daemonStopped = (await stopDaemonLocked()).stopped;
277
+ }
278
+ catch (error) {
279
+ cleanupFailure = { error, phase: 'daemon' };
280
+ }
281
+ let paths = new Set();
282
+ const releases = [];
283
+ let operationFailed = false;
284
+ let operationError;
285
+ let result = null;
286
+ try {
287
+ await updateConfig(async (current) => {
288
+ const currentResolved = resolveConfig(current);
289
+ let effectiveUserId = currentResolved.auth.userId;
290
+ if (!effectiveUserId
291
+ && daemonInfo?.authenticated
292
+ && daemonInfo.tokenFingerprint === (currentResolved.auth.tokenFingerprint ?? null)) {
293
+ effectiveUserId = daemonInfo.userId ?? undefined;
294
+ }
295
+ if (!effectiveUserId
296
+ && currentResolved.auth.tokenFingerprint === resolved.auth.tokenFingerprint) {
297
+ effectiveUserId = initiallyResolvedUserId;
298
+ }
299
+ if (options.clearLocalDb && currentResolved.auth.authorization && !effectiveUserId && !cleanupFailure) {
300
+ try {
301
+ const hydrated = await hydrateResolvedAuthIdentity(currentResolved, {
302
+ timeoutMs: currentResolved.sync.timeoutMs,
303
+ });
304
+ effectiveUserId = hydrated.config.auth.userId;
305
+ }
306
+ catch (error) {
307
+ cleanupFailure ??= { error, phase: 'identity' };
308
+ }
309
+ }
310
+ paths = options.clearLocalDb
311
+ ? new Set([
312
+ getPowerSyncDbPath(current.auth?.userId),
313
+ getPowerSyncDbPath(effectiveUserId),
314
+ getLegacyPowerSyncDbPath(),
315
+ ].sort())
316
+ : new Set();
317
+ if (!cleanupFailure) {
318
+ for (const dbPath of paths) {
319
+ try {
320
+ releases.push(await acquirePowerSyncOwnerLease(dbPath, currentResolved.sync.timeoutMs));
321
+ }
322
+ catch (error) {
323
+ cleanupFailure = { error, phase: 'owner-lease' };
324
+ break;
325
+ }
326
+ }
327
+ }
328
+ return rotatePersistedAuthStateRevision({
329
+ ...current,
330
+ auth: {
331
+ ...(current.auth ?? {}),
332
+ authorization: undefined,
333
+ userId: undefined,
334
+ username: undefined,
335
+ email: undefined,
336
+ userInfoSyncedAt: undefined,
337
+ },
338
+ });
339
+ }, { timeoutMs: resolved.sync.timeoutMs });
340
+ if (cleanupFailure) {
341
+ throw createPartialLogoutError({
342
+ failure: cleanupFailure,
343
+ environmentTokenActive: envTokenActive,
344
+ paths,
345
+ });
72
346
  }
73
- printHumanReadableLine('Login verified.');
74
- printHumanReadableLine(`- endpoint: ${result.endpoint}`);
75
- printHumanReadableLine(`- db: ${result.dbPath}`);
76
- printHumanReadableLine(`- userId: ${result.userId}`);
77
- if (result.lastSyncedAt) {
78
- printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
347
+ for (const dbPath of paths) {
348
+ try {
349
+ await removeSqliteArtifacts(dbPath);
350
+ }
351
+ catch (error) {
352
+ throw new DinoxError('Saved credentials were cleared, but the local cache could not be fully removed.', {
353
+ code: 'CACHE_CLEANUP_FAILED',
354
+ cause: error,
355
+ details: {
356
+ persistedCredentialsCleared: true,
357
+ environmentTokenActive: envTokenActive,
358
+ cachePath: dbPath,
359
+ },
360
+ suggestion: `Stop Dinox processes, inspect filesystem permissions, and remove the reported cache path: ${dbPath}`,
361
+ recoverable: true,
362
+ });
363
+ }
79
364
  }
80
- if (stale) {
81
- printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
365
+ result = {
366
+ ok: true,
367
+ loggedOut: !envTokenActive,
368
+ persistedCredentialsCleared: true,
369
+ environmentTokenActive: envTokenActive,
370
+ daemonStopped,
371
+ clearedLocalDb: Boolean(options.clearLocalDb),
372
+ };
373
+ }
374
+ catch (error) {
375
+ operationFailed = true;
376
+ operationError = error;
377
+ }
378
+ const releaseErrors = await releaseOwnerLeases(releases);
379
+ if (operationFailed) {
380
+ if (releaseErrors.length > 0) {
381
+ printHumanReadableWarning(`warning: logout also failed to release ${releaseErrors.length} local database owner lease(s); the primary error was preserved.`);
82
382
  }
383
+ throw operationError;
83
384
  }
84
- finally {
85
- await db.close().catch(() => undefined);
385
+ if (releaseErrors.length > 0) {
386
+ throw createOwnerLeaseReleaseError({
387
+ errors: releaseErrors,
388
+ environmentTokenActive: envTokenActive,
389
+ paths,
390
+ });
86
391
  }
392
+ return result;
87
393
  }
88
394
  async function runAuthLogout(context) {
89
395
  const options = context.options;
90
- const config = await loadConfig();
91
- const currentUserId = config.auth?.userId;
92
- if (config.auth) {
93
- config.auth.authorization = undefined;
94
- config.auth.userId = undefined;
95
- config.auth.username = undefined;
96
- config.auth.email = undefined;
97
- config.auth.userInfoSyncedAt = undefined;
98
- }
99
- await saveConfig(config);
100
- if (options.clearLocalDb) {
101
- await removeSqliteArtifacts(getPowerSyncDbPath(currentUserId));
102
- await removeSqliteArtifacts(getLegacyPowerSyncDbPath());
103
- }
396
+ const result = await withDaemonStartLock(() => runAuthLogoutLocked(options));
104
397
  if (getRequestedStructuredOutputFormat()) {
105
- printSuccess({
106
- ok: true,
107
- loggedOut: true,
108
- clearedLocalDb: Boolean(options.clearLocalDb),
109
- });
398
+ printSuccess(result);
399
+ return;
400
+ }
401
+ printHumanReadableLine('Saved login credentials cleared.');
402
+ printHumanReadableLine(`- daemonStopped: ${result.daemonStopped ? 'yes' : 'no'}`);
403
+ if (result.clearedLocalDb) {
404
+ printHumanReadableLine('- local cache: cleared');
405
+ }
406
+ if (result.environmentTokenActive) {
407
+ printHumanReadableLine('warning: DINOX_TOKEN is still set, so commands remain authenticated in this environment.');
408
+ }
409
+ else {
410
+ printHumanReadableLine('Logged out.');
110
411
  }
111
412
  }
112
413
  async function runAuthStatus(context) {
@@ -150,12 +451,12 @@ async function runAuthStatus(context) {
150
451
  printHumanReadableLine(`powersync.uploadBaseUrl: ${payload.powersync.uploadBaseUrl ?? '(unset)'}`);
151
452
  return;
152
453
  }
153
- const { db, stale } = await connectPowerSync({
454
+ await withExclusivePowerSync({
154
455
  config: resolved,
155
456
  offline: false,
156
457
  timeoutMs,
157
- });
158
- try {
458
+ expectedTokenFingerprint: resolved.auth.tokenFingerprint ?? null,
459
+ }, async ({ db, stale }) => {
159
460
  const status = getStatusSnapshot(db);
160
461
  const payload = { ...base, offline: false, stale, status };
161
462
  if (structuredOutput) {
@@ -175,10 +476,7 @@ async function runAuthStatus(context) {
175
476
  if (payload.stale) {
176
477
  printHumanReadableLine('warning: sync timed out; local cache may be stale');
177
478
  }
178
- }
179
- finally {
180
- await db.close().catch(() => undefined);
181
- }
479
+ });
182
480
  }
183
481
  export const authCommandGroup = {
184
482
  name: 'auth',
@@ -187,7 +485,8 @@ export const authCommandGroup = {
187
485
  {
188
486
  name: 'login',
189
487
  description: 'Save login token and verify PowerSync connectivity',
190
- arguments: [{ syntax: '<token>', description: 'Login token (without Bearer prefix)' }],
488
+ arguments: [{ syntax: '[token]', description: 'Login token (omit when using --token-stdin)' }],
489
+ options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
191
490
  schema: {
192
491
  path: 'auth.login',
193
492
  risk: 'write',
@@ -198,7 +497,7 @@ export const authCommandGroup = {
198
497
  summary: 'Login verification result',
199
498
  fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId'],
200
499
  },
201
- examples: ['dino auth login "<token>" --format json'],
500
+ examples: ['dino auth login --token-stdin --format json'],
202
501
  },
203
502
  action: runAuthLogin,
204
503
  },
@@ -214,7 +513,7 @@ export const authCommandGroup = {
214
513
  output: {
215
514
  type: 'structured',
216
515
  summary: 'Logout acknowledgement',
217
- fields: ['loggedOut', 'clearedLocalDb'],
516
+ fields: ['loggedOut', 'persistedCredentialsCleared', 'environmentTokenActive', 'daemonStopped', 'clearedLocalDb'],
218
517
  },
219
518
  examples: ['dino auth logout --format json', 'dino auth logout --clear-local-db --format json'],
220
519
  },
@@ -5,7 +5,7 @@ import { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } from '../writ
5
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
6
6
  import { DinoxError } from '../../utils/errors.js';
7
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
8
- import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
8
+ import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
9
9
  import { addBox, inspectAddBox, listBoxes } from './repo.js';
10
10
  import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
11
11
  const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -71,7 +71,6 @@ async function runBoxAdd(context) {
71
71
  description,
72
72
  color,
73
73
  });
74
- printUploadDisabledWarning(runtime);
75
74
  await waitForMutationUpload(runtime);
76
75
  const payload = { ok: true, stale: commandStale(runtime), ...result };
77
76
  if (structuredOutput) {
@@ -114,7 +113,6 @@ async function runBoxRename(context) {
114
113
  }));
115
114
  return;
116
115
  }
117
- printUploadDisabledWarning(runtime);
118
116
  await waitForMutationUpload(runtime);
119
117
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
120
118
  });
@@ -140,7 +138,6 @@ async function runBoxMove(context) {
140
138
  }));
141
139
  return;
142
140
  }
143
- printUploadDisabledWarning(runtime);
144
141
  await waitForMutationUpload(runtime);
145
142
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
146
143
  });
@@ -166,7 +163,6 @@ async function runBoxMerge(context) {
166
163
  }));
167
164
  return;
168
165
  }
169
- printUploadDisabledWarning(runtime);
170
166
  await waitForMutationUpload(runtime);
171
167
  printSuccess({ ok: true, stale: commandStale(runtime), ...result });
172
168
  });
@@ -182,7 +178,7 @@ async function runBoxCleanup(context) {
182
178
  offline: runtime.offline,
183
179
  command: 'dino box cleanup',
184
180
  action: 'cleanup_boxes',
185
- risk: 'write',
181
+ risk: 'read',
186
182
  summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
187
183
  inputs: {},
188
184
  targets: result.issues,
@@ -31,4 +31,7 @@ export declare function trimString(value: string | null | undefined): string;
31
31
  export declare function toNullableId(value: string | null | undefined): string | null;
32
32
  export declare function getLeafName(path: string): string;
33
33
  export declare function assertMaxLength(label: string, value: string, maxLength: number): void;
34
+ export declare function assertMaxUtf8ByteLength(label: string, value: string, maxBytes: number): void;
35
+ export declare function assertTextLimits(label: string, value: string, maxLength: number): void;
36
+ export declare function assertBoxPathLimits(path: string, label?: string): void;
34
37
  export declare function parseStoredBoxIds(raw: string | null): string[];
@@ -1,3 +1,4 @@
1
+ import { DinoxError } from '../../utils/errors.js';
1
2
  export const MAX_BOX_NAME_LENGTH = 500;
2
3
  export const MAX_BOX_PATH_LENGTH = 2000;
3
4
  export const MAX_BOX_DESCRIPTION_LENGTH = 200_000;
@@ -50,6 +51,21 @@ export function assertMaxLength(label, value, maxLength) {
50
51
  throw new DinoxError(`${label} must be at most ${maxLength} characters`);
51
52
  }
52
53
  }
54
+ export function assertMaxUtf8ByteLength(label, value, maxBytes) {
55
+ if (Buffer.byteLength(value, 'utf8') > maxBytes) {
56
+ throw new DinoxError(`${label} must be at most ${maxBytes} UTF-8 bytes`);
57
+ }
58
+ }
59
+ export function assertTextLimits(label, value, maxLength) {
60
+ assertMaxLength(label, value, maxLength);
61
+ assertMaxUtf8ByteLength(label, value, maxLength);
62
+ }
63
+ export function assertBoxPathLimits(path, label = 'Box path') {
64
+ assertTextLimits(label, path, MAX_BOX_PATH_LENGTH);
65
+ for (const segment of path.split('/')) {
66
+ assertTextLimits('Box path segment', segment, MAX_BOX_NAME_LENGTH);
67
+ }
68
+ }
53
69
  export function parseStoredBoxIds(raw) {
54
70
  if (!raw) {
55
71
  return [];
@@ -75,4 +91,3 @@ export function parseStoredBoxIds(raw) {
75
91
  return [];
76
92
  }
77
93
  }
78
- import { DinoxError } from '../../utils/errors.js';