@popoverai/dotrequirements 0.18.0 → 0.20.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/README.md +49 -0
- package/dist/cli.js +7 -0
- package/dist/commands/browsertest.d.ts +6 -0
- package/dist/commands/browsertest.js +167 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +108 -0
- package/dist/convex.d.ts +8 -0
- package/dist/convex.js +8 -0
- package/dist/templates/context-file-section.md +4 -0
- package/dist/utils/project-settings.d.ts +11 -0
- package/dist/utils/project-settings.js +25 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,8 +96,26 @@ Initialize a new project. Creates `.requirements/` directory, example files, and
|
|
|
96
96
|
```bash
|
|
97
97
|
dotreq init
|
|
98
98
|
dotreq init --name my-project
|
|
99
|
+
dotreq init --invite abc123xyz
|
|
99
100
|
```
|
|
100
101
|
|
|
102
|
+
**Options:**
|
|
103
|
+
|
|
104
|
+
| Option | Description |
|
|
105
|
+
|--------|-------------|
|
|
106
|
+
| `--name <name>` | Set project name (skips prompt) |
|
|
107
|
+
| `--invite <token>` | Join a team using an invite token |
|
|
108
|
+
|
|
109
|
+
**Joining a team via invite:**
|
|
110
|
+
|
|
111
|
+
If a team admin shares an invite command with you, join the team and connect in one step:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx dotrequirements init --invite abc123xyz
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This shows the team name, prompts you to sign in, adds you to the team, and lets you select a project.
|
|
118
|
+
|
|
101
119
|
### `dotreq link`
|
|
102
120
|
|
|
103
121
|
Link your local environment to an existing project (when you already have a project in dot•requirements cloud).
|
|
@@ -146,6 +164,37 @@ dotreq test
|
|
|
146
164
|
dotreq test --file .requirements/auth.requirements.md
|
|
147
165
|
```
|
|
148
166
|
|
|
167
|
+
### `dotreq browsertest`
|
|
168
|
+
|
|
169
|
+
Run browser-based acceptance tests against requirements. Uses AI-powered browser automation to verify that your application behaves as specified.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
dotreq browsertest LOGIN-1
|
|
173
|
+
dotreq browsertest LOGIN-1 https://example.com
|
|
174
|
+
dotreq browsertest LOGIN-1 --json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Options:**
|
|
178
|
+
|
|
179
|
+
| Option | Description |
|
|
180
|
+
|--------|-------------|
|
|
181
|
+
| `--json` | Output results as JSON (for CI/CD integration) |
|
|
182
|
+
|
|
183
|
+
**Configuration required:**
|
|
184
|
+
|
|
185
|
+
Browser testing requires credentials in `project-settings.json`:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"defaultURL": "https://your-app.com",
|
|
190
|
+
"browserTest": {
|
|
191
|
+
"geminiApiKey": "your-gemini-api-key"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Optional settings: `vercelBypassSecret`, `browserbaseApiKey`, `browserbaseProjectId`.
|
|
197
|
+
|
|
149
198
|
### `dotreq mcp-setup`
|
|
150
199
|
|
|
151
200
|
Configure the MCP server for AI assistants (Claude Code, Cursor, etc.).
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { linkCommand } from './commands/link.js';
|
|
|
5
5
|
import { pullCommand } from './commands/pull.js';
|
|
6
6
|
import { pushCommand } from './commands/push.js';
|
|
7
7
|
import { testCommand } from './commands/test.js';
|
|
8
|
+
import { browserTestCommand } from './commands/browsertest.js';
|
|
8
9
|
import { mcpCommand } from './commands/mcp.js';
|
|
9
10
|
import { mcpSetupCommand } from './commands/mcp-setup.js';
|
|
10
11
|
import { loadEnvFile } from './utils/env.js';
|
|
@@ -47,6 +48,7 @@ program
|
|
|
47
48
|
.command('init')
|
|
48
49
|
.description('Initialize a new dotrequirements project')
|
|
49
50
|
.option('-n, --name <name>', 'Project name (defaults to package.json name or directory name)')
|
|
51
|
+
.option('-i, --invite <token>', 'Join a team using an invite token')
|
|
50
52
|
.action(wrapCommand(initCommand));
|
|
51
53
|
program
|
|
52
54
|
.command('link')
|
|
@@ -69,6 +71,11 @@ program
|
|
|
69
71
|
.description('Validate requirements files in .requirements/')
|
|
70
72
|
.option('-f, --file <path>', 'Specific file to validate')
|
|
71
73
|
.action(wrapCommand(testCommand));
|
|
74
|
+
program
|
|
75
|
+
.command('browsertest <requirement-key> [url]')
|
|
76
|
+
.description('Run browser-based acceptance test for a requirement')
|
|
77
|
+
.option('--json', 'Output results as JSON')
|
|
78
|
+
.action(wrapCommand(browserTestCommand));
|
|
72
79
|
program
|
|
73
80
|
.command('mcp')
|
|
74
81
|
.description('Start the MCP (Model Context Protocol) server for AI assistant integration')
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { getProjectInfo, readProjectSettings, } from '../utils/project-settings.js';
|
|
4
|
+
import { loadAllRequirements, getRequirementTree, formatRequirement, } from '../mcp/requirements.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function browserTestCommand(requirementKey, url, options) {
|
|
7
|
+
// 1. Find project and load settings
|
|
8
|
+
const projectInfo = getProjectInfo();
|
|
9
|
+
if (!projectInfo) {
|
|
10
|
+
console.error('Error: No dotrequirements project found.');
|
|
11
|
+
console.log('Run "dotrequirements init" to create one.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
let settings;
|
|
15
|
+
try {
|
|
16
|
+
settings = readProjectSettings(projectInfo.rootPath);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
// Settings file exists but is invalid
|
|
20
|
+
console.error(`Error: ${err.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// 2. Determine URL
|
|
24
|
+
const targetURL = url || settings?.defaultURL;
|
|
25
|
+
if (!targetURL) {
|
|
26
|
+
console.error('Error: No URL provided and no defaultURL in project-settings.json.');
|
|
27
|
+
console.log('Either provide a URL argument or add "defaultURL" to .requirements/project-settings.json');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// 3. Check for required credentials
|
|
31
|
+
const geminiApiKey = settings?.browserTest?.geminiApiKey;
|
|
32
|
+
if (!geminiApiKey) {
|
|
33
|
+
console.error('Error: browserTest.geminiApiKey not found in project-settings.json.');
|
|
34
|
+
console.log('Add "browserTest": { "geminiApiKey": "..." } to .requirements/project-settings.json');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// 4. Load requirements
|
|
38
|
+
const { flattened } = await loadAllRequirements(projectInfo.rootPath);
|
|
39
|
+
if (flattened.length === 0) {
|
|
40
|
+
console.error('Error: No requirements found in .requirements/');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// 5. Find the requirement tree
|
|
44
|
+
const requirementTree = getRequirementTree(flattened, requirementKey);
|
|
45
|
+
if (requirementTree.length === 0) {
|
|
46
|
+
console.error(`Error: Requirement "${requirementKey}" not found.`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// 6. Format each requirement as an assertion
|
|
50
|
+
const assertions = requirementTree.map((req) => formatRequirement(req));
|
|
51
|
+
// 7. Build environment with injected secrets
|
|
52
|
+
const env = { ...process.env };
|
|
53
|
+
env.GEMINI_API_KEY = geminiApiKey;
|
|
54
|
+
if (settings?.browserTest?.vercelBypassSecret) {
|
|
55
|
+
env.VERCEL_AUTOMATION_BYPASS_SECRET = settings.browserTest.vercelBypassSecret;
|
|
56
|
+
}
|
|
57
|
+
if (settings?.browserTest?.browserbaseApiKey) {
|
|
58
|
+
env.BROWSERBASE_API_KEY = settings.browserTest.browserbaseApiKey;
|
|
59
|
+
}
|
|
60
|
+
if (settings?.browserTest?.browserbaseProjectId) {
|
|
61
|
+
env.BROWSERBASE_PROJECT_ID = settings.browserTest.browserbaseProjectId;
|
|
62
|
+
}
|
|
63
|
+
// 8. Run browser-automation
|
|
64
|
+
if (!options.json) {
|
|
65
|
+
console.log(`Testing ${requirementKey} against ${targetURL}...\n`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync('npx', ['@popoverai/browser-automation', 'test', targetURL, ...assertions], { env, maxBuffer: 10 * 1024 * 1024 });
|
|
69
|
+
// 9. Parse results
|
|
70
|
+
let results;
|
|
71
|
+
try {
|
|
72
|
+
results = JSON.parse(stdout.trim());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.error('Error: Could not parse browser-automation output');
|
|
76
|
+
console.error(stdout);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
// 10. Display results
|
|
80
|
+
if (options.json) {
|
|
81
|
+
outputJson(requirementTree, results);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
displayResults(requirementTree, results);
|
|
85
|
+
}
|
|
86
|
+
// 11. Exit with appropriate code
|
|
87
|
+
const failedCount = results.filter((r) => r.status !== 'passed').length;
|
|
88
|
+
if (failedCount > 0) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const error = err;
|
|
94
|
+
// browser-automation returns exit code 1 on failure, but still outputs JSON
|
|
95
|
+
if (error.stdout) {
|
|
96
|
+
try {
|
|
97
|
+
const results = JSON.parse(error.stdout.trim());
|
|
98
|
+
if (options.json) {
|
|
99
|
+
outputJson(requirementTree, results);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
displayResults(requirementTree, results);
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Not JSON, show raw output
|
|
108
|
+
console.error('browser-automation error:');
|
|
109
|
+
if (error.stdout)
|
|
110
|
+
console.error(error.stdout);
|
|
111
|
+
if (error.stderr)
|
|
112
|
+
console.error(error.stderr);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`Error running browser-automation: ${error.message}`);
|
|
118
|
+
if (error.stderr)
|
|
119
|
+
console.error(error.stderr);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function displayResults(requirements, results) {
|
|
125
|
+
let passedCount = 0;
|
|
126
|
+
let failedCount = 0;
|
|
127
|
+
for (let i = 0; i < requirements.length; i++) {
|
|
128
|
+
const req = requirements[i];
|
|
129
|
+
const result = results[i];
|
|
130
|
+
if (!result) {
|
|
131
|
+
console.log(` ? ${req.id}: No result`);
|
|
132
|
+
failedCount++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const indent = ' '.repeat(req.path.length);
|
|
136
|
+
const label = req.label ? ` (${req.label})` : '';
|
|
137
|
+
const icon = result.status === 'passed' ? '\u2713' : '\u2717';
|
|
138
|
+
console.log(`${indent}${icon} ${req.id}${label}: ${req.content}`);
|
|
139
|
+
if (result.status !== 'passed' && result.notes) {
|
|
140
|
+
console.log(`${indent} \u2192 ${result.notes}`);
|
|
141
|
+
}
|
|
142
|
+
if (result.status === 'passed') {
|
|
143
|
+
passedCount++;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
failedCount++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log();
|
|
150
|
+
if (failedCount === 0) {
|
|
151
|
+
console.log(`${passedCount}/${requirements.length} passed`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(`${passedCount}/${requirements.length} passed, ${failedCount} failed`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function outputJson(requirements, results) {
|
|
158
|
+
const combined = requirements.map((req, i) => ({
|
|
159
|
+
id: req.id,
|
|
160
|
+
label: req.label || null,
|
|
161
|
+
content: req.content,
|
|
162
|
+
status: results[i]?.status || 'blocked',
|
|
163
|
+
notes: results[i]?.notes || null,
|
|
164
|
+
}));
|
|
165
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=browsertest.js.map
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -25,6 +25,19 @@ export async function initCommand(options = {}) {
|
|
|
25
25
|
console.log('Use "dotrequirements link" to connect or reconnect to cloud.\n');
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
+
// MEMBERSHIP-18: If invite token provided, use invite flow
|
|
29
|
+
if (options.invite) {
|
|
30
|
+
const initSucceeded = await inviteFlow(cwd, options.invite);
|
|
31
|
+
if (initSucceeded) {
|
|
32
|
+
console.log('\nDocumentation: https://docs.dotrequirements.io/getting-started');
|
|
33
|
+
console.log('By using this service, you agree to our Terms: https://app.dotrequirements.io/terms');
|
|
34
|
+
const setupMcp = await promptConfirm('\nConfigure AI assistant integration?', true);
|
|
35
|
+
if (setupMcp) {
|
|
36
|
+
await mcpSetupCommand();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
28
41
|
// INIT-1: Ask user whether to authenticate
|
|
29
42
|
const authChoice = await promptChoice('Do you want to authenticate before initializing a project? (Recommended)', [
|
|
30
43
|
{ title: 'Yes', value: 'yes' },
|
|
@@ -290,6 +303,101 @@ async function connectExistingProjectFlow(cwd, client, team) {
|
|
|
290
303
|
console.log(' 1. Review the requirements in .requirements/');
|
|
291
304
|
console.log(' 2. Add the test harness to your test setup (see README)');
|
|
292
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* MEMBERSHIP-18: Invite flow - join a team using an invite token
|
|
308
|
+
* Returns true if successfully joined and connected
|
|
309
|
+
*/
|
|
310
|
+
async function inviteFlow(cwd, token) {
|
|
311
|
+
// MEMBERSHIP-18.0: Validate token before auth (no auth needed for this query)
|
|
312
|
+
const client = new ConvexHttpClient(getConvexUrl());
|
|
313
|
+
console.log('Validating invite token...');
|
|
314
|
+
const inviteInfo = await client.query(api.teamInvites.queries.getInviteInfo, { token });
|
|
315
|
+
// MEMBERSHIP-18.3: Handle invalid tokens
|
|
316
|
+
if (!inviteInfo.valid) {
|
|
317
|
+
const errorMessages = {
|
|
318
|
+
invalid: 'This invite token is not valid.',
|
|
319
|
+
expired: 'This invite token has expired.',
|
|
320
|
+
already_used: 'This invite token has already been used.',
|
|
321
|
+
};
|
|
322
|
+
const message = errorMessages[inviteInfo.error] || 'Invalid invite token.';
|
|
323
|
+
console.error(`\n✗ ${message}`);
|
|
324
|
+
console.log('Please ask the team admin for a new invite link.\n');
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
console.log(`\nYou've been invited to join team "${inviteInfo.teamName}"\n`);
|
|
328
|
+
// MEMBERSHIP-18.1: Auth flow
|
|
329
|
+
let oauthResult;
|
|
330
|
+
try {
|
|
331
|
+
oauthResult = await executeOAuthFlow();
|
|
332
|
+
console.log();
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.error('\nAuthentication failed:', error instanceof Error ? error.message : String(error));
|
|
336
|
+
console.log('Please try again later.\n');
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
// Set up authenticated client
|
|
340
|
+
client.setAuth(oauthResult.accessToken);
|
|
341
|
+
// Ensure default team (this also claims any pending email invites via MEMBERSHIP-4)
|
|
342
|
+
await client.mutation(api.teams.mutations.ensureDefaultTeam, {});
|
|
343
|
+
// MEMBERSHIP-18.1: Accept invite (joins team, marks token used)
|
|
344
|
+
// MEMBERSHIP-18.2: If already a member, continue gracefully
|
|
345
|
+
try {
|
|
346
|
+
await client.action(api.polar.acceptTeamInvite, { token });
|
|
347
|
+
console.log(`✓ Joined team "${inviteInfo.teamName}"`);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
351
|
+
if (errorMessage.includes('already a member')) {
|
|
352
|
+
console.log(`Already a member of "${inviteInfo.teamName}"`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// MEMBERSHIP-18.1.0: Select project from team
|
|
359
|
+
const teamId = inviteInfo.teamId;
|
|
360
|
+
// Get team tier for secret expiry prompt
|
|
361
|
+
const teams = await client.query(api.teams.queries.listForUserWithUsage);
|
|
362
|
+
const team = teams.find((t) => t.teamId === teamId);
|
|
363
|
+
const tier = team?.tier || 'free';
|
|
364
|
+
const selectedProject = await selectProject(client, teamId);
|
|
365
|
+
if (!selectedProject) {
|
|
366
|
+
console.log('Initialization cancelled.');
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
// AUTHZ-3: Prompt for secret expiry (paid tier only)
|
|
370
|
+
const expiryDays = await promptExpiryDays(tier);
|
|
371
|
+
// Get or create secret for this project
|
|
372
|
+
console.log('\nGetting project credentials...');
|
|
373
|
+
const secretResult = await getOrCreateProjectSecret(client, selectedProject.projectId, expiryDays);
|
|
374
|
+
const projectId = selectedProject.projectSlug || selectedProject.projectId;
|
|
375
|
+
const secret = secretResult.secret;
|
|
376
|
+
if (secretResult.created) {
|
|
377
|
+
console.log('✓ Created new project secret');
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
console.log('✓ Retrieved existing project secret');
|
|
381
|
+
}
|
|
382
|
+
// Write credentials to project-settings.json
|
|
383
|
+
writeProjectSettings(cwd, { projectId, projectSecret: secret });
|
|
384
|
+
console.log('✓ Wrote credentials to .requirements/project-settings.json');
|
|
385
|
+
// Create directory structure
|
|
386
|
+
await createRequirementsDirectory(cwd, projectId);
|
|
387
|
+
// Update .gitignore
|
|
388
|
+
ensureGitignore(cwd);
|
|
389
|
+
console.log('✓ Updated .gitignore\n');
|
|
390
|
+
// Pull existing requirements from cloud
|
|
391
|
+
console.log('Pulling requirements from cloud...');
|
|
392
|
+
await pullCommand({ project: projectId });
|
|
393
|
+
console.log(`\n✓ Successfully connected to project: ${selectedProject.projectName}`);
|
|
394
|
+
console.log(` Project ID: ${projectId}`);
|
|
395
|
+
// Show guidance
|
|
396
|
+
console.log('\nNext steps:');
|
|
397
|
+
console.log(' 1. Review the requirements in .requirements/');
|
|
398
|
+
console.log(' 2. Add the test harness to your test setup (see README)');
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
293
401
|
/**
|
|
294
402
|
* INIT-5: Create .requirements/ directory with README and example
|
|
295
403
|
*/
|
package/dist/convex.d.ts
CHANGED
package/dist/convex.js
CHANGED
|
@@ -41,6 +41,14 @@ export const api = {
|
|
|
41
41
|
ensureDefaultTeam: 'teams/mutations:ensureDefaultTeam',
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
|
+
teamInvites: {
|
|
45
|
+
queries: {
|
|
46
|
+
getInviteInfo: 'teamInvites/queries:getInviteInfo',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
polar: {
|
|
50
|
+
acceptTeamInvite: 'polar:acceptTeamInvite',
|
|
51
|
+
},
|
|
44
52
|
projectSecrets: {
|
|
45
53
|
queries: {
|
|
46
54
|
getOwnSecret: 'projectSecrets/queries:getOwnSecret',
|
|
@@ -4,6 +4,10 @@ This project uses dotrequirements for requirements tracking. Requirements live i
|
|
|
4
4
|
|
|
5
5
|
### Workflow
|
|
6
6
|
|
|
7
|
+
**Plan mode:** When planning behavioral changes, your plan should ALWAYS begin
|
|
8
|
+
with a step to write or update requirements. Implementation starts with
|
|
9
|
+
requirements, so your plan should too.
|
|
10
|
+
|
|
7
11
|
**ALWAYS follow this workflow when changing system behavior** (new features, bug fixes, any behavioral change). Only pure refactoring (same behavior, different code) may skip requirements.
|
|
8
12
|
|
|
9
13
|
1. **Find or write requirements** - Check `.requirements/` for existing specs; write new ones if needed
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser test configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface BrowserTestSettings {
|
|
5
|
+
geminiApiKey?: string;
|
|
6
|
+
vercelBypassSecret?: string;
|
|
7
|
+
browserbaseApiKey?: string;
|
|
8
|
+
browserbaseProjectId?: string;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Project credentials stored in project-settings.json
|
|
3
12
|
*/
|
|
4
13
|
export interface ProjectSettings {
|
|
5
14
|
projectId: string;
|
|
6
15
|
projectSecret: string;
|
|
16
|
+
defaultURL?: string;
|
|
17
|
+
browserTest?: BrowserTestSettings;
|
|
7
18
|
}
|
|
8
19
|
/**
|
|
9
20
|
* Project status indicating whether it's connected to cloud
|
|
@@ -68,10 +68,32 @@ export function readProjectSettings(projectRoot) {
|
|
|
68
68
|
typeof parsed.projectSecret !== 'string') {
|
|
69
69
|
throw new Error('project-settings.json is invalid: missing projectId or projectSecret');
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const record = parsed;
|
|
72
|
+
const settings = {
|
|
73
|
+
projectId: record.projectId,
|
|
74
|
+
projectSecret: record.projectSecret,
|
|
74
75
|
};
|
|
76
|
+
// Optional fields
|
|
77
|
+
if (typeof record.defaultURL === 'string') {
|
|
78
|
+
settings.defaultURL = record.defaultURL;
|
|
79
|
+
}
|
|
80
|
+
if (typeof record.browserTest === 'object' && record.browserTest !== null) {
|
|
81
|
+
const bt = record.browserTest;
|
|
82
|
+
settings.browserTest = {};
|
|
83
|
+
if (typeof bt.geminiApiKey === 'string') {
|
|
84
|
+
settings.browserTest.geminiApiKey = bt.geminiApiKey;
|
|
85
|
+
}
|
|
86
|
+
if (typeof bt.vercelBypassSecret === 'string') {
|
|
87
|
+
settings.browserTest.vercelBypassSecret = bt.vercelBypassSecret;
|
|
88
|
+
}
|
|
89
|
+
if (typeof bt.browserbaseApiKey === 'string') {
|
|
90
|
+
settings.browserTest.browserbaseApiKey = bt.browserbaseApiKey;
|
|
91
|
+
}
|
|
92
|
+
if (typeof bt.browserbaseProjectId === 'string') {
|
|
93
|
+
settings.browserTest.browserbaseProjectId = bt.browserbaseProjectId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return settings;
|
|
75
97
|
}
|
|
76
98
|
/**
|
|
77
99
|
* Write project settings to .requirements/project-settings.json
|