@dinoxx/dinox-cli 1.0.19 → 1.0.21

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 (88) hide show
  1. package/README.md +756 -472
  2. package/dist/auth/userInfo.d.ts +10 -1
  3. package/dist/auth/userInfo.js +66 -17
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +29 -14
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +45 -16
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/storage/repo.d.ts +1 -1
  55. package/dist/commands/storage/repo.js +20 -1
  56. package/dist/commands/sync.js +4 -2
  57. package/dist/commands/tags/content.d.ts +32 -0
  58. package/dist/commands/tags/content.js +243 -0
  59. package/dist/commands/tags/index.js +285 -49
  60. package/dist/commands/tags/model.d.ts +38 -0
  61. package/dist/commands/tags/model.js +100 -0
  62. package/dist/commands/tags/operations.d.ts +105 -0
  63. package/dist/commands/tags/operations.js +438 -0
  64. package/dist/commands/tags/repo.d.ts +5 -1
  65. package/dist/commands/tags/repo.js +142 -218
  66. package/dist/commands/tags/store.d.ts +28 -0
  67. package/dist/commands/tags/store.js +166 -0
  68. package/dist/commands/todo/index.js +30 -61
  69. package/dist/commands/writeDryRun.d.ts +25 -0
  70. package/dist/commands/writeDryRun.js +31 -1
  71. package/dist/config/paths.d.ts +2 -1
  72. package/dist/config/paths.js +11 -1
  73. package/dist/config/resolve.js +9 -7
  74. package/dist/daemon/index.d.ts +2 -0
  75. package/dist/daemon/index.js +176 -58
  76. package/dist/powersync/runtime.d.ts +2 -1
  77. package/dist/powersync/runtime.js +15 -3
  78. package/dist/powersync/schema/content.d.ts +2 -0
  79. package/dist/powersync/schema/content.js +2 -0
  80. package/dist/powersync/schema/index.d.ts +2 -0
  81. package/dist/skills/check.d.ts +1 -1
  82. package/dist/skills/check.js +27 -2
  83. package/dist/utils/authToken.d.ts +2 -0
  84. package/dist/utils/authToken.js +7 -2
  85. package/dist/utils/noteContentEngine.d.ts +333 -0
  86. package/dist/utils/noteContentEngine.js +740 -0
  87. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  88. package/package.json +4 -8
@@ -1,14 +1,11 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
2
  import { registerCommandGroup } from '../descriptor/register.js';
3
3
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
4
- import { loadConfig } from '../../config/store.js';
5
- import { resolveConfig } from '../../config/resolve.js';
6
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
7
4
  import { readStringOrFile } from '../../utils/argValue.js';
8
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
9
6
  import { DinoxError } from '../../utils/errors.js';
10
- import { printHumanReadableLine, printSuccess } from '../../utils/output.js';
11
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
7
+ import { printSuccess } from '../../utils/output.js';
8
+ import { commandStale, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
12
9
  import { buildTodoAppendDryRunPreview, buildTodoCreateDryRunPreview, buildTodoUpdateDryRunPreview, } from './dryRun.js';
13
10
  import { appendTodoTasks, createTodoNote, normalizeTaskTexts, searchTodoTasks, updateTodoTaskStatus, } from './repo.js';
14
11
  const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
@@ -192,35 +189,14 @@ async function parseTaskInput(args) {
192
189
  }
193
190
  return normalizeTaskTexts(merged);
194
191
  }
195
- async function syncBeforeTodoCommand(db, offline, timeoutMs) {
196
- if (offline) {
197
- return;
198
- }
199
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
200
- }
201
192
  async function withTodoDatabase(globals, run) {
202
- const rawConfig = await loadConfig();
203
- const config = resolveConfig(rawConfig);
204
- const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
205
- const offline = Boolean(globals.offline);
206
- const { db } = await connectPowerSync({ config, offline, timeoutMs });
207
- try {
208
- await run({
209
- db,
210
- rawConfig,
211
- config,
212
- offline,
213
- timeoutMs,
214
- });
215
- }
216
- finally {
217
- await db.close().catch(() => undefined);
218
- }
193
+ await withCommandRuntime(globals, run);
219
194
  }
