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.
@@ -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
- '*::arg:->args'
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
- _arguments '--turn[interchange to replay]:turn:' '--char[character id]:char:' '--limit[rows per path]:limit:' '--port[server port]:port:' '--json[raw JSON output]' '--help[show help]'
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
- _arguments '--all[read every top-level file, not just dataless ones]' '--stall-ms[per-chunk stall threshold in ms]:ms:' '--json[machine-readable output]' '--help[show help]'
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
- if (( CURRENT == 2 )); then
151
- _describe 'db subcommand' subverbs
152
- return
153
- fi
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
- if [[ "$words[2]" == "characters" ]]; then
156
- local -a char_verbs char_opts
157
- char_verbs=(
158
- 'status:Per-character vault status report'
159
- 'archives:List archived characters and ARCHIVE bundles'
160
- 'archive:Archive a character (runs through the server)'
161
- 'rehydrate:Wake an archived character (runs through the server)'
162
- 'export:Write a plaintext .qtap for a character'
163
- )
164
- char_opts=(
165
- '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
166
- '(-d --data-dir)'{-d,--data-dir}'[Data directory]:directory:_directories'
167
- '--passphrase[Database passphrase]:passphrase:'
168
- '--json[JSON output]'
169
- '--limit[Result limit]:limit:'
170
- '--diverged[Only characters whose DB and vault differ]'
171
- '--blocked[Only characters with vault issues]'
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
- _arguments $db_opts
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
- '--mount[Mount name or id]:mount:'
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
- if (( CURRENT == 2 )); then
248
- _describe 'docs subcommand' subverbs
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
- _arguments $docs_opts
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 -a subverbs registry_verbs themes_opts registry_opts
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
- if (( CURRENT == 2 )); then
275
- _describe 'themes subcommand' subverbs
276
- return
277
- fi
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
- if [[ "$words[2]" == "registry" ]]; then
280
- registry_verbs=(
281
- 'list:List registries'
282
- 'add:Add a registry'
283
- 'remove:Remove a registry'
284
- 'refresh:Refresh registries'
285
- 'keygen:Generate Ed25519 key'
286
- 'sign:Sign a registry or bundle'
287
- )
288
- registry_opts=(
289
- '(-k --key)'{-k,--key}'[Signing key path]:path:_files'
290
- '(-n --name)'{-n,--name}'[Registry name]:name:'
291
- '(-o --output)'{-o,--output}'[Output path]:path:_files'
292
- )
293
- if (( CURRENT == 3 )); then
294
- _describe 'registry subcommand' registry_verbs
295
- fi
296
- _arguments $registry_opts
297
- return
298
- fi
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
- _arguments $themes_opts
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
- if (( CURRENT == 2 )); then
330
- _describe 'instances subcommand' subverbs
331
- return
332
- fi
469
+ _arguments -C $inst_opts \
470
+ '1: :->verb' \
471
+ '2: :->name' \
472
+ '*: :'
333
473
 
334
- case "$words[2]" in
335
- show|remove|rm|delete|set-passphrase|passphrase|default|rename)
336
- if (( CURRENT == 3 )); then
337
- _values 'instance' ${(f)"$(command quilltap instances list --names-only 2>/dev/null)"}
338
- return
339
- fi
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
- '(-i --instance)'{-i,--instance}'[Registered instance name]:instance:_quilltap_instance_names'
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
- '(-p --port)'{-p,--port}'[Server port]:port:'
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
- if (( CURRENT == 2 )); then
395
- _describe 'memories subcommand' subverbs
396
- fi
397
- _arguments $mem_opts
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
- if (( CURRENT == 2 )); then
441
- _describe 'migrations subcommand' subverbs
442
- fi
443
- _arguments $mig_opts
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
- if (( CURRENT == 2 )); then
462
- _describe 'maintenance subcommand' subverbs
463
- fi
464
- _arguments $maint_opts
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
- _values 'shell' bash zsh fish
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
- _values 'instance' $instances
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 crypto = require('crypto');
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
- function tryDecrypt(pass) {
166
- const salt = Buffer.from(data.salt, 'hex');
167
- const key = crypto.pbkdf2Sync(pass, new Uint8Array(salt), data.kdfIterations, 32, data.kdfDigest);
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
- try {
182
- return tryDecrypt(INTERNAL_PASSPHRASE);
183
- } catch {
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 tryDecrypt(passphrase);
175
+ return decryptDbKey(data, passphrase);
199
176
  }
200
177
 
201
178
  function openEncryptedDb(dbPath, pepper, { readonly = true, friendlyName = 'database' } = {}) {