@dinoxx/dinox-cli 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo/read.d.ts +8 -0
  19. package/dist/commands/notes/repo/read.js +321 -0
  20. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  21. package/dist/commands/notes/repo/resolve.js +187 -0
  22. package/dist/commands/notes/repo/search.d.ts +42 -0
  23. package/dist/commands/notes/repo/search.js +513 -0
  24. package/dist/commands/notes/repo/shared.d.ts +190 -0
  25. package/dist/commands/notes/repo/shared.js +102 -0
  26. package/dist/commands/notes/repo/write.d.ts +24 -0
  27. package/dist/commands/notes/repo/write.js +251 -0
  28. package/dist/commands/notes/repo.d.ts +7 -177
  29. package/dist/commands/notes/repo.js +7 -1310
  30. package/dist/commands/notes/searchOutput.d.ts +2 -1
  31. package/dist/commands/notes/searchOutput.js +2 -1
  32. package/dist/commands/notes/searchSql.js +9 -3
  33. package/dist/commands/prompt/index.d.ts +1 -0
  34. package/dist/commands/prompt/index.js +26 -11
  35. package/dist/commands/savedViews/index.d.ts +7 -0
  36. package/dist/commands/savedViews/index.js +458 -0
  37. package/dist/commands/savedViews/model.d.ts +87 -0
  38. package/dist/commands/savedViews/model.js +306 -0
  39. package/dist/commands/savedViews/repo.d.ts +123 -0
  40. package/dist/commands/savedViews/repo.js +512 -0
  41. package/dist/commands/savedViews/semantics.d.ts +46 -0
  42. package/dist/commands/savedViews/semantics.js +738 -0
  43. package/dist/commands/schema/commandMetadata.js +4 -4
  44. package/dist/commands/schema/index.d.ts +1 -0
  45. package/dist/commands/schema/index.js +23 -2
  46. package/dist/commands/schema/renderSkills.d.ts +6 -0
  47. package/dist/commands/schema/renderSkills.js +109 -11
  48. package/dist/commands/schema/spec.js +4 -0
  49. package/dist/commands/skills/index.d.ts +80 -0
  50. package/dist/commands/skills/index.js +572 -0
  51. package/dist/commands/storage/index.js +2 -2
  52. package/dist/commands/storage/repo/configs.d.ts +30 -0
  53. package/dist/commands/storage/repo/configs.js +259 -0
  54. package/dist/commands/storage/repo/operations.d.ts +25 -0
  55. package/dist/commands/storage/repo/operations.js +337 -0
  56. package/dist/commands/storage/repo/remote.d.ts +32 -0
  57. package/dist/commands/storage/repo/remote.js +132 -0
  58. package/dist/commands/storage/repo/resources.d.ts +12 -0
  59. package/dist/commands/storage/repo/resources.js +134 -0
  60. package/dist/commands/storage/repo/shared.d.ts +181 -0
  61. package/dist/commands/storage/repo/shared.js +36 -0
  62. package/dist/commands/storage/repo/targets.d.ts +12 -0
  63. package/dist/commands/storage/repo/targets.js +178 -0
  64. package/dist/commands/storage/repo.d.ts +3 -166
  65. package/dist/commands/storage/repo.js +4 -1034
  66. package/dist/commands/sync.js +2 -1
  67. package/dist/commands/tags/index.js +30 -11
  68. package/dist/commands/tags/operations.d.ts +3 -3
  69. package/dist/commands/tags/operations.js +45 -20
  70. package/dist/commands/templates/model.d.ts +50 -0
  71. package/dist/commands/templates/model.js +239 -0
  72. package/dist/commands/todo/index.js +3 -3
  73. package/dist/commands/update.d.ts +1 -0
  74. package/dist/commands/update.js +46 -9
  75. package/dist/commands/writeConfirmation.d.ts +13 -0
  76. package/dist/commands/writeConfirmation.js +54 -0
  77. package/dist/config/resolve.js +5 -10
  78. package/dist/config/types.d.ts +3 -6
  79. package/dist/constants/links.d.ts +18 -1
  80. package/dist/constants/links.js +26 -1
  81. package/dist/contract/snapshot.d.ts +30 -0
  82. package/dist/contract/snapshot.js +114 -0
  83. package/dist/daemon/client.d.ts +2 -2
  84. package/dist/daemon/client.js +108 -54
  85. package/dist/daemon/index.d.ts +5 -3
  86. package/dist/daemon/index.js +28 -5
  87. package/dist/daemon/lifecycle.d.ts +7 -2
  88. package/dist/daemon/lifecycle.js +53 -13
  89. package/dist/daemon/pid.d.ts +1 -1
  90. package/dist/daemon/pid.js +6 -3
  91. package/dist/dinox.js +9 -1
  92. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  93. package/dist/powersync/exclusiveRuntime.js +26 -8
  94. package/dist/powersync/runtime.d.ts +3 -2
  95. package/dist/powersync/runtime.js +19 -3
  96. package/dist/powersync/schema/content.d.ts +1 -78
  97. package/dist/powersync/schema/content.js +1 -78
  98. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  99. package/dist/powersync/schema/electronCanonical.js +1154 -0
  100. package/dist/powersync/schema/index.d.ts +573 -129
  101. package/dist/powersync/schema/index.js +3 -32
  102. package/dist/powersync/schema/local.d.ts +1 -56
  103. package/dist/powersync/schema/local.js +1 -60
  104. package/dist/powersync/schema/note.d.ts +1 -35
  105. package/dist/powersync/schema/note.js +1 -35
  106. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  107. package/dist/powersync/schema/notesExtras.js +1 -71
  108. package/dist/powersync/schema/projects.d.ts +1 -104
  109. package/dist/powersync/schema/projects.js +1 -104
  110. package/dist/powersync/schema/savedViews.d.ts +1 -0
  111. package/dist/powersync/schema/savedViews.js +1 -0
  112. package/dist/powersync/schema/storage.d.ts +1 -24
  113. package/dist/powersync/schema/storage.js +1 -24
  114. package/dist/powersync/schema/tags.d.ts +1 -37
  115. package/dist/powersync/schema/tags.js +1 -37
  116. package/dist/powersync/syncGate.d.ts +20 -2
  117. package/dist/powersync/uploader.d.ts +16 -2
  118. package/dist/powersync/uploader.js +152 -158
  119. package/dist/skills/check.d.ts +1 -0
  120. package/dist/skills/check.js +538 -70
  121. package/dist/skills/commandRefs.d.ts +21 -0
  122. package/dist/skills/commandRefs.js +132 -0
  123. package/dist/skills/generate.d.ts +13 -0
  124. package/dist/skills/generate.js +80 -0
  125. package/dist/skills/registry.d.ts +13 -0
  126. package/dist/skills/registry.js +58 -0
  127. package/dist/skills/retired.d.ts +60 -0
  128. package/dist/skills/retired.js +58 -0
  129. package/dist/utils/deadline.d.ts +8 -0
  130. package/dist/utils/deadline.js +31 -0
  131. package/dist/utils/dryRun.d.ts +20 -1
  132. package/dist/utils/dryRun.js +22 -3
  133. package/dist/utils/errorCodes.d.ts +311 -0
  134. package/dist/utils/errorCodes.js +92 -0
  135. package/dist/utils/errors.d.ts +15 -2
  136. package/dist/utils/errors.js +13 -0
  137. package/dist/utils/processExit.d.ts +1 -0
  138. package/dist/utils/processExit.js +33 -0
  139. package/dist/utils/updateNotice.d.ts +1 -0
  140. package/dist/utils/updateNotice.js +1 -1
  141. package/package.json +18 -8
