quilltap 4.9.0-dev.121 → 4.9.0-dev.134

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 CHANGED
@@ -99,8 +99,11 @@ quilltap instances list # Registered instances
99
99
  quilltap instances default Friday # Make it the fall-through for flag-free runs
100
100
  quilltap instances rename Friday Weekday # Rename, preserving the stored passphrase
101
101
  quilltap instances remove Friday # Unregister
102
+ quilltap instances restore-key Friday # Rebuild a lost or passphrase-locked .dbkey
102
103
  ```
103
104
 
105
+ If an instance's `quilltap.dbkey` goes missing — or its passphrase does — `instances restore-key` rebuilds it. The file only *wraps* the pepper; the pepper itself is the database key, so an operator who kept the one printed at first-run setup can get back in. The pepper is read from `ENCRYPTION_MASTER_PEPPER` or prompted for hidden, never passed as a flag, and it is proved against the encrypted databases on disk before anything is written. Run it with the server down — the command refuses while the instance lock is held. Flags: `--passphrase <pass>` / `--no-passphrase` (the new wrapping), `-d, --data-dir <path>`, `--force`, `-y, --yes`.
106
+
104
107
  Every subcommand then accepts `--instance <name>` in place of `--data-dir`. The registry lives at `<app-support>/Quilltap/instances.json` (mode 0600; e.g. `~/Library/Application Support/Quilltap/instances.json` on macOS). **Resolution precedence:** `--data-dir` > `--instance` > registered default > `QUILLTAP_DATA_DIR` > the OS platform default. Pass the **instance root** (e.g. `~/iCloud/Quilltap/Friday`), not its `data/` subdirectory.
105
108
 
106
109
  ## Database Tool
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Bug 120 — `quilltap instances default --json`.
3
+ *
4
+ * `cmdDefault` takes BOTH an options object and a positional instance name, so
5
+ * the dispatcher has to strip `--json` as well as read it. Reading without
6
+ * stripping leaves `args.length === 1`, which sends control past the report
7
+ * branch and into the set branch, where the flag becomes the instance name to
8
+ * set. `cmdList`, whose shape this arm was copied from, has no positionals and
9
+ * so needs no filter — which is exactly why the missing step looked complete.
10
+ *
11
+ * Driven through the real binary so the arg parsing under test is the arg
12
+ * parsing that ships. HOME is redirected per-test, so the registry these cases
13
+ * read and write is a throwaway one and never the developer's own.
14
+ *
15
+ * @jest-environment node
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const { execFileSync } = require('child_process');
24
+
25
+ const BIN = path.join(__dirname, '..', '..', 'bin', 'quilltap.js');
26
+
27
+ let home;
28
+
29
+ function run(...args) {
30
+ return execFileSync(process.execPath, [BIN, 'instances', ...args], {
31
+ encoding: 'utf8',
32
+ input: '',
33
+ env: { ...process.env, HOME: home, USERPROFILE: home, APPDATA: path.join(home, 'AppData') },
34
+ }).trim();
35
+ }
36
+
37
+ /**
38
+ * `instances add` always prompts for a passphrase, so register through stdin.
39
+ * The directory is created first — otherwise a second "save anyway?" prompt
40
+ * consumes the answer meant for the passphrase question.
41
+ */
42
+ function addInstance(name) {
43
+ const dir = path.join(home, name.toLowerCase());
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ execFileSync(process.execPath, [BIN, 'instances', 'add', name, dir], {
46
+ encoding: 'utf8',
47
+ input: 'n\n',
48
+ env: { ...process.env, HOME: home, USERPROFILE: home, APPDATA: path.join(home, 'AppData') },
49
+ });
50
+ return dir;
51
+ }
52
+
53
+ beforeEach(() => {
54
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'qt-inst-'));
55
+ });
56
+
57
+ afterEach(() => {
58
+ fs.rmSync(home, { recursive: true, force: true });
59
+ });
60
+
61
+ describe('instances default --json', () => {
62
+ it('reports null on an empty registry instead of hunting for an instance named "--json"', () => {
63
+ const out = run('default', '--json');
64
+ expect(JSON.parse(out)).toEqual({ defaultInstance: null });
65
+ // The bug 120 failure mode, spelled out so a regression is unambiguous.
66
+ expect(out).not.toMatch(/Unknown instance/);
67
+ });
68
+
69
+ it('reports the registered default as JSON', () => {
70
+ addInstance('Friday');
71
+ run('default', 'Friday');
72
+ expect(JSON.parse(run('default', '--json'))).toEqual({ defaultInstance: 'Friday' });
73
+ });
74
+
75
+ it('leaves the set path alone — a name still sets, with or without the flag', () => {
76
+ addInstance('Friday');
77
+ expect(run('default', 'Friday')).toMatch(/Set default instance to "Friday"/);
78
+ expect(run('default')).toBe('Friday');
79
+ expect(run('default', 'Friday', '--json')).toMatch(/Set default instance to "Friday"/);
80
+ });
81
+
82
+ it('still prints the plain report with no flag', () => {
83
+ expect(run('default')).toBe('(none)');
84
+ });
85
+ });
86
+
87
+ describe('instances list --json', () => {
88
+ it('emits the registry as an array', () => {
89
+ expect(JSON.parse(run('list', '--json'))).toEqual([]);
90
+ addInstance('Friday');
91
+ const [entry] = JSON.parse(run('list', '--json'));
92
+ expect(entry).toMatchObject({ name: 'Friday', hasPassphrase: false });
93
+ });
94
+ });
95
+
96
+ describe('the flags the help text advertises actually parse', () => {
97
+ // The completion-coverage suite checks help -> templates. This checks the
98
+ // other direction for the two flags bug 120 was about: that a flag the help
99
+ // names is one the parser honours.
100
+ it('names --json on the list verb in `instances --help`', () => {
101
+ const help = execFileSync(process.execPath, [BIN, 'instances', '--help'], { encoding: 'utf8' });
102
+ expect(help).toMatch(/^\s*list \[--json\]/m);
103
+ });
104
+ });
@@ -234,6 +234,9 @@ for verb in show remove rm delete set-passphrase passphrase default rename resto
234
234
  end
235
235
 
236
236
  complete -c quilltap -n '__quilltap_using_subcommand instances' -l 'names-only' -d 'Print one name per line'
237
+ for verb in list ls
238
+ complete -c quilltap -n "__quilltap_using_subverb instances $verb" -l 'json' -d 'JSON output'
239
+ end
237
240
  complete -c quilltap -n '__quilltap_using_subverb instances default' -l 'clear' -d 'Clear default instance'
238
241
  for verb in restore-key rebuild-key
239
242
  complete -c quilltap -n "__quilltap_using_subverb instances $verb" -l 'passphrase' -d 'Passphrase for the rebuilt .dbkey'
@@ -30,7 +30,7 @@ Quilltap Instance Registry
30
30
  Usage: quilltap instances <verb> [args]
31
31
 
32
32
  Verbs:
33
- list List registered instances (default)
33
+ list [--json] List registered instances (default)
34
34
  show <name> Show one instance (passphrase status only)
35
35
  path Print the path to instances.json
36
36
  add <name> [<path>] Register an instance (prompts for missing path / passphrase)
@@ -366,9 +366,15 @@ async function instancesCommand(args) {
366
366
  case 'passphrase':
367
367
  await cmdSetPassphrase(rest);
368
368
  return;
369
- case 'default':
370
- cmdDefault(rest);
369
+ case 'default': {
370
+ // `--json` has to be lifted out of the positionals as well as read:
371
+ // cmdDefault treats args[0] as the instance name to set, so leaving the
372
+ // flag in place made `instances default --json` try to set an instance
373
+ // literally named "--json" and left cmdDefault's json branch unreachable.
374
+ const json = rest.includes('--json');
375
+ cmdDefault(rest.filter((a) => a !== '--json'), { json });
371
376
  return;
377
+ }
372
378
  case 'rename':
373
379
  cmdRename(rest);
374
380
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.9.0-dev.121",
3
+ "version": "4.9.0-dev.134",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -39,7 +39,7 @@
39
39
  "@napi-rs/canvas": "^0.1.100",
40
40
  "better-sqlite3-multiple-ciphers": "^12.11.1",
41
41
  "node-pty": "^1.1.0",
42
- "sharp": "^0.35.3",
42
+ "sharp": "^0.35.4",
43
43
  "tar": "^7.5.22",
44
44
  "yauzl": "^3.4.0"
45
45
  },