@popoverai/dotrequirements 0.11.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.
Files changed (127) hide show
  1. package/README.md +478 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +82 -0
  4. package/dist/commands/init.d.ts +6 -0
  5. package/dist/commands/init.js +355 -0
  6. package/dist/commands/link.d.ts +15 -0
  7. package/dist/commands/link.js +156 -0
  8. package/dist/commands/login.d.ts +12 -0
  9. package/dist/commands/login.js +117 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +17 -0
  12. package/dist/commands/mcp-setup.d.ts +5 -0
  13. package/dist/commands/mcp-setup.js +367 -0
  14. package/dist/commands/mcp.d.ts +6 -0
  15. package/dist/commands/mcp.js +10 -0
  16. package/dist/commands/pull.d.ts +7 -0
  17. package/dist/commands/pull.js +171 -0
  18. package/dist/commands/push.d.ts +11 -0
  19. package/dist/commands/push.js +310 -0
  20. package/dist/commands/test.d.ts +6 -0
  21. package/dist/commands/test.js +78 -0
  22. package/dist/config.d.ts +5 -0
  23. package/dist/config.js +16 -0
  24. package/dist/convex.d.ts +56 -0
  25. package/dist/convex.js +58 -0
  26. package/dist/harness/cache.d.ts +135 -0
  27. package/dist/harness/cache.js +342 -0
  28. package/dist/harness/convexReporting.d.ts +15 -0
  29. package/dist/harness/convexReporting.js +136 -0
  30. package/dist/harness/coverageCache.d.ts +30 -0
  31. package/dist/harness/coverageCache.js +70 -0
  32. package/dist/harness/finalize.d.ts +48 -0
  33. package/dist/harness/finalize.js +299 -0
  34. package/dist/harness/index.d.ts +70 -0
  35. package/dist/harness/index.js +103 -0
  36. package/dist/harness/localReporting.d.ts +6 -0
  37. package/dist/harness/localReporting.js +49 -0
  38. package/dist/harness/prepare.d.ts +41 -0
  39. package/dist/harness/prepare.js +83 -0
  40. package/dist/harness/requirementsLoader.d.ts +45 -0
  41. package/dist/harness/requirementsLoader.js +201 -0
  42. package/dist/harness/tracking.d.ts +49 -0
  43. package/dist/harness/tracking.js +179 -0
  44. package/dist/harness/types.d.ts +12 -0
  45. package/dist/harness/types.js +6 -0
  46. package/dist/mcp/convexClient.d.ts +43 -0
  47. package/dist/mcp/convexClient.js +101 -0
  48. package/dist/mcp/grep.d.ts +24 -0
  49. package/dist/mcp/grep.js +261 -0
  50. package/dist/mcp/index.d.ts +3 -0
  51. package/dist/mcp/index.js +1758 -0
  52. package/dist/mcp/requirements.d.ts +47 -0
  53. package/dist/mcp/requirements.js +141 -0
  54. package/dist/mcp/testCodeExtractor.d.ts +22 -0
  55. package/dist/mcp/testCodeExtractor.js +152 -0
  56. package/dist/mcp/types.d.ts +27 -0
  57. package/dist/mcp/types.js +2 -0
  58. package/dist/schema/browser.d.ts +12 -0
  59. package/dist/schema/browser.js +24 -0
  60. package/dist/schema/builder.d.ts +25 -0
  61. package/dist/schema/builder.js +125 -0
  62. package/dist/schema/conversions.d.ts +69 -0
  63. package/dist/schema/conversions.js +201 -0
  64. package/dist/schema/index.d.ts +14 -0
  65. package/dist/schema/index.js +24 -0
  66. package/dist/schema/parser-core.d.ts +61 -0
  67. package/dist/schema/parser-core.js +247 -0
  68. package/dist/schema/parser.d.ts +44 -0
  69. package/dist/schema/parser.js +295 -0
  70. package/dist/schema/resolver.d.ts +66 -0
  71. package/dist/schema/resolver.js +185 -0
  72. package/dist/schema/schemas.d.ts +312 -0
  73. package/dist/schema/schemas.js +258 -0
  74. package/dist/schema/test-schema.d.ts +5 -0
  75. package/dist/schema/test-schema.js +81 -0
  76. package/dist/templates/antigravity-gemini.md +3 -0
  77. package/dist/templates/antigravity-overview-rule.md +3 -0
  78. package/dist/templates/antigravity-test-rule.md +3 -0
  79. package/dist/templates/behavioral-core.md +25 -0
  80. package/dist/templates/claude-code-overview-skill.md +6 -0
  81. package/dist/templates/claude-code-skill.md +6 -0
  82. package/dist/templates/claude-code-test-skill.md +6 -0
  83. package/dist/templates/codex-agents.md +3 -0
  84. package/dist/templates/codex-overview-agents.md +3 -0
  85. package/dist/templates/codex-test-agents.md +3 -0
  86. package/dist/templates/cursor-overview-rule.mdc +5 -0
  87. package/dist/templates/cursor-rule.mdc +5 -0
  88. package/dist/templates/cursor-test-rule.mdc +5 -0
  89. package/dist/templates/example-requirements.d.ts +8 -0
  90. package/dist/templates/example-requirements.js +88 -0
  91. package/dist/templates/example-requirements.ts +88 -0
  92. package/dist/templates/overview-core.md +27 -0
  93. package/dist/templates/requirements-readme.d.ts +5 -0
  94. package/dist/templates/requirements-readme.js +31 -0
  95. package/dist/templates/requirements-readme.ts +30 -0
  96. package/dist/templates/test-writing-core.md +72 -0
  97. package/dist/utils/brand.d.ts +5 -0
  98. package/dist/utils/brand.js +8 -0
  99. package/dist/utils/browser-launch.d.ts +19 -0
  100. package/dist/utils/browser-launch.js +36 -0
  101. package/dist/utils/detect-existing-project.d.ts +5 -0
  102. package/dist/utils/detect-existing-project.js +34 -0
  103. package/dist/utils/env.d.ts +19 -0
  104. package/dist/utils/env.js +56 -0
  105. package/dist/utils/gitignore.d.ts +7 -0
  106. package/dist/utils/gitignore.js +29 -0
  107. package/dist/utils/local-project.d.ts +31 -0
  108. package/dist/utils/local-project.js +33 -0
  109. package/dist/utils/oauth-callback-server.d.ts +28 -0
  110. package/dist/utils/oauth-callback-server.js +156 -0
  111. package/dist/utils/oauth-flow.d.ts +22 -0
  112. package/dist/utils/oauth-flow.js +120 -0
  113. package/dist/utils/project-discovery.d.ts +57 -0
  114. package/dist/utils/project-discovery.js +146 -0
  115. package/dist/utils/project-name.d.ts +8 -0
  116. package/dist/utils/project-name.js +48 -0
  117. package/dist/utils/project-selector.d.ts +25 -0
  118. package/dist/utils/project-selector.js +69 -0
  119. package/dist/utils/prompts.d.ts +33 -0
  120. package/dist/utils/prompts.js +60 -0
  121. package/dist/utils/templates.d.ts +29 -0
  122. package/dist/utils/templates.js +67 -0
  123. package/dist/utils/token-refresh.d.ts +24 -0
  124. package/dist/utils/token-refresh.js +69 -0
  125. package/dist/utils/token-storage.d.ts +31 -0
  126. package/dist/utils/token-storage.js +57 -0
  127. package/package.json +82 -0
