@dinoxx/dinox-cli 2.1.1 → 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 +72 -20
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +12 -5
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +8 -2
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/shared.d.ts +2 -0
- package/dist/commands/notes/repo/write.d.ts +3 -0
- package/dist/commands/notes/repo/write.js +16 -5
- 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;
|
|
@@ -39,11 +41,17 @@ async function executeNoteUpdateOperation(args) {
|
|
|
39
41
|
await syncBeforeNoteCommand(runtime);
|
|
40
42
|
const durability = resolveWriteDurabilityForRuntime(runtime, args.durability);
|
|
41
43
|
const ids = await parseTargetNoteIds(args.id, args.idsOption, 'update');
|
|
44
|
+
const hasTitle = typeof args.title === 'string';
|
|
42
45
|
const hasTags = typeof args.tagsOption === 'string';
|
|
43
46
|
const hasBoxes = typeof args.boxOption === 'string';
|
|
44
47
|
const hasStarred = typeof args.starred === 'boolean';
|
|
45
|
-
if (!hasTags && !hasBoxes && !hasStarred) {
|
|
46
|
-
throw new DinoxError('At least one option is required: --tags, --boxes, or --starred');
|
|
48
|
+
if (!hasTitle && !hasTags && !hasBoxes && !hasStarred) {
|
|
49
|
+
throw new DinoxError('At least one option is required: --title, --tags, --boxes, or --starred');
|
|
50
|
+
}
|
|
51
|
+
if (hasTitle && ids.length !== 1) {
|
|
52
|
+
throw new DinoxError('--title can only be used with a single note id; run one `dino note update <id> --title` per note', {
|
|
53
|
+
code: 'USAGE_ERROR',
|
|
54
|
+
});
|
|
47
55
|
}
|
|
48
56
|
const requestedTags = hasTags
|
|
49
57
|
? await parseMutationListInput(args.tagsOption, '--tags')
|
|
@@ -68,6 +76,7 @@ async function executeNoteUpdateOperation(args) {
|
|
|
68
76
|
offline,
|
|
69
77
|
userId: tagUserId,
|
|
70
78
|
ids,
|
|
79
|
+
title: args.title,
|
|
71
80
|
tagsInput: tags,
|
|
72
81
|
zettelBoxNames: requestedBoxNames,
|
|
73
82
|
starred: hasStarred ? args.starred : undefined,
|
|
@@ -81,6 +90,7 @@ async function executeNoteUpdateOperation(args) {
|
|
|
81
90
|
for (const targetId of ids) {
|
|
82
91
|
results.push(await updateNote(db, {
|
|
83
92
|
id: targetId,
|
|
93
|
+
title: args.title,
|
|
84
94
|
tags,
|
|
85
95
|
zettelBoxIds,
|
|
86
96
|
starred: hasStarred ? args.starred : undefined,
|
|
@@ -98,6 +108,7 @@ async function executeNoteUpdateOperation(args) {
|
|
|
98
108
|
results: results.map((item) => ({
|
|
99
109
|
id: item.id,
|
|
100
110
|
updated: item.updated,
|
|
111
|
+
title: item.title,
|
|
101
112
|
tags: item.tags,
|
|
102
113
|
boxes: item.zettelBoxIds,
|
|
103
114
|
is_starred: item.is_starred,
|
|
@@ -359,14 +370,31 @@ export async function runNoteDetail({ command, options, args }) {
|
|
|
359
370
|
export async function runNoteCreate({ command, options }) {
|
|
360
371
|
const createOptions = options;
|
|
361
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
|
+
}
|
|
362
376
|
await withNoteDatabase(command, async (runtime) => {
|
|
363
377
|
const { db, config, offline } = runtime;
|
|
364
378
|
await syncBeforeNoteCommand(runtime);
|
|
365
379
|
const durability = resolveWriteDurabilityForRuntime(runtime, createOptions.durability);
|
|
366
380
|
const identity = await resolveAuthIdentity(config);
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
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);
|
|
370
398
|
const tagsInput = typeof createOptions.tags === 'string'
|
|
371
399
|
? await parseStringListInput(createOptions.tags, '--tags')
|
|
372
400
|
: [];
|
|
@@ -374,8 +402,21 @@ export async function runNoteCreate({ command, options }) {
|
|
|
374
402
|
? await parseStringListInput(createOptions.boxes, '--boxes')
|
|
375
403
|
: [];
|
|
376
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
|
+
});
|
|
377
418
|
if (Boolean(createOptions.dryRun)) {
|
|
378
|
-
|
|
419
|
+
const preview = await buildNoteCreateDryRunPreview({
|
|
379
420
|
db,
|
|
380
421
|
config,
|
|
381
422
|
offline,
|
|
@@ -385,27 +426,24 @@ export async function runNoteCreate({ command, options }) {
|
|
|
385
426
|
contentMd,
|
|
386
427
|
tagsInput,
|
|
387
428
|
zettelBoxNames: boxNames,
|
|
388
|
-
})
|
|
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);
|
|
389
436
|
return;
|
|
390
437
|
}
|
|
391
|
-
|
|
392
|
-
await createNote(db, {
|
|
393
|
-
id,
|
|
394
|
-
title: String(createOptions.title),
|
|
395
|
-
noteType,
|
|
396
|
-
contentMd: canonicalContentMd,
|
|
397
|
-
contentJson,
|
|
398
|
-
contentText,
|
|
399
|
-
tags: await validateTagsForCreate(db, tagsInput, identity.userId),
|
|
400
|
-
zettelBoxIds: await resolveZettelBoxIdsForCreate(db, boxNames),
|
|
401
|
-
userId: identity.userId,
|
|
402
|
-
});
|
|
438
|
+
await createNoteWithSeed(db, { noteId: id, userId: identity.userId, seed, insert });
|
|
403
439
|
const upload = await waitForMutationUpload(runtime, { durability });
|
|
404
440
|
const payload = {
|
|
405
441
|
ok: true,
|
|
406
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 } : {}),
|
|
407
445
|
...(await readNoteWriteReceipt(db, id, {
|
|
408
|
-
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'] : [])],
|
|
409
447
|
upload,
|
|
410
448
|
stale: commandStale(runtime),
|
|
411
449
|
})),
|
|
@@ -452,6 +490,17 @@ export async function runNoteDelete({ command, options, args }) {
|
|
|
452
490
|
printStaleCacheWarning(runtime);
|
|
453
491
|
});
|
|
454
492
|
}
|
|
493
|
+
// Titles are single-line metadata; reject input that would render as a multi-line title.
|
|
494
|
+
function parseNoteTitleOption(raw) {
|
|
495
|
+
const title = raw.trim();
|
|
496
|
+
if (!title) {
|
|
497
|
+
throw new DinoxError('--title must not be empty', { code: 'USAGE_ERROR' });
|
|
498
|
+
}
|
|
499
|
+
if (/[\u0000-\u001f\u007f]/.test(title)) {
|
|
500
|
+
throw new DinoxError('--title must be a single line without control characters', { code: 'USAGE_ERROR' });
|
|
501
|
+
}
|
|
502
|
+
return title;
|
|
503
|
+
}
|
|
455
504
|
export async function runNoteUpdate({ command, options, args }) {
|
|
456
505
|
const updateOptions = options;
|
|
457
506
|
await executeNoteUpdateOperation({
|
|
@@ -463,6 +512,9 @@ export async function runNoteUpdate({ command, options, args }) {
|
|
|
463
512
|
starred: typeof updateOptions.starred === 'string'
|
|
464
513
|
? parseStarredOption(updateOptions.starred, '--starred')
|
|
465
514
|
: undefined,
|
|
515
|
+
title: typeof updateOptions.title === 'string'
|
|
516
|
+
? parseNoteTitleOption(updateOptions.title)
|
|
517
|
+
: undefined,
|
|
466
518
|
tagsOption: updateOptions.tags,
|
|
467
519
|
commandName: 'dino note update',
|
|
468
520
|
actionName: 'update_note',
|
|
@@ -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,18 +196,22 @@ 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
|
},
|
|
203
208
|
{
|
|
204
209
|
name: 'update',
|
|
205
|
-
description: 'Full-replace note metadata for explicit note ids',
|
|
210
|
+
description: 'Full-replace note metadata (title, tags, boxes, starred) for explicit note ids',
|
|
206
211
|
arguments: [{ syntax: '[id]', description: 'Optional note id for single-note full replacement' }],
|
|
207
212
|
options: [
|
|
208
213
|
{ flags: '--ids <string|@file>', description: 'Batch note ids (JSON array or comma/newline-separated)', type: 'string' },
|
|
214
|
+
{ flags: '--title <title>', description: 'Replace the note title (single note id only; content is unchanged)', type: 'string' },
|
|
209
215
|
{ flags: '--tags <string|@file>', description: 'Replace the entire tag list; use [] to clear all tags', type: 'string' },
|
|
210
216
|
{ flags: '--boxes <string|@file>', description: 'Replace the entire box list; use [] to clear all boxes', type: 'string' },
|
|
211
217
|
{ flags: '--starred <true|false>', description: 'Replace the starred state', type: 'string', enum: ['true', 'false'] },
|
|
@@ -224,6 +230,7 @@ export const noteCommandGroup = {
|
|
|
224
230
|
},
|
|
225
231
|
examples: [
|
|
226
232
|
'dino note update <id> --tags "work,ai" --format json --dry-run',
|
|
233
|
+
'dino note update <id> --title "New title" --format json --dry-run',
|
|
227
234
|
'dino note update --ids @/tmp/note-ids.txt --boxes "Inbox" --starred true --format json',
|
|
228
235
|
],
|
|
229
236
|
},
|
|
@@ -67,9 +67,14 @@ export async function buildNoteUpdateDryRunPreview(args) {
|
|
|
67
67
|
const nextBoxes = Array.isArray(args.zettelBoxNames) ? normalizeDistinctStrings(args.zettelBoxNames) : note.zettel_boxes;
|
|
68
68
|
const tagsTo = nextTags ?? currentTags;
|
|
69
69
|
const starredTo = typeof args.starred === 'boolean' ? args.starred : note.is_starred;
|
|
70
|
+
const titleFrom = note.title ?? '';
|
|
71
|
+
const titleTo = typeof args.title === 'string' ? args.title : titleFrom;
|
|
70
72
|
return {
|
|
71
73
|
id: note.id,
|
|
72
|
-
title:
|
|
74
|
+
title: titleFrom,
|
|
75
|
+
...(typeof args.title === 'string'
|
|
76
|
+
? { title_from: titleFrom, title_to: titleTo, title_changed: titleFrom !== titleTo }
|
|
77
|
+
: {}),
|
|
73
78
|
tags_from: currentTags,
|
|
74
79
|
tags_to: tagsTo,
|
|
75
80
|
tags_changed: Array.isArray(args.tagsInput) ? !sameNormalizedSet(currentTags, tagsTo) : false,
|
|
@@ -81,7 +86,7 @@ export async function buildNoteUpdateDryRunPreview(args) {
|
|
|
81
86
|
starred_changed: typeof args.starred === 'boolean' ? note.is_starred !== starredTo : false,
|
|
82
87
|
};
|
|
83
88
|
});
|
|
84
|
-
const changedCount = changes.filter((item) => item.tags_changed || item.boxes_changed || item.starred_changed).length;
|
|
89
|
+
const changedCount = changes.filter((item) => item.title_changed === true || item.tags_changed || item.boxes_changed || item.starred_changed).length;
|
|
85
90
|
return {
|
|
86
91
|
dry_run: true,
|
|
87
92
|
command: args.commandName,
|
|
@@ -91,6 +96,7 @@ export async function buildNoteUpdateDryRunPreview(args) {
|
|
|
91
96
|
warnings: buildExecutionWarnings(args.config, args.offline),
|
|
92
97
|
inputs: {
|
|
93
98
|
ids: args.ids,
|
|
99
|
+
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
94
100
|
...(Array.isArray(args.tagsInput) ? { tags: nextTags } : {}),
|
|
95
101
|
...(Array.isArray(args.zettelBoxNames) ? { boxes: normalizeDistinctStrings(args.zettelBoxNames) } : {}),
|
|
96
102
|
...(typeof args.starred === 'boolean' ? { starred: args.starred } : {}),
|