@popoverai/dotrequirements 0.27.4 → 0.29.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 +24 -20
- package/dist/cli.js +34 -12
- package/dist/codebase-to-spec/compose.d.ts +3 -2
- package/dist/codebase-to-spec/compose.js +3 -3
- package/dist/commands/aliases.d.ts +26 -0
- package/dist/commands/aliases.js +31 -0
- package/dist/commands/diff.d.ts +14 -0
- package/dist/commands/diff.js +62 -0
- package/dist/commands/init.js +5 -5
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/link.js +10 -7
- package/dist/commands/sync-common.d.ts +21 -0
- package/dist/commands/sync-common.js +24 -0
- package/dist/commands/sync.d.ts +26 -0
- package/dist/commands/sync.js +187 -0
- package/dist/convex.d.ts +1 -1
- package/dist/convex.js +2 -2
- package/dist/push/core.d.ts +18 -116
- package/dist/push/core.js +23 -267
- package/dist/push/index.d.ts +3 -3
- package/dist/push/index.js +4 -4
- package/dist/requirements/index.d.ts +2 -0
- package/dist/requirements/index.js +6 -1
- package/dist/requirements/style-guide.js +4 -4
- package/dist/schema/browser.d.ts +1 -0
- package/dist/schema/browser.js +3 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/parser.d.ts +3 -0
- package/dist/schema/parser.js +1 -1
- package/dist/schema/schemas.d.ts +24 -22
- package/dist/schema/schemas.js +9 -12
- package/dist/schema/title-markdown.d.ts +30 -0
- package/dist/schema/title-markdown.js +38 -0
- package/dist/sync/compare.d.ts +21 -0
- package/dist/sync/compare.js +285 -0
- package/dist/sync/execute.d.ts +53 -0
- package/dist/sync/execute.js +225 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.js +52 -0
- package/dist/sync/local-files.d.ts +26 -0
- package/dist/sync/local-files.js +93 -0
- package/dist/sync/plan.d.ts +39 -0
- package/dist/sync/plan.js +90 -0
- package/dist/sync/render.d.ts +17 -0
- package/dist/sync/render.js +46 -0
- package/dist/sync/segment.d.ts +40 -0
- package/dist/sync/segment.js +76 -0
- package/dist/sync/snapshot.d.ts +30 -0
- package/dist/sync/snapshot.js +122 -0
- package/dist/sync/types.d.ts +82 -0
- package/dist/sync/types.js +12 -0
- package/dist/templates/context-file-section.md +2 -1
- package/dist/templates/example-requirements.js +0 -2
- package/dist/templates/example-requirements.ts +0 -2
- package/dist/templates/requirements-readme.js +3 -4
- package/dist/templates/requirements-readme.ts +3 -4
- package/dist/templates/skills/codebase-to-spec/SKILL.md +2 -2
- package/dist/utils/project-settings.d.ts +8 -2
- package/dist/utils/project-settings.js +47 -24
- package/package.json +1 -1
- package/dist/commands/pull.d.ts +0 -8
- package/dist/commands/pull.js +0 -230
- package/dist/commands/push.d.ts +0 -6
- package/dist/commands/push.js +0 -244
package/dist/commands/pull.js
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { ConvexHttpClient } from "convex/browser";
|
|
4
|
-
import { getConvexUrl } from "../config.js";
|
|
5
|
-
import { api } from "../convex.js";
|
|
6
|
-
import { findRequirementsFiles } from "../requirements/index.js";
|
|
7
|
-
import { buildRequirementsFile } from "../schema/index.js";
|
|
8
|
-
import { extractFrontmatterBlock } from "../schema/parser-core.js";
|
|
9
|
-
import { brand } from "../utils/brand.js";
|
|
10
|
-
import { getProjectCredentials } from "../utils/project-settings.js";
|
|
11
|
-
export async function pullCommand(options) {
|
|
12
|
-
// Connect to Convex
|
|
13
|
-
const convexUrl = getConvexUrl();
|
|
14
|
-
const client = new ConvexHttpClient(convexUrl);
|
|
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}"`);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
// SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully.
|
|
37
|
-
// Production redacts ConvexError messages to "Server Error" but
|
|
38
|
-
// preserves error.data — check both, or the friendly message only
|
|
39
|
-
// ever appears against dev deployments.
|
|
40
|
-
const data = error.data;
|
|
41
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
42
|
-
if (data?.message === "Invalid share token" ||
|
|
43
|
-
errorMessage.includes("Invalid share token")) {
|
|
44
|
-
throw new Error("Invalid share token. The token may be incorrect or has been revoked.\n" +
|
|
45
|
-
"Please request a new share token from your team.");
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
console.log(`Syncing requirements from ${brand} cloud...`);
|
|
52
|
-
// Get project credentials from .requirements/project-settings.json
|
|
53
|
-
// This throws helpful errors if project not found or not connected to cloud
|
|
54
|
-
const credentials = getProjectCredentials();
|
|
55
|
-
// Allow --project flag to override, but still need secret from settings
|
|
56
|
-
const projectId = options.project ?? credentials.projectId;
|
|
57
|
-
const projectSecret = credentials.projectSecret;
|
|
58
|
-
// Fetch documents
|
|
59
|
-
if (options.document) {
|
|
60
|
-
// Fetch specific document
|
|
61
|
-
const doc = await client.query(api.documents.queries.exportForCli, {
|
|
62
|
-
projectAuth: {
|
|
63
|
-
projectSlug: projectId,
|
|
64
|
-
projectSecret,
|
|
65
|
-
},
|
|
66
|
-
target: { type: "document", id: options.document },
|
|
67
|
-
documentId: options.document,
|
|
68
|
-
});
|
|
69
|
-
documents = doc ? [doc] : [];
|
|
70
|
-
}
|
|
71
|
-
else if (projectId) {
|
|
72
|
-
// Fetch all documents for project
|
|
73
|
-
// SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
|
|
74
|
-
const result = await client.query(api.documents.queries.exportProjectForCli, {
|
|
75
|
-
projectAuth: {
|
|
76
|
-
projectSlug: projectId,
|
|
77
|
-
projectSecret,
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
documents = result.documents;
|
|
81
|
-
projectName = result.projectName;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
throw new Error("Either --project or --document must be specified");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (documents.length === 0) {
|
|
88
|
-
console.log("No documents found.");
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// Ensure .requirements/ directory exists (for new documents)
|
|
92
|
-
const requirementsDir = path.join(process.cwd(), ".requirements");
|
|
93
|
-
if (!fs.existsSync(requirementsDir)) {
|
|
94
|
-
fs.mkdirSync(requirementsDir, { recursive: true });
|
|
95
|
-
}
|
|
96
|
-
// Build index of existing files by document ID (search entire workspace)
|
|
97
|
-
const existingFilesByDocId = await buildDocumentIdIndex(process.cwd());
|
|
98
|
-
// SYNC-WEB-CREATE-2.0: track paths claimed during this pull so colliding
|
|
99
|
-
// titles don't silently overwrite each other within one operation
|
|
100
|
-
const usedPaths = new Set(existingFilesByDocId.values());
|
|
101
|
-
// Write each document as a Markdown file
|
|
102
|
-
for (const doc of documents) {
|
|
103
|
-
// Check if an existing file has this document ID
|
|
104
|
-
const existingFilePath = existingFilesByDocId.get(doc.documentId);
|
|
105
|
-
let filePath;
|
|
106
|
-
if (existingFilePath) {
|
|
107
|
-
filePath = existingFilePath;
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// SYNC-WEB-CREATE-2.1: an all-symbols title sanitizes to nothing —
|
|
111
|
-
// fall back to the document ID rather than a hidden ".requirements.md"
|
|
112
|
-
const baseName = sanitizeFileName(doc.title) || doc.documentId;
|
|
113
|
-
let candidate = path.join(requirementsDir, `${baseName}.requirements.md`);
|
|
114
|
-
// SYNC-WEB-CREATE-2.0: disambiguate later collisions with a numeric
|
|
115
|
-
// suffix; also avoid clobbering an on-disk file that belongs to a
|
|
116
|
-
// different (or no) document
|
|
117
|
-
let suffix = 2;
|
|
118
|
-
while (usedPaths.has(candidate) || fs.existsSync(candidate)) {
|
|
119
|
-
candidate = path.join(requirementsDir, `${baseName}-${suffix}.requirements.md`);
|
|
120
|
-
suffix++;
|
|
121
|
-
}
|
|
122
|
-
filePath = candidate;
|
|
123
|
-
}
|
|
124
|
-
usedPaths.add(filePath);
|
|
125
|
-
const fileName = path.basename(filePath);
|
|
126
|
-
// IMPORT-1: carry the CTS run marker forward from the existing local
|
|
127
|
-
// file — pull rebuilds frontmatter from cloud data, and silently dropping
|
|
128
|
-
// the marker would strip a committed spec's import credential before a
|
|
129
|
-
// teammate ever pushes it
|
|
130
|
-
let existingCtsRun;
|
|
131
|
-
if (existingFilePath && fs.existsSync(existingFilePath)) {
|
|
132
|
-
const match = fs
|
|
133
|
-
.readFileSync(existingFilePath, "utf-8")
|
|
134
|
-
.match(/^ctsRun: (.+)$/m);
|
|
135
|
-
existingCtsRun = match?.[1].trim();
|
|
136
|
-
}
|
|
137
|
-
// Build metadata with pulledAt for conflict detection
|
|
138
|
-
// SYNC-CLI-EDIT-2: pulledAt is compared with cloud updatedAt during push
|
|
139
|
-
const metadata = {
|
|
140
|
-
pulledAt: new Date().toISOString(),
|
|
141
|
-
version: doc.version,
|
|
142
|
-
...(existingCtsRun ? { ctsRun: existingCtsRun } : {}),
|
|
143
|
-
document: {
|
|
144
|
-
id: doc.documentId,
|
|
145
|
-
title: doc.title,
|
|
146
|
-
defaultPrefix: doc.defaultPrefix,
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
// SYNC-ARCH-1: Use markdownContent directly from cloud (source of truth)
|
|
150
|
-
// No need to rebuild from requirements - just combine with frontmatter
|
|
151
|
-
const fileContent = buildRequirementsFile(metadata, doc.markdownContent);
|
|
152
|
-
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
153
|
-
console.log(`✓ Synced: ${fileName} (version ${doc.version})`);
|
|
154
|
-
}
|
|
155
|
-
console.log(`\nSuccessfully synced ${documents.length} document(s) to .requirements/`);
|
|
156
|
-
// SHARE-TOKEN-CLI-3: Show next steps for share token users
|
|
157
|
-
if (isShareTokenFlow) {
|
|
158
|
-
console.log("\n─────────────────────────────────────────────────────────");
|
|
159
|
-
console.log("Next steps to get full access (push, coverage):");
|
|
160
|
-
console.log(" Run: dotrequirements link");
|
|
161
|
-
console.log("─────────────────────────────────────────────────────────");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Sanitize document title for use as filename
|
|
166
|
-
*/
|
|
167
|
-
function sanitizeFileName(title) {
|
|
168
|
-
return title
|
|
169
|
-
.toLowerCase()
|
|
170
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
171
|
-
.replace(/^-+|-+$/g, "");
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Build an index mapping document IDs to their file paths.
|
|
175
|
-
* Scans existing .requirements.md files across the entire workspace and extracts document.id from frontmatter.
|
|
176
|
-
* SYNC-DISCOVERY-2.0: Uses the same discovery logic as push and MCP.
|
|
177
|
-
*/
|
|
178
|
-
async function buildDocumentIdIndex(workspaceRoot) {
|
|
179
|
-
const index = new Map();
|
|
180
|
-
// Use the same file discovery as MCP and push
|
|
181
|
-
const allFiles = await findRequirementsFiles(workspaceRoot);
|
|
182
|
-
for (const filePath of allFiles) {
|
|
183
|
-
const docId = extractDocumentIdFromFile(filePath);
|
|
184
|
-
if (docId) {
|
|
185
|
-
index.set(docId, filePath);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return index;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Extract document.id from a requirements file's frontmatter.
|
|
192
|
-
* Returns undefined if the file can't be read or doesn't have a document ID.
|
|
193
|
-
* SYNC-DISCOVERY-3: only the leading YAML frontmatter block is consulted —
|
|
194
|
-
* a document id quoted in body prose or a fenced example must never mark
|
|
195
|
-
* the file as owning that document.
|
|
196
|
-
*/
|
|
197
|
-
function extractDocumentIdFromFile(filePath) {
|
|
198
|
-
try {
|
|
199
|
-
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
200
|
-
// Isolate the leading ---...--- frontmatter block; no frontmatter means
|
|
201
|
-
// the file is unlinked to any cloud document (SYNC-DISCOVERY-3.2).
|
|
202
|
-
// Shared helper normalizes CRLF (SYNC-FORMAT-1) so a Windows-saved file
|
|
203
|
-
// keeps matching its document and is updated in place (SYNC-DISCOVERY-2.1).
|
|
204
|
-
const content = extractFrontmatterBlock(fileContent);
|
|
205
|
-
if (content === undefined) {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
// Match document.id in YAML frontmatter - handles both inline and nested formats
|
|
209
|
-
// Inline: document: { id: "abc123", ... }
|
|
210
|
-
// Nested (id: can appear at any position within the indented document block):
|
|
211
|
-
// document:
|
|
212
|
-
// title: Some Title
|
|
213
|
-
// defaultPrefix: REQ
|
|
214
|
-
// id: abc123
|
|
215
|
-
const inlineMatch = content.match(/document:\s*\{[^}]*id:\s*["']?([^"',}\s]+)/);
|
|
216
|
-
if (inlineMatch) {
|
|
217
|
-
return inlineMatch[1];
|
|
218
|
-
}
|
|
219
|
-
// Match id: anywhere within the document block (allows other properties before it)
|
|
220
|
-
const nestedMatch = content.match(/document:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+id:\s*["']?([^"'\s\n]+)/);
|
|
221
|
-
if (nestedMatch) {
|
|
222
|
-
return nestedMatch[1];
|
|
223
|
-
}
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
//# sourceMappingURL=pull.js.map
|
package/dist/commands/push.d.ts
DELETED
package/dist/commands/push.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import * as readline from "node:readline";
|
|
4
|
-
import { getConvexUrl } from "../config.js";
|
|
5
|
-
import { dryRunPush, executePush, parseFilesForPushIndividually, } from "../push/index.js";
|
|
6
|
-
import { findRequirementsFiles } from "../requirements/index.js";
|
|
7
|
-
import { brand } from "../utils/brand.js";
|
|
8
|
-
import { getProjectCredentials } from "../utils/project-settings.js";
|
|
9
|
-
export async function pushCommand(file, options) {
|
|
10
|
-
console.log(`Preparing to push requirements to ${brand} cloud...\n`);
|
|
11
|
-
// Get project credentials from .requirements/project-settings.json
|
|
12
|
-
// This throws helpful errors if project not found or not connected to cloud
|
|
13
|
-
const { projectId, projectSecret } = getProjectCredentials();
|
|
14
|
-
// Determine which files to push
|
|
15
|
-
let filesToPush;
|
|
16
|
-
if (file) {
|
|
17
|
-
// Push specific file
|
|
18
|
-
const filePath = path.resolve(file);
|
|
19
|
-
if (!fs.existsSync(filePath)) {
|
|
20
|
-
throw new Error(`File not found: ${file}`);
|
|
21
|
-
}
|
|
22
|
-
filesToPush = [filePath];
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
// SYNC-DISCOVERY-1.0: Push all *.requirements.md files in workspace (not just .requirements/)
|
|
26
|
-
const workspaceRoot = process.cwd();
|
|
27
|
-
filesToPush = await findRequirementsFiles(workspaceRoot);
|
|
28
|
-
if (filesToPush.length === 0) {
|
|
29
|
-
console.log("No *.requirements.md files found. Nothing to push.");
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Parse local files. SYNC-FAIL-4: one invalid file is skipped while the
|
|
34
|
-
// rest of the batch still pushes — same helper as the MCP push handler.
|
|
35
|
-
console.log("Parsing local requirements...");
|
|
36
|
-
const { parsedFiles, totalRequirements, parseFailures } = parseFilesForPushIndividually(filesToPush);
|
|
37
|
-
// Log file parsing progress
|
|
38
|
-
for (const file of parsedFiles) {
|
|
39
|
-
const fileName = path.basename(file.filePath);
|
|
40
|
-
const doc = file.metadata.document;
|
|
41
|
-
console.log(` Reading ${fileName}...`);
|
|
42
|
-
// DOC-HEADER-11.4: parseFilesForPush mutates the metadata when it infers
|
|
43
|
-
// the prefix, so the flag it returns is the only record of the inference
|
|
44
|
-
if (doc?.defaultPrefix && file.inferredDefaultPrefix) {
|
|
45
|
-
console.log(` Inferred defaultPrefix "${doc.defaultPrefix}" from first requirement`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// SYNC-FAIL-4.1: every file in the push failed to parse — report each
|
|
49
|
-
// skipped file, then fail honestly instead of dry-running nothing
|
|
50
|
-
if (parsedFiles.length === 0) {
|
|
51
|
-
for (const failure of parseFailures) {
|
|
52
|
-
console.log(` ✗ Skipped ${path.basename(failure.filePath)}: ${failure.error}`);
|
|
53
|
-
}
|
|
54
|
-
console.log("\nNo valid documents to push.");
|
|
55
|
-
process.exitCode = 1;
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
console.log(`\nFound ${totalRequirements} requirement(s) in ${parsedFiles.length} document(s).\n`);
|
|
59
|
-
// Build credentials
|
|
60
|
-
const credentials = {
|
|
61
|
-
projectId,
|
|
62
|
-
projectSecret,
|
|
63
|
-
convexUrl: getConvexUrl(),
|
|
64
|
-
};
|
|
65
|
-
// Phase 1: Dry run
|
|
66
|
-
console.log("Validating documents...");
|
|
67
|
-
const dryRunResult = await dryRunPush(parsedFiles, credentials);
|
|
68
|
-
// Display unified summary
|
|
69
|
-
displayDryRunSummary(dryRunResult, parseFailures);
|
|
70
|
-
// Check if there's anything to push
|
|
71
|
-
const pushableCount = dryRunResult.updates.length +
|
|
72
|
-
dryRunResult.creates.length +
|
|
73
|
-
dryRunResult.notFound.length;
|
|
74
|
-
if (pushableCount === 0) {
|
|
75
|
-
console.log("No valid documents to push.");
|
|
76
|
-
// SYNC-FAIL-4.1: every file in the push was invalid — whether it failed
|
|
77
|
-
// local parse or dry-run validation — so the command must fail honestly,
|
|
78
|
-
// matching the MCP handler's merged rule
|
|
79
|
-
if (parseFailures.length > 0 || dryRunResult.invalid.length > 0) {
|
|
80
|
-
process.exitCode = 1;
|
|
81
|
-
}
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Single confirmation prompt
|
|
85
|
-
if (!options.yes) {
|
|
86
|
-
const hasConflicts = dryRunResult.conflicts.length > 0;
|
|
87
|
-
const prompt = hasConflicts
|
|
88
|
-
? "Continue with push? (will overwrite cloud changes) [y/N] "
|
|
89
|
-
: "Continue with push? [y/N] ";
|
|
90
|
-
const confirmed = await confirm(prompt);
|
|
91
|
-
if (!confirmed) {
|
|
92
|
-
console.log("Push cancelled.");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Phase 2: Execute push
|
|
97
|
-
console.log("\nPushing documents to cloud...");
|
|
98
|
-
const result = await executePush(dryRunResult, credentials);
|
|
99
|
-
// Display results
|
|
100
|
-
for (const { file, result: dryResult } of [
|
|
101
|
-
...dryRunResult.updates,
|
|
102
|
-
...dryRunResult.creates,
|
|
103
|
-
...dryRunResult.notFound,
|
|
104
|
-
]) {
|
|
105
|
-
const fileName = path.basename(file.filePath);
|
|
106
|
-
const isCreate = dryResult.action === "create" || dryResult.action === "not_found";
|
|
107
|
-
// Check if this file had an error — match by full path, since two pushed
|
|
108
|
-
// files can share a basename
|
|
109
|
-
const error = result.errors.find((e) => e.filePath === file.filePath);
|
|
110
|
-
if (error) {
|
|
111
|
-
console.log(` ✗ Failed: ${fileName} - ${error.error}`);
|
|
112
|
-
}
|
|
113
|
-
else if (isCreate) {
|
|
114
|
-
console.log(` ✓ Created: ${fileName} → ${file.metadata.document?.id}`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
console.log(` ✓ Updated: ${fileName}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// SYNC-FAIL-2.1: the cloud save succeeded but the local file couldn't be
|
|
121
|
-
// updated — name the file, say the cloud is fine, and give the recovery
|
|
122
|
-
// step that re-links the file instead of minting a duplicate on retry
|
|
123
|
-
if (result.writeBackWarnings?.length > 0) {
|
|
124
|
-
console.log();
|
|
125
|
-
for (const warning of result.writeBackWarnings) {
|
|
126
|
-
console.log(`⚠ ${warning.fileName}: saved to the cloud, but the local file could not be updated (${warning.error}). ` +
|
|
127
|
-
`To avoid creating a duplicate, add "id: ${warning.documentId}" under "document:" in the frontmatter of ${warning.filePath}, then push again.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// IMPORT-3: marker failures are loud but never fatal
|
|
131
|
-
if (result.importWarnings.length > 0) {
|
|
132
|
-
console.log();
|
|
133
|
-
for (const { fileName, status } of result.importWarnings) {
|
|
134
|
-
if (status === "already_used") {
|
|
135
|
-
console.log(`⚠ ${fileName}: this file's import marker was already used, so its requirements were saved as natively authored (they count toward your plan's requirement limit). Re-running codebase-to-spec produces a fresh marker.`);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
console.log(`⚠ ${fileName}: this file's import marker was not recognized — your CLI may need updating. Its requirements were saved as natively authored (they count toward your plan's requirement limit).`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// SYNC-FAIL-1: a failed push must not masquerade as success — agents and
|
|
143
|
-
// scripts drive this command and rely on the exit code to tell the truth
|
|
144
|
-
const failed = result.errors.length;
|
|
145
|
-
const syncedCount = result.created + result.updated;
|
|
146
|
-
if (failed > 0) {
|
|
147
|
-
process.exitCode = 1;
|
|
148
|
-
if (syncedCount === 0) {
|
|
149
|
-
console.log(`\n✗ Push failed: ${failed} document(s) could not be saved.`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
console.log(`\n⚠ Push incomplete: ${syncedCount} document(s) synced, ${failed} failed.`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
console.log(`\n✓ Push complete!`);
|
|
157
|
-
}
|
|
158
|
-
if (result.created > 0) {
|
|
159
|
-
console.log(` Created: ${result.created} document(s)`);
|
|
160
|
-
}
|
|
161
|
-
if (result.updated > 0) {
|
|
162
|
-
console.log(` Updated: ${result.updated} document(s)`);
|
|
163
|
-
}
|
|
164
|
-
if (failed === 0 && syncedCount === 0) {
|
|
165
|
-
console.log(" No documents were pushed.");
|
|
166
|
-
}
|
|
167
|
-
// SYNC-LAND-1: land the user on their spec, not at a dead end
|
|
168
|
-
if (result.synced.length > 0) {
|
|
169
|
-
console.log("\nReview in dot•requirements:");
|
|
170
|
-
for (const doc of result.synced) {
|
|
171
|
-
console.log(` ${doc.fileName} → ${doc.url}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Display the dry run summary.
|
|
177
|
-
*/
|
|
178
|
-
function displayDryRunSummary(dryRunResult, parseFailures = []) {
|
|
179
|
-
const { updates, creates, notFound, invalid, conflicts } = dryRunResult;
|
|
180
|
-
console.log("\n=== Push Summary ===\n");
|
|
181
|
-
if (updates.length > 0) {
|
|
182
|
-
console.log(`Updated documents (${updates.length}):`);
|
|
183
|
-
for (const { file, result } of updates) {
|
|
184
|
-
const fileName = path.basename(file.filePath);
|
|
185
|
-
const hasConflict = conflicts.some((c) => c.item.file === file);
|
|
186
|
-
const conflictNote = hasConflict ? " ⚠️ [cloud changed since pull]" : "";
|
|
187
|
-
const warningNote = result.warning ? ` (${result.warning})` : "";
|
|
188
|
-
console.log(` ~ ${fileName}${conflictNote}${warningNote}`);
|
|
189
|
-
}
|
|
190
|
-
console.log();
|
|
191
|
-
}
|
|
192
|
-
if (creates.length > 0) {
|
|
193
|
-
console.log(`New documents (${creates.length}):`);
|
|
194
|
-
for (const { file, result } of creates) {
|
|
195
|
-
const fileName = path.basename(file.filePath);
|
|
196
|
-
const warningNote = result.warning ? ` (${result.warning})` : "";
|
|
197
|
-
console.log(` + ${fileName}${warningNote}`);
|
|
198
|
-
}
|
|
199
|
-
console.log();
|
|
200
|
-
}
|
|
201
|
-
if (notFound.length > 0) {
|
|
202
|
-
console.log(`Documents not found in cloud (${notFound.length}) - will create new:`);
|
|
203
|
-
for (const { file, result } of notFound) {
|
|
204
|
-
const fileName = path.basename(file.filePath);
|
|
205
|
-
console.log(` ! ${fileName} (ID: ${result.documentId})`);
|
|
206
|
-
}
|
|
207
|
-
console.log();
|
|
208
|
-
}
|
|
209
|
-
// SYNC-FAIL-4.0: local parse failures surface alongside dry-run invalids,
|
|
210
|
-
// each named by file, while the rest of the batch proceeds
|
|
211
|
-
const skipped = [
|
|
212
|
-
...parseFailures.map((failure) => ({
|
|
213
|
-
fileName: path.basename(failure.filePath),
|
|
214
|
-
error: failure.error,
|
|
215
|
-
})),
|
|
216
|
-
...invalid.map(({ file, result }) => ({
|
|
217
|
-
fileName: path.basename(file.filePath),
|
|
218
|
-
error: result.error,
|
|
219
|
-
})),
|
|
220
|
-
];
|
|
221
|
-
if (skipped.length > 0) {
|
|
222
|
-
console.log(`Skipped - invalid files (${skipped.length}):`);
|
|
223
|
-
for (const { fileName, error } of skipped) {
|
|
224
|
-
console.log(` ✗ ${fileName}: ${error}`);
|
|
225
|
-
}
|
|
226
|
-
console.log();
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Prompt user for confirmation
|
|
231
|
-
*/
|
|
232
|
-
function confirm(question) {
|
|
233
|
-
const rl = readline.createInterface({
|
|
234
|
-
input: process.stdin,
|
|
235
|
-
output: process.stdout,
|
|
236
|
-
});
|
|
237
|
-
return new Promise((resolve) => {
|
|
238
|
-
rl.question(question, (answer) => {
|
|
239
|
-
rl.close();
|
|
240
|
-
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
//# sourceMappingURL=push.js.map
|