@dinoxx/dinox-cli 1.0.22 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +74 -15
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.js +16 -12
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +42 -0
- package/dist/daemon/lifecycle.js +115 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +19 -0
- package/dist/powersync/exclusiveRuntime.js +42 -0
- package/dist/powersync/runtime.d.ts +24 -4
- package/dist/powersync/runtime.js +234 -36
- package/dist/powersync/syncGate.d.ts +3 -1
- package/dist/powersync/syncGate.js +7 -1
- package/dist/powersync/tokenIndex.d.ts +11 -0
- package/dist/powersync/tokenIndex.js +159 -22
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +12 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
package/dist/commands/update.js
CHANGED
|
@@ -1,41 +1,121 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { registerStandaloneCommand } from './descriptor/register.js';
|
|
4
|
+
import { FORMAT_OPTIONS } from './schema/globalOptions.js';
|
|
3
5
|
import { SKILLS_REPO_URL } from '../constants/links.js';
|
|
4
|
-
import { printHumanReadableLine } from '../utils/output.js';
|
|
6
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
7
|
+
import { DinoxError } from '../utils/errors.js';
|
|
5
8
|
import { getPackageVersion } from '../utils/version.js';
|
|
6
9
|
const PACKAGE_NAME = '@dinoxx/dinox-cli';
|
|
10
|
+
const MAX_CAPTURED_UPDATE_OUTPUT_BYTES = 64 * 1024;
|
|
7
11
|
function logHumanReadable(...parts) {
|
|
8
12
|
printHumanReadableLine(parts.map((part) => String(part)).join(' '));
|
|
9
13
|
}
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
14
|
+
function packageManagerFromHint(value) {
|
|
15
|
+
const hint = value?.trim().toLowerCase().replaceAll('\\', '/');
|
|
16
|
+
if (!hint)
|
|
17
|
+
return null;
|
|
18
|
+
if (hint.startsWith('bun/') || hint.includes('/.bun/') || /\/bun(?:\.exe)?$/.test(hint))
|
|
13
19
|
return 'bun';
|
|
14
|
-
if (
|
|
20
|
+
if (hint.startsWith('pnpm/') || hint.includes('/.pnpm/') || /\/pnpm(?:\.c?js|\.cmd|\.exe)?$/.test(hint))
|
|
15
21
|
return 'pnpm';
|
|
16
|
-
if (
|
|
22
|
+
if (hint.startsWith('yarn/') || hint.includes('/.yarn/') || hint.includes('/yarn/global') || /\/yarn(?:\.c?js|\.cmd|\.exe)?$/.test(hint))
|
|
17
23
|
return 'yarn';
|
|
24
|
+
if (hint.startsWith('npm/') || /\/npm(?:-cli\.js|\.cmd|\.exe)?$/.test(hint))
|
|
25
|
+
return 'npm';
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function detectPackageManager(input = {}) {
|
|
29
|
+
const env = input.env ?? process.env;
|
|
30
|
+
const scriptPath = input.scriptPath ?? process.argv[1] ?? '';
|
|
31
|
+
const hints = [env.npm_config_user_agent, env.npm_execpath, scriptPath];
|
|
32
|
+
try {
|
|
33
|
+
if (scriptPath)
|
|
34
|
+
hints.push(realpathSync(scriptPath));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// The entrypoint may disappear while a global package is being replaced.
|
|
38
|
+
}
|
|
39
|
+
for (const hint of hints) {
|
|
40
|
+
const detected = packageManagerFromHint(hint);
|
|
41
|
+
if (detected)
|
|
42
|
+
return detected;
|
|
43
|
+
}
|
|
18
44
|
return 'npm';
|
|
19
45
|
}
|
|
20
46
|
const PM_COMMANDS = {
|
|
21
|
-
npm: { bin: 'npm', args: ['
|
|
22
|
-
pnpm: { bin: 'pnpm', args: ['
|
|
23
|
-
yarn: { bin: 'yarn', args: ['global', '
|
|
24
|
-
bun: { bin: 'bun', args: ['add', '--global', PACKAGE_NAME] },
|
|
47
|
+
npm: { bin: 'npm', args: ['install', '--global', `${PACKAGE_NAME}@latest`] },
|
|
48
|
+
pnpm: { bin: 'pnpm', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
|
|
49
|
+
yarn: { bin: 'yarn', args: ['global', 'add', `${PACKAGE_NAME}@latest`] },
|
|
50
|
+
bun: { bin: 'bun', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
|
|
25
51
|
};
|
|
26
|
-
function
|
|
52
|
+
export function resolveUpdateSpawnSpec(pm, platform = process.platform) {
|
|
27
53
|
const { bin, args } = PM_COMMANDS[pm];
|
|
54
|
+
const usesWindowsCommandShim = platform === 'win32' && bin !== 'bun';
|
|
55
|
+
if (usesWindowsCommandShim) {
|
|
56
|
+
return {
|
|
57
|
+
command: [`${bin}.cmd`, ...args].join(' '),
|
|
58
|
+
args: [],
|
|
59
|
+
shell: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
command: bin,
|
|
64
|
+
args: [...args],
|
|
65
|
+
shell: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function appendBoundedOutput(current, chunk) {
|
|
69
|
+
if (Buffer.byteLength(current, 'utf8') >= MAX_CAPTURED_UPDATE_OUTPUT_BYTES) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
const remaining = MAX_CAPTURED_UPDATE_OUTPUT_BYTES - Buffer.byteLength(current, 'utf8');
|
|
73
|
+
return current + chunk.subarray(0, remaining).toString('utf8');
|
|
74
|
+
}
|
|
75
|
+
function runUpdate(pm, options) {
|
|
76
|
+
const { command, args, shell } = resolveUpdateSpawnSpec(pm);
|
|
77
|
+
const invocation = [command, ...args].join(' ');
|
|
28
78
|
return new Promise((resolve, reject) => {
|
|
29
|
-
|
|
79
|
+
let stdout = '';
|
|
80
|
+
let stderr = '';
|
|
81
|
+
const child = spawn(command, args, {
|
|
82
|
+
shell,
|
|
83
|
+
stdio: options.structuredOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
84
|
+
});
|
|
85
|
+
if (options.structuredOutput) {
|
|
86
|
+
child.stdout?.on('data', (chunk) => {
|
|
87
|
+
stdout = appendBoundedOutput(stdout, chunk);
|
|
88
|
+
});
|
|
89
|
+
child.stderr?.on('data', (chunk) => {
|
|
90
|
+
stderr = appendBoundedOutput(stderr, chunk);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
30
93
|
child.on('close', (code) => {
|
|
31
94
|
if (code === 0) {
|
|
32
95
|
resolve();
|
|
33
96
|
}
|
|
34
97
|
else {
|
|
35
|
-
reject(new
|
|
98
|
+
reject(new DinoxError(`${command} exited with code ${code ?? 'unknown'}`, {
|
|
99
|
+
code: 'UPDATE_FAILED',
|
|
100
|
+
details: {
|
|
101
|
+
packageManager: pm,
|
|
102
|
+
exitCode: code,
|
|
103
|
+
...(stdout.trim() ? { stdout: stdout.trim() } : {}),
|
|
104
|
+
...(stderr.trim() ? { stderr: stderr.trim() } : {}),
|
|
105
|
+
},
|
|
106
|
+
suggestion: `Run ${invocation} manually to inspect the failure.`,
|
|
107
|
+
recoverable: true,
|
|
108
|
+
}));
|
|
36
109
|
}
|
|
37
110
|
});
|
|
38
|
-
child.on('error',
|
|
111
|
+
child.on('error', (error) => {
|
|
112
|
+
reject(new DinoxError(`Failed to start ${command}: ${error.message}`, {
|
|
113
|
+
code: 'UPDATE_FAILED',
|
|
114
|
+
cause: error,
|
|
115
|
+
suggestion: `Verify that ${command} is installed and available on PATH.`,
|
|
116
|
+
recoverable: true,
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
39
119
|
});
|
|
40
120
|
}
|
|
41
121
|
function resolveDeps(overrides) {
|
|
@@ -51,28 +131,60 @@ function printSkillsReminder(log) {
|
|
|
51
131
|
log(`skills repo: ${SKILLS_REPO_URL}`);
|
|
52
132
|
log('ai reminder: review and update local Dinox skills after CLI updates (especially dino-dinox and dino-update-cli).');
|
|
53
133
|
}
|
|
134
|
+
function parsePackageManagerOverride(value) {
|
|
135
|
+
if (value === undefined || value === null || value === '') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
if (value === 'npm' || value === 'pnpm' || value === 'yarn' || value === 'bun') {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
throw new DinoxError('--package-manager must be one of: npm, pnpm, yarn, bun', {
|
|
142
|
+
code: 'USAGE_ERROR',
|
|
143
|
+
recoverable: true,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
54
146
|
export function createUpdateCommandDescriptor(overrides = {}) {
|
|
55
147
|
const deps = resolveDeps(overrides);
|
|
56
148
|
return {
|
|
57
149
|
name: 'update',
|
|
58
150
|
description: `Update ${PACKAGE_NAME} to the latest version`,
|
|
151
|
+
options: [{
|
|
152
|
+
flags: '--package-manager <manager>',
|
|
153
|
+
description: 'Override package manager detection',
|
|
154
|
+
type: 'string',
|
|
155
|
+
enum: ['npm', 'pnpm', 'yarn', 'bun'],
|
|
156
|
+
}],
|
|
59
157
|
schema: {
|
|
60
158
|
path: 'update',
|
|
61
159
|
risk: 'write',
|
|
62
160
|
auth: 'none',
|
|
63
|
-
globalOptions:
|
|
161
|
+
globalOptions: FORMAT_OPTIONS,
|
|
64
162
|
output: {
|
|
65
163
|
type: 'structured',
|
|
66
|
-
summary: '
|
|
67
|
-
fields: ['
|
|
164
|
+
summary: 'Updater result and bundled skills reminder',
|
|
165
|
+
fields: ['previousVersion', 'packageManager', 'package', 'target', 'skillsRepo'],
|
|
68
166
|
},
|
|
69
|
-
examples: ['dino update'],
|
|
167
|
+
examples: ['dino update', 'dino update --package-manager pnpm --format json'],
|
|
70
168
|
},
|
|
71
|
-
action: async () => {
|
|
72
|
-
const pm = deps.detectPackageManager();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
169
|
+
action: async (context) => {
|
|
170
|
+
const pm = parsePackageManagerOverride(context.options.packageManager) ?? deps.detectPackageManager();
|
|
171
|
+
const previousVersion = deps.getVersion();
|
|
172
|
+
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
173
|
+
if (!structuredOutput) {
|
|
174
|
+
deps.log(`current version: ${previousVersion}`);
|
|
175
|
+
deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
|
|
176
|
+
}
|
|
177
|
+
await deps.runUpdate(pm, { structuredOutput });
|
|
178
|
+
if (structuredOutput) {
|
|
179
|
+
printSuccess({
|
|
180
|
+
previousVersion,
|
|
181
|
+
packageManager: pm,
|
|
182
|
+
package: PACKAGE_NAME,
|
|
183
|
+
target: 'latest',
|
|
184
|
+
skillsRepo: SKILLS_REPO_URL,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
76
188
|
deps.log('done');
|
|
77
189
|
printSkillsReminder(deps.log);
|
|
78
190
|
},
|
|
@@ -14,7 +14,7 @@ export declare function buildTagMutationDryRunPreview(args: {
|
|
|
14
14
|
command: string;
|
|
15
15
|
action: 'rename_tag' | 'move_tag' | 'merge_tag' | 'cleanup_tags';
|
|
16
16
|
summary: string;
|
|
17
|
-
risk: 'write' | 'high-risk-write';
|
|
17
|
+
risk: 'read' | 'write' | 'high-risk-write';
|
|
18
18
|
inputs: Record<string, unknown>;
|
|
19
19
|
changes?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
20
20
|
targets?: Array<Record<string, unknown>>;
|
|
@@ -37,7 +37,7 @@ export declare function buildBoxMutationDryRunPreview(args: {
|
|
|
37
37
|
command: string;
|
|
38
38
|
action: 'rename_box' | 'move_box' | 'merge_box' | 'cleanup_boxes';
|
|
39
39
|
summary: string;
|
|
40
|
-
risk: 'write' | 'high-risk-write';
|
|
40
|
+
risk: 'read' | 'write' | 'high-risk-write';
|
|
41
41
|
inputs: Record<string, unknown>;
|
|
42
42
|
changes?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
43
43
|
targets?: Array<Record<string, unknown>>;
|
|
@@ -68,6 +68,7 @@ export declare function buildStorageUploadDryRunPreview(args: {
|
|
|
68
68
|
storageKey: string;
|
|
69
69
|
storageUrl: string;
|
|
70
70
|
selectionSource: 'storage_id' | 'active' | 'single_available_config';
|
|
71
|
+
overwrite: boolean;
|
|
71
72
|
}): DryRunPreview;
|
|
72
73
|
export declare function buildStorageTestDryRunPreview(args: {
|
|
73
74
|
config: ResolvedDinoxConfig;
|
|
@@ -3,9 +3,6 @@ function buildExecutionWarnings(config, offline) {
|
|
|
3
3
|
if (offline) {
|
|
4
4
|
warnings.push('Execution would run in offline mode and only use the local cache.');
|
|
5
5
|
}
|
|
6
|
-
if (!offline && !config.powersync.uploadBaseUrl) {
|
|
7
|
-
warnings.push('Execution would be local-only because powersync.uploadBaseUrl is unset.');
|
|
8
|
-
}
|
|
9
6
|
return warnings;
|
|
10
7
|
}
|
|
11
8
|
export function buildTagAddDryRunPreview(args) {
|
|
@@ -105,6 +102,9 @@ export function buildStorageUploadDryRunPreview(args) {
|
|
|
105
102
|
if (args.storage.isPrivate) {
|
|
106
103
|
warnings.push('The target bucket is private; the final storageUrl may require signed read access.');
|
|
107
104
|
}
|
|
105
|
+
if (args.overwrite) {
|
|
106
|
+
warnings.push('Existing objects at the explicit key and derived thumbnail key may be replaced.');
|
|
107
|
+
}
|
|
108
108
|
return {
|
|
109
109
|
dry_run: true,
|
|
110
110
|
command: 'dino storage upload',
|
|
@@ -117,6 +117,7 @@ export function buildStorageUploadDryRunPreview(args) {
|
|
|
117
117
|
category: args.category,
|
|
118
118
|
size_bytes: args.sizeBytes,
|
|
119
119
|
selection_source: args.selectionSource,
|
|
120
|
+
overwrite: args.overwrite,
|
|
120
121
|
},
|
|
121
122
|
changes: {
|
|
122
123
|
storage_config_id: args.storage.id,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CommandOptionDescriptor } from './descriptor/types.js';
|
|
2
|
+
export declare const WRITE_DURABILITY_VALUES: readonly ["local", "uploaded"];
|
|
3
|
+
export type WriteDurability = typeof WRITE_DURABILITY_VALUES[number];
|
|
4
|
+
export declare const WRITE_DURABILITY_OPTION: CommandOptionDescriptor;
|
|
5
|
+
export declare function parseWriteDurability(value: unknown, optionName?: string): WriteDurability;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DinoxError } from '../utils/errors.js';
|
|
2
|
+
export const WRITE_DURABILITY_VALUES = ['local', 'uploaded'];
|
|
3
|
+
export const WRITE_DURABILITY_OPTION = {
|
|
4
|
+
flags: '--durability <local|uploaded>',
|
|
5
|
+
description: 'Required write durability before success: local saves to the local DB; uploaded waits for the PowerSync upload queue to drain',
|
|
6
|
+
type: 'string',
|
|
7
|
+
defaultValue: 'local',
|
|
8
|
+
enum: [...WRITE_DURABILITY_VALUES],
|
|
9
|
+
};
|
|
10
|
+
export function parseWriteDurability(value, optionName = '--durability') {
|
|
11
|
+
if (value === undefined || value === null || value === '') {
|
|
12
|
+
return 'local';
|
|
13
|
+
}
|
|
14
|
+
if (typeof value !== 'string') {
|
|
15
|
+
throw new DinoxError(`${optionName} must be one of: local, uploaded`, { code: 'USAGE_ERROR' });
|
|
16
|
+
}
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
if (normalized === 'local' || normalized === 'uploaded') {
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
throw new DinoxError(`${optionName} must be one of: local, uploaded`, { code: 'USAGE_ERROR' });
|
|
22
|
+
}
|
package/dist/config/paths.js
CHANGED
|
@@ -2,7 +2,12 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
|
|
4
4
|
function normalizeUserPathSegment(userId) {
|
|
5
|
-
|
|
5
|
+
const normalized = userId.trim();
|
|
6
|
+
const encoded = encodeURIComponent(normalized);
|
|
7
|
+
if (encoded === '.' || encoded === '..') {
|
|
8
|
+
return `user-${Buffer.from(normalized, 'utf8').toString('hex')}`;
|
|
9
|
+
}
|
|
10
|
+
return encoded;
|
|
6
11
|
}
|
|
7
12
|
function resolveConfigHome() {
|
|
8
13
|
if (process.env.DINOX_CONFIG_DIR) {
|
package/dist/config/resolve.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { DinoxConfig, ResolvedDinoxConfig } from './types.js';
|
|
2
|
+
export declare function fingerprintAuthToken(authorization: string | undefined): string | undefined;
|
|
3
|
+
export declare function fingerprintDaemonRuntimeConfig(config: ResolvedDinoxConfig): string;
|
|
2
4
|
export declare function resolveConfig(raw: DinoxConfig): ResolvedDinoxConfig;
|
package/dist/config/resolve.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import { POWERSYNC_DEFAULT_ENDPOINT, POWERSYNC_DEFAULT_TOKEN_ENDPOINT, POWERSYNC_DEFAULT_UPLOAD_BASE_URL, } from './serviceEndpoints.js';
|
|
2
3
|
import { normalizeAuthToken, readEnvAuthToken } from '../utils/authToken.js';
|
|
4
|
+
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
3
5
|
const DEFAULT_UPLOAD_V4_PATH = '/powersync/v4/uploadData';
|
|
4
6
|
const DEFAULT_UPLOAD_V2_PATH = '/powersync/v2/uploadData';
|
|
5
7
|
const DEFAULT_SYNC_TIMEOUT_MS = 300_000;
|
|
@@ -10,15 +12,26 @@ function nonEmptyString(value) {
|
|
|
10
12
|
const trimmed = value.trim();
|
|
11
13
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
12
14
|
}
|
|
13
|
-
function
|
|
14
|
-
if (
|
|
15
|
+
export function fingerprintAuthToken(authorization) {
|
|
16
|
+
if (!authorization) {
|
|
15
17
|
return undefined;
|
|
16
18
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return `sha256:${crypto.createHash('sha256').update(authorization, 'utf8').digest('hex')}`;
|
|
20
|
+
}
|
|
21
|
+
export function fingerprintDaemonRuntimeConfig(config) {
|
|
22
|
+
const payload = JSON.stringify({
|
|
23
|
+
powersync: {
|
|
24
|
+
endpoint: config.powersync.endpoint,
|
|
25
|
+
tokenEndpoint: config.powersync.tokenEndpoint,
|
|
26
|
+
uploadBaseUrl: config.powersync.uploadBaseUrl,
|
|
27
|
+
uploadV4Path: config.powersync.uploadV4Path,
|
|
28
|
+
uploadV2Path: config.powersync.uploadV2Path,
|
|
29
|
+
},
|
|
30
|
+
sync: {
|
|
31
|
+
timeoutMs: config.sync.timeoutMs,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return `sha256:${crypto.createHash('sha256').update(payload, 'utf8').digest('hex')}`;
|
|
22
35
|
}
|
|
23
36
|
export function resolveConfig(raw) {
|
|
24
37
|
const fileAuth = normalizeAuthToken(raw.auth?.authorization);
|
|
@@ -29,17 +42,18 @@ export function resolveConfig(raw) {
|
|
|
29
42
|
const fileTokenEndpoint = nonEmptyString(raw.powersync?.tokenEndpoint);
|
|
30
43
|
const fileUploadV4Path = nonEmptyString(raw.powersync?.uploadV4Path);
|
|
31
44
|
const fileUploadV2Path = nonEmptyString(raw.powersync?.uploadV2Path);
|
|
32
|
-
const fileTimeoutMs =
|
|
45
|
+
const fileTimeoutMs = parseSyncTimeoutMs(raw.sync?.timeoutMs, { invalid: 'undefined' });
|
|
33
46
|
const envAuth = readEnvAuthToken();
|
|
34
47
|
const envTokenEndpoint = nonEmptyString(process.env.DINOX_POWERSYNC_TOKEN_ENDPOINT) ?? nonEmptyString(process.env.POWERSYNC_TOKEN_ENDPOINT);
|
|
35
48
|
const envUploadV4Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V4_PATH);
|
|
36
49
|
const envUploadV2Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V2_PATH);
|
|
37
|
-
const envTimeoutMs =
|
|
50
|
+
const envTimeoutMs = parseSyncTimeoutMs(process.env.DINOX_SYNC_TIMEOUT_MS, { invalid: 'undefined' });
|
|
38
51
|
const effectiveAuth = envAuth ?? fileAuth;
|
|
39
52
|
const canReusePersistedIdentity = !envAuth || envAuth === fileAuth;
|
|
40
53
|
return {
|
|
41
54
|
auth: {
|
|
42
55
|
authorization: effectiveAuth,
|
|
56
|
+
tokenFingerprint: fingerprintAuthToken(effectiveAuth),
|
|
43
57
|
userId: canReusePersistedIdentity ? fileUserId : undefined,
|
|
44
58
|
username: canReusePersistedIdentity ? fileUsername : undefined,
|
|
45
59
|
email: canReusePersistedIdentity ? fileEmail : undefined,
|
package/dist/config/store.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { DinoxConfig } from './types.js';
|
|
2
2
|
export declare function loadConfig(): Promise<DinoxConfig>;
|
|
3
3
|
export declare function saveConfig(config: DinoxConfig): Promise<void>;
|
|
4
|
+
export declare function updateConfig(update: (current: DinoxConfig) => DinoxConfig | Promise<DinoxConfig>, options?: {
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}): Promise<DinoxConfig>;
|
|
4
7
|
export declare function setConfigValue(keyPath: string, value: unknown): Promise<void>;
|
|
5
8
|
export declare function getConfigValue(keyPath?: string): Promise<unknown>;
|
package/dist/config/store.js
CHANGED
|
@@ -3,6 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { getConfigDir, getConfigFilePath } from './paths.js';
|
|
4
4
|
import { generateId } from '../utils/id.js';
|
|
5
5
|
import { DinoxError } from '../utils/errors.js';
|
|
6
|
+
import { acquireExclusiveSqliteLease } from '../utils/sqliteLease.js';
|
|
7
|
+
import { withAsyncRelease } from '../utils/asyncRelease.js';
|
|
8
|
+
const CONFIG_WRITE_LOCK_TIMEOUT_MS = 10_000;
|
|
6
9
|
function isPlainObject(value) {
|
|
7
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
11
|
}
|
|
@@ -25,6 +28,24 @@ async function ensureConfigDir() {
|
|
|
25
28
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
26
29
|
return dir;
|
|
27
30
|
}
|
|
31
|
+
function getConfigWriteLockPath() {
|
|
32
|
+
return `${getConfigFilePath()}.write.sqlite`;
|
|
33
|
+
}
|
|
34
|
+
async function acquireConfigWriteLock(timeoutMs = CONFIG_WRITE_LOCK_TIMEOUT_MS) {
|
|
35
|
+
return acquireExclusiveSqliteLease({
|
|
36
|
+
lockPath: getConfigWriteLockPath(),
|
|
37
|
+
timeoutMs,
|
|
38
|
+
maxWaitMs: CONFIG_WRITE_LOCK_TIMEOUT_MS,
|
|
39
|
+
createBusyError: () => new DinoxError('Another Dinox process is updating the configuration.', {
|
|
40
|
+
code: 'CONFIG_WRITE_BUSY',
|
|
41
|
+
suggestion: 'Wait for the other Dinox command to finish, then retry.',
|
|
42
|
+
recoverable: true,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function withConfigWriteLock(run, timeoutMs = CONFIG_WRITE_LOCK_TIMEOUT_MS) {
|
|
47
|
+
return withAsyncRelease(() => acquireConfigWriteLock(timeoutMs), run);
|
|
48
|
+
}
|
|
28
49
|
export async function loadConfig() {
|
|
29
50
|
const filePath = getConfigFilePath();
|
|
30
51
|
try {
|
|
@@ -39,27 +60,52 @@ export async function loadConfig() {
|
|
|
39
60
|
if (error?.code === 'ENOENT') {
|
|
40
61
|
return {};
|
|
41
62
|
}
|
|
63
|
+
if (error instanceof SyntaxError) {
|
|
64
|
+
throw new DinoxError(`Invalid config JSON at ${filePath}: ${error.message}`, {
|
|
65
|
+
code: 'CONFIG_INVALID_JSON',
|
|
66
|
+
cause: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
42
69
|
throw error;
|
|
43
70
|
}
|
|
44
71
|
}
|
|
45
|
-
|
|
72
|
+
async function writeConfigFile(config) {
|
|
46
73
|
const dir = await ensureConfigDir();
|
|
47
74
|
const filePath = getConfigFilePath();
|
|
48
75
|
const tmpPath = path.join(dir, `config.${generateId()}.tmp`);
|
|
49
76
|
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
50
|
-
// Write config atomically to avoid partial reads.
|
|
51
|
-
const handle = await fs.open(tmpPath, 'wx', 0o600);
|
|
52
77
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
// Write config atomically to avoid partial reads.
|
|
79
|
+
const handle = await fs.open(tmpPath, 'wx', 0o600);
|
|
80
|
+
try {
|
|
81
|
+
await handle.writeFile(payload, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
if (process.platform !== 'win32') {
|
|
87
|
+
// Best-effort: keep the config private.
|
|
88
|
+
await fs.chmod(tmpPath, 0o600).catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
await fs.rename(tmpPath, filePath);
|
|
57
91
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
92
|
+
catch (error) {
|
|
93
|
+
await fs.rm(tmpPath, { force: true }).catch(() => undefined);
|
|
94
|
+
throw error;
|
|
61
95
|
}
|
|
62
|
-
|
|
96
|
+
}
|
|
97
|
+
export async function saveConfig(config) {
|
|
98
|
+
await withConfigWriteLock(async () => {
|
|
99
|
+
await writeConfigFile(config);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export async function updateConfig(update, options = {}) {
|
|
103
|
+
return withConfigWriteLock(async () => {
|
|
104
|
+
const current = await loadConfig();
|
|
105
|
+
const next = await update(current);
|
|
106
|
+
await writeConfigFile(next);
|
|
107
|
+
return next;
|
|
108
|
+
}, options.timeoutMs);
|
|
63
109
|
}
|
|
64
110
|
function setDeepValue(target, keyPath, value) {
|
|
65
111
|
const segments = splitKeyPath(keyPath);
|
|
@@ -89,9 +135,10 @@ function getDeepValue(target, keyPath) {
|
|
|
89
135
|
return cursor;
|
|
90
136
|
}
|
|
91
137
|
export async function setConfigValue(keyPath, value) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
138
|
+
await updateConfig((config) => {
|
|
139
|
+
setDeepValue(config, keyPath, value);
|
|
140
|
+
return config;
|
|
141
|
+
});
|
|
95
142
|
}
|
|
96
143
|
export async function getConfigValue(keyPath) {
|
|
97
144
|
const config = await loadConfig();
|
package/dist/config/types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type DinoxConfig = {
|
|
|
11
11
|
username?: string | null;
|
|
12
12
|
email?: string | null;
|
|
13
13
|
userInfoSyncedAt?: string;
|
|
14
|
+
/** Internal CAS token changed by every persisted login/logout transition. */
|
|
15
|
+
stateRevision?: string;
|
|
14
16
|
};
|
|
15
17
|
powersync?: {
|
|
16
18
|
/** PowerSync sync service endpoint (downloads/subscriptions). */
|
|
@@ -32,6 +34,8 @@ export type DinoxConfig = {
|
|
|
32
34
|
export type ResolvedDinoxConfig = {
|
|
33
35
|
auth: {
|
|
34
36
|
authorization?: string;
|
|
37
|
+
/** Runtime-only SHA-256 fingerprint used to bind long-lived processes to the active token. */
|
|
38
|
+
tokenFingerprint?: string;
|
|
35
39
|
userId?: string;
|
|
36
40
|
username?: string | null;
|
|
37
41
|
email?: string | null;
|
package/dist/daemon/client.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import type { DaemonPingResult, DaemonShutdownResult } from './types.js';
|
|
2
|
+
export declare function sendDaemonRequest(method: string, params?: unknown, requestOptions?: {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
}): Promise<unknown>;
|
|
2
5
|
export declare function isDaemonRunning(): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* Ping the daemon and return its handshake payload (version + connected identity),
|
|
8
|
+
* or null when no daemon is reachable. Callers use this to detect a stale daemon
|
|
9
|
+
* (wrong version or wrong account) rather than only "running / not running".
|
|
10
|
+
*/
|
|
11
|
+
export declare function getDaemonInfo(): Promise<DaemonPingResult | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a reachable daemon matches the current CLI version and signed-in account.
|
|
14
|
+
* A mismatch means the daemon was started by an older build or a different user and
|
|
15
|
+
* must be restarted before its cached data can be trusted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isDaemonFresh(info: DaemonPingResult, expectedVersion: string, expectedUserId: string | null, expectedTokenFingerprint: string | null, expectedRuntimeConfigFingerprint: string): boolean;
|
|
18
|
+
export declare function isLegacyDaemonInfo(info: DaemonPingResult): boolean;
|
|
19
|
+
export declare function shutdownLegacyDaemon(initialInfo: DaemonPingResult, timeoutMs?: number): Promise<boolean>;
|
|
20
|
+
/** Poll until the daemon answers a ping or the timeout elapses. */
|
|
21
|
+
export declare function waitForDaemonReady(timeoutMs?: number, intervalMs?: number): Promise<boolean>;
|
|
22
|
+
/** Poll until the daemon answers with the expected authenticated identity. */
|
|
23
|
+
export declare function waitForFreshDaemon(expectedVersion: string, expectedUserId: string | null, expectedTokenFingerprint: string | null, expectedRuntimeConfigFingerprint: string, timeoutMs?: number, intervalMs?: number): Promise<boolean>;
|
|
24
|
+
/** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
|
|
25
|
+
export declare function requestDaemonShutdown(expectedInstanceId: string, timeoutMs?: number): Promise<DaemonShutdownResult>;
|
|
26
|
+
/** Stop a reachable daemon before another process acquires the exclusive PowerSync owner lease. */
|
|
27
|
+
export declare function shutdownDaemonForExclusiveAccess(timeoutMs?: number, expectedInstanceId?: string): Promise<boolean>;
|