luxlabs 1.0.10 → 1.0.11
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/commands/ab-tests.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A/B Tests Commands
|
|
3
3
|
*
|
|
4
|
-
* CLI commands for reading A/B test configurations.
|
|
4
|
+
* CLI commands for reading and managing A/B test configurations.
|
|
5
5
|
* These are primarily used by Claude Code to understand what variants exist
|
|
6
6
|
* and what they should do (via the descriptions).
|
|
7
7
|
*
|
|
8
|
-
* Commands:
|
|
8
|
+
* Read Commands:
|
|
9
9
|
* - lux ab-tests list-tests [interface] List all A/B tests
|
|
10
10
|
* - lux ab-tests get-test <key> [interface] Get details for a specific test
|
|
11
11
|
* - lux ab-tests get-variant <key> <variant> [interface] Get details for a specific variant
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle Commands (called by agent after implementing changes):
|
|
14
|
+
* - lux ab-tests create-finished <test-id> --interface <id> Sync new test to PostHog
|
|
15
|
+
* - lux ab-tests update-finished <test-id> --interface <id> Sync updated test to PostHog
|
|
16
|
+
* - lux ab-tests delete-finished <test-id> --interface <id> Remove test from PostHog
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
const chalk = require('chalk');
|
|
15
20
|
const fs = require('fs');
|
|
16
21
|
const path = require('path');
|
|
22
|
+
const ora = require('ora');
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Show help for ab-tests commands
|
|
20
26
|
*/
|
|
21
27
|
function showHelp() {
|
|
22
28
|
console.log(chalk.cyan('\nLux A/B Tests Commands:\n'));
|
|
29
|
+
console.log(chalk.yellow('Read Commands:\n'));
|
|
23
30
|
console.log(chalk.white(' lux ab-tests list-tests [interface]'));
|
|
24
31
|
console.log(chalk.dim(' List all A/B tests for an interface'));
|
|
25
32
|
console.log(chalk.dim(' Options: --json (JSON output)\n'));
|
|
@@ -29,6 +36,16 @@ function showHelp() {
|
|
|
29
36
|
console.log(chalk.white(' lux ab-tests get-variant <test-key> <variant-key> [interface]'));
|
|
30
37
|
console.log(chalk.dim(' Get details for a specific variant within a test'));
|
|
31
38
|
console.log(chalk.dim(' Options: --json (JSON output)\n'));
|
|
39
|
+
console.log(chalk.yellow('Lifecycle Commands (called by agent after implementing changes):\n'));
|
|
40
|
+
console.log(chalk.white(' lux ab-tests create-finished <test-id> --interface <id>'));
|
|
41
|
+
console.log(chalk.dim(' Sync a new A/B test to PostHog after implementation'));
|
|
42
|
+
console.log(chalk.dim(' Updates status to "active"\n'));
|
|
43
|
+
console.log(chalk.white(' lux ab-tests update-finished <test-id> --interface <id>'));
|
|
44
|
+
console.log(chalk.dim(' Sync updated A/B test to PostHog after code changes'));
|
|
45
|
+
console.log(chalk.dim(' Updates status to "active"\n'));
|
|
46
|
+
console.log(chalk.white(' lux ab-tests delete-finished <test-id> --interface <id>'));
|
|
47
|
+
console.log(chalk.dim(' Remove A/B test from PostHog and database after code removal'));
|
|
48
|
+
console.log(chalk.dim(' Deletes test completely\n'));
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
/**
|
|
@@ -264,15 +281,15 @@ async function getTest(testKey, interfaceId, options) {
|
|
|
264
281
|
}
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
// Show implementation pattern
|
|
268
|
-
|
|
269
|
-
if (nonControlVariants.length > 0) {
|
|
284
|
+
// Show implementation pattern - all variants are now UUID-keyed
|
|
285
|
+
if (test.variants.length > 0) {
|
|
270
286
|
console.log(chalk.dim('\n Implementation Pattern:'));
|
|
271
287
|
console.log(chalk.gray(` import { useFeatureFlagVariantKey } from 'posthog-js/react';`));
|
|
272
288
|
console.log(chalk.gray(` const variant = useFeatureFlagVariantKey('${test.key}');`));
|
|
273
|
-
for (const v of
|
|
289
|
+
for (const v of test.variants) {
|
|
290
|
+
const letter = v.letter || 'A';
|
|
274
291
|
const comment = v.description ? ` /* ${v.description} */` : '';
|
|
275
|
-
console.log(chalk.gray(` if (variant === '${v.key}') {${comment} }`));
|
|
292
|
+
console.log(chalk.gray(` if (variant === '${v.key}') { // Variant ${letter}${comment} }`));
|
|
276
293
|
}
|
|
277
294
|
}
|
|
278
295
|
|
|
@@ -347,19 +364,18 @@ async function getVariant(testKey, variantKey, interfaceId, options) {
|
|
|
347
364
|
}
|
|
348
365
|
}
|
|
349
366
|
|
|
350
|
-
// Show implementation hint
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
console.log(chalk.dim('\n Note: This is the control/baseline variant (default behavior).'));
|
|
367
|
+
// Show implementation hint - all variants now have UUID keys
|
|
368
|
+
const isFirstVariant = variant.letter === 'A';
|
|
369
|
+
console.log(chalk.dim('\n Implementation:'));
|
|
370
|
+
console.log(chalk.gray(` import { useFeatureFlagVariantKey } from 'posthog-js/react';`));
|
|
371
|
+
console.log(chalk.gray(` const variant = useFeatureFlagVariantKey('${test.key}');`));
|
|
372
|
+
console.log(chalk.gray(` if (variant === '${variant.key}') { // Variant ${variant.letter || 'A'}`));
|
|
373
|
+
if (variant.description) {
|
|
374
|
+
console.log(chalk.gray(` // ${variant.description}`));
|
|
375
|
+
}
|
|
376
|
+
console.log(chalk.gray(` }`));
|
|
377
|
+
if (isFirstVariant) {
|
|
378
|
+
console.log(chalk.dim('\n Note: This is variant A (typically the baseline/control).'));
|
|
363
379
|
}
|
|
364
380
|
|
|
365
381
|
console.log();
|
|
@@ -369,12 +385,18 @@ async function getVariant(testKey, variantKey, interfaceId, options) {
|
|
|
369
385
|
* Parse command options from args array
|
|
370
386
|
*/
|
|
371
387
|
function parseOptions(args) {
|
|
372
|
-
const options = { json: false };
|
|
388
|
+
const options = { json: false, interface: null };
|
|
373
389
|
const remaining = [];
|
|
374
390
|
|
|
375
|
-
for (
|
|
391
|
+
for (let i = 0; i < args.length; i++) {
|
|
392
|
+
const arg = args[i];
|
|
376
393
|
if (arg === '--json') {
|
|
377
394
|
options.json = true;
|
|
395
|
+
} else if (arg === '--interface' || arg === '-i') {
|
|
396
|
+
// Next arg is the interface ID
|
|
397
|
+
if (i + 1 < args.length) {
|
|
398
|
+
options.interface = args[++i];
|
|
399
|
+
}
|
|
378
400
|
} else if (!arg.startsWith('-')) {
|
|
379
401
|
remaining.push(arg);
|
|
380
402
|
}
|
|
@@ -383,6 +405,234 @@ function parseOptions(args) {
|
|
|
383
405
|
return { options, remaining };
|
|
384
406
|
}
|
|
385
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Get credentials from config file
|
|
410
|
+
*/
|
|
411
|
+
function getCredentials() {
|
|
412
|
+
const os = require('os');
|
|
413
|
+
const configPath = path.join(os.homedir(), '.lux', 'config.json');
|
|
414
|
+
|
|
415
|
+
if (!fs.existsSync(configPath)) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
421
|
+
return {
|
|
422
|
+
apiKey: config.apiKey,
|
|
423
|
+
orgId: config.orgId,
|
|
424
|
+
projectId: config.projectId,
|
|
425
|
+
apiUrl: config.apiUrl || 'https://v2.uselux.ai'
|
|
426
|
+
};
|
|
427
|
+
} catch (e) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get interface ID from options or auto-detect
|
|
434
|
+
*/
|
|
435
|
+
function getInterfaceId(options) {
|
|
436
|
+
if (options.interface) {
|
|
437
|
+
return options.interface;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Try to auto-detect from .lux.json in current interface repo
|
|
441
|
+
const luxConfigPath = path.join(process.cwd(), '.lux.json');
|
|
442
|
+
if (fs.existsSync(luxConfigPath)) {
|
|
443
|
+
try {
|
|
444
|
+
const luxConfig = JSON.parse(fs.readFileSync(luxConfigPath, 'utf-8'));
|
|
445
|
+
if (luxConfig.interfaceId) {
|
|
446
|
+
return luxConfig.interfaceId;
|
|
447
|
+
}
|
|
448
|
+
} catch (e) { /* ignore */ }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Try to find from interfaces directory structure
|
|
452
|
+
const interfacesDir = path.join(process.cwd(), 'interfaces');
|
|
453
|
+
if (fs.existsSync(interfacesDir)) {
|
|
454
|
+
const entries = fs.readdirSync(interfacesDir, { withFileTypes: true });
|
|
455
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
456
|
+
if (dirs.length === 1) {
|
|
457
|
+
return dirs[0].name;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Call API endpoint for lifecycle operations
|
|
466
|
+
*/
|
|
467
|
+
async function callLifecycleApi(endpoint, testId, interfaceId, creds) {
|
|
468
|
+
const url = `${creds.apiUrl}/api/interfaces/${interfaceId}/ab-tests/${testId}/${endpoint}`;
|
|
469
|
+
|
|
470
|
+
const response = await fetch(url, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
headers: {
|
|
473
|
+
'Authorization': `Bearer ${creds.apiKey}`,
|
|
474
|
+
'X-Org-Id': creds.orgId,
|
|
475
|
+
'X-Project-Id': creds.projectId,
|
|
476
|
+
'Content-Type': 'application/json'
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const data = await response.json();
|
|
481
|
+
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
throw new Error(data.error || `API returned ${response.status}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return data;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Update local ab-tests.json status
|
|
491
|
+
*/
|
|
492
|
+
function updateLocalTestStatus(interfaceId, testId, status) {
|
|
493
|
+
const testsPath = getABTestsPath(interfaceId);
|
|
494
|
+
if (!testsPath || !fs.existsSync(testsPath)) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const tests = JSON.parse(fs.readFileSync(testsPath, 'utf-8'));
|
|
500
|
+
const testIndex = tests.findIndex(t => t.id === testId);
|
|
501
|
+
if (testIndex === -1) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
tests[testIndex].status = status;
|
|
506
|
+
tests[testIndex].updatedAt = Date.now();
|
|
507
|
+
|
|
508
|
+
fs.writeFileSync(testsPath, JSON.stringify(tests, null, 2));
|
|
509
|
+
return true;
|
|
510
|
+
} catch (e) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Remove test from local ab-tests.json
|
|
517
|
+
*/
|
|
518
|
+
function removeLocalTest(interfaceId, testId) {
|
|
519
|
+
const testsPath = getABTestsPath(interfaceId);
|
|
520
|
+
if (!testsPath || !fs.existsSync(testsPath)) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const tests = JSON.parse(fs.readFileSync(testsPath, 'utf-8'));
|
|
526
|
+
const testIndex = tests.findIndex(t => t.id === testId);
|
|
527
|
+
if (testIndex === -1) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
tests.splice(testIndex, 1);
|
|
532
|
+
fs.writeFileSync(testsPath, JSON.stringify(tests, null, 2));
|
|
533
|
+
return true;
|
|
534
|
+
} catch (e) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Create finished - sync new test to PostHog after agent implementation
|
|
541
|
+
*/
|
|
542
|
+
async function createFinished(testId, options) {
|
|
543
|
+
const spinner = ora('Syncing new A/B test to PostHog...').start();
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const creds = getCredentials();
|
|
547
|
+
if (!creds || !creds.apiKey) {
|
|
548
|
+
spinner.fail('Not authenticated. Run "lux login" first.');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const interfaceId = getInterfaceId(options);
|
|
553
|
+
if (!interfaceId) {
|
|
554
|
+
spinner.fail('Could not determine interface ID. Use --interface <id> to specify.');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Call API to sync to PostHog and update DB
|
|
559
|
+
const result = await callLifecycleApi('create-finished', testId, interfaceId, creds);
|
|
560
|
+
|
|
561
|
+
// Update local file status
|
|
562
|
+
updateLocalTestStatus(interfaceId, testId, 'active');
|
|
563
|
+
|
|
564
|
+
spinner.succeed(`A/B test "${result.test?.name || testId}" synced to PostHog`);
|
|
565
|
+
console.log(chalk.dim(` PostHog flag ID: ${result.posthogFlagId}`));
|
|
566
|
+
console.log(chalk.dim(` Status: active`));
|
|
567
|
+
} catch (error) {
|
|
568
|
+
spinner.fail(`Failed to sync A/B test: ${error.message}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Update finished - sync updated test to PostHog after agent code changes
|
|
574
|
+
*/
|
|
575
|
+
async function updateFinished(testId, options) {
|
|
576
|
+
const spinner = ora('Syncing A/B test changes to PostHog...').start();
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const creds = getCredentials();
|
|
580
|
+
if (!creds || !creds.apiKey) {
|
|
581
|
+
spinner.fail('Not authenticated. Run "lux login" first.');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const interfaceId = getInterfaceId(options);
|
|
586
|
+
if (!interfaceId) {
|
|
587
|
+
spinner.fail('Could not determine interface ID. Use --interface <id> to specify.');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Call API to sync to PostHog and update DB
|
|
592
|
+
const result = await callLifecycleApi('update-finished', testId, interfaceId, creds);
|
|
593
|
+
|
|
594
|
+
// Update local file status
|
|
595
|
+
updateLocalTestStatus(interfaceId, testId, 'active');
|
|
596
|
+
|
|
597
|
+
spinner.succeed(`A/B test "${result.test?.name || testId}" updated and synced`);
|
|
598
|
+
console.log(chalk.dim(` Status: active`));
|
|
599
|
+
} catch (error) {
|
|
600
|
+
spinner.fail(`Failed to sync A/B test: ${error.message}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Delete finished - remove test from PostHog and DB after agent removes code
|
|
606
|
+
*/
|
|
607
|
+
async function deleteFinished(testId, options) {
|
|
608
|
+
const spinner = ora('Removing A/B test from PostHog...').start();
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const creds = getCredentials();
|
|
612
|
+
if (!creds || !creds.apiKey) {
|
|
613
|
+
spinner.fail('Not authenticated. Run "lux login" first.');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const interfaceId = getInterfaceId(options);
|
|
618
|
+
if (!interfaceId) {
|
|
619
|
+
spinner.fail('Could not determine interface ID. Use --interface <id> to specify.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Call API to delete from PostHog and DB
|
|
624
|
+
const result = await callLifecycleApi('delete-finished', testId, interfaceId, creds);
|
|
625
|
+
|
|
626
|
+
// Remove from local file
|
|
627
|
+
removeLocalTest(interfaceId, testId);
|
|
628
|
+
|
|
629
|
+
spinner.succeed(`A/B test "${result.deletedTest?.name || testId}" deleted`);
|
|
630
|
+
console.log(chalk.dim(` Removed from PostHog, database, and local storage`));
|
|
631
|
+
} catch (error) {
|
|
632
|
+
spinner.fail(`Failed to delete A/B test: ${error.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
386
636
|
/**
|
|
387
637
|
* Handle ab-tests commands
|
|
388
638
|
*/
|
|
@@ -425,6 +675,32 @@ async function handleABTests(args) {
|
|
|
425
675
|
await getVariant(remaining[0], remaining[1], remaining[2], options);
|
|
426
676
|
break;
|
|
427
677
|
|
|
678
|
+
case 'create-finished':
|
|
679
|
+
if (!remaining[0]) {
|
|
680
|
+
console.log(chalk.red('Missing test ID. Usage: lux ab-tests create-finished <test-id> --interface <id>'));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
await createFinished(remaining[0], options);
|
|
684
|
+
break;
|
|
685
|
+
|
|
686
|
+
case 'update-finished':
|
|
687
|
+
if (!remaining[0]) {
|
|
688
|
+
console.log(chalk.red('Missing test ID. Usage: lux ab-tests update-finished <test-id> --interface <id>'));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
await updateFinished(remaining[0], options);
|
|
692
|
+
break;
|
|
693
|
+
|
|
694
|
+
case 'delete-finished':
|
|
695
|
+
case 'delete':
|
|
696
|
+
case 'rm':
|
|
697
|
+
if (!remaining[0]) {
|
|
698
|
+
console.log(chalk.red('Missing test ID. Usage: lux ab-tests delete-finished <test-id> --interface <id>'));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
await deleteFinished(remaining[0], options);
|
|
702
|
+
break;
|
|
703
|
+
|
|
428
704
|
default:
|
|
429
705
|
console.log(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
430
706
|
showHelp();
|
package/package.json
CHANGED
|
@@ -74,6 +74,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|
|
74
74
|
// Override feature flags for Lux Studio preview
|
|
75
75
|
posthog.featureFlags.override(flagOverrides)
|
|
76
76
|
console.log('[PostHog] Applied Lux Studio flag overrides:', flagOverrides)
|
|
77
|
+
|
|
78
|
+
// Force reload to make React hooks aware of the change
|
|
79
|
+
posthog.reloadFeatureFlags()
|
|
77
80
|
} else {
|
|
78
81
|
// Clear any previous overrides when not in preview mode
|
|
79
82
|
posthog.featureFlags.override(false)
|