@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.
- package/README.md +478 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +82 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +355 -0
- package/dist/commands/link.d.ts +15 -0
- package/dist/commands/link.js +156 -0
- package/dist/commands/login.d.ts +12 -0
- package/dist/commands/login.js +117 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/mcp-setup.d.ts +5 -0
- package/dist/commands/mcp-setup.js +367 -0
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +10 -0
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/push.js +310 -0
- package/dist/commands/test.d.ts +6 -0
- package/dist/commands/test.js +78 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +16 -0
- package/dist/convex.d.ts +56 -0
- package/dist/convex.js +58 -0
- package/dist/harness/cache.d.ts +135 -0
- package/dist/harness/cache.js +342 -0
- package/dist/harness/convexReporting.d.ts +15 -0
- package/dist/harness/convexReporting.js +136 -0
- package/dist/harness/coverageCache.d.ts +30 -0
- package/dist/harness/coverageCache.js +70 -0
- package/dist/harness/finalize.d.ts +48 -0
- package/dist/harness/finalize.js +299 -0
- package/dist/harness/index.d.ts +70 -0
- package/dist/harness/index.js +103 -0
- package/dist/harness/localReporting.d.ts +6 -0
- package/dist/harness/localReporting.js +49 -0
- package/dist/harness/prepare.d.ts +41 -0
- package/dist/harness/prepare.js +83 -0
- package/dist/harness/requirementsLoader.d.ts +45 -0
- package/dist/harness/requirementsLoader.js +201 -0
- package/dist/harness/tracking.d.ts +49 -0
- package/dist/harness/tracking.js +179 -0
- package/dist/harness/types.d.ts +12 -0
- package/dist/harness/types.js +6 -0
- package/dist/mcp/convexClient.d.ts +43 -0
- package/dist/mcp/convexClient.js +101 -0
- package/dist/mcp/grep.d.ts +24 -0
- package/dist/mcp/grep.js +261 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +1758 -0
- package/dist/mcp/requirements.d.ts +47 -0
- package/dist/mcp/requirements.js +141 -0
- package/dist/mcp/testCodeExtractor.d.ts +22 -0
- package/dist/mcp/testCodeExtractor.js +152 -0
- package/dist/mcp/types.d.ts +27 -0
- package/dist/mcp/types.js +2 -0
- package/dist/schema/browser.d.ts +12 -0
- package/dist/schema/browser.js +24 -0
- package/dist/schema/builder.d.ts +25 -0
- package/dist/schema/builder.js +125 -0
- package/dist/schema/conversions.d.ts +69 -0
- package/dist/schema/conversions.js +201 -0
- package/dist/schema/index.d.ts +14 -0
- package/dist/schema/index.js +24 -0
- package/dist/schema/parser-core.d.ts +61 -0
- package/dist/schema/parser-core.js +247 -0
- package/dist/schema/parser.d.ts +44 -0
- package/dist/schema/parser.js +295 -0
- package/dist/schema/resolver.d.ts +66 -0
- package/dist/schema/resolver.js +185 -0
- package/dist/schema/schemas.d.ts +312 -0
- package/dist/schema/schemas.js +258 -0
- package/dist/schema/test-schema.d.ts +5 -0
- package/dist/schema/test-schema.js +81 -0
- package/dist/templates/antigravity-gemini.md +3 -0
- package/dist/templates/antigravity-overview-rule.md +3 -0
- package/dist/templates/antigravity-test-rule.md +3 -0
- package/dist/templates/behavioral-core.md +25 -0
- package/dist/templates/claude-code-overview-skill.md +6 -0
- package/dist/templates/claude-code-skill.md +6 -0
- package/dist/templates/claude-code-test-skill.md +6 -0
- package/dist/templates/codex-agents.md +3 -0
- package/dist/templates/codex-overview-agents.md +3 -0
- package/dist/templates/codex-test-agents.md +3 -0
- package/dist/templates/cursor-overview-rule.mdc +5 -0
- package/dist/templates/cursor-rule.mdc +5 -0
- package/dist/templates/cursor-test-rule.mdc +5 -0
- package/dist/templates/example-requirements.d.ts +8 -0
- package/dist/templates/example-requirements.js +88 -0
- package/dist/templates/example-requirements.ts +88 -0
- package/dist/templates/overview-core.md +27 -0
- package/dist/templates/requirements-readme.d.ts +5 -0
- package/dist/templates/requirements-readme.js +31 -0
- package/dist/templates/requirements-readme.ts +30 -0
- package/dist/templates/test-writing-core.md +72 -0
- package/dist/utils/brand.d.ts +5 -0
- package/dist/utils/brand.js +8 -0
- package/dist/utils/browser-launch.d.ts +19 -0
- package/dist/utils/browser-launch.js +36 -0
- package/dist/utils/detect-existing-project.d.ts +5 -0
- package/dist/utils/detect-existing-project.js +34 -0
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.js +56 -0
- package/dist/utils/gitignore.d.ts +7 -0
- package/dist/utils/gitignore.js +29 -0
- package/dist/utils/local-project.d.ts +31 -0
- package/dist/utils/local-project.js +33 -0
- package/dist/utils/oauth-callback-server.d.ts +28 -0
- package/dist/utils/oauth-callback-server.js +156 -0
- package/dist/utils/oauth-flow.d.ts +22 -0
- package/dist/utils/oauth-flow.js +120 -0
- package/dist/utils/project-discovery.d.ts +57 -0
- package/dist/utils/project-discovery.js +146 -0
- package/dist/utils/project-name.d.ts +8 -0
- package/dist/utils/project-name.js +48 -0
- package/dist/utils/project-selector.d.ts +25 -0
- package/dist/utils/project-selector.js +69 -0
- package/dist/utils/prompts.d.ts +33 -0
- package/dist/utils/prompts.js +60 -0
- package/dist/utils/templates.d.ts +29 -0
- package/dist/utils/templates.js +67 -0
- package/dist/utils/token-refresh.d.ts +24 -0
- package/dist/utils/token-refresh.js +69 -0
- package/dist/utils/token-storage.d.ts +31 -0
- package/dist/utils/token-storage.js +57 -0
- 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
|