@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
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { resolveAuthIdentity } from '../../auth/userInfo.js';
|
|
2
|
+
import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
5
|
+
import { ACTIVE_PROPERTY_TYPES, isPropertyKeyValid, normalizePropertyDefConfig, PROPERTY_OPTION_COLORS, STATUS_PROPERTY_GROUPS, } from '../../vendor/electron/shared/index.js';
|
|
6
|
+
import { registerCommandGroup } from '../descriptor/register.js';
|
|
7
|
+
import { assertExclusive, collectOption, parseIntegerOption, parsePositiveIntegerOption, readJsonOption, requiredArgument, } from '../domain/options.js';
|
|
8
|
+
import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
9
|
+
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS, } from '../schema/globalOptions.js';
|
|
10
|
+
import { parseExpectedCount } from '../writeConfirmation.js';
|
|
11
|
+
import { WRITE_DURABILITY_OPTION } from '../writeDurability.js';
|
|
12
|
+
import { configureProperty, createProperty, deleteProperty, fillEmptyNoteProperties, listProperties, migrateLegacyOptions, migrationAffectedCount, previewConfigureProperty, previewCreateProperty, previewDeleteProperty, previewMigrateLegacyOptions, readNoteProperties, reorderNotePropertySlots, requireProperty, setNoteProperties, updateNotePropertySlots, } from './repo.js';
|
|
13
|
+
const PROP_GLOBAL_OPTIONS = [
|
|
14
|
+
...FORMAT_OPTIONS,
|
|
15
|
+
...OFFLINE_OPTIONS,
|
|
16
|
+
...REQUIRE_SYNC_OPTIONS,
|
|
17
|
+
...SYNC_TIMEOUT_OPTIONS,
|
|
18
|
+
];
|
|
19
|
+
const DRY_RUN_OPTION = { flags: '--dry-run', description: 'Run the write in a rolled-back transaction and preview the result', type: 'boolean' };
|
|
20
|
+
const COLORS = new Set(PROPERTY_OPTION_COLORS);
|
|
21
|
+
const GROUPS = new Set(STATUS_PROPERTY_GROUPS);
|
|
22
|
+
/** Parses `label[:color[:group]]`, peeling recognised color/group suffixes off the right. */
|
|
23
|
+
export function parseOptionSpec(spec) {
|
|
24
|
+
const parts = spec.split(':');
|
|
25
|
+
let group;
|
|
26
|
+
let color;
|
|
27
|
+
if (parts.length > 1 && GROUPS.has(parts[parts.length - 1].trim()))
|
|
28
|
+
group = parts.pop().trim();
|
|
29
|
+
if (parts.length > 1 && COLORS.has(parts[parts.length - 1].trim()))
|
|
30
|
+
color = parts.pop().trim();
|
|
31
|
+
const label = parts.join(':').trim();
|
|
32
|
+
if (!label)
|
|
33
|
+
throw new DinoxError(`Option label is required: ${spec}`, { code: 'USAGE_ERROR' });
|
|
34
|
+
return { label, ...(color ? { color } : {}), ...(group ? { group } : {}) };
|
|
35
|
+
}
|
|
36
|
+
/** Parses `key=value`; the value is JSON when it parses as JSON, otherwise a plain string. */
|
|
37
|
+
export function parseAssignment(spec) {
|
|
38
|
+
const index = spec.indexOf('=');
|
|
39
|
+
if (index <= 0)
|
|
40
|
+
throw new DinoxError(`Expected key=value, got: ${spec}`, { code: 'USAGE_ERROR' });
|
|
41
|
+
const key = spec.slice(0, index).trim();
|
|
42
|
+
const raw = spec.slice(index + 1);
|
|
43
|
+
if (!isPropertyKeyValid(key))
|
|
44
|
+
throw new DinoxError(`Invalid property key: ${key}`, { code: 'USAGE_ERROR' });
|
|
45
|
+
try {
|
|
46
|
+
return [key, JSON.parse(raw)];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [key, raw];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function parseBooleanOption(value) {
|
|
53
|
+
const normalized = value.trim().toLowerCase();
|
|
54
|
+
if (normalized === 'true')
|
|
55
|
+
return true;
|
|
56
|
+
if (normalized === 'false')
|
|
57
|
+
return false;
|
|
58
|
+
throw new DinoxError('Expected true or false', { code: 'USAGE_ERROR' });
|
|
59
|
+
}
|
|
60
|
+
function parseTypeOption(value) {
|
|
61
|
+
const normalized = value.trim();
|
|
62
|
+
if (!ACTIVE_PROPERTY_TYPES.includes(normalized)) {
|
|
63
|
+
throw new DinoxError(`--type must be one of ${ACTIVE_PROPERTY_TYPES.join(', ')}`, { code: 'USAGE_ERROR' });
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
async function readConfigOption(value) {
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
const raw = await readJsonOption(value, '--config', 'prop.create');
|
|
71
|
+
const config = normalizePropertyDefConfig(raw);
|
|
72
|
+
if (!config) {
|
|
73
|
+
throw new DinoxError('--config is not a valid property config envelope', {
|
|
74
|
+
code: 'PROPERTY_DEF_CONFIG_INVALID',
|
|
75
|
+
suggestion: 'Use e.g. {"version":1,"type":"date","mode":"datetime"} or {"version":1,"type":"select","mode":"closed"}.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
async function readOptionsJson(value, schemaPath) {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const raw = await readJsonOption(value, '--options', schemaPath);
|
|
84
|
+
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
85
|
+
if (!Array.isArray(items)) {
|
|
86
|
+
throw new DinoxError('--options must be a JSON array of {id?, label, color?, sortRank?, archived?, group?}', { code: 'USAGE_ERROR' });
|
|
87
|
+
}
|
|
88
|
+
return items;
|
|
89
|
+
}
|
|
90
|
+
async function readValuesJson(value, label, schemaPath) {
|
|
91
|
+
const raw = await readJsonOption(value, label, schemaPath);
|
|
92
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
93
|
+
throw new DinoxError(`${label} must be a JSON object of property key to value`, { code: 'USAGE_ERROR' });
|
|
94
|
+
}
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
function stringList(value) {
|
|
98
|
+
return Array.isArray(value) ? value : [];
|
|
99
|
+
}
|
|
100
|
+
function propertyLine(property) {
|
|
101
|
+
return `${property.id}\t${property.key}\t${property.type}\t${property.name}`;
|
|
102
|
+
}
|
|
103
|
+
function printNoteProperties(view) {
|
|
104
|
+
for (const entry of view.entries) {
|
|
105
|
+
const display = entry.display === null ? '(empty)' : typeof entry.display === 'string' ? entry.display : JSON.stringify(entry.display);
|
|
106
|
+
printHumanReadableLine(`${entry.key}\t${entry.type}${entry.defined ? '' : '?'}\t${display}`);
|
|
107
|
+
}
|
|
108
|
+
if (view.entries.length === 0)
|
|
109
|
+
printHumanReadableLine('No properties on this note.');
|
|
110
|
+
}
|
|
111
|
+
async function withPropRuntime(context, run) {
|
|
112
|
+
await withCommandRuntime(context, async (runtime) => {
|
|
113
|
+
const identity = await resolveAuthIdentity(runtime.config);
|
|
114
|
+
await syncBeforeCommand(runtime);
|
|
115
|
+
await run(runtime, identity.userId);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function finishWrite(runtime, options, payload, human) {
|
|
119
|
+
const receipt = await waitForMutationUpload(runtime, { durability: options.durability });
|
|
120
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
121
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...payload, ...receipt });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
human();
|
|
125
|
+
printStaleCacheWarning(runtime);
|
|
126
|
+
}
|
|
127
|
+
async function runPropList(context) {
|
|
128
|
+
const limit = context.options.limit;
|
|
129
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
130
|
+
const properties = await listProperties(runtime.db, { userId, limit });
|
|
131
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
132
|
+
printSuccess({ ok: true, stale: commandStale(runtime), count: properties.length, properties });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const property of properties)
|
|
136
|
+
printHumanReadableLine(propertyLine(property));
|
|
137
|
+
if (properties.length === 0)
|
|
138
|
+
printHumanReadableLine('No property definitions found.');
|
|
139
|
+
printStaleCacheWarning(runtime);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function runPropGet(context) {
|
|
143
|
+
const ref = requiredArgument(context.args.ref, 'Property id or key');
|
|
144
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
145
|
+
const property = await requireProperty(runtime.db, { userId, ref });
|
|
146
|
+
printSuccess({ ok: true, stale: commandStale(runtime), property });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function runPropCreate(context) {
|
|
150
|
+
const options = context.options;
|
|
151
|
+
const key = typeof options.key === 'string' ? options.key : '';
|
|
152
|
+
if (!isPropertyKeyValid(key)) {
|
|
153
|
+
throw new DinoxError('--key must be 1-64 characters without leading/trailing spaces, tabs, or newlines', { code: 'USAGE_ERROR' });
|
|
154
|
+
}
|
|
155
|
+
assertExclusive([[options.options !== undefined && stringList(options.option).length > 0, '--options cannot be used with --option']]);
|
|
156
|
+
const optionItems = options.options !== undefined
|
|
157
|
+
? await readOptionsJson(options.options, 'prop.create')
|
|
158
|
+
: stringList(options.option).length > 0 ? stringList(options.option).map(parseOptionSpec) : undefined;
|
|
159
|
+
const input = {
|
|
160
|
+
key,
|
|
161
|
+
...(typeof options.name === 'string' ? { name: options.name } : {}),
|
|
162
|
+
...(options.type === undefined ? {} : { type: parseTypeOption(String(options.type)) }),
|
|
163
|
+
...(optionItems === undefined ? {} : { optionItems }),
|
|
164
|
+
...(typeof options.uniqueIdPrefix === 'string' ? { uniqueIdPrefix: options.uniqueIdPrefix } : {}),
|
|
165
|
+
...(typeof options.description === 'string' ? { description: options.description } : {}),
|
|
166
|
+
...(typeof options.icon === 'string' ? { icon: options.icon } : {}),
|
|
167
|
+
...(options.source === 'ai' || options.source === 'manual' ? { source: options.source } : {}),
|
|
168
|
+
};
|
|
169
|
+
const config = await readConfigOption(options.config);
|
|
170
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
171
|
+
const createInput = { ...input, userId, ...(config ? { config } : {}) };
|
|
172
|
+
if (options.dryRun) {
|
|
173
|
+
const preview = await previewCreateProperty(runtime.db, createInput);
|
|
174
|
+
printDryRunPreview({
|
|
175
|
+
dry_run: true,
|
|
176
|
+
command: 'dino prop create',
|
|
177
|
+
action: preview.created ? 'create_property' : 'reuse_property',
|
|
178
|
+
risk: 'write',
|
|
179
|
+
summary: preview.created
|
|
180
|
+
? `Create ${preview.property.type} property "${key}".`
|
|
181
|
+
: `Property "${key}" already exists; new options would be merged into it.`,
|
|
182
|
+
changes: { created: preview.created, property: preview.property },
|
|
183
|
+
}, runtime);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const result = await createProperty(runtime.db, createInput);
|
|
187
|
+
await finishWrite(runtime, options, { created: result.created, property: result.property }, () => {
|
|
188
|
+
printHumanReadableLine(`${result.created ? 'Created' : 'Reused'}\t${propertyLine(result.property)}`);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function optionEdits(options) {
|
|
193
|
+
const edits = [];
|
|
194
|
+
for (const spec of stringList(options.addOption))
|
|
195
|
+
edits.push({ kind: 'add', ...parseOptionSpec(spec) });
|
|
196
|
+
for (const ref of stringList(options.archiveOption))
|
|
197
|
+
edits.push({ kind: 'archive', ref });
|
|
198
|
+
for (const ref of stringList(options.restoreOption))
|
|
199
|
+
edits.push({ kind: 'restore', ref });
|
|
200
|
+
for (const spec of stringList(options.renameOption)) {
|
|
201
|
+
const index = spec.indexOf('=');
|
|
202
|
+
if (index <= 0)
|
|
203
|
+
throw new DinoxError(`--rename-option expects <id|label>=<new label>, got: ${spec}`, { code: 'USAGE_ERROR' });
|
|
204
|
+
edits.push({ kind: 'rename', ref: spec.slice(0, index), label: spec.slice(index + 1) });
|
|
205
|
+
}
|
|
206
|
+
for (const spec of stringList(options.optionColor)) {
|
|
207
|
+
const index = spec.lastIndexOf('=');
|
|
208
|
+
const color = spec.slice(index + 1).trim();
|
|
209
|
+
if (index <= 0 || !COLORS.has(color)) {
|
|
210
|
+
throw new DinoxError(`--option-color expects <id|label>=<${PROPERTY_OPTION_COLORS.join('|')}>, got: ${spec}`, { code: 'USAGE_ERROR' });
|
|
211
|
+
}
|
|
212
|
+
edits.push({ kind: 'color', ref: spec.slice(0, index), color: color });
|
|
213
|
+
}
|
|
214
|
+
return edits;
|
|
215
|
+
}
|
|
216
|
+
async function runPropUpdate(context) {
|
|
217
|
+
const ref = requiredArgument(context.args.ref, 'Property id or key');
|
|
218
|
+
const options = context.options;
|
|
219
|
+
const edits = optionEdits(options);
|
|
220
|
+
assertExclusive([
|
|
221
|
+
[options.options !== undefined && edits.length > 0, '--options cannot be combined with --add-option/--archive-option/--restore-option/--rename-option/--option-color'],
|
|
222
|
+
[options.config !== undefined && Boolean(options.clearConfig), '--config cannot be used with --clear-config'],
|
|
223
|
+
[options.description !== undefined && Boolean(options.clearDescription), '--description cannot be used with --clear-description'],
|
|
224
|
+
[options.icon !== undefined && Boolean(options.clearIcon), '--icon cannot be used with --clear-icon'],
|
|
225
|
+
[Boolean(options.repairInvalidOptions) && options.options === undefined, '--repair-invalid-options requires --options'],
|
|
226
|
+
]);
|
|
227
|
+
const replacement = await readOptionsJson(options.options, 'prop.update');
|
|
228
|
+
const config = options.clearConfig ? null : await readConfigOption(options.config);
|
|
229
|
+
const patch = {
|
|
230
|
+
...(typeof options.name === 'string' ? { name: options.name } : {}),
|
|
231
|
+
...(options.clearDescription ? { description: null } : typeof options.description === 'string' ? { description: options.description } : {}),
|
|
232
|
+
...(options.clearIcon ? { icon: null } : typeof options.icon === 'string' ? { icon: options.icon } : {}),
|
|
233
|
+
...(options.showOnCard === undefined ? {} : { showOnCard: options.showOnCard }),
|
|
234
|
+
...(options.sortRank === undefined ? {} : { sortRank: options.sortRank }),
|
|
235
|
+
...(replacement === undefined ? {} : { options: replacement }),
|
|
236
|
+
...(edits.length > 0 ? { optionEdits: edits } : {}),
|
|
237
|
+
...(typeof options.uniqueIdPrefix === 'string' ? { uniqueIdPrefix: options.uniqueIdPrefix } : {}),
|
|
238
|
+
...(options.repairInvalidOptions ? { repairInvalidOptions: true } : {}),
|
|
239
|
+
...(config === undefined ? {} : { config }),
|
|
240
|
+
};
|
|
241
|
+
if (Object.keys(patch).length === 0) {
|
|
242
|
+
throw new DinoxError('At least one property field must be updated', { code: 'USAGE_ERROR' });
|
|
243
|
+
}
|
|
244
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
245
|
+
const input = { ...patch, userId, ref };
|
|
246
|
+
if (options.dryRun) {
|
|
247
|
+
const preview = await previewConfigureProperty(runtime.db, input);
|
|
248
|
+
printDryRunPreview({
|
|
249
|
+
dry_run: true,
|
|
250
|
+
command: 'dino prop update',
|
|
251
|
+
action: 'update_property',
|
|
252
|
+
risk: 'write',
|
|
253
|
+
summary: `Update property "${preview.before.key}".`,
|
|
254
|
+
targets: [{ id: preview.before.id, key: preview.before.key }],
|
|
255
|
+
changes: { changed: preview.changed, before: preview.before, after: preview.property },
|
|
256
|
+
}, runtime);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const result = await configureProperty(runtime.db, input);
|
|
260
|
+
await finishWrite(runtime, options, { changed: result.changed, property: result.property }, () => {
|
|
261
|
+
printHumanReadableLine(propertyLine(result.property));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function runPropDelete(context) {
|
|
266
|
+
const ref = requiredArgument(context.args.ref, 'Property id or key');
|
|
267
|
+
const options = context.options;
|
|
268
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
269
|
+
if (options.dryRun) {
|
|
270
|
+
const property = await previewDeleteProperty(runtime.db, { userId, ref });
|
|
271
|
+
printDryRunPreview({
|
|
272
|
+
dry_run: true,
|
|
273
|
+
command: 'dino prop delete',
|
|
274
|
+
action: 'delete_property',
|
|
275
|
+
risk: 'write',
|
|
276
|
+
summary: `Soft-delete property "${property.key}". Existing note values are kept and become untyped.`,
|
|
277
|
+
targets: [{ id: property.id, key: property.key }],
|
|
278
|
+
}, runtime);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const property = await deleteProperty(runtime.db, { userId, ref });
|
|
282
|
+
await finishWrite(runtime, options, { deleted: true, property }, () => {
|
|
283
|
+
printHumanReadableLine(`Deleted ${propertyLine(property)}`);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async function runPropMigrateOptions(context) {
|
|
288
|
+
const options = context.options;
|
|
289
|
+
const expectedCount = parseExpectedCount(options.expectedCount);
|
|
290
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
291
|
+
if (options.dryRun) {
|
|
292
|
+
const result = await previewMigrateLegacyOptions(runtime.db, userId);
|
|
293
|
+
printDryRunPreview({
|
|
294
|
+
dry_run: true,
|
|
295
|
+
command: 'dino prop migrate-options',
|
|
296
|
+
action: 'migrate_legacy_property_options',
|
|
297
|
+
risk: 'high-risk-write',
|
|
298
|
+
summary: 'Assign stable ids to legacy select options and rewrite note values and saved-view filters that reference them.',
|
|
299
|
+
changes: { ...result, affected_count: migrationAffectedCount(result) },
|
|
300
|
+
}, runtime);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!options.confirm)
|
|
304
|
+
throw new DinoxError('dino prop migrate-options requires --confirm for writes', { code: 'USAGE_ERROR' });
|
|
305
|
+
if (expectedCount === null)
|
|
306
|
+
throw new DinoxError('dino prop migrate-options requires --expected-count <n> for writes', { code: 'USAGE_ERROR' });
|
|
307
|
+
const result = await migrateLegacyOptions(runtime.db, { userId, expectedCount });
|
|
308
|
+
await finishWrite(runtime, options, { ...result, affected_count: migrationAffectedCount(result) }, () => {
|
|
309
|
+
printHumanReadableLine(`Migrated ${result.definitionsMigrated} definitions, ${result.notesUpdated} notes, ${result.savedViewsUpdated} views.`);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async function runPropShow(context) {
|
|
314
|
+
const noteId = requiredArgument(context.args.noteId, 'Note id');
|
|
315
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
316
|
+
const view = await readNoteProperties(runtime.db, { userId, noteId });
|
|
317
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
318
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...view });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
printNoteProperties(view);
|
|
322
|
+
printStaleCacheWarning(runtime);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function finishNoteMutation(runtime, options, command, summary, mutation) {
|
|
326
|
+
if (options.dryRun) {
|
|
327
|
+
printDryRunPreview({
|
|
328
|
+
dry_run: true,
|
|
329
|
+
command,
|
|
330
|
+
action: 'update_note_properties',
|
|
331
|
+
risk: 'write',
|
|
332
|
+
summary,
|
|
333
|
+
targets: [{ id: mutation.before.noteId }],
|
|
334
|
+
changes: { changed: mutation.changed, before: mutation.before.properties, after: mutation.after.properties, result: mutation.result },
|
|
335
|
+
}, runtime);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const payload = {
|
|
339
|
+
noteId: mutation.after.noteId,
|
|
340
|
+
changed: mutation.changed,
|
|
341
|
+
result: mutation.result,
|
|
342
|
+
properties: mutation.after.properties,
|
|
343
|
+
entries: mutation.after.entries,
|
|
344
|
+
};
|
|
345
|
+
if (!mutation.changed) {
|
|
346
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
347
|
+
printSuccess({ ok: true, stale: commandStale(runtime), ...payload });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
printHumanReadableLine('No property changes.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
await finishWrite(runtime, options, payload, () => printNoteProperties(mutation.after));
|
|
354
|
+
}
|
|
355
|
+
async function runPropSet(context) {
|
|
356
|
+
const noteId = requiredArgument(context.args.noteId, 'Note id');
|
|
357
|
+
const options = context.options;
|
|
358
|
+
const patch = options.patch === undefined ? {} : await readValuesJson(options.patch, '--patch', 'prop.set');
|
|
359
|
+
for (const spec of stringList(options.set)) {
|
|
360
|
+
const [key, value] = parseAssignment(spec);
|
|
361
|
+
patch[key] = value;
|
|
362
|
+
}
|
|
363
|
+
for (const key of stringList(options.unset)) {
|
|
364
|
+
if (!isPropertyKeyValid(key))
|
|
365
|
+
throw new DinoxError(`Invalid property key: ${key}`, { code: 'USAGE_ERROR' });
|
|
366
|
+
patch[key] = null;
|
|
367
|
+
}
|
|
368
|
+
if (Object.keys(patch).length === 0) {
|
|
369
|
+
throw new DinoxError('Pass at least one --set key=value, --unset key, or --patch', { code: 'USAGE_ERROR' });
|
|
370
|
+
}
|
|
371
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
372
|
+
const mutation = await setNoteProperties(runtime.db, {
|
|
373
|
+
userId,
|
|
374
|
+
noteId,
|
|
375
|
+
patch: patch,
|
|
376
|
+
dryRun: Boolean(options.dryRun),
|
|
377
|
+
});
|
|
378
|
+
await finishNoteMutation(runtime, options, 'dino prop set', `Update ${Object.keys(patch).length} properties on note ${noteId}.`, mutation);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async function runPropFill(context) {
|
|
382
|
+
const noteId = requiredArgument(context.args.noteId, 'Note id');
|
|
383
|
+
const options = context.options;
|
|
384
|
+
const values = await readValuesJson(options.values, '--values', 'prop.fill');
|
|
385
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
386
|
+
const mutation = await fillEmptyNoteProperties(runtime.db, {
|
|
387
|
+
userId,
|
|
388
|
+
noteId,
|
|
389
|
+
values: values,
|
|
390
|
+
dryRun: Boolean(options.dryRun),
|
|
391
|
+
});
|
|
392
|
+
await finishNoteMutation(runtime, options, 'dino prop fill', `Fill empty declared properties on note ${noteId}.`, mutation);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async function runPropSlots(context) {
|
|
396
|
+
const noteId = requiredArgument(context.args.noteId, 'Note id');
|
|
397
|
+
const options = context.options;
|
|
398
|
+
const operations = ['declare', 'clear', 'remove'].filter((operation) => stringList(options[operation]).length > 0);
|
|
399
|
+
const order = stringList(options.order);
|
|
400
|
+
if (operations.length + (order.length > 0 ? 1 : 0) !== 1) {
|
|
401
|
+
throw new DinoxError('Pass exactly one of --declare, --clear, --remove, or --order', { code: 'USAGE_ERROR' });
|
|
402
|
+
}
|
|
403
|
+
await withPropRuntime(context, async (runtime, userId) => {
|
|
404
|
+
const operation = operations[0];
|
|
405
|
+
const mutation = operation
|
|
406
|
+
? await updateNotePropertySlots(runtime.db, { userId, noteId, operation, keys: stringList(options[operation]), dryRun: Boolean(options.dryRun) })
|
|
407
|
+
: await reorderNotePropertySlots(runtime.db, { userId, noteId, orderedKeys: order, dryRun: Boolean(options.dryRun) });
|
|
408
|
+
await finishNoteMutation(runtime, options, 'dino prop slots', `${operation ?? 'reorder'} property slots on note ${noteId}.`, mutation);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const NOTE_MUTATION_FIELDS = ['noteId', 'changed', 'result', 'properties', 'entries', 'durability', 'upload_queue_remaining', 'stale'];
|
|
412
|
+
export const propertyCommandGroup = {
|
|
413
|
+
name: 'prop',
|
|
414
|
+
description: 'Manage property definitions and note property values',
|
|
415
|
+
commands: [
|
|
416
|
+
{
|
|
417
|
+
name: 'list',
|
|
418
|
+
description: 'List property definitions (types, option ids, config)',
|
|
419
|
+
options: [
|
|
420
|
+
{ flags: '--limit <n>', description: 'Maximum returned definitions (1-5000)', parser: parsePositiveIntegerOption, type: 'number', defaultValue: 1000 },
|
|
421
|
+
],
|
|
422
|
+
schema: {
|
|
423
|
+
path: 'prop.list',
|
|
424
|
+
risk: 'read',
|
|
425
|
+
auth: 'required',
|
|
426
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
427
|
+
output: { type: 'structured', summary: 'Property definition catalog in sort order', fields: ['stale', 'count', 'properties'] },
|
|
428
|
+
examples: ['dino prop list --format json'],
|
|
429
|
+
},
|
|
430
|
+
action: runPropList,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'get',
|
|
434
|
+
description: 'Get one property definition by id or key',
|
|
435
|
+
arguments: [{ syntax: '<ref>', description: 'Property definition id or key' }],
|
|
436
|
+
schema: {
|
|
437
|
+
path: 'prop.get',
|
|
438
|
+
risk: 'read',
|
|
439
|
+
auth: 'required',
|
|
440
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
441
|
+
output: { type: 'structured', summary: 'Property definition', fields: ['stale', 'property'] },
|
|
442
|
+
examples: ['dino prop get 状态 --format json'],
|
|
443
|
+
},
|
|
444
|
+
action: runPropGet,
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'create',
|
|
448
|
+
description: 'Create a property definition, or reuse the existing one with this key (new options are merged)',
|
|
449
|
+
options: [
|
|
450
|
+
{ flags: '--key <key>', description: 'Stable key stored in note properties; immutable after creation', required: true, type: 'string' },
|
|
451
|
+
{ flags: '--name <name>', description: 'Display name (defaults to the key)', type: 'string' },
|
|
452
|
+
{ flags: '--type <type>', description: `Property type: ${ACTIVE_PROPERTY_TYPES.join(', ')} (defaults to text)`, type: 'string', enum: [...ACTIVE_PROPERTY_TYPES] },
|
|
453
|
+
{ flags: '--option <label[:color[:group]]>', description: 'Add a select/multi_select/status option (repeatable); status defaults to todo/in_progress/complete options', parser: collectOption, type: 'string[]' },
|
|
454
|
+
{ flags: '--options <json|@file>', description: 'Option list JSON: [{label, color?, sortRank?, archived?, group?}]', type: 'string' },
|
|
455
|
+
{ flags: '--config <json|@file>', description: 'Type config envelope, e.g. {"version":1,"type":"date","mode":"datetime"}', type: 'string' },
|
|
456
|
+
{ flags: '--unique-id-prefix <prefix>', description: 'Prefix for unique_id values (A-Z0-9, max 12)', type: 'string' },
|
|
457
|
+
{ flags: '--description <text>', description: 'Semantic description (helps AI fill values)', type: 'string' },
|
|
458
|
+
{ flags: '--icon <icon>', description: 'Icon text', type: 'string' },
|
|
459
|
+
{ flags: '--source <source>', description: 'Definition source: manual or ai', type: 'string', enum: ['manual', 'ai'] },
|
|
460
|
+
WRITE_DURABILITY_OPTION,
|
|
461
|
+
DRY_RUN_OPTION,
|
|
462
|
+
],
|
|
463
|
+
schema: {
|
|
464
|
+
path: 'prop.create',
|
|
465
|
+
risk: 'write',
|
|
466
|
+
auth: 'required',
|
|
467
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
468
|
+
output: { type: 'structured', summary: 'Created or reused property definition', fields: ['created', 'property', 'durability', 'upload_queue_remaining', 'stale'] },
|
|
469
|
+
examples: [
|
|
470
|
+
'dino prop create --key 状态 --type status --format json --dry-run',
|
|
471
|
+
'dino prop create --key priority --type select --option High:red --option Low:gray --format json',
|
|
472
|
+
'dino prop create --key due --type date --config \'{"version":1,"type":"date","mode":"datetime"}\' --format json',
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
action: runPropCreate,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: 'update',
|
|
479
|
+
description: 'Update a property definition: name, description, icon, card visibility, order, options, config',
|
|
480
|
+
arguments: [{ syntax: '<ref>', description: 'Property definition id or key' }],
|
|
481
|
+
options: [
|
|
482
|
+
{ flags: '--name <name>', description: 'Rename (note data is keyed by the immutable key and is not touched)', type: 'string' },
|
|
483
|
+
{ flags: '--description <text>', description: 'Replace the description', type: 'string' },
|
|
484
|
+
{ flags: '--clear-description', description: 'Clear the description', type: 'boolean' },
|
|
485
|
+
{ flags: '--icon <icon>', description: 'Replace the icon', type: 'string' },
|
|
486
|
+
{ flags: '--clear-icon', description: 'Clear the icon', type: 'boolean' },
|
|
487
|
+
{ flags: '--show-on-card <bool>', description: 'Show on compact note cards: true or false', parser: parseBooleanOption, type: 'boolean' },
|
|
488
|
+
{ flags: '--sort-rank <n>', description: 'Catalog sort rank', parser: parseIntegerOption, type: 'number' },
|
|
489
|
+
{ flags: '--add-option <label[:color[:group]]>', description: 'Append an option (repeatable)', parser: collectOption, type: 'string[]' },
|
|
490
|
+
{ flags: '--archive-option <id|label>', description: 'Archive an option; options are never removed (repeatable)', parser: collectOption, type: 'string[]' },
|
|
491
|
+
{ flags: '--restore-option <id|label>', description: 'Un-archive an option (repeatable)', parser: collectOption, type: 'string[]' },
|
|
492
|
+
{ flags: '--rename-option <id|label=new>', description: 'Relabel an option; stored values keep its id (repeatable)', parser: collectOption, type: 'string[]' },
|
|
493
|
+
{ flags: '--option-color <id|label=color>', description: 'Recolor an option (repeatable)', parser: collectOption, type: 'string[]' },
|
|
494
|
+
{ flags: '--options <json|@file>', description: 'Replace the whole option list; existing options must keep their id (archive instead of removing)', type: 'string' },
|
|
495
|
+
{ flags: '--repair-invalid-options', description: 'With --options: replace malformed stored options by id-less items', type: 'boolean' },
|
|
496
|
+
{ flags: '--unique-id-prefix <prefix>', description: 'Replace the unique_id prefix (issued numbers are kept)', type: 'string' },
|
|
497
|
+
{ flags: '--config <json|@file>', description: 'Replace the type config envelope', type: 'string' },
|
|
498
|
+
{ flags: '--clear-config', description: 'Remove the explicit config and restore the type default', type: 'boolean' },
|
|
499
|
+
WRITE_DURABILITY_OPTION,
|
|
500
|
+
DRY_RUN_OPTION,
|
|
501
|
+
],
|
|
502
|
+
schema: {
|
|
503
|
+
path: 'prop.update',
|
|
504
|
+
risk: 'write',
|
|
505
|
+
auth: 'required',
|
|
506
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
507
|
+
output: { type: 'structured', summary: 'Updated property definition', fields: ['changed', 'property', 'durability', 'upload_queue_remaining', 'stale'] },
|
|
508
|
+
examples: [
|
|
509
|
+
'dino prop update priority --add-option Medium:yellow --format json --dry-run',
|
|
510
|
+
'dino prop update priority --rename-option High=Urgent --archive-option Low --format json',
|
|
511
|
+
'dino prop update due --config \'{"version":1,"type":"date","mode":"date"}\' --format json',
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
action: runPropUpdate,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: 'delete',
|
|
518
|
+
description: 'Soft-delete a property definition; note values are kept and shown as untyped',
|
|
519
|
+
arguments: [{ syntax: '<ref>', description: 'Property definition id or key' }],
|
|
520
|
+
options: [WRITE_DURABILITY_OPTION, DRY_RUN_OPTION],
|
|
521
|
+
schema: {
|
|
522
|
+
path: 'prop.delete',
|
|
523
|
+
risk: 'write',
|
|
524
|
+
auth: 'required',
|
|
525
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
526
|
+
output: { type: 'structured', summary: 'Deleted property definition', fields: ['deleted', 'property', 'durability', 'upload_queue_remaining', 'stale'] },
|
|
527
|
+
examples: ['dino prop delete priority --format json --dry-run'],
|
|
528
|
+
},
|
|
529
|
+
action: runPropDelete,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: 'migrate-options',
|
|
533
|
+
description: 'Migrate legacy string-array select options to stable option ids (rewrites note values and view filters)',
|
|
534
|
+
options: [
|
|
535
|
+
DRY_RUN_OPTION,
|
|
536
|
+
{ flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
|
|
537
|
+
{ flags: '--expected-count <n>', description: 'Must equal the dry run changes.affected_count', type: 'number' },
|
|
538
|
+
WRITE_DURABILITY_OPTION,
|
|
539
|
+
],
|
|
540
|
+
schema: {
|
|
541
|
+
path: 'prop.migrate-options',
|
|
542
|
+
risk: 'high-risk-write',
|
|
543
|
+
auth: 'required',
|
|
544
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
545
|
+
output: { type: 'structured', summary: 'Migration counts', fields: ['definitionsMigrated', 'optionsCreated', 'notesUpdated', 'savedViewsUpdated', 'affected_count', 'durability', 'upload_queue_remaining', 'stale'] },
|
|
546
|
+
examples: [
|
|
547
|
+
'dino prop migrate-options --dry-run --format json',
|
|
548
|
+
'dino prop migrate-options --expected-count 42 --confirm --format json',
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
action: runPropMigrateOptions,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: 'show',
|
|
555
|
+
description: 'Show a note\'s property values resolved against their definitions',
|
|
556
|
+
arguments: [{ syntax: '<noteId>', description: 'Note id' }],
|
|
557
|
+
schema: {
|
|
558
|
+
path: 'prop.show',
|
|
559
|
+
risk: 'read',
|
|
560
|
+
auth: 'required',
|
|
561
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
562
|
+
output: { type: 'structured', summary: 'Note properties (null = declared empty slot) with typed entries', fields: ['stale', 'noteId', 'valid', 'properties', 'entries'] },
|
|
563
|
+
examples: ['dino prop show <noteId> --format json'],
|
|
564
|
+
},
|
|
565
|
+
action: runPropShow,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: 'set',
|
|
569
|
+
description: 'Set or remove note property values; values are coerced and validated against their definitions',
|
|
570
|
+
arguments: [{ syntax: '<noteId>', description: 'Note id' }],
|
|
571
|
+
options: [
|
|
572
|
+
{ flags: '--set <key=value>', description: 'Set a value (repeatable); JSON values are parsed, select options accept a label or id', parser: collectOption, type: 'string[]' },
|
|
573
|
+
{ flags: '--unset <key>', description: 'Remove a property from the note (repeatable)', parser: collectOption, type: 'string[]' },
|
|
574
|
+
{ flags: '--patch <json|@file>', description: 'JSON object of key to value; null removes the key', type: 'string' },
|
|
575
|
+
WRITE_DURABILITY_OPTION,
|
|
576
|
+
DRY_RUN_OPTION,
|
|
577
|
+
],
|
|
578
|
+
schema: {
|
|
579
|
+
path: 'prop.set',
|
|
580
|
+
risk: 'write',
|
|
581
|
+
auth: 'required',
|
|
582
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
583
|
+
output: { type: 'structured', summary: 'Note properties after the write', fields: NOTE_MUTATION_FIELDS },
|
|
584
|
+
examples: [
|
|
585
|
+
'dino prop set <noteId> --set 状态=进行中 --set rating=4 --format json --dry-run',
|
|
586
|
+
'dino prop set <noteId> --patch \'{"due":"2026-10-31","tags":["a","b"]}\' --format json',
|
|
587
|
+
'dino prop set <noteId> --unset obsolete --format json',
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
action: runPropSet,
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: 'fill',
|
|
594
|
+
description: 'Fill only properties that are declared but still empty on the note; other keys are skipped',
|
|
595
|
+
arguments: [{ syntax: '<noteId>', description: 'Note id' }],
|
|
596
|
+
options: [
|
|
597
|
+
{ flags: '--values <json|@file>', description: 'JSON object of key to value', required: true, type: 'string' },
|
|
598
|
+
WRITE_DURABILITY_OPTION,
|
|
599
|
+
DRY_RUN_OPTION,
|
|
600
|
+
],
|
|
601
|
+
schema: {
|
|
602
|
+
path: 'prop.fill',
|
|
603
|
+
risk: 'write',
|
|
604
|
+
auth: 'required',
|
|
605
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
606
|
+
output: { type: 'structured', summary: 'Applied and skipped keys plus resulting properties', fields: NOTE_MUTATION_FIELDS },
|
|
607
|
+
examples: ['dino prop fill <noteId> --values \'{"summary":"…","rating":3}\' --format json'],
|
|
608
|
+
},
|
|
609
|
+
action: runPropFill,
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'slots',
|
|
613
|
+
description: 'Declare empty property slots, clear values, remove keys, or reorder keys on a note',
|
|
614
|
+
arguments: [{ syntax: '<noteId>', description: 'Note id' }],
|
|
615
|
+
options: [
|
|
616
|
+
{ flags: '--declare <key>', description: 'Add an empty slot for a key (repeatable)', parser: collectOption, type: 'string[]' },
|
|
617
|
+
{ flags: '--clear <key>', description: 'Clear a value but keep the slot (repeatable)', parser: collectOption, type: 'string[]' },
|
|
618
|
+
{ flags: '--remove <key>', description: 'Remove a key and its value (repeatable)', parser: collectOption, type: 'string[]' },
|
|
619
|
+
{ flags: '--order <key>', description: 'Preferred display order of visible keys (repeat in order, at least 2)', parser: collectOption, type: 'string[]' },
|
|
620
|
+
WRITE_DURABILITY_OPTION,
|
|
621
|
+
DRY_RUN_OPTION,
|
|
622
|
+
],
|
|
623
|
+
schema: {
|
|
624
|
+
path: 'prop.slots',
|
|
625
|
+
risk: 'write',
|
|
626
|
+
auth: 'required',
|
|
627
|
+
globalOptions: PROP_GLOBAL_OPTIONS,
|
|
628
|
+
output: { type: 'structured', summary: 'Note properties after the slot change', fields: NOTE_MUTATION_FIELDS },
|
|
629
|
+
examples: [
|
|
630
|
+
'dino prop slots <noteId> --declare 状态 --declare due --format json',
|
|
631
|
+
'dino prop slots <noteId> --order due --order 状态 --format json',
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
action: runPropSlots,
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
export function registerPropertyCommands(program) {
|
|
639
|
+
registerCommandGroup(program, propertyCommandGroup);
|
|
640
|
+
}
|