@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
|
@@ -3,6 +3,7 @@ import { resolveConfig } from '../../config/resolve.js';
|
|
|
3
3
|
import { DinoxError } from '../../utils/errors.js';
|
|
4
4
|
import { readStringOrFile } from '../../utils/argValue.js';
|
|
5
5
|
import { printYaml } from '../../utils/output.js';
|
|
6
|
+
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
6
7
|
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
7
8
|
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
8
9
|
import { appendTodoTasks, createTodoNote, normalizeTaskTexts, searchTodoTasks, updateTodoTaskStatus, } from './repo.js';
|
|
@@ -12,16 +13,6 @@ const DEFAULT_TASK_LIMIT = 50;
|
|
|
12
13
|
const DEFAULT_NOTE_SCAN_LIMIT = 200;
|
|
13
14
|
const MAX_TASK_LIMIT = 500;
|
|
14
15
|
const MAX_NOTE_SCAN_LIMIT = 5000;
|
|
15
|
-
function parseSyncTimeoutMs(value) {
|
|
16
|
-
if (typeof value !== 'string') {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const parsed = Number(value);
|
|
20
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
21
|
-
throw new DinoxError('--sync-timeout must be a positive integer');
|
|
22
|
-
}
|
|
23
|
-
return Math.trunc(parsed);
|
|
24
|
-
}
|
|
25
16
|
function parseTaskStatus(value) {
|
|
26
17
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
27
18
|
if (normalized === 'all' || normalized === 'completed' || normalized === 'uncompleted') {
|
package/dist/config/resolve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { POWERSYNC_DEFAULT_ENDPOINT, POWERSYNC_DEFAULT_TOKEN_ENDPOINT, POWERSYNC_DEFAULT_UPLOAD_BASE_URL, } from './serviceEndpoints.js';
|
|
2
|
+
import { normalizeAuthToken } from '../utils/authToken.js';
|
|
2
3
|
const DEFAULT_UPLOAD_V4_PATH = '/powersync/v4/uploadData';
|
|
3
4
|
const DEFAULT_UPLOAD_V2_PATH = '/powersync/v2/uploadData';
|
|
4
5
|
const DEFAULT_SYNC_TIMEOUT_MS = 300_000;
|
|
@@ -20,7 +21,7 @@ function parsePositiveInt(value) {
|
|
|
20
21
|
return Math.trunc(parsed);
|
|
21
22
|
}
|
|
22
23
|
export function resolveConfig(raw) {
|
|
23
|
-
const fileAuth =
|
|
24
|
+
const fileAuth = normalizeAuthToken(raw.auth?.authorization);
|
|
24
25
|
const fileUserId = nonEmptyString(raw.auth?.userId);
|
|
25
26
|
const fileUsername = nonEmptyString(raw.auth?.username ?? undefined) ?? raw.auth?.username ?? undefined;
|
|
26
27
|
const fileEmail = nonEmptyString(raw.auth?.email ?? undefined) ?? raw.auth?.email ?? undefined;
|
|
@@ -29,7 +30,7 @@ export function resolveConfig(raw) {
|
|
|
29
30
|
const fileUploadV4Path = nonEmptyString(raw.powersync?.uploadV4Path);
|
|
30
31
|
const fileUploadV2Path = nonEmptyString(raw.powersync?.uploadV2Path);
|
|
31
32
|
const fileTimeoutMs = parsePositiveInt(raw.sync?.timeoutMs);
|
|
32
|
-
const envAuth =
|
|
33
|
+
const envAuth = normalizeAuthToken(process.env.DINOX_AUTHORIZATION);
|
|
33
34
|
const envTokenEndpoint = nonEmptyString(process.env.DINOX_POWERSYNC_TOKEN_ENDPOINT) ?? nonEmptyString(process.env.POWERSYNC_TOKEN_ENDPOINT);
|
|
34
35
|
const envUploadV4Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V4_PATH);
|
|
35
36
|
const envUploadV2Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V2_PATH);
|
package/dist/config/store.js
CHANGED
|
@@ -2,9 +2,24 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getConfigDir, getConfigFilePath } from './paths.js';
|
|
4
4
|
import { generateId } from '../utils/id.js';
|
|
5
|
+
import { DinoxError } from '../utils/errors.js';
|
|
5
6
|
function isPlainObject(value) {
|
|
6
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
8
|
}
|
|
9
|
+
const FORBIDDEN_KEY_PATH_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
10
|
+
function splitKeyPath(keyPath) {
|
|
11
|
+
const segments = keyPath
|
|
12
|
+
.split('.')
|
|
13
|
+
.map(segment => segment.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
for (const segment of segments) {
|
|
16
|
+
const normalized = segment.toLowerCase();
|
|
17
|
+
if (FORBIDDEN_KEY_PATH_SEGMENTS.has(normalized)) {
|
|
18
|
+
throw new DinoxError(`Invalid config key path: ${keyPath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return segments;
|
|
22
|
+
}
|
|
8
23
|
async function ensureConfigDir() {
|
|
9
24
|
const dir = getConfigDir();
|
|
10
25
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
@@ -47,7 +62,7 @@ export async function saveConfig(config) {
|
|
|
47
62
|
await fs.rename(tmpPath, filePath);
|
|
48
63
|
}
|
|
49
64
|
function setDeepValue(target, keyPath, value) {
|
|
50
|
-
const segments = keyPath
|
|
65
|
+
const segments = splitKeyPath(keyPath);
|
|
51
66
|
if (segments.length === 0) {
|
|
52
67
|
return;
|
|
53
68
|
}
|
|
@@ -63,7 +78,7 @@ function setDeepValue(target, keyPath, value) {
|
|
|
63
78
|
cursor[segments[segments.length - 1]] = value;
|
|
64
79
|
}
|
|
65
80
|
function getDeepValue(target, keyPath) {
|
|
66
|
-
const segments = keyPath
|
|
81
|
+
const segments = splitKeyPath(keyPath);
|
|
67
82
|
let cursor = target;
|
|
68
83
|
for (const segment of segments) {
|
|
69
84
|
if (!isPlainObject(cursor)) {
|
package/dist/config/types.d.ts
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
3
|
+
import { readPid } from './pid.js';
|
|
4
|
+
const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 2_000;
|
|
5
|
+
const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
export async function sendDaemonRequest(method, params) {
|
|
7
|
+
const pidInfo = readPid();
|
|
8
|
+
const port = pidInfo?.port || DEFAULT_DAEMON_PORT;
|
|
9
|
+
const request = {
|
|
10
|
+
id: Date.now().toString(),
|
|
11
|
+
method,
|
|
12
|
+
params,
|
|
13
|
+
};
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const finalize = (error, value) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
if (error) {
|
|
21
|
+
reject(error);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolve(value);
|
|
25
|
+
};
|
|
26
|
+
const data = JSON.stringify(request);
|
|
27
|
+
const options = {
|
|
28
|
+
hostname: '127.0.0.1',
|
|
29
|
+
port,
|
|
30
|
+
path: '/',
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Content-Length': Buffer.byteLength(data),
|
|
35
|
+
},
|
|
36
|
+
timeout: DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
|
|
37
|
+
};
|
|
38
|
+
const req = httpRequest(options, (res) => {
|
|
39
|
+
const statusCode = res.statusCode ?? 0;
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let totalBytes = 0;
|
|
42
|
+
res.on('data', (chunk) => {
|
|
43
|
+
totalBytes += chunk.length;
|
|
44
|
+
if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
|
|
45
|
+
req.destroy(new Error('Daemon response too large'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
});
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
53
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
54
|
+
finalize(new Error(`Daemon request failed (HTTP ${statusCode || 'unknown'}): ${body || '(empty)'}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const response = JSON.parse(body);
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
finalize(undefined, response.result);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
finalize(new Error(response.error || 'Unknown error'));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
finalize(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
req.on('timeout', () => {
|
|
71
|
+
req.destroy(new Error('Daemon request timed out'));
|
|
72
|
+
});
|
|
73
|
+
req.on('error', (error) => finalize(error));
|
|
74
|
+
req.write(data);
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function isDaemonRunning() {
|
|
79
|
+
try {
|
|
80
|
+
await sendDaemonRequest('ping');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureDaemonRunning(): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { isDaemonRunning } from './client.js';
|
|
3
|
+
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
4
|
+
export async function ensureDaemonRunning() {
|
|
5
|
+
if (await isDaemonRunning()) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
console.log('Starting daemon...');
|
|
9
|
+
const args = ['daemon', 'start', '--port', String(DEFAULT_DAEMON_PORT), '--no-detach'];
|
|
10
|
+
const child = spawn(process.argv[0], [process.argv[1], ...args], {
|
|
11
|
+
detached: true,
|
|
12
|
+
stdio: 'ignore',
|
|
13
|
+
});
|
|
14
|
+
child.unref();
|
|
15
|
+
for (let i = 0; i < 10; i++) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
17
|
+
if (await isDaemonRunning()) {
|
|
18
|
+
console.log('Daemon started');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Failed to start daemon');
|
|
23
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { DaemonServer } from './server.js';
|
|
3
|
+
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
4
|
+
import { readPid, isProcessRunning, removePid } from './pid.js';
|
|
5
|
+
import { isDaemonRunning } from './client.js';
|
|
6
|
+
import { getPackageVersion } from '../utils/version.js';
|
|
7
|
+
import { DinoxError } from '../utils/errors.js';
|
|
8
|
+
const DAEMON_STOP_POLL_INTERVAL_MS = 100;
|
|
9
|
+
const DAEMON_STOP_TIMEOUT_MS = 5_000;
|
|
10
|
+
function parsePort(value) {
|
|
11
|
+
if (value === undefined || value === null || value === '') {
|
|
12
|
+
return DEFAULT_DAEMON_PORT;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value !== 'string') {
|
|
15
|
+
throw new DinoxError('--port must be a valid port number');
|
|
16
|
+
}
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
19
|
+
throw new DinoxError('--port must be a valid port number');
|
|
20
|
+
}
|
|
21
|
+
const parsed = Number(trimmed);
|
|
22
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
23
|
+
throw new DinoxError('--port must be between 1 and 65535');
|
|
24
|
+
}
|
|
25
|
+
return Math.trunc(parsed);
|
|
26
|
+
}
|
|
27
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
while (Date.now() < deadline) {
|
|
30
|
+
if (!isProcessRunning(pid)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, DAEMON_STOP_POLL_INTERVAL_MS));
|
|
34
|
+
}
|
|
35
|
+
return !isProcessRunning(pid);
|
|
36
|
+
}
|
|
37
|
+
async function startDaemon(port, detached = true) {
|
|
38
|
+
if (await isDaemonRunning()) {
|
|
39
|
+
console.log('Daemon is already running');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (detached) {
|
|
43
|
+
const args = ['daemon', 'start', '--port', String(port), '--no-detach'];
|
|
44
|
+
const child = spawn(process.argv[0], [process.argv[1], ...args], {
|
|
45
|
+
detached: true,
|
|
46
|
+
stdio: 'ignore',
|
|
47
|
+
});
|
|
48
|
+
child.unref();
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
50
|
+
if (await isDaemonRunning()) {
|
|
51
|
+
console.log(`Daemon started on port ${port}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error('Failed to start daemon');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(`Starting daemon on port ${port}...`);
|
|
59
|
+
const server = new DaemonServer();
|
|
60
|
+
await server.initialize();
|
|
61
|
+
await server.start(port);
|
|
62
|
+
console.log(`Daemon running on port ${port} (PID: ${process.pid})`);
|
|
63
|
+
process.on('SIGINT', async () => {
|
|
64
|
+
console.log('\nStopping daemon...');
|
|
65
|
+
await server.stop();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
68
|
+
process.on('SIGTERM', async () => {
|
|
69
|
+
await server.stop();
|
|
70
|
+
process.exit(0);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function stopDaemon() {
|
|
75
|
+
const pidInfo = readPid();
|
|
76
|
+
if (!pidInfo) {
|
|
77
|
+
console.log('Daemon is not running');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!isProcessRunning(pidInfo.pid)) {
|
|
81
|
+
console.log('Daemon is not running (stale PID file)');
|
|
82
|
+
removePid();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pidInfo.pid, 'SIGTERM');
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new DinoxError(`Failed to stop daemon: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
const stopped = await waitForProcessExit(pidInfo.pid, DAEMON_STOP_TIMEOUT_MS);
|
|
92
|
+
if (!stopped) {
|
|
93
|
+
throw new DinoxError(`Daemon did not stop within ${DAEMON_STOP_TIMEOUT_MS}ms. Try stopping it manually and rerun \`dino daemon status\`.`);
|
|
94
|
+
}
|
|
95
|
+
removePid();
|
|
96
|
+
console.log('Daemon stopped');
|
|
97
|
+
}
|
|
98
|
+
async function daemonStatus() {
|
|
99
|
+
const pidInfo = readPid();
|
|
100
|
+
if (!pidInfo) {
|
|
101
|
+
console.log('Status: Not running');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const running = await isDaemonRunning();
|
|
105
|
+
if (running) {
|
|
106
|
+
console.log('Status: Running');
|
|
107
|
+
console.log(`PID: ${pidInfo.pid}`);
|
|
108
|
+
console.log(`Port: ${pidInfo.port}`);
|
|
109
|
+
console.log(`Version: ${getPackageVersion()}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log('Status: Not running (stale PID file)');
|
|
113
|
+
removePid();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function registerDaemonCommands(program) {
|
|
117
|
+
const daemon = program.command('daemon').description('Manage daemon process');
|
|
118
|
+
daemon
|
|
119
|
+
.command('start')
|
|
120
|
+
.description('Start daemon process')
|
|
121
|
+
.option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
|
|
122
|
+
.option('--no-detach', 'run in foreground (for debugging)')
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
const port = parsePort(options.port);
|
|
125
|
+
await startDaemon(port, options.detach !== false);
|
|
126
|
+
});
|
|
127
|
+
daemon
|
|
128
|
+
.command('stop')
|
|
129
|
+
.description('Stop daemon process')
|
|
130
|
+
.action(async () => {
|
|
131
|
+
await stopDaemon();
|
|
132
|
+
});
|
|
133
|
+
daemon
|
|
134
|
+
.command('status')
|
|
135
|
+
.description('Show daemon status')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
await daemonStatus();
|
|
138
|
+
});
|
|
139
|
+
daemon
|
|
140
|
+
.command('restart')
|
|
141
|
+
.description('Restart daemon process')
|
|
142
|
+
.option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
|
|
143
|
+
.action(async (options) => {
|
|
144
|
+
await stopDaemon();
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
146
|
+
await startDaemon(parsePort(options.port));
|
|
147
|
+
});
|
|
148
|
+
daemon.action((_options, command) => {
|
|
149
|
+
command.outputHelp();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getDaemonDir(): string;
|
|
2
|
+
export declare function getPidFilePath(): string;
|
|
3
|
+
export declare function getPortFilePath(): string;
|
|
4
|
+
export declare function savePid(pid: number, port: number): void;
|
|
5
|
+
export declare function readPid(): {
|
|
6
|
+
pid: number;
|
|
7
|
+
port: number;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare function removePid(): void;
|
|
10
|
+
export declare function isProcessRunning(pid: number): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
export function getDaemonDir() {
|
|
5
|
+
return join(homedir(), '.dinox', 'daemon');
|
|
6
|
+
}
|
|
7
|
+
export function getPidFilePath() {
|
|
8
|
+
return join(getDaemonDir(), 'daemon.pid');
|
|
9
|
+
}
|
|
10
|
+
export function getPortFilePath() {
|
|
11
|
+
return join(getDaemonDir(), 'daemon.port');
|
|
12
|
+
}
|
|
13
|
+
export function savePid(pid, port) {
|
|
14
|
+
const dir = getDaemonDir();
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
writeFileSync(getPidFilePath(), String(pid));
|
|
19
|
+
writeFileSync(getPortFilePath(), String(port));
|
|
20
|
+
}
|
|
21
|
+
export function readPid() {
|
|
22
|
+
try {
|
|
23
|
+
const pid = parseInt(readFileSync(getPidFilePath(), 'utf-8').trim());
|
|
24
|
+
const port = parseInt(readFileSync(getPortFilePath(), 'utf-8').trim());
|
|
25
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return { pid, port };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function removePid() {
|
|
38
|
+
for (const path of [getPidFilePath(), getPortFilePath()]) {
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(path);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Ignore missing files/stale paths.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function isProcessRunning(pid) {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DaemonRequest, DaemonResponse } from './types.js';
|
|
2
|
+
export declare class DaemonServer {
|
|
3
|
+
private db;
|
|
4
|
+
private server;
|
|
5
|
+
private startTime;
|
|
6
|
+
initialize(): Promise<void>;
|
|
7
|
+
handleRequest(req: DaemonRequest): Promise<DaemonResponse>;
|
|
8
|
+
start(port: number): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { loadConfig } from '../config/store.js';
|
|
3
|
+
import { resolveConfig } from '../config/resolve.js';
|
|
4
|
+
import { connectPowerSync, waitForIdleDataFlow } from '../powersync/runtime.js';
|
|
5
|
+
import { searchNotes, getNoteDetails } from '../commands/notes/repo.js';
|
|
6
|
+
import { getBacklinks, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
|
|
7
|
+
import { searchTodoTasks } from '../commands/todo/repo.js';
|
|
8
|
+
import { savePid, removePid } from './pid.js';
|
|
9
|
+
import { getPackageVersion } from '../utils/version.js';
|
|
10
|
+
const MAX_DAEMON_REQUEST_BYTES = 1 * 1024 * 1024;
|
|
11
|
+
async function readJsonBody(req) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let totalBytes = 0;
|
|
15
|
+
req.on('data', (chunk) => {
|
|
16
|
+
totalBytes += chunk.length;
|
|
17
|
+
if (totalBytes > MAX_DAEMON_REQUEST_BYTES) {
|
|
18
|
+
req.destroy(new Error('Request body too large'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
});
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
try {
|
|
25
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
26
|
+
resolve(JSON.parse(body));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export class DaemonServer {
|
|
36
|
+
db = null;
|
|
37
|
+
server = null;
|
|
38
|
+
startTime = Date.now();
|
|
39
|
+
async initialize() {
|
|
40
|
+
const config = resolveConfig(await loadConfig());
|
|
41
|
+
const { db } = await connectPowerSync({ config, offline: false, timeoutMs: config.sync.timeoutMs });
|
|
42
|
+
this.db = db;
|
|
43
|
+
setInterval(async () => {
|
|
44
|
+
if (this.db) {
|
|
45
|
+
await waitForIdleDataFlow(this.db, 5000).catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
}, 30000);
|
|
48
|
+
}
|
|
49
|
+
async handleRequest(req) {
|
|
50
|
+
try {
|
|
51
|
+
if (!this.db) {
|
|
52
|
+
throw new Error('Database not initialized');
|
|
53
|
+
}
|
|
54
|
+
const { method, params } = req;
|
|
55
|
+
switch (method) {
|
|
56
|
+
case 'ping':
|
|
57
|
+
return { id: req.id, ok: true, result: { uptime: Date.now() - this.startTime, version: getPackageVersion() } };
|
|
58
|
+
case 'note.search':
|
|
59
|
+
const notes = await searchNotes(this.db, params.query || '', {
|
|
60
|
+
includeDeleted: params.includeDeleted || false,
|
|
61
|
+
tagsExpression: params.tags,
|
|
62
|
+
createdAtFrom: params.from,
|
|
63
|
+
createdAtTo: params.to,
|
|
64
|
+
});
|
|
65
|
+
return { id: req.id, ok: true, result: notes };
|
|
66
|
+
case 'note.get':
|
|
67
|
+
const noteDetails = await getNoteDetails(this.db, [params.id]);
|
|
68
|
+
return { id: req.id, ok: true, result: noteDetails[0] || null };
|
|
69
|
+
case 'graph.backlinks':
|
|
70
|
+
const backlinks = await getBacklinks(this.db, params.id);
|
|
71
|
+
return { id: req.id, ok: true, result: backlinks };
|
|
72
|
+
case 'graph.outlinks':
|
|
73
|
+
const outlinks = await getOutlinks(this.db, params.id);
|
|
74
|
+
return { id: req.id, ok: true, result: outlinks };
|
|
75
|
+
case 'graph.related':
|
|
76
|
+
const related = await getRelatedNotes(this.db, params.id, params.depth || 2);
|
|
77
|
+
return { id: req.id, ok: true, result: related };
|
|
78
|
+
case 'todo.search':
|
|
79
|
+
const todos = await searchTodoTasks(this.db, params.query || '', {
|
|
80
|
+
includeDeleted: false,
|
|
81
|
+
taskStatus: params.status || 'all',
|
|
82
|
+
taskTagFilter: params.tags || [],
|
|
83
|
+
limit: params.limit || 50,
|
|
84
|
+
scanLimit: params.scanLimit || 200,
|
|
85
|
+
});
|
|
86
|
+
return { id: req.id, ok: true, result: todos };
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unknown method: ${method}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { id: req.id, ok: false, error: error.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async start(port) {
|
|
96
|
+
this.server = createServer(async (req, res) => {
|
|
97
|
+
if (req.method === 'POST') {
|
|
98
|
+
try {
|
|
99
|
+
const daemonReq = await readJsonBody(req);
|
|
100
|
+
const daemonRes = await this.handleRequest(daemonReq);
|
|
101
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify(daemonRes));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
const statusCode = message.includes('too large') ? 413 : 400;
|
|
107
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
108
|
+
res.end(JSON.stringify({ ok: false, error: message }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
res.writeHead(405);
|
|
113
|
+
res.end();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
this.server.headersTimeout = 5_000;
|
|
117
|
+
this.server.requestTimeout = 10_000;
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
if (!this.server) {
|
|
120
|
+
reject(new Error('Server not initialized'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.server.once('error', reject);
|
|
124
|
+
this.server.listen(port, '127.0.0.1', () => resolve());
|
|
125
|
+
});
|
|
126
|
+
savePid(process.pid, port);
|
|
127
|
+
}
|
|
128
|
+
async stop() {
|
|
129
|
+
if (this.server) {
|
|
130
|
+
await new Promise((resolve) => this.server?.close(() => resolve()));
|
|
131
|
+
}
|
|
132
|
+
if (this.db) {
|
|
133
|
+
await this.db.close().catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
removePid();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type DaemonRequest = {
|
|
2
|
+
id: string;
|
|
3
|
+
method: string;
|
|
4
|
+
params?: any;
|
|
5
|
+
};
|
|
6
|
+
export type DaemonResponse = {
|
|
7
|
+
id: string;
|
|
8
|
+
ok: boolean;
|
|
9
|
+
result?: any;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export type DaemonStatus = {
|
|
13
|
+
running: boolean;
|
|
14
|
+
pid?: number;
|
|
15
|
+
port?: number;
|
|
16
|
+
uptime?: number;
|
|
17
|
+
version?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const DEFAULT_DAEMON_PORT = 45678;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_DAEMON_PORT = 45678;
|
|
@@ -13,9 +13,10 @@ export declare class PowerSyncConnector implements PowerSyncBackendConnector {
|
|
|
13
13
|
fetchCredentials(): Promise<PowerSyncCredentials>;
|
|
14
14
|
uploadData(database: AbstractPowerSyncDatabase): Promise<void>;
|
|
15
15
|
}
|
|
16
|
-
export declare function fetchCredentialsWithAuthorization({ authorization, endpoint, credentialsUrl, signal, }: {
|
|
16
|
+
export declare function fetchCredentialsWithAuthorization({ authorization, endpoint, credentialsUrl, timeoutMs, signal, }: {
|
|
17
17
|
authorization: string;
|
|
18
18
|
endpoint: string;
|
|
19
19
|
credentialsUrl: string;
|
|
20
|
+
timeoutMs?: number;
|
|
20
21
|
signal?: AbortSignal;
|
|
21
22
|
}): Promise<PowerSyncCredentials>;
|