nicefox-graphdb 0.1.0

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +417 -0
  3. package/package.json +78 -0
  4. package/packages/nicefox-graphdb/LICENSE +21 -0
  5. package/packages/nicefox-graphdb/README.md +417 -0
  6. package/packages/nicefox-graphdb/dist/auth.d.ts +66 -0
  7. package/packages/nicefox-graphdb/dist/auth.d.ts.map +1 -0
  8. package/packages/nicefox-graphdb/dist/auth.js +148 -0
  9. package/packages/nicefox-graphdb/dist/auth.js.map +1 -0
  10. package/packages/nicefox-graphdb/dist/backup.d.ts +51 -0
  11. package/packages/nicefox-graphdb/dist/backup.d.ts.map +1 -0
  12. package/packages/nicefox-graphdb/dist/backup.js +201 -0
  13. package/packages/nicefox-graphdb/dist/backup.js.map +1 -0
  14. package/packages/nicefox-graphdb/dist/cli-helpers.d.ts +17 -0
  15. package/packages/nicefox-graphdb/dist/cli-helpers.d.ts.map +1 -0
  16. package/packages/nicefox-graphdb/dist/cli-helpers.js +121 -0
  17. package/packages/nicefox-graphdb/dist/cli-helpers.js.map +1 -0
  18. package/packages/nicefox-graphdb/dist/cli.d.ts +3 -0
  19. package/packages/nicefox-graphdb/dist/cli.d.ts.map +1 -0
  20. package/packages/nicefox-graphdb/dist/cli.js +660 -0
  21. package/packages/nicefox-graphdb/dist/cli.js.map +1 -0
  22. package/packages/nicefox-graphdb/dist/db.d.ts +118 -0
  23. package/packages/nicefox-graphdb/dist/db.d.ts.map +1 -0
  24. package/packages/nicefox-graphdb/dist/db.js +245 -0
  25. package/packages/nicefox-graphdb/dist/db.js.map +1 -0
  26. package/packages/nicefox-graphdb/dist/executor.d.ts +272 -0
  27. package/packages/nicefox-graphdb/dist/executor.d.ts.map +1 -0
  28. package/packages/nicefox-graphdb/dist/executor.js +3579 -0
  29. package/packages/nicefox-graphdb/dist/executor.js.map +1 -0
  30. package/packages/nicefox-graphdb/dist/index.d.ts +54 -0
  31. package/packages/nicefox-graphdb/dist/index.d.ts.map +1 -0
  32. package/packages/nicefox-graphdb/dist/index.js +74 -0
  33. package/packages/nicefox-graphdb/dist/index.js.map +1 -0
  34. package/packages/nicefox-graphdb/dist/local.d.ts +7 -0
  35. package/packages/nicefox-graphdb/dist/local.d.ts.map +1 -0
  36. package/packages/nicefox-graphdb/dist/local.js +115 -0
  37. package/packages/nicefox-graphdb/dist/local.js.map +1 -0
  38. package/packages/nicefox-graphdb/dist/parser.d.ts +300 -0
  39. package/packages/nicefox-graphdb/dist/parser.d.ts.map +1 -0
  40. package/packages/nicefox-graphdb/dist/parser.js +1891 -0
  41. package/packages/nicefox-graphdb/dist/parser.js.map +1 -0
  42. package/packages/nicefox-graphdb/dist/remote.d.ts +6 -0
  43. package/packages/nicefox-graphdb/dist/remote.d.ts.map +1 -0
  44. package/packages/nicefox-graphdb/dist/remote.js +87 -0
  45. package/packages/nicefox-graphdb/dist/remote.js.map +1 -0
  46. package/packages/nicefox-graphdb/dist/routes.d.ts +31 -0
  47. package/packages/nicefox-graphdb/dist/routes.d.ts.map +1 -0
  48. package/packages/nicefox-graphdb/dist/routes.js +202 -0
  49. package/packages/nicefox-graphdb/dist/routes.js.map +1 -0
  50. package/packages/nicefox-graphdb/dist/translator.d.ts +136 -0
  51. package/packages/nicefox-graphdb/dist/translator.d.ts.map +1 -0
  52. package/packages/nicefox-graphdb/dist/translator.js +4849 -0
  53. package/packages/nicefox-graphdb/dist/translator.js.map +1 -0
  54. package/packages/nicefox-graphdb/dist/types.d.ts +133 -0
  55. package/packages/nicefox-graphdb/dist/types.d.ts.map +1 -0
  56. package/packages/nicefox-graphdb/dist/types.js +21 -0
  57. package/packages/nicefox-graphdb/dist/types.js.map +1 -0
