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.
@@ -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
- const nonControlVariants = test.variants.filter(v => v.key !== 'current' && v.key !== 'control');
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 nonControlVariants) {
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 isControl = variant.key === 'current' || variant.key === 'control';
352
- if (!isControl) {
353
- console.log(chalk.dim('\n Implementation:'));
354
- console.log(chalk.gray(` import { useFeatureFlagVariantKey } from 'posthog-js/react';`));
355
- console.log(chalk.gray(` const variant = useFeatureFlagVariantKey('${test.key}');`));
356
- console.log(chalk.gray(` if (variant === '${variant.key}') {`));
357
- if (variant.description) {
358
- console.log(chalk.gray(` // ${variant.description}`));
359
- }
360
- console.log(chalk.gray(` }`));
361
- } else {
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 (const arg of args) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxlabs",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "CLI tool for Lux - Upload and deploy interfaces from your terminal",
5
5
  "author": "Jason Henkel <jason@uselux.ai>",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -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)