@@ -0,0 +1,13 @@
1
+ export type ConfirmedWriteOptions = {
2
+ dryRun?: boolean;
3
+ confirm?: boolean;
4
+ expectedCount?: number | null;
5
+ };
6
+ export declare function parseExpectedCount(value: unknown): number | null;
7
+ export declare function assertConfirmedWrite(args: {
8
+ operation: string;
9
+ schemaPath: string;
10
+ actualCount: number;
11
+ options?: ConfirmedWriteOptions;
12
+ }): void;
13
+ export declare function assertWriteConfirmationProvided(operation: string, options?: ConfirmedWriteOptions): void;
@@ -0,0 +1,54 @@
1
+ import { DinoxError } from '../utils/errors.js';
2
+ export function parseExpectedCount(value) {
3
+ if (value === undefined || value === null || value === '') {
4
+ return null;
5
+ }
6
+ if (typeof value !== 'string') {
7
+ throw new DinoxError('--expected-count must be a non-negative integer');
8
+ }
9
+ const trimmed = value.trim();
10
+ if (!/^\d+$/.test(trimmed)) {
11
+ throw new DinoxError('--expected-count must be a non-negative integer');
12
+ }
13
+ const parsed = Number(trimmed);
14
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
15
+ throw new DinoxError('--expected-count must be a non-negative integer');
16
+ }
17
+ return parsed;
18
+ }
19
+ export function assertConfirmedWrite(args) {
20
+ if (args.options?.dryRun) {
21
+ return;
22
+ }
23
+ const options = args.options;
24
+ assertWriteConfirmationProvided(args.operation, options);
25
+ const expectedCount = options?.expectedCount;
26
+ if (expectedCount === undefined || expectedCount === null) {
27
+ throw new DinoxError(`${args.operation} requires --expected-count <n> for writes`);
28
+ }
29
+ if (expectedCount !== args.actualCount) {
30
+ throw new DinoxError(`--expected-count ${expectedCount} does not match current affected record count ${args.actualCount}`, {
31
+ code: 'PRECONDITION_FAILED',
32
+ details: {
33
+ expectedCount,
34
+ actualCount: args.actualCount,
35
+ },
36
+ suggestion: `Re-run the merge with --dry-run, review changes.affected_count, then retry with the matching count. See \`dino schema ${args.schemaPath} --format json\`.`,
37
+ suggestedAction: {
38
+ command: `dino schema ${args.schemaPath} --format json`,
39
+ reason: 'Review the merge guard contract, then run a fresh dry-run before retrying.',
40
+ },
41
+ });
42
+ }
43
+ }
44
+ export function assertWriteConfirmationProvided(operation, options) {
45
+ if (options?.dryRun) {
46
+ return;
47
+ }
48
+ if (!options?.confirm) {
49
+ throw new DinoxError(`${operation} requires --confirm for writes`);
50
+ }
51
+ if (options.expectedCount === undefined || options.expectedCount === null) {
52
+ throw new DinoxError(`${operation} requires --expected-count <n> for writes`);
53
+ }
54
+ }
@@ -2,8 +2,7 @@ import crypto from 'node:crypto';
2
2
  import { POWERSYNC_DEFAULT_ENDPOINT, POWERSYNC_DEFAULT_TOKEN_ENDPOINT, POWERSYNC_DEFAULT_UPLOAD_BASE_URL, } from './serviceEndpoints.js';
3
3
  import { normalizeAuthToken, readEnvAuthToken } from '../utils/authToken.js';
4
4
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
5
- const DEFAULT_UPLOAD_V4_PATH = '/powersync/v4/uploadData';
6
- const DEFAULT_UPLOAD_V2_PATH = '/powersync/v2/uploadData';
5
+ const DEFAULT_UPLOAD_PATH = '/powersync/v6/uploadData';
7
6
  const DEFAULT_SYNC_TIMEOUT_MS = 300_000;
8
7
  function nonEmptyString(value) {
9
8
  if (typeof value !== 'string') {
@@ -24,8 +23,7 @@ export function fingerprintDaemonRuntimeConfig(config) {
24
23
  endpoint: config.powersync.endpoint,
25
24
  tokenEndpoint: config.powersync.tokenEndpoint,
26
25
  uploadBaseUrl: config.powersync.uploadBaseUrl,
27
- uploadV4Path: config.powersync.uploadV4Path,
28
- uploadV2Path: config.powersync.uploadV2Path,
26
+ uploadPath: config.powersync.uploadPath,
29
27
  },
30
28
  sync: {
31
29
  timeoutMs: config.sync.timeoutMs,
@@ -40,13 +38,11 @@ export function resolveConfig(raw) {
40
38
  const fileEmail = nonEmptyString(raw.auth?.email ?? undefined) ?? raw.auth?.email ?? undefined;
41
39
  const fileUserInfoSyncedAt = nonEmptyString(raw.auth?.userInfoSyncedAt);
42
40
  const fileTokenEndpoint = nonEmptyString(raw.powersync?.tokenEndpoint);
43
- const fileUploadV4Path = nonEmptyString(raw.powersync?.uploadV4Path);
44
- const fileUploadV2Path = nonEmptyString(raw.powersync?.uploadV2Path);
41
+ const fileUploadPath = nonEmptyString(raw.powersync?.uploadPath);
45
42
  const fileTimeoutMs = parseSyncTimeoutMs(raw.sync?.timeoutMs, { invalid: 'undefined' });
46
43
  const envAuth = readEnvAuthToken();
47
44
  const envTokenEndpoint = nonEmptyString(process.env.DINOX_POWERSYNC_TOKEN_ENDPOINT) ?? nonEmptyString(process.env.POWERSYNC_TOKEN_ENDPOINT);
48
- const envUploadV4Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V4_PATH);
49
- const envUploadV2Path = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_V2_PATH);
45
+ const envUploadPath = nonEmptyString(process.env.DINOX_POWERSYNC_UPLOAD_PATH);
50
46
  const envTimeoutMs = parseSyncTimeoutMs(process.env.DINOX_SYNC_TIMEOUT_MS, { invalid: 'undefined' });
51
47
  const effectiveAuth = envAuth ?? fileAuth;
52
48
  const canReusePersistedIdentity = !envAuth || envAuth === fileAuth;
@@ -63,8 +59,7 @@ export function resolveConfig(raw) {
63
59
  endpoint: POWERSYNC_DEFAULT_ENDPOINT,
64
60
  tokenEndpoint: envTokenEndpoint ?? fileTokenEndpoint ?? POWERSYNC_DEFAULT_TOKEN_ENDPOINT,
65
61
  uploadBaseUrl: POWERSYNC_DEFAULT_UPLOAD_BASE_URL,
66
- uploadV4Path: envUploadV4Path ?? fileUploadV4Path ?? DEFAULT_UPLOAD_V4_PATH,
67
- uploadV2Path: envUploadV2Path ?? fileUploadV2Path ?? DEFAULT_UPLOAD_V2_PATH,
62
+ uploadPath: envUploadPath ?? fileUploadPath ?? DEFAULT_UPLOAD_PATH,
68
63
  },
69
64
  sync: {
70
65
  timeoutMs: envTimeoutMs ?? fileTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS,
@@ -21,10 +21,8 @@ export type DinoxConfig = {
21
21
  tokenEndpoint?: string;
22
22
  /** Base URL for upload endpoints. */
23
23
  uploadBaseUrl?: string;
24
- /** Defaults to /powersync/v4/uploadData */
25
- uploadV4Path?: string;
26
- /** Defaults to /powersync/v2/uploadData */
27
- uploadV2Path?: string;
24
+ /** Batch upload endpoint path. Defaults to /powersync/v6/uploadData */
25
+ uploadPath?: string;
28
26
  };
29
27
  sync?: {
30
28
  /** Default sync/connect timeout in milliseconds. */
@@ -45,8 +43,7 @@ export type ResolvedDinoxConfig = {
45
43
  endpoint: string;
46
44
  tokenEndpoint: string;
47
45
  uploadBaseUrl: string;
48
- uploadV4Path: string;
49
- uploadV2Path: string;
46
+ uploadPath: string;
50
47
  };
51
48
  sync: {
52
49
  timeoutMs: number;
@@ -1 +1,18 @@
1
- export declare const SKILLS_REPO_URL = "https://github.com/ryzencool/dinox-cli/tree/main/skills";
1
+ export declare const SKILLS_REPO_SLUG = "ryzencool/dinox-cli-skills";
2
+ export declare const SKILLS_REPO_URL = "https://github.com/ryzencool/dinox-cli-skills";
3
+ export declare const SKILLS_INSTALLER_NAME = "skills";
4
+ export declare const SKILLS_INSTALLER_VERSION = "1.5.16";
5
+ export declare const SKILLS_INSTALL_AGENTS: readonly ["codex", "claude-code", "hermes-agent", "openclaw"];
6
+ export type SkillsInstallerMetadata = {
7
+ name: typeof SKILLS_INSTALLER_NAME;
8
+ version: typeof SKILLS_INSTALLER_VERSION;
9
+ };
10
+ export type SkillsReleaseMetadata = {
11
+ repository: string;
12
+ version: string;
13
+ tag: string;
14
+ release: string;
15
+ installer: SkillsInstallerMetadata;
16
+ installCommand: string;
17
+ };
18
+ export declare function getSkillsReleaseMetadata(version: string): SkillsReleaseMetadata;
@@ -1 +1,26 @@
1
- export const SKILLS_REPO_URL = 'https://github.com/ryzencool/dinox-cli/tree/main/skills';
1
+ export const SKILLS_REPO_SLUG = 'ryzencool/dinox-cli-skills';
2
+ export const SKILLS_REPO_URL = `https://github.com/${SKILLS_REPO_SLUG}`;
3
+ export const SKILLS_INSTALLER_NAME = 'skills';
4
+ export const SKILLS_INSTALLER_VERSION = '1.5.16';
5
+ export const SKILLS_INSTALL_AGENTS = Object.freeze([
6
+ 'codex',
7
+ 'claude-code',
8
+ 'hermes-agent',
9
+ 'openclaw',
10
+ ]);
11
+ export function getSkillsReleaseMetadata(version) {
12
+ const normalizedVersion = version.trim();
13
+ const tag = `v${normalizedVersion}`;
14
+ const agentFlags = SKILLS_INSTALL_AGENTS.map((agent) => `-a ${agent}`).join(' ');
15
+ return {
16
+ repository: SKILLS_REPO_URL,
17
+ version: normalizedVersion,
18
+ tag,
19
+ release: `${SKILLS_REPO_URL}/tree/${encodeURIComponent(tag)}`,
20
+ installer: {
21
+ name: SKILLS_INSTALLER_NAME,
22
+ version: SKILLS_INSTALLER_VERSION,
23
+ },
24
+ installCommand: `npx --yes ${SKILLS_INSTALLER_NAME}@${SKILLS_INSTALLER_VERSION} add ${SKILLS_REPO_SLUG}#${tag} -g ${agentFlags} --skill '*' -y`,
25
+ };
26
+ }
@@ -0,0 +1,30 @@
1
+ export type ContractOption = {
2
+ name: string;
3
+ required: boolean;
4
+ type?: string;
5
+ enum?: string[];
6
+ };
7
+ export type ContractCommand = {
8
+ path: string;
9
+ command: string;
10
+ risk: string;
11
+ auth: string;
12
+ arguments: Array<{
13
+ name: string;
14
+ required: boolean;
15
+ }>;
16
+ options: ContractOption[];
17
+ outputFields: string[];
18
+ outputMetaFields: string[];
19
+ };
20
+ export type CliContractSnapshot = {
21
+ contractVersion: 1;
22
+ cliVersion: string;
23
+ commands: ContractCommand[];
24
+ errorCodes: Record<string, number>;
25
+ };
26
+ export declare function buildContractSnapshot(cliVersion: string): CliContractSnapshot;
27
+ export declare function renderContractSnapshot(snapshot: CliContractSnapshot): string;
28
+ /** Changes that can break an existing agent or script that relied on the previous contract. */
29
+ export declare function findBreakingChanges(previous: CliContractSnapshot, next: CliContractSnapshot): string[];
30
+ export declare function majorVersion(version: string): number;
@@ -0,0 +1,114 @@
1
+ // Machine-readable snapshot of the agent-facing CLI contract: command paths, arguments,
2
+ // options, risk/auth, structured output fields, and error codes with exit statuses.
3
+ // The committed copy (contract/cli-contract.json) makes every contract change visible in
4
+ // review, and `findBreakingChanges` refuses breaking changes without a major version bump.
5
+ import { listCommandSchemas } from '../commands/schema/spec.js';
6
+ import { ERROR_CODES } from '../utils/errorCodes.js';
7
+ const RISK_ORDER = ['read', 'write', 'high-risk-write'];
8
+ function flagOf(optionName) {
9
+ return optionName.match(/--[a-z0-9][a-z0-9-]*/i)?.[0] ?? optionName;
10
+ }
11
+ export function buildContractSnapshot(cliVersion) {
12
+ const commands = listCommandSchemas()
13
+ .map((schema) => ({
14
+ path: schema.path,
15
+ command: schema.command,
16
+ risk: schema.risk,
17
+ auth: schema.auth,
18
+ arguments: (schema.arguments ?? []).map((argument) => ({ name: argument.name, required: argument.required })),
19
+ options: (schema.options ?? [])
20
+ .map((option) => ({
21
+ name: option.name,
22
+ required: option.required === true,
23
+ ...(option.type ? { type: option.type } : {}),
24
+ ...(option.enum ? { enum: [...option.enum] } : {}),
25
+ }))
26
+ .sort((left, right) => flagOf(left.name).localeCompare(flagOf(right.name))),
27
+ outputFields: [...(schema.output.fields ?? [])],
28
+ outputMetaFields: [...(schema.output.metaFields ?? [])],
29
+ }))
30
+ .sort((left, right) => left.path.localeCompare(right.path));
31
+ const errorCodes = Object.fromEntries(Object.keys(ERROR_CODES).sort().map((code) => [code, ERROR_CODES[code].exitCode]));
32
+ return { contractVersion: 1, cliVersion, commands, errorCodes };
33
+ }
34
+ export function renderContractSnapshot(snapshot) {
35
+ return `${JSON.stringify(snapshot, null, 2)}\n`;
36
+ }
37
+ /** Changes that can break an existing agent or script that relied on the previous contract. */
38
+ export function findBreakingChanges(previous, next) {
39
+ const changes = [];
40
+ const nextCommands = new Map(next.commands.map((command) => [command.path, command]));
41
+ for (const before of previous.commands) {
42
+ const after = nextCommands.get(before.path);
43
+ if (!after) {
44
+ changes.push(`command ${before.path} was removed`);
45
+ continue;
46
+ }
47
+ if (after.command !== before.command) {
48
+ changes.push(`command ${before.path} changed invocation from \`${before.command}\` to \`${after.command}\``);
49
+ }
50
+ if (RISK_ORDER.indexOf(after.risk) > RISK_ORDER.indexOf(before.risk)) {
51
+ changes.push(`command ${before.path} risk escalated from ${before.risk} to ${after.risk}`);
52
+ }
53
+ if (before.auth !== 'required' && after.auth === 'required') {
54
+ changes.push(`command ${before.path} now requires auth`);
55
+ }
56
+ const requiredArgsBefore = before.arguments.filter((argument) => argument.required).length;
57
+ const requiredArgsAfter = after.arguments.filter((argument) => argument.required).length;
58
+ if (requiredArgsAfter > requiredArgsBefore) {
59
+ changes.push(`command ${before.path} gained a required positional argument`);
60
+ }
61
+ if (after.arguments.length < before.arguments.length) {
62
+ changes.push(`command ${before.path} lost a positional argument`);
63
+ }
64
+ const optionsAfter = new Map(after.options.map((option) => [flagOf(option.name), option]));
65
+ for (const option of before.options) {
66
+ const flag = flagOf(option.name);
67
+ const updated = optionsAfter.get(flag);
68
+ if (!updated) {
69
+ changes.push(`option ${before.path} ${flag} was removed`);
70
+ continue;
71
+ }
72
+ if (!option.required && updated.required) {
73
+ changes.push(`option ${before.path} ${flag} became required`);
74
+ }
75
+ if (option.type && updated.type && option.type !== updated.type) {
76
+ changes.push(`option ${before.path} ${flag} changed type from ${option.type} to ${updated.type}`);
77
+ }
78
+ for (const value of option.enum ?? []) {
79
+ if (updated.enum && !updated.enum.includes(value)) {
80
+ changes.push(`option ${before.path} ${flag} no longer accepts ${value}`);
81
+ }
82
+ }
83
+ }
84
+ for (const option of after.options) {
85
+ if (option.required && !before.options.some((previousOption) => flagOf(previousOption.name) === flagOf(option.name))) {
86
+ changes.push(`option ${before.path} ${flagOf(option.name)} was added as required`);
87
+ }
88
+ }
89
+ for (const field of before.outputFields) {
90
+ if (!after.outputFields.includes(field)) {
91
+ changes.push(`output field ${before.path}.${field} was removed`);
92
+ }
93
+ }
94
+ for (const field of before.outputMetaFields) {
95
+ if (!after.outputMetaFields.includes(field)) {
96
+ changes.push(`output meta field ${before.path}.${field} was removed`);
97
+ }
98
+ }
99
+ }
100
+ for (const [code, exitCode] of Object.entries(previous.errorCodes)) {
101
+ const updated = next.errorCodes[code];
102
+ if (updated === undefined) {
103
+ changes.push(`error code ${code} was removed`);
104
+ }
105
+ else if (updated !== exitCode) {
106
+ changes.push(`error code ${code} exit status changed from ${exitCode} to ${updated}`);
107
+ }
108
+ }
109
+ return changes;
110
+ }
111
+ export function majorVersion(version) {
112
+ const major = Number.parseInt(version.split('.')[0] ?? '', 10);
113
+ return Number.isFinite(major) ? major : 0;
114
+ }
@@ -2,13 +2,13 @@ import type { DaemonPingResult, DaemonShutdownResult } from './types.js';
2
2
  export declare function sendDaemonRequest(method: string, params?: unknown, requestOptions?: {
3
3
  timeoutMs?: number;
4
4
  }): Promise<unknown>;
5
- export declare function isDaemonRunning(): Promise<boolean>;
5
+ export declare function isDaemonRunning(timeoutMs?: number): Promise<boolean>;
6
6
  /**
7
7
  * Ping the daemon and return its handshake payload (version + connected identity),
8
8
  * or null when no daemon is reachable. Callers use this to detect a stale daemon
9
9
  * (wrong version or wrong account) rather than only "running / not running".
10
10
  */
11
- export declare function getDaemonInfo(): Promise<DaemonPingResult | null>;
11
+ export declare function getDaemonInfo(timeoutMs?: number): Promise<DaemonPingResult | null>;
12
12
  /**
13
13
  * Whether a reachable daemon matches the current CLI version and signed-in account.
14
14
  * A mismatch means the daemon was started by an older build or a different user and
@@ -1,10 +1,32 @@
1
1
  import { request as httpRequest } from 'node:http';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
- import { DinoxError } from '../utils/errors.js';
4
+ import { remainingDeadlineMs } from '../utils/deadline.js';
5
+ import { DinoxError, wireErrorCode } from '../utils/errors.js';
5
6
  const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
6
7
  const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
8
+ function normalizedTimeoutMs(timeoutMs, fallbackMs) {
9
+ return Number.isFinite(timeoutMs)
10
+ ? Math.max(0, Math.floor(timeoutMs))
11
+ : fallbackMs;
12
+ }
13
+ function createDaemonReleaseTimeoutError(message) {
14
+ return new DinoxError(message, {
15
+ code: 'DAEMON_RELEASE_TIMEOUT',
16
+ suggestion: 'Run `dino daemon stop`, then retry the command.',
17
+ recoverable: true,
18
+ });
19
+ }
20
+ async function waitForNextPoll(deadlineAtMs, intervalMs) {
21
+ const waitMs = remainingDeadlineMs(deadlineAtMs, Math.max(0, intervalMs));
22
+ if (waitMs <= 0) {
23
+ return false;
24
+ }
25
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1, waitMs)));
26
+ return true;
27
+ }
7
28
  export async function sendDaemonRequest(method, params, requestOptions = {}) {
29
+ const timeoutMs = Math.max(1, normalizedTimeoutMs(requestOptions.timeoutMs ?? DEFAULT_DAEMON_REQUEST_TIMEOUT_MS, DEFAULT_DAEMON_REQUEST_TIMEOUT_MS));
8
30
  const request = {
9
31
  id: Date.now().toString(),
10
32
  method,
@@ -12,10 +34,14 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
12
34
  };
13
35
  return new Promise((resolve, reject) => {
14
36
  let settled = false;
37
+ let deadlineTimer;
15
38
  const finalize = (error, value) => {
16
39
  if (settled)
17
40
  return;
18
41
  settled = true;
42
+ if (deadlineTimer) {
43
+ clearTimeout(deadlineTimer);
44
+ }
19
45
  if (error) {
20
46
  reject(error);
21
47
  return;
@@ -32,7 +58,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
32
58
  'Content-Type': 'application/json',
33
59
  'Content-Length': Buffer.byteLength(data),
34
60
  },
35
- timeout: requestOptions.timeoutMs ?? DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
61
+ timeout: timeoutMs,
36
62
  };
37
63
  const req = httpRequest(options, (res) => {
38
64
  const statusCode = res.statusCode ?? 0;
@@ -70,6 +96,10 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
70
96
  req.destroy(new Error('Daemon request timed out'));
71
97
  });
72
98
  req.on('error', (error) => finalize(error));
99
+ deadlineTimer = setTimeout(() => {
100
+ req.destroy(new Error('Daemon request timed out'));
101
+ }, timeoutMs);
102
+ deadlineTimer.unref?.();
73
103
  req.write(data);
74
104
  req.end();
75
105
  });
@@ -84,7 +114,7 @@ function toDaemonError(error) {
84
114
  if (isStructuredDaemonError(error)) {
85
115
  if (error.name === 'DinoxError' || error.code !== 'INTERNAL_ERROR') {
86
116
  return new DinoxError(error.message, {
87
- code: error.code,
117
+ code: wireErrorCode(error.code),
88
118
  details: error.details,
89
119
  suggestion: error.suggestion,
90
120
  suggestedAction: error.suggested_action,
@@ -97,17 +127,17 @@ function toDaemonError(error) {
97
127
  }
98
128
  return new Error(typeof error === 'string' && error.trim() ? error : 'Unknown error');
99
129
  }
100
- export async function isDaemonRunning() {
101
- return (await getDaemonInfo()) !== null;
130
+ export async function isDaemonRunning(timeoutMs) {
131
+ return (await getDaemonInfo(timeoutMs)) !== null;
102
132
  }
103
133
  /**
104
134
  * Ping the daemon and return its handshake payload (version + connected identity),
105
135
  * or null when no daemon is reachable. Callers use this to detect a stale daemon
106
136
  * (wrong version or wrong account) rather than only "running / not running".
107
137
  */
108
- export async function getDaemonInfo() {
138
+ export async function getDaemonInfo(timeoutMs) {
109
139
  try {
110
- const result = await sendDaemonRequest('ping');
140
+ const result = await sendDaemonRequest('ping', undefined, { timeoutMs });
111
141
  if (result && typeof result === 'object') {
112
142
  const info = result;
113
143
  return {
@@ -147,25 +177,38 @@ export function isLegacyDaemonInfo(info) {
147
177
  return info.pid === 0 && info.instanceId.length === 0;
148
178
  }
149
179
  export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
180
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
181
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
182
+ const remainingTimeoutMs = () => remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
183
+ const requireRemainingTimeoutMs = () => {
184
+ const remainingMs = remainingTimeoutMs();
185
+ if (remainingMs <= 0) {
186
+ throw createDaemonReleaseTimeoutError('Timed out while verifying the legacy daemon process.');
187
+ }
188
+ return remainingMs;
189
+ };
150
190
  const pidInfo = readPid();
151
- if (!isLegacyDaemonInfo(initialInfo)
152
- || !pidInfo
153
- || pidInfo.instanceId !== null
154
- || !isProcessRunning(pidInfo.pid)
155
- || !await isLegacyDaemonProcess(pidInfo.pid, initialInfo.uptime)) {
191
+ const legacyProcessVerified = Boolean(isLegacyDaemonInfo(initialInfo)
192
+ && pidInfo
193
+ && pidInfo.instanceId === null
194
+ && isProcessRunning(pidInfo.pid)
195
+ && await isLegacyDaemonProcess(pidInfo.pid, initialInfo.uptime, requireRemainingTimeoutMs()));
196
+ if (!legacyProcessVerified
197
+ || !pidInfo) {
156
198
  throw new DinoxError('A legacy daemon is responding, but its process identity could not be verified safely.', {
157
199
  code: 'DAEMON_LEGACY_UNVERIFIED',
158
200
  suggestion: 'Inspect the daemon PID and command line, stop that process manually, then retry.',
159
201
  recoverable: true,
160
202
  });
161
203
  }
162
- const confirmedInfo = await getDaemonInfo();
204
+ const confirmedInfo = await getDaemonInfo(requireRemainingTimeoutMs());
163
205
  const confirmedPidInfo = readPid();
164
- if (!confirmedInfo
165
- || !isLegacyDaemonInfo(confirmedInfo)
166
- || confirmedPidInfo?.pid !== pidInfo.pid
167
- || confirmedPidInfo.instanceId !== null
168
- || !await isLegacyDaemonProcess(pidInfo.pid, confirmedInfo.uptime)) {
206
+ const confirmedLegacyProcess = Boolean(confirmedInfo
207
+ && isLegacyDaemonInfo(confirmedInfo)
208
+ && confirmedPidInfo?.pid === pidInfo.pid
209
+ && confirmedPidInfo.instanceId === null
210
+ && await isLegacyDaemonProcess(pidInfo.pid, confirmedInfo.uptime, requireRemainingTimeoutMs()));
211
+ if (!confirmedLegacyProcess) {
169
212
  throw new DinoxError('The legacy daemon changed while its process identity was being verified.', {
170
213
  code: 'DAEMON_INSTANCE_CHANGED',
171
214
  suggestion: 'Retry after checking `dino daemon status`.',
@@ -183,9 +226,8 @@ export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
183
226
  recoverable: true,
184
227
  });
185
228
  }
186
- const deadline = Date.now() + timeoutMs;
187
- while (Date.now() < deadline) {
188
- const current = await getDaemonInfo();
229
+ while (remainingTimeoutMs() > 0) {
230
+ const current = await getDaemonInfo(requireRemainingTimeoutMs());
189
231
  const processRunning = isProcessRunning(pidInfo.pid);
190
232
  if (!current && !processRunning) {
191
233
  removePid(undefined, { expectedPid: pidInfo.pid });
@@ -199,37 +241,46 @@ export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
199
241
  recoverable: true,
200
242
  });
201
243
  }
202
- await new Promise((resolve) => setTimeout(resolve, 100));
244
+ if (!await waitForNextPoll(deadlineAtMs, 100)) {
245
+ break;
246
+ }
203
247
  }
204
- throw new DinoxError('Timed out waiting for the verified legacy daemon to stop.', {
205
- code: 'DAEMON_RELEASE_TIMEOUT',
206
- suggestion: 'Inspect the daemon PID and stop that process manually before retrying.',
207
- recoverable: true,
208
- });
248
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the verified legacy daemon to stop.');
209
249
  }
210
250
  /** Poll until the daemon answers a ping or the timeout elapses. */
211
251
  export async function waitForDaemonReady(timeoutMs = 5_000, intervalMs = 250) {
212
- const deadline = Date.now() + timeoutMs;
213
- while (Date.now() < deadline) {
214
- if (await isDaemonRunning()) {
252
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
253
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
254
+ while (true) {
255
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
256
+ if (remainingMs <= 0) {
257
+ return false;
258
+ }
259
+ if (await isDaemonRunning(remainingMs)) {
215
260
  return true;
216
261
  }
217
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
262
+ if (!await waitForNextPoll(deadlineAtMs, intervalMs)) {
263
+ return false;
264
+ }
218
265
  }
219
- return isDaemonRunning();
220
266
  }
221
267
  /** Poll until the daemon answers with the expected authenticated identity. */
222
268
  export async function waitForFreshDaemon(expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint, timeoutMs = 5_000, intervalMs = 250) {
223
- const deadline = Date.now() + timeoutMs;
224
- while (Date.now() < deadline) {
225
- const info = await getDaemonInfo();
269
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
270
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
271
+ while (true) {
272
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
273
+ if (remainingMs <= 0) {
274
+ return false;
275
+ }
276
+ const info = await getDaemonInfo(remainingMs);
226
277
  if (info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint)) {
227
278
  return true;
228
279
  }
229
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
280
+ if (!await waitForNextPoll(deadlineAtMs, intervalMs)) {
281
+ return false;
282
+ }
230
283
  }
231
- const info = await getDaemonInfo();
232
- return Boolean(info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint));
233
284
  }
234
285
  /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
235
286
  export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
@@ -255,7 +306,17 @@ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_00
255
306
  }
256
307
  /** Stop a reachable daemon before another process acquires the exclusive PowerSync owner lease. */
257
308
  export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expectedInstanceId) {
258
- const info = await getDaemonInfo();
309
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
310
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
311
+ const remainingTimeoutMs = () => remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
312
+ const requireRemainingTimeoutMs = () => {
313
+ const remainingMs = remainingTimeoutMs();
314
+ if (remainingMs <= 0) {
315
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the daemon to release the PowerSync runtime.');
316
+ }
317
+ return remainingMs;
318
+ };
319
+ const info = await getDaemonInfo(requireRemainingTimeoutMs());
259
320
  if (!info) {
260
321
  const pidInfo = readPid();
261
322
  const socketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
@@ -269,7 +330,7 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
269
330
  return false;
270
331
  }
271
332
  if (isLegacyDaemonInfo(info)) {
272
- return shutdownLegacyDaemon(info, timeoutMs);
333
+ return shutdownLegacyDaemon(info, requireRemainingTimeoutMs());
273
334
  }
274
335
  if (expectedInstanceId && info.instanceId !== expectedInstanceId) {
275
336
  throw new DinoxError('Daemon instance changed before exclusive access could be acquired.', {
@@ -280,7 +341,7 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
280
341
  });
281
342
  }
282
343
  try {
283
- await requestDaemonShutdown(info.instanceId, Math.min(timeoutMs, 2_000));
344
+ await requestDaemonShutdown(info.instanceId, Math.min(requireRemainingTimeoutMs(), 2_000));
284
345
  }
285
346
  catch (error) {
286
347
  throw new DinoxError('The running daemon could not release the PowerSync runtime.', {
@@ -290,21 +351,14 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
290
351
  recoverable: true,
291
352
  });
292
353
  }
293
- const deadline = Date.now() + timeoutMs;
294
- while (Date.now() < deadline) {
295
- const current = await getDaemonInfo();
354
+ while (remainingTimeoutMs() > 0) {
355
+ const current = await getDaemonInfo(requireRemainingTimeoutMs());
296
356
  if (!current || current.instanceId !== info.instanceId) {
297
357
  return true;
298
358
  }
299
- await new Promise((resolve) => setTimeout(resolve, 100));
300
- }
301
- const current = await getDaemonInfo();
302
- if (!current || current.instanceId !== info.instanceId) {
303
- return true;
359
+ if (!await waitForNextPoll(deadlineAtMs, 100)) {
360
+ break;
361
+ }
304
362
  }
305
- throw new DinoxError('Timed out waiting for the daemon to release the PowerSync runtime.', {
306
- code: 'DAEMON_RELEASE_TIMEOUT',
307
- suggestion: 'Run `dino daemon stop`, then retry the command.',
308
- recoverable: true,
309
- });
363
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the daemon to release the PowerSync runtime.');
310
364
  }
@@ -8,14 +8,16 @@ type DaemonStartResult = {
8
8
  detached: boolean;
9
9
  alreadyRunning: boolean;
10
10
  };
11
+ type DaemonStartOptions = {
12
+ quiet?: boolean;
13
+ timeoutMs?: number;
14
+ };
11
15
  type DaemonStopResult = {
12
16
  stopped: boolean;
13
17
  wasRunning: boolean;
14
18
  stalePidFile: boolean;
15
19
  };
16
- export declare function startDaemonLocked(port: number, detached: boolean, options?: {
17
- quiet?: boolean;
18
- }): Promise<DaemonStartResult>;
20
+ export declare function startDaemonLocked(port: number, detached: boolean, options?: DaemonStartOptions): Promise<DaemonStartResult>;
19
21
  /** Caller must hold the daemon start lock. */
20
22
  export declare function stopDaemonLocked(): Promise<DaemonStopResult>;
21
23
  export declare function stopDaemon(): Promise<DaemonStopResult>;