@git-stunts/git-warp 10.8.0 → 11.3.3

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 (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,350 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { parseArgs as nodeParseArgs } from 'node:util';
4
+
5
+ /** @typedef {import('./types.js').CliOptions} CliOptions */
6
+
7
+ export const EXIT_CODES = {
8
+ OK: 0,
9
+ USAGE: 1,
10
+ NOT_FOUND: 2,
11
+ INTERNAL: 3,
12
+ /** Trust policy denial (enforce mode). */
13
+ TRUST_FAIL: 4,
14
+ /** Valid result but negative (e.g. no path found). Follows grep convention. */
15
+ NO_MATCH: 1,
16
+ };
17
+
18
+ /**
19
+ * Reads an environment variable across Node, Bun, and Deno runtimes.
20
+ * @param {string} name
21
+ * @returns {string|undefined}
22
+ */
23
+ export function getEnvVar(name) {
24
+ if (typeof process !== 'undefined' && process.env) {
25
+ return process.env[name];
26
+ }
27
+ if (typeof Deno !== 'undefined') {
28
+ // eslint-disable-next-line no-undef
29
+ try { return Deno.env.get(name); } catch { return undefined; }
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ export const HELP_TEXT = `warp-graph <command> [options]
35
+ (or: git warp <command> [options])
36
+
37
+ Commands:
38
+ info Summarize graphs in the repo
39
+ query Run a logical graph query
40
+ path Find a logical path between two nodes
41
+ history Show writer history
42
+ check Report graph health/GC status
43
+ doctor Diagnose structural issues and suggest fixes
44
+ verify-audit Verify audit receipt chain integrity
45
+ trust Evaluate writer trust from signed evidence
46
+ materialize Materialize and checkpoint all graphs
47
+ seek Time-travel: step through graph history by Lamport tick
48
+ patch Decode and inspect raw patches
49
+ tree ASCII tree traversal from root nodes
50
+ view Interactive TUI graph browser (requires @git-stunts/git-warp-tui)
51
+ install-hooks Install post-merge git hook
52
+
53
+ Options:
54
+ --repo <path> Path to git repo (default: cwd)
55
+ --json Emit JSON output (pretty-printed, sorted keys)
56
+ --ndjson Emit compact single-line JSON (for piping/scripting)
57
+ --view [mode] Visual output (ascii, browser, svg:FILE, html:FILE)
58
+ --graph <name> Graph name (required if repo has multiple graphs)
59
+ --writer <id> Writer id (default: cli)
60
+ -h, --help Show this help
61
+
62
+ Install-hooks options:
63
+ --force Replace existing hook (backs up original)
64
+
65
+ Query options:
66
+ --match <glob> Match node ids (default: *)
67
+ --outgoing [label] Traverse outgoing edge (repeatable)
68
+ --incoming [label] Traverse incoming edge (repeatable)
69
+ --where-prop k=v Filter nodes by prop equality (repeatable)
70
+ --select <fields> Fields to select (id, props)
71
+
72
+ Path options:
73
+ --from <id> Start node id
74
+ --to <id> End node id
75
+ --dir <out|in|both> Traversal direction (default: out)
76
+ --label <label> Filter by edge label (repeatable, comma-separated)
77
+ --max-depth <n> Maximum depth
78
+
79
+ History options:
80
+ --node <id> Filter patches touching node id
81
+
82
+ Doctor options:
83
+ --strict Treat warnings as failures (exit 4)
84
+
85
+ Verify-audit options:
86
+ --writer <id> Verify a single writer's chain (default: all)
87
+ --since <commit> Verify from tip down to this commit (inclusive)
88
+ --trust-mode <mode> Trust evaluation mode (warn, enforce)
89
+ --trust-pin <sha> Pin trust evaluation to a specific record chain commit
90
+
91
+ Trust options:
92
+ --mode <warn|enforce> Override trust evaluation mode
93
+ --trust-pin <sha> Pin trust evaluation to a specific record chain commit
94
+
95
+ Seek options:
96
+ --tick <N|+N|-N> Jump to tick N, or step forward/backward
97
+ --latest Clear cursor, return to present
98
+ --save <name> Save current position as named cursor
99
+ --load <name> Restore a saved cursor
100
+ --list List all saved cursors
101
+ --drop <name> Delete a saved cursor
102
+ --diff Show structural diff (added/removed nodes, edges, props)
103
+ --diff-limit <N> Max diff entries (default 2000)
104
+
105
+ Patch options:
106
+ show <sha> Decode and display a single patch as JSON
107
+ list List all patches sorted by Lamport clock
108
+ --writer <id> Filter by writer (list only)
109
+ --limit <n> Max entries to show (list only)
110
+
111
+ Tree options:
112
+ [rootNode] Root node id (auto-detected if omitted)
113
+ --edge <label> Follow only this edge label
114
+ --prop <key> Annotate nodes with this property (repeatable)
115
+ --max-depth <n> Maximum traversal depth
116
+ `;
117
+
118
+ /**
119
+ * Structured CLI error with exit code and error code.
120
+ */
121
+ export class CliError extends Error {
122
+ /**
123
+ * @param {string} message - Human-readable error message
124
+ * @param {Object} [options]
125
+ * @param {string} [options.code='E_CLI'] - Machine-readable error code
126
+ * @param {number} [options.exitCode=3] - Process exit code
127
+ * @param {Error} [options.cause] - Underlying cause
128
+ */
129
+ constructor(message, { code = 'E_CLI', exitCode = EXIT_CODES.INTERNAL, cause } = {}) {
130
+ super(message);
131
+ this.code = code;
132
+ this.exitCode = exitCode;
133
+ this.cause = cause;
134
+ }
135
+ }
136
+
137
+ /** @param {string} message */
138
+ export function usageError(message) {
139
+ return new CliError(message, { code: 'E_USAGE', exitCode: EXIT_CODES.USAGE });
140
+ }
141
+
142
+ /** @param {string} message */
143
+ export function notFoundError(message) {
144
+ return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
145
+ }
146
+
147
+ export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'patch', 'tree', 'install-hooks', 'view'];
148
+
149
+ const BASE_OPTIONS = {
150
+ repo: { type: 'string', short: 'r' },
151
+ json: { type: 'boolean', default: false },
152
+ ndjson: { type: 'boolean', default: false },
153
+ view: { type: 'string' },
154
+ graph: { type: 'string' },
155
+ writer: { type: 'string', default: 'cli' },
156
+ help: { type: 'boolean', short: 'h', default: false },
157
+ };
158
+
159
+ /**
160
+ * Pre-processes argv to handle --view's optional-value semantics.
161
+ * If --view is followed by a command name or flag (or is last), injects 'ascii'.
162
+ * Validates the view mode value.
163
+ * @param {string[]} argv
164
+ * @returns {string[]}
165
+ */
166
+ function preprocessView(argv) {
167
+ const idx = argv.indexOf('--view');
168
+ if (idx === -1) {
169
+ return argv;
170
+ }
171
+ const next = argv[idx + 1];
172
+ const needsDefault = !next || next.startsWith('-') || KNOWN_COMMANDS.includes(next);
173
+ if (needsDefault) {
174
+ return [...argv.slice(0, idx + 1), 'ascii', ...argv.slice(idx + 1)];
175
+ }
176
+ const validModes = ['ascii', 'browser'];
177
+ const validPrefixes = ['svg:', 'html:'];
178
+ const isValid = validModes.includes(next) ||
179
+ validPrefixes.some((prefix) => next.startsWith(prefix));
180
+ if (!isValid) {
181
+ throw usageError(`Invalid view mode: ${next}. Valid modes: ascii, browser, svg:FILE, html:FILE`);
182
+ }
183
+ return argv;
184
+ }
185
+
186
+ /** String flags that always consume a value argument */
187
+ const BASE_STRING_FLAGS = new Set(['--repo', '-r', '--graph', '--writer']);
188
+ /** Boolean flags (no value) */
189
+ const BASE_BOOL_FLAGS = new Set(['--json', '--ndjson', '--help', '-h']);
190
+
191
+ /**
192
+ * Checks if a value looks like it belongs to --view (not a flag or command).
193
+ * @param {string|undefined} next
194
+ * @returns {boolean}
195
+ */
196
+ function isViewValue(next) {
197
+ if (!next || next.startsWith('-') || KNOWN_COMMANDS.includes(next)) {
198
+ return false;
199
+ }
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Extracts base flags from anywhere in argv, leaving command + commandArgs.
205
+ *
206
+ * Base flags (--repo, --graph, --writer, --view, --json, --ndjson, --help)
207
+ * can appear before or after the command. Everything else (unknown flags,
208
+ * positionals after the command) becomes commandArgs.
209
+ *
210
+ * @param {string[]} argv
211
+ * @returns {{baseArgs: string[], command: string|undefined, commandArgs: string[]}}
212
+ */
213
+ function extractBaseArgs(argv) {
214
+ /** @type {string[]} */
215
+ const baseArgs = [];
216
+ /** @type {string[]} */
217
+ const rest = [];
218
+ /** @type {string|undefined} */
219
+ let command;
220
+ let pastCommand = false;
221
+
222
+ for (let i = 0; i < argv.length; i++) {
223
+ const arg = argv[i];
224
+
225
+ if (arg === '--') {
226
+ rest.push(...argv.slice(i + 1));
227
+ break;
228
+ }
229
+
230
+ if (BASE_STRING_FLAGS.has(arg)) {
231
+ baseArgs.push(arg);
232
+ if (i + 1 < argv.length) {
233
+ baseArgs.push(argv[++i]);
234
+ }
235
+ continue;
236
+ }
237
+
238
+ // Handle --flag=value form for string flags
239
+ if (arg.startsWith('--') && BASE_STRING_FLAGS.has(arg.split('=')[0])) {
240
+ baseArgs.push(arg);
241
+ continue;
242
+ }
243
+
244
+ // --view has optional-value semantics: consume next only if it looks like a view mode
245
+ if (arg === '--view') {
246
+ baseArgs.push(arg);
247
+ if (isViewValue(argv[i + 1])) {
248
+ baseArgs.push(argv[++i]);
249
+ }
250
+ continue;
251
+ }
252
+
253
+ if (arg.startsWith('--view=')) {
254
+ baseArgs.push(arg);
255
+ continue;
256
+ }
257
+
258
+ if (BASE_BOOL_FLAGS.has(arg)) {
259
+ baseArgs.push(arg);
260
+ continue;
261
+ }
262
+
263
+ if (!pastCommand && !arg.startsWith('-')) {
264
+ command = arg;
265
+ pastCommand = true;
266
+ continue;
267
+ }
268
+
269
+ rest.push(arg);
270
+ }
271
+
272
+ return { baseArgs, command, commandArgs: rest };
273
+ }
274
+
275
+ /**
276
+ * Two-pass arg parser using node:util.parseArgs.
277
+ *
278
+ * Pass 1: extract base flags from anywhere in argv.
279
+ * Pass 2: pre-process --view (optional-value semantics) on base args.
280
+ * Pass 3: parseArgs with strict:true on base args only.
281
+ *
282
+ * @param {string[]} argv
283
+ * @returns {{options: CliOptions, command: string|undefined, commandArgs: string[]}}
284
+ */
285
+ export function parseArgs(argv) {
286
+ const { baseArgs, command, commandArgs } = extractBaseArgs(argv);
287
+ const processed = preprocessView(baseArgs);
288
+
289
+ /** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
290
+ let parsed;
291
+ try {
292
+ parsed = nodeParseArgs({
293
+ args: processed,
294
+ options: /** @type {import('node:util').ParseArgsConfig['options']} */ (BASE_OPTIONS),
295
+ strict: true,
296
+ allowPositionals: false,
297
+ });
298
+ } catch (err) {
299
+ throw usageError(err instanceof Error ? err.message : String(err));
300
+ }
301
+
302
+ const { values } = parsed;
303
+
304
+ /** @type {CliOptions} */
305
+ const options = {
306
+ repo: path.resolve(typeof values.repo === 'string' ? values.repo : process.cwd()),
307
+ json: Boolean(values.json),
308
+ ndjson: Boolean(values.ndjson),
309
+ view: typeof values.view === 'string' ? values.view : null,
310
+ graph: typeof values.graph === 'string' ? values.graph : null,
311
+ writer: typeof values.writer === 'string' ? values.writer : 'cli',
312
+ help: Boolean(values.help),
313
+ };
314
+
315
+ return { options, command, commandArgs };
316
+ }
317
+
318
+ /**
319
+ * Parses command-level args using node:util.parseArgs + Zod validation.
320
+ *
321
+ * @param {string[]} args - Command-specific args (after command name)
322
+ * @param {Object} config - parseArgs options config
323
+ * @param {import('zod').ZodType} schema - Zod schema to validate/transform parsed values
324
+ * @param {Object} [opts]
325
+ * @param {boolean} [opts.allowPositionals=false] - Whether to allow positional arguments
326
+ * @returns {{values: *, positionals: string[]}}
327
+ */
328
+ export function parseCommandArgs(args, config, schema, { allowPositionals = false } = {}) {
329
+ /** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
330
+ let parsed;
331
+ try {
332
+ parsed = nodeParseArgs({
333
+ args,
334
+ options: /** @type {import('node:util').ParseArgsConfig['options']} */ (config),
335
+ strict: true,
336
+ allowPositionals,
337
+ });
338
+ } catch (err) {
339
+ throw usageError(err instanceof Error ? err.message : String(err));
340
+ }
341
+
342
+ const result = schema.safeParse(parsed.values);
343
+ if (!result.success) {
344
+ const msg = result.error.issues.map((/** @type {{message: string}} */ issue) => issue.message).join('; ');
345
+ throw usageError(msg);
346
+ }
347
+
348
+ return { values: result.data, positionals: parsed.positionals || [] };
349
+ }
350
+
@@ -0,0 +1,177 @@
1
+ import { z } from 'zod';
2
+
3
+ // ============================================================================
4
+ // History
5
+ // ============================================================================
6
+
7
+ export const historySchema = z.object({
8
+ node: z.string().optional(),
9
+ }).strict();
10
+
11
+ // ============================================================================
12
+ // Install-hooks
13
+ // ============================================================================
14
+
15
+ export const installHooksSchema = z.object({
16
+ force: z.boolean().default(false),
17
+ }).strict();
18
+
19
+ // ============================================================================
20
+ // Verify-audit
21
+ // ============================================================================
22
+
23
+ export const verifyAuditSchema = z.object({
24
+ since: z.string().min(1, 'Missing value for --since').optional(),
25
+ writer: z.string().min(1, 'Missing value for --writer').optional(),
26
+ 'trust-mode': z.enum(['warn', 'enforce']).optional(),
27
+ 'trust-pin': z.string().min(1, 'Missing value for --trust-pin').optional(),
28
+ }).strict();
29
+
30
+ // ============================================================================
31
+ // Path
32
+ // ============================================================================
33
+
34
+ export const pathSchema = z.object({
35
+ from: z.string().optional(),
36
+ to: z.string().optional(),
37
+ dir: z.enum(['out', 'in', 'both']).optional(),
38
+ label: z.union([z.string(), z.array(z.string())]).optional(),
39
+ 'max-depth': z.coerce.number().int().nonnegative().optional(),
40
+ }).strict().transform((val) => ({
41
+ from: val.from ?? null,
42
+ to: val.to ?? null,
43
+ dir: val.dir,
44
+ labels: Array.isArray(val.label) ? val.label : val.label ? [val.label] : [],
45
+ maxDepth: val['max-depth'],
46
+ }));
47
+
48
+ // ============================================================================
49
+ // Query
50
+ // ============================================================================
51
+
52
+ export const querySchema = z.object({
53
+ match: z.string().optional(),
54
+ 'where-prop': z.union([z.string(), z.array(z.string())]).optional(),
55
+ select: z.string().optional(),
56
+ }).strict().transform((val) => ({
57
+ match: val.match ?? null,
58
+ whereProp: Array.isArray(val['where-prop']) ? val['where-prop'] : val['where-prop'] ? [val['where-prop']] : [],
59
+ select: val.select,
60
+ }));
61
+
62
+ // ============================================================================
63
+ // View
64
+ // ============================================================================
65
+
66
+ export const viewSchema = z.object({
67
+ list: z.boolean().default(false),
68
+ log: z.boolean().default(false),
69
+ }).strict();
70
+
71
+ // ============================================================================
72
+ // Trust
73
+ // ============================================================================
74
+
75
+ export const trustSchema = z.object({
76
+ mode: z.enum(['warn', 'enforce']).optional(),
77
+ 'trust-pin': z.string().min(1, 'Missing value for --trust-pin').optional(),
78
+ }).strict().transform((val) => ({
79
+ mode: val.mode ?? null,
80
+ trustPin: val['trust-pin'] ?? null,
81
+ }));
82
+
83
+ // ============================================================================
84
+ // Doctor
85
+ // ============================================================================
86
+
87
+ export const doctorSchema = z.object({
88
+ strict: z.boolean().default(false),
89
+ }).strict();
90
+
91
+ // ============================================================================
92
+ // Seek
93
+ // ============================================================================
94
+
95
+ export const seekSchema = z.object({
96
+ tick: z.string().optional(),
97
+ latest: z.boolean().default(false),
98
+ save: z.string().min(1, 'Missing value for --save').optional(),
99
+ load: z.string().min(1, 'Missing value for --load').optional(),
100
+ list: z.boolean().default(false),
101
+ drop: z.string().min(1, 'Missing value for --drop').optional(),
102
+ 'clear-cache': z.boolean().default(false),
103
+ 'no-persistent-cache': z.boolean().default(false),
104
+ diff: z.boolean().default(false),
105
+ 'diff-limit': z.coerce.number().int({ message: '--diff-limit must be a positive integer' }).positive({ message: '--diff-limit must be a positive integer' }).default(2000),
106
+ }).strict().superRefine((val, ctx) => {
107
+ // Count mutually exclusive action flags
108
+ const actions = [
109
+ val.tick !== undefined,
110
+ val.latest,
111
+ val.save !== undefined,
112
+ val.load !== undefined,
113
+ val.list,
114
+ val.drop !== undefined,
115
+ val['clear-cache'],
116
+ ].filter(Boolean);
117
+
118
+ if (actions.length > 1) {
119
+ ctx.addIssue({
120
+ code: z.ZodIssueCode.custom,
121
+ message: 'Only one seek action flag allowed at a time (--tick, --latest, --save, --load, --list, --drop, --clear-cache)',
122
+ });
123
+ }
124
+
125
+ // --diff only with tick/latest/load
126
+ const DIFF_ACTIONS = val.tick !== undefined || val.latest || val.load !== undefined;
127
+ if (val.diff && !DIFF_ACTIONS) {
128
+ ctx.addIssue({
129
+ code: z.ZodIssueCode.custom,
130
+ message: '--diff cannot be used without --tick, --latest, or --load',
131
+ });
132
+ }
133
+
134
+ // --diff-limit requires --diff
135
+ if (val['diff-limit'] !== 2000 && !val.diff) {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ message: '--diff-limit requires --diff',
139
+ });
140
+ }
141
+ }).transform((val) => {
142
+ /** @type {string} */
143
+ let action = 'status';
144
+ /** @type {string|null} */
145
+ let tickValue = null;
146
+ /** @type {string|null} */
147
+ let name = null;
148
+
149
+ if (val.tick !== undefined) {
150
+ action = 'tick';
151
+ tickValue = val.tick;
152
+ } else if (val.latest) {
153
+ action = 'latest';
154
+ } else if (val.save !== undefined) {
155
+ action = 'save';
156
+ name = val.save;
157
+ } else if (val.load !== undefined) {
158
+ action = 'load';
159
+ name = val.load;
160
+ } else if (val.list) {
161
+ action = 'list';
162
+ } else if (val.drop !== undefined) {
163
+ action = 'drop';
164
+ name = val.drop;
165
+ } else if (val['clear-cache']) {
166
+ action = 'clear-cache';
167
+ }
168
+
169
+ return {
170
+ action,
171
+ tickValue,
172
+ name,
173
+ noPersistentCache: val['no-persistent-cache'],
174
+ diff: val.diff,
175
+ diffLimit: val['diff-limit'],
176
+ };
177
+ });