@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.
- package/README.md +33 -10
- 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 +89 -19
- 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.d.ts +23 -1
- package/dist/commands/sync.js +105 -60
- 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 +44 -0
- package/dist/daemon/lifecycle.js +118 -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 +20 -0
- package/dist/powersync/exclusiveRuntime.js +48 -0
- package/dist/powersync/runtime.d.ts +25 -4
- package/dist/powersync/runtime.js +255 -35
- package/dist/powersync/syncGate.d.ts +4 -1
- package/dist/powersync/syncGate.js +11 -2
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +13 -0
- package/dist/powersync/tokenIndex.js +216 -37
- 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 +14 -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/sync.js
CHANGED
|
@@ -1,18 +1,118 @@
|
|
|
1
1
|
import { loadConfig } from '../config/store.js';
|
|
2
2
|
import { resolveConfig } from '../config/resolve.js';
|
|
3
3
|
import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
4
|
+
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
4
5
|
import { registerStandaloneCommand } from './descriptor/register.js';
|
|
5
6
|
import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
|
|
6
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
7
8
|
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
8
|
-
import {
|
|
9
|
+
import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
|
|
9
10
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
10
11
|
import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
|
|
12
|
+
const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
|
|
13
|
+
const syncCommandDependencies = {
|
|
14
|
+
loadConfig,
|
|
15
|
+
resolveConfig,
|
|
16
|
+
hydrateResolvedAuthIdentity,
|
|
17
|
+
withExclusivePowerSync,
|
|
18
|
+
getRequestedStructuredOutputFormat,
|
|
19
|
+
getStatusSnapshot,
|
|
20
|
+
waitForLastSyncedAfter,
|
|
21
|
+
runSingleSyncPass,
|
|
22
|
+
printHumanReadableLine,
|
|
23
|
+
printSuccess,
|
|
24
|
+
now: Date.now,
|
|
25
|
+
};
|
|
26
|
+
export async function executeSyncCommand(context, overrides = {}) {
|
|
27
|
+
const dependencies = { ...syncCommandDependencies, ...overrides };
|
|
28
|
+
const startedAtMs = dependencies.now();
|
|
29
|
+
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
30
|
+
const baseConfig = dependencies.resolveConfig(await dependencies.loadConfig());
|
|
31
|
+
const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
|
|
32
|
+
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
33
|
+
const deadlineAtMs = startedAtMs + timeoutMs;
|
|
34
|
+
const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
|
|
35
|
+
const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
|
|
36
|
+
const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
|
|
37
|
+
const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
38
|
+
timeoutMs: remainingTimeoutMs(),
|
|
39
|
+
});
|
|
40
|
+
const strict = Boolean(context.options.strict);
|
|
41
|
+
const result = await dependencies.withExclusivePowerSync({
|
|
42
|
+
config,
|
|
43
|
+
offline: false,
|
|
44
|
+
timeoutMs: remainingTimeoutMs(),
|
|
45
|
+
deadlineAtMs: operationDeadlineAtMs,
|
|
46
|
+
waitForInitialSync: false,
|
|
47
|
+
expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
|
|
48
|
+
cleanupFailureMode: 'throw',
|
|
49
|
+
}, async ({ db, dbPath, stale }) => {
|
|
50
|
+
const recentSync = strict
|
|
51
|
+
? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
52
|
+
: true;
|
|
53
|
+
const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
|
|
54
|
+
const status = dependencies.getStatusSnapshot(db);
|
|
55
|
+
const gate = evaluateSyncGate({
|
|
56
|
+
offline: false,
|
|
57
|
+
firstSyncStale: stale,
|
|
58
|
+
idle: syncPass.downloadIdle,
|
|
59
|
+
indexComplete: syncPass.tokenIndex.complete,
|
|
60
|
+
recentSync,
|
|
61
|
+
status,
|
|
62
|
+
});
|
|
63
|
+
const payload = {
|
|
64
|
+
ok: true,
|
|
65
|
+
dbPath,
|
|
66
|
+
stale: !gate.ok,
|
|
67
|
+
idle: syncPass.idle,
|
|
68
|
+
downloadIdle: syncPass.downloadIdle,
|
|
69
|
+
uploadIdle: syncPass.uploadIdle,
|
|
70
|
+
strict,
|
|
71
|
+
gate,
|
|
72
|
+
uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
|
|
73
|
+
tokenIndex: syncPass.tokenIndex,
|
|
74
|
+
status,
|
|
75
|
+
};
|
|
76
|
+
if (strict && !gate.ok) {
|
|
77
|
+
throw createSyncRequiredError(gate, status);
|
|
78
|
+
}
|
|
79
|
+
return payload;
|
|
80
|
+
});
|
|
81
|
+
const payload = {
|
|
82
|
+
...result,
|
|
83
|
+
elapsedMs: Math.max(0, dependencies.now() - startedAtMs),
|
|
84
|
+
};
|
|
85
|
+
if (structuredOutput) {
|
|
86
|
+
dependencies.printSuccess(payload);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
dependencies.printHumanReadableLine(`db: ${payload.dbPath}`);
|
|
90
|
+
if (payload.status.lastSyncedAt) {
|
|
91
|
+
dependencies.printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
|
|
92
|
+
}
|
|
93
|
+
if (payload.stale) {
|
|
94
|
+
dependencies.printHumanReadableLine('warning: sync freshness or local index could not be fully verified');
|
|
95
|
+
}
|
|
96
|
+
if (!payload.downloadIdle) {
|
|
97
|
+
dependencies.printHumanReadableLine('warning: download flow did not settle before timeout');
|
|
98
|
+
}
|
|
99
|
+
if (!payload.uploadIdle) {
|
|
100
|
+
dependencies.printHumanReadableLine('warning: uploads are still active; downloaded data may already be current');
|
|
101
|
+
}
|
|
102
|
+
if (!payload.tokenIndex.complete) {
|
|
103
|
+
dependencies.printHumanReadableLine('warning: local token index did not finish before timeout');
|
|
104
|
+
}
|
|
105
|
+
dependencies.printHumanReadableLine(`tokenIndex: complete=${payload.tokenIndex.complete} scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
|
|
106
|
+
}
|
|
11
107
|
export const syncCommandDescriptor = {
|
|
12
108
|
name: 'sync',
|
|
13
109
|
description: 'Connect and synchronize the local PowerSync database',
|
|
14
110
|
options: [
|
|
15
|
-
{
|
|
111
|
+
{
|
|
112
|
+
flags: '--strict',
|
|
113
|
+
description: 'Fail unless connected, a current checkpoint completes, downloads settle, and the local index finishes',
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
},
|
|
16
116
|
],
|
|
17
117
|
schema: {
|
|
18
118
|
path: 'sync',
|
|
@@ -22,66 +122,11 @@ export const syncCommandDescriptor = {
|
|
|
22
122
|
output: {
|
|
23
123
|
type: 'structured',
|
|
24
124
|
summary: 'Sync result with connection status and token index counters',
|
|
25
|
-
fields: ['dbPath', 'stale', 'idle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status'],
|
|
125
|
+
fields: ['dbPath', 'stale', 'idle', 'downloadIdle', 'uploadIdle', 'strict', 'gate', 'uploadEnabled', 'tokenIndex', 'status', 'elapsedMs'],
|
|
26
126
|
},
|
|
27
|
-
examples: ['dino sync --format json', 'dino sync --strict --sync-timeout
|
|
28
|
-
},
|
|
29
|
-
action: async (context) => {
|
|
30
|
-
const baseConfig = resolveConfig(await loadConfig());
|
|
31
|
-
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
32
|
-
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
33
|
-
const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs });
|
|
34
|
-
const { db, dbPath, stale } = await connectPowerSync({
|
|
35
|
-
config,
|
|
36
|
-
offline: false,
|
|
37
|
-
timeoutMs,
|
|
38
|
-
});
|
|
39
|
-
try {
|
|
40
|
-
const syncPass = await runSingleSyncPass(db, timeoutMs);
|
|
41
|
-
const status = getStatusSnapshot(db);
|
|
42
|
-
const gate = evaluateSyncGate({
|
|
43
|
-
offline: false,
|
|
44
|
-
firstSyncStale: stale,
|
|
45
|
-
idle: syncPass.idle,
|
|
46
|
-
status,
|
|
47
|
-
});
|
|
48
|
-
const payload = {
|
|
49
|
-
ok: true,
|
|
50
|
-
dbPath,
|
|
51
|
-
stale: !gate.ok,
|
|
52
|
-
idle: syncPass.idle,
|
|
53
|
-
strict: Boolean(context.options.strict),
|
|
54
|
-
gate,
|
|
55
|
-
uploadEnabled: Boolean(config.powersync.uploadBaseUrl),
|
|
56
|
-
tokenIndex: syncPass.tokenIndex,
|
|
57
|
-
status,
|
|
58
|
-
};
|
|
59
|
-
if (context.options.strict && !gate.ok) {
|
|
60
|
-
throw createSyncRequiredError(gate, status);
|
|
61
|
-
}
|
|
62
|
-
if (structuredOutput) {
|
|
63
|
-
printSuccess(payload);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
printHumanReadableLine(`db: ${payload.dbPath}`);
|
|
67
|
-
if (!payload.uploadEnabled) {
|
|
68
|
-
printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset)');
|
|
69
|
-
}
|
|
70
|
-
if (payload.status.lastSyncedAt) {
|
|
71
|
-
printHumanReadableLine(`lastSyncedAt: ${payload.status.lastSyncedAt}`);
|
|
72
|
-
}
|
|
73
|
-
if (payload.stale) {
|
|
74
|
-
printHumanReadableLine('warning: sync timed out; local cache may be stale');
|
|
75
|
-
}
|
|
76
|
-
if (!payload.idle) {
|
|
77
|
-
printHumanReadableLine('warning: data flow did not become idle before timeout');
|
|
78
|
-
}
|
|
79
|
-
printHumanReadableLine(`tokenIndex: scanned=${payload.tokenIndex.scanned} reindexed=${payload.tokenIndex.reindexed} skipped=${payload.tokenIndex.skipped} removed=${payload.tokenIndex.removed} ftsRowsUpserted=${payload.tokenIndex.ftsRowsUpserted}`);
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
await db.close().catch(() => undefined);
|
|
83
|
-
}
|
|
127
|
+
examples: ['dino sync --sync-timeout 20000 --format json', 'dino sync --strict --sync-timeout 20000 --format json'],
|
|
84
128
|
},
|
|
129
|
+
action: executeSyncCommand,
|
|
85
130
|
};
|
|
86
131
|
export function registerSyncCommand(program) {
|
|
87
132
|
registerStandaloneCommand(program, syncCommandDescriptor);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalizeTagPath, isTagPathWithinPrefix, renameTagPathWithPrefix, } from './model.js';
|
|
2
|
+
import { findMarkdownLiteralRanges, isMarkdownOffsetInSortedRanges, } from '../../utils/tiptapMarkdownSyntax.js';
|
|
2
3
|
function dedupeTags(values) {
|
|
3
4
|
const result = [];
|
|
4
5
|
const seen = new Set();
|
|
@@ -191,7 +192,15 @@ function rewriteHashtagsInMarkdown(raw, oldPrefix, newPrefix, preserveSuffix) {
|
|
|
191
192
|
}
|
|
192
193
|
let output = '';
|
|
193
194
|
let changed = false;
|
|
195
|
+
const protectedRanges = findMarkdownLiteralRanges(raw);
|
|
194
196
|
for (let index = 0; index < raw.length;) {
|
|
197
|
+
if (isMarkdownOffsetInSortedRanges(index, protectedRanges)) {
|
|
198
|
+
const range = protectedRanges.find((candidate) => index >= candidate.start && index < candidate.end);
|
|
199
|
+
const end = range?.end ?? index + 1;
|
|
200
|
+
output += raw.slice(index, end);
|
|
201
|
+
index = end;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
195
204
|
if (raw.charCodeAt(index) !== 0x23) {
|
|
196
205
|
output += raw[index];
|
|
197
206
|
index += 1;
|
|
@@ -5,7 +5,7 @@ import { buildTagAddDryRunPreview, buildTagMutationDryRunPreview } 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,
|
|
8
|
+
import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
9
9
|
import { addTag, inspectAddTag, listTags } from './repo.js';
|
|
10
10
|
import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
|
|
11
11
|
const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -75,7 +75,6 @@ async function runTagAdd(context) {
|
|
|
75
75
|
userId: identity.userId,
|
|
76
76
|
emoji,
|
|
77
77
|
});
|
|
78
|
-
printUploadDisabledWarning(runtime);
|
|
79
78
|
await waitForMutationUpload(runtime);
|
|
80
79
|
const payload = { ok: true, stale: commandStale(runtime), ...result };
|
|
81
80
|
if (structuredOutput) {
|
|
@@ -118,7 +117,6 @@ async function runTagRename(context) {
|
|
|
118
117
|
}));
|
|
119
118
|
return;
|
|
120
119
|
}
|
|
121
|
-
printUploadDisabledWarning(runtime);
|
|
122
120
|
await waitForMutationUpload(runtime);
|
|
123
121
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
124
122
|
});
|
|
@@ -144,7 +142,6 @@ async function runTagMove(context) {
|
|
|
144
142
|
}));
|
|
145
143
|
return;
|
|
146
144
|
}
|
|
147
|
-
printUploadDisabledWarning(runtime);
|
|
148
145
|
await waitForMutationUpload(runtime);
|
|
149
146
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
150
147
|
});
|
|
@@ -170,7 +167,6 @@ async function runTagMerge(context) {
|
|
|
170
167
|
}));
|
|
171
168
|
return;
|
|
172
169
|
}
|
|
173
|
-
printUploadDisabledWarning(runtime);
|
|
174
170
|
await waitForMutationUpload(runtime);
|
|
175
171
|
printSuccess({ ok: true, stale: commandStale(runtime), ...result });
|
|
176
172
|
});
|
|
@@ -193,7 +189,7 @@ async function runTagCleanup(context) {
|
|
|
193
189
|
offline: runtime.offline,
|
|
194
190
|
command: 'dino tag cleanup',
|
|
195
191
|
action: 'cleanup_tags',
|
|
196
|
-
risk: '
|
|
192
|
+
risk: 'read',
|
|
197
193
|
summary: `Inspect tag cleanup candidates (${result.issues.length} issues).`,
|
|
198
194
|
inputs: {},
|
|
199
195
|
targets: result.issues,
|
|
@@ -2,14 +2,15 @@ import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
|
2
2
|
import { DinoxError } from '../../utils/errors.js';
|
|
3
3
|
import { nowIsoUtc } from '../../utils/time.js';
|
|
4
4
|
import { mergeTagReferencesInNote, renameTagReferencesInNote } from './content.js';
|
|
5
|
-
import {
|
|
5
|
+
import { MAX_TAG_SEGMENT_LENGTH, REQUIRED_NOTE_TAG_COLUMNS, REQUIRED_TAG_NODE_COLUMNS, canonicalizeTagPath, getLeafName, isActiveTagNode, isActualTagNode, isTagPathWithinPrefix, normalizeTagLookupKey, parseStoredTags, parentPathOf, renameTagPathWithPrefix, trimString, } from './model.js';
|
|
6
6
|
import { activeTagNodes, actualActiveTagNodes, assertCanWriteTag, assertRequiredColumns, assertWritableTagNodes, descendantsOf, ensureNoTagPathConflict, getTableColumns, loadTagNodes, parentIdForPath, requireActiveTagPath, toTagListItem, } from './store.js';
|
|
7
|
+
import { assertTagPathLimits, assertTagTextLimits } from './repo.js';
|
|
7
8
|
function parseSingleSegment(value, label) {
|
|
8
9
|
const normalized = canonicalizeTagPath(value);
|
|
9
10
|
if (!normalized || normalized.includes('/')) {
|
|
10
11
|
throw new DinoxError(`${label} must be a single path segment`);
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
assertTagTextLimits(label, normalized, MAX_TAG_SEGMENT_LENGTH);
|
|
13
14
|
return normalized;
|
|
14
15
|
}
|
|
15
16
|
function assertPath(value, label) {
|
|
@@ -17,16 +18,20 @@ function assertPath(value, label) {
|
|
|
17
18
|
if (!normalized) {
|
|
18
19
|
throw new DinoxError(`${label} is required`);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
assertTagPathLimits(normalized, label);
|
|
21
22
|
return normalized;
|
|
22
23
|
}
|
|
23
24
|
function buildSubtreeChanges(nodes, root, newRootPath) {
|
|
24
25
|
const targetPath = assertPath(newRootPath, 'Target tag path');
|
|
25
|
-
return descendantsOf(nodes, root).map((node) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
return descendantsOf(nodes, root).map((node) => {
|
|
27
|
+
const toPath = renameTagPathWithPrefix(node.fullPath, root.fullPath, targetPath);
|
|
28
|
+
assertTagPathLimits(toPath, 'Target tag path');
|
|
29
|
+
return {
|
|
30
|
+
id: node.id,
|
|
31
|
+
fromPath: node.fullPath,
|
|
32
|
+
toPath,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
30
35
|
}
|
|
31
36
|
function toTreeNode(node) {
|
|
32
37
|
return {
|
|
@@ -75,13 +80,13 @@ async function loadNoteTagRows(db, userId) {
|
|
|
75
80
|
FROM c_note
|
|
76
81
|
WHERE TRIM(COALESCE(id, '')) <> ''
|
|
77
82
|
AND (is_del IS NULL OR is_del = 0)
|
|
78
|
-
AND
|
|
83
|
+
AND TRIM(COALESCE(user_no, '')) = ?
|
|
79
84
|
`, [userId]);
|
|
80
85
|
}
|
|
81
86
|
async function loadNoteTagStats(db, userId) {
|
|
82
87
|
const normalizedUserId = trimString(userId);
|
|
83
88
|
const userFilter = normalizedUserId
|
|
84
|
-
? `AND
|
|
89
|
+
? `AND TRIM(COALESCE(n.user_no, '')) = ?`
|
|
85
90
|
: '';
|
|
86
91
|
const rows = await db.getAll(`
|
|
87
92
|
SELECT
|
|
@@ -20,6 +20,8 @@ export type InspectAddTagResult = {
|
|
|
20
20
|
restoredPaths: string[];
|
|
21
21
|
emoji: string;
|
|
22
22
|
};
|
|
23
|
+
export declare function assertTagTextLimits(label: string, value: string, maxLength: number): void;
|
|
24
|
+
export declare function assertTagPathLimits(path: string, label?: string): void;
|
|
23
25
|
export declare function listTags(db: AbstractPowerSyncDatabase, input?: ListTagsInput): Promise<string[]>;
|
|
24
26
|
export declare function addTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<AddTagResult>;
|
|
25
27
|
export declare function inspectAddTag(db: AbstractPowerSyncDatabase, input: AddTagInput): Promise<InspectAddTagResult>;
|
|
@@ -14,6 +14,18 @@ function tagNodeMapByPath(nodes) {
|
|
|
14
14
|
}
|
|
15
15
|
return map;
|
|
16
16
|
}
|
|
17
|
+
export function assertTagTextLimits(label, value, maxLength) {
|
|
18
|
+
assertMaxLength(label, value, maxLength);
|
|
19
|
+
if (Buffer.byteLength(value, 'utf8') > maxLength) {
|
|
20
|
+
throw new DinoxError(`${label} must be at most ${maxLength} UTF-8 bytes`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function assertTagPathLimits(path, label = 'Tag path') {
|
|
24
|
+
assertTagTextLimits(label, path, MAX_TAG_PATH_LENGTH);
|
|
25
|
+
for (const segment of path.split('/')) {
|
|
26
|
+
assertTagTextLimits('Tag path segment', segment, MAX_TAG_SEGMENT_LENGTH);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
function assertAddTagInput(input) {
|
|
18
30
|
const userId = trimString(input.userId);
|
|
19
31
|
const emoji = trimString(input.emoji);
|
|
@@ -21,15 +33,15 @@ function assertAddTagInput(input) {
|
|
|
21
33
|
if (!tag) {
|
|
22
34
|
throw new DinoxError('Tag name is required');
|
|
23
35
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
assertTagPathLimits(tag);
|
|
37
|
+
assertTagTextLimits('Tag name', getLeafName(tag), MAX_TAG_SEGMENT_LENGTH);
|
|
38
|
+
assertTagTextLimits('Tag emoji', emoji, MAX_TAG_EMOJI_LENGTH);
|
|
27
39
|
return { tag, userId, emoji };
|
|
28
40
|
}
|
|
29
41
|
export async function listTags(db, input = {}) {
|
|
30
42
|
const userId = trimString(input.userId);
|
|
31
43
|
const userFilter = userId
|
|
32
|
-
? `AND
|
|
44
|
+
? `AND TRIM(COALESCE(user_id, '')) = ?`
|
|
33
45
|
: '';
|
|
34
46
|
const rows = await db.getAll(`
|
|
35
47
|
SELECT DISTINCT TRIM(COALESCE(full_path, path_key, '')) AS tag
|
|
@@ -112,7 +124,8 @@ export async function addTag(db, input) {
|
|
|
112
124
|
UPDATE c_tag_node
|
|
113
125
|
SET ${assignments.join(', ')}
|
|
114
126
|
WHERE id = ?
|
|
115
|
-
|
|
127
|
+
AND TRIM(COALESCE(user_id, '')) = ?
|
|
128
|
+
`, [...params, userId]);
|
|
116
129
|
if (existing.isDel === 1) {
|
|
117
130
|
restoredNodes += 1;
|
|
118
131
|
existing.isDel = 0;
|
|
@@ -20,7 +20,7 @@ export function assertCanWriteTag(columnNames, userId, action) {
|
|
|
20
20
|
export async function loadTagNodes(db, columnNames, options = {}) {
|
|
21
21
|
const userId = trimString(options.userId);
|
|
22
22
|
const userFilter = userId
|
|
23
|
-
? `AND
|
|
23
|
+
? `AND TRIM(COALESCE(user_id, '')) = ?`
|
|
24
24
|
: '';
|
|
25
25
|
const rows = await db.getAll(`
|
|
26
26
|
SELECT
|
|
@@ -14,17 +14,18 @@ export type ApplyTaskCheckedStatusResult = {
|
|
|
14
14
|
nextDoc: JsonRecord;
|
|
15
15
|
taskOrder: string[];
|
|
16
16
|
matchedIndices: number[];
|
|
17
|
-
|
|
17
|
+
matchedCheckedStates: boolean[];
|
|
18
|
+
statusChangedCount: number;
|
|
18
19
|
};
|
|
19
20
|
export declare function parseTiptapDocFromDb(rawContentJson: string | null): JsonRecord;
|
|
20
21
|
export declare function createTodoDocFromTasks(taskTexts: string[]): JsonRecord;
|
|
21
22
|
export declare function appendTasksToDoc(doc: JsonRecord, taskTexts: string[]): JsonRecord;
|
|
22
|
-
export declare function deriveImageDetailFromContentJson(doc: JsonRecord): TodoTaskItemNode[];
|
|
23
|
+
export declare function deriveImageDetailFromContentJson(doc: JsonRecord, noteId: string): TodoTaskItemNode[];
|
|
23
24
|
export declare function buildAppendMarkdown(existingMarkdown: string | null, taskTexts: string[]): string;
|
|
24
25
|
export declare function buildCreateMarkdown(taskTexts: string[]): string;
|
|
25
26
|
export declare function buildContentTextPreview(contentMd: string): string;
|
|
26
27
|
export declare function buildSummaryTitle(taskTexts: string[]): string;
|
|
27
28
|
export declare function normalizeTaskTexts(values: string[]): string[];
|
|
28
|
-
export declare function applyTaskCheckedStatus(doc: JsonRecord, taskId: string, checked: boolean): ApplyTaskCheckedStatusResult;
|
|
29
|
+
export declare function applyTaskCheckedStatus(doc: JsonRecord, noteId: string, taskId: string, checked: boolean): ApplyTaskCheckedStatusResult;
|
|
29
30
|
export declare function patchTaskMarkdownByOrder(contentMd: string | null, taskOrder: string[], matchedIndices: number[], checked: boolean): string;
|
|
30
31
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DinoxError } from '../../utils/errors.js';
|
|
2
2
|
import { generateId } from '../../utils/id.js';
|
|
3
3
|
import { stripMarkdown } from '../../utils/text.js';
|
|
4
|
+
import { escapeLiteralMarkdownText, findMarkdownCodeRanges, isMarkdownOffsetInSortedRanges, } from '../../utils/tiptapMarkdownSyntax.js';
|
|
5
|
+
import { buildLegacyTaskId } from './taskIdentity.js';
|
|
4
6
|
const CONTENT_TEXT_PREVIEW_LIMIT = 300;
|
|
5
7
|
const DEFAULT_TITLE_PREFIX = '待办清单';
|
|
6
8
|
export function parseTiptapDocFromDb(rawContentJson) {
|
|
@@ -46,10 +48,15 @@ export function appendTasksToDoc(doc, taskTexts) {
|
|
|
46
48
|
});
|
|
47
49
|
return nextDoc;
|
|
48
50
|
}
|
|
49
|
-
export function deriveImageDetailFromContentJson(doc) {
|
|
51
|
+
export function deriveImageDetailFromContentJson(doc, noteId) {
|
|
50
52
|
const root = assertTiptapDoc(doc);
|
|
53
|
+
const normalizedNoteId = noteId.trim();
|
|
54
|
+
if (!normalizedNoteId) {
|
|
55
|
+
throw new DinoxError('Note id is required to derive todo task details');
|
|
56
|
+
}
|
|
51
57
|
const rootContent = ensureNodeContentArray(root);
|
|
52
58
|
const result = [];
|
|
59
|
+
const preorderState = { nextIndex: 0 };
|
|
53
60
|
const nodeStack = [];
|
|
54
61
|
const insideTaskStack = [];
|
|
55
62
|
for (let index = rootContent.length - 1; index >= 0; index -= 1) {
|
|
@@ -66,7 +73,7 @@ export function deriveImageDetailFromContentJson(doc) {
|
|
|
66
73
|
const type = typeof node.type === 'string' ? node.type : '';
|
|
67
74
|
const content = Array.isArray(node.content) ? node.content : [];
|
|
68
75
|
if (type === 'taskItem' && !insideTaskItem) {
|
|
69
|
-
result.push(normalizeTaskItemNode(node));
|
|
76
|
+
result.push(normalizeTaskItemNode(node, normalizedNoteId, preorderState));
|
|
70
77
|
}
|
|
71
78
|
const nextInsideTaskItem = insideTaskItem || type === 'taskItem';
|
|
72
79
|
for (let index = content.length - 1; index >= 0; index -= 1) {
|
|
@@ -77,9 +84,9 @@ export function deriveImageDetailFromContentJson(doc) {
|
|
|
77
84
|
return result;
|
|
78
85
|
}
|
|
79
86
|
export function buildAppendMarkdown(existingMarkdown, taskTexts) {
|
|
80
|
-
const trimmed = (existingMarkdown ?? '').
|
|
87
|
+
const trimmed = (existingMarkdown ?? '').replace(/(?:\r\n|\n|\r)+$/, '');
|
|
81
88
|
const taskBlock = buildTaskMarkdown(taskTexts);
|
|
82
|
-
if (!trimmed) {
|
|
89
|
+
if (!trimmed.trim()) {
|
|
83
90
|
return taskBlock;
|
|
84
91
|
}
|
|
85
92
|
return `${trimmed}\n\n${taskBlock}`;
|
|
@@ -111,14 +118,23 @@ export function buildSummaryTitle(taskTexts) {
|
|
|
111
118
|
}
|
|
112
119
|
export function normalizeTaskTexts(values) {
|
|
113
120
|
const normalized = values
|
|
114
|
-
.map((value) => value
|
|
121
|
+
.map((value) => value
|
|
122
|
+
.replace(/\r\n?/g, '\n')
|
|
123
|
+
.split('\n')
|
|
124
|
+
.map((line) => line.trim())
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(' '))
|
|
115
127
|
.filter((value) => value.length > 0);
|
|
116
128
|
if (normalized.length === 0) {
|
|
117
129
|
throw new DinoxError('At least one non-empty task is required');
|
|
118
130
|
}
|
|
119
131
|
return normalized;
|
|
120
132
|
}
|
|
121
|
-
export function applyTaskCheckedStatus(doc, taskId, checked) {
|
|
133
|
+
export function applyTaskCheckedStatus(doc, noteId, taskId, checked) {
|
|
134
|
+
const normalizedNoteId = noteId.trim();
|
|
135
|
+
if (!normalizedNoteId) {
|
|
136
|
+
throw new DinoxError('Note id is required');
|
|
137
|
+
}
|
|
122
138
|
const normalizedTaskId = taskId.trim();
|
|
123
139
|
if (!normalizedTaskId) {
|
|
124
140
|
throw new DinoxError('taskId is required');
|
|
@@ -126,7 +142,9 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
|
|
|
126
142
|
const nextDoc = cloneJson(assertTiptapDoc(doc));
|
|
127
143
|
const taskOrder = [];
|
|
128
144
|
const matchedIndices = [];
|
|
129
|
-
|
|
145
|
+
const matchedCheckedStates = [];
|
|
146
|
+
let statusChangedCount = 0;
|
|
147
|
+
let preorderIndex = 0;
|
|
130
148
|
const visitNode = (value) => {
|
|
131
149
|
if (!value || typeof value !== 'object') {
|
|
132
150
|
return;
|
|
@@ -134,22 +152,29 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
|
|
|
134
152
|
const node = value;
|
|
135
153
|
const type = typeof node.type === 'string' ? node.type : '';
|
|
136
154
|
if (type === 'taskItem') {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
const explicitTaskId = resolveTaskId(node);
|
|
156
|
+
const currentTaskId = explicitTaskId || buildLegacyTaskId(normalizedNoteId, preorderIndex, node);
|
|
157
|
+
preorderIndex += 1;
|
|
158
|
+
const currentIndex = taskOrder.length;
|
|
159
|
+
taskOrder.push(currentTaskId);
|
|
160
|
+
let attrs = null;
|
|
161
|
+
if (!explicitTaskId) {
|
|
162
|
+
attrs = ensureNodeAttrs(node);
|
|
163
|
+
node.id = currentTaskId;
|
|
164
|
+
attrs.id = currentTaskId;
|
|
165
|
+
}
|
|
166
|
+
if (currentTaskId === normalizedTaskId) {
|
|
167
|
+
matchedIndices.push(currentIndex);
|
|
168
|
+
attrs ??= ensureNodeAttrs(node);
|
|
169
|
+
const previousChecked = normalizeChecked(node.checked) ?? normalizeChecked(attrs.checked) ?? false;
|
|
170
|
+
matchedCheckedStates.push(previousChecked);
|
|
171
|
+
if (previousChecked !== checked) {
|
|
172
|
+
statusChangedCount += 1;
|
|
152
173
|
}
|
|
174
|
+
node.id = currentTaskId;
|
|
175
|
+
node.checked = checked;
|
|
176
|
+
attrs.id = currentTaskId;
|
|
177
|
+
attrs.checked = checked;
|
|
153
178
|
}
|
|
154
179
|
}
|
|
155
180
|
const content = Array.isArray(node.content) ? node.content : [];
|
|
@@ -162,7 +187,8 @@ export function applyTaskCheckedStatus(doc, taskId, checked) {
|
|
|
162
187
|
nextDoc,
|
|
163
188
|
taskOrder,
|
|
164
189
|
matchedIndices,
|
|
165
|
-
|
|
190
|
+
matchedCheckedStates,
|
|
191
|
+
statusChangedCount,
|
|
166
192
|
};
|
|
167
193
|
}
|
|
168
194
|
export function patchTaskMarkdownByOrder(contentMd, taskOrder, matchedIndices, checked) {
|
|
@@ -174,11 +200,15 @@ export function patchTaskMarkdownByOrder(contentMd, taskOrder, matchedIndices, c
|
|
|
174
200
|
throw new DinoxError('Cannot update task markdown: note has task entries in content_json but empty content_md.');
|
|
175
201
|
}
|
|
176
202
|
const lines = markdown.split('\n');
|
|
203
|
+
const codeRanges = findMarkdownCodeRanges(markdown);
|
|
177
204
|
const checklistLineIndexes = [];
|
|
205
|
+
let lineOffset = 0;
|
|
178
206
|
for (let index = 0; index < lines.length; index += 1) {
|
|
179
|
-
|
|
207
|
+
const line = lines[index] ?? '';
|
|
208
|
+
if (!isMarkdownOffsetInSortedRanges(lineOffset, codeRanges) && TASK_MARKDOWN_LINE_RE.test(line)) {
|
|
180
209
|
checklistLineIndexes.push(index);
|
|
181
210
|
}
|
|
211
|
+
lineOffset += line.length + (index < lines.length - 1 ? 1 : 0);
|
|
182
212
|
}
|
|
183
213
|
if (checklistLineIndexes.length !== taskOrder.length) {
|
|
184
214
|
throw new DinoxError(`Cannot update task markdown: checklist line count (${checklistLineIndexes.length}) does not match content_json task count (${taskOrder.length}).`);
|
|
@@ -220,7 +250,7 @@ function buildTaskItemNodes(taskTexts) {
|
|
|
220
250
|
}
|
|
221
251
|
function buildTaskMarkdown(taskTexts) {
|
|
222
252
|
const normalizedTasks = normalizeTaskTexts(taskTexts);
|
|
223
|
-
return normalizedTasks.map((task) => `- [ ] ${task}`).join('\n');
|
|
253
|
+
return normalizedTasks.map((task) => `- [ ] ${escapeLiteralMarkdownText(task)}`).join('\n');
|
|
224
254
|
}
|
|
225
255
|
function assertTiptapDoc(value) {
|
|
226
256
|
if (!value || typeof value !== 'object') {
|
|
@@ -269,11 +299,13 @@ function resolveTaskId(node) {
|
|
|
269
299
|
const attrsId = normalizeString(attrs?.id);
|
|
270
300
|
return attrsId;
|
|
271
301
|
}
|
|
272
|
-
function normalizeTaskItemNode(node) {
|
|
302
|
+
function normalizeTaskItemNode(node, noteId, preorderState) {
|
|
273
303
|
const attrsSource = isObject(node.attrs) ? node.attrs : {};
|
|
274
304
|
const nodeId = normalizeString(node.id);
|
|
275
305
|
const attrsId = normalizeString(attrsSource.id);
|
|
276
|
-
const
|
|
306
|
+
const preorderIndex = preorderState.nextIndex;
|
|
307
|
+
preorderState.nextIndex += 1;
|
|
308
|
+
const taskId = nodeId || attrsId || buildLegacyTaskId(noteId, preorderIndex, node);
|
|
277
309
|
const checked = normalizeChecked(node.checked) ?? normalizeChecked(attrsSource.checked) ?? false;
|
|
278
310
|
return {
|
|
279
311
|
type: 'taskItem',
|
|
@@ -284,9 +316,27 @@ function normalizeTaskItemNode(node) {
|
|
|
284
316
|
id: taskId,
|
|
285
317
|
checked,
|
|
286
318
|
},
|
|
287
|
-
content: Array.isArray(node.content)
|
|
319
|
+
content: Array.isArray(node.content)
|
|
320
|
+
? node.content.map((child) => normalizeTaskDetailContent(child, noteId, preorderState))
|
|
321
|
+
: [],
|
|
288
322
|
};
|
|
289
323
|
}
|
|
324
|
+
function normalizeTaskDetailContent(value, noteId, preorderState) {
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
return value.map((item) => normalizeTaskDetailContent(item, noteId, preorderState));
|
|
327
|
+
}
|
|
328
|
+
if (!isObject(value)) {
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
if (value.type === 'taskItem') {
|
|
332
|
+
return normalizeTaskItemNode(value, noteId, preorderState);
|
|
333
|
+
}
|
|
334
|
+
const normalized = {};
|
|
335
|
+
for (const [key, child] of Object.entries(value)) {
|
|
336
|
+
normalized[key] = normalizeTaskDetailContent(child, noteId, preorderState);
|
|
337
|
+
}
|
|
338
|
+
return normalized;
|
|
339
|
+
}
|
|
290
340
|
function normalizeChecked(value) {
|
|
291
341
|
if (value === true)
|
|
292
342
|
return true;
|