@dinoxx/dinox-cli 1.0.18 → 1.0.20

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 (118) hide show
  1. package/README.md +807 -418
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +28 -11
  4. package/dist/cli.js +15 -1
  5. package/dist/commands/auth/index.d.ts +2 -0
  6. package/dist/commands/auth/index.js +217 -163
  7. package/dist/commands/boxes/index.d.ts +2 -0
  8. package/dist/commands/boxes/index.js +119 -75
  9. package/dist/commands/boxes/repo.d.ts +8 -0
  10. package/dist/commands/boxes/repo.js +41 -0
  11. package/dist/commands/config/index.d.ts +2 -0
  12. package/dist/commands/config/index.js +85 -36
  13. package/dist/commands/descriptor/register.d.ts +8 -0
  14. package/dist/commands/descriptor/register.js +113 -0
  15. package/dist/commands/descriptor/types.d.ts +37 -0
  16. package/dist/commands/descriptor/types.js +17 -0
  17. package/dist/commands/graph/index.d.ts +2 -0
  18. package/dist/commands/graph/index.js +143 -73
  19. package/dist/commands/info/index.d.ts +2 -0
  20. package/dist/commands/info/index.js +41 -14
  21. package/dist/commands/mcp/index.js +11 -10
  22. package/dist/commands/mcp/server.js +8 -4
  23. package/dist/commands/mcp/tools.js +30 -104
  24. package/dist/commands/mcp/types.d.ts +8 -1
  25. package/dist/commands/meta/index.d.ts +2 -0
  26. package/dist/commands/meta/index.js +64 -33
  27. package/dist/commands/meta/repo.d.ts +6 -0
  28. package/dist/commands/meta/repo.js +6 -0
  29. package/dist/commands/notes/actions.d.ts +10 -0
  30. package/dist/commands/notes/actions.js +403 -0
  31. package/dist/commands/notes/descriptors.d.ts +4 -0
  32. package/dist/commands/notes/descriptors.js +250 -0
  33. package/dist/commands/notes/dryRun.d.ts +31 -0
  34. package/dist/commands/notes/dryRun.js +130 -0
  35. package/dist/commands/notes/index.d.ts +2 -51
  36. package/dist/commands/notes/index.js +2 -581
  37. package/dist/commands/notes/repo.d.ts +9 -0
  38. package/dist/commands/notes/repo.js +56 -10
  39. package/dist/commands/notes/runtime.d.ts +28 -0
  40. package/dist/commands/notes/runtime.js +185 -0
  41. package/dist/commands/notes/searchOutput.d.ts +10 -2
  42. package/dist/commands/notes/searchOutput.js +14 -2
  43. package/dist/commands/notes/searchResponse.d.ts +51 -0
  44. package/dist/commands/notes/searchResponse.js +56 -0
  45. package/dist/commands/notes/searchSql.js +2 -0
  46. package/dist/commands/prompt/index.d.ts +2 -0
  47. package/dist/commands/prompt/index.js +122 -72
  48. package/dist/commands/prompt/repo.d.ts +7 -0
  49. package/dist/commands/prompt/repo.js +46 -0
  50. package/dist/commands/schema/commandMetadata.d.ts +2 -0
  51. package/dist/commands/schema/commandMetadata.js +18 -0
  52. package/dist/commands/schema/globalOptions.d.ts +3 -0
  53. package/dist/commands/schema/globalOptions.js +3 -0
  54. package/dist/commands/schema/index.d.ts +4 -0
  55. package/dist/commands/schema/index.js +109 -0
  56. package/dist/commands/schema/renderSkills.d.ts +6 -0
  57. package/dist/commands/schema/renderSkills.js +216 -0
  58. package/dist/commands/schema/spec.d.ts +6 -0
  59. package/dist/commands/schema/spec.js +40 -0
  60. package/dist/commands/schema/types.d.ts +47 -0
  61. package/dist/commands/schema/types.js +1 -0
  62. package/dist/commands/storage/index.d.ts +4 -0
  63. package/dist/commands/storage/index.js +292 -0
  64. package/dist/commands/storage/repo.d.ts +162 -0
  65. package/dist/commands/storage/repo.js +770 -0
  66. package/dist/commands/sync.d.ts +2 -0
  67. package/dist/commands/sync.js +41 -27
  68. package/dist/commands/tags/index.d.ts +2 -0
  69. package/dist/commands/tags/index.js +121 -79
  70. package/dist/commands/tags/repo.d.ts +7 -0
  71. package/dist/commands/tags/repo.js +52 -0
  72. package/dist/commands/todo/dryRun.d.ts +23 -0
  73. package/dist/commands/todo/dryRun.js +133 -0
  74. package/dist/commands/todo/index.d.ts +2 -0
  75. package/dist/commands/todo/index.js +285 -135
  76. package/dist/commands/todo/repo.d.ts +11 -0
  77. package/dist/commands/todo/repo.js +37 -9
  78. package/dist/commands/update.d.ts +3 -0
  79. package/dist/commands/update.js +35 -13
  80. package/dist/commands/writeDryRun.d.ts +62 -0
  81. package/dist/commands/writeDryRun.js +131 -0
  82. package/dist/config/paths.d.ts +2 -1
  83. package/dist/config/paths.js +11 -1
  84. package/dist/config/resolve.js +7 -5
  85. package/dist/constants/links.d.ts +1 -1
  86. package/dist/constants/links.js +1 -1
  87. package/dist/daemon/ensure.js +3 -2
  88. package/dist/daemon/index.js +17 -15
  89. package/dist/dinox.js +9 -13
  90. package/dist/powersync/runtime.d.ts +1 -1
  91. package/dist/powersync/runtime.js +5 -3
  92. package/dist/powersync/schema/index.d.ts +27 -0
  93. package/dist/powersync/schema/index.js +2 -0
  94. package/dist/powersync/schema/note.d.ts +1 -0
  95. package/dist/powersync/schema/note.js +1 -0
  96. package/dist/powersync/schema/projects.d.ts +3 -0
  97. package/dist/powersync/schema/projects.js +3 -0
  98. package/dist/powersync/schema/storage.d.ts +24 -0
  99. package/dist/powersync/schema/storage.js +24 -0
  100. package/dist/skills/check.d.ts +6 -0
  101. package/dist/skills/check.js +254 -0
  102. package/dist/utils/bidlinkCanonicalization.d.ts +2 -0
  103. package/dist/utils/bidlinkCanonicalization.js +161 -0
  104. package/dist/utils/dryRun.d.ts +13 -0
  105. package/dist/utils/dryRun.js +69 -0
  106. package/dist/utils/output.d.ts +19 -0
  107. package/dist/utils/output.js +155 -1
  108. package/dist/utils/terminalSanitization.d.ts +2 -0
  109. package/dist/utils/terminalSanitization.js +33 -0
  110. package/dist/utils/tiptapMarkdown.d.ts +4 -1
  111. package/dist/utils/tiptapMarkdown.js +285 -113
  112. package/dist/utils/tiptapMarkdownSchema.d.ts +37 -0
  113. package/dist/utils/tiptapMarkdownSchema.js +577 -0
  114. package/dist/utils/tiptapMarkdownSchemaUtils.d.ts +74 -0
  115. package/dist/utils/tiptapMarkdownSchemaUtils.js +394 -0
  116. package/dist/utils/updateNotice.d.ts +7 -0
  117. package/dist/utils/updateNotice.js +64 -0
  118. package/package.json +25 -3
