@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 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,6 @@
1
+ interface BrowserTestOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function browserTestCommand(requirementKey: string, url: string | undefined, options: BrowserTestOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=browsertest.d.ts.map
@@ -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
@@ -1,5 +1,6 @@
1
1
  interface InitOptions {
2
2
  name?: string;
3
+ invite?: string;
3
4
  }
4
5
  export declare function initCommand(options?: InitOptions): Promise<void>;
5
6
  export {};
@@ -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
@@ -39,6 +39,14 @@ export declare const api: {
39
39
  ensureDefaultTeam: any;
40
40
  };
41
41
  };
42
+ teamInvites: {
43
+ queries: {
44
+ getInviteInfo: any;
45
+ };
46
+ };
47
+ polar: {
48
+ acceptTeamInvite: any;
49
+ };
42
50
  projectSecrets: {
43
51
  queries: {
44
52
  getOwnSecret: any;
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
- return {
72
- projectId: parsed.projectId,
73
- projectSecret: parsed.projectSecret,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {