@popoverai/dotrequirements 0.15.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/dist/commands/push.d.ts +0 -5
- package/dist/commands/push.js +87 -204
- package/dist/harness/cache.d.ts +2 -1
- package/dist/harness/cache.js +32 -1
- package/dist/mcp/convexClient.d.ts +7 -0
- package/dist/mcp/convexClient.js +50 -2
- package/dist/mcp/index.js +115 -59
- 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/package.json +1 -1
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/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);
|
|
@@ -47,5 +47,12 @@ export declare function getProjectCoverage(projectId: string, projectSecret: str
|
|
|
47
47
|
}>;
|
|
48
48
|
untested: string[];
|
|
49
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>;
|
|
50
57
|
export {};
|
|
51
58
|
//# sourceMappingURL=convexClient.d.ts.map
|
package/dist/mcp/convexClient.js
CHANGED
|
@@ -47,7 +47,11 @@ export async function getRequirementCoverage(requirementKey, projectId, projectS
|
|
|
47
47
|
if (!response.ok) {
|
|
48
48
|
throw new Error(`Failed to query coverage: ${response.statusText}`);
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
const json = await response.json();
|
|
51
|
+
if (json.status === 'error') {
|
|
52
|
+
throw new Error(`Convex query failed: ${json.errorMessage}`);
|
|
53
|
+
}
|
|
54
|
+
return json.value;
|
|
51
55
|
}
|
|
52
56
|
/**
|
|
53
57
|
* Query Convex for project coverage summary
|
|
@@ -76,6 +80,50 @@ export async function getProjectCoverage(projectId, projectSecret, convexUrl, op
|
|
|
76
80
|
if (!response.ok) {
|
|
77
81
|
throw new Error(`Failed to query coverage: ${response.statusText}`);
|
|
78
82
|
}
|
|
79
|
-
|
|
83
|
+
const json = await response.json();
|
|
84
|
+
if (json.status === 'error') {
|
|
85
|
+
throw new Error(`Convex query failed: ${json.errorMessage}`);
|
|
86
|
+
}
|
|
87
|
+
return json.value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* PROJ-CONTEXT-7: Query Convex for project context (AI style guidelines)
|
|
91
|
+
*/
|
|
92
|
+
export async function getProjectContext(projectId, projectSecret, convexUrl) {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${convexUrl}/api/query`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
path: 'projects/queries:get',
|
|
99
|
+
args: {
|
|
100
|
+
projectAuth: {
|
|
101
|
+
projectSlug: projectId,
|
|
102
|
+
projectSecret,
|
|
103
|
+
},
|
|
104
|
+
target: {
|
|
105
|
+
type: 'project',
|
|
106
|
+
slug: projectId,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
format: 'json',
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const json = await response.json();
|
|
116
|
+
if (json.status === 'error' || !json.value) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
projectContext: json.value.projectContext ?? null,
|
|
121
|
+
requirementsStyleContext: json.value.requirementsStyleContext ?? null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Non-fatal: return null if query fails
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
80
128
|
}
|
|
81
129
|
//# sourceMappingURL=convexClient.js.map
|
package/dist/mcp/index.js
CHANGED
|
@@ -6,12 +6,14 @@ import { loadAllRequirements, searchRequirements, getRequirementById, getRequire
|
|
|
6
6
|
import { findAllTestReferences, getReferencedRequirementIds, } from './grep.js';
|
|
7
7
|
import { findTestCodeForRequirement, findFilesWithRequirement, } from './testCodeExtractor.js';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
|
-
import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
|
|
9
|
+
import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, getProjectContext, } from './convexClient.js';
|
|
10
10
|
import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
|
|
11
11
|
import { getCredentialsFromEnv } from '../utils/project-settings.js';
|
|
12
|
-
import { readFileSync } from 'fs';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import { dirname, join } from 'path';
|
|
14
|
+
import { dirname, join, resolve, basename } from 'path';
|
|
15
|
+
import { parseFilesForPush, dryRunPush, executePush, } from '../push/index.js';
|
|
16
|
+
import { findRequirementsFiles } from './requirements.js';
|
|
15
17
|
// Read version from package.json
|
|
16
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
19
|
const __dirname = dirname(__filename);
|
|
@@ -1054,6 +1056,24 @@ ${projectList || '(none found)'}`,
|
|
|
1054
1056
|
|
|
1055
1057
|
**Use concise domain prefixes** like \`AUTH-1\`, \`LOGIN-1\`, etc. Start numbering at 1 and increment sequentially.`;
|
|
1056
1058
|
}
|
|
1059
|
+
// PROJ-CONTEXT-7: Try to fetch user-provided style context from cloud
|
|
1060
|
+
let userStyleGuidance = '';
|
|
1061
|
+
try {
|
|
1062
|
+
const project = await getProjectFromDiscovery();
|
|
1063
|
+
const context = await getProjectContext(project.projectId, project.projectSecret, CONVEX_URL);
|
|
1064
|
+
if (context?.requirementsStyleContext?.trim()) {
|
|
1065
|
+
userStyleGuidance = `
|
|
1066
|
+
|
|
1067
|
+
## User-Provided Style Guidelines
|
|
1068
|
+
|
|
1069
|
+
The following style guidelines were provided by the project owner. When these conflict with the defaults above, prioritize the user's guidelines.
|
|
1070
|
+
|
|
1071
|
+
${context.requirementsStyleContext.trim()}`;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
catch {
|
|
1075
|
+
// No credentials or cloud unavailable - continue without user context
|
|
1076
|
+
}
|
|
1057
1077
|
const template = `---
|
|
1058
1078
|
document:
|
|
1059
1079
|
title: "Example Requirements"
|
|
@@ -1149,6 +1169,7 @@ ${labelGuidance}
|
|
|
1149
1169
|
- Note: Even technical requirements can be user-centric: "95 percent of users experience under 1 second of delay"
|
|
1150
1170
|
|
|
1151
1171
|
**11. Decompose Large Requirements**: If it can't be validated with a single test, break it down.
|
|
1172
|
+
${userStyleGuidance}
|
|
1152
1173
|
|
|
1153
1174
|
## Example Requirements
|
|
1154
1175
|
|
|
@@ -1281,21 +1302,13 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1281
1302
|
}
|
|
1282
1303
|
case 'push_requirements': {
|
|
1283
1304
|
const { filePath, confirmed = false, projectId: projectIdParam } = args;
|
|
1284
|
-
const fs = await import('fs');
|
|
1285
|
-
const path = await import('path');
|
|
1286
|
-
const { parseRequirementsFromFile, getAllRequirements } = await import('../schema/index.js');
|
|
1287
|
-
const { ConvexHttpClient } = await import('convex/browser');
|
|
1288
1305
|
// Get project (credentials come from project object, works with both file and env auth)
|
|
1289
1306
|
const project = await getProjectFromDiscovery(projectIdParam);
|
|
1290
|
-
const convexUrl = CONVEX_URL;
|
|
1291
|
-
const projectId = project.projectId;
|
|
1292
|
-
const projectSecret = project.projectSecret;
|
|
1293
1307
|
// Determine files to push
|
|
1294
|
-
const requirementsDir = path.resolve(project.path, '.requirements');
|
|
1295
1308
|
let filesToPush;
|
|
1296
1309
|
if (filePath) {
|
|
1297
|
-
const fullPath =
|
|
1298
|
-
if (!
|
|
1310
|
+
const fullPath = resolve(project.path, filePath);
|
|
1311
|
+
if (!existsSync(fullPath)) {
|
|
1299
1312
|
return {
|
|
1300
1313
|
content: [
|
|
1301
1314
|
{
|
|
@@ -1309,78 +1322,121 @@ describe(requirement('AUTH-LOGIN-1'), () => {
|
|
|
1309
1322
|
filesToPush = [fullPath];
|
|
1310
1323
|
}
|
|
1311
1324
|
else {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
content: [
|
|
1315
|
-
{
|
|
1316
|
-
type: 'text',
|
|
1317
|
-
text: 'No .requirements/ directory found',
|
|
1318
|
-
},
|
|
1319
|
-
],
|
|
1320
|
-
isError: true,
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
const files = fs.readdirSync(requirementsDir);
|
|
1324
|
-
filesToPush = files
|
|
1325
|
-
.filter((f) => f.endsWith('.requirements.md'))
|
|
1326
|
-
.map((f) => path.join(requirementsDir, f));
|
|
1325
|
+
// Use same file discovery as CLI - searches entire workspace
|
|
1326
|
+
filesToPush = await findRequirementsFiles(project.path);
|
|
1327
1327
|
if (filesToPush.length === 0) {
|
|
1328
1328
|
return {
|
|
1329
1329
|
content: [
|
|
1330
1330
|
{
|
|
1331
1331
|
type: 'text',
|
|
1332
|
-
text: 'No
|
|
1332
|
+
text: 'No *.requirements.md files found. Nothing to push.',
|
|
1333
1333
|
},
|
|
1334
1334
|
],
|
|
1335
|
-
isError: true,
|
|
1336
1335
|
};
|
|
1337
1336
|
}
|
|
1338
1337
|
}
|
|
1339
|
-
// Parse
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1338
|
+
// Parse files
|
|
1339
|
+
const { parsedFiles, totalRequirements } = parseFilesForPush(filesToPush);
|
|
1340
|
+
// Build credentials
|
|
1341
|
+
const credentials = {
|
|
1342
|
+
projectId: project.projectId,
|
|
1343
|
+
projectSecret: project.projectSecret,
|
|
1344
|
+
convexUrl: CONVEX_URL,
|
|
1345
|
+
};
|
|
1346
|
+
// Run dry run (always, even when confirmed - ensures fresh state)
|
|
1347
|
+
const dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
1348
|
+
// Check if there's anything to push
|
|
1349
|
+
const pushableCount = dryRunResult.updates.length +
|
|
1350
|
+
dryRunResult.creates.length +
|
|
1351
|
+
dryRunResult.notFound.length;
|
|
1352
|
+
if (pushableCount === 0 && dryRunResult.invalid.length > 0) {
|
|
1353
|
+
// Only invalid files
|
|
1354
|
+
const invalidList = dryRunResult.invalid
|
|
1355
|
+
.map(({ file, result }) => `- ${basename(file.filePath)}: ${result.error}`)
|
|
1356
|
+
.join('\n');
|
|
1357
|
+
return {
|
|
1358
|
+
content: [
|
|
1359
|
+
{
|
|
1360
|
+
type: 'text',
|
|
1361
|
+
text: `✗ No valid documents to push.\n\n**Invalid files:**\n${invalidList}`,
|
|
1362
|
+
},
|
|
1363
|
+
],
|
|
1364
|
+
isError: true,
|
|
1365
|
+
};
|
|
1353
1366
|
}
|
|
1354
1367
|
if (!confirmed) {
|
|
1355
|
-
// First call: show
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1368
|
+
// First call: show preview with real dry run results
|
|
1369
|
+
let summary = `# Push Preview\n\n**Files:** ${parsedFiles.length}\n**Requirements:** ${totalRequirements}\n\n`;
|
|
1370
|
+
if (dryRunResult.updates.length > 0) {
|
|
1371
|
+
summary += `## Updates (${dryRunResult.updates.length})\n`;
|
|
1372
|
+
for (const { file, result } of dryRunResult.updates) {
|
|
1373
|
+
const fileName = basename(file.filePath);
|
|
1374
|
+
const hasConflict = dryRunResult.conflicts.some((c) => c.item.file === file);
|
|
1375
|
+
const conflictNote = hasConflict ? ' ⚠️ [cloud changed since pull]' : '';
|
|
1376
|
+
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
1377
|
+
summary += `- ~ ${fileName}${conflictNote}${warningNote}\n`;
|
|
1378
|
+
}
|
|
1379
|
+
summary += '\n';
|
|
1380
|
+
}
|
|
1381
|
+
if (dryRunResult.creates.length > 0) {
|
|
1382
|
+
summary += `## New Documents (${dryRunResult.creates.length})\n`;
|
|
1383
|
+
for (const { file, result } of dryRunResult.creates) {
|
|
1384
|
+
const fileName = basename(file.filePath);
|
|
1385
|
+
const warningNote = result.warning ? ` (${result.warning})` : '';
|
|
1386
|
+
summary += `- + ${fileName}${warningNote}\n`;
|
|
1387
|
+
}
|
|
1388
|
+
summary += '\n';
|
|
1389
|
+
}
|
|
1390
|
+
if (dryRunResult.notFound.length > 0) {
|
|
1391
|
+
summary += `## Not Found in Cloud (${dryRunResult.notFound.length}) - will create new\n`;
|
|
1392
|
+
for (const { file, result } of dryRunResult.notFound) {
|
|
1393
|
+
const fileName = basename(file.filePath);
|
|
1394
|
+
summary += `- ! ${fileName} (ID: ${result.documentId})\n`;
|
|
1395
|
+
}
|
|
1396
|
+
summary += '\n';
|
|
1397
|
+
}
|
|
1398
|
+
if (dryRunResult.invalid.length > 0) {
|
|
1399
|
+
summary += `## Skipped - Invalid (${dryRunResult.invalid.length})\n`;
|
|
1400
|
+
for (const { file, result } of dryRunResult.invalid) {
|
|
1401
|
+
const fileName = basename(file.filePath);
|
|
1402
|
+
summary += `- ✗ ${fileName}: ${result.error}\n`;
|
|
1403
|
+
}
|
|
1404
|
+
summary += '\n';
|
|
1405
|
+
}
|
|
1406
|
+
if (dryRunResult.conflicts.length > 0) {
|
|
1407
|
+
summary += `⚠️ **Warning:** ${dryRunResult.conflicts.length} file(s) have cloud changes since last pull. Pushing will overwrite those changes.\n\n`;
|
|
1408
|
+
}
|
|
1409
|
+
summary += `**To proceed:** Call this tool again with \`confirmed: true\``;
|
|
1359
1410
|
return {
|
|
1360
1411
|
content: [
|
|
1361
1412
|
{
|
|
1362
1413
|
type: 'text',
|
|
1363
|
-
text:
|
|
1414
|
+
text: summary,
|
|
1364
1415
|
},
|
|
1365
1416
|
],
|
|
1366
1417
|
};
|
|
1367
1418
|
}
|
|
1368
1419
|
// Second call: execute push
|
|
1369
|
-
const client = new ConvexHttpClient(convexUrl);
|
|
1370
1420
|
try {
|
|
1371
|
-
const result = await
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1421
|
+
const result = await executePush(dryRunResult, credentials);
|
|
1422
|
+
let output = '✓ Push complete!\n\n';
|
|
1423
|
+
if (result.created > 0) {
|
|
1424
|
+
output += `**Created:** ${result.created} document(s)\n`;
|
|
1425
|
+
}
|
|
1426
|
+
if (result.updated > 0) {
|
|
1427
|
+
output += `**Updated:** ${result.updated} document(s)\n`;
|
|
1428
|
+
}
|
|
1429
|
+
if (result.errors.length > 0) {
|
|
1430
|
+
output += `\n**Errors:**\n`;
|
|
1431
|
+
for (const { fileName, error } of result.errors) {
|
|
1432
|
+
output += `- ${fileName}: ${error}\n`;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1379
1435
|
return {
|
|
1380
1436
|
content: [
|
|
1381
1437
|
{
|
|
1382
1438
|
type: 'text',
|
|
1383
|
-
text:
|
|
1439
|
+
text: output,
|
|
1384
1440
|
},
|
|
1385
1441
|
],
|
|
1386
1442
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared push logic for CLI and MCP.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the core push functionality that both the CLI push command
|
|
5
|
+
* and MCP push_requirements tool use. It handles:
|
|
6
|
+
* - Parsing local files
|
|
7
|
+
* - Dry run validation with Convex
|
|
8
|
+
* - Conflict detection
|
|
9
|
+
* - Executing the push
|
|
10
|
+
* - Updating local files with document IDs and timestamps
|
|
11
|
+
*/
|
|
12
|
+
import type { Metadata } from '../schema/index.js';
|
|
13
|
+
export interface PushCredentials {
|
|
14
|
+
projectId: string;
|
|
15
|
+
projectSecret: string;
|
|
16
|
+
convexUrl: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parsed file ready for push.
|
|
20
|
+
* SYNC-ARCH-1: Documents are the sync unit, not individual requirements.
|
|
21
|
+
*/
|
|
22
|
+
export interface ParsedFile {
|
|
23
|
+
filePath: string;
|
|
24
|
+
metadata: Metadata;
|
|
25
|
+
/** Raw file content (markdown with frontmatter stripped) */
|
|
26
|
+
markdownContent: string;
|
|
27
|
+
/** Requirement count for display */
|
|
28
|
+
requirementCount: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cloud document metadata for conflict detection.
|
|
32
|
+
* SYNC-CLI-EDIT-2: Compare pulledAt with cloud updatedAt.
|
|
33
|
+
*/
|
|
34
|
+
export interface CloudDocumentMetadata {
|
|
35
|
+
documentId: string;
|
|
36
|
+
version: number;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Dry run result from saveWithRequirements.
|
|
41
|
+
*/
|
|
42
|
+
export interface DryRunResultItem {
|
|
43
|
+
dryRun: true;
|
|
44
|
+
action: 'create' | 'update' | 'not_found' | 'invalid';
|
|
45
|
+
documentId?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
warning?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* File with its dry run result.
|
|
52
|
+
*/
|
|
53
|
+
export interface FileWithDryRun {
|
|
54
|
+
file: ParsedFile;
|
|
55
|
+
result: DryRunResultItem;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Conflict information for a file.
|
|
59
|
+
*/
|
|
60
|
+
export interface ConflictInfo {
|
|
61
|
+
item: FileWithDryRun;
|
|
62
|
+
cloudMeta: CloudDocumentMetadata;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Result of the dry run phase.
|
|
66
|
+
*/
|
|
67
|
+
export interface DryRunResult {
|
|
68
|
+
updates: FileWithDryRun[];
|
|
69
|
+
creates: FileWithDryRun[];
|
|
70
|
+
notFound: FileWithDryRun[];
|
|
71
|
+
invalid: FileWithDryRun[];
|
|
72
|
+
conflicts: ConflictInfo[];
|
|
73
|
+
totalRequirements: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Result of the execute phase.
|
|
77
|
+
*/
|
|
78
|
+
export interface PushResult {
|
|
79
|
+
created: number;
|
|
80
|
+
updated: number;
|
|
81
|
+
errors: Array<{
|
|
82
|
+
fileName: string;
|
|
83
|
+
error: string;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract markdown content from a file, stripping YAML frontmatter.
|
|
88
|
+
*/
|
|
89
|
+
export declare function extractMarkdownContent(rawContent: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Parse files for push. Returns parsed files with metadata and content.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseFilesForPush(filePaths: string[]): {
|
|
94
|
+
parsedFiles: ParsedFile[];
|
|
95
|
+
totalRequirements: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Execute dry run phase: validate all files against Convex and detect conflicts.
|
|
99
|
+
*/
|
|
100
|
+
export declare function dryRunPush(parsedFiles: ParsedFile[], credentials: PushCredentials): Promise<DryRunResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Execute the push: save all pushable files to Convex and update local files.
|
|
103
|
+
*/
|
|
104
|
+
export declare function executePush(dryRunResult: DryRunResult, credentials: PushCredentials): Promise<PushResult>;
|
|
105
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared push logic for CLI and MCP.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the core push functionality that both the CLI push command
|
|
5
|
+
* and MCP push_requirements tool use. It handles:
|
|
6
|
+
* - Parsing local files
|
|
7
|
+
* - Dry run validation with Convex
|
|
8
|
+
* - Conflict detection
|
|
9
|
+
* - Executing the push
|
|
10
|
+
* - Updating local files with document IDs and timestamps
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { ConvexHttpClient } from 'convex/browser';
|
|
15
|
+
import { parseRequirementsFromFile, getAllRequirements, parseRequirementKey, buildRequirementsFile, } from '../schema/index.js';
|
|
16
|
+
import { api } from '../convex.js';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Parsing
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Extract markdown content from a file, stripping YAML frontmatter.
|
|
22
|
+
*/
|
|
23
|
+
export function extractMarkdownContent(rawContent) {
|
|
24
|
+
// Match YAML frontmatter: starts with ---, ends with ---
|
|
25
|
+
const frontmatterMatch = rawContent.match(/^---\n[\s\S]*?\n---\n*/);
|
|
26
|
+
if (frontmatterMatch) {
|
|
27
|
+
return rawContent.slice(frontmatterMatch[0].length);
|
|
28
|
+
}
|
|
29
|
+
return rawContent;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse files for push. Returns parsed files with metadata and content.
|
|
33
|
+
*/
|
|
34
|
+
export function parseFilesForPush(filePaths) {
|
|
35
|
+
const parsedFiles = [];
|
|
36
|
+
let totalRequirements = 0;
|
|
37
|
+
for (const filePath of filePaths) {
|
|
38
|
+
// Read raw file content
|
|
39
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
40
|
+
// Parse to get metadata and requirements
|
|
41
|
+
const parsed = parseRequirementsFromFile(filePath);
|
|
42
|
+
const flatRequirements = getAllRequirements(parsed.requirements);
|
|
43
|
+
// Extract markdown content (everything after frontmatter)
|
|
44
|
+
const markdownContent = extractMarkdownContent(rawContent);
|
|
45
|
+
// DOC-HEADER-11.1/11.2: Infer prefix from first requirement if not specified
|
|
46
|
+
const doc = parsed.metadata.document;
|
|
47
|
+
if (doc && !doc.defaultPrefix && flatRequirements.length > 0) {
|
|
48
|
+
const firstReq = flatRequirements[0];
|
|
49
|
+
if (firstReq) {
|
|
50
|
+
const parsedKey = parseRequirementKey(firstReq.id);
|
|
51
|
+
if (parsedKey) {
|
|
52
|
+
doc.defaultPrefix = parsedKey.prefix;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
parsedFiles.push({
|
|
57
|
+
filePath,
|
|
58
|
+
metadata: parsed.metadata,
|
|
59
|
+
markdownContent,
|
|
60
|
+
requirementCount: flatRequirements.length,
|
|
61
|
+
});
|
|
62
|
+
totalRequirements += flatRequirements.length;
|
|
63
|
+
}
|
|
64
|
+
return { parsedFiles, totalRequirements };
|
|
65
|
+
}
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Dry Run
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/**
|
|
70
|
+
* Execute dry run phase: validate all files against Convex and detect conflicts.
|
|
71
|
+
*/
|
|
72
|
+
export async function dryRunPush(parsedFiles, credentials) {
|
|
73
|
+
const client = new ConvexHttpClient(credentials.convexUrl);
|
|
74
|
+
const dryRunResults = [];
|
|
75
|
+
// Phase 1: Dry run to categorize all files
|
|
76
|
+
for (const file of parsedFiles) {
|
|
77
|
+
const doc = file.metadata.document;
|
|
78
|
+
const fileName = path.basename(file.filePath);
|
|
79
|
+
if (!doc) {
|
|
80
|
+
// No document section - mark as invalid locally
|
|
81
|
+
dryRunResults.push({
|
|
82
|
+
file,
|
|
83
|
+
result: {
|
|
84
|
+
dryRun: true,
|
|
85
|
+
action: 'invalid',
|
|
86
|
+
title: fileName,
|
|
87
|
+
error: 'Missing document section in frontmatter',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const result = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
|
|
94
|
+
projectAuth: {
|
|
95
|
+
projectSlug: credentials.projectId,
|
|
96
|
+
projectSecret: credentials.projectSecret,
|
|
97
|
+
},
|
|
98
|
+
target: { type: 'project', slug: credentials.projectId },
|
|
99
|
+
documentId: doc.id,
|
|
100
|
+
title: doc.title,
|
|
101
|
+
markdownContent: file.markdownContent,
|
|
102
|
+
defaultPrefix: doc.defaultPrefix,
|
|
103
|
+
dryRun: true,
|
|
104
|
+
}));
|
|
105
|
+
dryRunResults.push({ file, result });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
dryRunResults.push({
|
|
109
|
+
file,
|
|
110
|
+
result: {
|
|
111
|
+
dryRun: true,
|
|
112
|
+
action: 'invalid',
|
|
113
|
+
title: doc.title,
|
|
114
|
+
error: err.message,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Categorize results
|
|
120
|
+
const updates = dryRunResults.filter((r) => r.result.action === 'update');
|
|
121
|
+
const creates = dryRunResults.filter((r) => r.result.action === 'create');
|
|
122
|
+
const notFound = dryRunResults.filter((r) => r.result.action === 'not_found');
|
|
123
|
+
const invalid = dryRunResults.filter((r) => r.result.action === 'invalid');
|
|
124
|
+
// Check for conflicts on updates (cloud changed since last pull)
|
|
125
|
+
const conflicts = [];
|
|
126
|
+
if (updates.length > 0) {
|
|
127
|
+
const docIds = updates.map((u) => u.result.documentId);
|
|
128
|
+
const cloudMetadata = await client.query(api.documents.queries.getDocumentsMetadata, {
|
|
129
|
+
projectAuth: {
|
|
130
|
+
projectSlug: credentials.projectId,
|
|
131
|
+
projectSecret: credentials.projectSecret,
|
|
132
|
+
},
|
|
133
|
+
target: { type: 'project', slug: credentials.projectId },
|
|
134
|
+
documentIds: docIds,
|
|
135
|
+
});
|
|
136
|
+
const cloudMetaByDocId = new Map(cloudMetadata.map((m) => [m.documentId, m]));
|
|
137
|
+
for (const item of updates) {
|
|
138
|
+
const docId = item.result.documentId;
|
|
139
|
+
const cloudMeta = cloudMetaByDocId.get(docId);
|
|
140
|
+
if (cloudMeta && item.file.metadata.pulledAt) {
|
|
141
|
+
const pulledAtMs = new Date(item.file.metadata.pulledAt).getTime();
|
|
142
|
+
if (cloudMeta.updatedAt > pulledAtMs) {
|
|
143
|
+
conflicts.push({ item, cloudMeta });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
updates,
|
|
150
|
+
creates,
|
|
151
|
+
notFound,
|
|
152
|
+
invalid,
|
|
153
|
+
conflicts,
|
|
154
|
+
totalRequirements: parsedFiles.reduce((sum, f) => sum + f.requirementCount, 0),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Execute Push
|
|
159
|
+
// ============================================================================
|
|
160
|
+
/**
|
|
161
|
+
* Execute the push: save all pushable files to Convex and update local files.
|
|
162
|
+
*/
|
|
163
|
+
export async function executePush(dryRunResult, credentials) {
|
|
164
|
+
const client = new ConvexHttpClient(credentials.convexUrl);
|
|
165
|
+
const pushableFiles = [
|
|
166
|
+
...dryRunResult.updates,
|
|
167
|
+
...dryRunResult.creates,
|
|
168
|
+
...dryRunResult.notFound,
|
|
169
|
+
];
|
|
170
|
+
let created = 0;
|
|
171
|
+
let updated = 0;
|
|
172
|
+
const errors = [];
|
|
173
|
+
for (const { file, result } of pushableFiles) {
|
|
174
|
+
const doc = file.metadata.document;
|
|
175
|
+
const fileName = path.basename(file.filePath);
|
|
176
|
+
// For not_found, clear the ID so we create a new document
|
|
177
|
+
const effectiveDocId = result.action === 'not_found' ? undefined : doc.id;
|
|
178
|
+
try {
|
|
179
|
+
const pushResult = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
|
|
180
|
+
projectAuth: {
|
|
181
|
+
projectSlug: credentials.projectId,
|
|
182
|
+
projectSecret: credentials.projectSecret,
|
|
183
|
+
},
|
|
184
|
+
target: { type: 'project', slug: credentials.projectId },
|
|
185
|
+
documentId: effectiveDocId,
|
|
186
|
+
title: doc.title,
|
|
187
|
+
markdownContent: file.markdownContent,
|
|
188
|
+
defaultPrefix: doc.defaultPrefix,
|
|
189
|
+
dryRun: false,
|
|
190
|
+
}));
|
|
191
|
+
const isCreate = result.action === 'create' || result.action === 'not_found';
|
|
192
|
+
if (isCreate) {
|
|
193
|
+
// New document - write ID back to file
|
|
194
|
+
doc.id = pushResult;
|
|
195
|
+
file.metadata.pulledAt = new Date().toISOString();
|
|
196
|
+
file.metadata.version = 1;
|
|
197
|
+
const updatedContent = buildRequirementsFile(file.metadata, file.markdownContent);
|
|
198
|
+
fs.writeFileSync(file.filePath, updatedContent, 'utf-8');
|
|
199
|
+
created++;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Update pulledAt to reflect this push
|
|
203
|
+
file.metadata.pulledAt = new Date().toISOString();
|
|
204
|
+
file.metadata.version = (file.metadata.version || 0) + 1;
|
|
205
|
+
const updatedContent = buildRequirementsFile(file.metadata, file.markdownContent);
|
|
206
|
+
fs.writeFileSync(file.filePath, updatedContent, 'utf-8');
|
|
207
|
+
updated++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
errors.push({ fileName, error: err.message });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { created, updated, errors };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Module
|
|
3
|
+
*
|
|
4
|
+
* Shared push logic for syncing local requirements files to the cloud.
|
|
5
|
+
* Used by both CLI push command and MCP push_requirements tool.
|
|
6
|
+
*/
|
|
7
|
+
export { type PushCredentials, type ParsedFile, type CloudDocumentMetadata, type DryRunResultItem, type FileWithDryRun, type ConflictInfo, type DryRunResult, type PushResult, extractMarkdownContent, parseFilesForPush, dryRunPush, executePush, } from './core.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Module
|
|
3
|
+
*
|
|
4
|
+
* Shared push logic for syncing local requirements files to the cloud.
|
|
5
|
+
* Used by both CLI push command and MCP push_requirements tool.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
// Functions
|
|
9
|
+
extractMarkdownContent, parseFilesForPush, dryRunPush, executePush, } from './core.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|