@@ -1,12 +1,17 @@
1
+ import { resolveAuthIdentity } from '../../auth/userInfo.js';
1
2
  import { loadConfig } from '../../config/store.js';
2
3
  import { resolveConfig } from '../../config/resolve.js';
3
- import { DinoxError } from '../../utils/errors.js';
4
- import { printYaml } from '../../utils/output.js';
5
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
6
4
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
7
5
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
8
- import { resolveAuthIdentity } from '../../auth/userInfo.js';
9
- import { addBox, listBoxes } from './repo.js';
6
+ import { registerCommandGroup } from '../descriptor/register.js';
7
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
8
+ import { buildBoxAddDryRunPreview } from '../writeDryRun.js';
9
+ import { printDryRunPreview } from '../../utils/dryRun.js';
10
+ import { DinoxError } from '../../utils/errors.js';
11
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
12
+ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
13
+ import { addBox, inspectAddBox, listBoxes } from './repo.js';
14
+ const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
10
15
  async function syncBeforeBoxCommand(db, offline, timeoutMs) {
11
16
  if (offline) {
12
17
  return;
@@ -14,85 +19,124 @@ async function syncBeforeBoxCommand(db, offline, timeoutMs) {
14
19
  await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
15
20
  await syncNoteTokenIndex(db);
16
21
  }
17
- async function runBoxListCommand(options, command) {
18
- const globals = command.optsWithGlobals?.() ?? {};
19
- const offline = Boolean(options.offline ?? globals.offline);
20
- const config = resolveConfig(await loadConfig());
21
- const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
22
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
22
+ async function withBoxDb(context, run) {
23
+ const rawConfig = await loadConfig();
24
+ const config = resolveConfig(rawConfig);
25
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? config.sync.timeoutMs;
26
+ const offline = Boolean(context.globals.offline);
27
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
23
28
  try {
24
- await syncBeforeBoxCommand(db, offline, timeoutMs);
25
- const boxes = await listBoxes(db);
26
- printYaml(boxes);
29
+ await run({ db, config, rawConfig, offline, timeoutMs, stale });
27
30
  }
28
31
  finally {
29
32
  await db.close().catch(() => undefined);
30
33
  }
31
34
  }
32
- export function registerBoxesCommand(program) {
33
- const box = program.command('box').description('Manage zettel boxes');
34
- box
35
- .command('list')
36
- .description('List zettel boxes (id and name) from c_zettel_box')
37
- .option('--offline', 'skip connect/sync and only use local cache')
38
- .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
39
- .action(runBoxListCommand);
40
- box
41
- .command('add')
42
- .description('Create a new zettel box in c_zettel_box')
43
- .argument('[name]', 'box name')
44
- .option('--name <string>', 'box name (alternative to positional name)')
45
- .option('--description <string>', 'box purpose/usage description (helps AI route notes to this box)')
46
- .option('--color <string>', 'box color')
47
- .option('--json', 'output machine-readable YAML')
48
- .option('--offline', 'skip connect/sync and only use local cache')
49
- .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
50
- .action(async (nameArg, options, command) => {
51
- const globals = command.optsWithGlobals?.() ?? {};
52
- const offline = Boolean(options.offline ?? globals.offline);
53
- const jsonOutput = Boolean(options.json ?? globals.json);
54
- const optionName = typeof options.name === 'string' ? options.name : undefined;
55
- const name = (nameArg ?? optionName ?? '').trim();
56
- if (!name) {
57
- throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
35
+ async function runBoxList(context) {
36
+ await withBoxDb(context, async ({ db, offline, timeoutMs }) => {
37
+ await syncBeforeBoxCommand(db, offline, timeoutMs);
38
+ printSuccess(await listBoxes(db));
39
+ });
40
+ }
41
+ async function runBoxAdd(context) {
42
+ const nameArg = context.args.name;
43
+ const optionName = typeof context.options.name === 'string' ? context.options.name : undefined;
44
+ const name = (nameArg ?? optionName ?? '').trim();
45
+ if (!name) {
46
+ throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
47
+ }
48
+ if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
49
+ throw new DinoxError('Conflicting box name values from positional argument and --name option.');
50
+ }
51
+ const structuredOutput = getRequestedStructuredOutputFormat() !== null;
52
+ await withBoxDb(context, async ({ db, config, rawConfig, offline, timeoutMs, stale }) => {
53
+ const identity = await resolveAuthIdentity(rawConfig, config);
54
+ await syncBeforeBoxCommand(db, offline, timeoutMs);
55
+ const description = typeof context.options.description === 'string' ? context.options.description : undefined;
56
+ const color = typeof context.options.color === 'string' ? context.options.color : undefined;
57
+ if (Boolean(context.options.dryRun)) {
58
+ printDryRunPreview(buildBoxAddDryRunPreview({
59
+ config,
60
+ offline,
61
+ ...(await inspectAddBox(db, {
62
+ name,
63
+ userId: identity.userId,
64
+ description,
65
+ color,
66
+ })),
67
+ }));
68
+ return;
58
69
  }
59
- if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
60
- throw new DinoxError('Conflicting box name values from positional argument and --name option.');
70
+ const result = await addBox(db, {
71
+ name,
72
+ userId: identity.userId,
73
+ description,
74
+ color,
75
+ });
76
+ if (!offline && !config.powersync.uploadBaseUrl) {
77
+ printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
61
78
  }
62
- const rawConfig = await loadConfig();
63
- const config = resolveConfig(rawConfig);
64
- const timeoutMs = parseSyncTimeoutMs(options.syncTimeout ?? globals.syncTimeout) ?? config.sync.timeoutMs;
65
- const identity = await resolveAuthIdentity(rawConfig, config);
66
- const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
67
- try {
68
- await syncBeforeBoxCommand(db, offline, timeoutMs);
69
- const result = await addBox(db, {
70
- name,
71
- userId: identity.userId,
72
- description: typeof options.description === 'string' ? options.description : undefined,
73
- color: typeof options.color === 'string' ? options.color : undefined,
74
- });
75
- if (!offline && !config.powersync.uploadBaseUrl) {
76
- console.log('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
77
- }
78
- if (!offline) {
79
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
80
- }
81
- const payload = { ok: true, stale: offline ? true : stale, ...result };
82
- if (jsonOutput) {
83
- printYaml(payload);
84
- return;
85
- }
86
- console.log(result.id);
87
- if (stale && !offline) {
88
- console.log('warning: sync timed out; results may be stale');
89
- }
79
+ if (!offline) {
80
+ await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
90
81
  }
91
- finally {
92
- await db.close().catch(() => undefined);
82
+ const payload = { ok: true, stale: offline ? true : stale, ...result };
83
+ if (structuredOutput) {
84
+ printSuccess(payload);
85
+ return;
86
+ }
87
+ printHumanReadableLine(result.id);
88
+ if (stale && !offline) {
89
+ printHumanReadableLine('warning: sync timed out; results may be stale');
93
90
  }
94
91
  });
95
- box.action((_options, command) => {
96
- command.outputHelp();
97
- });
92
+ }
93
+ export const boxCommandGroup = {
94
+ name: 'box',
95
+ description: 'Manage zettel boxes',
96
+ commands: [
97
+ {
98
+ name: 'list',
99
+ description: 'List all zettel boxes from c_zettel_box',
100
+ schema: {
101
+ path: 'box.list',
102
+ risk: 'read',
103
+ auth: 'required',
104
+ globalOptions: BOX_GLOBAL_OPTIONS,
105
+ output: {
106
+ type: 'structured',
107
+ summary: 'Box list payload',
108
+ fields: ['id', 'name', 'description'],
109
+ },
110
+ examples: ['dino box list --format json'],
111
+ },
112
+ action: runBoxList,
113
+ },
114
+ {
115
+ name: 'add',
116
+ description: 'Create a zettel box, restoring a deleted row when possible',
117
+ arguments: [{ syntax: '[name]', description: 'Box name' }],
118
+ options: [
119
+ { flags: '--name <string>', description: 'Box name (alternative to positional name)' },
120
+ { flags: '--description <string>', description: 'Box purpose/usage description' },
121
+ { flags: '--color <string>', description: 'Box color' },
122
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
123
+ ],
124
+ schema: {
125
+ path: 'box.add',
126
+ risk: 'write',
127
+ auth: 'required',
128
+ globalOptions: BOX_GLOBAL_OPTIONS,
129
+ output: {
130
+ type: 'structured',
131
+ summary: 'Box creation acknowledgement',
132
+ fields: ['id', 'name', 'description', 'color', 'restored', 'stale'],
133
+ },
134
+ examples: ['dino box add "Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
135
+ },
136
+ action: runBoxAdd,
137
+ },
138
+ ],
139
+ };
140
+ export function registerBoxesCommand(program) {
141
+ registerCommandGroup(program, boxCommandGroup);
98
142
  }
@@ -17,5 +17,13 @@ export type AddBoxResult = {
17
17
  color: string;
18
18
  restored: boolean;
19
19
  };
20
+ export type InspectAddBoxResult = {
21
+ name: string;
22
+ description: string;
23
+ color: string;
24
+ restored: boolean;
25
+ existingId?: string;
26
+ };
20
27
  export declare function listBoxes(db: AbstractPowerSyncDatabase): Promise<BoxListItem[]>;
21
28
  export declare function addBox(db: AbstractPowerSyncDatabase, input: AddBoxInput): Promise<AddBoxResult>;
29
+ export declare function inspectAddBox(db: AbstractPowerSyncDatabase, input: AddBoxInput): Promise<InspectAddBoxResult>;
@@ -152,3 +152,44 @@ export async function addBox(db, input) {
152
152
  restored: false,
153
153
  };
154
154
  }
155
+ export async function inspectAddBox(db, input) {
156
+ const userId = input.userId.trim();
157
+ const name = normalizeName(input.name);
158
+ if (!name) {
159
+ throw new DinoxError('Box name is required');
160
+ }
161
+ const description = normalizeName(input.description ?? '');
162
+ const color = normalizeName(input.color ?? '');
163
+ const lookup = normalizeLookup(name);
164
+ const columnNames = await getBoxColumns(db);
165
+ const requiredColumns = ['id', 'name', 'user_no'];
166
+ const missingRequired = requiredColumns.filter((column) => !columnNames.has(column));
167
+ if (missingRequired.length > 0) {
168
+ throw new DinoxError(`c_zettel_box table is missing required columns: ${missingRequired.join(', ')}`);
169
+ }
170
+ if (!userId) {
171
+ throw new DinoxError('c_zettel_box insert requires user_no. Missing persisted userId. Run `dino auth login` first.');
172
+ }
173
+ const rows = await listBoxRowsForWrite(db);
174
+ const sameNameRows = rows.filter((row) => normalizeLookup(row.name ?? '') === lookup);
175
+ const activeRows = sameNameRows.filter((row) => row.is_del !== 1);
176
+ if (activeRows.length > 0) {
177
+ const ids = activeRows.map((row) => row.id?.trim() ?? '').filter(Boolean);
178
+ throw new DinoxError(`Zettel box already exists: ${name}`, {
179
+ details: {
180
+ type: 'box_exists',
181
+ name,
182
+ ids,
183
+ question: 'This box already exists. Do you want to keep using the existing box?',
184
+ },
185
+ });
186
+ }
187
+ const deletedRow = sameNameRows.find((row) => (row.id?.trim() ?? '').length > 0);
188
+ return {
189
+ name,
190
+ description,
191
+ color,
192
+ restored: Boolean(deletedRow?.id),
193
+ ...(deletedRow?.id ? { existingId: deletedRow.id } : {}),
194
+ };
195
+ }
@@ -1,3 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
2
3
  export declare function sanitizeConfigForDisplay(config: unknown): unknown;
4
+ export declare const configCommandGroup: CommandGroupDescriptor;
3
5
  export declare function registerConfigCommands(program: Command): void;
@@ -1,8 +1,10 @@
1
1
  import { isConfigKey, isFixedConfigKey, isReadableConfigKey } from '../../config/keys.js';
2
2
  import { getConfigValue, setConfigValue } from '../../config/store.js';
3
- import { redactAuthorization } from '../../utils/redact.js';
3
+ import { registerCommandGroup } from '../descriptor/register.js';
4
+ import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
4
5
  import { DinoxError } from '../../utils/errors.js';
5
- import { printYaml } from '../../utils/output.js';
6
+ import { getRequestedStructuredOutputFormat, printSuccess } from '../../utils/output.js';
7
+ import { redactAuthorization } from '../../utils/redact.js';
6
8
  export function sanitizeConfigForDisplay(config) {
7
9
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
8
10
  return config;
@@ -15,40 +17,87 @@ export function sanitizeConfigForDisplay(config) {
15
17
  delete clone.powersync;
16
18
  return clone;
17
19
  }
18
- export function registerConfigCommands(program) {
19
- const configCmd = program.command('config').description('Read and write Dinox configuration');
20
- configCmd
21
- .command('get')
22
- .description('Get configuration value')
23
- .argument('[key]', 'config key (dot path)')
24
- .action(async (key) => {
25
- if (key && !isReadableConfigKey(key)) {
26
- throw new DinoxError(`Unknown config key: ${key}`);
27
- }
28
- const value = await getConfigValue(key);
29
- const redacted = key ? value : sanitizeConfigForDisplay(value);
30
- printYaml(redacted);
31
- });
32
- configCmd
33
- .command('set')
34
- .description('Set configuration value')
35
- .argument('<key>', 'config key (dot path)')
36
- .argument('<value>', 'value')
37
- .action(async (key, value) => {
38
- if (isFixedConfigKey(key)) {
39
- throw new DinoxError(`${key} is fixed and cannot be changed`);
40
- }
41
- if (!isConfigKey(key)) {
42
- throw new DinoxError(`Unknown config key: ${key}`);
20
+ async function runConfigGet(context) {
21
+ const key = context.args.key;
22
+ if (key && !isReadableConfigKey(key)) {
23
+ throw new DinoxError(`Unknown config key: ${key}`);
24
+ }
25
+ const value = await getConfigValue(key);
26
+ printSuccess(key ? value : sanitizeConfigForDisplay(value));
27
+ }
28
+ async function runConfigSet(context) {
29
+ const key = context.args.key;
30
+ const value = context.args.value;
31
+ if (!key || value === undefined) {
32
+ throw new DinoxError('Config key and value are required');
33
+ }
34
+ if (isFixedConfigKey(key)) {
35
+ throw new DinoxError(`${key} is fixed and cannot be changed`);
36
+ }
37
+ if (!isConfigKey(key)) {
38
+ throw new DinoxError(`Unknown config key: ${key}`);
39
+ }
40
+ if (key === 'sync.timeoutMs') {
41
+ const parsed = Number(value);
42
+ if (!Number.isFinite(parsed) || parsed <= 0) {
43
+ throw new DinoxError('sync.timeoutMs must be a positive integer');
43
44
  }
44
- if (key === 'sync.timeoutMs') {
45
- const parsed = Number(value);
46
- if (!Number.isFinite(parsed) || parsed <= 0) {
47
- throw new DinoxError('sync.timeoutMs must be a positive integer');
48
- }
49
- await setConfigValue(key, Math.trunc(parsed));
50
- return;
45
+ await setConfigValue(key, Math.trunc(parsed));
46
+ if (getRequestedStructuredOutputFormat()) {
47
+ printSuccess({ ok: true, key, value: Math.trunc(parsed) });
51
48
  }
52
- await setConfigValue(key, value);
53
- });
49
+ return;
50
+ }
51
+ await setConfigValue(key, value);
52
+ if (getRequestedStructuredOutputFormat()) {
53
+ printSuccess({ ok: true, key, value });
54
+ }
55
+ }
56
+ export const configCommandGroup = {
57
+ name: 'config',
58
+ description: 'Read and write Dinox configuration',
59
+ commands: [
60
+ {
61
+ name: 'get',
62
+ description: 'Read sanitized Dinox CLI configuration values',
63
+ arguments: [{ syntax: '[key]', description: 'Optional config key (dot path)' }],
64
+ schema: {
65
+ path: 'config.get',
66
+ risk: 'read',
67
+ auth: 'none',
68
+ globalOptions: FORMAT_OPTIONS,
69
+ output: {
70
+ type: 'structured',
71
+ summary: 'Requested config value or sanitized config object',
72
+ fields: ['sync.timeoutMs', 'auth'],
73
+ },
74
+ examples: ['dino config get --format json', 'dino config get sync.timeoutMs --format json'],
75
+ },
76
+ action: runConfigGet,
77
+ },
78
+ {
79
+ name: 'set',
80
+ description: 'Write configurable Dinox CLI settings',
81
+ arguments: [
82
+ { syntax: '<key>', description: 'Config key (dot path)' },
83
+ { syntax: '<value>', description: 'Config value' },
84
+ ],
85
+ schema: {
86
+ path: 'config.set',
87
+ risk: 'write',
88
+ auth: 'none',
89
+ globalOptions: FORMAT_OPTIONS,
90
+ output: {
91
+ type: 'structured',
92
+ summary: 'Config write acknowledgement',
93
+ fields: ['key', 'value'],
94
+ },
95
+ examples: ['dino config set sync.timeoutMs 20000 --format json'],
96
+ },
97
+ action: runConfigSet,
98
+ },
99
+ ],
100
+ };
101
+ export function registerConfigCommands(program) {
102
+ registerCommandGroup(program, configCommandGroup);
54
103
  }
@@ -0,0 +1,8 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandSchema } from '../schema/types.js';
3
+ import type { CommandDescriptor, CommandGroupDescriptor } from './types.js';
4
+ export declare function registerCommandGroup(program: Command, group: CommandGroupDescriptor): Command;
5
+ export declare function registerStandaloneCommand(program: Command, descriptor: CommandDescriptor): Command;
6
+ export declare function commandSchemaFromDescriptor(groupName: string, descriptor: CommandDescriptor): CommandSchema;
7
+ export declare function commandSchemasFromGroupDescriptor(group: CommandGroupDescriptor): CommandSchema[];
8
+ export declare function standaloneCommandSchemaFromDescriptor(descriptor: CommandDescriptor): CommandSchema;
@@ -0,0 +1,113 @@
1
+ import { toSchemaArgument, toSchemaOption } from './types.js';
2
+ function deriveArgumentKey(syntax) {
3
+ const normalized = syntax.trim().replace(/^[<[.]+|[>\].]+$/g, '');
4
+ return normalized.replace(/[-_]+([a-zA-Z0-9])/g, (_match, segment) => segment.toUpperCase());
5
+ }
6
+ function addArgument(command, argument) {
7
+ command.argument(argument.syntax, argument.description);
8
+ }
9
+ function addOption(command, option) {
10
+ if (option.required) {
11
+ if (option.parser && option.defaultValue !== undefined) {
12
+ command.requiredOption(option.flags, option.description, option.parser, option.defaultValue);
13
+ return;
14
+ }
15
+ if (option.parser) {
16
+ command.requiredOption(option.flags, option.description, option.parser);
17
+ return;
18
+ }
19
+ if (option.defaultValue !== undefined) {
20
+ command.requiredOption(option.flags, option.description, option.defaultValue);
21
+ return;
22
+ }
23
+ command.requiredOption(option.flags, option.description);
24
+ return;
25
+ }
26
+ if (option.parser && option.defaultValue !== undefined) {
27
+ command.option(option.flags, option.description, option.parser, option.defaultValue);
28
+ return;
29
+ }
30
+ if (option.parser) {
31
+ command.option(option.flags, option.description, option.parser);
32
+ return;
33
+ }
34
+ if (option.defaultValue !== undefined) {
35
+ command.option(option.flags, option.description, option.defaultValue);
36
+ return;
37
+ }
38
+ command.option(option.flags, option.description);
39
+ }
40
+ function buildExecutionContext(command, descriptors, values) {
41
+ const namedArgs = Object.fromEntries(descriptors.map((descriptor, index) => [deriveArgumentKey(descriptor.syntax), values[index]]));
42
+ return {
43
+ command,
44
+ globals: (command.optsWithGlobals?.() ?? {}),
45
+ options: (command.opts() ?? {}),
46
+ args: namedArgs,
47
+ };
48
+ }
49
+ export function registerCommandGroup(program, group) {
50
+ const parent = program.command(group.name).description(group.description);
51
+ for (const descriptor of group.commands) {
52
+ const subcommand = parent.command(descriptor.name).description(descriptor.description);
53
+ const argumentsList = descriptor.arguments ?? [];
54
+ for (const argument of argumentsList) {
55
+ addArgument(subcommand, argument);
56
+ }
57
+ for (const option of descriptor.options ?? []) {
58
+ addOption(subcommand, option);
59
+ }
60
+ subcommand.action(async (...actionArgs) => {
61
+ const command = actionArgs[actionArgs.length - 1];
62
+ const positionalValues = actionArgs.slice(0, argumentsList.length);
63
+ await descriptor.action(buildExecutionContext(command, argumentsList, positionalValues));
64
+ });
65
+ }
66
+ parent.action((_options, command) => {
67
+ command.outputHelp();
68
+ });
69
+ return parent;
70
+ }
71
+ function buildCommandInvocation(groupName, descriptor) {
72
+ return `dino ${groupName} ${descriptor.name}`.trimEnd();
73
+ }
74
+ function registerCommandDescriptor(command, descriptor) {
75
+ const argumentsList = descriptor.arguments ?? [];
76
+ for (const argument of argumentsList) {
77
+ addArgument(command, argument);
78
+ }
79
+ for (const option of descriptor.options ?? []) {
80
+ addOption(command, option);
81
+ }
82
+ command.action(async (...actionArgs) => {
83
+ const currentCommand = actionArgs[actionArgs.length - 1];
84
+ const positionalValues = actionArgs.slice(0, argumentsList.length);
85
+ await descriptor.action(buildExecutionContext(currentCommand, argumentsList, positionalValues));
86
+ });
87
+ return command;
88
+ }
89
+ export function registerStandaloneCommand(program, descriptor) {
90
+ const command = program.command(descriptor.name).description(descriptor.description);
91
+ return registerCommandDescriptor(command, descriptor);
92
+ }
93
+ export function commandSchemaFromDescriptor(groupName, descriptor) {
94
+ return {
95
+ ...descriptor.schema,
96
+ command: buildCommandInvocation(groupName, descriptor),
97
+ summary: descriptor.description,
98
+ ...(descriptor.arguments?.length ? { arguments: descriptor.arguments.map(toSchemaArgument) } : {}),
99
+ ...(descriptor.options?.length ? { options: descriptor.options.map(toSchemaOption) } : {}),
100
+ };
101
+ }
102
+ export function commandSchemasFromGroupDescriptor(group) {
103
+ return group.commands.map((descriptor) => commandSchemaFromDescriptor(group.name, descriptor));
104
+ }
105
+ export function standaloneCommandSchemaFromDescriptor(descriptor) {
106
+ return {
107
+ ...descriptor.schema,
108
+ command: `dino ${descriptor.name}`,
109
+ summary: descriptor.description,
110
+ ...(descriptor.arguments?.length ? { arguments: descriptor.arguments.map(toSchemaArgument) } : {}),
111
+ ...(descriptor.options?.length ? { options: descriptor.options.map(toSchemaOption) } : {}),
112
+ };
113
+ }
@@ -0,0 +1,37 @@
1
+ import type { Command } from 'commander';
2
+ import type { CommandSchema, CommandSchemaArgument, CommandSchemaOption } from '../schema/types.js';
3
+ export type CommandOptionDescriptor = {
4
+ flags: string;
5
+ description: string;
6
+ type?: 'string' | 'number' | 'boolean' | 'string[]';
7
+ required?: boolean;
8
+ parser?: (value: string, previous: unknown) => unknown;
9
+ defaultValue?: unknown;
10
+ enum?: string[];
11
+ };
12
+ export type CommandArgumentDescriptor = {
13
+ syntax: string;
14
+ description: string;
15
+ };
16
+ export type CommandExecutionContext = {
17
+ command: Command;
18
+ globals: Record<string, unknown>;
19
+ options: Record<string, unknown>;
20
+ args: Record<string, string | undefined>;
21
+ };
22
+ export type CommandDescriptorSchema = Omit<CommandSchema, 'command' | 'summary' | 'arguments' | 'options'>;
23
+ export type CommandDescriptor = {
24
+ name: string;
25
+ description: string;
26
+ arguments?: CommandArgumentDescriptor[];
27
+ options?: CommandOptionDescriptor[];
28
+ schema: CommandDescriptorSchema;
29
+ action: (context: CommandExecutionContext) => Promise<void> | void;
30
+ };
31
+ export type CommandGroupDescriptor = {
32
+ name: string;
33
+ description: string;
34
+ commands: CommandDescriptor[];
35
+ };
36
+ export declare function toSchemaArgument(argument: CommandArgumentDescriptor): CommandSchemaArgument;
37
+ export declare function toSchemaOption(option: CommandOptionDescriptor): CommandSchemaOption;
@@ -0,0 +1,17 @@
1
+ export function toSchemaArgument(argument) {
2
+ return {
3
+ name: argument.syntax,
4
+ required: argument.syntax.trim().startsWith('<'),
5
+ description: argument.description,
6
+ };
7
+ }
8
+ export function toSchemaOption(option) {
9
+ return {
10
+ name: option.flags,
11
+ description: option.description,
12
+ ...(option.type ? { type: option.type } : {}),
13
+ ...(option.required ? { required: true } : {}),
14
+ ...(option.defaultValue !== undefined ? { defaultValue: option.defaultValue } : {}),
15
+ ...(option.enum ? { enum: option.enum } : {}),
16
+ };
17
+ }
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../descriptor/types.js';
3
+ export declare const graphCommandGroup: CommandGroupDescriptor;
2
4
  export declare function registerGraphCommands(program: Command): void;