@@ -0,0 +1,355 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { detectProjectName } from '../utils/project-name.js';
4
+ import { ensureGitignore } from '../utils/gitignore.js';
5
+ import { generateRequirementsReadme } from '../templates/requirements-readme.js';
6
+ import { generateExampleRequirements } from '../templates/example-requirements.js';
7
+ import { promptChoice, promptConfirm } from '../utils/prompts.js';
8
+ import { pullCommand } from './pull.js';
9
+ import { LOCAL_PROJECT_CREDENTIALS } from '../utils/local-project.js';
10
+ import { loadEnvFile } from '../utils/env.js';
11
+ import { hasValidTokens, getTokens } from '../utils/token-storage.js';
12
+ import { executeOAuthFlow } from '../utils/oauth-flow.js';
13
+ import { ConvexHttpClient } from 'convex/browser';
14
+ import { getConvexUrl } from '../config.js';
15
+ import { api } from '../convex.js';
16
+ import { brand } from '../utils/brand.js';
17
+ import { selectTeam as selectTeamFromList, selectProject, getOrCreateProjectSecret } from '../utils/project-selector.js';
18
+ export async function initCommand(options = {}) {
19
+ try {
20
+ console.log(`Initializing ${brand} project...\n`);
21
+ const cwd = process.cwd();
22
+ const envPath = path.join(cwd, '.env.local');
23
+ // 1. Pre-flight check: already initialized?
24
+ if (fs.existsSync(envPath)) {
25
+ const content = fs.readFileSync(envPath, 'utf-8');
26
+ if (content.includes('DOTREQUIREMENTS_PROJECT_ID')) {
27
+ const shouldReinit = await promptConfirm('Project already initialized. Reinitialize?', false);
28
+ if (!shouldReinit) {
29
+ console.log('Aborted.');
30
+ return;
31
+ }
32
+ }
33
+ }
34
+ // 2. Ask user to choose between creating new or connecting to existing
35
+ const flow = await promptChoice('What would you like to do?', [
36
+ { title: 'Create a new project', value: 'create' },
37
+ { title: 'Connect to an existing project', value: 'connect' },
38
+ ]);
39
+ if (!flow) {
40
+ console.log('Aborted.');
41
+ return;
42
+ }
43
+ // 3. Execute the appropriate flow
44
+ if (flow === 'create') {
45
+ await createNewProjectFlow(cwd, envPath, options);
46
+ }
47
+ else {
48
+ await connectExistingProjectFlow(cwd, envPath);
49
+ }
50
+ console.log('\nDocumentation: https://docs.dotrequirements.io/getting-started');
51
+ }
52
+ catch (error) {
53
+ console.error(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`);
54
+ if (error instanceof Error && error.message.includes('fetch')) {
55
+ console.error('\nCheck your internet connection and try again.');
56
+ console.error('If the problem persists, visit https://status.convex.dev');
57
+ }
58
+ process.exit(1);
59
+ }
60
+ }
61
+ /**
62
+ * INIT-13: Prompt user to select a team for the new project
63
+ * Returns:
64
+ * - teamId string if user selected a team
65
+ * - null if no teams exist (auto-create flow)
66
+ * - undefined if user cancelled
67
+ */
68
+ async function selectTeam(client) {
69
+ console.log('Fetching your teams...\n');
70
+ // INIT-13.0: Query all teams for the authenticated user
71
+ const teams = await client.query(api.teams.queries.listForUserWithUsage);
72
+ if (teams.length === 0) {
73
+ console.log('\nYou are not a member of any teams.');
74
+ console.log('To create a new team, visit: https://app.dotrequirements.io/teams/new');
75
+ console.log('After creating your team, run "dotrequirements init" again.\n');
76
+ process.exit(0);
77
+ }
78
+ // INIT-13.1 & INIT-13.3: Build team selection options with usage display
79
+ const teamChoices = teams.map((team) => {
80
+ const limitDisplay = team.projectLimit === -1
81
+ ? 'unlimited'
82
+ : `${team.projectCount}/${team.projectLimit}`;
83
+ // INIT-13.2: Mark teams at capacity
84
+ const capacityMarker = team.isAtCapacity ? ' [at capacity]' : '';
85
+ return {
86
+ title: `${team.name} (${team.tierName} - ${limitDisplay} projects)${capacityMarker}`,
87
+ value: team.teamId,
88
+ disabled: team.isAtCapacity, // Make at-capacity teams unselectable
89
+ };
90
+ });
91
+ // INIT-13.3: Add "Create new team" option
92
+ teamChoices.push({
93
+ title: 'Create new team',
94
+ value: 'CREATE_NEW_TEAM',
95
+ disabled: false,
96
+ });
97
+ const selectedTeamId = await promptChoice('Select a team for this project:', teamChoices);
98
+ if (!selectedTeamId) {
99
+ // User cancelled
100
+ return undefined;
101
+ }
102
+ // INIT-14: Handle selection of at-capacity team (should not happen due to disabled flag, but defensive check)
103
+ const selectedTeam = teams.find((t) => t.teamId === selectedTeamId);
104
+ if (selectedTeam && selectedTeam.isAtCapacity) {
105
+ console.error('\n✗ This team is at its project limit. Please select a different team or create a new one.\n');
106
+ // Retry selection
107
+ return await selectTeam(client);
108
+ }
109
+ // INIT-15: Handle "Create new team" selection
110
+ if (selectedTeamId === 'CREATE_NEW_TEAM') {
111
+ console.log('\nTo create a new team, visit: https://app.dotrequirements.io/teams/new');
112
+ console.log('After creating your team, run "dotrequirements init" again.\n');
113
+ process.exit(0);
114
+ }
115
+ // INIT-13.4: Return selected team ID
116
+ return selectedTeamId;
117
+ }
118
+ /**
119
+ * Create a new project flow (INIT-3, INIT-4, INIT-13)
120
+ */
121
+ async function createNewProjectFlow(cwd, envPath, options) {
122
+ // 1. Detect/prompt for project name
123
+ const projectName = await detectProjectName({ name: options.name });
124
+ console.log(`Project name: ${projectName}\n`);
125
+ // 2. Check if user is authenticated
126
+ const isAuthenticated = await hasValidTokens();
127
+ let projectId; // Can be slug (new) or Convex ID (old/local)
128
+ let secret;
129
+ if (isAuthenticated) {
130
+ // INIT-4: Authenticated user - create cloud project
131
+ const tokens = await getTokens();
132
+ if (!tokens) {
133
+ throw new Error('Authentication tokens not found');
134
+ }
135
+ const client = new ConvexHttpClient(getConvexUrl());
136
+ client.setAuth(tokens.accessToken);
137
+ // INIT-13: Select team for the project (returns null if no teams exist, undefined if cancelled)
138
+ const selectedTeamId = await selectTeam(client);
139
+ if (selectedTeamId === undefined) {
140
+ // User cancelled (note: "Create new team" exits in selectTeam)
141
+ console.log('Aborted.');
142
+ return;
143
+ }
144
+ console.log('Creating cloud project...');
145
+ const result = await client.mutation(api.projects.mutations.create, {
146
+ name: projectName,
147
+ teamId: selectedTeamId ?? undefined, // null becomes undefined for auto-create
148
+ });
149
+ // Use slug as the user-facing project identifier
150
+ projectId = result.slug;
151
+ secret = result.secret;
152
+ console.log(`✓ Cloud project created: ${projectId}`);
153
+ }
154
+ else {
155
+ // INIT-3: Unauthenticated user - create local-only project with placeholder credentials
156
+ console.log('Initializing local project...');
157
+ projectId = LOCAL_PROJECT_CREDENTIALS.projectId;
158
+ secret = LOCAL_PROJECT_CREDENTIALS.secret;
159
+ console.log(`✓ Project initialized locally`);
160
+ }
161
+ // 3. Write .env.local
162
+ const shouldContinue = await writeEnvFile(envPath, projectId, secret);
163
+ if (!shouldContinue) {
164
+ // User declined to continue - abort init
165
+ return;
166
+ }
167
+ // Reload env file so newly written credentials are available to subsequent commands
168
+ loadEnvFile(cwd);
169
+ // 4. Create .requirements/ directory
170
+ await createRequirementsDirectory(cwd, projectId);
171
+ // 5. Update .gitignore
172
+ ensureGitignore(cwd);
173
+ console.log('✓ Updated .gitignore\n');
174
+ // 6. Show next steps
175
+ console.log('Next steps:');
176
+ console.log(' 1. Create requirements in .requirements/ directory');
177
+ console.log(' 2. Add the test harness to your test setup (see README)');
178
+ if (!isAuthenticated) {
179
+ // INIT-3.1: Local-only guidance
180
+ console.log(' 3. Run "dotrequirements login" for cloud features (push/pull, coverage, AI)');
181
+ }
182
+ }
183
+ /**
184
+ * Connect to existing project flow
185
+ */
186
+ async function connectExistingProjectFlow(cwd, envPath) {
187
+ // INIT-6.0: Check if user is authenticated, prompt to log in if not
188
+ let isAuthenticated = await hasValidTokens();
189
+ if (!isAuthenticated) {
190
+ const shouldLogin = await promptConfirm('You need to authenticate to connect to an existing project. Log in now?', true);
191
+ if (!shouldLogin) {
192
+ console.log('Aborted.');
193
+ return;
194
+ }
195
+ // INIT-6.1: Initiate OAuth login flow inline
196
+ try {
197
+ console.log();
198
+ await executeOAuthFlow();
199
+ console.log();
200
+ isAuthenticated = true;
201
+ }
202
+ catch (error) {
203
+ console.error('\nAuthentication failed:', error instanceof Error ? error.message : String(error));
204
+ console.log('Please try running "dotrequirements login" separately.\n');
205
+ return;
206
+ }
207
+ }
208
+ // Set up authenticated client
209
+ const tokens = await getTokens();
210
+ if (!tokens) {
211
+ throw new Error('Authentication tokens not found');
212
+ }
213
+ const client = new ConvexHttpClient(getConvexUrl());
214
+ client.setAuth(tokens.accessToken);
215
+ // INIT-6.2: Select team
216
+ const selectedTeamId = await selectTeamFromList(client);
217
+ if (!selectedTeamId) {
218
+ console.log('Aborted.');
219
+ return;
220
+ }
221
+ // INIT-6.3: Select project
222
+ const selectedProject = await selectProject(client, selectedTeamId);
223
+ if (!selectedProject) {
224
+ console.log('Aborted.');
225
+ return;
226
+ }
227
+ // INIT-6.4: Get or create secret for this project
228
+ console.log('\nGetting project credentials...');
229
+ const secretResult = await getOrCreateProjectSecret(client, selectedProject.projectId);
230
+ const projectId = selectedProject.projectSlug || selectedProject.projectId;
231
+ const secret = secretResult.secret;
232
+ if (secretResult.created) {
233
+ console.log('✓ Created new project secret');
234
+ }
235
+ else {
236
+ console.log('✓ Retrieved existing project secret');
237
+ }
238
+ // Write .env.local
239
+ const shouldContinue = await writeEnvFile(envPath, projectId, secret);
240
+ if (!shouldContinue) {
241
+ // User declined to continue - abort init
242
+ return;
243
+ }
244
+ // Reload env file so newly written credentials are available to pullCommand
245
+ loadEnvFile(cwd);
246
+ // Create .requirements/ directory
247
+ await createRequirementsDirectory(cwd, projectId);
248
+ // Update .gitignore
249
+ ensureGitignore(cwd);
250
+ console.log('✓ Updated .gitignore\n');
251
+ // Pull existing requirements from cloud
252
+ console.log('Pulling requirements from cloud...');
253
+ await pullCommand({ project: projectId });
254
+ console.log(`\n✓ Successfully connected to project: ${selectedProject.projectName}`);
255
+ console.log(` Project ID: ${projectId}`);
256
+ console.log('\nNext steps:');
257
+ console.log(' 1. Review the requirements in .requirements/');
258
+ console.log(' 2. Add the test harness to your test setup (see README)');
259
+ }
260
+ /**
261
+ * Write .env.local file with confirmation if it already exists
262
+ * Returns true if credentials were written or user chose to continue with existing
263
+ * Returns false if user aborted
264
+ */
265
+ async function writeEnvFile(envPath, projectId, secret) {
266
+ const newEnvLines = [
267
+ '# dot•requirements configuration',
268
+ `DOTREQUIREMENTS_PROJECT_ID=${projectId}`,
269
+ `DOTREQUIREMENTS_PROJECT_SECRET=${secret}`,
270
+ ];
271
+ if (fs.existsSync(envPath)) {
272
+ // Read existing content
273
+ const existingContent = fs.readFileSync(envPath, 'utf-8');
274
+ // Check if dotrequirements variables already exist
275
+ const hasProjectId = existingContent.includes('DOTREQUIREMENTS_PROJECT_ID');
276
+ const hasProjectSecret = existingContent.includes('DOTREQUIREMENTS_PROJECT_SECRET');
277
+ if (hasProjectId || hasProjectSecret) {
278
+ // INIT-2.0: Ask if user wants to update existing values
279
+ const shouldUpdate = await promptConfirm(`.env.local already contains ${brand} configuration. Update it?`, false);
280
+ if (!shouldUpdate) {
281
+ // INIT-2.1: Ask if user wants to continue with existing credentials
282
+ const shouldContinue = await promptConfirm('Continue with existing credentials?', true);
283
+ if (!shouldContinue) {
284
+ // INIT-2.3: User declined to continue - abort
285
+ console.log('Aborted.');
286
+ return false;
287
+ }
288
+ // INIT-2.2: User wants to continue with existing credentials
289
+ console.log('Continuing with existing credentials...');
290
+ return true;
291
+ }
292
+ // Remove old dotrequirements lines
293
+ const lines = existingContent.split('\n');
294
+ const filteredLines = lines.filter(line => !line.includes('DOTREQUIREMENTS_PROJECT_ID') &&
295
+ !line.includes('DOTREQUIREMENTS_PROJECT_SECRET') &&
296
+ !(line.trim() === '# dotrequirements configuration') &&
297
+ !(line.trim() === '# dot•requirements configuration'));
298
+ // Append new configuration
299
+ const updatedContent = filteredLines.join('\n').trimEnd() + '\n\n' + newEnvLines.join('\n') + '\n';
300
+ fs.writeFileSync(envPath, updatedContent);
301
+ console.log(`✓ Updated ${brand} configuration in .env.local`);
302
+ return true;
303
+ }
304
+ else {
305
+ // File exists but doesn't have dotrequirements config - just append
306
+ const shouldAppend = await promptConfirm(`.env.local already exists. Add ${brand} configuration to it?`, true);
307
+ if (!shouldAppend) {
308
+ console.log('\nSkipped writing to .env.local. Please add these values manually:');
309
+ console.log(` DOTREQUIREMENTS_PROJECT_ID=${projectId}`);
310
+ console.log(` DOTREQUIREMENTS_PROJECT_SECRET=${secret}\n`);
311
+ return true; // Continue with init even though we didn't write the file
312
+ }
313
+ // Append to existing file
314
+ const appendContent = '\n' + newEnvLines.join('\n') + '\n';
315
+ fs.appendFileSync(envPath, appendContent);
316
+ console.log(`✓ Added ${brand} configuration to .env.local`);
317
+ return true;
318
+ }
319
+ }
320
+ else {
321
+ // File doesn't exist - create it
322
+ const envContent = newEnvLines.join('\n') + '\n';
323
+ fs.writeFileSync(envPath, envContent);
324
+ console.log('✓ Wrote configuration to .env.local');
325
+ return true;
326
+ }
327
+ }
328
+ /**
329
+ * Create .requirements/ directory with confirmation if it already exists
330
+ */
331
+ async function createRequirementsDirectory(cwd, projectId) {
332
+ const requirementsDir = path.join(cwd, '.requirements');
333
+ if (fs.existsSync(requirementsDir)) {
334
+ const shouldOverwrite = await promptConfirm('.requirements/ directory already exists. Continue?', true);
335
+ if (!shouldOverwrite) {
336
+ console.log('Skipped creating .requirements/ directory');
337
+ return;
338
+ }
339
+ }
340
+ else {
341
+ fs.mkdirSync(requirementsDir, { recursive: true });
342
+ console.log('✓ Created .requirements/ directory');
343
+ }
344
+ // Always create/update README
345
+ const readmePath = path.join(requirementsDir, 'README.md');
346
+ fs.writeFileSync(readmePath, generateRequirementsReadme());
347
+ console.log('✓ Created .requirements/README.md');
348
+ // Create example.requirements.md (INIT-5.1)
349
+ const examplePath = path.join(requirementsDir, 'example.requirements.md');
350
+ if (!fs.existsSync(examplePath)) {
351
+ fs.writeFileSync(examplePath, generateExampleRequirements(projectId));
352
+ console.log('✓ Created .requirements/example.requirements.md');
353
+ }
354
+ }
355
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Link command - connect local environment to an existing project.
3
+ *
4
+ * Flow:
5
+ * 1. Check authentication (prompt to login if not)
6
+ * 2. Fetch user's teams with usage info
7
+ * 3. Let user select a team
8
+ * 4. Fetch projects for that team
9
+ * 5. Let user select a project
10
+ * 6. Get or create user's secret for that project
11
+ * 7. Write credentials to .env.local
12
+ * 8. Optionally pull requirements
13
+ */
14
+ export declare function linkCommand(): Promise<void>;
15
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1,156 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { promptConfirm } from '../utils/prompts.js';
4
+ import { pullCommand } from './pull.js';
5
+ import { hasValidTokens, getTokens } from '../utils/token-storage.js';
6
+ import { executeOAuthFlow } from '../utils/oauth-flow.js';
7
+ import { ConvexHttpClient } from 'convex/browser';
8
+ import { getConvexUrl } from '../config.js';
9
+ import { brand } from '../utils/brand.js';
10
+ import { selectTeam, selectProject, getOrCreateProjectSecret } from '../utils/project-selector.js';
11
+ /**
12
+ * Link command - connect local environment to an existing project.
13
+ *
14
+ * Flow:
15
+ * 1. Check authentication (prompt to login if not)
16
+ * 2. Fetch user's teams with usage info
17
+ * 3. Let user select a team
18
+ * 4. Fetch projects for that team
19
+ * 5. Let user select a project
20
+ * 6. Get or create user's secret for that project
21
+ * 7. Write credentials to .env.local
22
+ * 8. Optionally pull requirements
23
+ */
24
+ export async function linkCommand() {
25
+ try {
26
+ console.log(`Linking to existing ${brand} project...\n`);
27
+ const cwd = process.cwd();
28
+ const envPath = path.join(cwd, '.env.local');
29
+ // 1. Check authentication
30
+ let isAuthenticated = await hasValidTokens();
31
+ if (!isAuthenticated) {
32
+ const shouldLogin = await promptConfirm('You need to authenticate to link to a project. Log in now?', true);
33
+ if (!shouldLogin) {
34
+ console.log('Aborted.');
35
+ return;
36
+ }
37
+ try {
38
+ console.log();
39
+ await executeOAuthFlow();
40
+ console.log();
41
+ isAuthenticated = true;
42
+ }
43
+ catch (error) {
44
+ console.error('\nAuthentication failed:', error instanceof Error ? error.message : String(error));
45
+ console.log('Please try running "dotrequirements login" separately.\n');
46
+ return;
47
+ }
48
+ }
49
+ // 2. Set up authenticated client
50
+ const tokens = await getTokens();
51
+ if (!tokens) {
52
+ throw new Error('Authentication tokens not found');
53
+ }
54
+ const client = new ConvexHttpClient(getConvexUrl());
55
+ client.setAuth(tokens.accessToken);
56
+ // 3. Select team
57
+ const selectedTeamId = await selectTeam(client);
58
+ if (!selectedTeamId) {
59
+ console.log('Aborted.');
60
+ return;
61
+ }
62
+ // 4. Select project
63
+ const selectedProject = await selectProject(client, selectedTeamId);
64
+ if (!selectedProject) {
65
+ console.log('Aborted.');
66
+ return;
67
+ }
68
+ // 5. Get or create secret for this project
69
+ console.log('\nGetting project credentials...');
70
+ const secretResult = await getOrCreateProjectSecret(client, selectedProject.projectId);
71
+ const projectSlug = selectedProject.projectSlug || selectedProject.projectId;
72
+ const secret = secretResult.secret;
73
+ if (secretResult.created) {
74
+ console.log('✓ Created new project secret');
75
+ }
76
+ else {
77
+ console.log('✓ Retrieved existing project secret');
78
+ }
79
+ // 6. Write .env.local
80
+ const shouldContinue = await writeEnvFile(envPath, projectSlug, secret);
81
+ if (!shouldContinue) {
82
+ return;
83
+ }
84
+ // 7. Optionally pull requirements
85
+ const shouldPull = await promptConfirm('Pull requirements from cloud now?', true);
86
+ if (shouldPull) {
87
+ console.log('\nPulling requirements...');
88
+ await pullCommand({ project: projectSlug });
89
+ }
90
+ console.log(`\n✓ Successfully linked to project: ${selectedProject.projectName}`);
91
+ console.log(` Project ID: ${projectSlug}`);
92
+ console.log('\nDocumentation: https://docs.dotrequirements.io/getting-started');
93
+ }
94
+ catch (error) {
95
+ console.error(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`);
96
+ if (error instanceof Error && error.message.includes('fetch')) {
97
+ console.error('\nCheck your internet connection and try again.');
98
+ console.error('If the problem persists, visit https://status.convex.dev');
99
+ }
100
+ process.exit(1);
101
+ }
102
+ }
103
+ /**
104
+ * Write .env.local file with confirmation if it already exists
105
+ * Returns true if credentials were written or user chose to continue
106
+ * Returns false if user aborted
107
+ */
108
+ async function writeEnvFile(envPath, projectId, secret) {
109
+ const newEnvLines = [
110
+ '# dot•requirements configuration',
111
+ `DOTREQUIREMENTS_PROJECT_ID=${projectId}`,
112
+ `DOTREQUIREMENTS_PROJECT_SECRET=${secret}`,
113
+ ];
114
+ if (fs.existsSync(envPath)) {
115
+ const existingContent = fs.readFileSync(envPath, 'utf-8');
116
+ const hasProjectId = existingContent.includes('DOTREQUIREMENTS_PROJECT_ID');
117
+ const hasProjectSecret = existingContent.includes('DOTREQUIREMENTS_PROJECT_SECRET');
118
+ if (hasProjectId || hasProjectSecret) {
119
+ const shouldUpdate = await promptConfirm(`.env.local already contains ${brand} configuration. Update it?`, true);
120
+ if (!shouldUpdate) {
121
+ console.log('Aborted.');
122
+ return false;
123
+ }
124
+ // Remove old dotrequirements lines
125
+ const lines = existingContent.split('\n');
126
+ const filteredLines = lines.filter(line => !line.includes('DOTREQUIREMENTS_PROJECT_ID') &&
127
+ !line.includes('DOTREQUIREMENTS_PROJECT_SECRET') &&
128
+ !(line.trim() === '# dotrequirements configuration') &&
129
+ !(line.trim() === '# dot•requirements configuration'));
130
+ const updatedContent = filteredLines.join('\n').trimEnd() + '\n\n' + newEnvLines.join('\n') + '\n';
131
+ fs.writeFileSync(envPath, updatedContent);
132
+ console.log(`✓ Updated ${brand} configuration in .env.local`);
133
+ return true;
134
+ }
135
+ else {
136
+ const shouldAppend = await promptConfirm(`.env.local already exists. Add ${brand} configuration to it?`, true);
137
+ if (!shouldAppend) {
138
+ console.log('\nSkipped writing to .env.local. Please add these values manually:');
139
+ console.log(` DOTREQUIREMENTS_PROJECT_ID=${projectId}`);
140
+ console.log(` DOTREQUIREMENTS_PROJECT_SECRET=${secret}\n`);
141
+ return true;
142
+ }
143
+ const appendContent = '\n' + newEnvLines.join('\n') + '\n';
144
+ fs.appendFileSync(envPath, appendContent);
145
+ console.log(`✓ Added ${brand} configuration to .env.local`);
146
+ return true;
147
+ }
148
+ }
149
+ else {
150
+ const envContent = newEnvLines.join('\n') + '\n';
151
+ fs.writeFileSync(envPath, envContent);
152
+ console.log('✓ Wrote configuration to .env.local');
153
+ return true;
154
+ }
155
+ }
156
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Login command - authenticate with WorkOS and manage project credentials.
3
+ *
4
+ * Implements:
5
+ * - LOGIN-1: CLI OAuth implementation
6
+ * - LOGIN-2: Upgrade from placeholder to real credentials
7
+ * - LOGIN-3: Refresh existing project credentials
8
+ * - LOGIN-4: Already logged in detection
9
+ * - LOGIN-6: Handle missing .env.local
10
+ */
11
+ export declare function loginCommand(): Promise<void>;
12
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Login command - authenticate with WorkOS and manage project credentials.
3
+ *
4
+ * Implements:
5
+ * - LOGIN-1: CLI OAuth implementation
6
+ * - LOGIN-2: Upgrade from placeholder to real credentials
7
+ * - LOGIN-3: Refresh existing project credentials
8
+ * - LOGIN-4: Already logged in detection
9
+ * - LOGIN-6: Handle missing .env.local
10
+ */
11
+ import { executeOAuthFlow } from '../utils/oauth-flow.js';
12
+ import { getConvexUrl } from '../config.js';
13
+ import { hasValidTokens } from '../utils/token-storage.js';
14
+ import { isLocalOnlyProject } from '../utils/local-project.js';
15
+ import * as fs from 'fs';
16
+ import { ConvexHttpClient } from 'convex/browser';
17
+ import { findUp } from 'find-up';
18
+ import { brand } from '../utils/brand.js';
19
+ export async function loginCommand() {
20
+ console.log(`Logging in to ${brand}...\n`);
21
+ // LOGIN-4: Check if already logged in
22
+ if (await hasValidTokens()) {
23
+ console.log('✓ Already logged in');
24
+ console.log('Run "dotrequirements logout" to log in as a different user.');
25
+ return;
26
+ }
27
+ // LOGIN-1: Execute OAuth flow
28
+ const result = await executeOAuthFlow();
29
+ console.log('\n✓ Logged in successfully!');
30
+ // LOGIN-6: Check if there's a .env.local in the current directory
31
+ const envPath = await findUp('.env.local');
32
+ if (envPath) {
33
+ await handleExistingProject(envPath, result.accessToken);
34
+ }
35
+ else {
36
+ // LOGIN-6.1: No project in current directory
37
+ console.log('\nNo project found in this directory.');
38
+ console.log('Run "dotrequirements init" to create a new project in the current directory.');
39
+ }
40
+ }
41
+ /**
42
+ * Handle existing project based on credential type (placeholder vs real)
43
+ */
44
+ async function handleExistingProject(envPath, accessToken) {
45
+ try {
46
+ // Read .env.local to get project ID and secret
47
+ const envContent = fs.readFileSync(envPath, 'utf-8');
48
+ const projectIdMatch = envContent.match(/DOTREQUIREMENTS_PROJECT_ID=(.+)/);
49
+ const secretMatch = envContent.match(/DOTREQUIREMENTS_PROJECT_SECRET=(.+)/);
50
+ if (!projectIdMatch || !secretMatch) {
51
+ console.log('\n.env.local found but missing project credentials.');
52
+ return;
53
+ }
54
+ const projectId = projectIdMatch[1].trim();
55
+ const projectSecret = secretMatch[1].trim();
56
+ const convexUrl = getConvexUrl();
57
+ const client = new ConvexHttpClient(convexUrl);
58
+ client.setAuth(accessToken);
59
+ // LOGIN-2: Check if placeholder credentials
60
+ if (isLocalOnlyProject(projectId)) {
61
+ await upgradeFromPlaceholder(envPath, envContent, client);
62
+ }
63
+ else {
64
+ // LOGIN-3: Refresh existing project credentials
65
+ await refreshCredentials(envPath, envContent, projectId, client);
66
+ }
67
+ }
68
+ catch (error) {
69
+ console.error('\nFailed to update project credentials:');
70
+ console.error(error instanceof Error ? error.message : String(error));
71
+ console.log('\nYour authentication was successful.');
72
+ console.log('You can continue using the project with its existing credentials.');
73
+ }
74
+ }
75
+ /**
76
+ * LOGIN-2: Upgrade from placeholder credentials to real project
77
+ */
78
+ async function upgradeFromPlaceholder(envPath, envContent, client) {
79
+ console.log('\nUpgrading from local-only mode to cloud access...');
80
+ // LOGIN-2.0, LOGIN-2.1: Create team and project
81
+ const result = await client.mutation('lib/authMutations:createProjectForAuthenticatedUser', {});
82
+ if (!result.success) {
83
+ throw new Error(result.error || 'Unknown error');
84
+ }
85
+ console.log('✓ Created new project in your team');
86
+ // LOGIN-2.3: Update .env.local with real credentials
87
+ const updatedEnv = envContent
88
+ .replace(/DOTREQUIREMENTS_PROJECT_ID=.+/, `DOTREQUIREMENTS_PROJECT_ID=${result.projectId}`)
89
+ .replace(/DOTREQUIREMENTS_PROJECT_SECRET=.+/, `DOTREQUIREMENTS_PROJECT_SECRET=${result.projectSecret}`);
90
+ fs.writeFileSync(envPath, updatedEnv);
91
+ console.log('✓ Updated .env.local with cloud credentials');
92
+ // LOGIN-2.4: Display next steps
93
+ console.log('\nYour project is now connected to the cloud!');
94
+ console.log('Run "dotrequirements push" to sync your requirements.');
95
+ }
96
+ /**
97
+ * LOGIN-3: Refresh existing project credentials
98
+ */
99
+ async function refreshCredentials(envPath, envContent, projectId, client) {
100
+ console.log('\nRefreshing your project credentials...');
101
+ // LOGIN-3.0: Verify access and generate new secret
102
+ const result = await client.mutation('lib/authMutations:refreshProjectSecret', {
103
+ projectId: projectId, // Cast to Id<"projects">
104
+ });
105
+ if (!result.success) {
106
+ // LOGIN-3.4: User doesn't have access
107
+ throw new Error(result.error || 'Unknown error');
108
+ }
109
+ // LOGIN-3.2: Update .env.local with new secret
110
+ const updatedEnv = envContent.replace(/DOTREQUIREMENTS_PROJECT_SECRET=.+/, `DOTREQUIREMENTS_PROJECT_SECRET=${result.projectSecret}`);
111
+ fs.writeFileSync(envPath, updatedEnv);
112
+ // LOGIN-3.3: Confirm access extended
113
+ const expiryDate = new Date(result.expiresAt).toLocaleDateString();
114
+ console.log(`✓ Access extended until ${expiryDate}`);
115
+ console.log('✓ Updated .env.local with new credentials');
116
+ }
117
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Logout command - clear stored authentication tokens.
3
+ */
4
+ export declare function logoutCommand(): Promise<void>;
5
+ //# sourceMappingURL=logout.d.ts.map