@popoverai/dotrequirements 0.14.0 → 0.16.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 +45 -1
- package/dist/cli.js +1 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +70 -31
- package/dist/commands/push.d.ts +0 -5
- package/dist/commands/push.js +87 -204
- package/dist/config.js +0 -4
- package/dist/harness/cache.d.ts +2 -1
- package/dist/harness/cache.js +32 -1
- package/dist/mcp/convexClient.d.ts +11 -0
- package/dist/mcp/convexClient.js +51 -3
- package/dist/mcp/index.js +217 -140
- package/dist/mcp/requirements.d.ts +10 -0
- package/dist/mcp/requirements.js +15 -1
- package/dist/push/core.d.ts +105 -0
- package/dist/push/core.js +216 -0
- package/dist/push/index.d.ts +8 -0
- package/dist/push/index.js +10 -0
- package/dist/utils/project-settings.d.ts +9 -0
- package/dist/utils/project-settings.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,8 +114,19 @@ Sync requirements from dot•requirements cloud to local `.requirements/` files.
|
|
|
114
114
|
dotreq pull
|
|
115
115
|
dotreq pull --project <project-id>
|
|
116
116
|
dotreq pull --document <document-id>
|
|
117
|
+
dotreq pull --share <token>
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
**First-time setup with a share token:**
|
|
121
|
+
|
|
122
|
+
If a team member shares a pull command with you, you can pull requirements without creating an account:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx @popoverai/dotrequirements pull --share drt_abc123...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This gives you read-only access to view requirements. To push changes or report coverage, run `dotreq link` afterward.
|
|
129
|
+
|
|
119
130
|
### `dotreq push`
|
|
120
131
|
|
|
121
132
|
Push local requirements to dot•requirements cloud.
|
|
@@ -421,7 +432,7 @@ Follow the prompts to configure for your AI assistant (Claude Code, Cursor, etc.
|
|
|
421
432
|
**Authoring:**
|
|
422
433
|
- `create_requirement_document` — Get a Markdown template with format examples
|
|
423
434
|
- `validate_requirements` — Validate file schema (works offline)
|
|
424
|
-
- `style_check` — AI-powered style feedback on requirements
|
|
435
|
+
- `style_check` — AI-powered style feedback on requirements (supports optional `requirementKeys` filter)
|
|
425
436
|
- `review_test` — Comprehensive test review (style + semantic correctness)
|
|
426
437
|
|
|
427
438
|
**Cloud:**
|
|
@@ -432,6 +443,39 @@ Follow the prompts to configure for your AI assistant (Claude Code, Cursor, etc.
|
|
|
432
443
|
**Diagnostic:**
|
|
433
444
|
- `debug_mcp_environment` — Debug MCP server configuration
|
|
434
445
|
|
|
446
|
+
### CI/CD Mode
|
|
447
|
+
|
|
448
|
+
For CI/CD environments (GitHub Actions, etc.), use the `--auth-from-env` flag to read credentials from environment variables instead of `project-settings.json`:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
node packages/cli/dist/mcp/index.js --auth-from-env
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Required environment variables:
|
|
455
|
+
- `DOTREQ_PROJECT_ID` — Your project slug
|
|
456
|
+
- `DOTREQ_PROJECT_SECRET` — Your project secret
|
|
457
|
+
|
|
458
|
+
**Important:** Without `--auth-from-env`, these environment variables are ignored. This explicit opt-in prevents credential conflicts between local and CI environments.
|
|
459
|
+
|
|
460
|
+
Example MCP config for GitHub Actions:
|
|
461
|
+
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"mcpServers": {
|
|
465
|
+
"dotrequirements": {
|
|
466
|
+
"command": "node",
|
|
467
|
+
"args": ["packages/cli/dist/mcp/index.js", "--auth-from-env"],
|
|
468
|
+
"env": {
|
|
469
|
+
"DOTREQ_PROJECT_ID": "${DOTREQ_PROJECT_ID}",
|
|
470
|
+
"DOTREQ_PROJECT_SECRET": "${DOTREQ_PROJECT_SECRET}"
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
See the [CI/CD Integration docs](https://dotrequirements.io/tools/ai/ci-cd) for complete examples.
|
|
478
|
+
|
|
435
479
|
---
|
|
436
480
|
|
|
437
481
|
## Package Exports
|
package/dist/cli.js
CHANGED
|
@@ -57,6 +57,7 @@ program
|
|
|
57
57
|
.description('Sync requirements from cloud to local .requirements/ files')
|
|
58
58
|
.option('-p, --project <id>', 'Project ID to sync')
|
|
59
59
|
.option('-d, --document <id>', 'Specific document ID to sync')
|
|
60
|
+
.option('-s, --share <token>', 'Read-only share token for quick onboarding (no setup required)')
|
|
60
61
|
.action(wrapCommand(pullCommand));
|
|
61
62
|
program
|
|
62
63
|
.command('push [file]')
|
package/dist/commands/pull.d.ts
CHANGED
package/dist/commands/pull.js
CHANGED
|
@@ -9,43 +9,75 @@ import { findRequirementsFiles } from '../mcp/requirements.js';
|
|
|
9
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
|
-
// Get project credentials from .requirements/project-settings.json
|
|
17
|
-
// This throws helpful errors if project not found or not connected to cloud
|
|
18
|
-
const credentials = getProjectCredentials();
|
|
19
|
-
// Allow --project flag to override, but still need secret from settings
|
|
20
|
-
const projectId = options.project ?? credentials.projectId;
|
|
21
|
-
const projectSecret = credentials.projectSecret;
|
|
22
|
-
// Fetch documents
|
|
23
15
|
let documents;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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}"`);
|
|
34
|
+
}
|
|
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;
|
|
43
|
+
}
|
|
46
44
|
}
|
|
47
45
|
else {
|
|
48
|
-
|
|
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
|
+
}
|
|
49
81
|
}
|
|
50
82
|
if (documents.length === 0) {
|
|
51
83
|
console.log('No documents found.');
|
|
@@ -82,6 +114,13 @@ export async function pullCommand(options) {
|
|
|
82
114
|
console.log(`✓ Synced: ${fileName} (version ${doc.version})`);
|
|
83
115
|
}
|
|
84
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
|
+
}
|
|
85
124
|
}
|
|
86
125
|
catch (error) {
|
|
87
126
|
// Re-throw to let the CLI error handler display it cleanly
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -2,10 +2,5 @@ interface PushOptions {
|
|
|
2
2
|
yes?: boolean;
|
|
3
3
|
}
|
|
4
4
|
export declare function pushCommand(file: string | undefined, options: PushOptions): Promise<void>;
|
|
5
|
-
/**
|
|
6
|
-
* Extract markdown content from a file, stripping YAML frontmatter.
|
|
7
|
-
* Exported for testing.
|
|
8
|
-
*/
|
|
9
|
-
export declare function extractMarkdownContent(rawContent: string): string;
|
|
10
5
|
export {};
|
|
11
6
|
//# sourceMappingURL=push.d.ts.map
|
package/dist/commands/push.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { ConvexHttpClient } from 'convex/browser';
|
|
4
|
-
import { getConvexUrl } from '../config.js';
|
|
5
|
-
import { parseRequirementsFromFile, getAllRequirements, parseRequirementKey, buildRequirementsFile } from '../schema/index.js';
|
|
6
|
-
import { api } from '../convex.js';
|
|
7
3
|
import * as readline from 'readline';
|
|
8
4
|
import { brand } from '../utils/brand.js';
|
|
9
5
|
import { findRequirementsFiles } from '../mcp/requirements.js';
|
|
10
6
|
import { getProjectCredentials } from '../utils/project-settings.js';
|
|
7
|
+
import { getConvexUrl } from '../config.js';
|
|
8
|
+
import { parseFilesForPush, dryRunPush, executePush, } from '../push/index.js';
|
|
11
9
|
export async function pushCommand(file, options) {
|
|
12
10
|
try {
|
|
13
11
|
console.log(`Preparing to push requirements to ${brand} cloud...\n`);
|
|
@@ -35,164 +33,42 @@ export async function pushCommand(file, options) {
|
|
|
35
33
|
}
|
|
36
34
|
// Parse all local files
|
|
37
35
|
console.log('Parsing local requirements...');
|
|
38
|
-
const parsedFiles =
|
|
39
|
-
|
|
40
|
-
for (const filePath of filesToPush) {
|
|
41
|
-
const fileName = path.basename(filePath);
|
|
42
|
-
console.log(` Reading ${fileName}...`);
|
|
43
|
-
// Read raw file content
|
|
44
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
-
// Parse to get metadata and requirements
|
|
46
|
-
const parsed = parseRequirementsFromFile(filePath);
|
|
47
|
-
const flatRequirements = getAllRequirements(parsed.requirements);
|
|
48
|
-
// Extract markdown content (everything after frontmatter)
|
|
49
|
-
const markdownContent = extractMarkdownContent(rawContent);
|
|
50
|
-
// DOC-HEADER-11.1/11.2: Infer prefix from first requirement if not specified
|
|
51
|
-
const doc = parsed.metadata.document;
|
|
52
|
-
if (doc && !doc.defaultPrefix && flatRequirements.length > 0) {
|
|
53
|
-
const firstReq = flatRequirements[0];
|
|
54
|
-
if (firstReq) {
|
|
55
|
-
const parsedKey = parseRequirementKey(firstReq.id);
|
|
56
|
-
if (parsedKey) {
|
|
57
|
-
doc.defaultPrefix = parsedKey.prefix;
|
|
58
|
-
console.log(` Inferred defaultPrefix "${doc.defaultPrefix}" from first requirement`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
parsedFiles.push({
|
|
63
|
-
filePath,
|
|
64
|
-
metadata: parsed.metadata,
|
|
65
|
-
markdownContent,
|
|
66
|
-
requirementCount: flatRequirements.length,
|
|
67
|
-
});
|
|
68
|
-
totalRequirements += flatRequirements.length;
|
|
69
|
-
}
|
|
70
|
-
console.log(`\nFound ${totalRequirements} requirement(s) in ${parsedFiles.length} document(s).\n`);
|
|
71
|
-
// Connect to Convex
|
|
72
|
-
const convexUrl = getConvexUrl();
|
|
73
|
-
const client = new ConvexHttpClient(convexUrl);
|
|
74
|
-
// Phase 1: Dry run to categorize all files
|
|
75
|
-
console.log('Validating documents...');
|
|
76
|
-
const dryRunResults = [];
|
|
36
|
+
const { parsedFiles, totalRequirements } = parseFilesForPush(filesToPush);
|
|
37
|
+
// Log file parsing progress
|
|
77
38
|
for (const file of parsedFiles) {
|
|
78
|
-
const doc = file.metadata.document;
|
|
79
39
|
const fileName = path.basename(file.filePath);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
result: {
|
|
85
|
-
dryRun: true,
|
|
86
|
-
action: 'invalid',
|
|
87
|
-
title: fileName,
|
|
88
|
-
error: 'Missing document section in frontmatter',
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const result = await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
|
|
95
|
-
projectAuth: {
|
|
96
|
-
projectSlug: projectId,
|
|
97
|
-
projectSecret,
|
|
98
|
-
},
|
|
99
|
-
target: { type: 'project', slug: projectId },
|
|
100
|
-
documentId: doc.id,
|
|
101
|
-
title: doc.title,
|
|
102
|
-
markdownContent: file.markdownContent,
|
|
103
|
-
defaultPrefix: doc.defaultPrefix,
|
|
104
|
-
dryRun: true,
|
|
105
|
-
});
|
|
106
|
-
dryRunResults.push({ file, result });
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
dryRunResults.push({
|
|
110
|
-
file,
|
|
111
|
-
result: {
|
|
112
|
-
dryRun: true,
|
|
113
|
-
action: 'invalid',
|
|
114
|
-
title: doc.title,
|
|
115
|
-
error: err.message,
|
|
116
|
-
},
|
|
117
|
-
});
|
|
40
|
+
const doc = file.metadata.document;
|
|
41
|
+
if (doc?.defaultPrefix && !file.metadata.document?.defaultPrefix) {
|
|
42
|
+
console.log(` Reading ${fileName}...`);
|
|
43
|
+
console.log(` Inferred defaultPrefix "${doc.defaultPrefix}" from first requirement`);
|
|
118
44
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const updates = dryRunResults.filter(r => r.result.action === 'update');
|
|
122
|
-
const creates = dryRunResults.filter(r => r.result.action === 'create');
|
|
123
|
-
const notFound = dryRunResults.filter(r => r.result.action === 'not_found');
|
|
124
|
-
const invalid = dryRunResults.filter(r => r.result.action === 'invalid');
|
|
125
|
-
// Check for conflicts on updates (cloud changed since last pull)
|
|
126
|
-
const conflicts = [];
|
|
127
|
-
if (updates.length > 0) {
|
|
128
|
-
const docIds = updates.map(u => u.result.documentId);
|
|
129
|
-
const cloudMetadata = await client.query(api.documents.queries.getDocumentsMetadata, {
|
|
130
|
-
projectAuth: {
|
|
131
|
-
projectSlug: projectId,
|
|
132
|
-
projectSecret,
|
|
133
|
-
},
|
|
134
|
-
target: { type: 'project', slug: projectId },
|
|
135
|
-
documentIds: docIds,
|
|
136
|
-
});
|
|
137
|
-
const cloudMetaByDocId = new Map(cloudMetadata.map((m) => [m.documentId, m]));
|
|
138
|
-
for (const item of updates) {
|
|
139
|
-
const docId = item.result.documentId;
|
|
140
|
-
const cloudMeta = cloudMetaByDocId.get(docId);
|
|
141
|
-
if (cloudMeta && item.file.metadata.pulledAt) {
|
|
142
|
-
const pulledAtMs = new Date(item.file.metadata.pulledAt).getTime();
|
|
143
|
-
if (cloudMeta.updatedAt > pulledAtMs) {
|
|
144
|
-
conflicts.push({ item, cloudMeta });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(` Reading ${fileName}...`);
|
|
147
47
|
}
|
|
148
48
|
}
|
|
49
|
+
console.log(`\nFound ${totalRequirements} requirement(s) in ${parsedFiles.length} document(s).\n`);
|
|
50
|
+
// Build credentials
|
|
51
|
+
const credentials = {
|
|
52
|
+
projectId,
|
|
53
|
+
projectSecret,
|
|
54
|
+
convexUrl: getConvexUrl(),
|
|
55
|
+
};
|
|
56
|
+
// Phase 1: Dry run
|
|
57
|
+
console.log('Validating documents...');
|
|
58
|
+
const dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
149
59
|
// Display unified summary
|
|
150
|
-
|
|
151
|
-
if (updates.length > 0) {
|
|
152
|
-
console.log(`Updated documents (${updates.length}):`);
|
|
153
|
-
for (const { file, result } of updates) {
|
|
154
|
-
const fileName = path.basename(file.filePath);
|
|
155
|
-
const hasConflict = conflicts.some(c => c.item.file === file);
|
|
156
|
-
const conflictNote = hasConflict ? ' ⚠️ [cloud changed since pull]' : '';
|
|
157
|
-
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
158
|
-
console.log(` ~ ${fileName}${conflictNote}${warningNote}`);
|
|
159
|
-
}
|
|
160
|
-
console.log();
|
|
161
|
-
}
|
|
162
|
-
if (creates.length > 0) {
|
|
163
|
-
console.log(`New documents (${creates.length}):`);
|
|
164
|
-
for (const { file, result } of creates) {
|
|
165
|
-
const fileName = path.basename(file.filePath);
|
|
166
|
-
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
167
|
-
console.log(` + ${fileName}${warningNote}`);
|
|
168
|
-
}
|
|
169
|
-
console.log();
|
|
170
|
-
}
|
|
171
|
-
if (notFound.length > 0) {
|
|
172
|
-
console.log(`Documents not found in cloud (${notFound.length}) - will create new:`);
|
|
173
|
-
for (const { file, result } of notFound) {
|
|
174
|
-
const fileName = path.basename(file.filePath);
|
|
175
|
-
console.log(` ! ${fileName} (ID: ${result.documentId})`);
|
|
176
|
-
}
|
|
177
|
-
console.log();
|
|
178
|
-
}
|
|
179
|
-
if (invalid.length > 0) {
|
|
180
|
-
console.log(`Skipped - invalid files (${invalid.length}):`);
|
|
181
|
-
for (const { file, result } of invalid) {
|
|
182
|
-
const fileName = path.basename(file.filePath);
|
|
183
|
-
console.log(` ✗ ${fileName}: ${result.error}`);
|
|
184
|
-
}
|
|
185
|
-
console.log();
|
|
186
|
-
}
|
|
60
|
+
displayDryRunSummary(dryRunResult);
|
|
187
61
|
// Check if there's anything to push
|
|
188
|
-
const
|
|
189
|
-
|
|
62
|
+
const pushableCount = dryRunResult.updates.length +
|
|
63
|
+
dryRunResult.creates.length +
|
|
64
|
+
dryRunResult.notFound.length;
|
|
65
|
+
if (pushableCount === 0) {
|
|
190
66
|
console.log('No valid documents to push.');
|
|
191
67
|
return;
|
|
192
68
|
}
|
|
193
69
|
// Single confirmation prompt
|
|
194
70
|
if (!options.yes) {
|
|
195
|
-
const hasConflicts = conflicts.length > 0;
|
|
71
|
+
const hasConflicts = dryRunResult.conflicts.length > 0;
|
|
196
72
|
const prompt = hasConflicts
|
|
197
73
|
? 'Continue with push? (will overwrite cloud changes) [y/N] '
|
|
198
74
|
: 'Continue with push? [y/N] ';
|
|
@@ -204,59 +80,35 @@ export async function pushCommand(file, options) {
|
|
|
204
80
|
}
|
|
205
81
|
// Phase 2: Execute push
|
|
206
82
|
console.log('\nPushing documents to cloud...');
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const { file, result } of
|
|
210
|
-
|
|
83
|
+
const result = await executePush(dryRunResult, credentials);
|
|
84
|
+
// Display results
|
|
85
|
+
for (const { file, result: dryResult } of [
|
|
86
|
+
...dryRunResult.updates,
|
|
87
|
+
...dryRunResult.creates,
|
|
88
|
+
...dryRunResult.notFound,
|
|
89
|
+
]) {
|
|
211
90
|
const fileName = path.basename(file.filePath);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
projectSlug: projectId,
|
|
218
|
-
projectSecret,
|
|
219
|
-
},
|
|
220
|
-
target: { type: 'project', slug: projectId },
|
|
221
|
-
documentId: effectiveDocId,
|
|
222
|
-
title: doc.title,
|
|
223
|
-
markdownContent: file.markdownContent,
|
|
224
|
-
defaultPrefix: doc.defaultPrefix,
|
|
225
|
-
dryRun: false,
|
|
226
|
-
});
|
|
227
|
-
const isCreate = result.action === 'create' || result.action === 'not_found';
|
|
228
|
-
if (isCreate) {
|
|
229
|
-
// New document - write ID back to file
|
|
230
|
-
doc.id = pushResult;
|
|
231
|
-
file.metadata.pulledAt = new Date().toISOString();
|
|
232
|
-
file.metadata.version = 1;
|
|
233
|
-
const updatedContent = buildRequirementsFile(file.metadata, file.markdownContent);
|
|
234
|
-
fs.writeFileSync(file.filePath, updatedContent, 'utf-8');
|
|
235
|
-
console.log(` ✓ Created: ${fileName} → ${pushResult}`);
|
|
236
|
-
created++;
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
// Update pulledAt to reflect this push
|
|
240
|
-
file.metadata.pulledAt = new Date().toISOString();
|
|
241
|
-
file.metadata.version = (file.metadata.version || 0) + 1;
|
|
242
|
-
const updatedContent = buildRequirementsFile(file.metadata, file.markdownContent);
|
|
243
|
-
fs.writeFileSync(file.filePath, updatedContent, 'utf-8');
|
|
244
|
-
console.log(` ✓ Updated: ${fileName}`);
|
|
245
|
-
updated++;
|
|
246
|
-
}
|
|
91
|
+
const isCreate = dryResult.action === 'create' || dryResult.action === 'not_found';
|
|
92
|
+
// Check if this file had an error
|
|
93
|
+
const error = result.errors.find((e) => e.fileName === fileName);
|
|
94
|
+
if (error) {
|
|
95
|
+
console.log(` ✗ Failed: ${fileName} - ${error.error}`);
|
|
247
96
|
}
|
|
248
|
-
|
|
249
|
-
console.
|
|
97
|
+
else if (isCreate) {
|
|
98
|
+
console.log(` ✓ Created: ${fileName} → ${file.metadata.document?.id}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(` ✓ Updated: ${fileName}`);
|
|
250
102
|
}
|
|
251
103
|
}
|
|
252
104
|
console.log(`\n✓ Push complete!`);
|
|
253
|
-
if (created > 0) {
|
|
254
|
-
console.log(` Created: ${created} document(s)`);
|
|
105
|
+
if (result.created > 0) {
|
|
106
|
+
console.log(` Created: ${result.created} document(s)`);
|
|
255
107
|
}
|
|
256
|
-
if (updated > 0) {
|
|
257
|
-
console.log(` Updated: ${updated} document(s)`);
|
|
108
|
+
if (result.updated > 0) {
|
|
109
|
+
console.log(` Updated: ${result.updated} document(s)`);
|
|
258
110
|
}
|
|
259
|
-
if (created === 0 && updated === 0) {
|
|
111
|
+
if (result.created === 0 && result.updated === 0) {
|
|
260
112
|
console.log(' No documents were pushed.');
|
|
261
113
|
}
|
|
262
114
|
}
|
|
@@ -266,16 +118,47 @@ export async function pushCommand(file, options) {
|
|
|
266
118
|
}
|
|
267
119
|
}
|
|
268
120
|
/**
|
|
269
|
-
*
|
|
270
|
-
* Exported for testing.
|
|
121
|
+
* Display the dry run summary.
|
|
271
122
|
*/
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
123
|
+
function displayDryRunSummary(dryRunResult) {
|
|
124
|
+
const { updates, creates, notFound, invalid, conflicts } = dryRunResult;
|
|
125
|
+
console.log('\n=== Push Summary ===\n');
|
|
126
|
+
if (updates.length > 0) {
|
|
127
|
+
console.log(`Updated documents (${updates.length}):`);
|
|
128
|
+
for (const { file, result } of updates) {
|
|
129
|
+
const fileName = path.basename(file.filePath);
|
|
130
|
+
const hasConflict = conflicts.some((c) => c.item.file === file);
|
|
131
|
+
const conflictNote = hasConflict ? ' ⚠️ [cloud changed since pull]' : '';
|
|
132
|
+
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
133
|
+
console.log(` ~ ${fileName}${conflictNote}${warningNote}`);
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
if (creates.length > 0) {
|
|
138
|
+
console.log(`New documents (${creates.length}):`);
|
|
139
|
+
for (const { file, result } of creates) {
|
|
140
|
+
const fileName = path.basename(file.filePath);
|
|
141
|
+
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
142
|
+
console.log(` + ${fileName}${warningNote}`);
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
if (notFound.length > 0) {
|
|
147
|
+
console.log(`Documents not found in cloud (${notFound.length}) - will create new:`);
|
|
148
|
+
for (const { file, result } of notFound) {
|
|
149
|
+
const fileName = path.basename(file.filePath);
|
|
150
|
+
console.log(` ! ${fileName} (ID: ${result.documentId})`);
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
154
|
+
if (invalid.length > 0) {
|
|
155
|
+
console.log(`Skipped - invalid files (${invalid.length}):`);
|
|
156
|
+
for (const { file, result } of invalid) {
|
|
157
|
+
const fileName = path.basename(file.filePath);
|
|
158
|
+
console.log(` ✗ ${fileName}: ${result.error}`);
|
|
159
|
+
}
|
|
160
|
+
console.log();
|
|
277
161
|
}
|
|
278
|
-
return rawContent;
|
|
279
162
|
}
|
|
280
163
|
/**
|
|
281
164
|
* Prompt user for confirmation
|
package/dist/config.js
CHANGED
|
@@ -7,10 +7,6 @@ const CONVEX_URL = 'https://data.dotrequirements.io';
|
|
|
7
7
|
* Get the Convex deployment URL
|
|
8
8
|
*/
|
|
9
9
|
export function getConvexUrl() {
|
|
10
|
-
// Show deprecation warning if user has CONVEX_URL in their env
|
|
11
|
-
if (process.env.CONVEX_URL) {
|
|
12
|
-
console.warn('⚠️ CONVEX_URL is no longer needed and will be ignored. You can safely remove it from your .env.local');
|
|
13
|
-
}
|
|
14
10
|
return CONVEX_URL;
|
|
15
11
|
}
|
|
16
12
|
//# sourceMappingURL=config.js.map
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -70,7 +70,8 @@ export declare function findRequirementsFiles(projectRoot: string): string[];
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function writeLookupCache(requirementsDir: string, requirements: RequirementNode[]): void;
|
|
72
72
|
/**
|
|
73
|
-
* Read the lookup cache, returning null if not found or
|
|
73
|
+
* Read the lookup cache, returning null if not found, invalid, or stale.
|
|
74
|
+
* Cache is considered stale if any .requirements.md file is newer than the cache.
|
|
74
75
|
*/
|
|
75
76
|
export declare function readLookupCache(requirementsDir: string): LookupCache | null;
|
|
76
77
|
/**
|
package/dist/harness/cache.js
CHANGED
|
@@ -101,7 +101,28 @@ export function writeLookupCache(requirementsDir, requirements) {
|
|
|
101
101
|
fs.writeFileSync(lookupPath, JSON.stringify(lookup, null, 2));
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Check if any source file is newer than the cache file.
|
|
105
|
+
* Returns true if cache is stale and should be invalidated.
|
|
106
|
+
*/
|
|
107
|
+
function isCacheStale(requirementsDir, cacheMtime) {
|
|
108
|
+
const projectRoot = path.dirname(requirementsDir);
|
|
109
|
+
const sourceFiles = findRequirementsFiles(projectRoot);
|
|
110
|
+
for (const file of sourceFiles) {
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.statSync(file);
|
|
113
|
+
if (stat.mtimeMs > cacheMtime) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// File might have been deleted, ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read the lookup cache, returning null if not found, invalid, or stale.
|
|
125
|
+
* Cache is considered stale if any .requirements.md file is newer than the cache.
|
|
105
126
|
*/
|
|
106
127
|
export function readLookupCache(requirementsDir) {
|
|
107
128
|
const cacheDir = getCacheDir(requirementsDir);
|
|
@@ -109,6 +130,16 @@ export function readLookupCache(requirementsDir) {
|
|
|
109
130
|
if (!fs.existsSync(lookupPath)) {
|
|
110
131
|
return null;
|
|
111
132
|
}
|
|
133
|
+
// Check if cache is stale (any source file newer than cache)
|
|
134
|
+
try {
|
|
135
|
+
const cacheStat = fs.statSync(lookupPath);
|
|
136
|
+
if (isCacheStale(requirementsDir, cacheStat.mtimeMs)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
112
143
|
try {
|
|
113
144
|
const content = fs.readFileSync(lookupPath, 'utf-8');
|
|
114
145
|
return JSON.parse(content);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production Convex deployment URL
|
|
3
|
+
*/
|
|
4
|
+
export declare const CONVEX_URL = "https://data.dotrequirements.io";
|
|
1
5
|
/**
|
|
2
6
|
* Convex configuration for cloud operations
|
|
3
7
|
*/
|
|
@@ -43,5 +47,12 @@ export declare function getProjectCoverage(projectId: string, projectSecret: str
|
|
|
43
47
|
}>;
|
|
44
48
|
untested: string[];
|
|
45
49
|
}>;
|
|
50
|
+
/**
|
|
51
|
+
* PROJ-CONTEXT-7: Query Convex for project context (AI style guidelines)
|
|
52
|
+
*/
|
|
53
|
+
export declare function getProjectContext(projectId: string, projectSecret: string, convexUrl: string): Promise<{
|
|
54
|
+
projectContext: string | null;
|
|
55
|
+
requirementsStyleContext: string | null;
|
|
56
|
+
} | null>;
|
|
46
57
|
export {};
|
|
47
58
|
//# sourceMappingURL=convexClient.d.ts.map
|