@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.
Files changed (68) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/commands/domain/options.d.ts +10 -0
  3. package/dist/commands/domain/options.js +75 -0
  4. package/dist/commands/domain/run.d.ts +24 -0
  5. package/dist/commands/domain/run.js +137 -0
  6. package/dist/commands/notes/actions.js +72 -20
  7. package/dist/commands/notes/createFromTemplate.d.ts +35 -0
  8. package/dist/commands/notes/createFromTemplate.js +58 -0
  9. package/dist/commands/notes/descriptors.js +12 -5
  10. package/dist/commands/notes/dryRun.d.ts +1 -0
  11. package/dist/commands/notes/dryRun.js +8 -2
  12. package/dist/commands/notes/repo/read.js +4 -23
  13. package/dist/commands/notes/repo/shared.d.ts +2 -0
  14. package/dist/commands/notes/repo/write.d.ts +3 -0
  15. package/dist/commands/notes/repo/write.js +16 -5
  16. package/dist/commands/properties/index.d.ts +16 -0
  17. package/dist/commands/properties/index.js +640 -0
  18. package/dist/commands/properties/repo.d.ts +150 -0
  19. package/dist/commands/properties/repo.js +272 -0
  20. package/dist/commands/savedViews/fields.d.ts +28 -0
  21. package/dist/commands/savedViews/fields.js +87 -0
  22. package/dist/commands/savedViews/index.js +203 -164
  23. package/dist/commands/savedViews/model.d.ts +27 -69
  24. package/dist/commands/savedViews/model.js +63 -279
  25. package/dist/commands/savedViews/repo.d.ts +64 -62
  26. package/dist/commands/savedViews/repo.js +309 -428
  27. package/dist/commands/schema/renderSkills.js +29 -5
  28. package/dist/commands/schema/spec.js +4 -0
  29. package/dist/commands/templates/index.d.ts +4 -0
  30. package/dist/commands/templates/index.js +290 -0
  31. package/dist/commands/templates/model.d.ts +12 -34
  32. package/dist/commands/templates/model.js +39 -83
  33. package/dist/commands/templates/repo.d.ts +53 -0
  34. package/dist/commands/templates/repo.js +78 -0
  35. package/dist/skills/registry.js +7 -0
  36. package/dist/utils/errorCodes.d.ts +212 -0
  37. package/dist/utils/errorCodes.js +53 -0
  38. package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
  39. package/dist/vendor/electron/domain/note-templates.js +232 -0
  40. package/dist/vendor/electron/domain/properties.d.ts +10 -0
  41. package/dist/vendor/electron/domain/properties.js +1538 -0
  42. package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
  43. package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
  44. package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
  45. package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
  46. package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
  47. package/dist/vendor/electron/domain/saved-views.js +1117 -0
  48. package/dist/vendor/electron/rpc.d.ts +22 -0
  49. package/dist/vendor/electron/rpc.js +27 -0
  50. package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
  51. package/dist/vendor/electron/shared/ai-errors.js +107 -0
  52. package/dist/vendor/electron/shared/external-types.d.ts +2 -0
  53. package/dist/vendor/electron/shared/external-types.js +1 -0
  54. package/dist/vendor/electron/shared/index.d.ts +9 -0
  55. package/dist/vendor/electron/shared/index.js +7 -0
  56. package/dist/vendor/electron/shared/note-properties.d.ts +312 -0
  57. package/dist/vendor/electron/shared/note-properties.js +982 -0
  58. package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
  59. package/dist/vendor/electron/shared/note-templates.js +84 -0
  60. package/dist/vendor/electron/shared/powersync-read-models.d.ts +1710 -0
  61. package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
  62. package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
  63. package/dist/vendor/electron/shared/saved-views.js +981 -0
  64. package/dist/vendor/electron/sql-utils.d.ts +5 -0
  65. package/dist/vendor/electron/sql-utils.js +32 -0
  66. package/package.json +6 -3
  67. package/dist/commands/savedViews/semantics.d.ts +0 -46
  68. 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 contentMd = await parseStringInput(String(createOptions.content));
368
- const canonicalContentMd = await canonicalizeBidlinksInMarkdown(db, contentMd, identity.userId);
369
- const { contentJson, contentText } = convertMarkdownToNoteContent(canonicalContentMd);
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
- printDryRunPreview(await buildNoteCreateDryRunPreview({
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
- }), runtime);
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
- const id = generateId();
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', required: true, type: 'string' },
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: ['dino note create --title "Weekly note" --content @/tmp/note.md --type note --durability uploaded --format json'],
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
  },
@@ -18,6 +18,7 @@ export declare function buildNoteUpdateDryRunPreview(args: {
18
18
  offline: boolean;
19
19
  userId?: string;
20
20
  ids: string[];
21
+ title?: string;
21
22
  tagsInput?: string[];
22
23
  zettelBoxNames?: string[];
23
24
  starred?: boolean;
@@ -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: note.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 } : {}),