@dinoxx/dinox-cli 1.0.14 → 1.0.16
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.
- package/dist/auth/userInfo.d.ts +1 -0
- package/dist/auth/userInfo.js +29 -10
- package/dist/cli.js +6 -0
- package/dist/commands/auth/index.js +16 -19
- package/dist/commands/boxes/index.js +1 -10
- package/dist/commands/graph/index.d.ts +2 -0
- package/dist/commands/graph/index.js +98 -0
- package/dist/commands/graph/repo.d.ts +33 -0
- package/dist/commands/graph/repo.js +76 -0
- package/dist/commands/mcp/auth.d.ts +4 -0
- package/dist/commands/mcp/auth.js +75 -0
- package/dist/commands/mcp/index.d.ts +2 -0
- package/dist/commands/mcp/index.js +84 -0
- package/dist/commands/mcp/server.d.ts +30 -0
- package/dist/commands/mcp/server.js +324 -0
- package/dist/commands/mcp/tools.d.ts +2 -0
- package/dist/commands/mcp/tools.js +90 -0
- package/dist/commands/mcp/types.d.ts +24 -0
- package/dist/commands/mcp/types.js +1 -0
- package/dist/commands/notes/index.d.ts +39 -0
- package/dist/commands/notes/index.js +61 -12
- package/dist/commands/prompt/index.js +1 -10
- package/dist/commands/sync.js +1 -11
- package/dist/commands/tags/index.js +1 -10
- package/dist/commands/todo/index.js +1 -10
- package/dist/config/resolve.js +3 -2
- package/dist/config/store.js +17 -2
- package/dist/config/types.d.ts +1 -2
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +86 -0
- package/dist/daemon/ensure.d.ts +1 -0
- package/dist/daemon/ensure.js +23 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +151 -0
- package/dist/daemon/pid.d.ts +10 -0
- package/dist/daemon/pid.js +55 -0
- package/dist/daemon/server.d.ts +10 -0
- package/dist/daemon/server.js +137 -0
- package/dist/daemon/types.d.ts +19 -0
- package/dist/daemon/types.js +1 -0
- package/dist/powersync/connector.d.ts +2 -1
- package/dist/powersync/connector.js +27 -9
- package/dist/powersync/runtime.js +4 -9
- package/dist/powersync/uploader.d.ts +1 -0
- package/dist/powersync/uploader.js +36 -13
- package/dist/utils/abort.d.ts +6 -0
- package/dist/utils/abort.js +23 -0
- package/dist/utils/authToken.d.ts +2 -0
- package/dist/utils/authToken.js +23 -0
- package/dist/utils/syncTimeout.d.ts +1 -0
- package/dist/utils/syncTimeout.js +19 -0
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
|
|
2
|
+
import { toAuthorizationHeaderValue } from '../utils/authToken.js';
|
|
1
3
|
/**
|
|
2
4
|
* Connector that fetches PowerSync credentials from your backend.
|
|
3
5
|
* This is injectable for testability.
|
|
@@ -23,7 +25,7 @@ export class PowerSyncConnector {
|
|
|
23
25
|
await this.uploader(database);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
|
-
export async function fetchCredentialsWithAuthorization({ authorization, endpoint, credentialsUrl, signal, }) {
|
|
28
|
+
export async function fetchCredentialsWithAuthorization({ authorization, endpoint, credentialsUrl, timeoutMs, signal, }) {
|
|
27
29
|
if (!authorization) {
|
|
28
30
|
throw new Error('PowerSync credential exchange requires an authorization token');
|
|
29
31
|
}
|
|
@@ -33,14 +35,30 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
|
|
|
33
35
|
if (!credentialsUrl) {
|
|
34
36
|
throw new Error('PowerSync credential exchange requires a token endpoint');
|
|
35
37
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const timeout = typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
39
|
+
? createTimeoutSignal(timeoutMs)
|
|
40
|
+
: null;
|
|
41
|
+
const requestSignal = mergeAbortSignals([signal, timeout?.signal]);
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(credentialsUrl, {
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: toAuthorizationHeaderValue(authorization),
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
signal: requestSignal,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (timeout?.signal.aborted && !signal?.aborted) {
|
|
55
|
+
throw new Error(`PowerSync credential exchange timed out after ${timeoutMs}ms`);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
timeout?.cancel();
|
|
61
|
+
}
|
|
44
62
|
if (!response.ok) {
|
|
45
63
|
const errorText = await response.text().catch(() => '');
|
|
46
64
|
throw new Error(`Received ${response.status} from ${credentialsUrl}: ${errorText}`);
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import { PowerSyncDatabase } from '@powersync/node';
|
|
3
3
|
import { getDataDir, getPowerSyncDbPath } from '../config/paths.js';
|
|
4
4
|
import { DinoxError } from '../utils/errors.js';
|
|
5
|
+
import { createTimeoutSignal } from '../utils/abort.js';
|
|
5
6
|
import { AppSchema } from './schema/index.js';
|
|
6
7
|
import { PowerSyncConnector, fetchCredentialsWithAuthorization } from './connector.js';
|
|
7
8
|
import { createPowerSyncUploadHandler } from './uploader.js';
|
|
@@ -10,14 +11,6 @@ async function ensureDataDir() {
|
|
|
10
11
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
11
12
|
return dir;
|
|
12
13
|
}
|
|
13
|
-
function createTimeoutSignal(timeoutMs) {
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
-
return {
|
|
17
|
-
signal: controller.signal,
|
|
18
|
-
cancel: () => clearTimeout(timer),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
14
|
export function getStatusSnapshot(db) {
|
|
22
15
|
const raw = db.currentStatus?.toJSON?.() ?? {};
|
|
23
16
|
const lastSyncedAt = raw.lastSyncedAt instanceof Date ? raw.lastSyncedAt.toISOString() : raw.lastSyncedAt ?? null;
|
|
@@ -55,7 +48,7 @@ export async function openPowerSyncDatabase() {
|
|
|
55
48
|
}
|
|
56
49
|
function assertConfigured(config) {
|
|
57
50
|
if (!config.auth.authorization) {
|
|
58
|
-
throw new DinoxError('Not logged in. Run: dino auth login <
|
|
51
|
+
throw new DinoxError('Not logged in. Run: dino auth login <token>');
|
|
59
52
|
}
|
|
60
53
|
}
|
|
61
54
|
export async function connectPowerSync(args) {
|
|
@@ -70,6 +63,7 @@ export async function connectPowerSync(args) {
|
|
|
70
63
|
uploadBaseUrl: args.config.powersync.uploadBaseUrl,
|
|
71
64
|
uploadV4Path: args.config.powersync.uploadV4Path,
|
|
72
65
|
uploadV2Path: args.config.powersync.uploadV2Path,
|
|
66
|
+
timeoutMs: args.timeoutMs,
|
|
73
67
|
})
|
|
74
68
|
: undefined;
|
|
75
69
|
const connector = new PowerSyncConnector(async () => {
|
|
@@ -77,6 +71,7 @@ export async function connectPowerSync(args) {
|
|
|
77
71
|
authorization: args.config.auth.authorization,
|
|
78
72
|
endpoint: args.config.powersync.endpoint,
|
|
79
73
|
credentialsUrl: args.config.powersync.tokenEndpoint,
|
|
74
|
+
timeoutMs: args.timeoutMs,
|
|
80
75
|
});
|
|
81
76
|
}, uploader);
|
|
82
77
|
await db.connect(connector);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { UpdateType } from '@powersync/common';
|
|
2
|
+
import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
|
|
3
|
+
import { toAuthorizationHeaderValue } from '../utils/authToken.js';
|
|
2
4
|
const FATAL_RESPONSE_CODES = [/^22...$/, /^23...$/, /^42501$/];
|
|
3
5
|
function shouldFallbackToLegacy(status) {
|
|
4
6
|
return status === 404 || status === 405;
|
|
@@ -45,14 +47,34 @@ function isFatalUploadError(error) {
|
|
|
45
47
|
return typeof code === 'string' && FATAL_RESPONSE_CODES.some(regex => regex.test(code));
|
|
46
48
|
}
|
|
47
49
|
async function dispatchMutation(options) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
const timeout = typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
51
|
+
? createTimeoutSignal(options.timeoutMs)
|
|
52
|
+
: null;
|
|
53
|
+
const requestSignal = mergeAbortSignals([options.signal, timeout?.signal]);
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = await fetch(options.url, {
|
|
57
|
+
method: options.method,
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: toAuthorizationHeaderValue(options.authorization),
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(options.payload),
|
|
63
|
+
signal: requestSignal,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (timeout?.signal.aborted && !options.signal?.aborted) {
|
|
68
|
+
const err = new Error(`PowerSync upload timed out (${options.method} ${options.url}) after ${options.timeoutMs}ms`);
|
|
69
|
+
err.name = 'PowerSyncUploadError';
|
|
70
|
+
err.status = 408;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
timeout?.cancel();
|
|
77
|
+
}
|
|
56
78
|
if (response.ok) {
|
|
57
79
|
return;
|
|
58
80
|
}
|
|
@@ -75,11 +97,11 @@ async function uploadTransaction(args) {
|
|
|
75
97
|
switch (op.op) {
|
|
76
98
|
case UpdateType.PUT:
|
|
77
99
|
try {
|
|
78
|
-
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PUT', payload: record });
|
|
100
|
+
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PUT', payload: record, timeoutMs: args.timeoutMs });
|
|
79
101
|
}
|
|
80
102
|
catch (error) {
|
|
81
103
|
if (shouldFallbackToLegacy(error?.status)) {
|
|
82
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PUT', payload: record });
|
|
104
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PUT', payload: record, timeoutMs: args.timeoutMs });
|
|
83
105
|
}
|
|
84
106
|
else {
|
|
85
107
|
throw error;
|
|
@@ -88,11 +110,11 @@ async function uploadTransaction(args) {
|
|
|
88
110
|
break;
|
|
89
111
|
case UpdateType.PATCH:
|
|
90
112
|
try {
|
|
91
|
-
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PATCH', payload: record });
|
|
113
|
+
await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method: 'PATCH', payload: record, timeoutMs: args.timeoutMs });
|
|
92
114
|
}
|
|
93
115
|
catch (error) {
|
|
94
116
|
if (shouldFallbackToLegacy(error?.status)) {
|
|
95
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PATCH', payload: record });
|
|
117
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'PATCH', payload: record, timeoutMs: args.timeoutMs });
|
|
96
118
|
}
|
|
97
119
|
else {
|
|
98
120
|
throw error;
|
|
@@ -100,7 +122,7 @@ async function uploadTransaction(args) {
|
|
|
100
122
|
}
|
|
101
123
|
break;
|
|
102
124
|
case UpdateType.DELETE:
|
|
103
|
-
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record });
|
|
125
|
+
await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record, timeoutMs: args.timeoutMs });
|
|
104
126
|
break;
|
|
105
127
|
default:
|
|
106
128
|
break;
|
|
@@ -120,6 +142,7 @@ export function createPowerSyncUploadHandler(options) {
|
|
|
120
142
|
uploadBaseUrl: options.uploadBaseUrl,
|
|
121
143
|
uploadV4Path: options.uploadV4Path,
|
|
122
144
|
uploadV2Path: options.uploadV2Path,
|
|
145
|
+
timeoutMs: options.timeoutMs,
|
|
123
146
|
});
|
|
124
147
|
await transaction.complete();
|
|
125
148
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type TimeoutSignal = {
|
|
2
|
+
signal: AbortSignal;
|
|
3
|
+
cancel: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function createTimeoutSignal(timeoutMs: number): TimeoutSignal;
|
|
6
|
+
export declare function mergeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createTimeoutSignal(timeoutMs) {
|
|
2
|
+
const controller = new AbortController();
|
|
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
|
+
return {
|
|
9
|
+
signal: controller.signal,
|
|
10
|
+
cancel: () => clearTimeout(timer),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function mergeAbortSignals(signals) {
|
|
14
|
+
const filtered = signals.filter(Boolean);
|
|
15
|
+
if (filtered.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (filtered.length === 1) {
|
|
19
|
+
return filtered[0];
|
|
20
|
+
}
|
|
21
|
+
// Node.js 20+ supports AbortSignal.any().
|
|
22
|
+
return AbortSignal.any(filtered);
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function normalizeAuthToken(value) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (trimmed.toLowerCase().startsWith('bearer ')) {
|
|
10
|
+
const withoutPrefix = trimmed.slice('bearer '.length).trim();
|
|
11
|
+
return withoutPrefix || undefined;
|
|
12
|
+
}
|
|
13
|
+
return trimmed;
|
|
14
|
+
}
|
|
15
|
+
export function toAuthorizationHeaderValue(token) {
|
|
16
|
+
// Backend expects a standard Authorization header. Be defensive: callers may pass
|
|
17
|
+
// a full "Bearer ..." header value or a raw token.
|
|
18
|
+
const normalized = normalizeAuthToken(token);
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
throw new Error('Missing auth token');
|
|
21
|
+
}
|
|
22
|
+
return `Bearer ${normalized}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseSyncTimeoutMs(value: unknown): number | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DinoxError } from './errors.js';
|
|
2
|
+
export function parseSyncTimeoutMs(value) {
|
|
3
|
+
if (value === undefined || value === null) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const text = typeof value === 'string' ? value.trim() : typeof value === 'number' ? String(value) : '';
|
|
7
|
+
if (!text) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
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
|
+
const parsed = Number(text);
|
|
15
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
16
|
+
throw new DinoxError('--sync-timeout must be a positive integer');
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|