@dinoxx/dinox-cli 2.2.0 → 2.3.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/dist/cli.js +4 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/notes/actions.js +47 -18
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +9 -4
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/write.d.ts +2 -0
- package/dist/commands/notes/repo/write.js +1 -1
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +29 -5
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1538 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1117 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +2 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +312 -0
- package/dist/vendor/electron/shared/note-properties.js +982 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1710 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +6 -3
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- package/dist/commands/savedViews/semantics.js +0 -738
package/dist/cli.js
CHANGED
|
@@ -10,12 +10,14 @@ import { registerMetaCommands } from './commands/meta/index.js';
|
|
|
10
10
|
import { registerNoteCommands } from './commands/notes/index.js';
|
|
11
11
|
import { registerPromptCommands } from './commands/prompt/index.js';
|
|
12
12
|
import { registerSchemaCommands } from './commands/schema/index.js';
|
|
13
|
+
import { registerPropertyCommands } from './commands/properties/index.js';
|
|
13
14
|
import { registerSavedViewCommands } from './commands/savedViews/index.js';
|
|
14
15
|
import { registerSkillsCommands } from './commands/skills/index.js';
|
|
15
16
|
import { registerSyncCommand } from './commands/sync.js';
|
|
16
17
|
import { registerStorageCommands } from './commands/storage/index.js';
|
|
17
18
|
import { registerUpdateCommand } from './commands/update.js';
|
|
18
19
|
import { registerTagsCommand } from './commands/tags/index.js';
|
|
20
|
+
import { registerTemplateCommands } from './commands/templates/index.js';
|
|
19
21
|
import { registerTodoCommands } from './commands/todo/index.js';
|
|
20
22
|
import { getPackageVersion } from './utils/version.js';
|
|
21
23
|
import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
|
|
@@ -85,6 +87,8 @@ export function createCli() {
|
|
|
85
87
|
registerUpdateCommand(program);
|
|
86
88
|
registerPromptCommands(program);
|
|
87
89
|
registerSavedViewCommands(program);
|
|
90
|
+
registerPropertyCommands(program);
|
|
91
|
+
registerTemplateCommands(program);
|
|
88
92
|
registerSchemaCommands(program);
|
|
89
93
|
registerSkillsCommands(program);
|
|
90
94
|
registerStorageCommands(program);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function parseIntegerOption(value: string): number;
|
|
2
|
+
export declare function parsePositiveIntegerOption(value: string): number;
|
|
3
|
+
export declare function parseNonNegativeIntegerOption(value: string): number;
|
|
4
|
+
/** Commander collector for repeatable options. */
|
|
5
|
+
export declare function collectOption(value: string, previous: unknown): string[];
|
|
6
|
+
export declare function parseTimeZoneOption(value: string): string;
|
|
7
|
+
/** Reads an inline JSON value or `@file` and parses it. */
|
|
8
|
+
export declare function readJsonOption(value: unknown, label: string, schemaPath: string): Promise<unknown>;
|
|
9
|
+
export declare function requiredArgument(value: string | undefined, label: string): string;
|
|
10
|
+
export declare function assertExclusive(conflicts: Array<[boolean, string]>): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Option parsers shared by the commands backed by the vendored desktop domain
|
|
2
|
+
// (view, prop, template).
|
|
3
|
+
import { readStringOrFile } from '../../utils/argValue.js';
|
|
4
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
5
|
+
export function parseIntegerOption(value) {
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
if (!/^-?\d+$/.test(normalized)) {
|
|
8
|
+
throw new DinoxError('Expected an integer', { code: 'USAGE_ERROR' });
|
|
9
|
+
}
|
|
10
|
+
const parsed = Number(normalized);
|
|
11
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
12
|
+
throw new DinoxError('Expected a safe integer', { code: 'USAGE_ERROR' });
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
export function parsePositiveIntegerOption(value) {
|
|
17
|
+
const parsed = parseIntegerOption(value);
|
|
18
|
+
if (parsed <= 0)
|
|
19
|
+
throw new DinoxError('Expected a positive integer', { code: 'USAGE_ERROR' });
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
export function parseNonNegativeIntegerOption(value) {
|
|
23
|
+
const parsed = parseIntegerOption(value);
|
|
24
|
+
if (parsed < 0)
|
|
25
|
+
throw new DinoxError('Expected a non-negative integer', { code: 'USAGE_ERROR' });
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
/** Commander collector for repeatable options. */
|
|
29
|
+
export function collectOption(value, previous) {
|
|
30
|
+
return [...(Array.isArray(previous) ? previous : []), value];
|
|
31
|
+
}
|
|
32
|
+
export function parseTimeZoneOption(value) {
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
try {
|
|
35
|
+
new Intl.DateTimeFormat('en-US', { timeZone: normalized });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new DinoxError(`Invalid IANA time zone: ${value}`, { code: 'SAVED_VIEW_INVALID_TIME_ZONE' });
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
/** Reads an inline JSON value or `@file` and parses it. */
|
|
43
|
+
export async function readJsonOption(value, label, schemaPath) {
|
|
44
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
45
|
+
throw new DinoxError(`${label} requires inline JSON or @file`, { code: 'USAGE_ERROR' });
|
|
46
|
+
}
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await readStringOrFile(value);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new DinoxError(`Failed to read ${label}`, { code: 'USAGE_ERROR', cause: error });
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new DinoxError(`${label} must be valid JSON`, {
|
|
59
|
+
code: 'USAGE_ERROR',
|
|
60
|
+
cause: error,
|
|
61
|
+
suggestion: `Pass inline JSON or use @file. See \`dino schema ${schemaPath} --format json\`.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function requiredArgument(value, label) {
|
|
66
|
+
const normalized = value?.trim();
|
|
67
|
+
if (!normalized)
|
|
68
|
+
throw new DinoxError(`${label} is required`, { code: 'USAGE_ERROR' });
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
export function assertExclusive(conflicts) {
|
|
72
|
+
const conflict = conflicts.find(([active]) => active);
|
|
73
|
+
if (conflict)
|
|
74
|
+
throw new DinoxError(conflict[1], { code: 'USAGE_ERROR' });
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type z } from 'zod';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
export type DomainCommandResult<T> = {
|
|
5
|
+
result: T;
|
|
6
|
+
affectedTables: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Converts a desktop domain failure into a DinoxError; DinoxErrors pass through unchanged. */
|
|
9
|
+
export declare function toDinoxError(error: unknown, label?: string): DinoxError;
|
|
10
|
+
export declare function parseDomainInput<T>(definition: {
|
|
11
|
+
inputSchema: z.ZodType<T>;
|
|
12
|
+
}, input: unknown, label: string): T;
|
|
13
|
+
export declare function runDomainCommand<T>(db: AbstractPowerSyncDatabase, name: string, input: unknown): Promise<DomainCommandResult<T>>;
|
|
14
|
+
export declare function fetchDomainReadModel<T>(db: AbstractPowerSyncDatabase, name: string, input: unknown): Promise<T[]>;
|
|
15
|
+
/** Runs `run` in one read transaction (a consistent snapshot) when the database offers it. */
|
|
16
|
+
export declare function withReadTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
17
|
+
/** Runs `run` in one write transaction; vendored commands called inside reuse it. */
|
|
18
|
+
export declare function withNestedWriteTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Executes `run` and then `inspect` inside one write transaction that is always rolled
|
|
21
|
+
* back, so a preview reflects exactly what the real write would do (including desktop
|
|
22
|
+
* validation and derived values) without persisting or queueing an upload.
|
|
23
|
+
*/
|
|
24
|
+
export declare function previewInRolledBackTransaction<TResult, TPreview>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<TResult>, inspect: (tx: AbstractPowerSyncDatabase, result: TResult) => Promise<TPreview>): Promise<TPreview>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Runs the vendored desktop domain (src/vendor/electron) against the CLI's PowerSync
|
|
2
|
+
// database: validates inputs with the desktop zod schemas, maps desktop error strings onto
|
|
3
|
+
// registered DinoxError codes, and previews writes by executing them inside a transaction
|
|
4
|
+
// that is always rolled back.
|
|
5
|
+
import { ZodError } from 'zod';
|
|
6
|
+
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
7
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
8
|
+
import { isRegisteredErrorCode } from '../../utils/errorCodes.js';
|
|
9
|
+
import { createNoteTemplateCommands, createNoteTemplateReadModels } from '../../vendor/electron/domain/note-templates.js';
|
|
10
|
+
import { createPropertyCommands, createPropertyReadModels } from '../../vendor/electron/domain/properties.js';
|
|
11
|
+
import { createSavedViewCommands, createSavedViewReadModels } from '../../vendor/electron/domain/saved-views.js';
|
|
12
|
+
const COMMANDS = new Map([...createPropertyCommands(), ...createSavedViewCommands(), ...createNoteTemplateCommands()]
|
|
13
|
+
.map((definition) => [definition.name, definition]));
|
|
14
|
+
const READ_MODELS = new Map([...createPropertyReadModels(), ...createSavedViewReadModels(), ...createNoteTemplateReadModels()]
|
|
15
|
+
.map((definition) => [definition.name, definition]));
|
|
16
|
+
const DOMAIN_CODE_PATTERN = /^[A-Z][A-Z0-9_]+$/;
|
|
17
|
+
function zodIssues(error) {
|
|
18
|
+
return error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message }));
|
|
19
|
+
}
|
|
20
|
+
/** Converts a desktop domain failure into a DinoxError; DinoxErrors pass through unchanged. */
|
|
21
|
+
export function toDinoxError(error, label = 'Dinox domain operation') {
|
|
22
|
+
if (error instanceof DinoxError)
|
|
23
|
+
return error;
|
|
24
|
+
if (error instanceof ZodError) {
|
|
25
|
+
return new DinoxError(`${label} input is invalid`, {
|
|
26
|
+
code: 'INVALID_ARGUMENT',
|
|
27
|
+
details: { issues: zodIssues(error) },
|
|
28
|
+
recoverable: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
if (DOMAIN_CODE_PATTERN.test(message) && isRegisteredErrorCode(message)) {
|
|
33
|
+
return new DinoxError(message, { code: message, cause: error, recoverable: false });
|
|
34
|
+
}
|
|
35
|
+
if (/^Note not found: /.test(message)) {
|
|
36
|
+
return new DinoxError(message, { code: 'NOTE_NOT_FOUND', cause: error });
|
|
37
|
+
}
|
|
38
|
+
if (/^Invalid note template .* input$/.test(message)) {
|
|
39
|
+
return new DinoxError(message, { code: 'INVALID_ARGUMENT', cause: error, recoverable: false });
|
|
40
|
+
}
|
|
41
|
+
return new DinoxError(message || `${label} failed`, { code: 'COMMAND_FAILED', cause: error });
|
|
42
|
+
}
|
|
43
|
+
function requireCommand(name) {
|
|
44
|
+
const definition = COMMANDS.get(name);
|
|
45
|
+
if (!definition)
|
|
46
|
+
throw new DinoxError(`Unknown domain command: ${name}`, { code: 'INTERNAL_ERROR' });
|
|
47
|
+
return definition;
|
|
48
|
+
}
|
|
49
|
+
function requireReadModel(name) {
|
|
50
|
+
const definition = READ_MODELS.get(name);
|
|
51
|
+
if (!definition)
|
|
52
|
+
throw new DinoxError(`Unknown domain read model: ${name}`, { code: 'INTERNAL_ERROR' });
|
|
53
|
+
return definition;
|
|
54
|
+
}
|
|
55
|
+
export function parseDomainInput(definition, input, label) {
|
|
56
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
57
|
+
if (!parsed.success)
|
|
58
|
+
throw toDinoxError(parsed.error, label);
|
|
59
|
+
return parsed.data;
|
|
60
|
+
}
|
|
61
|
+
export async function runDomainCommand(db, name, input) {
|
|
62
|
+
const definition = requireCommand(name);
|
|
63
|
+
const parsed = parseDomainInput(definition, input, name);
|
|
64
|
+
try {
|
|
65
|
+
return await definition.run(db, parsed);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw toDinoxError(error, name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function fetchDomainReadModel(db, name, input) {
|
|
72
|
+
const definition = requireReadModel(name);
|
|
73
|
+
const parsed = parseDomainInput(definition, input, name);
|
|
74
|
+
try {
|
|
75
|
+
return await definition.fetch(db, parsed);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw toDinoxError(error, name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Exposes an open transaction as a database whose nested write/read transactions reuse it,
|
|
83
|
+
* so vendored commands that open their own transaction run inside the caller's.
|
|
84
|
+
*/
|
|
85
|
+
function nestedDatabase(tx) {
|
|
86
|
+
const proxy = new Proxy(tx, {
|
|
87
|
+
get(target, property, receiver) {
|
|
88
|
+
if (property === 'writeTransaction' || property === 'readTransaction') {
|
|
89
|
+
return async (run) => run(proxy);
|
|
90
|
+
}
|
|
91
|
+
const value = Reflect.get(target, property, receiver);
|
|
92
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return proxy;
|
|
96
|
+
}
|
|
97
|
+
/** Runs `run` in one read transaction (a consistent snapshot) when the database offers it. */
|
|
98
|
+
export async function withReadTransaction(db, run) {
|
|
99
|
+
const candidate = db;
|
|
100
|
+
if (typeof candidate.readTransaction !== 'function')
|
|
101
|
+
return run(db);
|
|
102
|
+
return candidate.readTransaction.call(db, (tx) => run(nestedDatabase(tx)));
|
|
103
|
+
}
|
|
104
|
+
/** Runs `run` in one write transaction; vendored commands called inside reuse it. */
|
|
105
|
+
export async function withNestedWriteTransaction(db, run) {
|
|
106
|
+
try {
|
|
107
|
+
return await withPowerSyncWriteTransaction(db, (tx) => run(nestedDatabase(tx)));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw toDinoxError(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class DryRunRollback extends Error {
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Executes `run` and then `inspect` inside one write transaction that is always rolled
|
|
117
|
+
* back, so a preview reflects exactly what the real write would do (including desktop
|
|
118
|
+
* validation and derived values) without persisting or queueing an upload.
|
|
119
|
+
*/
|
|
120
|
+
export async function previewInRolledBackTransaction(db, run, inspect) {
|
|
121
|
+
let preview = null;
|
|
122
|
+
try {
|
|
123
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
124
|
+
const nested = nestedDatabase(tx);
|
|
125
|
+
const result = await run(nested);
|
|
126
|
+
preview = { value: await inspect(nested, result) };
|
|
127
|
+
throw new DryRunRollback('dry-run rollback');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!(error instanceof DryRunRollback))
|
|
132
|
+
throw toDinoxError(error);
|
|
133
|
+
}
|
|
134
|
+
if (!preview)
|
|
135
|
+
throw new DinoxError('Dry-run preview did not complete', { code: 'INTERNAL_ERROR' });
|
|
136
|
+
return preview.value;
|
|
137
|
+
}
|
|
@@ -15,6 +15,8 @@ import { parseNoteSearchFields, projectNoteSearchFields } from './searchOutput.j
|
|
|
15
15
|
import { parseCreatedAtRange } from './searchTime.js';
|
|
16
16
|
import { buildNoteSearchResponse, normalizeDaysForMeta } from './searchResponse.js';
|
|
17
17
|
import { parseMutationListInput, parseNoteType, parsePreviewLineCount, parseSearchLimit, parseSearchListInput, parseSearchOffset, parseStarredOption, parseStringInput, parseStringListInput, parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
18
|
+
import { readNoteProperties } from '../properties/repo.js';
|
|
19
|
+
import { createNoteWithSeed, previewNoteCreateProperties, resolveNoteCreateSeed } from './createFromTemplate.js';
|
|
18
20
|
function toPublicNoteContext(note) {
|
|
19
21
|
if (!note) {
|
|
20
22
|
return null;
|
|
@@ -368,14 +370,31 @@ export async function runNoteDetail({ command, options, args }) {
|
|
|
368
370
|
export async function runNoteCreate({ command, options }) {
|
|
369
371
|
const createOptions = options;
|
|
370
372
|
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
373
|
+
if (createOptions.content === undefined && !createOptions.template) {
|
|
374
|
+
throw new DinoxError('--content is required unless --template is given', { code: 'USAGE_ERROR' });
|
|
375
|
+
}
|
|
371
376
|
await withNoteDatabase(command, async (runtime) => {
|
|
372
377
|
const { db, config, offline } = runtime;
|
|
373
378
|
await syncBeforeNoteCommand(runtime);
|
|
374
379
|
const durability = resolveWriteDurabilityForRuntime(runtime, createOptions.durability);
|
|
375
380
|
const identity = await resolveAuthIdentity(config);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
const seed = await resolveNoteCreateSeed(db, {
|
|
382
|
+
userId: identity.userId,
|
|
383
|
+
templateId: createOptions.template,
|
|
384
|
+
properties: createOptions.properties,
|
|
385
|
+
});
|
|
386
|
+
// Template content is used as the hydrated Tiptap document itself (as desktop does), so
|
|
387
|
+
// only explicit --content goes through Markdown conversion.
|
|
388
|
+
const useTemplateContent = createOptions.content === undefined && seed.templateContent !== null;
|
|
389
|
+
const contentMd = useTemplateContent
|
|
390
|
+
? seed.templateContent.contentMd
|
|
391
|
+
: createOptions.content !== undefined ? await parseStringInput(String(createOptions.content)) : '';
|
|
392
|
+
const canonicalContentMd = useTemplateContent
|
|
393
|
+
? contentMd
|
|
394
|
+
: await canonicalizeBidlinksInMarkdown(db, contentMd, identity.userId);
|
|
395
|
+
const { contentJson, contentText } = useTemplateContent
|
|
396
|
+
? { contentJson: seed.templateContent.contentJson, contentText: seed.templateContent.contentText }
|
|
397
|
+
: convertMarkdownToNoteContent(canonicalContentMd);
|
|
379
398
|
const tagsInput = typeof createOptions.tags === 'string'
|
|
380
399
|
? await parseStringListInput(createOptions.tags, '--tags')
|
|
381
400
|
: [];
|
|
@@ -383,8 +402,21 @@ export async function runNoteCreate({ command, options }) {
|
|
|
383
402
|
? await parseStringListInput(createOptions.boxes, '--boxes')
|
|
384
403
|
: [];
|
|
385
404
|
const noteType = parseNoteType(createOptions.type);
|
|
405
|
+
const id = generateId();
|
|
406
|
+
const insert = async (tx) => createNote(tx, {
|
|
407
|
+
id,
|
|
408
|
+
title: String(createOptions.title),
|
|
409
|
+
noteType,
|
|
410
|
+
contentMd: canonicalContentMd,
|
|
411
|
+
contentJson,
|
|
412
|
+
contentText,
|
|
413
|
+
tags: await validateTagsForCreate(tx, tagsInput, identity.userId),
|
|
414
|
+
zettelBoxIds: await resolveZettelBoxIdsForCreate(tx, boxNames),
|
|
415
|
+
userId: identity.userId,
|
|
416
|
+
properties: seed.propertiesJson,
|
|
417
|
+
});
|
|
386
418
|
if (Boolean(createOptions.dryRun)) {
|
|
387
|
-
|
|
419
|
+
const preview = await buildNoteCreateDryRunPreview({
|
|
388
420
|
db,
|
|
389
421
|
config,
|
|
390
422
|
offline,
|
|
@@ -394,27 +426,24 @@ export async function runNoteCreate({ command, options }) {
|
|
|
394
426
|
contentMd,
|
|
395
427
|
tagsInput,
|
|
396
428
|
zettelBoxNames: boxNames,
|
|
397
|
-
})
|
|
429
|
+
});
|
|
430
|
+
if (seed.template || seed.values) {
|
|
431
|
+
const properties = await previewNoteCreateProperties(db, { noteId: id, userId: identity.userId, seed, insert });
|
|
432
|
+
preview.inputs = { ...preview.inputs, ...(seed.template ? { template: { id: seed.template.id, name: seed.template.name } } : {}) };
|
|
433
|
+
preview.changes = { ...preview.changes, properties: properties.properties };
|
|
434
|
+
}
|
|
435
|
+
printDryRunPreview(preview, runtime);
|
|
398
436
|
return;
|
|
399
437
|
}
|
|
400
|
-
|
|
401
|
-
await createNote(db, {
|
|
402
|
-
id,
|
|
403
|
-
title: String(createOptions.title),
|
|
404
|
-
noteType,
|
|
405
|
-
contentMd: canonicalContentMd,
|
|
406
|
-
contentJson,
|
|
407
|
-
contentText,
|
|
408
|
-
tags: await validateTagsForCreate(db, tagsInput, identity.userId),
|
|
409
|
-
zettelBoxIds: await resolveZettelBoxIdsForCreate(db, boxNames),
|
|
410
|
-
userId: identity.userId,
|
|
411
|
-
});
|
|
438
|
+
await createNoteWithSeed(db, { noteId: id, userId: identity.userId, seed, insert });
|
|
412
439
|
const upload = await waitForMutationUpload(runtime, { durability });
|
|
413
440
|
const payload = {
|
|
414
441
|
ok: true,
|
|
415
442
|
id,
|
|
443
|
+
...(seed.template ? { template: { id: seed.template.id, name: seed.template.name } } : {}),
|
|
444
|
+
...(seed.template || seed.values ? { properties: (await readNoteProperties(db, { userId: identity.userId, noteId: id })).properties } : {}),
|
|
416
445
|
...(await readNoteWriteReceipt(db, id, {
|
|
417
|
-
changed: ['title', 'type', 'content_md', 'content_json', 'content_text', 'tags', 'boxes'],
|
|
446
|
+
changed: ['title', 'type', 'content_md', 'content_json', 'content_text', 'tags', 'boxes', ...(seed.template || seed.values ? ['properties'] : [])],
|
|
418
447
|
upload,
|
|
419
448
|
stale: commandStale(runtime),
|
|
420
449
|
})),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type NotePropertiesPatch } from '../../vendor/electron/shared/index.js';
|
|
3
|
+
import { type NotePropertiesView } from '../properties/repo.js';
|
|
4
|
+
import { type NoteTemplateDetail } from '../templates/model.js';
|
|
5
|
+
export type NoteCreateSeed = {
|
|
6
|
+
template: NoteTemplateDetail | null;
|
|
7
|
+
/** Hydrated template content projected like stored note content; null without content. */
|
|
8
|
+
templateContent: {
|
|
9
|
+
contentJson: unknown;
|
|
10
|
+
contentMd: string;
|
|
11
|
+
contentText: string;
|
|
12
|
+
} | null;
|
|
13
|
+
/** Serialized `c_note.properties` holding the template's declared empty slots. */
|
|
14
|
+
propertiesJson: string | null;
|
|
15
|
+
values: NotePropertiesPatch | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function resolveNoteCreateSeed(db: AbstractPowerSyncDatabase, input: {
|
|
18
|
+
userId: string;
|
|
19
|
+
templateId?: string;
|
|
20
|
+
properties?: unknown;
|
|
21
|
+
}): Promise<NoteCreateSeed>;
|
|
22
|
+
/** Inserts the note and its initial property values atomically. */
|
|
23
|
+
export declare function createNoteWithSeed(db: AbstractPowerSyncDatabase, input: {
|
|
24
|
+
noteId: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
seed: NoteCreateSeed;
|
|
27
|
+
insert: (tx: AbstractPowerSyncDatabase) => Promise<unknown>;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
/** Dry-run: performs the insert and property writes, reads the resulting properties, rolls back. */
|
|
30
|
+
export declare function previewNoteCreateProperties(db: AbstractPowerSyncDatabase, input: {
|
|
31
|
+
noteId: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
seed: NoteCreateSeed;
|
|
34
|
+
insert: (tx: AbstractPowerSyncDatabase) => Promise<unknown>;
|
|
35
|
+
}): Promise<NotePropertiesView>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// `note create --template/--properties`: seeds a new note the way desktop's "create from
|
|
2
|
+
// template" does — hydrated template content plus the template's property keys declared as
|
|
3
|
+
// empty slots — and optionally writes initial property values in the same transaction.
|
|
4
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
5
|
+
import { applyNotePropertySlotOperation, serializeRawNoteProperties, } from '../../vendor/electron/shared/index.js';
|
|
6
|
+
import { previewInRolledBackTransaction, runDomainCommand, withNestedWriteTransaction } from '../domain/run.js';
|
|
7
|
+
import { readJsonOption } from '../domain/options.js';
|
|
8
|
+
import { readNoteProperties } from '../properties/repo.js';
|
|
9
|
+
import { hydrateNoteTemplateDocument } from '../templates/model.js';
|
|
10
|
+
import { getNoteTemplate } from '../templates/repo.js';
|
|
11
|
+
import { projectNoteContent } from './contentEngine.js';
|
|
12
|
+
export async function resolveNoteCreateSeed(db, input) {
|
|
13
|
+
const template = input.templateId ? await getNoteTemplate(db, { userId: input.userId, id: input.templateId }) : null;
|
|
14
|
+
const projection = template?.document ? projectNoteContent(hydrateNoteTemplateDocument(template.document)) : null;
|
|
15
|
+
const templateContent = projection
|
|
16
|
+
? { contentJson: projection.document, contentMd: projection.contentMd, contentText: projection.contentText }
|
|
17
|
+
: null;
|
|
18
|
+
const declared = applyNotePropertySlotOperation({}, 'declare', template?.propertyKeys ?? []).next;
|
|
19
|
+
let values = null;
|
|
20
|
+
if (input.properties !== undefined) {
|
|
21
|
+
const raw = await readJsonOption(input.properties, '--properties', 'note.create');
|
|
22
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
23
|
+
throw new DinoxError('--properties must be a JSON object of property key to value', { code: 'USAGE_ERROR' });
|
|
24
|
+
}
|
|
25
|
+
values = raw;
|
|
26
|
+
if (Object.keys(values).length === 0)
|
|
27
|
+
values = null;
|
|
28
|
+
}
|
|
29
|
+
return { template, templateContent, propertiesJson: serializeRawNoteProperties(declared), values };
|
|
30
|
+
}
|
|
31
|
+
async function applyValues(tx, noteId, userId, seed) {
|
|
32
|
+
if (!seed.values)
|
|
33
|
+
return;
|
|
34
|
+
await runDomainCommand(tx, 'notes.updateProperties', {
|
|
35
|
+
noteId,
|
|
36
|
+
userId,
|
|
37
|
+
patch: seed.values,
|
|
38
|
+
updatedAt: new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Inserts the note and its initial property values atomically. */
|
|
42
|
+
export async function createNoteWithSeed(db, input) {
|
|
43
|
+
if (!input.seed.values) {
|
|
44
|
+
await input.insert(db);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await withNestedWriteTransaction(db, async (tx) => {
|
|
48
|
+
await input.insert(tx);
|
|
49
|
+
await applyValues(tx, input.noteId, input.userId, input.seed);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Dry-run: performs the insert and property writes, reads the resulting properties, rolls back. */
|
|
53
|
+
export async function previewNoteCreateProperties(db, input) {
|
|
54
|
+
return previewInRolledBackTransaction(db, async (tx) => {
|
|
55
|
+
await input.insert(tx);
|
|
56
|
+
await applyValues(tx, input.noteId, input.userId, input.seed);
|
|
57
|
+
}, (tx) => readNoteProperties(tx, { userId: input.userId, noteId: input.noteId }));
|
|
58
|
+
}
|
|
@@ -176,10 +176,12 @@ export const noteCommandGroup = {
|
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
name: 'create',
|
|
179
|
-
description: 'Create a new note from markdown content',
|
|
179
|
+
description: 'Create a new note from markdown content or a note template',
|
|
180
180
|
options: [
|
|
181
181
|
{ flags: '--title <string>', description: 'Note title', required: true, type: 'string' },
|
|
182
|
-
{ flags: '--content <string|@file>', description: 'Markdown content
|
|
182
|
+
{ flags: '--content <string|@file>', description: 'Markdown content (required unless --template is given; overrides template content)', type: 'string' },
|
|
183
|
+
{ flags: '--template <id>', description: 'Note template id: hydrates its content ({{date}}, {{time}}, ...) and declares its property keys as empty slots', type: 'string' },
|
|
184
|
+
{ flags: '--properties <json|@file>', description: 'Initial property values as a JSON object; validated against property definitions', type: 'string' },
|
|
183
185
|
{ flags: '--type <note|crawl>', description: 'Note type: note or crawl', type: 'string', defaultValue: 'crawl', enum: ['note', 'crawl'] },
|
|
184
186
|
{ flags: '--tags <string|@file>', description: 'Tag list (JSON array or comma/newline-separated)', type: 'string' },
|
|
185
187
|
{ flags: '--boxes <string|@file>', description: 'Box paths or unique names (JSON array or comma/newline-separated)', type: 'string' },
|
|
@@ -194,9 +196,12 @@ export const noteCommandGroup = {
|
|
|
194
196
|
output: {
|
|
195
197
|
type: 'structured',
|
|
196
198
|
summary: 'Creation acknowledgement with write durability, queue state, and note revision receipt',
|
|
197
|
-
fields: ['id', ...NOTE_WRITE_RECEIPT_FIELDS],
|
|
199
|
+
fields: ['id', 'template', 'properties', ...NOTE_WRITE_RECEIPT_FIELDS],
|
|
198
200
|
},
|
|
199
|
-
examples: [
|
|
201
|
+
examples: [
|
|
202
|
+
'dino note create --title "Weekly note" --content @/tmp/note.md --type note --durability uploaded --format json',
|
|
203
|
+
'dino note create --title "Dune" --template <templateId> --properties \'{"rating":5}\' --type note --format json --dry-run',
|
|
204
|
+
],
|
|
200
205
|
},
|
|
201
206
|
action: runNoteCreate,
|
|
202
207
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Note reads: single-note lookup, context, preview, detail, and export rows.
|
|
2
|
-
import { DinoxError } from '../../../utils/errors.js';
|
|
3
2
|
import { loadZettelBoxNameMap, normalizeStoredFlag, normalizeStoredStarred, parseStoredTags, parseZettelBoxIds, resolveSummary, } from './shared.js';
|
|
3
|
+
import { parseRawNoteProperties } from '../../../vendor/electron/shared/index.js';
|
|
4
4
|
export async function getNote(db, id) {
|
|
5
5
|
return db.getOptional(`
|
|
6
6
|
SELECT
|
|
@@ -158,29 +158,10 @@ function parseStoredJsonValue(raw) {
|
|
|
158
158
|
return raw;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
// Same forgiving read as desktop: malformed storage reads as {} instead of failing the
|
|
162
|
+
// whole note read; `dino prop show` reports it via `valid: false`.
|
|
161
163
|
function parseStoredNoteProperties(raw) {
|
|
162
|
-
|
|
163
|
-
if (!value) {
|
|
164
|
-
return {};
|
|
165
|
-
}
|
|
166
|
-
let parsed;
|
|
167
|
-
try {
|
|
168
|
-
parsed = JSON.parse(value);
|
|
169
|
-
}
|
|
170
|
-
catch (cause) {
|
|
171
|
-
throw new DinoxError('c_note.properties must be valid JSON', {
|
|
172
|
-
code: 'NOTE_INVALID_PROPERTIES',
|
|
173
|
-
details: { cause: cause instanceof Error ? cause.message : String(cause) },
|
|
174
|
-
recoverable: false,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
178
|
-
throw new DinoxError('c_note.properties must be a JSON object', {
|
|
179
|
-
code: 'NOTE_INVALID_PROPERTIES',
|
|
180
|
-
recoverable: false,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return Object.fromEntries(Object.entries(parsed));
|
|
164
|
+
return parseRawNoteProperties(raw);
|
|
184
165
|
}
|
|
185
166
|
function mapNoteExportRow(note, boxNameMap) {
|
|
186
167
|
const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
|
|
@@ -11,6 +11,8 @@ export declare function createNote(db: AbstractPowerSyncDatabase, input: {
|
|
|
11
11
|
zettelBoxIds: string[];
|
|
12
12
|
userId: string;
|
|
13
13
|
imageDetail?: unknown[];
|
|
14
|
+
/** Serialized `c_note.properties` (e.g. template-declared empty slots); defaults to NULL. */
|
|
15
|
+
properties?: string | null;
|
|
14
16
|
}): Promise<{
|
|
15
17
|
id: string;
|
|
16
18
|
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { PROPERTY_OPTION_COLORS, STATUS_PROPERTY_GROUPS } from '../../vendor/electron/shared/index.js';
|
|
3
|
+
import type { CommandGroupDescriptor } from '../descriptor/types.js';
|
|
4
|
+
type Color = (typeof PROPERTY_OPTION_COLORS)[number];
|
|
5
|
+
type Group = (typeof STATUS_PROPERTY_GROUPS)[number];
|
|
6
|
+
/** Parses `label[:color[:group]]`, peeling recognised color/group suffixes off the right. */
|
|
7
|
+
export declare function parseOptionSpec(spec: string): {
|
|
8
|
+
label: string;
|
|
9
|
+
color?: Color;
|
|
10
|
+
group?: Group;
|
|
11
|
+
};
|
|
12
|
+
/** Parses `key=value`; the value is JSON when it parses as JSON, otherwise a plain string. */
|
|
13
|
+
export declare function parseAssignment(spec: string): [string, unknown];
|
|
14
|
+
export declare const propertyCommandGroup: CommandGroupDescriptor;
|
|
15
|
+
export declare function registerPropertyCommands(program: Command): void;
|
|
16
|
+
export {};
|