@dinoxx/dinox-cli 1.0.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 +294 -0
- package/dist/auth/userInfo.d.ts +14 -0
- package/dist/auth/userInfo.js +115 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/cliTypes.d.ts +6 -0
- package/dist/cliTypes.js +1 -0
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +193 -0
- package/dist/commands/boxes/index.d.ts +2 -0
- package/dist/commands/boxes/index.js +107 -0
- package/dist/commands/boxes/repo.d.ts +21 -0
- package/dist/commands/boxes/repo.js +154 -0
- package/dist/commands/config/index.d.ts +2 -0
- package/dist/commands/config/index.js +67 -0
- package/dist/commands/info/index.d.ts +2 -0
- package/dist/commands/info/index.js +20 -0
- package/dist/commands/notes/index.d.ts +2 -0
- package/dist/commands/notes/index.js +271 -0
- package/dist/commands/notes/repo.d.ts +70 -0
- package/dist/commands/notes/repo.js +674 -0
- package/dist/commands/notes/searchTime.d.ts +9 -0
- package/dist/commands/notes/searchTime.js +85 -0
- package/dist/commands/prompt/index.d.ts +2 -0
- package/dist/commands/prompt/index.js +51 -0
- package/dist/commands/prompt/repo.d.ts +6 -0
- package/dist/commands/prompt/repo.js +18 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +68 -0
- package/dist/commands/tags/index.d.ts +2 -0
- package/dist/commands/tags/index.js +120 -0
- package/dist/commands/tags/repo.d.ts +14 -0
- package/dist/commands/tags/repo.js +247 -0
- package/dist/config/keys.d.ts +9 -0
- package/dist/config/keys.js +17 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +39 -0
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +56 -0
- package/dist/config/serviceEndpoints.d.ts +3 -0
- package/dist/config/serviceEndpoints.js +3 -0
- package/dist/config/store.d.ts +5 -0
- package/dist/config/store.js +87 -0
- package/dist/config/types.d.ts +51 -0
- package/dist/config/types.js +1 -0
- package/dist/dinox.d.ts +2 -0
- package/dist/dinox.js +50 -0
- package/dist/powersync/connector.d.ts +21 -0
- package/dist/powersync/connector.js +58 -0
- package/dist/powersync/runtime.d.ts +37 -0
- package/dist/powersync/runtime.js +107 -0
- package/dist/powersync/schema/content.d.ts +76 -0
- package/dist/powersync/schema/content.js +76 -0
- package/dist/powersync/schema/index.d.ts +371 -0
- package/dist/powersync/schema/index.js +35 -0
- package/dist/powersync/schema/local.d.ts +68 -0
- package/dist/powersync/schema/local.js +83 -0
- package/dist/powersync/schema/note.d.ts +34 -0
- package/dist/powersync/schema/note.js +34 -0
- package/dist/powersync/schema/notesExtras.d.ts +62 -0
- package/dist/powersync/schema/notesExtras.js +71 -0
- package/dist/powersync/schema/projects.d.ts +101 -0
- package/dist/powersync/schema/projects.js +101 -0
- package/dist/powersync/schema/tags.d.ts +37 -0
- package/dist/powersync/schema/tags.js +37 -0
- package/dist/powersync/tokenIndex.d.ts +17 -0
- package/dist/powersync/tokenIndex.js +202 -0
- package/dist/powersync/uploader.d.ts +7 -0
- package/dist/powersync/uploader.js +134 -0
- package/dist/utils/argValue.d.ts +1 -0
- package/dist/utils/argValue.js +17 -0
- package/dist/utils/errors.d.ts +10 -0
- package/dist/utils/errors.js +17 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.js +4 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +10 -0
- package/dist/utils/text.d.ts +1 -0
- package/dist/utils/text.js +35 -0
- package/dist/utils/time.d.ts +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/tiptapMarkdown.d.ts +6 -0
- package/dist/utils/tiptapMarkdown.js +149 -0
- package/dist/utils/tokenize.d.ts +1 -0
- package/dist/utils/tokenize.js +56 -0
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.js +21 -0
- package/package.json +63 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { loadConfig } from '../../config/store.js';
|
|
2
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
import { printYaml } from '../../utils/output.js';
|
|
5
|
+
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
6
|
+
import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
|
|
7
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
8
|
+
import { addBox, listBoxes } from './repo.js';
|
|
9
|
+
function parseSyncTimeoutMs(value) {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
15
|
+
throw new DinoxError('--sync-timeout must be a positive integer');
|
|
16
|
+
}
|
|
17
|
+
return Math.trunc(parsed);
|
|
18
|
+
}
|
|
19
|
+
async function syncBeforeBoxCommand(db, offline, timeoutMs) {
|
|
20
|
+
if (offline) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
|
|
24
|
+
await syncNoteTokenIndex(db);
|
|
25
|
+
}
|
|
26
|
+
async function runBoxListCommand(options, command) {
|
|
27
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
28
|
+
const offline = Boolean(options.offline ?? globals.offline);
|
|
29
|
+
const config = resolveConfig(await loadConfig());
|
|
30
|
+
const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
31
|
+
const { db } = await connectPowerSync({ config, offline, timeoutMs });
|
|
32
|
+
try {
|
|
33
|
+
await syncBeforeBoxCommand(db, offline, timeoutMs);
|
|
34
|
+
const boxes = await listBoxes(db);
|
|
35
|
+
printYaml(boxes);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await db.close().catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function registerBoxesCommand(program) {
|
|
42
|
+
const box = program.command('box').description('Manage zettel boxes');
|
|
43
|
+
box
|
|
44
|
+
.command('list')
|
|
45
|
+
.description('List zettel boxes (id and name) from c_zettel_box')
|
|
46
|
+
.option('--offline', 'skip connect/sync and only use local cache')
|
|
47
|
+
.option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
|
|
48
|
+
.action(runBoxListCommand);
|
|
49
|
+
box
|
|
50
|
+
.command('add')
|
|
51
|
+
.description('Create a new zettel box in c_zettel_box')
|
|
52
|
+
.argument('[name]', 'box name')
|
|
53
|
+
.option('--name <string>', 'box name (alternative to positional name)')
|
|
54
|
+
.option('--description <string>', 'box purpose/usage description (helps AI route notes to this box)')
|
|
55
|
+
.option('--color <string>', 'box color')
|
|
56
|
+
.option('--json', 'output machine-readable YAML')
|
|
57
|
+
.option('--offline', 'skip connect/sync and only use local cache')
|
|
58
|
+
.option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
|
|
59
|
+
.action(async (nameArg, options, command) => {
|
|
60
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
61
|
+
const offline = Boolean(options.offline ?? globals.offline);
|
|
62
|
+
const jsonOutput = Boolean(options.json ?? globals.json);
|
|
63
|
+
const optionName = typeof options.name === 'string' ? options.name : undefined;
|
|
64
|
+
const name = (nameArg ?? optionName ?? '').trim();
|
|
65
|
+
if (!name) {
|
|
66
|
+
throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
|
|
67
|
+
}
|
|
68
|
+
if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
|
|
69
|
+
throw new DinoxError('Conflicting box name values from positional argument and --name option.');
|
|
70
|
+
}
|
|
71
|
+
const rawConfig = await loadConfig();
|
|
72
|
+
const config = resolveConfig(rawConfig);
|
|
73
|
+
const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
74
|
+
const identity = await resolveAuthIdentity(rawConfig, config);
|
|
75
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
76
|
+
try {
|
|
77
|
+
await syncBeforeBoxCommand(db, offline, timeoutMs);
|
|
78
|
+
const result = await addBox(db, {
|
|
79
|
+
name,
|
|
80
|
+
userId: identity.userId,
|
|
81
|
+
description: typeof options.description === 'string' ? options.description : undefined,
|
|
82
|
+
color: typeof options.color === 'string' ? options.color : undefined,
|
|
83
|
+
});
|
|
84
|
+
if (!offline && !config.powersync.uploadBaseUrl) {
|
|
85
|
+
console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
|
|
86
|
+
}
|
|
87
|
+
if (!offline) {
|
|
88
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
|
|
89
|
+
}
|
|
90
|
+
const payload = { ok: true, stale: offline ? true : stale, ...result };
|
|
91
|
+
if (jsonOutput) {
|
|
92
|
+
printYaml(payload);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(result.id);
|
|
96
|
+
if (stale && !offline) {
|
|
97
|
+
console.log('warning: sync timed out; results may be stale');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await db.close().catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
box.action((_options, command) => {
|
|
105
|
+
command.outputHelp();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
export type BoxListItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
export type AddBoxInput = {
|
|
8
|
+
name: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
export type AddBoxResult = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
color: string;
|
|
18
|
+
restored: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function listBoxes(db: AbstractPowerSyncDatabase): Promise<BoxListItem[]>;
|
|
21
|
+
export declare function addBox(db: AbstractPowerSyncDatabase, input: AddBoxInput): Promise<AddBoxResult>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { generateId } from '../../utils/id.js';
|
|
2
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
3
|
+
function normalizeName(value) {
|
|
4
|
+
return value.trim();
|
|
5
|
+
}
|
|
6
|
+
function normalizeLookup(value) {
|
|
7
|
+
return normalizeName(value).toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
async function getBoxColumns(db) {
|
|
10
|
+
const rows = await db.getAll('PRAGMA table_info(c_zettel_box)', []);
|
|
11
|
+
return new Set(rows.map((row) => row.name).filter(Boolean));
|
|
12
|
+
}
|
|
13
|
+
async function listBoxRowsForWrite(db) {
|
|
14
|
+
return db.getAll(`
|
|
15
|
+
SELECT
|
|
16
|
+
TRIM(COALESCE(id, '')) AS id,
|
|
17
|
+
TRIM(COALESCE(name, '')) AS name,
|
|
18
|
+
TRIM(COALESCE(description, '')) AS description,
|
|
19
|
+
TRIM(COALESCE(color, '')) AS color,
|
|
20
|
+
is_del,
|
|
21
|
+
TRIM(COALESCE(user_no, '')) AS user_no
|
|
22
|
+
FROM c_zettel_box
|
|
23
|
+
WHERE TRIM(COALESCE(id, '')) <> ''
|
|
24
|
+
ORDER BY
|
|
25
|
+
CASE WHEN is_del IS NULL OR is_del = 0 THEN 0 ELSE 1 END ASC,
|
|
26
|
+
updated_at DESC,
|
|
27
|
+
created_at DESC,
|
|
28
|
+
id DESC
|
|
29
|
+
`, []);
|
|
30
|
+
}
|
|
31
|
+
export async function listBoxes(db) {
|
|
32
|
+
const rows = await db.getAll(`
|
|
33
|
+
SELECT DISTINCT
|
|
34
|
+
TRIM(COALESCE(id, '')) AS id,
|
|
35
|
+
TRIM(COALESCE(name, '')) AS name,
|
|
36
|
+
TRIM(COALESCE(description, '')) AS description
|
|
37
|
+
FROM c_zettel_box
|
|
38
|
+
WHERE (is_del IS NULL OR is_del = 0)
|
|
39
|
+
AND TRIM(COALESCE(id, '')) <> ''
|
|
40
|
+
ORDER BY name COLLATE NOCASE ASC, id ASC
|
|
41
|
+
`, []);
|
|
42
|
+
return rows
|
|
43
|
+
.map((row) => ({
|
|
44
|
+
id: row.id?.trim() ?? '',
|
|
45
|
+
name: row.name?.trim() ?? '',
|
|
46
|
+
description: row.description?.trim() ?? '',
|
|
47
|
+
}))
|
|
48
|
+
.filter((row) => row.id.length > 0);
|
|
49
|
+
}
|
|
50
|
+
export async function addBox(db, input) {
|
|
51
|
+
const userId = input.userId.trim();
|
|
52
|
+
const name = normalizeName(input.name);
|
|
53
|
+
if (!name) {
|
|
54
|
+
throw new DinoxError('Box name is required');
|
|
55
|
+
}
|
|
56
|
+
const description = normalizeName(input.description ?? '');
|
|
57
|
+
const color = normalizeName(input.color ?? '');
|
|
58
|
+
const lookup = normalizeLookup(name);
|
|
59
|
+
const columnNames = await getBoxColumns(db);
|
|
60
|
+
const requiredColumns = ['id', 'name', 'user_no'];
|
|
61
|
+
const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
|
|
62
|
+
if (missingRequired.length > 0) {
|
|
63
|
+
throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
if (!userId) {
|
|
66
|
+
throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
|
|
67
|
+
}
|
|
68
|
+
const rows = await listBoxRowsForWrite(db);
|
|
69
|
+
const sameNameRows = rows.filter((row) => normalizeLookup(row.name ?? '') === lookup);
|
|
70
|
+
const activeRows = sameNameRows.filter((row) => row.is_del !== 1);
|
|
71
|
+
if (activeRows.length > 0) {
|
|
72
|
+
const ids = activeRows.map((row) => row.id?.trim() ?? '').filter(Boolean);
|
|
73
|
+
throw new DinoxError(`Zettel box already exists: ${name}`, {
|
|
74
|
+
details: {
|
|
75
|
+
type: 'box_exists',
|
|
76
|
+
name,
|
|
77
|
+
ids,
|
|
78
|
+
question: 'This box already exists. Do you want to keep using the existing box?',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const timestamp = new Date().toISOString();
|
|
83
|
+
const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
|
|
84
|
+
if (deletedRow?.id) {
|
|
85
|
+
const setParts = [];
|
|
86
|
+
const params = [];
|
|
87
|
+
const setValue = (column, value) => {
|
|
88
|
+
if (!columnNames.has(column)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setParts.push(`${column} = ?`);
|
|
92
|
+
params.push(value);
|
|
93
|
+
};
|
|
94
|
+
setValue('name', name);
|
|
95
|
+
setValue('is_del', 0);
|
|
96
|
+
setValue('updated_at', timestamp);
|
|
97
|
+
if (description) {
|
|
98
|
+
setValue('description', description);
|
|
99
|
+
}
|
|
100
|
+
if (color) {
|
|
101
|
+
setValue('color', color);
|
|
102
|
+
}
|
|
103
|
+
setValue('user_no', userId);
|
|
104
|
+
if (setParts.length > 0) {
|
|
105
|
+
params.push(deletedRow.id);
|
|
106
|
+
await db.execute(`
|
|
107
|
+
UPDATE c_zettel_box
|
|
108
|
+
SET ${setParts.join(', ')}
|
|
109
|
+
WHERE id = ?
|
|
110
|
+
`, params);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: deletedRow.id,
|
|
114
|
+
name,
|
|
115
|
+
description: description || (deletedRow.description?.trim() ?? ''),
|
|
116
|
+
color: color || (deletedRow.color?.trim() ?? ''),
|
|
117
|
+
restored: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const id = generateId();
|
|
121
|
+
const insertEntries = [];
|
|
122
|
+
const pushInsert = (column, value) => {
|
|
123
|
+
if (!columnNames.has(column)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
insertEntries.push([column, value]);
|
|
127
|
+
};
|
|
128
|
+
pushInsert('id', id);
|
|
129
|
+
pushInsert('name', name);
|
|
130
|
+
pushInsert('description', description || null);
|
|
131
|
+
pushInsert('color', color || null);
|
|
132
|
+
pushInsert('priority', 0);
|
|
133
|
+
pushInsert('is_show', 1);
|
|
134
|
+
pushInsert('cover_url', '');
|
|
135
|
+
pushInsert('created_at', timestamp);
|
|
136
|
+
pushInsert('updated_at', timestamp);
|
|
137
|
+
pushInsert('extra_data', '{}');
|
|
138
|
+
pushInsert('user_no', userId);
|
|
139
|
+
pushInsert('is_del', 0);
|
|
140
|
+
const insertColumns = insertEntries.map(([column]) => column);
|
|
141
|
+
const placeholders = insertColumns.map(() => '?').join(', ');
|
|
142
|
+
const insertParams = insertEntries.map(([, value]) => value);
|
|
143
|
+
await db.execute(`
|
|
144
|
+
INSERT INTO c_zettel_box (${insertColumns.join(', ')})
|
|
145
|
+
VALUES (${placeholders})
|
|
146
|
+
`, insertParams);
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
name,
|
|
150
|
+
description,
|
|
151
|
+
color,
|
|
152
|
+
restored: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isConfigKey, isFixedConfigKey, isReadableConfigKey } from '../../config/keys.js';
|
|
2
|
+
import { getConfigValue, loadConfig, setConfigValue } from '../../config/store.js';
|
|
3
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
4
|
+
import { redactAuthorization } from '../../utils/redact.js';
|
|
5
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
6
|
+
import { printYaml } from '../../utils/output.js';
|
|
7
|
+
function redactConfigForDisplay(config) {
|
|
8
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
12
|
+
const auth = clone?.auth;
|
|
13
|
+
if (auth && typeof auth === 'object') {
|
|
14
|
+
auth.authorization = redactAuthorization(auth.authorization);
|
|
15
|
+
}
|
|
16
|
+
return clone;
|
|
17
|
+
}
|
|
18
|
+
function getByPath(value, keyPath) {
|
|
19
|
+
return keyPath.split('.').reduce((current, segment) => {
|
|
20
|
+
if (!current || typeof current !== 'object') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return current[segment];
|
|
24
|
+
}, value);
|
|
25
|
+
}
|
|
26
|
+
export function registerConfigCommands(program) {
|
|
27
|
+
const configCmd = program.command('config').description('Read and write Dinox configuration');
|
|
28
|
+
configCmd
|
|
29
|
+
.command('get')
|
|
30
|
+
.description('Get configuration value')
|
|
31
|
+
.argument('[key]', 'config key (dot path)')
|
|
32
|
+
.action(async (key) => {
|
|
33
|
+
if (key && !isReadableConfigKey(key)) {
|
|
34
|
+
throw new DinoxError(`Unknown config key: ${key}`);
|
|
35
|
+
}
|
|
36
|
+
if (key && isFixedConfigKey(key)) {
|
|
37
|
+
const resolved = resolveConfig(await loadConfig());
|
|
38
|
+
printYaml(getByPath(resolved, key));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const value = await getConfigValue(key);
|
|
42
|
+
const redacted = key ? value : redactConfigForDisplay(value);
|
|
43
|
+
printYaml(redacted);
|
|
44
|
+
});
|
|
45
|
+
configCmd
|
|
46
|
+
.command('set')
|
|
47
|
+
.description('Set configuration value')
|
|
48
|
+
.argument('<key>', 'config key (dot path)')
|
|
49
|
+
.argument('<value>', 'value')
|
|
50
|
+
.action(async (key, value) => {
|
|
51
|
+
if (isFixedConfigKey(key)) {
|
|
52
|
+
throw new DinoxError(`${key} is fixed and cannot be changed`);
|
|
53
|
+
}
|
|
54
|
+
if (!isConfigKey(key)) {
|
|
55
|
+
throw new DinoxError(`Unknown config key: ${key}`);
|
|
56
|
+
}
|
|
57
|
+
if (key === 'sync.timeoutMs') {
|
|
58
|
+
const parsed = Number(value);
|
|
59
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
60
|
+
throw new DinoxError('sync.timeoutMs must be a positive integer');
|
|
61
|
+
}
|
|
62
|
+
await setConfigValue(key, Math.trunc(parsed));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await setConfigValue(key, value);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { printYaml } from '../../utils/output.js';
|
|
2
|
+
import { getPackageVersion } from '../../utils/version.js';
|
|
3
|
+
export function registerInfoCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('info')
|
|
6
|
+
.description('Show CLI version info')
|
|
7
|
+
.action((_options, command) => {
|
|
8
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
9
|
+
const jsonOutput = Boolean(globals.json);
|
|
10
|
+
const payload = {
|
|
11
|
+
name: 'dino',
|
|
12
|
+
version: getPackageVersion(),
|
|
13
|
+
};
|
|
14
|
+
if (jsonOutput) {
|
|
15
|
+
printYaml(payload);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(payload.version);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { loadConfig } from '../../config/store.js';
|
|
2
|
+
import { resolveConfig } from '../../config/resolve.js';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
import { readStringOrFile } from '../../utils/argValue.js';
|
|
5
|
+
import { generateId } from '../../utils/id.js';
|
|
6
|
+
import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
|
|
7
|
+
import { printYaml } from '../../utils/output.js';
|
|
8
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
9
|
+
import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
|
|
10
|
+
import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
|
|
11
|
+
import { createNote, getNote, getNoteDetail, resolveZettelBoxIdsForCreate, searchNotes, softDeleteNote, validateTagsForCreate, } from './repo.js';
|
|
12
|
+
import { parseCreatedAtRange } from './searchTime.js';
|
|
13
|
+
function parseSyncTimeoutMs(value) {
|
|
14
|
+
if (typeof value !== 'string') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
19
|
+
throw new DinoxError('--sync-timeout must be a positive integer');
|
|
20
|
+
}
|
|
21
|
+
return Math.trunc(parsed);
|
|
22
|
+
}
|
|
23
|
+
function parseNoteType(value) {
|
|
24
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
25
|
+
if (normalized === 'note' || normalized === 'crawl') {
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
throw new DinoxError('--type must be one of: note, crawl');
|
|
29
|
+
}
|
|
30
|
+
async function parseStringInput(value) {
|
|
31
|
+
return readStringOrFile(value);
|
|
32
|
+
}
|
|
33
|
+
function parseListInput(raw, optionName) {
|
|
34
|
+
const value = raw.trim();
|
|
35
|
+
if (!value) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (value.startsWith('[')) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(value);
|
|
41
|
+
if (!Array.isArray(parsed)) {
|
|
42
|
+
throw new DinoxError(`${optionName} must be a JSON array`);
|
|
43
|
+
}
|
|
44
|
+
return Array.from(new Set(parsed
|
|
45
|
+
.map((entry) => String(entry).trim())
|
|
46
|
+
.filter((entry) => entry.length > 0)));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof DinoxError) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
throw new DinoxError(`Invalid list for ${optionName}`, { cause: error });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(new Set(value
|
|
56
|
+
.split(/[\n,,]/g)
|
|
57
|
+
.map((entry) => entry.trim())
|
|
58
|
+
.filter((entry) => entry.length > 0)));
|
|
59
|
+
}
|
|
60
|
+
async function parseStringListInput(value, optionName) {
|
|
61
|
+
const raw = await readStringOrFile(value);
|
|
62
|
+
return parseListInput(raw, optionName);
|
|
63
|
+
}
|
|
64
|
+
async function syncBeforeNoteCommand(db, offline, timeoutMs) {
|
|
65
|
+
if (offline) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
|
|
69
|
+
await syncNoteTokenIndex(db);
|
|
70
|
+
}
|
|
71
|
+
export function registerNoteCommands(program) {
|
|
72
|
+
const note = program.command('note').description('Manage notes');
|
|
73
|
+
note
|
|
74
|
+
.command('search')
|
|
75
|
+
.description('Search notes and return note list for AI agents')
|
|
76
|
+
.argument('[query]', 'search query')
|
|
77
|
+
.option('--tags <expr>', 'tag expression with AND/OR/NOT, e.g. \'(work OR life) AND NOT archived\'')
|
|
78
|
+
.option('--from <date>', 'created_at start (YYYY-MM-DD or ISO datetime)')
|
|
79
|
+
.option('--to <date>', 'created_at end (YYYY-MM-DD or ISO datetime)')
|
|
80
|
+
.option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
|
|
81
|
+
.option('--include-deleted', 'include soft-deleted notes')
|
|
82
|
+
.action(async (query, options, command) => {
|
|
83
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
84
|
+
const offline = Boolean(globals.offline);
|
|
85
|
+
const config = resolveConfig(await loadConfig());
|
|
86
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
87
|
+
const { db } = await connectPowerSync({ config, offline, timeoutMs });
|
|
88
|
+
try {
|
|
89
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
90
|
+
const keyword = query?.trim() ?? '';
|
|
91
|
+
const tagsExpression = typeof options.tags === 'string' ? options.tags.trim() : '';
|
|
92
|
+
const { createdAtFrom, createdAtTo } = parseCreatedAtRange({
|
|
93
|
+
from: options.from,
|
|
94
|
+
to: options.to,
|
|
95
|
+
days: options.days,
|
|
96
|
+
});
|
|
97
|
+
const rows = await searchNotes(db, keyword, {
|
|
98
|
+
includeDeleted: Boolean(options.includeDeleted),
|
|
99
|
+
tagsExpression: tagsExpression || undefined,
|
|
100
|
+
createdAtFrom,
|
|
101
|
+
createdAtTo,
|
|
102
|
+
});
|
|
103
|
+
printYaml(rows);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await db.close().catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
note
|
|
110
|
+
.command('get')
|
|
111
|
+
.description('Get note by id')
|
|
112
|
+
.argument('<id>', 'note id')
|
|
113
|
+
.action(async (id, _options, command) => {
|
|
114
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
115
|
+
const offline = Boolean(globals.offline);
|
|
116
|
+
const jsonOutput = Boolean(globals.json);
|
|
117
|
+
const config = resolveConfig(await loadConfig());
|
|
118
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
119
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
120
|
+
try {
|
|
121
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
122
|
+
const note = await getNote(db, id);
|
|
123
|
+
if (!note) {
|
|
124
|
+
throw new DinoxError(`Note not found: ${id}`);
|
|
125
|
+
}
|
|
126
|
+
if (jsonOutput) {
|
|
127
|
+
printYaml({ stale, note });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (stale && !offline) {
|
|
131
|
+
console.log('warning: sync timed out; result may be stale');
|
|
132
|
+
}
|
|
133
|
+
console.log(`${note.id}`);
|
|
134
|
+
console.log(`title: ${note.title ?? ''}`);
|
|
135
|
+
if (note.updated_at) {
|
|
136
|
+
console.log(`updated_at: ${note.updated_at}`);
|
|
137
|
+
}
|
|
138
|
+
if (note.is_del) {
|
|
139
|
+
console.log('is_del: 1');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await db.close().catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
note
|
|
147
|
+
.command('detail')
|
|
148
|
+
.description('Get full note details by id')
|
|
149
|
+
.argument('<id>', 'note id')
|
|
150
|
+
.action(async (id, _options, command) => {
|
|
151
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
152
|
+
const offline = Boolean(globals.offline);
|
|
153
|
+
const config = resolveConfig(await loadConfig());
|
|
154
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
155
|
+
const { db } = await connectPowerSync({ config, offline, timeoutMs });
|
|
156
|
+
try {
|
|
157
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
158
|
+
const note = await getNoteDetail(db, id);
|
|
159
|
+
if (!note) {
|
|
160
|
+
throw new DinoxError(`Note not found: ${id}`);
|
|
161
|
+
}
|
|
162
|
+
printYaml(note);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await db.close().catch(() => undefined);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
note
|
|
169
|
+
.command('create')
|
|
170
|
+
.description('Create a new note')
|
|
171
|
+
.requiredOption('--title <string>', 'note title')
|
|
172
|
+
.requiredOption('--content <string|@file>', 'markdown content')
|
|
173
|
+
.option('--type <note|crawl>', 'note type: note or crawl', 'crawl')
|
|
174
|
+
.option('--tags <string|@file>', 'tag list (JSON array or comma/newline-separated)')
|
|
175
|
+
.option('--zettel_boxes <string|@file>', 'zettel box names (JSON array or comma/newline-separated)')
|
|
176
|
+
.action(async (options, command) => {
|
|
177
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
178
|
+
const offline = Boolean(globals.offline);
|
|
179
|
+
const jsonOutput = Boolean(globals.json);
|
|
180
|
+
const rawConfig = await loadConfig();
|
|
181
|
+
const config = resolveConfig(rawConfig);
|
|
182
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
183
|
+
const identity = await resolveAuthIdentity(rawConfig, config);
|
|
184
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
185
|
+
try {
|
|
186
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
187
|
+
const contentMd = await parseStringInput(options.content);
|
|
188
|
+
const { contentJson, contentText } = convertMarkdownToNoteContent(contentMd);
|
|
189
|
+
const tagsInput = typeof options.tags === 'string'
|
|
190
|
+
? await parseStringListInput(options.tags, '--tags')
|
|
191
|
+
: [];
|
|
192
|
+
const tags = await validateTagsForCreate(db, tagsInput);
|
|
193
|
+
const zettelBoxesOption = typeof options.zettel_boxes === 'string'
|
|
194
|
+
? options.zettel_boxes
|
|
195
|
+
: typeof options.zettelBoxes === 'string'
|
|
196
|
+
? options.zettelBoxes
|
|
197
|
+
: undefined;
|
|
198
|
+
const zettelBoxNames = zettelBoxesOption
|
|
199
|
+
? await parseStringListInput(zettelBoxesOption, '--zettel_boxes')
|
|
200
|
+
: [];
|
|
201
|
+
const zettelBoxIds = await resolveZettelBoxIdsForCreate(db, zettelBoxNames);
|
|
202
|
+
const noteType = parseNoteType(options.type);
|
|
203
|
+
const id = generateId();
|
|
204
|
+
await createNote(db, {
|
|
205
|
+
id,
|
|
206
|
+
title: String(options.title),
|
|
207
|
+
noteType,
|
|
208
|
+
contentMd,
|
|
209
|
+
contentJson,
|
|
210
|
+
contentText,
|
|
211
|
+
tags,
|
|
212
|
+
zettelBoxIds,
|
|
213
|
+
userId: identity.userId,
|
|
214
|
+
});
|
|
215
|
+
if (!offline && !config.powersync.uploadBaseUrl) {
|
|
216
|
+
console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
|
|
217
|
+
}
|
|
218
|
+
if (!offline) {
|
|
219
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
|
|
220
|
+
}
|
|
221
|
+
const payload = { ok: true, id, stale: offline ? true : stale };
|
|
222
|
+
if (jsonOutput) {
|
|
223
|
+
printYaml(payload);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(id);
|
|
227
|
+
if (stale && !offline) {
|
|
228
|
+
console.log('warning: sync timed out; local cache may be stale');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await db.close().catch(() => undefined);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
note
|
|
237
|
+
.command('delete')
|
|
238
|
+
.description('Soft-delete a note (is_del=1)')
|
|
239
|
+
.argument('<id>', 'note id')
|
|
240
|
+
.action(async (id, _options, command) => {
|
|
241
|
+
const globals = command.optsWithGlobals?.() ?? {};
|
|
242
|
+
const offline = Boolean(globals.offline);
|
|
243
|
+
const jsonOutput = Boolean(globals.json);
|
|
244
|
+
const config = resolveConfig(await loadConfig());
|
|
245
|
+
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
|
|
246
|
+
const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
|
|
247
|
+
try {
|
|
248
|
+
await syncBeforeNoteCommand(db, offline, timeoutMs);
|
|
249
|
+
await softDeleteNote(db, id);
|
|
250
|
+
if (!offline && !config.powersync.uploadBaseUrl) {
|
|
251
|
+
console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
|
|
252
|
+
}
|
|
253
|
+
if (!offline) {
|
|
254
|
+
await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
|
|
255
|
+
}
|
|
256
|
+
const payload = { ok: true, id, stale: offline ? true : stale };
|
|
257
|
+
if (jsonOutput) {
|
|
258
|
+
printYaml(payload);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.log('OK');
|
|
262
|
+
if (stale && !offline) {
|
|
263
|
+
console.log('warning: sync timed out; local cache may be stale');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await db.close().catch(() => undefined);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|