luxlabs 1.0.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.
@@ -0,0 +1,966 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const {
7
+ getApiUrl,
8
+ getStudioApiUrl,
9
+ getAuthHeaders,
10
+ isAuthenticated,
11
+ getOrgId,
12
+ getProjectId,
13
+ loadConfig,
14
+ } = require('../lib/config');
15
+ const {
16
+ error,
17
+ success,
18
+ info,
19
+ formatJson,
20
+ formatTable,
21
+ requireArgs,
22
+ readFile,
23
+ parseJson,
24
+ } = require('../lib/helpers');
25
+
26
+ // Local storage directory (shared with Electron app)
27
+ const LUX_STUDIO_DIR = path.join(os.homedir(), '.lux-studio');
28
+
29
+ /**
30
+ * Get the tables directory for local storage (project-scoped)
31
+ */
32
+ function getTablesDir() {
33
+ const orgId = getOrgId();
34
+ const projectId = getProjectId();
35
+ if (!orgId) return null;
36
+ return path.join(LUX_STUDIO_DIR, orgId, 'projects', projectId, 'data', 'tables');
37
+ }
38
+
39
+ /**
40
+ * Get the tables API base URL (project-scoped)
41
+ */
42
+ function getTablesApiUrl() {
43
+ const studioApiUrl = getStudioApiUrl();
44
+ const projectId = getProjectId();
45
+ if (!projectId) {
46
+ throw new Error('No project ID found. Run this command from a Lux Studio project directory.');
47
+ }
48
+ return `${studioApiUrl}/api/projects/${projectId}/tables`;
49
+ }
50
+
51
+ /**
52
+ * Get auth headers for Studio API (uses Authorization Bearer)
53
+ */
54
+ function getStudioAuthHeaders() {
55
+ const config = loadConfig();
56
+ if (!config || !config.apiKey || !config.orgId) {
57
+ return {};
58
+ }
59
+ return {
60
+ 'Authorization': `Bearer ${config.apiKey}`,
61
+ 'X-Org-Id': config.orgId,
62
+ 'Content-Type': 'application/json',
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Ensure local storage directories exist
68
+ */
69
+ function ensureLocalDirs() {
70
+ const tablesDir = getTablesDir();
71
+ if (tablesDir && !fs.existsSync(tablesDir)) {
72
+ fs.mkdirSync(tablesDir, { recursive: true });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Validate table name
78
+ * - Must be lowercase alphanumeric with underscores and dots
79
+ * - Cannot start with system. or _ (reserved)
80
+ * - Must start with a letter
81
+ */
82
+ function validateTableName(name) {
83
+ // Only block system tables (matching API's isSystemTable check)
84
+ if (name.startsWith('system.') || name.startsWith('_')) {
85
+ return {
86
+ valid: false,
87
+ error: `Table name "${name}" is reserved. System tables cannot be created.`
88
+ };
89
+ }
90
+
91
+ // Validate format: lowercase alphanumeric with underscores and dots
92
+ if (!/^[a-z][a-z0-9_.]*$/.test(name)) {
93
+ return {
94
+ valid: false,
95
+ error: `Invalid table name "${name}"\n` +
96
+ ` Table names must be lowercase, start with a letter,\n` +
97
+ ` and contain only letters, numbers, underscores, and dots`
98
+ };
99
+ }
100
+
101
+ return { valid: true };
102
+ }
103
+
104
+ /**
105
+ * Save a table schema to local storage
106
+ * Project-scoped tables (no user. prefix)
107
+ */
108
+ function saveTableToLocal(tableName, columns = []) {
109
+ const tablesDir = getTablesDir();
110
+ if (!tablesDir) {
111
+ console.log(chalk.dim(' (No orgId - skipping local storage)'));
112
+ return;
113
+ }
114
+
115
+ ensureLocalDirs();
116
+
117
+ const tableSchema = {
118
+ id: tableName,
119
+ name: tableName,
120
+ type: 'user',
121
+ read_only: false,
122
+ schema: columns.map(col => ({
123
+ name: col.name,
124
+ type: col.type,
125
+ primaryKey: col.primaryKey || false,
126
+ notNull: col.notNull || false,
127
+ defaultValue: col.defaultValue || null,
128
+ })),
129
+ indexes: [],
130
+ syncedAt: new Date().toISOString(),
131
+ };
132
+
133
+ const safeName = tableName.replace(/[<>:"/\\|?*]/g, '_');
134
+ const tablePath = path.join(tablesDir, `${safeName}.json`);
135
+ fs.writeFileSync(tablePath, JSON.stringify(tableSchema, null, 2));
136
+ console.log(chalk.dim(` (Saved to local: ${tablePath})`));
137
+ }
138
+
139
+ /**
140
+ * Delete a table schema from local storage
141
+ */
142
+ function deleteTableFromLocal(tableName) {
143
+ const tablesDir = getTablesDir();
144
+ if (!tablesDir) return;
145
+
146
+ const safeName = tableName.replace(/[<>:"/\\|?*]/g, '_');
147
+ const tablePath = path.join(tablesDir, `${safeName}.json`);
148
+
149
+ if (fs.existsSync(tablePath)) {
150
+ fs.unlinkSync(tablePath);
151
+ console.log(chalk.dim(` (Removed from local: ${tablePath})`));
152
+ }
153
+ }
154
+
155
+ async function handleData(args) {
156
+ // Check authentication
157
+ if (!isAuthenticated()) {
158
+ console.log(
159
+ chalk.red('❌ Not authenticated. Run'),
160
+ chalk.white('lux login'),
161
+ chalk.red('first.')
162
+ );
163
+ process.exit(1);
164
+ }
165
+
166
+ const command = args[0];
167
+
168
+ if (!command) {
169
+ console.log(`
170
+ ${chalk.bold('Usage:')} lux data <command> [args]
171
+
172
+ ${chalk.bold('Table Commands:')}
173
+ tables list List all tables
174
+ tables sync Sync tables to local storage
175
+ tables init <name> [--schema <json>] Create new table
176
+ tables get <table-name> Get table schema
177
+ tables delete <table-name> Delete table
178
+ tables query <table-name> [limit] List rows (default limit: 100)
179
+ tables insert <table-name> <json> Insert row
180
+ tables update <table-name> <row-id> <json> Update row by rowid
181
+ tables delete-row <table-name> <row-id> Delete row by rowid
182
+ tables export <table-name> [file] Export table to CSV
183
+ tables import <table-name> <csv-file> Import CSV to table
184
+
185
+ ${chalk.bold('KV Commands:')}
186
+ kv list List all KV namespaces
187
+ kv init <name> [description] Create new KV namespace
188
+ kv delete <namespace-id> Delete namespace
189
+ kv keys <namespace-id> List all keys in namespace
190
+ kv get <namespace-id> <key> Get key value
191
+ kv set <namespace-id> <key> <value> Set key value
192
+ kv delete-key <namespace-id> <key> Delete key
193
+ kv export <namespace-id> [file] Export namespace to JSON
194
+ kv import <namespace-id> <json-file> Import JSON to namespace
195
+ kv sync [namespace-id] Sync KV data to local storage
196
+ kv local <namespace-id> Read KV from local storage
197
+
198
+ ${chalk.bold('Examples:')}
199
+ lux data tables init customers --schema '{"columns":[{"name":"id","type":"TEXT","primaryKey":true}]}'
200
+ lux data tables init customers (interactive mode)
201
+ lux data tables list
202
+ lux data tables query customers 50
203
+ lux data tables insert customers '{"id":"1","name":"John"}'
204
+ lux data kv init sessions "User session storage"
205
+ lux data kv set kv_abc user:123 '{"name":"John"}'
206
+ `);
207
+ process.exit(0);
208
+ }
209
+
210
+ const apiUrl = getApiUrl();
211
+
212
+ try {
213
+ // ============ TABLE COMMANDS ============
214
+ if (command === 'tables') {
215
+ const subCommand = args[1];
216
+
217
+ if (!subCommand) {
218
+ error('Missing table subcommand. Run "lux data" to see available commands.');
219
+ return;
220
+ }
221
+
222
+ switch (subCommand) {
223
+ case 'sync': {
224
+ // Sync all tables from cloud to local storage
225
+ info('Syncing tables to local storage...');
226
+ const tablesApiUrl = getTablesApiUrl();
227
+ const { data } = await axios.get(tablesApiUrl, {
228
+ headers: getStudioAuthHeaders(),
229
+ });
230
+
231
+ if (!data.tables || data.tables.length === 0) {
232
+ console.log('\n(No tables to sync)\n');
233
+ } else {
234
+ ensureLocalDirs();
235
+ let synced = 0;
236
+
237
+ for (const table of data.tables) {
238
+ const tableName = table.name;
239
+ if (!tableName) continue;
240
+
241
+ try {
242
+ // Fetch table schema details
243
+ const detailResponse = await axios.get(
244
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}`,
245
+ { headers: getStudioAuthHeaders() }
246
+ );
247
+
248
+ // New API returns { table: { columns } }
249
+ const tableData = detailResponse.data.table || {};
250
+ const columns = tableData.columns || [];
251
+
252
+ // Save to local storage
253
+ const tablesDir = getTablesDir();
254
+ if (tablesDir) {
255
+ const tableSchema = {
256
+ id: tableName,
257
+ name: tableName,
258
+ type: 'user',
259
+ read_only: false,
260
+ schema: columns.map(col => ({
261
+ name: col.name,
262
+ type: col.type,
263
+ primaryKey: col.pk === 1,
264
+ notNull: col.notnull === 1,
265
+ defaultValue: col.dflt_value,
266
+ })),
267
+ indexes: [],
268
+ syncedAt: new Date().toISOString(),
269
+ };
270
+
271
+ const safeName = tableName.replace(/[<>:"/\\|?*]/g, '_');
272
+ const tablePath = path.join(tablesDir, `${safeName}.json`);
273
+ fs.writeFileSync(tablePath, JSON.stringify(tableSchema, null, 2));
274
+ console.log(chalk.dim(` ✓ ${tableName}`));
275
+ synced++;
276
+ }
277
+ } catch (err) {
278
+ console.log(chalk.yellow(` ✗ ${tableName}: ${err.message}`));
279
+ }
280
+ }
281
+
282
+ success(`Synced ${synced}/${data.tables.length} tables to local storage`);
283
+ const tablesDir = getTablesDir();
284
+ if (tablesDir) {
285
+ console.log(chalk.dim(` Location: ${tablesDir}`));
286
+ }
287
+ }
288
+ break;
289
+ }
290
+
291
+ case 'list': {
292
+ info('Loading tables...');
293
+ const tablesApiUrl = getTablesApiUrl();
294
+ const { data } = await axios.get(tablesApiUrl, {
295
+ headers: getStudioAuthHeaders(),
296
+ });
297
+
298
+ if (!data.tables || data.tables.length === 0) {
299
+ console.log('\n(No tables found)\n');
300
+ } else {
301
+ console.log(`\nFound ${data.tables.length} table(s):\n`);
302
+ formatTable(data.tables.map(t => ({
303
+ name: t.name,
304
+ columns: t.columnCount || 0,
305
+ rows: t.rowCount || 0,
306
+ })));
307
+ console.log('');
308
+ }
309
+ break;
310
+ }
311
+
312
+ case 'init': {
313
+ requireArgs(args.slice(2), 1, 'lux data tables init <name> [--schema <json>]');
314
+ let name = args[2];
315
+
316
+ // Validate table name
317
+ const validation = validateTableName(name);
318
+ if (!validation.valid) {
319
+ error(validation.error);
320
+ process.exit(1);
321
+ }
322
+
323
+ // Check for --schema flag for non-interactive mode
324
+ const schemaFlagIndex = args.indexOf('--schema');
325
+ let schema;
326
+
327
+ if (schemaFlagIndex !== -1 && args[schemaFlagIndex + 1]) {
328
+ // Non-interactive mode: parse schema from --schema flag
329
+ const schemaArg = args[schemaFlagIndex + 1];
330
+ schema = parseJson(schemaArg, 'schema');
331
+ } else {
332
+ // Interactive mode: prompt for schema
333
+ console.log('\n' + chalk.yellow('📝 Define your table schema in JSON format:'));
334
+ console.log(chalk.gray('Example:'));
335
+ console.log(chalk.gray(JSON.stringify({
336
+ columns: [
337
+ { name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
338
+ { name: 'email', type: 'TEXT', notNull: true },
339
+ { name: 'created_at', type: 'INTEGER', notNull: true }
340
+ ]
341
+ }, null, 2)));
342
+ console.log('\n' + chalk.yellow('Paste your schema JSON (press Enter twice when done):'));
343
+
344
+ // Read multiline input
345
+ const lines = [];
346
+ const readline = require('readline').createInterface({
347
+ input: process.stdin,
348
+ output: process.stdout
349
+ });
350
+
351
+ let emptyLineCount = 0;
352
+ for await (const line of readline) {
353
+ if (line.trim() === '') {
354
+ emptyLineCount++;
355
+ if (emptyLineCount >= 2) {
356
+ break;
357
+ }
358
+ } else {
359
+ emptyLineCount = 0;
360
+ lines.push(line);
361
+ }
362
+ }
363
+ readline.close();
364
+
365
+ const schemaJson = lines.join('\n');
366
+ schema = parseJson(schemaJson, 'schema');
367
+ }
368
+
369
+ info(`Creating table: ${name}`);
370
+ const tablesApiUrl = getTablesApiUrl();
371
+
372
+ // Map columns to API format (matching Electron's format)
373
+ const apiColumns = schema.columns.map(col => ({
374
+ name: col.name,
375
+ type: col.type.toUpperCase(), // API expects uppercase types: TEXT, INTEGER, REAL, BLOB
376
+ nullable: !col.notNull,
377
+ primaryKey: col.primaryKey || false,
378
+ defaultValue: col.defaultValue,
379
+ }));
380
+
381
+ const { data } = await axios.post(
382
+ tablesApiUrl,
383
+ { name, columns: apiColumns },
384
+ { headers: getStudioAuthHeaders() }
385
+ );
386
+
387
+ success('Table created!');
388
+ console.log(` Name: ${data.table.name}`);
389
+
390
+ // Save to local storage for Electron app
391
+ saveTableToLocal(data.table.name, schema.columns);
392
+ break;
393
+ }
394
+
395
+ case 'get': {
396
+ requireArgs(args.slice(2), 1, 'lux data tables get <table-name>');
397
+ const tableName = args[2];
398
+
399
+ info(`Loading table: ${tableName}`);
400
+ const tablesApiUrl = getTablesApiUrl();
401
+ const { data } = await axios.get(
402
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}`,
403
+ { headers: getStudioAuthHeaders() }
404
+ );
405
+
406
+ console.log(`\n📝 Table: ${data.table.name}`);
407
+ console.log(`📊 Columns: ${data.table.columnCount}`);
408
+ console.log(`\n📋 Schema:\n`);
409
+ formatTable(data.table.columns.map(col => ({
410
+ name: col.name,
411
+ type: col.type,
412
+ primaryKey: col.pk === 1 ? 'Yes' : 'No',
413
+ notNull: col.notnull === 1 ? 'Yes' : 'No',
414
+ })));
415
+ console.log('');
416
+ break;
417
+ }
418
+
419
+ case 'delete': {
420
+ requireArgs(args.slice(2), 1, 'lux data tables delete <table-name>');
421
+ const tableName = args[2];
422
+
423
+ info(`Deleting table: ${tableName}`);
424
+ const tablesApiUrl = getTablesApiUrl();
425
+ await axios.delete(
426
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}`,
427
+ { headers: getStudioAuthHeaders() }
428
+ );
429
+
430
+ success('Table deleted!');
431
+
432
+ // Remove from local storage
433
+ deleteTableFromLocal(tableName);
434
+ break;
435
+ }
436
+
437
+ case 'query': {
438
+ // Query command - list rows from table
439
+ requireArgs(args.slice(2), 1, 'lux data tables query <table-name> [limit]');
440
+ const tableName = args[2];
441
+ const limit = args[3] ? parseInt(args[3]) : 100;
442
+
443
+ info(`Fetching rows from ${tableName}...`);
444
+ const tablesApiUrl = getTablesApiUrl();
445
+ const { data } = await axios.get(
446
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows?limit=${limit}`,
447
+ { headers: getStudioAuthHeaders() }
448
+ );
449
+
450
+ if (!data.rows || data.rows.length === 0) {
451
+ console.log('\n(No rows found)\n');
452
+ } else {
453
+ console.log('');
454
+ formatTable(data.rows);
455
+ console.log(`\n${chalk.green('✓')} ${data.rows.length} row(s) returned (total: ${data.pagination.total})\n`);
456
+ }
457
+ break;
458
+ }
459
+
460
+ case 'insert': {
461
+ requireArgs(args.slice(2), 2, 'lux data tables insert <table-name> <json>');
462
+ const tableName = args[2];
463
+ const jsonData = args.slice(3).join(' ');
464
+ const rowData = parseJson(jsonData, 'row data');
465
+
466
+ info('Inserting row...');
467
+ const tablesApiUrl = getTablesApiUrl();
468
+ const { data } = await axios.post(
469
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows`,
470
+ rowData,
471
+ { headers: getStudioAuthHeaders() }
472
+ );
473
+
474
+ success('Row inserted!');
475
+ if (data.row) {
476
+ console.log(` Row ID: ${data.row.rowid}`);
477
+ }
478
+ break;
479
+ }
480
+
481
+ case 'update': {
482
+ requireArgs(args.slice(2), 3, 'lux data tables update <table-name> <row-id> <json>');
483
+ const tableName = args[2];
484
+ const rowId = args[3];
485
+ const jsonData = args.slice(4).join(' ');
486
+ const rowData = parseJson(jsonData, 'row data');
487
+
488
+ info('Updating row...');
489
+ const tablesApiUrl = getTablesApiUrl();
490
+ const { data } = await axios.put(
491
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows/${rowId}`,
492
+ rowData,
493
+ { headers: getStudioAuthHeaders() }
494
+ );
495
+
496
+ success('Row updated!');
497
+ break;
498
+ }
499
+
500
+ case 'delete-row': {
501
+ requireArgs(args.slice(2), 2, 'lux data tables delete-row <table-name> <row-id>');
502
+ const tableName = args[2];
503
+ const rowId = args[3];
504
+
505
+ info('Deleting row...');
506
+ const tablesApiUrl = getTablesApiUrl();
507
+ await axios.delete(
508
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows/${rowId}`,
509
+ { headers: getStudioAuthHeaders() }
510
+ );
511
+
512
+ success('Row deleted!');
513
+ break;
514
+ }
515
+
516
+ case 'export': {
517
+ requireArgs(args.slice(2), 1, 'lux data tables export <table-name> [file]');
518
+ const tableName = args[2];
519
+ const outputFile = args[3];
520
+
521
+ info('Exporting table...');
522
+ const tablesApiUrl = getTablesApiUrl();
523
+ const { data } = await axios.get(
524
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows?limit=10000`,
525
+ { headers: getStudioAuthHeaders() }
526
+ );
527
+
528
+ if (!data.rows || data.rows.length === 0) {
529
+ console.log('\n(No rows to export)\n');
530
+ return;
531
+ }
532
+
533
+ // Convert to CSV
534
+ const headers = Object.keys(data.rows[0]).filter(h => h !== 'rowid');
535
+ const csv = [
536
+ headers.join(','),
537
+ ...data.rows.map(row => headers.map(h => JSON.stringify(row[h])).join(','))
538
+ ].join('\n');
539
+
540
+ if (outputFile) {
541
+ fs.writeFileSync(outputFile, csv);
542
+ success(`Exported ${data.rows.length} rows to ${outputFile}`);
543
+ } else {
544
+ console.log('\n' + csv + '\n');
545
+ }
546
+ break;
547
+ }
548
+
549
+ case 'import': {
550
+ requireArgs(args.slice(2), 2, 'lux data tables import <table-name> <csv-file>');
551
+ const tableName = args[2];
552
+ const csvFile = args[3];
553
+
554
+ const csvContent = readFile(csvFile);
555
+ const lines = csvContent.trim().split('\n');
556
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
557
+ const rows = lines.slice(1).map(line => {
558
+ const values = line.split(',').map(v => {
559
+ const trimmed = v.trim().replace(/^"|"$/g, '');
560
+ try {
561
+ return JSON.parse(trimmed);
562
+ } catch {
563
+ return trimmed;
564
+ }
565
+ });
566
+ const row = {};
567
+ headers.forEach((h, i) => row[h] = values[i]);
568
+ return row;
569
+ });
570
+
571
+ info(`Importing ${rows.length} rows...`);
572
+ const tablesApiUrl = getTablesApiUrl();
573
+
574
+ // Use bulk insert
575
+ const { data } = await axios.post(
576
+ `${tablesApiUrl}/${encodeURIComponent(tableName)}/rows/bulk`,
577
+ { rows },
578
+ { headers: getStudioAuthHeaders() }
579
+ );
580
+
581
+ success(`Imported ${data.inserted}/${rows.length} rows`);
582
+ if (data.errors && data.errors.length > 0) {
583
+ console.log(chalk.yellow(` ${data.errors.length} rows failed`));
584
+ }
585
+ break;
586
+ }
587
+
588
+ default:
589
+ error(`Unknown table subcommand: ${subCommand}`);
590
+ }
591
+ }
592
+
593
+ // ============ KV COMMANDS ============
594
+ else if (command === 'kv') {
595
+ const subCommand = args[1];
596
+
597
+ if (!subCommand) {
598
+ error('Missing KV subcommand. Run "lux data" to see available commands.');
599
+ return;
600
+ }
601
+
602
+ switch (subCommand) {
603
+ case 'list': {
604
+ info('Loading KV namespaces...');
605
+ const { data } = await axios.get(`${apiUrl}/api/kv`, {
606
+ headers: getAuthHeaders(),
607
+ });
608
+
609
+ if (!data.namespaces || data.namespaces.length === 0) {
610
+ console.log('\n(No KV namespaces found)\n');
611
+ } else {
612
+ console.log(`\nFound ${data.namespaces.length} namespace(s):\n`);
613
+ formatTable(data.namespaces.map(ns => ({
614
+ id: ns.id,
615
+ name: ns.name,
616
+ description: ns.description || '',
617
+ created_at: new Date(ns.created_at * 1000).toLocaleString(),
618
+ })));
619
+ console.log('');
620
+ }
621
+ break;
622
+ }
623
+
624
+ case 'init': {
625
+ requireArgs(args.slice(2), 1, 'lux data kv init <name> [description]');
626
+ const name = args[2];
627
+ const description = args[3] || '';
628
+
629
+ info(`Creating KV namespace: ${name}`);
630
+ const { data } = await axios.post(
631
+ `${apiUrl}/api/kv`,
632
+ { name, description },
633
+ { headers: getAuthHeaders() }
634
+ );
635
+
636
+ success('KV namespace created!');
637
+ console.log(` ID: ${data.namespace.id}`);
638
+ console.log(` Name: ${data.namespace.name}`);
639
+ break;
640
+ }
641
+
642
+ case 'delete': {
643
+ requireArgs(args.slice(2), 1, 'lux data kv delete <namespace-id>');
644
+ const namespaceId = args[2];
645
+
646
+ info(`Deleting namespace: ${namespaceId}`);
647
+ await axios.delete(
648
+ `${apiUrl}/api/kv/${namespaceId}`,
649
+ { headers: getAuthHeaders() }
650
+ );
651
+
652
+ success('Namespace deleted!');
653
+ break;
654
+ }
655
+
656
+ case 'keys': {
657
+ requireArgs(args.slice(2), 1, 'lux data kv keys <namespace-id>');
658
+ const namespaceId = args[2];
659
+
660
+ info('Loading keys...');
661
+ const { data } = await axios.get(
662
+ `${apiUrl}/api/kv/${namespaceId}/keys`,
663
+ { headers: getAuthHeaders() }
664
+ );
665
+
666
+ if (!data.keys || data.keys.length === 0) {
667
+ console.log('\n(No keys found)\n');
668
+ } else {
669
+ console.log(`\nFound ${data.keys.length} key(s):\n`);
670
+ data.keys.forEach(key => console.log(` - ${key.name}`));
671
+ console.log('');
672
+ }
673
+ break;
674
+ }
675
+
676
+ case 'get': {
677
+ requireArgs(args.slice(2), 2, 'lux data kv get <namespace-id> <key>');
678
+ const namespaceId = args[2];
679
+ const key = args[3];
680
+
681
+ info(`Getting key: ${key}`);
682
+ const { data } = await axios.get(
683
+ `${apiUrl}/api/kv/${namespaceId}/keys/${encodeURIComponent(key)}`,
684
+ { headers: getAuthHeaders() }
685
+ );
686
+
687
+ console.log(`\nKey: ${data.key}`);
688
+ console.log(`Value:\n`);
689
+ try {
690
+ console.log(formatJson(JSON.parse(data.value)));
691
+ } catch {
692
+ console.log(data.value);
693
+ }
694
+ console.log('');
695
+ break;
696
+ }
697
+
698
+ case 'set': {
699
+ requireArgs(args.slice(2), 3, 'lux data kv set <namespace-id> <key> <value>');
700
+ const namespaceId = args[2];
701
+ const key = args[3];
702
+ const value = args.slice(4).join(' ');
703
+
704
+ info(`Setting key: ${key}`);
705
+ await axios.put(
706
+ `${apiUrl}/api/kv/${namespaceId}/keys/${encodeURIComponent(key)}`,
707
+ { value },
708
+ { headers: getAuthHeaders() }
709
+ );
710
+
711
+ success('Key set!');
712
+ break;
713
+ }
714
+
715
+ case 'delete-key': {
716
+ requireArgs(args.slice(2), 2, 'lux data kv delete-key <namespace-id> <key>');
717
+ const namespaceId = args[2];
718
+ const key = args[3];
719
+
720
+ info(`Deleting key: ${key}`);
721
+ await axios.delete(
722
+ `${apiUrl}/api/kv/${namespaceId}/keys/${encodeURIComponent(key)}`,
723
+ { headers: getAuthHeaders() }
724
+ );
725
+
726
+ success('Key deleted!');
727
+ break;
728
+ }
729
+
730
+ case 'export': {
731
+ requireArgs(args.slice(2), 1, 'lux data kv export <namespace-id> [file]');
732
+ const namespaceId = args[2];
733
+ const outputFile = args[3];
734
+
735
+ info('Exporting namespace...');
736
+
737
+ // Get all keys
738
+ const { data: keysData } = await axios.get(
739
+ `${apiUrl}/api/kv/${namespaceId}/keys`,
740
+ { headers: getAuthHeaders() }
741
+ );
742
+
743
+ if (!keysData.keys || keysData.keys.length === 0) {
744
+ console.log('\n(No keys to export)\n');
745
+ return;
746
+ }
747
+
748
+ // Get all values
749
+ const exported = {};
750
+ for (const keyInfo of keysData.keys) {
751
+ const { data } = await axios.get(
752
+ `${apiUrl}/api/kv/${namespaceId}/keys/${encodeURIComponent(keyInfo.name)}`,
753
+ { headers: getAuthHeaders() }
754
+ );
755
+ exported[data.key] = data.value;
756
+ }
757
+
758
+ const json = JSON.stringify(exported, null, 2);
759
+
760
+ if (outputFile) {
761
+ fs.writeFileSync(outputFile, json);
762
+ success(`Exported ${keysData.keys.length} keys to ${outputFile}`);
763
+ } else {
764
+ console.log('\n' + json + '\n');
765
+ }
766
+ break;
767
+ }
768
+
769
+ case 'import': {
770
+ requireArgs(args.slice(2), 2, 'lux data kv import <namespace-id> <json-file>');
771
+ const namespaceId = args[2];
772
+ const jsonFile = args[3];
773
+
774
+ const jsonContent = readFile(jsonFile);
775
+ const data = parseJson(jsonContent, 'KV data');
776
+
777
+ const pairs = Object.entries(data).map(([key, value]) => ({
778
+ key,
779
+ value: typeof value === 'string' ? value : JSON.stringify(value),
780
+ }));
781
+
782
+ info(`Importing ${pairs.length} keys...`);
783
+ await axios.post(
784
+ `${apiUrl}/api/kv/${namespaceId}/bulk`,
785
+ { pairs },
786
+ { headers: getAuthHeaders() }
787
+ );
788
+
789
+ success(`Imported ${pairs.length} keys`);
790
+ break;
791
+ }
792
+
793
+ case 'sync': {
794
+ // Sync KV data from cloud to local storage
795
+ const namespaceId = args[2]; // Optional - if not provided, sync all
796
+
797
+ // Get org ID and storage path
798
+ const { getOrgId } = require('../lib/config');
799
+ const orgId = getOrgId();
800
+ const os = require('os');
801
+ const kvDir = path.join(os.homedir(), '.lux-studio', orgId, 'data', 'kv');
802
+
803
+ if (!fs.existsSync(kvDir)) {
804
+ fs.mkdirSync(kvDir, { recursive: true });
805
+ }
806
+
807
+ if (namespaceId) {
808
+ // Sync single namespace
809
+ info(`Syncing namespace ${namespaceId} to local storage...`);
810
+
811
+ // Fetch all keys with values
812
+ const { data: keysData } = await axios.get(
813
+ `${apiUrl}/api/kv/${namespaceId}/keys?limit=1000&fetchValues=true`,
814
+ { headers: getAuthHeaders() }
815
+ );
816
+
817
+ const keys = keysData.keys || [];
818
+ const nsDir = path.join(kvDir, namespaceId);
819
+ if (!fs.existsSync(nsDir)) {
820
+ fs.mkdirSync(nsDir, { recursive: true });
821
+ }
822
+
823
+ // Build index and save values
824
+ const index = {};
825
+ for (const k of keys) {
826
+ const sanitizedKey = k.name.replace(/[^a-zA-Z0-9._-]/g, '_');
827
+ index[k.name] = { sanitizedKey, metadata: k.metadata };
828
+
829
+ const data = {
830
+ key: k.name,
831
+ value: k.value || '',
832
+ metadata: k.metadata,
833
+ syncedAt: new Date().toISOString(),
834
+ };
835
+ fs.writeFileSync(
836
+ path.join(nsDir, `${sanitizedKey}.json`),
837
+ JSON.stringify(data, null, 2)
838
+ );
839
+ }
840
+ fs.writeFileSync(
841
+ path.join(nsDir, '_index.json'),
842
+ JSON.stringify(index, null, 2)
843
+ );
844
+
845
+ success(`Synced ${keys.length} keys to ${nsDir}`);
846
+ } else {
847
+ // Sync all namespaces
848
+ info('Syncing all KV namespaces to local storage...');
849
+
850
+ const { data: nsData } = await axios.get(`${apiUrl}/api/kv`, {
851
+ headers: getAuthHeaders(),
852
+ });
853
+
854
+ const namespaces = nsData.namespaces || [];
855
+ let totalKeys = 0;
856
+
857
+ for (const ns of namespaces) {
858
+ try {
859
+ const { data: keysData } = await axios.get(
860
+ `${apiUrl}/api/kv/${ns.id}/keys?limit=1000&fetchValues=true`,
861
+ { headers: getAuthHeaders() }
862
+ );
863
+
864
+ const keys = keysData.keys || [];
865
+ const nsDir = path.join(kvDir, ns.id);
866
+ if (!fs.existsSync(nsDir)) {
867
+ fs.mkdirSync(nsDir, { recursive: true });
868
+ }
869
+
870
+ const index = {};
871
+ for (const k of keys) {
872
+ const sanitizedKey = k.name.replace(/[^a-zA-Z0-9._-]/g, '_');
873
+ index[k.name] = { sanitizedKey, metadata: k.metadata };
874
+
875
+ const data = {
876
+ key: k.name,
877
+ value: k.value || '',
878
+ metadata: k.metadata,
879
+ syncedAt: new Date().toISOString(),
880
+ };
881
+ fs.writeFileSync(
882
+ path.join(nsDir, `${sanitizedKey}.json`),
883
+ JSON.stringify(data, null, 2)
884
+ );
885
+ }
886
+ fs.writeFileSync(
887
+ path.join(nsDir, '_index.json'),
888
+ JSON.stringify(index, null, 2)
889
+ );
890
+
891
+ totalKeys += keys.length;
892
+ console.log(` ✓ ${ns.name}: ${keys.length} keys`);
893
+ } catch (err) {
894
+ console.error(` ✗ ${ns.name}: ${err.message}`);
895
+ }
896
+ }
897
+
898
+ success(`Synced ${namespaces.length} namespaces (${totalKeys} total keys) to ${kvDir}`);
899
+ }
900
+ break;
901
+ }
902
+
903
+ case 'local': {
904
+ // Read KV from local storage
905
+ requireArgs(args.slice(2), 1, 'lux data kv local <namespace-id>');
906
+ const namespaceId = args[2];
907
+
908
+ const { getOrgId } = require('../lib/config');
909
+ const orgId = getOrgId();
910
+ const os = require('os');
911
+ const nsDir = path.join(os.homedir(), '.lux-studio', orgId, 'data', 'kv', namespaceId);
912
+
913
+ if (!fs.existsSync(nsDir)) {
914
+ console.log('\n(No local data found. Run "lux data kv sync" first.)\n');
915
+ return;
916
+ }
917
+
918
+ const indexPath = path.join(nsDir, '_index.json');
919
+ if (!fs.existsSync(indexPath)) {
920
+ console.log('\n(No index found. Run "lux data kv sync" first.)\n');
921
+ return;
922
+ }
923
+
924
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
925
+ const keys = Object.keys(index);
926
+
927
+ if (keys.length === 0) {
928
+ console.log('\n(No keys in local storage)\n');
929
+ return;
930
+ }
931
+
932
+ console.log(`\nFound ${keys.length} key(s) in local storage:\n`);
933
+ for (const key of keys) {
934
+ const entry = index[key];
935
+ const filePath = path.join(nsDir, `${entry.sanitizedKey}.json`);
936
+ if (fs.existsSync(filePath)) {
937
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
938
+ console.log(` ${chalk.cyan(key)}:`);
939
+ try {
940
+ console.log(` ${formatJson(JSON.parse(data.value))}`);
941
+ } catch {
942
+ console.log(` ${data.value}`);
943
+ }
944
+ console.log(` ${chalk.gray(`(synced: ${data.syncedAt})`)}`);
945
+ }
946
+ }
947
+ console.log('');
948
+ break;
949
+ }
950
+
951
+ default:
952
+ error(`Unknown KV subcommand: ${subCommand}`);
953
+ }
954
+ }
955
+
956
+ else {
957
+ error(`Unknown command: ${command}\n\nRun 'lux data' to see available commands`);
958
+ }
959
+ } catch (err) {
960
+ const errorMessage =
961
+ err.response?.data?.error || err.message || 'Unknown error';
962
+ error(`${errorMessage}`);
963
+ }
964
+ }
965
+
966
+ module.exports = { handleData };