@@ -0,0 +1,660 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { serve } from "@hono/node-server";
4
+ import Database from "better-sqlite3";
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { createServer, GraphDatabase, Executor, BackupManager, generateApiKey, VERSION, } from "./index.js";
8
+ import { formatBytes, getApiKeysPath, loadApiKeys, saveApiKeys, ensureDataDir, calculateColumnWidths, formatTableRow, listProjects, getProjectKeyCount, } from "./cli-helpers.js";
9
+ const program = new Command();
10
+ program
11
+ .name("nicefox-graphdb")
12
+ .description("NiceFox GraphDB - SQLite-based graph database with Cypher queries")
13
+ .version(VERSION);
14
+ // ============================================================================
15
+ // serve - Start the HTTP server
16
+ // ============================================================================
17
+ program
18
+ .command("serve")
19
+ .description("Start the NiceFox GraphDB HTTP server")
20
+ .option("-p, --port <port>", "Port to listen on", "3000")
21
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
22
+ .option("-H, --host <host>", "Host to bind to", "localhost")
23
+ .option("-b, --backup <path>", "Backup directory (enables backup endpoints)")
24
+ .action(async (options) => {
25
+ const port = parseInt(options.port, 10);
26
+ const dataPath = path.resolve(options.data);
27
+ const host = options.host;
28
+ const backupPath = options.backup ? path.resolve(options.backup) : undefined;
29
+ // Ensure data directory exists
30
+ ensureDataDir(dataPath);
31
+ // Load API keys from data directory
32
+ let apiKeys;
33
+ const keysFile = getApiKeysPath(dataPath);
34
+ if (fs.existsSync(keysFile)) {
35
+ try {
36
+ apiKeys = JSON.parse(fs.readFileSync(keysFile, "utf-8"));
37
+ console.log(`Loaded ${Object.keys(apiKeys).length} API key(s) from ${keysFile}`);
38
+ }
39
+ catch (err) {
40
+ console.error(`Failed to load API keys from ${keysFile}:`, err);
41
+ process.exit(1);
42
+ }
43
+ }
44
+ const { app, dbManager } = createServer({
45
+ port,
46
+ dataPath,
47
+ backupPath,
48
+ apiKeys,
49
+ });
50
+ const authStatus = apiKeys ? "enabled" : "disabled";
51
+ const backupStatus = backupPath ? backupPath.slice(0, 30) : "disabled";
52
+ console.log(`
53
+ ╔═══════════════════════════════════════════════════════════╗
54
+ ║ NiceFox GraphDB Server v${VERSION} ║
55
+ ╠═══════════════════════════════════════════════════════════╣
56
+ ║ Endpoint: http://${host}:${port.toString().padEnd(5)} ║
57
+ ║ Data: ${dataPath.slice(0, 43).padEnd(43)} ║
58
+ ║ Backups: ${backupStatus.padEnd(43)} ║
59
+ ║ Auth: ${authStatus.padEnd(43)} ║
60
+ ║ ║
61
+ ║ Routes: ║
62
+ ║ POST /query/:env/:project - Execute Cypher queries ║
63
+ ║ GET /health - Health check ║
64
+ ║ GET /admin/list - List all projects ║
65
+ ║ GET /admin/backup - Backup status ║
66
+ ║ POST /admin/backup - Trigger backup ║
67
+ ╚═══════════════════════════════════════════════════════════╝
68
+ `);
69
+ serve({
70
+ fetch: app.fetch,
71
+ port,
72
+ hostname: host,
73
+ });
74
+ // Handle graceful shutdown
75
+ process.on("SIGINT", () => {
76
+ console.log("\nShutting down...");
77
+ dbManager.closeAll();
78
+ process.exit(0);
79
+ });
80
+ process.on("SIGTERM", () => {
81
+ console.log("\nShutting down...");
82
+ dbManager.closeAll();
83
+ process.exit(0);
84
+ });
85
+ });
86
+ // ============================================================================
87
+ // create - Create a new project (both production and test DBs)
88
+ // ============================================================================
89
+ program
90
+ .command("create <project>")
91
+ .description("Create a new project with databases and API key")
92
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
93
+ .option("--no-key", "Skip API key generation")
94
+ .action((project, options) => {
95
+ const dataPath = path.resolve(options.data);
96
+ ensureDataDir(dataPath);
97
+ const envs = ["production", "test"];
98
+ let created = false;
99
+ for (const env of envs) {
100
+ const dbPath = path.join(dataPath, env, `${project}.db`);
101
+ const dbDir = path.dirname(dbPath);
102
+ if (!fs.existsSync(dbDir)) {
103
+ fs.mkdirSync(dbDir, { recursive: true });
104
+ }
105
+ if (fs.existsSync(dbPath)) {
106
+ console.log(` [skip] ${env}/${project}.db already exists`);
107
+ }
108
+ else {
109
+ const db = new GraphDatabase(dbPath);
110
+ db.initialize();
111
+ db.close();
112
+ console.log(` [created] ${env}/${project}.db`);
113
+ created = true;
114
+ }
115
+ }
116
+ // Generate API keys for the project (one for production, one for test)
117
+ if (options.key && created) {
118
+ const keys = loadApiKeys(dataPath);
119
+ const prodKey = generateApiKey();
120
+ const testKey = generateApiKey();
121
+ keys[prodKey] = { project, env: "production" };
122
+ keys[testKey] = { project, env: "test" };
123
+ saveApiKeys(dataPath, keys);
124
+ console.log(`\nProject '${project}' is ready.`);
125
+ console.log(`\nAPI Keys:`);
126
+ console.log(` production: ${prodKey}`);
127
+ console.log(` test: ${testKey}`);
128
+ }
129
+ else if (!created) {
130
+ console.log(`\nProject '${project}' already exists.`);
131
+ }
132
+ else {
133
+ console.log(`\nProject '${project}' is ready (no API key generated).`);
134
+ }
135
+ });
136
+ // ============================================================================
137
+ // delete - Delete a project (both production and test DBs)
138
+ // ============================================================================
139
+ program
140
+ .command("delete <project>")
141
+ .description("Delete a project (removes databases and API keys)")
142
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
143
+ .option("-f, --force", "Skip confirmation prompt", false)
144
+ .action((project, options) => {
145
+ const dataPath = path.resolve(options.data);
146
+ const envs = ["production", "test"];
147
+ // Check what exists
148
+ const existing = [];
149
+ for (const env of envs) {
150
+ const dbPath = path.join(dataPath, env, `${project}.db`);
151
+ if (fs.existsSync(dbPath)) {
152
+ existing.push(`${env}/${project}.db`);
153
+ }
154
+ }
155
+ // Check for API keys
156
+ const keys = loadApiKeys(dataPath);
157
+ const projectKeys = Object.entries(keys).filter(([_, config]) => config.project === project);
158
+ if (existing.length === 0 && projectKeys.length === 0) {
159
+ console.log(`Project '${project}' does not exist.`);
160
+ process.exit(1);
161
+ }
162
+ if (!options.force) {
163
+ console.log(`This will delete:`);
164
+ for (const file of existing) {
165
+ console.log(` - ${file}`);
166
+ }
167
+ if (projectKeys.length > 0) {
168
+ console.log(` - ${projectKeys.length} API key(s)`);
169
+ }
170
+ console.log(`\nUse --force to confirm deletion.`);
171
+ process.exit(1);
172
+ }
173
+ // Delete database files
174
+ for (const env of envs) {
175
+ const dbPath = path.join(dataPath, env, `${project}.db`);
176
+ if (fs.existsSync(dbPath)) {
177
+ fs.unlinkSync(dbPath);
178
+ console.log(` [deleted] ${env}/${project}.db`);
179
+ }
180
+ }
181
+ // Delete API keys for this project
182
+ if (projectKeys.length > 0) {
183
+ for (const [key] of projectKeys) {
184
+ delete keys[key];
185
+ }
186
+ saveApiKeys(dataPath, keys);
187
+ console.log(` [deleted] ${projectKeys.length} API key(s)`);
188
+ }
189
+ console.log(`\nProject '${project}' has been deleted.`);
190
+ });
191
+ // ============================================================================
192
+ // list - List all projects
193
+ // ============================================================================
194
+ program
195
+ .command("list")
196
+ .description("List all projects and their environments")
197
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
198
+ .action((options) => {
199
+ const dataPath = path.resolve(options.data);
200
+ if (!fs.existsSync(dataPath)) {
201
+ console.log("No data directory found. Run 'nicefox-graphdb create <project>' first.");
202
+ return;
203
+ }
204
+ const projects = listProjects(dataPath);
205
+ if (projects.size === 0) {
206
+ console.log("No projects found.");
207
+ return;
208
+ }
209
+ // Load API keys to show key count per project
210
+ const keys = loadApiKeys(dataPath);
211
+ console.log("\nProjects:\n");
212
+ for (const [project, envs] of projects) {
213
+ const envList = envs.map((e) => (e === "production" ? "prod" : "test")).join(", ");
214
+ const keyCount = getProjectKeyCount(keys, project);
215
+ const keyInfo = keyCount > 0 ? ` (${keyCount} key${keyCount > 1 ? "s" : ""})` : " (no keys)";
216
+ console.log(` ${project} [${envList}]${keyInfo}`);
217
+ }
218
+ console.log("");
219
+ });
220
+ // ============================================================================
221
+ // query - Execute a Cypher query
222
+ // ============================================================================
223
+ program
224
+ .command("query <env> <project> <cypher>")
225
+ .description("Execute a Cypher query against a project database")
226
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
227
+ .option("-p, --params <json>", "Query parameters as JSON", "{}")
228
+ .option("--json", "Output raw JSON", false)
229
+ .action((env, project, cypher, options) => {
230
+ const dataPath = path.resolve(options.data);
231
+ if (env !== "production" && env !== "test") {
232
+ console.error(`Invalid environment: ${env}. Must be 'production' or 'test'.`);
233
+ process.exit(1);
234
+ }
235
+ const dbPath = path.join(dataPath, env, `${project}.db`);
236
+ if (!fs.existsSync(dbPath)) {
237
+ console.error(`Database not found: ${dbPath}`);
238
+ console.error(`Run 'nicefox-graphdb create ${project}' first.`);
239
+ process.exit(1);
240
+ }
241
+ let params = {};
242
+ try {
243
+ params = JSON.parse(options.params);
244
+ }
245
+ catch {
246
+ console.error("Invalid JSON in --params");
247
+ process.exit(1);
248
+ }
249
+ const db = new GraphDatabase(dbPath);
250
+ db.initialize();
251
+ const executor = new Executor(db);
252
+ const result = executor.execute(cypher, params);
253
+ db.close();
254
+ if (!result.success) {
255
+ console.error(`Query failed: ${result.error.message}`);
256
+ if (result.error.position !== undefined) {
257
+ console.error(` at position ${result.error.position} (line ${result.error.line}, column ${result.error.column})`);
258
+ }
259
+ process.exit(1);
260
+ }
261
+ if (options.json) {
262
+ console.log(JSON.stringify(result, null, 2));
263
+ }
264
+ else {
265
+ console.log(`\nResults (${result.meta.count} rows, ${result.meta.time_ms}ms):\n`);
266
+ if (result.data.length === 0) {
267
+ console.log(" (no results)");
268
+ }
269
+ else {
270
+ // Print as table
271
+ const columns = Object.keys(result.data[0]);
272
+ printTable(columns, result.data);
273
+ }
274
+ console.log("");
275
+ }
276
+ });
277
+ // ============================================================================
278
+ // wipe - Wipe a test database (refuses on production)
279
+ // ============================================================================
280
+ program
281
+ .command("wipe <project>")
282
+ .description("Wipe test database for a project (production is protected)")
283
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
284
+ .option("-f, --force", "Skip confirmation prompt", false)
285
+ .action((project, options) => {
286
+ const dataPath = path.resolve(options.data);
287
+ const dbPath = path.join(dataPath, "test", `${project}.db`);
288
+ if (!fs.existsSync(dbPath)) {
289
+ console.error(`Test database not found: ${dbPath}`);
290
+ process.exit(1);
291
+ }
292
+ if (!options.force) {
293
+ console.log(`This will delete all data in test/${project}.db`);
294
+ console.log(`\nUse --force to confirm.`);
295
+ process.exit(1);
296
+ }
297
+ const db = new GraphDatabase(dbPath);
298
+ db.initialize();
299
+ db.execute("DELETE FROM edges");
300
+ db.execute("DELETE FROM nodes");
301
+ db.close();
302
+ console.log(`Wiped test/${project}.db`);
303
+ });
304
+ // ============================================================================
305
+ // clone - Clone production to test
306
+ // ============================================================================
307
+ program
308
+ .command("clone <project>")
309
+ .description("Clone production database to test (overwrites test)")
310
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
311
+ .option("-f, --force", "Skip confirmation prompt", false)
312
+ .action((project, options) => {
313
+ const dataPath = path.resolve(options.data);
314
+ const prodPath = path.join(dataPath, "production", `${project}.db`);
315
+ const testPath = path.join(dataPath, "test", `${project}.db`);
316
+ if (!fs.existsSync(prodPath)) {
317
+ console.error(`Production database not found: ${prodPath}`);
318
+ process.exit(1);
319
+ }
320
+ if (!options.force) {
321
+ console.log(`This will overwrite test/${project}.db with production/${project}.db`);
322
+ console.log(`\nUse --force to confirm.`);
323
+ process.exit(1);
324
+ }
325
+ // Ensure test directory exists
326
+ const testDir = path.dirname(testPath);
327
+ if (!fs.existsSync(testDir)) {
328
+ fs.mkdirSync(testDir, { recursive: true });
329
+ }
330
+ // Copy file
331
+ fs.copyFileSync(prodPath, testPath);
332
+ console.log(`Cloned production/${project}.db → test/${project}.db`);
333
+ });
334
+ // ============================================================================
335
+ // migrate - Migrate databases from old label format to JSON array
336
+ // ============================================================================
337
+ program
338
+ .command("migrate")
339
+ .description("Migrate databases from old label format (TEXT) to new format (JSON array)")
340
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
341
+ .option("-p, --project <name>", "Migrate specific project only")
342
+ .option("--dry-run", "Preview changes without modifying data", false)
343
+ .option("-f, --force", "Skip confirmation prompt", false)
344
+ .action((options) => {
345
+ const dataPath = path.resolve(options.data);
346
+ if (!fs.existsSync(dataPath)) {
347
+ console.error(`Data directory not found: ${dataPath}`);
348
+ process.exit(1);
349
+ }
350
+ // Find all databases to migrate
351
+ const databases = [];
352
+ for (const env of ["production", "test"]) {
353
+ const envPath = path.join(dataPath, env);
354
+ if (fs.existsSync(envPath)) {
355
+ const files = fs.readdirSync(envPath).filter((f) => f.endsWith(".db"));
356
+ for (const file of files) {
357
+ const project = file.replace(".db", "");
358
+ if (!options.project || options.project === project) {
359
+ databases.push({
360
+ env,
361
+ project,
362
+ path: path.join(envPath, file),
363
+ });
364
+ }
365
+ }
366
+ }
367
+ }
368
+ if (databases.length === 0) {
369
+ if (options.project) {
370
+ console.error(`Project '${options.project}' not found.`);
371
+ }
372
+ else {
373
+ console.log("No databases found.");
374
+ }
375
+ process.exit(1);
376
+ }
377
+ // Check what needs migration
378
+ console.log("\nChecking databases for migration...\n");
379
+ const toMigrate = [];
380
+ for (const dbInfo of databases) {
381
+ // Open database directly with better-sqlite3 to avoid schema initialization
382
+ const db = new Database(dbInfo.path);
383
+ try {
384
+ // Check if nodes table exists
385
+ const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'").get();
386
+ if (!tableExists) {
387
+ console.log(` ${dbInfo.env}/${dbInfo.project}.db: no nodes table (skipped)`);
388
+ continue;
389
+ }
390
+ // Count nodes that need migration (label is not valid JSON)
391
+ const result = db.prepare("SELECT COUNT(*) as count FROM nodes WHERE json_valid(label) = 0").get();
392
+ if (result.count > 0) {
393
+ toMigrate.push({ ...dbInfo, count: result.count });
394
+ console.log(` ${dbInfo.env}/${dbInfo.project}.db: ${result.count} node(s) need migration`);
395
+ }
396
+ else {
397
+ console.log(` ${dbInfo.env}/${dbInfo.project}.db: already migrated`);
398
+ }
399
+ }
400
+ finally {
401
+ db.close();
402
+ }
403
+ }
404
+ if (toMigrate.length === 0) {
405
+ console.log("\nAll databases are already migrated.");
406
+ return;
407
+ }
408
+ // Dry run - just show what would be done
409
+ if (options.dryRun) {
410
+ console.log(`\n[dry-run] Would migrate ${toMigrate.length} database(s)`);
411
+ return;
412
+ }
413
+ // Confirm before migrating
414
+ if (!options.force) {
415
+ console.log(`\nThis will migrate ${toMigrate.length} database(s).`);
416
+ console.log("Use --force to confirm, or --dry-run to preview.");
417
+ process.exit(1);
418
+ }
419
+ // Perform migration
420
+ console.log("\nMigrating...\n");
421
+ let successCount = 0;
422
+ let failCount = 0;
423
+ for (const dbInfo of toMigrate) {
424
+ const db = new Database(dbInfo.path);
425
+ try {
426
+ const start = Date.now();
427
+ // Migrate: wrap plain text labels in JSON array
428
+ const result = db.prepare("UPDATE nodes SET label = json_array(label) WHERE json_valid(label) = 0").run();
429
+ const duration = Date.now() - start;
430
+ console.log(` ${dbInfo.env}/${dbInfo.project}.db: ${result.changes} node(s) migrated (${duration}ms)`);
431
+ successCount++;
432
+ }
433
+ catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ console.error(` ${dbInfo.env}/${dbInfo.project}.db: FAILED - ${message}`);
436
+ failCount++;
437
+ }
438
+ finally {
439
+ db.close();
440
+ }
441
+ }
442
+ console.log(`\nMigration complete: ${successCount} database(s) updated`);
443
+ if (failCount > 0) {
444
+ console.log(` ${failCount} database(s) failed`);
445
+ process.exit(1);
446
+ }
447
+ });
448
+ // ============================================================================
449
+ // backup - Backup databases
450
+ // ============================================================================
451
+ program
452
+ .command("backup")
453
+ .description("Backup production databases")
454
+ .option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
455
+ .option("-o, --output <path>", "Backup output directory", "./backups")
456
+ .option("-p, --project <name>", "Backup specific project only")
457
+ .option("--include-test", "Also backup test databases", false)
458
+ .option("--keep <count>", "Number of backups to keep per project", "5")
459
+ .option("--status", "Show backup status only", false)
460
+ .action(async (options) => {
461
+ const dataPath = path.resolve(options.data);
462
+ const backupPath = path.resolve(options.output);
463
+ const keepCount = parseInt(options.keep, 10);
464
+ const manager = new BackupManager(backupPath);
465
+ // Status only mode
466
+ if (options.status) {
467
+ const status = manager.getBackupStatus();
468
+ console.log("\nBackup Status:\n");
469
+ console.log(` Total backups: ${status.totalBackups}`);
470
+ console.log(` Total size: ${formatBytes(status.totalSizeBytes)}`);
471
+ console.log(` Projects: ${status.projects.join(", ") || "(none)"}`);
472
+ if (status.oldestBackup) {
473
+ console.log(` Oldest backup: ${status.oldestBackup}`);
474
+ }
475
+ if (status.newestBackup) {
476
+ console.log(` Newest backup: ${status.newestBackup}`);
477
+ }
478
+ console.log("");
479
+ return;
480
+ }
481
+ // Check data directory exists
482
+ if (!fs.existsSync(dataPath)) {
483
+ console.error(`Data directory not found: ${dataPath}`);
484
+ process.exit(1);
485
+ }
486
+ // Single project backup
487
+ if (options.project) {
488
+ const sourcePath = path.join(dataPath, "production", `${options.project}.db`);
489
+ if (!fs.existsSync(sourcePath)) {
490
+ console.error(`Project not found: ${options.project}`);
491
+ process.exit(1);
492
+ }
493
+ console.log(`Backing up ${options.project}...`);
494
+ const result = await manager.backupDatabase(sourcePath, options.project);
495
+ if (result.success) {
496
+ console.log(` [success] ${result.backupPath}`);
497
+ console.log(` Size: ${formatBytes(result.sizeBytes || 0)}, Duration: ${result.durationMs}ms`);
498
+ // Cleanup old backups
499
+ const deleted = manager.cleanOldBackups(options.project, keepCount);
500
+ if (deleted > 0) {
501
+ console.log(` Cleaned up ${deleted} old backup(s)`);
502
+ }
503
+ }
504
+ else {
505
+ console.error(` [failed] ${result.error}`);
506
+ process.exit(1);
507
+ }
508
+ return;
509
+ }
510
+ // Backup all databases
511
+ console.log(`\nBacking up databases from ${dataPath}...\n`);
512
+ const results = await manager.backupAll(dataPath, { includeTest: options.includeTest });
513
+ if (results.length === 0) {
514
+ console.log("No databases found to backup.");
515
+ return;
516
+ }
517
+ let successCount = 0;
518
+ let failCount = 0;
519
+ for (const result of results) {
520
+ if (result.success) {
521
+ console.log(` [success] ${result.project} → ${path.basename(result.backupPath)}`);
522
+ successCount++;
523
+ // Cleanup old backups
524
+ const deleted = manager.cleanOldBackups(result.project, keepCount);
525
+ if (deleted > 0) {
526
+ console.log(` Cleaned up ${deleted} old backup(s)`);
527
+ }
528
+ }
529
+ else {
530
+ console.log(` [failed] ${result.project}: ${result.error}`);
531
+ failCount++;
532
+ }
533
+ }
534
+ console.log(`\nBackup complete: ${successCount} succeeded, ${failCount} failed`);
535
+ if (failCount > 0) {
536
+ process.exit(1);
537
+ }
538
+ });
539
+ // ============================================================================
540
+ // apikey - API Key Management
541
+ // ============================================================================
542
+ const apikey = program
543
+ .command("apikey")
544
+ .description("Manage API keys for project access");
545
+ apikey
546
+ .command("add <project>")
547
+ .description("Generate and add a new API key for a project")
548
+ .option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
549
+ .option("-e, --env <env>", "Restrict to specific environment (production/test)")
550
+ .option("--admin", "Create an admin key (ignores project/env)", false)
551
+ .action((project, options) => {
552
+ const dataPath = path.resolve(options.data);
553
+ const keys = loadApiKeys(dataPath);
554
+ // Generate new key
555
+ const newKey = generateApiKey();
556
+ // Build config
557
+ const config = {};
558
+ if (options.admin) {
559
+ config.admin = true;
560
+ }
561
+ else {
562
+ config.project = project;
563
+ if (options.env) {
564
+ if (options.env !== "production" && options.env !== "test") {
565
+ console.error(`Invalid environment: ${options.env}. Must be 'production' or 'test'.`);
566
+ process.exit(1);
567
+ }
568
+ config.env = options.env;
569
+ }
570
+ }
571
+ keys[newKey] = config;
572
+ saveApiKeys(dataPath, keys);
573
+ console.log(`\nAPI Key: ${newKey}`);
574
+ if (options.admin) {
575
+ console.log(`Access: admin (full access)`);
576
+ }
577
+ else {
578
+ console.log(`Project: ${project}`);
579
+ console.log(`Env: ${options.env || "all"}`);
580
+ }
581
+ });
582
+ apikey
583
+ .command("list")
584
+ .description("List all API keys (shows prefixes only)")
585
+ .option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
586
+ .action((options) => {
587
+ const dataPath = path.resolve(options.data);
588
+ const keys = loadApiKeys(dataPath);
589
+ if (Object.keys(keys).length === 0) {
590
+ console.log("No API keys configured.");
591
+ return;
592
+ }
593
+ console.log("\nAPI Keys:\n");
594
+ console.log(" Prefix | Access");
595
+ console.log(" ------------+---------------------------");
596
+ for (const [key, config] of Object.entries(keys)) {
597
+ const prefix = key.slice(0, 8) + "...";
598
+ let access;
599
+ if (config.admin) {
600
+ access = "admin";
601
+ }
602
+ else if (config.project) {
603
+ access = config.env ? `${config.project}/${config.env}` : `${config.project}/*`;
604
+ }
605
+ else {
606
+ access = "*/*";
607
+ }
608
+ console.log(` ${prefix.padEnd(12)}| ${access}`);
609
+ }
610
+ console.log("");
611
+ });
612
+ apikey
613
+ .command("remove <prefix>")
614
+ .description("Remove an API key by its prefix (first 8+ characters)")
615
+ .option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
616
+ .action((prefix, options) => {
617
+ const dataPath = path.resolve(options.data);
618
+ const keys = loadApiKeys(dataPath);
619
+ // Find key by prefix
620
+ const matchingKeys = Object.keys(keys).filter((k) => k.startsWith(prefix));
621
+ if (matchingKeys.length === 0) {
622
+ console.error(`No key found with prefix: ${prefix}`);
623
+ process.exit(1);
624
+ }
625
+ if (matchingKeys.length > 1) {
626
+ console.error(`Multiple keys match prefix '${prefix}'. Please be more specific.`);
627
+ for (const key of matchingKeys) {
628
+ console.error(` - ${key.slice(0, 12)}...`);
629
+ }
630
+ process.exit(1);
631
+ }
632
+ const keyToRemove = matchingKeys[0];
633
+ const config = keys[keyToRemove];
634
+ delete keys[keyToRemove];
635
+ saveApiKeys(dataPath, keys);
636
+ console.log(`\nRemoved API key: ${keyToRemove.slice(0, 8)}...`);
637
+ if (config.admin) {
638
+ console.log(`Access: admin`);
639
+ }
640
+ else {
641
+ console.log(`Access: ${config.project || "*"}/${config.env || "*"}`);
642
+ }
643
+ });
644
+ // ============================================================================
645
+ // Helpers
646
+ // ============================================================================
647
+ function printTable(columns, rows) {
648
+ const widths = calculateColumnWidths(columns, rows);
649
+ // Print header
650
+ const header = columns.map((col) => col.padEnd(widths[col])).join(" | ");
651
+ console.log(` ${header}`);
652
+ console.log(` ${columns.map((col) => "-".repeat(widths[col])).join("-+-")}`);
653
+ // Print rows
654
+ for (const row of rows) {
655
+ console.log(` ${formatTableRow(columns, row, widths)}`);
656
+ }
657
+ }
658
+ // Parse and run
659
+ program.parse();
660
+ //# sourceMappingURL=cli.js.map