@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.
@@ -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
@@ -20,6 +20,15 @@ export interface ProjectInfo {
20
20
  /** Credentials if cloud-connected */
21
21
  credentials?: ProjectSettings;
22
22
  }
23
+ /**
24
+ * Get project credentials from environment variables.
25
+ * Used in CI/CD environments where credentials are passed via env vars
26
+ * rather than stored in project-settings.json.
27
+ *
28
+ * Requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET to be set.
29
+ * Returns undefined if either is missing.
30
+ */
31
+ export declare function getCredentialsFromEnv(): ProjectSettings | undefined;
23
32
  /**
24
33
  * Find the project root by walking up from startDir looking for .requirements/ folder
25
34
  * Returns the directory containing .requirements/, or undefined if not found
@@ -2,6 +2,22 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  const REQUIREMENTS_DIR = '.requirements';
4
4
  const SETTINGS_FILE = 'project-settings.json';
5
+ /**
6
+ * Get project credentials from environment variables.
7
+ * Used in CI/CD environments where credentials are passed via env vars
8
+ * rather than stored in project-settings.json.
9
+ *
10
+ * Requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET to be set.
11
+ * Returns undefined if either is missing.
12
+ */
13
+ export function getCredentialsFromEnv() {
14
+ const projectId = process.env.DOTREQ_PROJECT_ID;
15
+ const projectSecret = process.env.DOTREQ_PROJECT_SECRET;
16
+ if (projectId && projectSecret) {
17
+ return { projectId, projectSecret };
18
+ }
19
+ return undefined;
20
+ }
5
21
  /**
6
22
  * Find the project root by walking up from startDir looking for .requirements/ folder
7
23
  * Returns the directory containing .requirements/, or undefined if not found
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {