@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.
- package/README.md +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- 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
|
+
});
|