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/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Lux AI Labs. All Rights Reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
proprietary and confidential property of Lux AI Labs.
|
|
7
|
+
|
|
8
|
+
RESTRICTIONS:
|
|
9
|
+
|
|
10
|
+
1. You may NOT copy, modify, merge, publish, distribute, sublicense, and/or
|
|
11
|
+
sell copies of the Software without explicit written permission from
|
|
12
|
+
Lux AI Labs.
|
|
13
|
+
|
|
14
|
+
2. You may NOT reverse engineer, decompile, or disassemble the Software.
|
|
15
|
+
|
|
16
|
+
3. You may NOT use the Software to create derivative works.
|
|
17
|
+
|
|
18
|
+
4. You may NOT remove or alter any proprietary notices, labels, or marks
|
|
19
|
+
on the Software.
|
|
20
|
+
|
|
21
|
+
PERMITTED USE:
|
|
22
|
+
|
|
23
|
+
You are granted a limited, non-exclusive, non-transferable license to use
|
|
24
|
+
the Software solely for your internal business purposes in connection with
|
|
25
|
+
the Lux platform, subject to the terms and conditions of your agreement
|
|
26
|
+
with Lux AI Labs.
|
|
27
|
+
|
|
28
|
+
DISCLAIMER:
|
|
29
|
+
|
|
30
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
31
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
32
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
33
|
+
LUX AI LABS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
34
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
35
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
36
|
+
|
|
37
|
+
For licensing inquiries, contact: sam@uselux.ai
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# luxlabs
|
|
2
|
+
|
|
3
|
+
Official CLI tool for Lux - Upload and deploy interfaces from your terminal.
|
|
4
|
+
|
|
5
|
+
**Author:** Jason Henkel at Lux Labs
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g luxlabs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Authenticate with Lux
|
|
17
|
+
lux login
|
|
18
|
+
|
|
19
|
+
# Initialize a new interface project
|
|
20
|
+
lux interface init
|
|
21
|
+
|
|
22
|
+
# Start local development with hot reload
|
|
23
|
+
lux dev
|
|
24
|
+
|
|
25
|
+
# Deploy your interface
|
|
26
|
+
lux interface up
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
### Authentication
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---------|-------------|
|
|
35
|
+
| `lux login` | Authenticate with Lux (opens browser) |
|
|
36
|
+
| `lux login --key <api-key>` | Authenticate with an API key |
|
|
37
|
+
| `lux logout` | Log out from Lux |
|
|
38
|
+
|
|
39
|
+
### Interface Management
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `lux interface init` | Initialize a new interface project |
|
|
44
|
+
| `lux interface up` | Upload and deploy your interface |
|
|
45
|
+
| `lux interface list` | List all your interfaces |
|
|
46
|
+
| `lux interface link` | Link current directory to an interface |
|
|
47
|
+
| `lux i <subcommand>` | Shorthand alias for interface commands |
|
|
48
|
+
|
|
49
|
+
### Development
|
|
50
|
+
|
|
51
|
+
| Command | Description |
|
|
52
|
+
|---------|-------------|
|
|
53
|
+
| `lux dev` | Start local dev server with tunnel |
|
|
54
|
+
| `lux dev -p <port>` | Specify custom port (default: 3000) |
|
|
55
|
+
| `lux dev --no-tunnel` | Disable tunnel (local only) |
|
|
56
|
+
| `lux servers` | List running dev servers |
|
|
57
|
+
| `lux logs <interface>` | View logs from a dev server |
|
|
58
|
+
|
|
59
|
+
### Data Management
|
|
60
|
+
|
|
61
|
+
| Command | Description |
|
|
62
|
+
|---------|-------------|
|
|
63
|
+
| `lux data tables` | Manage database tables |
|
|
64
|
+
| `lux data kv` | Manage KV namespaces |
|
|
65
|
+
|
|
66
|
+
### Storage
|
|
67
|
+
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `lux storage ls` | List files in storage |
|
|
71
|
+
| `lux storage get <key>` | Download a file |
|
|
72
|
+
| `lux storage put <key> <file>` | Upload a file |
|
|
73
|
+
| `lux storage rm <key>` | Delete a file |
|
|
74
|
+
|
|
75
|
+
### Workflows
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| `lux workflows list` | List all workflows |
|
|
80
|
+
| `lux workflows get <id>` | Get workflow details |
|
|
81
|
+
| `lux workflows create` | Create a new workflow |
|
|
82
|
+
| `lux workflows publish <id>` | Publish a workflow |
|
|
83
|
+
| `lux flow <subcommand>` | Shorthand alias |
|
|
84
|
+
|
|
85
|
+
### Agents
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---------|-------------|
|
|
89
|
+
| `lux agent list` | List all agents |
|
|
90
|
+
| `lux agent get <id>` | Get agent details |
|
|
91
|
+
| `lux agent create` | Create a new agent |
|
|
92
|
+
| `lux agent prompt <id>` | View/edit agent prompt |
|
|
93
|
+
|
|
94
|
+
### Knowledge Base
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---------|-------------|
|
|
98
|
+
| `lux knowledge list` | List knowledge bases |
|
|
99
|
+
| `lux knowledge upload <file>` | Upload to knowledge base |
|
|
100
|
+
| `lux kb <subcommand>` | Shorthand alias |
|
|
101
|
+
|
|
102
|
+
### Voice Agents
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---------|-------------|
|
|
106
|
+
| `lux voice-agents list` | List voice agents |
|
|
107
|
+
| `lux voice-agents create` | Create a voice agent |
|
|
108
|
+
| `lux va <subcommand>` | Shorthand alias |
|
|
109
|
+
|
|
110
|
+
### Secrets
|
|
111
|
+
|
|
112
|
+
| Command | Description |
|
|
113
|
+
|---------|-------------|
|
|
114
|
+
| `lux secrets list` | List organization secrets |
|
|
115
|
+
| `lux secrets set <key> <value>` | Set a secret |
|
|
116
|
+
| `lux secrets get <key>` | Get a secret value |
|
|
117
|
+
| `lux secrets delete <key>` | Delete a secret |
|
|
118
|
+
|
|
119
|
+
### A/B Tests
|
|
120
|
+
|
|
121
|
+
| Command | Description |
|
|
122
|
+
|---------|-------------|
|
|
123
|
+
| `lux ab-tests list-tests` | List all A/B tests |
|
|
124
|
+
| `lux ab-tests get-test <id>` | Get test details |
|
|
125
|
+
| `lux experiments <subcommand>` | Alias for ab-tests |
|
|
126
|
+
|
|
127
|
+
### Project Deployment
|
|
128
|
+
|
|
129
|
+
| Command | Description |
|
|
130
|
+
|---------|-------------|
|
|
131
|
+
| `lux project deploy` | Deploy project to GitHub |
|
|
132
|
+
| `lux proj <subcommand>` | Shorthand alias |
|
|
133
|
+
|
|
134
|
+
### Preview & Testing
|
|
135
|
+
|
|
136
|
+
| Command | Description |
|
|
137
|
+
|---------|-------------|
|
|
138
|
+
| `lux preview <interface-id>` | Start interface preview |
|
|
139
|
+
| `lux screenshot <interface-id>` | Take a screenshot |
|
|
140
|
+
| `lux click <interface-id> <selector>` | Click an element |
|
|
141
|
+
| `lux type <interface-id> <selector> <text>` | Type into an element |
|
|
142
|
+
| `lux eval <interface-id> <code>` | Execute JavaScript |
|
|
143
|
+
| `lux url <interface-id>` | Get current URL |
|
|
144
|
+
| `lux nav <interface-id> <url>` | Navigate to URL |
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
- Node.js >= 18.0.0
|
|
149
|
+
- npm or yarn
|
|
150
|
+
|
|
151
|
+
## Support
|
|
152
|
+
|
|
153
|
+
For support and documentation, visit [uselux.ai](https://uselux.ai)
|
|
154
|
+
|
|
155
|
+
For issues, visit [GitHub Issues](https://github.com/luxAILabs/lux-studio/issues)
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
Proprietary - See LICENSE file for details.
|
|
160
|
+
|
|
161
|
+
Copyright (c) 2024-2025 Lux AI Labs. All Rights Reserved.
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B Tests Commands
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for reading A/B test configurations.
|
|
5
|
+
* These are primarily used by Claude Code to understand what variants exist
|
|
6
|
+
* and what they should do (via the descriptions).
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* - lux ab-tests list-tests [interface] List all A/B tests
|
|
10
|
+
* - lux ab-tests get-test <key> [interface] Get details for a specific test
|
|
11
|
+
* - lux ab-tests get-variant <key> <variant> [interface] Get details for a specific variant
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Show help for ab-tests commands
|
|
20
|
+
*/
|
|
21
|
+
function showHelp() {
|
|
22
|
+
console.log(chalk.cyan('\nLux A/B Tests Commands:\n'));
|
|
23
|
+
console.log(chalk.white(' lux ab-tests list-tests [interface]'));
|
|
24
|
+
console.log(chalk.dim(' List all A/B tests for an interface'));
|
|
25
|
+
console.log(chalk.dim(' Options: --json (JSON output)\n'));
|
|
26
|
+
console.log(chalk.white(' lux ab-tests get-test <test-key> [interface]'));
|
|
27
|
+
console.log(chalk.dim(' Get details for a specific A/B test'));
|
|
28
|
+
console.log(chalk.dim(' Options: --json (JSON output)\n'));
|
|
29
|
+
console.log(chalk.white(' lux ab-tests get-variant <test-key> <variant-key> [interface]'));
|
|
30
|
+
console.log(chalk.dim(' Get details for a specific variant within a test'));
|
|
31
|
+
console.log(chalk.dim(' Options: --json (JSON output)\n'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the path to ab-tests.json for an interface
|
|
36
|
+
* Supports both interface ID and interface name
|
|
37
|
+
*/
|
|
38
|
+
function getABTestsPath(interfaceIdentifier) {
|
|
39
|
+
const interfacesDir = path.join(process.cwd(), 'interfaces');
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(interfacesDir)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If no identifier provided, try to find the only interface
|
|
46
|
+
if (!interfaceIdentifier) {
|
|
47
|
+
const entries = fs.readdirSync(interfacesDir, { withFileTypes: true });
|
|
48
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
49
|
+
|
|
50
|
+
if (dirs.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (dirs.length === 1) {
|
|
55
|
+
// Auto-select the only interface
|
|
56
|
+
interfaceIdentifier = dirs[0].name;
|
|
57
|
+
} else {
|
|
58
|
+
// Multiple interfaces - need to specify
|
|
59
|
+
console.log(chalk.yellow('Multiple interfaces found. Please specify which one:'));
|
|
60
|
+
for (const dir of dirs) {
|
|
61
|
+
const metaPath = path.join(interfacesDir, dir.name, 'metadata.json');
|
|
62
|
+
let name = dir.name;
|
|
63
|
+
if (fs.existsSync(metaPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
66
|
+
name = meta.name || dir.name;
|
|
67
|
+
} catch (e) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.dim(` - ${name} (${dir.name})`));
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if it's a direct interface ID (directory exists)
|
|
76
|
+
let interfaceDir = path.join(interfacesDir, interfaceIdentifier, 'repo');
|
|
77
|
+
if (fs.existsSync(interfaceDir)) {
|
|
78
|
+
return path.join(interfaceDir, '.lux', 'ab-tests.json');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to find by name
|
|
82
|
+
const entries = fs.readdirSync(interfacesDir, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (!entry.isDirectory()) continue;
|
|
85
|
+
|
|
86
|
+
const metaPath = path.join(interfacesDir, entry.name, 'metadata.json');
|
|
87
|
+
if (fs.existsSync(metaPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
90
|
+
if (meta.name && meta.name.toLowerCase() === interfaceIdentifier.toLowerCase()) {
|
|
91
|
+
return path.join(interfacesDir, entry.name, 'repo', '.lux', 'ab-tests.json');
|
|
92
|
+
}
|
|
93
|
+
} catch (e) { /* ignore */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get interface name from identifier
|
|
102
|
+
*/
|
|
103
|
+
function getInterfaceName(interfaceIdentifier) {
|
|
104
|
+
const interfacesDir = path.join(process.cwd(), 'interfaces');
|
|
105
|
+
|
|
106
|
+
if (!interfaceIdentifier) {
|
|
107
|
+
const entries = fs.readdirSync(interfacesDir, { withFileTypes: true });
|
|
108
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
109
|
+
if (dirs.length === 1) {
|
|
110
|
+
interfaceIdentifier = dirs[0].name;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!interfaceIdentifier) return 'Unknown';
|
|
115
|
+
|
|
116
|
+
const metaPath = path.join(interfacesDir, interfaceIdentifier, 'metadata.json');
|
|
117
|
+
if (fs.existsSync(metaPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
120
|
+
return meta.name || interfaceIdentifier;
|
|
121
|
+
} catch (e) { /* ignore */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return interfaceIdentifier;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load tests from file
|
|
129
|
+
*/
|
|
130
|
+
function loadTests(interfaceId) {
|
|
131
|
+
const testsPath = getABTestsPath(interfaceId);
|
|
132
|
+
|
|
133
|
+
if (!testsPath) {
|
|
134
|
+
return { error: 'Could not find interface. Make sure you are in a Lux project directory.' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(testsPath)) {
|
|
138
|
+
return { tests: [], empty: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const tests = JSON.parse(fs.readFileSync(testsPath, 'utf-8'));
|
|
143
|
+
return { tests, testsPath };
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { error: `Failed to parse ab-tests.json: ${e.message}` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List all A/B tests for an interface
|
|
151
|
+
*/
|
|
152
|
+
async function listTests(interfaceId, options) {
|
|
153
|
+
const result = loadTests(interfaceId);
|
|
154
|
+
|
|
155
|
+
if (result.error) {
|
|
156
|
+
console.log(chalk.red(result.error));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (result.empty) {
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(JSON.stringify({ tests: [] }, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(chalk.yellow('No A/B tests found for this interface.'));
|
|
165
|
+
console.log(chalk.dim('Create tests in the Settings tab of your interface.'));
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { tests } = result;
|
|
171
|
+
|
|
172
|
+
if (options.json) {
|
|
173
|
+
console.log(JSON.stringify({ tests }, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const interfaceName = getInterfaceName(interfaceId);
|
|
178
|
+
console.log(chalk.cyan(`\nA/B Tests for "${interfaceName}":`));
|
|
179
|
+
|
|
180
|
+
if (tests.length === 0) {
|
|
181
|
+
console.log(chalk.yellow('\nNo tests configured.'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const test of tests) {
|
|
186
|
+
console.log();
|
|
187
|
+
console.log(chalk.cyan(`📊 ${test.name}`) + chalk.dim(` (key: ${test.key})`));
|
|
188
|
+
console.log(chalk.dim(` Status: ${test.status}${test.posthogFlagId ? ` (PostHog flag: ${test.posthogFlagId})` : ''}`));
|
|
189
|
+
|
|
190
|
+
if (test.description) {
|
|
191
|
+
console.log(chalk.white(` Description: ${test.description}`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Show referenced components if any
|
|
195
|
+
if (test.referencedComponents && test.referencedComponents.length > 0) {
|
|
196
|
+
console.log(chalk.dim(` Referenced Components: ${test.referencedComponents.join(', ')}`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.dim('\n Variants:'));
|
|
200
|
+
for (const v of test.variants) {
|
|
201
|
+
const desc = v.description || 'No description';
|
|
202
|
+
console.log(chalk.white(` • ${v.key} (${v.percentage}%)`) + chalk.dim(` - ${desc}`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get details for a specific A/B test
|
|
211
|
+
*/
|
|
212
|
+
async function getTest(testKey, interfaceId, options) {
|
|
213
|
+
const result = loadTests(interfaceId);
|
|
214
|
+
|
|
215
|
+
if (result.error) {
|
|
216
|
+
console.log(chalk.red(result.error));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.empty) {
|
|
221
|
+
console.log(chalk.red('No A/B tests found for this interface.'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { tests } = result;
|
|
226
|
+
const test = tests.find(t => t.key === testKey);
|
|
227
|
+
|
|
228
|
+
if (!test) {
|
|
229
|
+
console.log(chalk.red(`Test "${testKey}" not found.`));
|
|
230
|
+
console.log(chalk.dim('\nAvailable tests:'));
|
|
231
|
+
for (const t of tests) {
|
|
232
|
+
console.log(chalk.dim(` - ${t.key} (${t.name})`));
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (options.json) {
|
|
238
|
+
console.log(JSON.stringify({ test }, null, 2));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(chalk.cyan(`📊 ${test.key}`));
|
|
244
|
+
console.log(chalk.white(` Name: ${test.name}`));
|
|
245
|
+
console.log(chalk.dim(` Status: ${test.status}${test.posthogFlagId ? ` (PostHog flag: ${test.posthogFlagId})` : ''}`));
|
|
246
|
+
|
|
247
|
+
if (test.description) {
|
|
248
|
+
console.log(chalk.white(` Description: ${test.description}`));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Show referenced components if any
|
|
252
|
+
if (test.referencedComponents && test.referencedComponents.length > 0) {
|
|
253
|
+
console.log(chalk.cyan('\n Referenced Components:'));
|
|
254
|
+
for (const comp of test.referencedComponents) {
|
|
255
|
+
console.log(chalk.white(` • ${comp}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(chalk.dim(`\n Variants (${test.variants.length}):`));
|
|
260
|
+
for (const v of test.variants) {
|
|
261
|
+
console.log(chalk.white(` • ${v.key} (${v.percentage}%)`));
|
|
262
|
+
if (v.description) {
|
|
263
|
+
console.log(chalk.dim(` ${v.description}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Show implementation pattern
|
|
268
|
+
const nonControlVariants = test.variants.filter(v => v.key !== 'current' && v.key !== 'control');
|
|
269
|
+
if (nonControlVariants.length > 0) {
|
|
270
|
+
console.log(chalk.dim('\n Implementation Pattern:'));
|
|
271
|
+
console.log(chalk.gray(` import { useFeatureFlagVariantKey } from 'posthog-js/react';`));
|
|
272
|
+
console.log(chalk.gray(` const variant = useFeatureFlagVariantKey('${test.key}');`));
|
|
273
|
+
for (const v of nonControlVariants) {
|
|
274
|
+
const comment = v.description ? ` /* ${v.description} */` : '';
|
|
275
|
+
console.log(chalk.gray(` if (variant === '${v.key}') {${comment} }`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get details for a specific variant within a test
|
|
284
|
+
*/
|
|
285
|
+
async function getVariant(testKey, variantKey, interfaceId, options) {
|
|
286
|
+
const result = loadTests(interfaceId);
|
|
287
|
+
|
|
288
|
+
if (result.error) {
|
|
289
|
+
console.log(chalk.red(result.error));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (result.empty) {
|
|
294
|
+
console.log(chalk.red('No A/B tests found for this interface.'));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { tests } = result;
|
|
299
|
+
const test = tests.find(t => t.key === testKey);
|
|
300
|
+
|
|
301
|
+
if (!test) {
|
|
302
|
+
console.log(chalk.red(`Test "${testKey}" not found.`));
|
|
303
|
+
console.log(chalk.dim('\nAvailable tests:'));
|
|
304
|
+
for (const t of tests) {
|
|
305
|
+
console.log(chalk.dim(` - ${t.key} (${t.name})`));
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const variant = test.variants.find(v => v.key === variantKey);
|
|
311
|
+
|
|
312
|
+
if (!variant) {
|
|
313
|
+
console.log(chalk.red(`Variant "${variantKey}" not found in test "${testKey}".`));
|
|
314
|
+
console.log(chalk.dim('\nAvailable variants:'));
|
|
315
|
+
for (const v of test.variants) {
|
|
316
|
+
console.log(chalk.dim(` - ${v.key} (${v.name || v.key})`));
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (options.json) {
|
|
322
|
+
console.log(JSON.stringify({
|
|
323
|
+
test: { key: test.key, name: test.name, description: test.description },
|
|
324
|
+
variant
|
|
325
|
+
}, null, 2));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log();
|
|
330
|
+
console.log(chalk.cyan(`🎯 Variant: ${variant.key}`));
|
|
331
|
+
console.log(chalk.dim(` Test: ${test.name} (${test.key})`));
|
|
332
|
+
console.log(chalk.white(` Name: ${variant.name || variant.key}`));
|
|
333
|
+
console.log(chalk.dim(` Traffic: ${variant.percentage}%`));
|
|
334
|
+
|
|
335
|
+
if (variant.description) {
|
|
336
|
+
console.log(chalk.cyan('\n Description (what this variant should do):'));
|
|
337
|
+
console.log(chalk.white(` ${variant.description}`));
|
|
338
|
+
} else {
|
|
339
|
+
console.log(chalk.yellow('\n No description set for this variant.'));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Show referenced components for this variant if any
|
|
343
|
+
if (variant.referencedComponents && variant.referencedComponents.length > 0) {
|
|
344
|
+
console.log(chalk.cyan('\n Referenced Components:'));
|
|
345
|
+
for (const comp of variant.referencedComponents) {
|
|
346
|
+
console.log(chalk.white(` • ${comp}`));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
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).'));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Parse command options from args array
|
|
370
|
+
*/
|
|
371
|
+
function parseOptions(args) {
|
|
372
|
+
const options = { json: false };
|
|
373
|
+
const remaining = [];
|
|
374
|
+
|
|
375
|
+
for (const arg of args) {
|
|
376
|
+
if (arg === '--json') {
|
|
377
|
+
options.json = true;
|
|
378
|
+
} else if (!arg.startsWith('-')) {
|
|
379
|
+
remaining.push(arg);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { options, remaining };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Handle ab-tests commands
|
|
388
|
+
*/
|
|
389
|
+
async function handleABTests(args) {
|
|
390
|
+
const subcommand = args[0];
|
|
391
|
+
const subArgs = args.slice(1);
|
|
392
|
+
|
|
393
|
+
if (!subcommand || subcommand === 'help') {
|
|
394
|
+
showHelp();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const { options, remaining } = parseOptions(subArgs);
|
|
399
|
+
|
|
400
|
+
switch (subcommand) {
|
|
401
|
+
case 'list-tests':
|
|
402
|
+
case 'list':
|
|
403
|
+
case 'ls':
|
|
404
|
+
await listTests(remaining[0], options);
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case 'get-test':
|
|
408
|
+
case 'get':
|
|
409
|
+
if (!remaining[0]) {
|
|
410
|
+
console.log(chalk.red('Missing test key. Usage: lux ab-tests get-test <test-key> [interface]'));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await getTest(remaining[0], remaining[1], options);
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case 'get-variant':
|
|
417
|
+
if (!remaining[0]) {
|
|
418
|
+
console.log(chalk.red('Missing test key. Usage: lux ab-tests get-variant <test-key> <variant-key> [interface]'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!remaining[1]) {
|
|
422
|
+
console.log(chalk.red('Missing variant key. Usage: lux ab-tests get-variant <test-key> <variant-key> [interface]'));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
await getVariant(remaining[0], remaining[1], remaining[2], options);
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
console.log(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
430
|
+
showHelp();
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = {
|
|
436
|
+
handleABTests,
|
|
437
|
+
};
|