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.
- package/LICENSE +37 -0
- package/README.md +161 -0
- package/commands/ab-tests.js +437 -0
- package/commands/agents.js +226 -0
- package/commands/data.js +966 -0
- package/commands/deploy.js +166 -0
- package/commands/dev.js +569 -0
- package/commands/init.js +126 -0
- package/commands/interface/boilerplate.js +52 -0
- package/commands/interface/git-utils.js +85 -0
- package/commands/interface/index.js +7 -0
- package/commands/interface/init.js +375 -0
- package/commands/interface/path.js +74 -0
- package/commands/interface.js +125 -0
- package/commands/knowledge.js +339 -0
- package/commands/link.js +127 -0
- package/commands/list.js +97 -0
- package/commands/login.js +247 -0
- package/commands/logout.js +19 -0
- package/commands/logs.js +182 -0
- package/commands/pricing.js +328 -0
- package/commands/project.js +704 -0
- package/commands/secrets.js +129 -0
- package/commands/servers.js +411 -0
- package/commands/storage.js +177 -0
- package/commands/up.js +211 -0
- package/commands/validate-data-lux.js +502 -0
- package/commands/voice-agents.js +1055 -0
- package/commands/webview.js +393 -0
- package/commands/workflows.js +836 -0
- package/lib/config.js +403 -0
- package/lib/helpers.js +189 -0
- package/lib/node-helper.js +120 -0
- package/lux.js +268 -0
- package/package.json +56 -0
- package/templates/next-env.d.ts +6 -0
package/commands/data.js
ADDED
|
@@ -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 };
|