220
195
  async function runTodoSearch({ args, globals, options }) {
221
196
  const searchOptions = options;
222
- await withTodoDatabase(globals, async ({ db, offline, timeoutMs }) => {
223
- await syncBeforeTodoCommand(db, offline, timeoutMs);
197
+ await withTodoDatabase(globals, async (runtime) => {
198
+ const { db } = runtime;
199
+ await syncBeforeCommand(runtime);
224
200
  const keyword = args.query?.trim() ?? '';
225
201
  const taskStatus = parseTaskStatus(searchOptions.status);
226
202
  const taskTagFilter = typeof searchOptions.tags === 'string'
@@ -241,13 +217,14 @@ async function runTodoSearch({ args, globals, options }) {
241
217
  limit,
242
218
  scanLimit,
243
219
  });
244
- printSuccess(result);
220
+ printSuccess({ stale: commandStale(runtime), ...result });
245
221
  });
246
222
  }
247
223
  async function runTodoAppend({ args, globals, options }) {
248
224
  const appendOptions = options;
249
- await withTodoDatabase(globals, async ({ db, config, offline, timeoutMs }) => {
250
- await syncBeforeTodoCommand(db, offline, timeoutMs);
225
+ await withTodoDatabase(globals, async (runtime) => {
226
+ const { db, config, offline } = runtime;
227
+ await syncBeforeCommand(runtime);
251
228
  const taskTexts = await parseTaskInput({
252
229
  positionalTask: args.task,
253
230
  tasksOption: typeof appendOptions.tasks === 'string' ? appendOptions.tasks : undefined,
@@ -268,13 +245,16 @@ async function runTodoAppend({ args, globals, options }) {
268
245
  noteId: typeof appendOptions.noteId === 'string' ? appendOptions.noteId : undefined,
269
246
  taskTexts,
270
247
  });
271
- printSuccess({ ok: true, ...result });
248
+ printUploadDisabledWarning(runtime);
249
+ await waitForMutationUpload(runtime);
250
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
272
251
  });
273
252
  }
274
253
  async function runTodoCreate({ args, globals, options }) {
275
254
  const createOptions = options;
276
- await withTodoDatabase(globals, async ({ db, rawConfig, config, offline, timeoutMs }) => {
277
- await syncBeforeTodoCommand(db, offline, timeoutMs);
255
+ await withTodoDatabase(globals, async (runtime) => {
256
+ const { db, config, offline } = runtime;
257
+ await syncBeforeCommand(runtime);
278
258
  const taskTexts = await parseTaskInput({
279
259
  positionalTask: args.task,
280
260
  tasksOption: typeof createOptions.tasks === 'string' ? createOptions.tasks : undefined,
@@ -290,25 +270,22 @@ async function runTodoCreate({ args, globals, options }) {
290
270
  printDryRunPreview(preview);
291
271
  return;
292
272
  }
293
- const identity = await resolveAuthIdentity(rawConfig, config);
273
+ const identity = await resolveAuthIdentity(config);
294
274
  const result = await createTodoNote(db, {
295
275
  taskTexts,
296
276
  title: typeof createOptions.title === 'string' ? createOptions.title : undefined,
297
277
  userId: identity.userId,
298
278
  });
299
- if (!offline && !config.powersync.uploadBaseUrl) {
300
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
301
- }
302
- if (!offline) {
303
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
304
- }
305
- printSuccess({ ok: true, ...result });
279
+ printUploadDisabledWarning(runtime);
280
+ await waitForMutationUpload(runtime);
281
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
306
282
  });
307
283
  }
308
284
  async function runTodoUpdate({ args, globals, options }) {
309
285
  const updateOptions = options;
310
- await withTodoDatabase(globals, async ({ db, config, offline, timeoutMs }) => {
311
- await syncBeforeTodoCommand(db, offline, timeoutMs);
286
+ await withTodoDatabase(globals, async (runtime) => {
287
+ const { db, config, offline } = runtime;
288
+ await syncBeforeCommand(runtime);
312
289
  const taskId = args.taskId;
313
290
  if (!taskId) {
314
291
  throw new DinoxError('taskId is required');
@@ -329,7 +306,9 @@ async function runTodoUpdate({ args, globals, options }) {
329
306
  taskId,
330
307
  checked,
331
308
  });
332
- printSuccess({ ok: true, ...result });
309
+ printUploadDisabledWarning(runtime);
310
+ await waitForMutationUpload(runtime);
311
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
333
312
  });
334
313
  }
335
314
  const todoSearchCommand = {
@@ -360,17 +339,7 @@ const todoSearchCommand = {
360
339
  type: 'structured',
361
340
  summary: 'Todo search payload with metadata and task list',
362
341
  fields: ['tasks.task_id', 'tasks.task_key', 'tasks.note_id', 'tasks.note_title', 'tasks.status', 'tasks.depth', 'tasks.parent_id', 'tasks.tags'],
363
- metaFields: ['meta.query', 'meta.returned_count', 'meta.limit', 'meta.task_status', 'meta.task_tag_filter', 'meta.task_date_range', 'meta.scanned_note_count'],
364
- },
365
- mcp: {
366
- toolName: 'dinox_search_todos',
367
- description: 'Search todo tasks',
368
- properties: [
369
- { name: 'query', type: 'string', description: 'Task text query' },
370
- { name: 'status', type: 'string', description: 'Task status', enum: ['all', 'completed', 'uncompleted'] },
371
- { name: 'tags', type: 'string[]', description: 'Task tags' },
372
- { name: 'limit', type: 'number', description: 'Max results (default: 50)' },
373
- ],
342
+ metaFields: ['stale', 'meta.query', 'meta.returned_count', 'meta.limit', 'meta.task_status', 'meta.task_tag_filter', 'meta.task_date_range', 'meta.scanned_note_count'],
374
343
  },
375
344
  examples: ['dino todo search "报销" --status uncompleted --format json', 'dino todo search --days 7 --format json'],
376
345
  },
@@ -404,7 +373,7 @@ const todoAppendCommand = {
404
373
  output: {
405
374
  type: 'structured',
406
375
  summary: 'Append result with target note and appended task ids',
407
- fields: ['noteId', 'appendedCount', 'taskIds'],
376
+ fields: ['noteId', 'appendedCount', 'taskIds', 'stale'],
408
377
  },
409
378
  examples: ['dino todo append --note-id <id> --task "补交报销单" --format json'],
410
379
  },
@@ -438,7 +407,7 @@ const todoCreateCommand = {
438
407
  output: {
439
408
  type: 'structured',
440
409
  summary: 'Creation result with new note id and task ids',
441
- fields: ['noteId', 'taskIds', 'title'],
410
+ fields: ['noteId', 'taskIds', 'title', 'stale'],
442
411
  },
443
412
  examples: ['dino todo create --task "补交报销单" --task "整理发票" --format json'],
444
413
  },
@@ -469,7 +438,7 @@ const todoUpdateCommand = {
469
438
  output: {
470
439
  type: 'structured',
471
440
  summary: 'Mutation result with updated status and target note id',
472
- fields: ['taskId', 'checked', 'updated', 'noteId'],
441
+ fields: ['taskId', 'checked', 'updated', 'noteId', 'stale'],
473
442
  },
474
443
  examples: ['dino todo update <taskId> --status completed --format json'],
475
444
  },
@@ -8,15 +8,40 @@ export declare function buildTagAddDryRunPreview(args: {
8
8
  createdPaths: string[];
9
9
  restoredPaths: string[];
10
10
  }): DryRunPreview;
11
+ export declare function buildTagMutationDryRunPreview(args: {
12
+ config: ResolvedDinoxConfig;
13
+ offline: boolean;
14
+ command: string;
15
+ action: 'rename_tag' | 'move_tag' | 'merge_tag' | 'cleanup_tags';
16
+ summary: string;
17
+ risk: 'write' | 'high-risk-write';
18
+ inputs: Record<string, unknown>;
19
+ changes?: Record<string, unknown> | Array<Record<string, unknown>>;
20
+ targets?: Array<Record<string, unknown>>;
21
+ }): DryRunPreview;
11
22
  export declare function buildBoxAddDryRunPreview(args: {
12
23
  config: ResolvedDinoxConfig;
13
24
  offline: boolean;
14
25
  name: string;
26
+ path: string;
15
27
  description: string;
16
28
  color: string;
17
29
  restored: boolean;
30
+ createdPaths: string[];
31
+ restoredPaths: string[];
18
32
  existingId?: string;
19
33
  }): DryRunPreview;
34
+ export declare function buildBoxMutationDryRunPreview(args: {
35
+ config: ResolvedDinoxConfig;
36
+ offline: boolean;
37
+ command: string;
38
+ action: 'rename_box' | 'move_box' | 'merge_box' | 'cleanup_boxes';
39
+ summary: string;
40
+ risk: 'write' | 'high-risk-write';
41
+ inputs: Record<string, unknown>;
42
+ changes?: Record<string, unknown> | Array<Record<string, unknown>>;
43
+ targets?: Array<Record<string, unknown>>;
44
+ }): DryRunPreview;
20
45
  export declare function buildPromptAddDryRunPreview(args: {
21
46
  config: ResolvedDinoxConfig;
22
47
  offline: boolean;
@@ -28,28 +28,58 @@ export function buildTagAddDryRunPreview(args) {
28
28
  },
29
29
  };
30
30
  }
31
+ export function buildTagMutationDryRunPreview(args) {
32
+ return {
33
+ dry_run: true,
34
+ command: args.command,
35
+ action: args.action,
36
+ risk: args.risk,
37
+ summary: args.summary,
38
+ warnings: buildExecutionWarnings(args.config, args.offline),
39
+ inputs: args.inputs,
40
+ ...(args.targets ? { targets: args.targets } : {}),
41
+ ...(args.changes ? { changes: args.changes } : {}),
42
+ };
43
+ }
31
44
  export function buildBoxAddDryRunPreview(args) {
32
45
  return {
33
46
  dry_run: true,
34
47
  command: 'dino box add',
35
48
  action: 'add_box',
36
49
  risk: 'write',
37
- summary: `${args.restored ? 'Restore' : 'Create'} zettel box "${args.name}".`,
50
+ summary: `${args.restored ? 'Restore' : 'Create'} zettel box "${args.path}".`,
38
51
  warnings: buildExecutionWarnings(args.config, args.offline),
39
52
  inputs: {
40
53
  name: args.name,
54
+ path: args.path,
41
55
  ...(args.description ? { description: args.description } : {}),
42
56
  ...(args.color ? { color: args.color } : {}),
43
57
  },
44
58
  changes: {
45
59
  name: args.name,
60
+ path: args.path,
46
61
  description: args.description,
47
62
  color: args.color,
48
63
  restored: args.restored,
64
+ created_paths: args.createdPaths,
65
+ restored_paths: args.restoredPaths,
49
66
  ...(args.existingId ? { existing_id: args.existingId } : {}),
50
67
  },
51
68
  };
52
69
  }
70
+ export function buildBoxMutationDryRunPreview(args) {
71
+ return {
72
+ dry_run: true,
73
+ command: args.command,
74
+ action: args.action,
75
+ risk: args.risk,
76
+ summary: args.summary,
77
+ warnings: buildExecutionWarnings(args.config, args.offline),
78
+ inputs: args.inputs,
79
+ ...(args.targets ? { targets: args.targets } : {}),
80
+ ...(args.changes ? { changes: args.changes } : {}),
81
+ };
82
+ }
53
83
  export function buildPromptAddDryRunPreview(args) {
54
84
  return {
55
85
  dry_run: true,
@@ -1,4 +1,5 @@
1
1
  export declare function getConfigDir(): string;
2
2
  export declare function getConfigFilePath(): string;
3
3
  export declare function getDataDir(): string;
4
- export declare function getPowerSyncDbPath(): string;
4
+ export declare function getLegacyPowerSyncDbPath(): string;
5
+ export declare function getPowerSyncDbPath(userId?: string | null): string;
@@ -1,6 +1,9 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  const POWERSYNC_DB_FILENAME = 'dinox-cli.sqlite';
4
+ function normalizeUserPathSegment(userId) {
5
+ return encodeURIComponent(userId.trim());
6
+ }
4
7
  function resolveConfigHome() {
5
8
  if (process.env.DINOX_CONFIG_DIR) {
6
9
  return process.env.DINOX_CONFIG_DIR;
@@ -35,6 +38,13 @@ export function getConfigFilePath() {
35
38
  export function getDataDir() {
36
39
  return path.join(resolveDataHome(), 'dinox');
37
40
  }
38
- export function getPowerSyncDbPath() {
41
+ export function getLegacyPowerSyncDbPath() {
39
42
  return path.join(getDataDir(), POWERSYNC_DB_FILENAME);
40
43
  }
44
+ export function getPowerSyncDbPath(userId) {
45
+ const normalizedUserId = typeof userId === 'string' ? userId.trim() : '';
46
+ if (!normalizedUserId) {
47
+ return getLegacyPowerSyncDbPath();
48
+ }
49
+ return path.join(getDataDir(), 'users', normalizeUserPathSegment(normalizedUserId), POWERSYNC_DB_FILENAME);
50
+ }
@@ -1,5 +1,5 @@
1
1
  import { POWERSYNC_DEFAULT_ENDPOINT, POWERSYNC_DEFAULT_TOKEN_ENDPOINT, POWERSYNC_DEFAULT_UPLOAD_BASE_URL, } from './serviceEndpoints.js';
2
- import { normalizeAuthToken } from '../utils/authToken.js';
2
+ import { normalizeAuthToken, readEnvAuthToken } from '../utils/authToken.js';
3
3
  const DEFAULT_UPLOAD_V4_PATH = '/powersync/v4/uploadData';
4
4
  const DEFAULT_UPLOAD_V2_PATH = '/powersync/v2/uploadData';
5
5
  const DEFAULT_SYNC_TIMEOUT_MS = 300_000;
@@ -30,18 +30,20 @@ export function resolveConfig(raw) {
30
30
  const fileUploadV4Path = nonEmptyString(raw.powersync?.uploadV4Path);
31
31
  const fileUploadV2Path = nonEmptyString(raw.powersync?.uploadV2Path);
32
32
  const fileTimeoutMs = parsePositiveInt(raw.sync?.timeoutMs);
33
- const envAuth = normalizeAuthToken(process.env.DINOX_AUTHORIZATION);
33
+ const envAuth = readEnvAuthToken();
34
34
  const envTokenEndpoint = nonEmptyString(process.env.DINOX_POWERSYNC_TOKEN_ENDPOINT) ?? nonEmptyString(process.env.POWERSYNC_TOKEN_ENDPOINT);
35
35
  const envUploadV4Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V4_PATH);
36
36
  const envUploadV2Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V2_PATH);
37
37
  const envTimeoutMs = parsePositiveInt(process.env.DINOX_SYNC_TIMEOUT_MS);
38
+ const effectiveAuth = envAuth ?? fileAuth;
39
+ const canReusePersistedIdentity = !envAuth || envAuth === fileAuth;
38
40
  return {
39
41
  auth: {
40
- authorization: envAuth ?? fileAuth,
41
- userId: fileUserId,
42
- username: fileUsername,
43
- email: fileEmail,
44
- userInfoSyncedAt: fileUserInfoSyncedAt,
42
+ authorization: effectiveAuth,
43
+ userId: canReusePersistedIdentity ? fileUserId : undefined,
44
+ username: canReusePersistedIdentity ? fileUsername : undefined,
45
+ email: canReusePersistedIdentity ? fileEmail : undefined,
46
+ userInfoSyncedAt: canReusePersistedIdentity ? fileUserInfoSyncedAt : undefined,
45
47
  },
46
48
  powersync: {
47
49
  endpoint: POWERSYNC_DEFAULT_ENDPOINT,
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandGroupDescriptor } from '../commands/descriptor/types.js';
3
+ export declare const daemonCommandGroup: CommandGroupDescriptor;
2
4
  export declare function registerDaemonCommands(program: Command): void;
@@ -5,17 +5,19 @@ import { readPid, isProcessRunning, removePid } from './pid.js';
5
5
  import { isDaemonRunning } from './client.js';
6
6
  import { getPackageVersion } from '../utils/version.js';
7
7
  import { DinoxError } from '../utils/errors.js';
8
- import { printHumanReadableError, printHumanReadableLine } from '../utils/output.js';
8
+ import { getRequestedStructuredOutputFormat, printHumanReadableError, printHumanReadableLine, printSuccess, } from '../utils/output.js';
9
+ import { registerCommandGroup } from '../commands/descriptor/register.js';
10
+ import { FORMAT_OPTIONS } from '../commands/schema/globalOptions.js';
9
11
  const DAEMON_STOP_POLL_INTERVAL_MS = 100;
10
12
  const DAEMON_STOP_TIMEOUT_MS = 5_000;
11
13
  function parsePort(value) {
12
14
  if (value === undefined || value === null || value === '') {
13
15
  return DEFAULT_DAEMON_PORT;
14
16
  }
15
- if (typeof value !== 'string') {
17
+ if (typeof value !== 'string' && typeof value !== 'number') {
16
18
  throw new DinoxError('--port must be a valid port number');
17
19
  }
18
- const trimmed = value.trim();
20
+ const trimmed = String(value).trim();
19
21
  if (!/^\d+$/.test(trimmed)) {
20
22
  throw new DinoxError('--port must be a valid port number');
21
23
  }
@@ -35,10 +37,9 @@ async function waitForProcessExit(pid, timeoutMs) {
35
37
  }
36
38
  return !isProcessRunning(pid);
37
39
  }
38
- async function startDaemon(port, detached = true) {
40
+ async function startDaemon(port, detached = true, options = {}) {
39
41
  if (await isDaemonRunning()) {
40
- printHumanReadableLine('Daemon is already running');
41
- return;
42
+ return { running: true, port, detached, alreadyRunning: true };
42
43
  }
43
44
  if (detached) {
44
45
  const args = ['daemon', 'start', '--port', String(port), '--no-detach'];
@@ -49,21 +50,27 @@ async function startDaemon(port, detached = true) {
49
50
  child.unref();
50
51
  await new Promise((resolve) => setTimeout(resolve, 1000));
51
52
  if (await isDaemonRunning()) {
52
- printHumanReadableLine(`Daemon started on port ${port}`);
53
+ return { running: true, port, detached, alreadyRunning: false };
53
54
  }
54
55
  else {
55
- printHumanReadableError('Failed to start daemon');
56
+ return { running: false, port, detached, alreadyRunning: false };
56
57
  }
57
58
  }
58
59
  else {
59
- printHumanReadableLine(`Starting daemon on port ${port}...`);
60
+ if (!options.quiet) {
61
+ printHumanReadableLine(`Starting daemon on port ${port}...`);
62
+ }
60
63
  const server = new DaemonServer();
61
64
  await server.initialize();
62
65
  await server.start(port);
63
- printHumanReadableLine(`Daemon running on port ${port} (PID: ${process.pid})`);
66
+ if (!options.quiet) {
67
+ printHumanReadableLine(`Daemon running on port ${port} (PID: ${process.pid})`);
68
+ }
64
69
  process.on('SIGINT', async () => {
65
- printHumanReadableLine('');
66
- printHumanReadableLine('Stopping daemon...');
70
+ if (!options.quiet) {
71
+ printHumanReadableLine('');
72
+ printHumanReadableLine('Stopping daemon...');
73
+ }
67
74
  await server.stop();
68
75
  process.exit(0);
69
76
  });
@@ -72,17 +79,16 @@ async function startDaemon(port, detached = true) {
72
79
  process.exit(0);
73
80
  });
74
81
  }
82
+ return { running: true, port, detached, alreadyRunning: false };
75
83
  }
76
84
  async function stopDaemon() {
77
85
  const pidInfo = readPid();
78
86
  if (!pidInfo) {
79
- printHumanReadableLine('Daemon is not running');
80
- return;
87
+ return { stopped: false, wasRunning: false, stalePidFile: false };
81
88
  }
82
89
  if (!isProcessRunning(pidInfo.pid)) {
83
- printHumanReadableLine('Daemon is not running (stale PID file)');
84
90
  removePid();
85
- return;
91
+ return { stopped: false, wasRunning: false, stalePidFile: true };
86
92
  }
87
93
  try {
88
94
  process.kill(pidInfo.pid, 'SIGTERM');
@@ -95,59 +101,171 @@ async function stopDaemon() {
95
101
  throw new DinoxError(`Daemon did not stop within ${DAEMON_STOP_TIMEOUT_MS}ms. Try stopping it manually and rerun \`dino daemon status\`.`);
96
102
  }
97
103
  removePid();
98
- printHumanReadableLine('Daemon stopped');
104
+ return { stopped: true, wasRunning: true, stalePidFile: false };
99
105
  }
100
106
  async function daemonStatus() {
101
107
  const pidInfo = readPid();
102
108
  if (!pidInfo) {
103
- printHumanReadableLine('Status: Not running');
104
- return;
109
+ return { running: false, pid: null, port: null, version: null, stalePidFile: false };
105
110
  }
106
111
  const running = await isDaemonRunning();
107
112
  if (running) {
108
- printHumanReadableLine('Status: Running');
109
- printHumanReadableLine(`PID: ${pidInfo.pid}`);
110
- printHumanReadableLine(`Port: ${pidInfo.port}`);
111
- printHumanReadableLine(`Version: ${getPackageVersion()}`);
113
+ return { running: true, pid: pidInfo.pid, port: pidInfo.port, version: getPackageVersion(), stalePidFile: false };
112
114
  }
113
115
  else {
114
- printHumanReadableLine('Status: Not running (stale PID file)');
115
116
  removePid();
117
+ return { running: false, pid: null, port: null, version: null, stalePidFile: true };
118
+ }
119
+ }
120
+ function printDaemonStartResult(result) {
121
+ if (result.alreadyRunning) {
122
+ printHumanReadableLine('Daemon is already running');
123
+ return;
124
+ }
125
+ if (result.running) {
126
+ printHumanReadableLine(`Daemon started on port ${result.port}`);
127
+ return;
128
+ }
129
+ printHumanReadableError('Failed to start daemon');
130
+ }
131
+ function printDaemonStopResult(result) {
132
+ if (result.stopped) {
133
+ printHumanReadableLine('Daemon stopped');
134
+ return;
135
+ }
136
+ if (result.stalePidFile) {
137
+ printHumanReadableLine('Daemon is not running (stale PID file)');
138
+ return;
139
+ }
140
+ printHumanReadableLine('Daemon is not running');
141
+ }
142
+ function printDaemonStatusResult(result) {
143
+ if (!result.running) {
144
+ printHumanReadableLine(result.stalePidFile ? 'Status: Not running (stale PID file)' : 'Status: Not running');
145
+ return;
146
+ }
147
+ printHumanReadableLine('Status: Running');
148
+ printHumanReadableLine(`PID: ${result.pid}`);
149
+ printHumanReadableLine(`Port: ${result.port}`);
150
+ printHumanReadableLine(`Version: ${result.version}`);
151
+ }
152
+ async function runDaemonStart(context) {
153
+ const port = parsePort(context.options.port);
154
+ const structured = Boolean(getRequestedStructuredOutputFormat());
155
+ const result = await startDaemon(port, context.options.detach !== false, { quiet: structured });
156
+ if (structured) {
157
+ printSuccess(result);
158
+ return;
159
+ }
160
+ printDaemonStartResult(result);
161
+ }
162
+ async function runDaemonStop() {
163
+ const result = await stopDaemon();
164
+ if (getRequestedStructuredOutputFormat()) {
165
+ printSuccess(result);
166
+ return;
167
+ }
168
+ printDaemonStopResult(result);
169
+ }
170
+ async function runDaemonStatus() {
171
+ const result = await daemonStatus();
172
+ if (getRequestedStructuredOutputFormat()) {
173
+ printSuccess(result);
174
+ return;
116
175
  }
176
+ printDaemonStatusResult(result);
117
177
  }
178
+ async function runDaemonRestart(context) {
179
+ const structured = Boolean(getRequestedStructuredOutputFormat());
180
+ const stopResult = await stopDaemon();
181
+ await new Promise((resolve) => setTimeout(resolve, 1000));
182
+ const startResult = await startDaemon(parsePort(context.options.port), true, { quiet: structured });
183
+ if (structured) {
184
+ printSuccess({ stopped: stopResult.stopped, ...startResult });
185
+ return;
186
+ }
187
+ printDaemonStopResult(stopResult);
188
+ printDaemonStartResult(startResult);
189
+ }
190
+ export const daemonCommandGroup = {
191
+ name: 'daemon',
192
+ description: 'Manage daemon process',
193
+ commands: [
194
+ {
195
+ name: 'start',
196
+ description: 'Start daemon process',
197
+ options: [
198
+ { flags: '--port <number>', description: 'Daemon port', type: 'number', defaultValue: DEFAULT_DAEMON_PORT },
199
+ { flags: '--no-detach', description: 'Run in foreground for debugging', type: 'boolean' },
200
+ ],
201
+ schema: {
202
+ path: 'daemon.start',
203
+ risk: 'write',
204
+ auth: 'none',
205
+ globalOptions: FORMAT_OPTIONS,
206
+ output: {
207
+ type: 'structured',
208
+ summary: 'Daemon start acknowledgement',
209
+ fields: ['running', 'port', 'detached', 'alreadyRunning'],
210
+ },
211
+ examples: ['dino daemon start', 'dino daemon start --port 17321', 'dino daemon start --no-detach'],
212
+ },
213
+ action: runDaemonStart,
214
+ },
215
+ {
216
+ name: 'stop',
217
+ description: 'Stop daemon process',
218
+ schema: {
219
+ path: 'daemon.stop',
220
+ risk: 'write',
221
+ auth: 'none',
222
+ globalOptions: FORMAT_OPTIONS,
223
+ output: {
224
+ type: 'structured',
225
+ summary: 'Daemon stop acknowledgement',
226
+ fields: ['stopped', 'wasRunning', 'stalePidFile'],
227
+ },
228
+ examples: ['dino daemon stop'],
229
+ },
230
+ action: runDaemonStop,
231
+ },
232
+ {
233
+ name: 'status',
234
+ description: 'Show daemon status',
235
+ schema: {
236
+ path: 'daemon.status',
237
+ risk: 'read',
238
+ auth: 'none',
239
+ globalOptions: FORMAT_OPTIONS,
240
+ output: {
241
+ type: 'structured',
242
+ summary: 'Daemon process status',
243
+ fields: ['running', 'pid', 'port', 'version', 'stalePidFile'],
244
+ },
245
+ examples: ['dino daemon status'],
246
+ },
247
+ action: runDaemonStatus,
248
+ },
249
+ {
250
+ name: 'restart',
251
+ description: 'Restart daemon process',
252
+ options: [{ flags: '--port <number>', description: 'Daemon port', type: 'number', defaultValue: DEFAULT_DAEMON_PORT }],
253
+ schema: {
254
+ path: 'daemon.restart',
255
+ risk: 'write',
256
+ auth: 'none',
257
+ globalOptions: FORMAT_OPTIONS,
258
+ output: {
259
+ type: 'structured',
260
+ summary: 'Daemon restart acknowledgement',
261
+ fields: ['stopped', 'running', 'port', 'detached', 'alreadyRunning'],
262
+ },
263
+ examples: ['dino daemon restart', 'dino daemon restart --port 17321'],
264
+ },
265
+ action: runDaemonRestart,
266
+ },
267
+ ],
268
+ };
118
269
  export function registerDaemonCommands(program) {
119
- const daemon = program.command('daemon').description('Manage daemon process');
120
- daemon
121
- .command('start')
122
- .description('Start daemon process')
123
- .option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
124
- .option('--no-detach', 'run in foreground (for debugging)')
125
- .action(async (options) => {
126
- const port = parsePort(options.port);
127
- await startDaemon(port, options.detach !== false);
128
- });
129
- daemon
130
- .command('stop')
131
- .description('Stop daemon process')
132
- .action(async () => {
133
- await stopDaemon();
134
- });
135
- daemon
136
- .command('status')
137
- .description('Show daemon status')
138
- .action(async () => {
139
- await daemonStatus();
140
- });
141
- daemon
142
- .command('restart')
143
- .description('Restart daemon process')
144
- .option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
145
- .action(async (options) => {
146
- await stopDaemon();
147
- await new Promise((resolve) => setTimeout(resolve, 1000));
148
- await startDaemon(parsePort(options.port));
149
- });
150
- daemon.action((_options, command) => {
151
- command.outputHelp();
152
- });
270
+ registerCommandGroup(program, daemonCommandGroup);
153
271
  }