quilltap 4.5.0-dev → 4.6.0-dev
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 +178 -0
- package/bin/quilltap.js +226 -20
- package/lib/completion/bash.template +121 -0
- package/lib/completion/fish.template +93 -0
- package/lib/completion/zsh.template +209 -0
- package/lib/completion-commands.js +77 -0
- package/lib/db-commands.js +1142 -0
- package/lib/db-helpers.js +173 -4
- package/lib/docs-commands.js +2157 -172
- package/lib/graph-integrity.js +105 -0
- package/lib/instances-commands.js +342 -0
- package/lib/instances.js +335 -0
- package/lib/lock-helpers.js +117 -0
- package/lib/logs-commands.js +383 -0
- package/lib/memories-commands.js +1374 -0
- package/lib/memory-diff-command.js +19 -3
- package/lib/migrations-commands.js +324 -0
- package/lib/theme-commands.js +18 -0
- package/package.json +1 -1
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const fs = require('fs');
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
resolveDataDirAndPassphrase,
|
|
18
|
+
printDefaultInstanceHint,
|
|
19
|
+
loadDbKey,
|
|
20
|
+
} = require('./db-helpers');
|
|
17
21
|
|
|
18
22
|
const RESET = '\x1b[0m';
|
|
19
23
|
const BOLD = '\x1b[1m';
|
|
@@ -39,6 +43,7 @@ so the comparison is non-destructive.
|
|
|
39
43
|
|
|
40
44
|
Options:
|
|
41
45
|
-d, --data-dir <path> Override data directory (instance root)
|
|
46
|
+
-i, --instance <name> Use a registered instance (see 'quilltap instances')
|
|
42
47
|
--passphrase <pass> Decrypt .dbkey if peppered
|
|
43
48
|
--port <number> Server port for API calls (default: 3000)
|
|
44
49
|
--out <dir> Output directory (default: cwd)
|
|
@@ -58,6 +63,7 @@ Examples:
|
|
|
58
63
|
function parseFlags(args) {
|
|
59
64
|
const flags = {
|
|
60
65
|
dataDir: '',
|
|
66
|
+
instance: '',
|
|
61
67
|
passphrase: '',
|
|
62
68
|
port: 3000,
|
|
63
69
|
out: process.cwd(),
|
|
@@ -73,6 +79,10 @@ function parseFlags(args) {
|
|
|
73
79
|
case '--data-dir':
|
|
74
80
|
flags.dataDir = args[++i];
|
|
75
81
|
break;
|
|
82
|
+
case '-i':
|
|
83
|
+
case '--instance':
|
|
84
|
+
flags.instance = args[++i];
|
|
85
|
+
break;
|
|
76
86
|
case '--passphrase':
|
|
77
87
|
flags.passphrase = args[++i];
|
|
78
88
|
break;
|
|
@@ -128,7 +138,13 @@ function tryParseJsonColumn(value, fallback) {
|
|
|
128
138
|
* columns are parsed; the binary `embedding` column is dropped from output.
|
|
129
139
|
*/
|
|
130
140
|
async function readExistingMemories(flags, chatId) {
|
|
131
|
-
const
|
|
141
|
+
const resolved = resolveDataDirAndPassphrase({
|
|
142
|
+
dataDir: flags.dataDir,
|
|
143
|
+
instance: flags.instance,
|
|
144
|
+
passphrase: flags.passphrase,
|
|
145
|
+
});
|
|
146
|
+
printDefaultInstanceHint(resolved);
|
|
147
|
+
const { dataDir, passphrase } = resolved;
|
|
132
148
|
const dbPath = path.join(dataDir, 'quilltap.db');
|
|
133
149
|
if (!fs.existsSync(dbPath)) {
|
|
134
150
|
console.error(`Database not found: ${dbPath}`);
|
|
@@ -137,7 +153,7 @@ async function readExistingMemories(flags, chatId) {
|
|
|
137
153
|
|
|
138
154
|
let pepper;
|
|
139
155
|
try {
|
|
140
|
-
pepper = await loadDbKey(dataDir,
|
|
156
|
+
pepper = await loadDbKey(dataDir, passphrase);
|
|
141
157
|
} catch (err) {
|
|
142
158
|
console.error(`Error: ${err.message}`);
|
|
143
159
|
process.exit(1);
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
resolveDataDirAndPassphrase,
|
|
7
|
+
printDefaultInstanceHint,
|
|
8
|
+
openMainDb,
|
|
9
|
+
loadDbKey,
|
|
10
|
+
} = require('./db-helpers');
|
|
11
|
+
|
|
12
|
+
// ---------- argument parsing ----------
|
|
13
|
+
|
|
14
|
+
function parseFlags(args) {
|
|
15
|
+
const flags = {
|
|
16
|
+
dataDir: '',
|
|
17
|
+
instance: '',
|
|
18
|
+
passphrase: '',
|
|
19
|
+
json: false,
|
|
20
|
+
help: false,
|
|
21
|
+
dryRun: false,
|
|
22
|
+
};
|
|
23
|
+
const positional = [];
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < args.length) {
|
|
26
|
+
const a = args[i];
|
|
27
|
+
switch (a) {
|
|
28
|
+
case '-d': case '--data-dir': flags.dataDir = args[++i]; break;
|
|
29
|
+
case '--instance': flags.instance = args[++i]; break;
|
|
30
|
+
case '--passphrase': flags.passphrase = args[++i]; break;
|
|
31
|
+
case '--json': flags.json = true; break;
|
|
32
|
+
case '-h': case '--help': flags.help = true; break;
|
|
33
|
+
case '--dry-run': flags.dryRun = true; break;
|
|
34
|
+
default:
|
|
35
|
+
if (!a.startsWith('-')) positional.push(a);
|
|
36
|
+
else console.error(`unknown flag: ${a}`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return { flags, positional };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------- migration list parsing ----------
|
|
45
|
+
|
|
46
|
+
// Reads the registered migration list from migrations/scripts/index.ts and resolves each
|
|
47
|
+
// entry's real `id:` value by reading the imported source file. Heuristic camelCase→kebab
|
|
48
|
+
// conversion is unreliable (some migrations omit `-v1`, others use `-v2`), so we always
|
|
49
|
+
// trust the file.
|
|
50
|
+
function extractMigrationsFromSource() {
|
|
51
|
+
const scriptsDir = path.join(__dirname, '../../..', 'migrations/scripts');
|
|
52
|
+
const indexPath = path.join(scriptsDir, 'index.ts');
|
|
53
|
+
if (!fs.existsSync(indexPath)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
57
|
+
|
|
58
|
+
// Pass 1: build name → { filePath, comment } from imports and preceding comments.
|
|
59
|
+
const lines = indexContent.split('\n');
|
|
60
|
+
const importInfo = {};
|
|
61
|
+
let lastComment = '';
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.match(/^\s*\/\/\s+/)) {
|
|
64
|
+
lastComment = line.replace(/^\s*\/\/\s+/, '').trim();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const importMatch = line.match(/import\s+\{\s*(\w+)\s*\}\s+from\s+['"]\.\/([^'"]+)['"]/);
|
|
68
|
+
if (importMatch) {
|
|
69
|
+
const [, name, filePath] = importMatch;
|
|
70
|
+
importInfo[name] = { filePath, comment: lastComment || '' };
|
|
71
|
+
lastComment = '';
|
|
72
|
+
} else if (line.trim() && !line.match(/^\s*\/\//) && !line.includes('import type')) {
|
|
73
|
+
// Reset comment if we hit a non-comment non-import line
|
|
74
|
+
if (!line.match(/^\s*$/)) lastComment = '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pass 2: find the active migrations array section and collect names in order.
|
|
79
|
+
const arrayStart = indexContent.indexOf('export const migrations: Migration[] = [');
|
|
80
|
+
if (arrayStart === -1) return [];
|
|
81
|
+
const arrayEnd = indexContent.indexOf('];', arrayStart);
|
|
82
|
+
if (arrayEnd === -1) return [];
|
|
83
|
+
const arrayContent = indexContent.substring(arrayStart, arrayEnd);
|
|
84
|
+
|
|
85
|
+
const activeNames = arrayContent.match(/\b([a-zA-Z]\w*Migration)\b/g) || [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
|
|
88
|
+
// Pass 3: for each active name, read the corresponding file and extract the real `id:`.
|
|
89
|
+
const migrations = [];
|
|
90
|
+
for (const name of activeNames) {
|
|
91
|
+
if (seen.has(name)) continue;
|
|
92
|
+
seen.add(name);
|
|
93
|
+
const info = importInfo[name];
|
|
94
|
+
if (!info) continue;
|
|
95
|
+
const filePath = path.join(scriptsDir, info.filePath + '.ts');
|
|
96
|
+
if (!fs.existsSync(filePath)) continue;
|
|
97
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
const idMatch = fileContent.match(/^\s*id:\s*['"]([^'"]+)['"]/m);
|
|
99
|
+
if (!idMatch) continue;
|
|
100
|
+
migrations.push({
|
|
101
|
+
id: idMatch[1],
|
|
102
|
+
description: info.comment || idMatch[1],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return migrations;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------- help text ----------
|
|
110
|
+
|
|
111
|
+
function printHelp() {
|
|
112
|
+
console.log(`Usage: quilltap migrations <command> [options]
|
|
113
|
+
|
|
114
|
+
Commands:
|
|
115
|
+
status Show applied and pending migrations
|
|
116
|
+
pending List pending migrations
|
|
117
|
+
run --dry-run Simulate what would run on next startup
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
-d, --data-dir <path> Use a specific data directory
|
|
121
|
+
--instance <name> Use a named instance
|
|
122
|
+
--passphrase <pass> Provide passphrase (prompts if needed)
|
|
123
|
+
--json Output as JSON
|
|
124
|
+
-h, --help Show this help message
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
quilltap migrations status
|
|
128
|
+
quilltap migrations pending --instance Friday
|
|
129
|
+
quilltap migrations run --dry-run --json
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- database access ----------
|
|
134
|
+
|
|
135
|
+
function getAppliedMigrations(db) {
|
|
136
|
+
try {
|
|
137
|
+
const rows = db.prepare(`
|
|
138
|
+
SELECT id, completedAt, quilltapVersion, itemsAffected, message
|
|
139
|
+
FROM migrations_state
|
|
140
|
+
ORDER BY completedAt ASC
|
|
141
|
+
`).all();
|
|
142
|
+
return rows || [];
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Table may not exist in brand-new instances
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------- command handlers ----------
|
|
150
|
+
|
|
151
|
+
async function openResolvedMainDb(flags) {
|
|
152
|
+
const resolved = resolveDataDirAndPassphrase(flags);
|
|
153
|
+
printDefaultInstanceHint(resolved);
|
|
154
|
+
const pepper = await loadDbKey(resolved.dataDir, resolved.passphrase);
|
|
155
|
+
return openMainDb(resolved.dataDir, pepper, { readonly: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function handleStatus(flags) {
|
|
159
|
+
let db;
|
|
160
|
+
try {
|
|
161
|
+
db = await openResolvedMainDb(flags);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`Error opening database: ${err.message}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const applied = getAppliedMigrations(db);
|
|
168
|
+
const source = extractMigrationsFromSource();
|
|
169
|
+
|
|
170
|
+
const appliedIds = new Set(applied.map(m => m.id));
|
|
171
|
+
const sourceIds = new Set(source.map(m => m.id));
|
|
172
|
+
const pending = source.filter(m => !appliedIds.has(m.id));
|
|
173
|
+
const retired = applied.filter(m => !sourceIds.has(m.id));
|
|
174
|
+
|
|
175
|
+
if (flags.json) {
|
|
176
|
+
console.log(JSON.stringify({
|
|
177
|
+
sourceTotal: source.length,
|
|
178
|
+
recordedApplied: applied.length,
|
|
179
|
+
pending: pending.length,
|
|
180
|
+
retired: retired.length,
|
|
181
|
+
pendingList: pending,
|
|
182
|
+
retiredList: retired.map(m => ({ id: m.id, completedAt: m.completedAt })),
|
|
183
|
+
mostRecent: applied.length > 0 ? applied[applied.length - 1] : null,
|
|
184
|
+
}, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`Migrations in source: ${source.length}`);
|
|
187
|
+
console.log(`Recorded as applied: ${applied.length}` +
|
|
188
|
+
(retired.length > 0 ? ` (${retired.length} retired from active list)` : ''));
|
|
189
|
+
console.log(`Not yet recorded: ${pending.length}` +
|
|
190
|
+
(pending.length > 0 ? ' (may include migrations whose shouldRun() returns false on this instance)' : ''));
|
|
191
|
+
if (applied.length > 0) {
|
|
192
|
+
const latest = applied[applied.length - 1];
|
|
193
|
+
console.log(`Most recent applied: ${latest.id} at ${latest.completedAt}`);
|
|
194
|
+
}
|
|
195
|
+
if (pending.length > 0) {
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log('Not yet recorded as applied:');
|
|
198
|
+
pending.forEach(m => {
|
|
199
|
+
console.log(` ${m.id.padEnd(50)} ${m.description}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
db.close();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function handlePending(flags) {
|
|
208
|
+
let db;
|
|
209
|
+
try {
|
|
210
|
+
db = await openResolvedMainDb(flags);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error(`Error opening database: ${err.message}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const applied = getAppliedMigrations(db);
|
|
217
|
+
const source = extractMigrationsFromSource();
|
|
218
|
+
|
|
219
|
+
const appliedIds = new Set(applied.map(m => m.id));
|
|
220
|
+
const pending = source.filter(m => !appliedIds.has(m.id));
|
|
221
|
+
|
|
222
|
+
if (flags.json) {
|
|
223
|
+
console.log(JSON.stringify(pending, null, 2));
|
|
224
|
+
} else {
|
|
225
|
+
if (pending.length === 0) {
|
|
226
|
+
console.log('No pending migrations.');
|
|
227
|
+
} else {
|
|
228
|
+
pending.forEach(m => {
|
|
229
|
+
console.log(`${m.id.padEnd(50)} ${m.description}`);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
db.close();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function handleRun(flags) {
|
|
238
|
+
if (!flags.dryRun) {
|
|
239
|
+
console.error('Error: migrations run requires --dry-run flag.');
|
|
240
|
+
console.error('Actual migration execution happens at server startup, where the loading screen');
|
|
241
|
+
console.error('and progress reporting are available. To see what would run on the next startup,');
|
|
242
|
+
console.error('use: quilltap migrations run --dry-run');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let db;
|
|
247
|
+
try {
|
|
248
|
+
db = await openResolvedMainDb(flags);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`Error opening database: ${err.message}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const applied = getAppliedMigrations(db);
|
|
255
|
+
const source = extractMigrationsFromSource();
|
|
256
|
+
|
|
257
|
+
const appliedIds = new Set(applied.map(m => m.id));
|
|
258
|
+
const pending = source.filter(m => !appliedIds.has(m.id));
|
|
259
|
+
|
|
260
|
+
if (flags.json) {
|
|
261
|
+
console.log(JSON.stringify({
|
|
262
|
+
pending: pending.length,
|
|
263
|
+
migrations: pending.map(m => ({
|
|
264
|
+
id: m.id,
|
|
265
|
+
description: m.description,
|
|
266
|
+
note: 'shouldRun() predicate is evaluated at startup; inspect migration source for details',
|
|
267
|
+
})),
|
|
268
|
+
}, null, 2));
|
|
269
|
+
} else {
|
|
270
|
+
console.log(`Dry run: ${pending.length} migrations would run on next startup`);
|
|
271
|
+
if (pending.length > 0) {
|
|
272
|
+
console.log('');
|
|
273
|
+
pending.forEach(m => {
|
|
274
|
+
console.log(` ${m.id.padEnd(50)} ${m.description}`);
|
|
275
|
+
});
|
|
276
|
+
console.log('');
|
|
277
|
+
console.log('Note: shouldRun() predicate is evaluated at startup.');
|
|
278
|
+
console.log('Inspect the migration source in migrations/scripts/ for conditional logic.');
|
|
279
|
+
} else {
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log('All migrations have been applied.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
db.close();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------- main entry point ----------
|
|
289
|
+
|
|
290
|
+
async function migrationsCommand(args) {
|
|
291
|
+
const { flags, positional } = parseFlags(args);
|
|
292
|
+
|
|
293
|
+
if (flags.help || positional.length === 0) {
|
|
294
|
+
printHelp();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const verb = positional[0];
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
switch (verb) {
|
|
302
|
+
case 'status':
|
|
303
|
+
await handleStatus(flags);
|
|
304
|
+
break;
|
|
305
|
+
case 'pending':
|
|
306
|
+
await handlePending(flags);
|
|
307
|
+
break;
|
|
308
|
+
case 'run':
|
|
309
|
+
await handleRun(flags);
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
console.error(`unknown migrations command: ${verb}`);
|
|
313
|
+
console.error('Use "quilltap migrations --help" for usage.');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`Error: ${err.message}`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
migrationsCommand,
|
|
324
|
+
};
|
package/lib/theme-commands.js
CHANGED
|
@@ -1076,6 +1076,7 @@ ${c.bold}Registry Operator Commands:${c.reset}
|
|
|
1076
1076
|
|
|
1077
1077
|
${c.bold}Options:${c.reset}
|
|
1078
1078
|
--data-dir <path> Override data directory
|
|
1079
|
+
-i, --instance <name> Use a registered instance (see 'quilltap instances')
|
|
1079
1080
|
-h, --help Show this help
|
|
1080
1081
|
|
|
1081
1082
|
${c.bold}Examples:${c.reset}
|
|
@@ -1098,6 +1099,7 @@ ${c.bold}Examples:${c.reset}
|
|
|
1098
1099
|
|
|
1099
1100
|
async function themesCommand(args) {
|
|
1100
1101
|
let dataDirOverride = '';
|
|
1102
|
+
let instanceName = '';
|
|
1101
1103
|
let showHelp = false;
|
|
1102
1104
|
let command = '';
|
|
1103
1105
|
const positional = [];
|
|
@@ -1109,6 +1111,7 @@ async function themesCommand(args) {
|
|
|
1109
1111
|
while (i < args.length) {
|
|
1110
1112
|
switch (args[i]) {
|
|
1111
1113
|
case '--data-dir': case '-d': dataDirOverride = args[++i]; break;
|
|
1114
|
+
case '--instance': case '-i': instanceName = args[++i]; break;
|
|
1112
1115
|
case '--output': case '-o': outputPath = args[++i]; break;
|
|
1113
1116
|
case '--help': case '-h': showHelp = true; break;
|
|
1114
1117
|
default:
|
|
@@ -1136,6 +1139,21 @@ async function themesCommand(args) {
|
|
|
1136
1139
|
process.exit(0);
|
|
1137
1140
|
}
|
|
1138
1141
|
|
|
1142
|
+
if (instanceName && dataDirOverride) {
|
|
1143
|
+
console.error('Error: Specify either --instance or --data-dir, not both.');
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
if (instanceName) {
|
|
1147
|
+
try {
|
|
1148
|
+
const { resolveInstance } = require('./instances');
|
|
1149
|
+
const inst = resolveInstance(instanceName);
|
|
1150
|
+
dataDirOverride = inst.path;
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
console.error(`Error: ${err.message}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1139
1157
|
switch (command) {
|
|
1140
1158
|
case 'list':
|
|
1141
1159
|
await listThemes(dataDirOverride);
|