@popoverai/dotrequirements 0.13.0 → 0.15.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 (57) hide show
  1. package/README.md +92 -48
  2. package/dist/cli.js +1 -10
  3. package/dist/commands/init.js +166 -226
  4. package/dist/commands/link.d.ts +9 -10
  5. package/dist/commands/link.js +81 -106
  6. package/dist/commands/mcp-setup.js +77 -94
  7. package/dist/commands/pull.d.ts +1 -0
  8. package/dist/commands/pull.js +70 -53
  9. package/dist/commands/push.js +4 -13
  10. package/dist/config.js +0 -4
  11. package/dist/harness/cache.d.ts +0 -5
  12. package/dist/harness/cache.js +0 -48
  13. package/dist/harness/convexReporting.js +7 -14
  14. package/dist/harness/finalize.js +7 -12
  15. package/dist/harness/prepare.js +7 -9
  16. package/dist/mcp/convexClient.d.ts +9 -1
  17. package/dist/mcp/convexClient.js +15 -35
  18. package/dist/mcp/index.js +112 -152
  19. package/dist/mcp/requirements.d.ts +10 -0
  20. package/dist/mcp/requirements.js +15 -1
  21. package/dist/templates/context-file-section.md +59 -0
  22. package/dist/utils/context-file.d.ts +38 -0
  23. package/dist/utils/context-file.js +94 -0
  24. package/dist/utils/env.d.ts +0 -13
  25. package/dist/utils/env.js +0 -19
  26. package/dist/utils/gitignore.d.ts +2 -2
  27. package/dist/utils/gitignore.js +4 -4
  28. package/dist/utils/oauth-flow.d.ts +0 -1
  29. package/dist/utils/oauth-flow.js +0 -9
  30. package/dist/utils/project-discovery.d.ts +3 -5
  31. package/dist/utils/project-discovery.js +18 -42
  32. package/dist/utils/project-selector.d.ts +17 -3
  33. package/dist/utils/project-selector.js +35 -3
  34. package/dist/utils/project-settings.d.ts +56 -0
  35. package/dist/utils/project-settings.js +126 -0
  36. package/dist/utils/templates.d.ts +0 -24
  37. package/dist/utils/templates.js +0 -39
  38. package/package.json +1 -1
  39. package/dist/harness/localReporting.d.ts +0 -6
  40. package/dist/harness/localReporting.js +0 -49
  41. package/dist/templates/antigravity-gemini.md +0 -3
  42. package/dist/templates/antigravity-overview-rule.md +0 -3
  43. package/dist/templates/antigravity-test-rule.md +0 -3
  44. package/dist/templates/behavioral-core.md +0 -25
  45. package/dist/templates/claude-code-overview-skill.md +0 -6
  46. package/dist/templates/claude-code-skill.md +0 -6
  47. package/dist/templates/claude-code-test-skill.md +0 -6
  48. package/dist/templates/codex-agents.md +0 -3
  49. package/dist/templates/codex-overview-agents.md +0 -3
  50. package/dist/templates/codex-test-agents.md +0 -3
  51. package/dist/templates/cursor-overview-rule.mdc +0 -5
  52. package/dist/templates/cursor-rule.mdc +0 -5
  53. package/dist/templates/cursor-test-rule.mdc +0 -5
  54. package/dist/templates/overview-core.md +0 -27
  55. package/dist/templates/test-writing-core.md +0 -72
  56. package/dist/utils/detect-existing-project.d.ts +0 -5
  57. package/dist/utils/detect-existing-project.js +0 -34
@@ -1,74 +1,81 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { promptConfirm } from '../utils/prompts.js';
1
+ import { promptConfirm, promptChoice } from '../utils/prompts.js';
4
2
  import { pullCommand } from './pull.js';
5
- import { hasValidTokens, getTokens } from '../utils/token-storage.js';
6
3
  import { executeOAuthFlow } from '../utils/oauth-flow.js';
7
4
  import { ConvexHttpClient } from 'convex/browser';
8
5
  import { getConvexUrl } from '../config.js';
9
6
  import { brand } from '../utils/brand.js';
10
- import { selectTeam, selectProject, getOrCreateProjectSecret } from '../utils/project-selector.js';
7
+ import { selectTeam, selectProject, getOrCreateProjectSecret, promptExpiryDays } from '../utils/project-selector.js';
8
+ import { findProjectRoot, readProjectSettings, writeProjectSettings, } from '../utils/project-settings.js';
11
9
  /**
12
- * Link command - connect local environment to an existing project.
10
+ * Link command - connect or reconnect local project to cloud.
13
11
  *
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
12
+ * Requirements:
13
+ * - LINK-1: Requires existing .requirements/ folder
14
+ * - LINK-2: Offers refresh when credentials exist
15
+ * - LINK-3: Uses inline OAuth (no persistent tokens)
16
+ * - LINK-4, LINK-5: Team and project selection
17
+ * - LINK-6: Saves credentials to project-settings.json
18
+ * - LINK-7: Optionally pulls requirements
19
+ * - LINK-8: Handles network errors gracefully
23
20
  */
24
21
  export async function linkCommand() {
25
22
  try {
26
- console.log(`Linking to existing ${brand} project...\n`);
23
+ console.log(`Linking to ${brand} cloud...\n`);
27
24
  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');
25
+ // LINK-1: Check if .requirements/ folder exists
26
+ const projectRoot = findProjectRoot(cwd);
27
+ if (!projectRoot || projectRoot !== cwd) {
28
+ console.log('No .requirements/ folder found.');
29
+ console.log('Run "dotrequirements init" to initialize a project first.\n');
30
+ return;
31
+ }
32
+ // LINK-2: Check for existing credentials and offer refresh
33
+ let existingCredentials;
34
+ try {
35
+ existingCredentials = readProjectSettings(projectRoot);
36
+ }
37
+ catch {
38
+ // Invalid settings - treat as no credentials
39
+ existingCredentials = undefined;
40
+ }
41
+ if (existingCredentials) {
42
+ const shouldContinue = await handleExistingCredentials(existingCredentials);
43
+ if (!shouldContinue) {
46
44
  return;
47
45
  }
48
46
  }
49
- // 2. Set up authenticated client
50
- const tokens = await getTokens();
51
- if (!tokens) {
52
- throw new Error('Authentication tokens not found');
47
+ // LINK-3: Inline OAuth (no persistent tokens)
48
+ let oauthResult;
49
+ try {
50
+ oauthResult = await executeOAuthFlow();
51
+ console.log();
52
+ }
53
+ catch (error) {
54
+ console.error('\nAuthentication failed:', error instanceof Error ? error.message : String(error));
55
+ console.log('Please try again later.\n');
56
+ return;
53
57
  }
58
+ // Set up authenticated client using the session token
54
59
  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
+ client.setAuth(oauthResult.accessToken);
61
+ // LINK-4: Select team
62
+ const selectedTeam = await selectTeam(client);
63
+ if (!selectedTeam) {
64
+ console.log('Link cancelled.');
60
65
  return;
61
66
  }
62
- // 4. Select project
63
- const selectedProject = await selectProject(client, selectedTeamId);
67
+ // LINK-5: Select project
68
+ const selectedProject = await selectProject(client, selectedTeam.teamId);
64
69
  if (!selectedProject) {
65
- console.log('Aborted.');
70
+ // selectProject already shows message about no projects
66
71
  return;
67
72
  }
68
- // 5. Get or create secret for this project
73
+ // AUTHZ-3: Prompt for secret expiry (paid tier only)
74
+ const expiryDays = await promptExpiryDays(selectedTeam.tier);
75
+ // LINK-5.3: Get or create secret for this project
69
76
  console.log('\nGetting project credentials...');
70
- const secretResult = await getOrCreateProjectSecret(client, selectedProject.projectId);
71
- const projectSlug = selectedProject.projectSlug || selectedProject.projectId;
77
+ const secretResult = await getOrCreateProjectSecret(client, selectedProject.projectId, expiryDays);
78
+ const projectId = selectedProject.projectSlug || selectedProject.projectId;
72
79
  const secret = secretResult.secret;
73
80
  if (secretResult.created) {
74
81
  console.log('✓ Created new project secret');
@@ -76,23 +83,23 @@ export async function linkCommand() {
76
83
  else {
77
84
  console.log('✓ Retrieved existing project secret');
78
85
  }
79
- // 6. Write .env.local
80
- const shouldContinue = await writeEnvFile(envPath, projectSlug, secret);
81
- if (!shouldContinue) {
82
- return;
83
- }
84
- // 7. Optionally pull requirements
86
+ // LINK-6: Write credentials to project-settings.json
87
+ writeProjectSettings(projectRoot, { projectId, projectSecret: secret });
88
+ console.log('✓ Wrote credentials to .requirements/project-settings.json');
89
+ // LINK-7: Optionally pull requirements
85
90
  const shouldPull = await promptConfirm('Pull requirements from cloud now?', true);
86
91
  if (shouldPull) {
87
92
  console.log('\nPulling requirements...');
88
- await pullCommand({ project: projectSlug });
93
+ await pullCommand({ project: projectId });
89
94
  }
95
+ // LINK-7.2: Success message
90
96
  console.log(`\n✓ Successfully linked to project: ${selectedProject.projectName}`);
91
- console.log(` Project ID: ${projectSlug}`);
97
+ console.log(` Project ID: ${projectId}`);
92
98
  console.log('\nDocumentation: https://docs.dotrequirements.io/getting-started');
93
99
  }
94
100
  catch (error) {
95
101
  console.error(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`);
102
+ // LINK-8: Network error guidance
96
103
  if (error instanceof Error && error.message.includes('fetch')) {
97
104
  console.error('\nCheck your internet connection and try again.');
98
105
  console.error('If the problem persists, visit https://status.convex.dev');
@@ -101,56 +108,24 @@ export async function linkCommand() {
101
108
  }
102
109
  }
103
110
  /**
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
111
+ * LINK-2: Handle existing credentials - offer refresh or connect to different project
112
+ * Returns true if user wants to continue (either refresh or different project)
113
+ * Returns false if user cancels
107
114
  */
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;
115
+ async function handleExistingCredentials(credentials) {
116
+ console.log(`Currently linked to: ${credentials.projectId}\n`);
117
+ const choice = await promptChoice('What would you like to do?', [
118
+ { title: 'Refresh credentials for this project', value: 'refresh' },
119
+ { title: 'Connect to a different project', value: 'different' },
120
+ ]);
121
+ if (!choice) {
122
+ console.log('Link cancelled.');
123
+ return false;
154
124
  }
125
+ // Both options continue with OAuth flow
126
+ // For 'refresh', we'll match on the same project after team/project selection
127
+ // For 'different', user selects any project
128
+ // The flow is the same - user authenticates and selects team/project
129
+ return true;
155
130
  }
156
131
  //# sourceMappingURL=link.js.map
@@ -1,98 +1,71 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
3
  import { homedir } from 'os';
4
- import { join, dirname } from 'path';
4
+ import { join } from 'path';
5
5
  import prompts from 'prompts';
6
- import { findUp } from 'find-up';
7
- import { loadPlatformTemplate, loadTestWritingTemplate, loadOverviewTemplate } from '../utils/templates.js';
6
+ import { loadTemplate } from '../utils/templates.js';
8
7
  import { brand } from '../utils/brand.js';
8
+ import { getContextFileName, getContextFilePath, findDotrequirementsSection, appendOrUpdateSection, } from '../utils/context-file.js';
9
9
  /**
10
- * Platform-specific skills directory paths (relative to git root)
10
+ * Load the context file section template
11
11
  */
12
- const PLATFORM_SKILLS_DIRS = {
13
- 'claude-code': '.claude/skills',
14
- 'cursor': '.cursor/skills',
15
- 'antigravity': '.agent/skills',
16
- 'codex': '.codex/skills',
17
- 'github-copilot': '.github/skills',
18
- };
19
- /**
20
- * Load skill content by name
21
- */
22
- function loadSkillContent(skillName) {
23
- switch (skillName) {
24
- case 'dotreq-requirements':
25
- return loadPlatformTemplate('claude-code-skill.md');
26
- case 'dotreq-tests':
27
- return loadTestWritingTemplate('claude-code-test-skill.md');
28
- case 'dotreq-overview':
29
- return loadOverviewTemplate('claude-code-overview-skill.md');
30
- }
12
+ function loadContextFileSection() {
13
+ return loadTemplate('context-file-section.md');
31
14
  }
32
15
  /**
33
- * Install a single skill to the specified base directory
16
+ * Install workflow guidance to the appropriate context file for a platform
34
17
  */
35
- async function installSkill(skillName, baseDir, gitRoot) {
36
- const content = loadSkillContent(skillName);
37
- const skillDir = join(gitRoot, baseDir, skillName);
38
- const skillPath = join(skillDir, 'SKILL.md');
39
- if (!existsSync(skillDir)) {
40
- mkdirSync(skillDir, { recursive: true });
18
+ async function installContextFileSection(platform) {
19
+ const contextFilePath = await getContextFilePath(platform);
20
+ if (!contextFilePath) {
21
+ console.error(`\n❌ Could not determine context file path for ${platform}`);
22
+ return;
41
23
  }
42
- writeFileSync(skillPath, content, 'utf-8');
43
- console.log(` Created: ${skillPath}`);
44
- }
45
- /**
46
- * Prompt user to install workflow guidance
47
- */
48
- async function promptForGuidanceInstall(installPath) {
24
+ const fileName = getContextFileName(platform);
25
+ const sectionContent = loadContextFileSection();
26
+ // Check if file exists and has existing section
27
+ let existingSection = null;
28
+ if (existsSync(contextFilePath)) {
29
+ const content = readFileSync(contextFilePath, 'utf-8');
30
+ const section = findDotrequirementsSection(content);
31
+ if (section) {
32
+ existingSection = section.content;
33
+ }
34
+ }
35
+ // Prompt user
36
+ const action = existingSection ? 'Update' : existsSync(contextFilePath) ? 'Add to' : 'Create';
49
37
  const response = await prompts({
50
38
  type: 'confirm',
51
39
  name: 'install',
52
- message: `Install workflow guidance? (recommended)\n → ${installPath}`,
40
+ message: `${action} ${fileName} with dotrequirements workflow guidance?`,
53
41
  initial: true,
54
42
  });
55
- return response.install ?? false;
56
- }
57
- /**
58
- * Install all three skills to a platform's skills directory
59
- */
60
- async function installAllSkills(platform, customPath) {
61
- const gitDir = await findUp('.git', { type: 'directory' });
62
- if (!gitDir) {
63
- console.error('\n❌ Could not find git root directory.');
64
- console.log(' Make sure you are inside a git repository.');
43
+ if (!response.install) {
44
+ console.log('');
65
45
  return;
66
46
  }
67
- const gitRoot = dirname(gitDir);
68
- const baseDir = customPath || PLATFORM_SKILLS_DIRS[platform];
69
- if (!baseDir) {
70
- console.error(`\n❌ Unknown platform: ${platform}`);
71
- return;
72
- }
73
- // Prompt for each skill
74
- const skills = [
75
- { name: 'dotreq-overview', label: 'Overview' },
76
- { name: 'dotreq-requirements', label: 'Requirements capture' },
77
- { name: 'dotreq-tests', label: 'Test writing' },
78
- ];
79
- for (const skill of skills) {
80
- const skillPath = join(gitRoot, baseDir, skill.name, 'SKILL.md');
81
- const shouldInstall = await promptForGuidanceInstall(skillPath);
82
- if (shouldInstall) {
83
- try {
84
- await installSkill(skill.name, baseDir, gitRoot);
85
- console.log(`✅ ${skill.label} guidance installed\n`);
86
- }
87
- catch (error) {
88
- console.error(`❌ Failed to install ${skill.label} guidance`);
89
- console.log(` Error: ${error instanceof Error ? error.message : String(error)}\n`);
90
- }
91
- }
92
- else {
93
- console.log('');
47
+ // If updating, show what will change
48
+ if (existingSection) {
49
+ const confirmUpdate = await prompts({
50
+ type: 'confirm',
51
+ name: 'confirm',
52
+ message: 'Existing dotrequirements section found. Replace it?',
53
+ initial: true,
54
+ });
55
+ if (!confirmUpdate.confirm) {
56
+ console.log(' Skipped.\n');
57
+ return;
94
58
  }
95
59
  }
60
+ try {
61
+ const result = appendOrUpdateSection(contextFilePath, sectionContent);
62
+ const actionVerb = result.action === 'created' ? 'Created' : result.action === 'appended' ? 'Added to' : 'Updated';
63
+ console.log(`✅ ${actionVerb} ${contextFilePath}\n`);
64
+ }
65
+ catch (error) {
66
+ console.error(`❌ Failed to update ${fileName}`);
67
+ console.log(` Error: ${error instanceof Error ? error.message : String(error)}\n`);
68
+ }
96
69
  }
97
70
  /**
98
71
  * MCP setup command - configures the MCP server for AI assistants
@@ -175,7 +148,7 @@ async function setupClaudeCode() {
175
148
  if (mcpConfigured) {
176
149
  console.log('\n⚠️ Important: Restart Claude Code for changes to take effect.');
177
150
  console.log('\nYou can now ask your AI assistant to help write requirements!\n');
178
- await installAllSkills('claude-code');
151
+ await installContextFileSection('claude-code');
179
152
  }
180
153
  }
181
154
  async function setupClaudeDesktop() {
@@ -285,7 +258,7 @@ async function setupCursor() {
285
258
  console.log(` Config file: ${configPath}\n`);
286
259
  console.log('⚠️ Important: Reload or restart Cursor for changes to take effect.');
287
260
  console.log('\nYou can now ask your AI assistant to help write requirements!\n');
288
- await installAllSkills('cursor');
261
+ await installContextFileSection('cursor');
289
262
  }
290
263
  catch (error) {
291
264
  console.error('❌ Failed to configure MCP automatically.');
@@ -379,7 +352,7 @@ async function setupAntigravity() {
379
352
  else {
380
353
  console.log('');
381
354
  }
382
- await installAllSkills('antigravity');
355
+ await installContextFileSection('antigravity');
383
356
  }
384
357
  catch (error) {
385
358
  console.error('❌ Failed to configure MCP automatically.');
@@ -396,7 +369,7 @@ async function setupCodex() {
396
369
  console.log('\n✅ MCP configured successfully for Codex!');
397
370
  console.log('\n⚠️ Important: Restart Codex for changes to take effect.');
398
371
  console.log('\nYou can now ask your AI assistant to help write requirements!\n');
399
- await installAllSkills('codex');
372
+ await installContextFileSection('codex');
400
373
  }
401
374
  catch (error) {
402
375
  console.error('\n❌ Failed to configure MCP automatically.');
@@ -413,24 +386,27 @@ async function setupGitHubCopilot() {
413
386
  console.log('ℹ️ GitHub Copilot discovers MCP servers from your IDE configuration.');
414
387
  console.log(' Please configure the MCP server in VS Code settings or GitHub settings.\n');
415
388
  console.log(' MCP server command: npx -y @popoverai/dotrequirements mcp\n');
416
- await installAllSkills('github-copilot');
389
+ await installContextFileSection('github-copilot');
417
390
  }
418
391
  async function setupOther() {
419
- console.log('\n📦 Custom Agent Skills Setup\n');
420
- console.log('ℹ️ Agent Skills are installed as SKILL.md files in a skills directory.');
421
- console.log(' Common locations:');
422
- console.log(' - .claude/skills/ (Claude Code)');
423
- console.log(' - .cursor/skills/ (Cursor)');
424
- console.log(' - .agent/skills/ (Antigravity)');
425
- console.log(' - .codex/skills/ (Codex)');
426
- console.log(' - .github/skills/ (GitHub Copilot)\n');
392
+ console.log('\n📦 Custom Context File Setup\n');
393
+ console.log('ℹ️ Workflow guidance is added to a context file at your project root.');
394
+ console.log(' Common file names by platform:');
395
+ console.log(' - CLAUDE.md (Claude Code)');
396
+ console.log(' - AGENTS.md (Cursor, Codex, GitHub Copilot)');
397
+ console.log(' - GEMINI.md (Antigravity)\n');
427
398
  const response = await prompts({
428
- type: 'text',
429
- name: 'path',
430
- message: 'Enter the skills directory path (relative to git root):',
431
- initial: '.skills',
399
+ type: 'select',
400
+ name: 'file',
401
+ message: 'Which context file should we update?',
402
+ choices: [
403
+ { title: 'AGENTS.md (most compatible)', value: 'AGENTS.md' },
404
+ { title: 'CLAUDE.md', value: 'CLAUDE.md' },
405
+ { title: 'GEMINI.md', value: 'GEMINI.md' },
406
+ ],
407
+ initial: 0,
432
408
  });
433
- if (!response.path) {
409
+ if (!response.file) {
434
410
  console.log('\nSetup cancelled.');
435
411
  return;
436
412
  }
@@ -438,6 +414,13 @@ async function setupOther() {
438
414
  console.log('Configure your AI assistant to run the MCP server:');
439
415
  console.log(' Command: npx');
440
416
  console.log(' Args: -y @popoverai/dotrequirements mcp\n');
441
- await installAllSkills('other', response.path);
417
+ // Determine platform based on file choice for the install function
418
+ const platformMap = {
419
+ 'CLAUDE.md': 'claude-code',
420
+ 'AGENTS.md': 'cursor', // Any AGENTS.md platform works
421
+ 'GEMINI.md': 'antigravity',
422
+ };
423
+ const platform = platformMap[response.file];
424
+ await installContextFileSection(platform);
442
425
  }
443
426
  //# sourceMappingURL=mcp-setup.js.map
@@ -1,6 +1,7 @@
1
1
  interface PullOptions {
2
2
  project?: string;
3
3
  document?: string;
4
+ share?: string;
4
5
  }
5
6
  export declare function pullCommand(options: PullOptions): Promise<void>;
6
7
  export {};
@@ -4,64 +4,80 @@ import { ConvexHttpClient } from 'convex/browser';
4
4
  import { getConvexUrl } from '../config.js';
5
5
  import { buildRequirementsFile } from '../schema/index.js';
6
6
  import { api } from '../convex.js';
7
- import { isLocalOnlyProject, CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE } from '../utils/local-project.js';
8
7
  import { brand } from '../utils/brand.js';
9
8
  import { findRequirementsFiles } from '../mcp/requirements.js';
9
+ import { getProjectCredentials } from '../utils/project-settings.js';
10
10
  export async function pullCommand(options) {
11
11
  try {
12
- console.log(`Syncing requirements from ${brand} cloud...`);
13
12
  // Connect to Convex
14
13
  const convexUrl = getConvexUrl();
15
14
  const client = new ConvexHttpClient(convexUrl);
16
- // Determine project ID
17
- let projectId = options.project;
18
- // If no project flag provided, try to read from .env.local
19
- if (!projectId && !options.document) {
20
- projectId = loadProjectIdFromEnv();
21
- if (!projectId) {
22
- throw new Error('Project ID not found. Either:\n' +
23
- ' 1. Run "dotreq init" to initialize the project, or\n' +
24
- ' 2. Use --project flag to specify the project ID');
15
+ let documents;
16
+ let projectName;
17
+ let isShareTokenFlow = false;
18
+ // SHARE-TOKEN-CLI-1: Handle share token flow (no project config needed)
19
+ if (options.share) {
20
+ isShareTokenFlow = true;
21
+ console.log(`Pulling requirements with share token...`);
22
+ try {
23
+ // SHARE-TOKEN-CLI-1.1: Pass only projectSecret, no projectSlug
24
+ // The authzQueryReadOnly wrapper will resolve project from the token
25
+ const result = await client.query(api.documents.queries.exportProjectForCli, {
26
+ projectAuth: {
27
+ projectSecret: options.share,
28
+ },
29
+ });
30
+ documents = result.documents;
31
+ projectName = result.projectName;
32
+ // SHARE-TOKEN-CLI-1.2: Display project name in output
33
+ console.log(`\nProject: "${projectName}"`);
25
34
  }
26
- // AUTHZ-2.0: Check if project is local-only
27
- if (isLocalOnlyProject(projectId)) {
28
- throw new Error(CLOUD_FEATURES_REQUIRE_AUTH_MESSAGE);
35
+ catch (error) {
36
+ // SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully
37
+ const errorMessage = error instanceof Error ? error.message : String(error);
38
+ if (errorMessage.includes('Invalid share token')) {
39
+ throw new Error('Invalid share token. The token may be incorrect or has been revoked.\n' +
40
+ 'Please request a new share token from your team.');
41
+ }
42
+ throw error;
29
43
  }
30
44
  }
31
- // Load project secret for authentication
32
- const projectSecret = process.env.DOTREQUIREMENTS_PROJECT_SECRET;
33
- if (!projectSecret) {
34
- throw new Error('DOTREQUIREMENTS_PROJECT_SECRET not found. Either:\n' +
35
- ' 1. Run "dotrequirements init" or "dotrequirements login" to set up authentication, or\n' +
36
- ' 2. Set DOTREQUIREMENTS_PROJECT_SECRET in .env.local');
37
- }
38
- // Fetch documents
39
- let documents;
40
- if (options.document) {
41
- // Fetch specific document
42
- const doc = await client.query(api.documents.queries.exportForCli, {
43
- projectAuth: {
44
- projectSlug: projectId,
45
- projectSecret,
46
- },
47
- target: { type: 'document', id: options.document },
48
- documentId: options.document,
49
- });
50
- documents = doc ? [doc] : [];
51
- }
52
- else if (projectId) {
53
- // Fetch all documents for project
54
- // SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
55
- documents = await client.query(api.documents.queries.exportProjectForCli, {
56
- projectAuth: {
57
- projectSlug: projectId,
58
- projectSecret,
59
- },
60
- target: { type: 'project', slug: projectId },
61
- });
62
- }
63
45
  else {
64
- throw new Error('Either --project or --document must be specified');
46
+ console.log(`Syncing requirements from ${brand} cloud...`);
47
+ // Get project credentials from .requirements/project-settings.json
48
+ // This throws helpful errors if project not found or not connected to cloud
49
+ const credentials = getProjectCredentials();
50
+ // Allow --project flag to override, but still need secret from settings
51
+ const projectId = options.project ?? credentials.projectId;
52
+ const projectSecret = credentials.projectSecret;
53
+ // Fetch documents
54
+ if (options.document) {
55
+ // Fetch specific document
56
+ const doc = await client.query(api.documents.queries.exportForCli, {
57
+ projectAuth: {
58
+ projectSlug: projectId,
59
+ projectSecret,
60
+ },
61
+ target: { type: 'document', id: options.document },
62
+ documentId: options.document,
63
+ });
64
+ documents = doc ? [doc] : [];
65
+ }
66
+ else if (projectId) {
67
+ // Fetch all documents for project
68
+ // SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
69
+ const result = await client.query(api.documents.queries.exportProjectForCli, {
70
+ projectAuth: {
71
+ projectSlug: projectId,
72
+ projectSecret,
73
+ },
74
+ });
75
+ documents = result.documents;
76
+ projectName = result.projectName;
77
+ }
78
+ else {
79
+ throw new Error('Either --project or --document must be specified');
80
+ }
65
81
  }
66
82
  if (documents.length === 0) {
67
83
  console.log('No documents found.');
@@ -98,6 +114,13 @@ export async function pullCommand(options) {
98
114
  console.log(`✓ Synced: ${fileName} (version ${doc.version})`);
99
115
  }
100
116
  console.log(`\nSuccessfully synced ${documents.length} document(s) to .requirements/`);
117
+ // SHARE-TOKEN-CLI-3: Show next steps for share token users
118
+ if (isShareTokenFlow) {
119
+ console.log('\n─────────────────────────────────────────────────────────');
120
+ console.log('Next steps to get full access (push, coverage):');
121
+ console.log(' Run: dotrequirements link');
122
+ console.log('─────────────────────────────────────────────────────────');
123
+ }
101
124
  }
102
125
  catch (error) {
103
126
  // Re-throw to let the CLI error handler display it cleanly
@@ -113,12 +136,6 @@ function sanitizeFileName(title) {
113
136
  .replace(/[^a-z0-9]+/g, '-')
114
137
  .replace(/^-+|-+$/g, '');
115
138
  }
116
- /**
117
- * Load project ID from process.env (already loaded from .env.local by CLI)
118
- */
119
- function loadProjectIdFromEnv() {
120
- return process.env.DOTREQUIREMENTS_PROJECT_ID;
121
- }
122
139
  /**
123
140
  * Build an index mapping document IDs to their file paths.
124
141
  * Scans existing .requirements.md files across the entire workspace and extracts document.id from frontmatter.