quilltap 4.9.0-dev → 4.9.0-dev.100
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 +5 -1
- package/bin/quilltap.js +42 -18
- package/lib/__tests__/completion-behavior.test.js +162 -0
- package/lib/__tests__/completion-coverage.test.js +95 -0
- package/lib/__tests__/dbkey-restore.test.js +162 -0
- package/lib/completion/bash.template +118 -26
- package/lib/completion/fish.template +18 -2
- package/lib/completion/zsh.template +340 -100
- package/lib/db-helpers.js +9 -32
- package/lib/dbkey-restore.js +347 -0
- package/lib/dbkey.js +188 -0
- package/lib/instances-commands.js +48 -0
- package/lib/instances.js +6 -30
- package/lib/lock-helpers.js +2 -4
- package/package.json +1 -1
|
@@ -2,12 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# Zsh completion for quilltap
|
|
4
4
|
|
|
5
|
+
# Position is worked out by _arguments, never by counting words. A hard-coded
|
|
6
|
+
# `CURRENT == 2` test means the verb only completes when it sits immediately
|
|
7
|
+
# after the subcommand, so `quilltap docs --instance Friday <TAB>` offers
|
|
8
|
+
# nothing at all. Every function below hands its option list *and* its
|
|
9
|
+
# positional specs to one `_arguments -C` call and branches on $state, so flags
|
|
10
|
+
# may appear anywhere the CLI itself accepts them.
|
|
11
|
+
|
|
5
12
|
local ret=1
|
|
6
13
|
|
|
7
14
|
_quilltap() {
|
|
8
15
|
local -a subcommands
|
|
9
16
|
local -a global_options
|
|
10
17
|
|
|
18
|
+
# The whole command line, kept before _arguments rebases `words` for the
|
|
19
|
+
# subcommand dispatch. The lookup helpers read it so that an --instance
|
|
20
|
+
# typed anywhere picks which database they query.
|
|
21
|
+
local -a _quilltap_line
|
|
22
|
+
local -i _quilltap_cword
|
|
23
|
+
_quilltap_line=("${words[@]}")
|
|
24
|
+
_quilltap_cword=$CURRENT
|
|
25
|
+
|
|
11
26
|
global_options=(
|
|
12
27
|
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
13
28
|
'(-i --instance)'{-i,--instance}'[Use a registered instance]:instance:_quilltap_instance_names'
|
|
@@ -34,10 +49,14 @@ _quilltap() {
|
|
|
34
49
|
'completion:Generate shell completion scripts'
|
|
35
50
|
)
|
|
36
51
|
|
|
52
|
+
# (-) on both positionals stops the top-level _arguments from swallowing a
|
|
53
|
+
# flag typed *after* the subcommand: without it `quilltap docs --instance
|
|
54
|
+
# Friday <TAB>` parses --instance as a global option, leaving the rest-arg
|
|
55
|
+
# array empty and the subcommand dispatch with nothing to dispatch on.
|
|
37
56
|
_arguments -C \
|
|
38
|
-
'1: :->subcommand' \
|
|
39
57
|
"${global_options[@]}" \
|
|
40
|
-
'
|
|
58
|
+
'(-): :->subcommand' \
|
|
59
|
+
'(-)*::arg:->args'
|
|
41
60
|
|
|
42
61
|
case "$state" in
|
|
43
62
|
subcommand)
|
|
@@ -74,7 +93,7 @@ _quilltap_subcommand() {
|
|
|
74
93
|
_quilltap_memory_diff
|
|
75
94
|
;;
|
|
76
95
|
recall-replay)
|
|
77
|
-
|
|
96
|
+
_quilltap_recall_replay
|
|
78
97
|
;;
|
|
79
98
|
logs)
|
|
80
99
|
_quilltap_logs
|
|
@@ -86,7 +105,7 @@ _quilltap_subcommand() {
|
|
|
86
105
|
_quilltap_maintenance
|
|
87
106
|
;;
|
|
88
107
|
file-verify)
|
|
89
|
-
|
|
108
|
+
_quilltap_file_verify
|
|
90
109
|
;;
|
|
91
110
|
completion)
|
|
92
111
|
_quilltap_completion
|
|
@@ -95,7 +114,10 @@ _quilltap_subcommand() {
|
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
_quilltap_db() {
|
|
117
|
+
local curcontext="$curcontext" state line
|
|
118
|
+
typeset -A opt_args
|
|
98
119
|
local -a subverbs db_opts
|
|
120
|
+
|
|
99
121
|
subverbs=(
|
|
100
122
|
'schema:Show database schema'
|
|
101
123
|
'find:Find entities by name'
|
|
@@ -147,46 +169,74 @@ _quilltap_db() {
|
|
|
147
169
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
148
170
|
)
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
# (-) again: a flag typed after the verb belongs to the verb's own
|
|
173
|
+
# _arguments call below, not to this one.
|
|
174
|
+
_arguments -C $db_opts \
|
|
175
|
+
'(-): :->verb' \
|
|
176
|
+
'(-)*::arg:->rest'
|
|
154
177
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'--id[Single character by name or id]:character:'
|
|
173
|
-
'--write[Perform the archive/rehydrate write]'
|
|
174
|
-
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
175
|
-
'--out[Output .qtap path]:path:_files'
|
|
176
|
-
'(-h --help)'{-h,--help}'[Show help]'
|
|
177
|
-
)
|
|
178
|
-
if (( CURRENT == 3 )); then
|
|
179
|
-
_describe 'characters subcommand' char_verbs
|
|
180
|
-
fi
|
|
181
|
-
_arguments $char_opts
|
|
182
|
-
return
|
|
183
|
-
fi
|
|
178
|
+
case "$state" in
|
|
179
|
+
verb)
|
|
180
|
+
_describe -t commands 'db subcommand' subverbs
|
|
181
|
+
;;
|
|
182
|
+
rest)
|
|
183
|
+
case "$line[1]" in
|
|
184
|
+
characters)
|
|
185
|
+
_quilltap_db_characters
|
|
186
|
+
;;
|
|
187
|
+
*)
|
|
188
|
+
# Nothing to complete positionally, but the flags still apply.
|
|
189
|
+
_arguments $db_opts
|
|
190
|
+
;;
|
|
191
|
+
esac
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
}
|
|
184
195
|
|
|
185
|
-
|
|
196
|
+
_quilltap_db_characters() {
|
|
197
|
+
local curcontext="$curcontext" state line
|
|
198
|
+
typeset -A opt_args
|
|
199
|
+
local -a char_verbs char_opts
|
|
200
|
+
|
|
201
|
+
char_verbs=(
|
|
202
|
+
'status:Per-character vault status report'
|
|
203
|
+
'archives:List archived characters and ARCHIVE bundles'
|
|
204
|
+
'archive:Archive a character (runs through the server)'
|
|
205
|
+
'rehydrate:Wake an archived character (runs through the server)'
|
|
206
|
+
'export:Write a plaintext .qtap for a character'
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
char_opts=(
|
|
210
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
211
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
212
|
+
'--passphrase[Database passphrase]:passphrase:'
|
|
213
|
+
'--json[JSON output]'
|
|
214
|
+
'--limit[Result limit]:limit:'
|
|
215
|
+
'--diverged[Only characters whose DB and vault differ]'
|
|
216
|
+
'--blocked[Only characters with vault issues]'
|
|
217
|
+
'--id[Single character by name or id]:character:'
|
|
218
|
+
'--write[Perform the archive/rehydrate write]'
|
|
219
|
+
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
220
|
+
'--out[Output .qtap path]:path:_files'
|
|
221
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
_arguments -C $char_opts \
|
|
225
|
+
'1: :->cverb' \
|
|
226
|
+
'*: :'
|
|
227
|
+
|
|
228
|
+
case "$state" in
|
|
229
|
+
cverb)
|
|
230
|
+
_describe -t commands 'characters subcommand' char_verbs
|
|
231
|
+
;;
|
|
232
|
+
esac
|
|
186
233
|
}
|
|
187
234
|
|
|
188
235
|
_quilltap_docs() {
|
|
236
|
+
local curcontext="$curcontext" state line
|
|
237
|
+
typeset -A opt_args
|
|
189
238
|
local -a subverbs docs_opts
|
|
239
|
+
|
|
190
240
|
subverbs=(
|
|
191
241
|
'list:List all mount points'
|
|
192
242
|
'show:Details for one mount point'
|
|
@@ -219,10 +269,12 @@ _quilltap_docs() {
|
|
|
219
269
|
'--passphrase[Database passphrase]:passphrase:'
|
|
220
270
|
'(-p --port)'{-p,--port}'[Server port]:port:'
|
|
221
271
|
'--json[JSON output]'
|
|
222
|
-
'--
|
|
272
|
+
'--uri[Show canonical qtap:// URIs as the locator]'
|
|
273
|
+
'--mount[Mount name or id]:mount:_quilltap_mount_names'
|
|
223
274
|
'--folder[Folder filter]:folder:'
|
|
224
275
|
'--force[Force operation]'
|
|
225
276
|
'--rendered[Render rich content]'
|
|
277
|
+
'--base64[Base64 transfer for binary files]'
|
|
226
278
|
'--links[Include link details]'
|
|
227
279
|
'--type[Filter by type]:type:(file folder)'
|
|
228
280
|
'--ext[Extension filter]:ext:'
|
|
@@ -241,17 +293,65 @@ _quilltap_docs() {
|
|
|
241
293
|
'--max-nodes[Maximum nodes in graph]:n:'
|
|
242
294
|
'--long[Long-form output]'
|
|
243
295
|
'--semantic[Use semantic search]'
|
|
296
|
+
'--format[For docker-mounts: output shape]:format:(args json)'
|
|
244
297
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
245
298
|
)
|
|
246
299
|
|
|
247
|
-
|
|
248
|
-
|
|
300
|
+
_arguments -C $docs_opts \
|
|
301
|
+
'1: :->verb' \
|
|
302
|
+
'2: :->pos2' \
|
|
303
|
+
'3: :->pos3' \
|
|
304
|
+
'4: :->pos4' \
|
|
305
|
+
'*: :'
|
|
306
|
+
|
|
307
|
+
case "$state" in
|
|
308
|
+
verb)
|
|
309
|
+
_describe -t commands 'docs subcommand' subverbs
|
|
310
|
+
;;
|
|
311
|
+
pos2|pos3|pos4)
|
|
312
|
+
_quilltap_docs_positional ${state#pos}
|
|
313
|
+
;;
|
|
314
|
+
esac
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Which docs positionals name a document store, and which name a local path.
|
|
318
|
+
# `move`/`copy`/`link` take <srcMount> <srcPath> <dstMount> <dstPath>, so a
|
|
319
|
+
# store is wanted at both 2 and 4; everything else that takes a store takes it
|
|
320
|
+
# first. `$line` comes from the caller's _arguments -C.
|
|
321
|
+
_quilltap_docs_positional() {
|
|
322
|
+
local -i n=$1
|
|
323
|
+
local verb="$line[1]"
|
|
324
|
+
local -a store_verbs
|
|
325
|
+
store_verbs=(
|
|
326
|
+
show files ls dir tree read export scan reindex embed
|
|
327
|
+
write delete mkdir rmdir mvdir move copy link
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if (( n == 2 )) && (( ${store_verbs[(I)$verb]} )); then
|
|
331
|
+
_quilltap_mount_names
|
|
332
|
+
return
|
|
249
333
|
fi
|
|
250
|
-
|
|
334
|
+
|
|
335
|
+
case "$verb" in
|
|
336
|
+
move|copy|link)
|
|
337
|
+
(( n == 4 )) && _quilltap_mount_names
|
|
338
|
+
;;
|
|
339
|
+
export)
|
|
340
|
+
# `docs export <mount> <outputDir>` — the second one is a local directory.
|
|
341
|
+
(( n == 3 )) && _directories
|
|
342
|
+
;;
|
|
343
|
+
write)
|
|
344
|
+
# `docs write <mount> <path> [file]` — the optional source is local.
|
|
345
|
+
(( n == 4 )) && _files
|
|
346
|
+
;;
|
|
347
|
+
esac
|
|
251
348
|
}
|
|
252
349
|
|
|
253
350
|
_quilltap_themes() {
|
|
254
|
-
local
|
|
351
|
+
local curcontext="$curcontext" state line
|
|
352
|
+
typeset -A opt_args
|
|
353
|
+
local -a subverbs themes_opts
|
|
354
|
+
|
|
255
355
|
subverbs=(
|
|
256
356
|
'list:List available themes'
|
|
257
357
|
'install:Install a theme'
|
|
@@ -271,37 +371,70 @@ _quilltap_themes() {
|
|
|
271
371
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
272
372
|
)
|
|
273
373
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
374
|
+
# (-) again: a flag typed after the verb belongs to the verb's own
|
|
375
|
+
# _arguments call below, not to this one.
|
|
376
|
+
_arguments -C $themes_opts \
|
|
377
|
+
'(-): :->verb' \
|
|
378
|
+
'(-)*::arg:->rest'
|
|
278
379
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
'
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
380
|
+
case "$state" in
|
|
381
|
+
verb)
|
|
382
|
+
_describe -t commands 'themes subcommand' subverbs
|
|
383
|
+
;;
|
|
384
|
+
rest)
|
|
385
|
+
case "$line[1]" in
|
|
386
|
+
registry)
|
|
387
|
+
_quilltap_themes_registry
|
|
388
|
+
;;
|
|
389
|
+
*)
|
|
390
|
+
_arguments $themes_opts
|
|
391
|
+
;;
|
|
392
|
+
esac
|
|
393
|
+
;;
|
|
394
|
+
esac
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_quilltap_themes_registry() {
|
|
398
|
+
local curcontext="$curcontext" state line
|
|
399
|
+
typeset -A opt_args
|
|
400
|
+
local -a registry_verbs registry_opts
|
|
401
|
+
|
|
402
|
+
registry_verbs=(
|
|
403
|
+
'list:List registries'
|
|
404
|
+
'add:Add a registry'
|
|
405
|
+
'remove:Remove a registry'
|
|
406
|
+
'refresh:Refresh registries'
|
|
407
|
+
'keygen:Generate Ed25519 key'
|
|
408
|
+
'sign:Sign a registry or bundle'
|
|
409
|
+
)
|
|
299
410
|
|
|
300
|
-
|
|
411
|
+
# themesCommand() parses -d/-i/-o/-h anywhere in the argv, including after
|
|
412
|
+
# `registry`, so they belong in this list too.
|
|
413
|
+
registry_opts=(
|
|
414
|
+
'(-k --key)'{-k,--key}'[Signing key path]:path:_files'
|
|
415
|
+
'(-n --name)'{-n,--name}'[Registry name]:name:'
|
|
416
|
+
'(-o --output)'{-o,--output}'[Output path]:path:_files'
|
|
417
|
+
'(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
|
|
418
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
419
|
+
'(-h --help)'{-h,--help}'[Show help]'
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
_arguments -C $registry_opts \
|
|
423
|
+
'1: :->rverb' \
|
|
424
|
+
'*: :'
|
|
425
|
+
|
|
426
|
+
case "$state" in
|
|
427
|
+
rverb)
|
|
428
|
+
_describe -t commands 'registry subcommand' registry_verbs
|
|
429
|
+
;;
|
|
430
|
+
esac
|
|
301
431
|
}
|
|
302
432
|
|
|
303
433
|
_quilltap_instances() {
|
|
434
|
+
local curcontext="$curcontext" state line
|
|
435
|
+
typeset -A opt_args
|
|
304
436
|
local -a subverbs inst_opts
|
|
437
|
+
|
|
305
438
|
subverbs=(
|
|
306
439
|
'list:List registered instances'
|
|
307
440
|
'ls:List registered instances'
|
|
@@ -317,34 +450,46 @@ _quilltap_instances() {
|
|
|
317
450
|
'passphrase:Set instance passphrase'
|
|
318
451
|
'default:Set/show/clear the default instance'
|
|
319
452
|
'rename:Rename an instance (preserves passphrase)'
|
|
453
|
+
'restore-key:Rebuild the .dbkey from the pepper'
|
|
454
|
+
'rebuild-key:Rebuild the .dbkey from the pepper'
|
|
320
455
|
)
|
|
321
456
|
|
|
322
457
|
inst_opts=(
|
|
323
458
|
'--names-only[Print one name per line (for completion)]'
|
|
324
459
|
'--json[JSON output]'
|
|
325
460
|
'--clear[Clear value (for default)]'
|
|
461
|
+
'--passphrase[Passphrase for the rebuilt .dbkey]:passphrase:'
|
|
462
|
+
'--no-passphrase[Rebuild the .dbkey with no passphrase]'
|
|
463
|
+
'(-d --data-dir)'{-d,--data-dir}'[Instance root to rebuild in]:directory:_files -/'
|
|
464
|
+
'--force[Write even with no encrypted database to prove the pepper against]'
|
|
465
|
+
'(-y --yes)'{-y,--yes}'[Skip confirmation prompts]'
|
|
326
466
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
327
467
|
)
|
|
328
468
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
469
|
+
_arguments -C $inst_opts \
|
|
470
|
+
'1: :->verb' \
|
|
471
|
+
'2: :->name' \
|
|
472
|
+
'*: :'
|
|
333
473
|
|
|
334
|
-
case "$
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
474
|
+
case "$state" in
|
|
475
|
+
verb)
|
|
476
|
+
_describe -t commands 'instances subcommand' subverbs
|
|
477
|
+
;;
|
|
478
|
+
name)
|
|
479
|
+
case "$line[1]" in
|
|
480
|
+
show|remove|rm|delete|set-passphrase|passphrase|default|rename|restore-key|rebuild-key)
|
|
481
|
+
_quilltap_instance_names
|
|
482
|
+
;;
|
|
483
|
+
esac
|
|
340
484
|
;;
|
|
341
485
|
esac
|
|
342
|
-
|
|
343
|
-
_arguments $inst_opts
|
|
344
486
|
}
|
|
345
487
|
|
|
346
488
|
_quilltap_memories() {
|
|
489
|
+
local curcontext="$curcontext" state line
|
|
490
|
+
typeset -A opt_args
|
|
347
491
|
local -a subverbs mem_opts
|
|
492
|
+
|
|
348
493
|
subverbs=(
|
|
349
494
|
'ls:List memories'
|
|
350
495
|
'find:Substring search'
|
|
@@ -356,10 +501,10 @@ _quilltap_memories() {
|
|
|
356
501
|
)
|
|
357
502
|
|
|
358
503
|
mem_opts=(
|
|
359
|
-
'
|
|
504
|
+
'--instance[Registered instance name]:instance:_quilltap_instance_names'
|
|
360
505
|
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
|
|
361
506
|
'--passphrase[Database passphrase]:passphrase:'
|
|
362
|
-
'
|
|
507
|
+
'--port[Server port]:port:'
|
|
363
508
|
'--json[JSON output]'
|
|
364
509
|
'--character[Character name or id]:character:'
|
|
365
510
|
'--about[Subject of memory]:character:'
|
|
@@ -379,7 +524,7 @@ _quilltap_memories() {
|
|
|
379
524
|
'--in[Restrict find-in field]:field:'
|
|
380
525
|
'--no-related[Hide related neighbors]'
|
|
381
526
|
'--list[List-only output]'
|
|
382
|
-
'(-i --ignore-case)'{-i,--ignore-case}'[Case-insensitive]'
|
|
527
|
+
'(-i --ignore-case)'{-i,--ignore-case}'[Case-insensitive (memories reserves -i for this, not --instance)]'
|
|
383
528
|
'(-l --paths-only)'{-l,--paths-only}'[Paths only]'
|
|
384
529
|
'--max[Maximum results]:n:'
|
|
385
530
|
'--context[Context lines]:n:'
|
|
@@ -391,10 +536,15 @@ _quilltap_memories() {
|
|
|
391
536
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
392
537
|
)
|
|
393
538
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
539
|
+
_arguments -C $mem_opts \
|
|
540
|
+
'1: :->verb' \
|
|
541
|
+
'*: :'
|
|
542
|
+
|
|
543
|
+
case "$state" in
|
|
544
|
+
verb)
|
|
545
|
+
_describe -t commands 'memories subcommand' subverbs
|
|
546
|
+
;;
|
|
547
|
+
esac
|
|
398
548
|
}
|
|
399
549
|
|
|
400
550
|
_quilltap_memory_diff() {
|
|
@@ -405,7 +555,22 @@ _quilltap_memory_diff() {
|
|
|
405
555
|
'--passphrase[Database passphrase]:passphrase:' \
|
|
406
556
|
'(-p --port)'{-p,--port}'[Server port]:port:' \
|
|
407
557
|
'--concurrency[Concurrency limit]:n:' \
|
|
408
|
-
'--out[Output file]:path:_files'
|
|
558
|
+
'--out[Output file]:path:_files' \
|
|
559
|
+
'*: :'
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
_quilltap_recall_replay() {
|
|
563
|
+
_arguments \
|
|
564
|
+
'(-h --help)'{-h,--help}'[Show help]' \
|
|
565
|
+
'(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
|
|
566
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
|
|
567
|
+
'--passphrase[Database passphrase]:passphrase:' \
|
|
568
|
+
'--turn[interchange to replay]:turn:' \
|
|
569
|
+
'--char[character id]:char:' \
|
|
570
|
+
'--limit[rows per path]:limit:' \
|
|
571
|
+
'--port[server port]:port:' \
|
|
572
|
+
'--json[raw JSON output]' \
|
|
573
|
+
'*: :'
|
|
409
574
|
}
|
|
410
575
|
|
|
411
576
|
_quilltap_logs() {
|
|
@@ -417,11 +582,27 @@ _quilltap_logs() {
|
|
|
417
582
|
'--stream[Which log stream]:stream:(combined error stdout stderr startup)' \
|
|
418
583
|
'--tail[Last N lines (0=full)]:n:' \
|
|
419
584
|
'(-f --follow)'{-f,--follow}'[Stream new lines]' \
|
|
420
|
-
'--grep[Regex filter]:pattern:'
|
|
585
|
+
'--grep[Regex filter]:pattern:' \
|
|
586
|
+
'*: :'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
_quilltap_file_verify() {
|
|
590
|
+
_arguments \
|
|
591
|
+
'(-h --help)'{-h,--help}'[Show help]' \
|
|
592
|
+
'(-i --instance)'{-i,--instance}'[Use instance]:instance:_quilltap_instance_names' \
|
|
593
|
+
'(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories' \
|
|
594
|
+
'--passphrase[Database passphrase]:passphrase:' \
|
|
595
|
+
'--all[read every top-level file, not just dataless ones]' \
|
|
596
|
+
'--stall-ms[per-chunk stall threshold in ms]:ms:' \
|
|
597
|
+
'--json[machine-readable output]' \
|
|
598
|
+
'*: :'
|
|
421
599
|
}
|
|
422
600
|
|
|
423
601
|
_quilltap_migrations() {
|
|
602
|
+
local curcontext="$curcontext" state line
|
|
603
|
+
typeset -A opt_args
|
|
424
604
|
local -a subverbs mig_opts
|
|
605
|
+
|
|
425
606
|
subverbs=(
|
|
426
607
|
'status:Migration status'
|
|
427
608
|
'pending:List pending migrations'
|
|
@@ -437,14 +618,22 @@ _quilltap_migrations() {
|
|
|
437
618
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
438
619
|
)
|
|
439
620
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
621
|
+
_arguments -C $mig_opts \
|
|
622
|
+
'1: :->verb' \
|
|
623
|
+
'*: :'
|
|
624
|
+
|
|
625
|
+
case "$state" in
|
|
626
|
+
verb)
|
|
627
|
+
_describe -t commands 'migrations subcommand' subverbs
|
|
628
|
+
;;
|
|
629
|
+
esac
|
|
444
630
|
}
|
|
445
631
|
|
|
446
632
|
_quilltap_maintenance() {
|
|
633
|
+
local curcontext="$curcontext" state line
|
|
634
|
+
typeset -A opt_args
|
|
447
635
|
local -a subverbs maint_opts
|
|
636
|
+
|
|
448
637
|
subverbs=(
|
|
449
638
|
'status:Show dry-run retention counts'
|
|
450
639
|
'run:Run retention/cleanup sweeps (lock-gated)'
|
|
@@ -458,20 +647,71 @@ _quilltap_maintenance() {
|
|
|
458
647
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
459
648
|
)
|
|
460
649
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
650
|
+
_arguments -C $maint_opts \
|
|
651
|
+
'1: :->verb' \
|
|
652
|
+
'*: :'
|
|
653
|
+
|
|
654
|
+
case "$state" in
|
|
655
|
+
verb)
|
|
656
|
+
_describe -t commands 'maintenance subcommand' subverbs
|
|
657
|
+
;;
|
|
658
|
+
esac
|
|
465
659
|
}
|
|
466
660
|
|
|
467
661
|
_quilltap_completion() {
|
|
468
|
-
|
|
662
|
+
local curcontext="$curcontext" state line
|
|
663
|
+
typeset -A opt_args
|
|
664
|
+
|
|
665
|
+
_arguments -C \
|
|
666
|
+
'(-h --help)'{-h,--help}'[Show help]' \
|
|
667
|
+
'1: :(bash zsh fish)' \
|
|
668
|
+
'*: :'
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# ---------------------------------------------------------------------------
|
|
672
|
+
# Lookup helpers
|
|
673
|
+
#
|
|
674
|
+
# These shell out to quilltap itself, so they have to be told which instance
|
|
675
|
+
# the user is addressing — otherwise `--instance Friday` completes mount names
|
|
676
|
+
# from the default instance. _quilltap_ctx_flags rebuilds the addressing flags
|
|
677
|
+
# from the command line being typed and the helpers pass them straight through.
|
|
678
|
+
# ---------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
_quilltap_ctx_flags() {
|
|
681
|
+
ctx_flags=()
|
|
682
|
+
local -i i=2
|
|
683
|
+
while (( i <= $#_quilltap_line )); do
|
|
684
|
+
case "$_quilltap_line[i]" in
|
|
685
|
+
-i|--instance|-d|--data-dir|--passphrase)
|
|
686
|
+
# The word under the cursor is a half-typed value, not an answer.
|
|
687
|
+
if (( i + 1 <= $#_quilltap_line && i + 1 != _quilltap_cword )) \
|
|
688
|
+
&& [[ -n "$_quilltap_line[i+1]" ]]; then
|
|
689
|
+
ctx_flags+=("$_quilltap_line[i]" "${(Q)_quilltap_line[i+1]}")
|
|
690
|
+
fi
|
|
691
|
+
(( i += 2 ))
|
|
692
|
+
;;
|
|
693
|
+
*)
|
|
694
|
+
(( i += 1 ))
|
|
695
|
+
;;
|
|
696
|
+
esac
|
|
697
|
+
done
|
|
469
698
|
}
|
|
470
699
|
|
|
471
700
|
_quilltap_instance_names() {
|
|
472
|
-
local instances
|
|
701
|
+
local -a instances expl
|
|
473
702
|
instances=(${(f)"$(command quilltap instances list --names-only 2>/dev/null)"})
|
|
474
|
-
|
|
703
|
+
(( $#instances )) || return 1
|
|
704
|
+
# compadd -a, not _values: instance and store names may contain spaces and
|
|
705
|
+
# colons, which _values would chop into value:description pairs.
|
|
706
|
+
_wanted instances expl 'instance' compadd -a instances
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
_quilltap_mount_names() {
|
|
710
|
+
local -a ctx_flags mounts expl
|
|
711
|
+
_quilltap_ctx_flags
|
|
712
|
+
mounts=(${(f)"$(command quilltap docs list --names-only $ctx_flags 2>/dev/null)"})
|
|
713
|
+
(( $#mounts )) || return 1
|
|
714
|
+
_wanted mounts expl 'document store' compadd -a mounts
|
|
475
715
|
}
|
|
476
716
|
|
|
477
717
|
_quilltap "$@"
|
package/lib/db-helpers.js
CHANGED
|
@@ -148,42 +148,19 @@ function promptPassphrase(prompt) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
async function loadDbKey(dataDir, passphrase) {
|
|
151
|
-
const
|
|
152
|
-
const dbkeyPath = path.join(dataDir, 'quilltap.dbkey');
|
|
153
|
-
if (!fs.existsSync(dbkeyPath)) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const data = JSON.parse(fs.readFileSync(dbkeyPath, 'utf8'));
|
|
158
|
-
const INTERNAL_PASSPHRASE = '__quilltap_no_passphrase__';
|
|
159
|
-
|
|
160
|
-
if ('hasPassphrase' in data) {
|
|
161
|
-
delete data.hasPassphrase;
|
|
162
|
-
fs.writeFileSync(dbkeyPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
163
|
-
}
|
|
151
|
+
const { INTERNAL_PASSPHRASE, readDbKeyFile, decryptDbKey, tryDecryptDbKey } = require('./dbkey');
|
|
164
152
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const iv = Buffer.from(data.iv, 'hex');
|
|
169
|
-
const decipher = crypto.createDecipheriv(data.algorithm, new Uint8Array(key), new Uint8Array(iv));
|
|
170
|
-
decipher.setAuthTag(new Uint8Array(Buffer.from(data.authTag, 'hex')));
|
|
171
|
-
let plaintext = decipher.update(data.ciphertext, 'hex', 'utf8');
|
|
172
|
-
plaintext += decipher.final('utf8');
|
|
173
|
-
|
|
174
|
-
const hash = crypto.createHash('sha256').update(plaintext).digest('hex');
|
|
175
|
-
if (hash !== data.pepperHash) {
|
|
176
|
-
throw new Error('Pepper hash mismatch');
|
|
177
|
-
}
|
|
178
|
-
return plaintext;
|
|
153
|
+
const data = readDbKeyFile(dataDir);
|
|
154
|
+
if (!data) {
|
|
155
|
+
return null;
|
|
179
156
|
}
|
|
180
157
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Internal passphrase failed — need user passphrase
|
|
158
|
+
const internal = tryDecryptDbKey(data, INTERNAL_PASSPHRASE);
|
|
159
|
+
if (internal !== null) {
|
|
160
|
+
return internal;
|
|
185
161
|
}
|
|
186
162
|
|
|
163
|
+
// Internal passphrase failed — need a user passphrase.
|
|
187
164
|
if (!passphrase && process.env.QUILLTAP_DB_PASSPHRASE) {
|
|
188
165
|
passphrase = process.env.QUILLTAP_DB_PASSPHRASE;
|
|
189
166
|
}
|
|
@@ -195,7 +172,7 @@ async function loadDbKey(dataDir, passphrase) {
|
|
|
195
172
|
}
|
|
196
173
|
}
|
|
197
174
|
|
|
198
|
-
return
|
|
175
|
+
return decryptDbKey(data, passphrase);
|
|
199
176
|
}
|
|
200
177
|
|
|
201
178
|
function openEncryptedDb(dbPath, pepper, { readonly = true, friendlyName = 'database' } = {}) {
|