@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
@@ -5,6 +5,8 @@ export type AuthIdentity = {
5
5
  email: string | null;
6
6
  syncedAt: string;
7
7
  };
8
+ export declare function buildPendingAuthConfig(rawConfig: DinoxConfig, authorization: string): DinoxConfig;
9
+ export declare function mergeAuthIdentityIntoConfig(rawConfig: DinoxConfig, authorization: string, identity: AuthIdentity): DinoxConfig;
8
10
  export declare function fetchUserInfoWithAuthorization(input: {
9
11
  authorization: string;
10
12
  config: ResolvedDinoxConfig;
@@ -12,4 +14,11 @@ export declare function fetchUserInfoWithAuthorization(input: {
12
14
  signal?: AbortSignal;
13
15
  }): Promise<AuthIdentity>;
14
16
  export declare function refreshAndPersistAuthIdentity(rawConfig: DinoxConfig, resolvedConfig: ResolvedDinoxConfig): Promise<AuthIdentity>;
15
- export declare function resolveAuthIdentity(rawConfig: DinoxConfig, resolvedConfig: ResolvedDinoxConfig): Promise<AuthIdentity>;
17
+ export declare function hydrateResolvedAuthIdentity(resolvedConfig: ResolvedDinoxConfig, options?: {
18
+ timeoutMs?: number;
19
+ }): Promise<{
20
+ config: ResolvedDinoxConfig;
21
+ identity: AuthIdentity | null;
22
+ hydrated: boolean;
23
+ }>;
24
+ export declare function resolveAuthIdentity(resolvedConfig: ResolvedDinoxConfig): Promise<AuthIdentity>;
@@ -3,6 +3,33 @@ import { DinoxError } from '../utils/errors.js';
3
3
  import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
4
4
  import { toAuthorizationHeaderValue } from '../utils/authToken.js';
5
5
  const AUTH_CODE_OK = '000000';
6
+ export function buildPendingAuthConfig(rawConfig, authorization) {
7
+ return {
8
+ ...rawConfig,
9
+ auth: {
10
+ ...(rawConfig.auth ?? {}),
11
+ authorization,
12
+ userId: undefined,
13
+ username: undefined,
14
+ email: undefined,
15
+ userInfoSyncedAt: undefined,
16
+ },
17
+ };
18
+ }
19
+ export function mergeAuthIdentityIntoConfig(rawConfig, authorization, identity) {
20
+ const pendingConfig = buildPendingAuthConfig(rawConfig, authorization);
21
+ return {
22
+ ...pendingConfig,
23
+ auth: {
24
+ ...(pendingConfig.auth ?? {}),
25
+ authorization,
26
+ userId: identity.userId,
27
+ username: identity.username,
28
+ email: identity.email,
29
+ userInfoSyncedAt: identity.syncedAt,
30
+ },
31
+ };
32
+ }
6
33
  function normalizeOptionalString(value) {
7
34
  if (typeof value !== 'string') {
8
35
  return undefined;
@@ -105,30 +132,52 @@ export async function refreshAndPersistAuthIdentity(rawConfig, resolvedConfig) {
105
132
  authorization,
106
133
  config: resolvedConfig,
107
134
  });
108
- const nextConfig = {
109
- ...rawConfig,
110
- auth: {
111
- ...(rawConfig.auth ?? {}),
112
- authorization: rawConfig.auth?.authorization ?? authorization,
113
- userId: identity.userId,
114
- username: identity.username,
115
- email: identity.email,
116
- userInfoSyncedAt: identity.syncedAt,
117
- },
118
- };
135
+ const nextConfig = mergeAuthIdentityIntoConfig(rawConfig, authorization, identity);
119
136
  await saveConfig(nextConfig);
120
137
  return identity;
121
138
  }
122
- export async function resolveAuthIdentity(rawConfig, resolvedConfig) {
123
- const savedUserId = normalizeOptionalString(rawConfig.auth?.userId ?? resolvedConfig.auth.userId);
124
- const syncedAt = normalizeOptionalString(rawConfig.auth?.userInfoSyncedAt ?? resolvedConfig.auth.userInfoSyncedAt);
139
+ function authIdentityFromResolvedConfig(resolvedConfig) {
140
+ const savedUserId = normalizeOptionalString(resolvedConfig.auth.userId);
141
+ const syncedAt = normalizeOptionalString(resolvedConfig.auth.userInfoSyncedAt);
125
142
  if (savedUserId) {
126
143
  return {
127
144
  userId: savedUserId,
128
- username: normalizeOptionalNullableString(rawConfig.auth?.username ?? resolvedConfig.auth.username),
129
- email: normalizeOptionalNullableString(rawConfig.auth?.email ?? resolvedConfig.auth.email),
145
+ username: normalizeOptionalNullableString(resolvedConfig.auth.username),
146
+ email: normalizeOptionalNullableString(resolvedConfig.auth.email),
130
147
  syncedAt: syncedAt ?? new Date().toISOString(),
131
148
  };
132
149
  }
133
- throw new DinoxError('Missing persisted userId. Run `dino auth login` first.');
150
+ return null;
151
+ }
152
+ export async function hydrateResolvedAuthIdentity(resolvedConfig, options = {}) {
153
+ const existing = authIdentityFromResolvedConfig(resolvedConfig);
154
+ if (existing || !resolvedConfig.auth.authorization) {
155
+ return { config: resolvedConfig, identity: existing, hydrated: false };
156
+ }
157
+ const identity = await fetchUserInfoWithAuthorization({
158
+ authorization: resolvedConfig.auth.authorization,
159
+ config: resolvedConfig,
160
+ timeoutMs: options.timeoutMs,
161
+ });
162
+ return {
163
+ config: {
164
+ ...resolvedConfig,
165
+ auth: {
166
+ ...resolvedConfig.auth,
167
+ userId: identity.userId,
168
+ username: identity.username,
169
+ email: identity.email,
170
+ userInfoSyncedAt: identity.syncedAt,
171
+ },
172
+ },
173
+ identity,
174
+ hydrated: true,
175
+ };
176
+ }
177
+ export async function resolveAuthIdentity(resolvedConfig) {
178
+ const identity = authIdentityFromResolvedConfig(resolvedConfig);
179
+ if (identity) {
180
+ return identity;
181
+ }
182
+ throw new DinoxError('Missing resolved userId for the current authorization. Run `dino auth login` first, or set DINOX_TOKEN and run an online command so the CLI can resolve the token identity.');
134
183
  }
package/dist/cli.js CHANGED
@@ -5,7 +5,6 @@ import { registerConfigCommands } from './commands/config/index.js';
5
5
  import { registerDaemonCommands } from './daemon/index.js';
6
6
  import { registerGraphCommands } from './commands/graph/index.js';
7
7
  import { registerInfoCommand } from './commands/info/index.js';
8
- import { registerMCPCommands } from './commands/mcp/index.js';
9
8
  import { registerMetaCommands } from './commands/meta/index.js';
10
9
  import { registerNoteCommands } from './commands/notes/index.js';
11
10
  import { registerPromptCommands } from './commands/prompt/index.js';
@@ -44,7 +43,6 @@ export function createCli() {
44
43
  registerDaemonCommands(program);
45
44
  registerGraphCommands(program);
46
45
  registerInfoCommand(program);
47
- registerMCPCommands(program);
48
46
  registerMetaCommands(program);
49
47
  registerSyncCommand(program);
50
48
  registerUpdateCommand(program);
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
- import { refreshAndPersistAuthIdentity } from '../../auth/userInfo.js';
3
- import { getPowerSyncDbPath } from '../../config/paths.js';
2
+ import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, } from '../../auth/userInfo.js';
3
+ import { getLegacyPowerSyncDbPath, getPowerSyncDbPath } from '../../config/paths.js';
4
4
  import { loadConfig, saveConfig } from '../../config/store.js';
5
5
  import { resolveConfig } from '../../config/resolve.js';
6
6
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
@@ -8,7 +8,7 @@ import { connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js'
8
8
  import { runSingleSyncPass } from '../../powersync/syncPass.js';
9
9
  import { registerCommandGroup } from '../descriptor/register.js';
10
10
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
11
- import { normalizeAuthToken } from '../../utils/authToken.js';
11
+ import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
12
12
  import { DinoxError } from '../../utils/errors.js';
13
13
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
14
14
  import { redactAuthorization } from '../../utils/redact.js';
@@ -26,10 +26,13 @@ async function runAuthLogin(context) {
26
26
  if (!rawToken) {
27
27
  throw new DinoxError('Token is required');
28
28
  }
29
- const config = await loadConfig();
30
- config.auth = { ...(config.auth ?? {}), authorization: rawToken };
31
- await saveConfig(config);
32
- const resolved = resolveConfig(config);
29
+ const envToken = readEnvAuthToken();
30
+ if (envToken && envToken !== rawToken) {
31
+ throw new DinoxError('DINOX_TOKEN is set and differs from the login token. Unset DINOX_TOKEN before running `dino auth login`, or use the same token.');
32
+ }
33
+ const previousConfig = await loadConfig();
34
+ const pendingConfig = buildPendingAuthConfig(previousConfig, rawToken);
35
+ const resolved = resolveConfig(pendingConfig);
33
36
  const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? resolved.sync.timeoutMs;
34
37
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
35
38
  await fetchCredentialsWithAuthorization({
@@ -38,19 +41,26 @@ async function runAuthLogin(context) {
38
41
  credentialsUrl: resolved.powersync.tokenEndpoint,
39
42
  timeoutMs,
40
43
  });
41
- const identity = await refreshAndPersistAuthIdentity(config, resolved);
42
- const { db, dbPath, stale } = await connectPowerSync({
44
+ const identity = await fetchUserInfoWithAuthorization({
45
+ authorization: rawToken,
43
46
  config: resolved,
47
+ timeoutMs,
48
+ });
49
+ const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
50
+ const verifiedResolved = resolveConfig(verifiedConfig);
51
+ const { db, dbPath, stale } = await connectPowerSync({
52
+ config: verifiedResolved,
44
53
  offline: false,
45
54
  timeoutMs,
46
55
  });
47
56
  try {
48
57
  await runSingleSyncPass(db, timeoutMs);
58
+ await saveConfig(verifiedConfig);
49
59
  const status = getStatusSnapshot(db);
50
60
  const result = {
51
61
  ok: true,
52
62
  verified: true,
53
- endpoint: resolved.powersync.endpoint,
63
+ endpoint: verifiedResolved.powersync.endpoint,
54
64
  dbPath,
55
65
  stale,
56
66
  lastSyncedAt: status.lastSyncedAt,
@@ -78,6 +88,7 @@ async function runAuthLogin(context) {
78
88
  async function runAuthLogout(context) {
79
89
  const options = context.options;
80
90
  const config = await loadConfig();
91
+ const currentUserId = config.auth?.userId;
81
92
  if (config.auth) {
82
93
  config.auth.authorization = undefined;
83
94
  config.auth.userId = undefined;
@@ -87,7 +98,8 @@ async function runAuthLogout(context) {
87
98
  }
88
99
  await saveConfig(config);
89
100
  if (options.clearLocalDb) {
90
- await removeSqliteArtifacts(getPowerSyncDbPath());
101
+ await removeSqliteArtifacts(getPowerSyncDbPath(currentUserId));
102
+ await removeSqliteArtifacts(getLegacyPowerSyncDbPath());
91
103
  }
92
104
  if (getRequestedStructuredOutputFormat()) {
93
105
  printSuccess({
@@ -99,11 +111,14 @@ async function runAuthLogout(context) {
99
111
  }
100
112
  async function runAuthStatus(context) {
101
113
  const config = await loadConfig();
102
- const resolved = resolveConfig(config);
114
+ const baseResolved = resolveConfig(config);
103
115
  const offline = Boolean(context.globals.offline);
104
116
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
105
- const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? resolved.sync.timeoutMs;
106
- const dbPath = getPowerSyncDbPath();
117
+ const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseResolved.sync.timeoutMs;
118
+ const { config: resolved } = offline
119
+ ? { config: baseResolved }
120
+ : await hydrateResolvedAuthIdentity(baseResolved, { timeoutMs });
121
+ const dbPath = getPowerSyncDbPath(resolved.auth.userId);
107
122
  const base = {
108
123
  loggedIn: Boolean(resolved.auth.authorization),
109
124
  authorization: redactAuthorization(resolved.auth.authorization),
@@ -1,41 +1,39 @@
1
1
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
2
- import { loadConfig } from '../../config/store.js';
3
- import { resolveConfig } from '../../config/resolve.js';
4
- import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
5
- import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
6
2
  import { registerCommandGroup } from '../descriptor/register.js';
7
3
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
8
- import { buildBoxAddDryRunPreview } from '../writeDryRun.js';
4
+ import { buildBoxAddDryRunPreview, buildBoxMutationDryRunPreview } from '../writeDryRun.js';
9
5
  import { printDryRunPreview } from '../../utils/dryRun.js';
10
6
  import { DinoxError } from '../../utils/errors.js';
11
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
12
- import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
8
+ import { commandStale, printStaleCacheWarning, printUploadDisabledWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
13
9
  import { addBox, inspectAddBox, listBoxes } from './repo.js';
10
+ import { getBoxStats, getBoxTree, inspectBoxCleanup, mergeBox, moveBox, renameBox, } from './operations.js';
14
11
  const BOX_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
15
- async function syncBeforeBoxCommand(db, offline, timeoutMs) {
16
- if (offline) {
17
- return;
18
- }
19
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 10_000));
20
- await syncNoteTokenIndex(db);
12
+ async function runBoxList(context) {
13
+ await withCommandRuntime(context, async (runtime) => {
14
+ const identity = await resolveAuthIdentity(runtime.config);
15
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
16
+ printSuccess(await listBoxes(runtime.db, { userId: identity.userId }));
17
+ });
21
18
  }
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 });
28
- try {
29
- await run({ db, config, rawConfig, offline, timeoutMs, stale });
30
- }
31
- finally {
32
- await db.close().catch(() => undefined);
33
- }
19
+ async function runBoxTree(context) {
20
+ await withCommandRuntime(context, async (runtime) => {
21
+ const identity = await resolveAuthIdentity(runtime.config);
22
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
23
+ printSuccess({
24
+ stale: commandStale(runtime),
25
+ tree: await getBoxTree(runtime.db, { userId: identity.userId }),
26
+ });
27
+ });
34
28
  }
35
- async function runBoxList(context) {
36
- await withBoxDb(context, async ({ db, offline, timeoutMs }) => {
37
- await syncBeforeBoxCommand(db, offline, timeoutMs);
38
- printSuccess(await listBoxes(db));
29
+ async function runBoxStats(context) {
30
+ await withCommandRuntime(context, async (runtime) => {
31
+ const identity = await resolveAuthIdentity(runtime.config);
32
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
33
+ printSuccess({
34
+ stale: commandStale(runtime),
35
+ ...(await getBoxStats(runtime.db, { userId: identity.userId })),
36
+ });
39
37
  });
40
38
  }
41
39
  async function runBoxAdd(context) {
@@ -43,22 +41,22 @@ async function runBoxAdd(context) {
43
41
  const optionName = typeof context.options.name === 'string' ? context.options.name : undefined;
44
42
  const name = (nameArg ?? optionName ?? '').trim();
45
43
  if (!name) {
46
- throw new DinoxError('Box name is required. Use `dino box add <name>` or `dino box add --name <name>`.');
44
+ throw new DinoxError('Box path is required. Use `dino box add <path>` or `dino box add --name <path>`.');
47
45
  }
48
46
  if (nameArg && optionName && nameArg.trim() !== optionName.trim()) {
49
- throw new DinoxError('Conflicting box name values from positional argument and --name option.');
47
+ throw new DinoxError('Conflicting box path values from positional argument and --name option.');
50
48
  }
51
49
  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);
50
+ await withCommandRuntime(context, async (runtime) => {
51
+ const identity = await resolveAuthIdentity(runtime.config);
52
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
55
53
  const description = typeof context.options.description === 'string' ? context.options.description : undefined;
56
54
  const color = typeof context.options.color === 'string' ? context.options.color : undefined;
57
55
  if (Boolean(context.options.dryRun)) {
58
56
  printDryRunPreview(buildBoxAddDryRunPreview({
59
- config,
60
- offline,
61
- ...(await inspectAddBox(db, {
57
+ config: runtime.config,
58
+ offline: runtime.offline,
59
+ ...(await inspectAddBox(runtime.db, {
62
60
  name,
63
61
  userId: identity.userId,
64
62
  description,
@@ -67,27 +65,131 @@ async function runBoxAdd(context) {
67
65
  }));
68
66
  return;
69
67
  }
70
- const result = await addBox(db, {
68
+ const result = await addBox(runtime.db, {
71
69
  name,
72
70
  userId: identity.userId,
73
71
  description,
74
72
  color,
75
73
  });
76
- if (!offline && !config.powersync.uploadBaseUrl) {
77
- printHumanReadableLine('warning: upload disabled (powersync.uploadBaseUrl is unset); changes are local-only for now');
78
- }
79
- if (!offline) {
80
- await waitForIdleDataFlow(db, Math.min(timeoutMs, 5_000));
81
- }
82
- const payload = { ok: true, stale: offline ? true : stale, ...result };
74
+ printUploadDisabledWarning(runtime);
75
+ await waitForMutationUpload(runtime);
76
+ const payload = { ok: true, stale: commandStale(runtime), ...result };
83
77
  if (structuredOutput) {
84
78
  printSuccess(payload);
85
79
  return;
86
80
  }
87
81
  printHumanReadableLine(result.id);
88
- if (stale && !offline) {
89
- printHumanReadableLine('warning: sync timed out; results may be stale');
82
+ printStaleCacheWarning(runtime, 'warning: sync timed out; results may be stale');
83
+ });
84
+ }
85
+ async function withBoxWriteRuntime(context, run) {
86
+ return withCommandRuntime(context, async (runtime) => {
87
+ const identity = await resolveAuthIdentity(runtime.config);
88
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
89
+ return run({ runtime, userId: identity.userId });
90
+ });
91
+ }
92
+ function requireArg(value, message) {
93
+ const trimmed = value?.trim() ?? '';
94
+ if (!trimmed) {
95
+ throw new DinoxError(message);
96
+ }
97
+ return trimmed;
98
+ }
99
+ async function runBoxRename(context) {
100
+ const path = requireArg(context.args.path, 'Box path is required');
101
+ const newName = requireArg(context.args.newName, 'New box name is required');
102
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
103
+ const result = await renameBox(runtime.db, { path, newName, userId }, { dryRun: Boolean(context.options.dryRun) });
104
+ if (Boolean(context.options.dryRun)) {
105
+ printDryRunPreview(buildBoxMutationDryRunPreview({
106
+ config: runtime.config,
107
+ offline: runtime.offline,
108
+ command: 'dino box rename',
109
+ action: 'rename_box',
110
+ risk: 'write',
111
+ summary: `Rename zettel box "${result.fromPath}" to "${result.toPath}".`,
112
+ inputs: { path, new_name: newName },
113
+ changes: { updated_boxes: result.updatedBoxes },
114
+ }));
115
+ return;
116
+ }
117
+ printUploadDisabledWarning(runtime);
118
+ await waitForMutationUpload(runtime);
119
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
120
+ });
121
+ }
122
+ async function runBoxMove(context) {
123
+ const path = requireArg(context.args.path, 'Box path is required');
124
+ const to = typeof context.options.to === 'string' ? context.options.to.trim() : '';
125
+ if (!to) {
126
+ throw new DinoxError('--to is required');
127
+ }
128
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
129
+ const result = await moveBox(runtime.db, { path, to, userId }, { dryRun: Boolean(context.options.dryRun) });
130
+ if (Boolean(context.options.dryRun)) {
131
+ printDryRunPreview(buildBoxMutationDryRunPreview({
132
+ config: runtime.config,
133
+ offline: runtime.offline,
134
+ command: 'dino box move',
135
+ action: 'move_box',
136
+ risk: 'write',
137
+ summary: `Move zettel box "${result.fromPath}" to "${result.toPath}".`,
138
+ inputs: { path, to },
139
+ changes: { updated_boxes: result.updatedBoxes },
140
+ }));
141
+ return;
142
+ }
143
+ printUploadDisabledWarning(runtime);
144
+ await waitForMutationUpload(runtime);
145
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
146
+ });
147
+ }
148
+ async function runBoxMerge(context) {
149
+ const from = requireArg(context.args.from, 'Source box path is required');
150
+ const to = requireArg(context.args.to, 'Target box path is required');
151
+ await withBoxWriteRuntime(context, async ({ runtime, userId }) => {
152
+ const result = await mergeBox(runtime.db, { from, to, userId }, { dryRun: Boolean(context.options.dryRun) });
153
+ if (Boolean(context.options.dryRun)) {
154
+ printDryRunPreview(buildBoxMutationDryRunPreview({
155
+ config: runtime.config,
156
+ offline: runtime.offline,
157
+ command: 'dino box merge',
158
+ action: 'merge_box',
159
+ risk: 'high-risk-write',
160
+ summary: `Merge zettel box subtree "${result.fromPath}" into "${result.toPath}".`,
161
+ inputs: { from, to },
162
+ changes: {
163
+ soft_deleted_box_ids: result.softDeletedBoxIds,
164
+ updated_notes: result.updatedNotes,
165
+ },
166
+ }));
167
+ return;
90
168
  }
169
+ printUploadDisabledWarning(runtime);
170
+ await waitForMutationUpload(runtime);
171
+ printSuccess({ ok: true, stale: commandStale(runtime), ...result });
172
+ });
173
+ }
174
+ async function runBoxCleanup(context) {
175
+ await withCommandRuntime(context, async (runtime) => {
176
+ const identity = await resolveAuthIdentity(runtime.config);
177
+ await syncBeforeCommand(runtime, { syncTokenIndex: true });
178
+ const result = await inspectBoxCleanup(runtime.db, { userId: identity.userId });
179
+ if (Boolean(context.options.dryRun)) {
180
+ printDryRunPreview(buildBoxMutationDryRunPreview({
181
+ config: runtime.config,
182
+ offline: runtime.offline,
183
+ command: 'dino box cleanup',
184
+ action: 'cleanup_boxes',
185
+ risk: 'write',
186
+ summary: `Inspect zettel box cleanup candidates (${result.issues.length} issues).`,
187
+ inputs: {},
188
+ targets: result.issues,
189
+ }));
190
+ return;
191
+ }
192
+ printSuccess({ stale: commandStale(runtime), ...result });
91
193
  });
92
194
  }
93
195
  export const boxCommandGroup = {
@@ -105,7 +207,7 @@ export const boxCommandGroup = {
105
207
  output: {
106
208
  type: 'structured',
107
209
  summary: 'Box list payload',
108
- fields: ['id', 'name', 'description'],
210
+ fields: ['id', 'name', 'path', 'parentId', 'description', 'color'],
109
211
  },
110
212
  examples: ['dino box list --format json'],
111
213
  },
@@ -113,10 +215,10 @@ export const boxCommandGroup = {
113
215
  },
114
216
  {
115
217
  name: 'add',
116
- description: 'Create a zettel box, restoring a deleted row when possible',
117
- arguments: [{ syntax: '[name]', description: 'Box name' }],
218
+ description: 'Create a zettel box path, restoring deleted path nodes when possible',
219
+ arguments: [{ syntax: '[path]', description: 'Box path, for example Work/Inbox' }],
118
220
  options: [
119
- { flags: '--name <string>', description: 'Box name (alternative to positional name)' },
221
+ { flags: '--name <string>', description: 'Box path (alternative to positional path)' },
120
222
  { flags: '--description <string>', description: 'Box purpose/usage description' },
121
223
  { flags: '--color <string>', description: 'Box color' },
122
224
  { flags: '--dry-run', description: 'Preview the write without executing it' },
@@ -129,12 +231,130 @@ export const boxCommandGroup = {
129
231
  output: {
130
232
  type: 'structured',
131
233
  summary: 'Box creation acknowledgement',
132
- fields: ['id', 'name', 'description', 'color', 'restored', 'stale'],
234
+ fields: ['id', 'name', 'path', 'parentId', 'description', 'color', 'restored', 'createdNodes', 'restoredNodes', 'stale'],
133
235
  },
134
- examples: ['dino box add "Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
236
+ examples: ['dino box add "Work/Inbox" --description "用于存放待整理的想法和资料" --format json --dry-run'],
135
237
  },
136
238
  action: runBoxAdd,
137
239
  },
240
+ {
241
+ name: 'tree',
242
+ description: 'Show zettel boxes as a hierarchy tree',
243
+ schema: {
244
+ path: 'box.tree',
245
+ risk: 'read',
246
+ auth: 'required',
247
+ globalOptions: BOX_GLOBAL_OPTIONS,
248
+ output: {
249
+ type: 'structured',
250
+ summary: 'Nested zettel box tree payload',
251
+ fields: ['tree.id', 'tree.name', 'tree.path', 'tree.parentId', 'tree.children', 'stale'],
252
+ },
253
+ examples: ['dino box tree --format json'],
254
+ },
255
+ action: runBoxTree,
256
+ },
257
+ {
258
+ name: 'stats',
259
+ description: 'Show zettel box note counts and empty-box status',
260
+ schema: {
261
+ path: 'box.stats',
262
+ risk: 'read',
263
+ auth: 'required',
264
+ globalOptions: BOX_GLOBAL_OPTIONS,
265
+ output: {
266
+ type: 'structured',
267
+ summary: 'Zettel box statistics payload',
268
+ fields: ['totalBoxes', 'emptyBoxes', 'boxes.id', 'boxes.path', 'boxes.directNoteCount', 'boxes.descendantNoteCount', 'boxes.latestNoteUpdatedAt', 'boxes.isEmpty', 'stale'],
269
+ },
270
+ examples: ['dino box stats --format json'],
271
+ },
272
+ action: runBoxStats,
273
+ },
274
+ {
275
+ name: 'rename',
276
+ description: 'Rename a zettel box and cascade descendant paths',
277
+ arguments: [
278
+ { syntax: '<path>', description: 'Existing box path' },
279
+ { syntax: '<new-name>', description: 'New leaf name' },
280
+ ],
281
+ options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
282
+ schema: {
283
+ path: 'box.rename',
284
+ risk: 'write',
285
+ auth: 'required',
286
+ globalOptions: BOX_GLOBAL_OPTIONS,
287
+ output: {
288
+ type: 'structured',
289
+ summary: 'Zettel box rename result',
290
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
291
+ },
292
+ examples: ['dino box rename "Work/Inbox" "Ideas" --format json --dry-run'],
293
+ },
294
+ action: runBoxRename,
295
+ },
296
+ {
297
+ name: 'move',
298
+ description: 'Move a zettel box under another parent and cascade descendant paths',
299
+ arguments: [{ syntax: '<path>', description: 'Existing box path' }],
300
+ options: [
301
+ { flags: '--to <parent-path>', description: 'Target parent box path', required: true, type: 'string' },
302
+ { flags: '--dry-run', description: 'Preview the write without executing it' },
303
+ ],
304
+ schema: {
305
+ path: 'box.move',
306
+ risk: 'write',
307
+ auth: 'required',
308
+ globalOptions: BOX_GLOBAL_OPTIONS,
309
+ output: {
310
+ type: 'structured',
311
+ summary: 'Zettel box move result',
312
+ fields: ['id', 'fromPath', 'toPath', 'updatedBoxes', 'stale'],
313
+ },
314
+ examples: ['dino box move "Inbox" --to "Work" --format json --dry-run'],
315
+ },
316
+ action: runBoxMove,
317
+ },
318
+ {
319
+ name: 'merge',
320
+ description: 'Merge a zettel box subtree into another box and soft-delete the source subtree',
321
+ arguments: [
322
+ { syntax: '<from>', description: 'Source box path' },
323
+ { syntax: '<to>', description: 'Target box path' },
324
+ ],
325
+ options: [{ flags: '--dry-run', description: 'Preview the write without executing it' }],
326
+ schema: {
327
+ path: 'box.merge',
328
+ risk: 'high-risk-write',
329
+ auth: 'required',
330
+ globalOptions: BOX_GLOBAL_OPTIONS,
331
+ output: {
332
+ type: 'structured',
333
+ summary: 'Zettel box merge result',
334
+ fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedBoxIds', 'updatedNotes', 'stale'],
335
+ },
336
+ examples: ['dino box merge "Work/Inbox" "Work/Archive" --format json --dry-run'],
337
+ },
338
+ action: runBoxMerge,
339
+ },
340
+ {
341
+ name: 'cleanup',
342
+ description: 'Inspect zettel box cleanup candidates',
343
+ options: [{ flags: '--dry-run', description: 'Preview cleanup candidates without writing' }],
344
+ schema: {
345
+ path: 'box.cleanup',
346
+ risk: 'read',
347
+ auth: 'required',
348
+ globalOptions: BOX_GLOBAL_OPTIONS,
349
+ output: {
350
+ type: 'structured',
351
+ summary: 'Zettel box cleanup inspection payload',
352
+ fields: ['issues', 'emptyBoxes', 'duplicatePaths', 'orphanParents', 'deletedWithActiveChildren', 'stale'],
353
+ },
354
+ examples: ['dino box cleanup --format json --dry-run'],
355
+ },
356
+ action: runBoxCleanup,
357
+ },
138
358
  ],
139
359
  };
140
360
  export function registerBoxesCommand(program) {