@popoverai/dotrequirements 0.27.3 → 0.28.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/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 +13 -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 +182 -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 +16 -267
- package/dist/push/index.d.ts +3 -3
- package/dist/push/index.js +4 -4
- package/dist/requirements/style-guide.js +3 -3
- package/dist/schema/schemas.js +1 -1
- package/dist/sync/compare.d.ts +21 -0
- package/dist/sync/compare.js +285 -0
- package/dist/sync/execute.d.ts +48 -0
- package/dist/sync/execute.js +186 -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 +91 -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 +118 -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/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/oauth-flow.d.ts +2 -0
- package/dist/utils/oauth-flow.js +8 -1
- 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
|
@@ -6,8 +6,7 @@ export function generateRequirementsReadme() {
|
|
|
6
6
|
|
|
7
7
|
This directory contains requirements for the project. Requirements can be:
|
|
8
8
|
- Hand-authored locally in Markdown files
|
|
9
|
-
-
|
|
10
|
-
- Synced bidirectionally with \`dotrequirements push\`
|
|
9
|
+
- Synced bidirectionally with dot•requirements cloud via \`dotrequirements sync\`
|
|
11
10
|
|
|
12
11
|
## File Format
|
|
13
12
|
|
|
@@ -21,8 +20,8 @@ Requirements use the \`*.requirements.md\` naming pattern. See the [Requirements
|
|
|
21
20
|
|
|
22
21
|
## Commands
|
|
23
22
|
|
|
24
|
-
- \`dotrequirements
|
|
25
|
-
- \`dotrequirements
|
|
23
|
+
- \`dotrequirements sync\` - Reconcile requirements with the cloud (both directions)
|
|
24
|
+
- \`dotrequirements diff\` - Show how the repo and the cloud differ (read-only)
|
|
26
25
|
- \`dotrequirements validate\` - Validate requirement files
|
|
27
26
|
|
|
28
27
|
Learn more: https://dotrequirements.io
|
|
@@ -6,8 +6,7 @@ export function generateRequirementsReadme(): string {
|
|
|
6
6
|
|
|
7
7
|
This directory contains requirements for the project. Requirements can be:
|
|
8
8
|
- Hand-authored locally in Markdown files
|
|
9
|
-
-
|
|
10
|
-
- Synced bidirectionally with \`dotrequirements push\`
|
|
9
|
+
- Synced bidirectionally with dot•requirements cloud via \`dotrequirements sync\`
|
|
11
10
|
|
|
12
11
|
## File Format
|
|
13
12
|
|
|
@@ -21,8 +20,8 @@ Requirements use the \`*.requirements.md\` naming pattern. See the [Requirements
|
|
|
21
20
|
|
|
22
21
|
## Commands
|
|
23
22
|
|
|
24
|
-
- \`dotrequirements
|
|
25
|
-
- \`dotrequirements
|
|
23
|
+
- \`dotrequirements sync\` - Reconcile requirements with the cloud (both directions)
|
|
24
|
+
- \`dotrequirements diff\` - Show how the repo and the cloud differ (read-only)
|
|
26
25
|
- \`dotrequirements validate\` - Validate requirement files
|
|
27
26
|
|
|
28
27
|
Learn more: https://dotrequirements.io
|
|
@@ -92,8 +92,8 @@ The spec is written; this step is about what the user does with it. End the run
|
|
|
92
92
|
|
|
93
93
|
1. If no cloud credentials exist, run `{{DOTREQ_CLI}} link --yes --json` via Bash. The user completes login in the browser; everything else is yours.
|
|
94
94
|
2. If link exits with code 2, its JSON is a `decision_needed` — multiple teams, existing projects, or a plan at its project limit. Relay the options conversationally (they are the user's choice, not yours), then retry link with the chosen option's `retryFlag` appended.
|
|
95
|
-
3. Once linked, run `{{DOTREQ_CLI}}
|
|
96
|
-
4. Hand over the share mechanics from link's JSON, matched to the teammate's surface: the `inviteUrl` for a teammate who will review in the web platform, and the `
|
|
95
|
+
3. Once linked, run `{{DOTREQ_CLI}} sync --repo-contributes --yes`. Present each document's web URL from the output as the place the user's team reviews it.
|
|
96
|
+
4. Hand over the share mechanics from link's JSON, matched to the teammate's surface: the `inviteUrl` for a teammate who will review in the web platform, and the `shareSyncCommand` for a teammate working in their IDE. If link omitted one (e.g. the user isn't a team admin), hand over what's there without apology.
|
|
97
97
|
5. With the spec in the cloud, offer to set up the assistant you are running as to work from the spec going forward — framed as that outcome ("I can work from this spec in future sessions"), not as configuration. On acceptance, run `{{DOTREQ_CLI}} ai-setup --assistant <id>` with the identifier for this assistant (e.g. `claude-code`).
|
|
98
98
|
|
|
99
99
|
If the user declines the team-review offer, note that it stands for later and don't repeat the pitch.
|
package/dist/utils/oauth-flow.js
CHANGED
|
@@ -66,12 +66,19 @@ export async function executeOAuthFlow() {
|
|
|
66
66
|
if (!result.success) {
|
|
67
67
|
throw new Error(`Token exchange failed: ${result.error}`);
|
|
68
68
|
}
|
|
69
|
-
const { accessToken, refreshToken, expiresAt } = result;
|
|
69
|
+
const { accessToken, refreshToken, expiresAt, email } = result;
|
|
70
70
|
console.log("✓ Authentication complete!");
|
|
71
|
+
// LINK-14 / INIT-20: name the account we signed in as. The browser session
|
|
72
|
+
// decides which identity authenticates; surfacing it here (one site shared by
|
|
73
|
+
// link, init, and the invite flow) makes a wrong-account login visible before
|
|
74
|
+
// anything is saved. In JSON mode the callers route console.log to stderr, so
|
|
75
|
+
// this line never corrupts a machine-readable result.
|
|
76
|
+
console.log(`Signed in as ${email}`);
|
|
71
77
|
return {
|
|
72
78
|
accessToken,
|
|
73
79
|
refreshToken,
|
|
74
80
|
expiresAt,
|
|
81
|
+
email,
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
84
|
/**
|
|
@@ -41,7 +41,6 @@ export interface ProjectInfo {
|
|
|
41
41
|
* Returns undefined if either is missing.
|
|
42
42
|
*/
|
|
43
43
|
export declare function getCredentialsFromEnv(): ProjectSettings | undefined;
|
|
44
|
-
export declare function setAuthFromEnv(enabled: boolean): void;
|
|
45
44
|
/**
|
|
46
45
|
* Find the project root by walking up from startDir looking for .requirements/ folder
|
|
47
46
|
* Returns the directory containing .requirements/, or undefined if not found
|
|
@@ -66,8 +65,15 @@ export declare function writeProjectSettings(projectRoot: string, settings: Proj
|
|
|
66
65
|
* Returns undefined if no project found
|
|
67
66
|
*/
|
|
68
67
|
export declare function getProjectInfo(startDir?: string): ProjectInfo | undefined;
|
|
68
|
+
export declare function setPreferEnvCredentials(enabled: boolean): void;
|
|
69
69
|
/**
|
|
70
|
-
* Get project credentials, throwing helpful error if not available
|
|
70
|
+
* Get project credentials, throwing a helpful error if not available.
|
|
71
|
+
*
|
|
72
|
+
* AUTHZ-6: the settings file wins when present; the environment is the fallback
|
|
73
|
+
* when no settings file is on disk. A local checkout's binding is never
|
|
74
|
+
* overridden by an ambient environment (AUTHZ-6.0), while a CI checkout with no
|
|
75
|
+
* settings file authenticates from DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET
|
|
76
|
+
* (AUTHZ-6.1) — no flag required.
|
|
71
77
|
*/
|
|
72
78
|
export declare function getProjectCredentials(startDir?: string): ProjectSettings;
|
|
73
79
|
//# sourceMappingURL=project-settings.d.ts.map
|
|
@@ -18,19 +18,6 @@ export function getCredentialsFromEnv() {
|
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* AUTHZ-6: explicit CI/CD credential injection. Set by the global
|
|
23
|
-
* --auth-from-env flag (cli.ts preAction hook); when enabled,
|
|
24
|
-
* getProjectCredentials reads DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET
|
|
25
|
-
* instead of file-based discovery. Without the flag those variables are
|
|
26
|
-
* ignored entirely — the explicit opt-in prevents credential conflicts
|
|
27
|
-
* between local and CI environments. (Previously the retired MCP server's
|
|
28
|
-
* --auth-from-env; ported to the CLI with identical semantics.)
|
|
29
|
-
*/
|
|
30
|
-
let authFromEnv = false;
|
|
31
|
-
export function setAuthFromEnv(enabled) {
|
|
32
|
-
authFromEnv = enabled;
|
|
33
|
-
}
|
|
34
21
|
/**
|
|
35
22
|
* Find the project root by walking up from startDir looking for .requirements/ folder
|
|
36
23
|
* Returns the directory containing .requirements/, or undefined if not found
|
|
@@ -167,25 +154,61 @@ export function getProjectInfo(startDir = process.cwd()) {
|
|
|
167
154
|
};
|
|
168
155
|
}
|
|
169
156
|
/**
|
|
170
|
-
*
|
|
157
|
+
* SYNC-ALIAS-1.3: the retired `--auth-from-env` global flag survives as a
|
|
158
|
+
* deprecated alias for the commands already in users' CI (the doc-site taught
|
|
159
|
+
* `dotreq --auth-from-env push`). When set, environment credentials replace
|
|
160
|
+
* file discovery entirely — the flag's original semantics — so an old
|
|
161
|
+
* invocation behaves identically.
|
|
162
|
+
*/
|
|
163
|
+
let preferEnvCredentials = false;
|
|
164
|
+
export function setPreferEnvCredentials(enabled) {
|
|
165
|
+
preferEnvCredentials = enabled;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get project credentials, throwing a helpful error if not available.
|
|
169
|
+
*
|
|
170
|
+
* AUTHZ-6: the settings file wins when present; the environment is the fallback
|
|
171
|
+
* when no settings file is on disk. A local checkout's binding is never
|
|
172
|
+
* overridden by an ambient environment (AUTHZ-6.0), while a CI checkout with no
|
|
173
|
+
* settings file authenticates from DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET
|
|
174
|
+
* (AUTHZ-6.1) — no flag required.
|
|
171
175
|
*/
|
|
172
176
|
export function getProjectCredentials(startDir = process.cwd()) {
|
|
173
|
-
//
|
|
174
|
-
// file
|
|
175
|
-
if (
|
|
177
|
+
// SYNC-ALIAS-1.3: deprecated --auth-from-env keeps its original semantics —
|
|
178
|
+
// env replaces file discovery entirely.
|
|
179
|
+
if (preferEnvCredentials) {
|
|
176
180
|
const envCredentials = getCredentialsFromEnv();
|
|
177
181
|
if (!envCredentials) {
|
|
178
|
-
throw new Error("
|
|
182
|
+
throw new Error("Both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET must be set to authenticate from the environment.");
|
|
179
183
|
}
|
|
180
184
|
return envCredentials;
|
|
181
185
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
// File wins: a present, valid settings file is authoritative. readProjectSettings
|
|
187
|
+
// throws on a present-but-invalid file, surfacing it rather than silently
|
|
188
|
+
// falling through to the environment.
|
|
189
|
+
const root = findProjectRoot(startDir);
|
|
190
|
+
if (root) {
|
|
191
|
+
const settings = readProjectSettings(root);
|
|
192
|
+
if (settings)
|
|
193
|
+
return settings;
|
|
194
|
+
}
|
|
195
|
+
// No settings file on disk — fall back to the environment (AUTHZ-6.1).
|
|
196
|
+
const envCredentials = getCredentialsFromEnv();
|
|
197
|
+
if (envCredentials) {
|
|
198
|
+
// AUTHZ-6.1: announce the fallback, on stderr so it never pollutes stdout
|
|
199
|
+
// that a caller may be parsing.
|
|
200
|
+
process.stderr.write("Authenticating from the DOTREQ_PROJECT_ID / DOTREQ_PROJECT_SECRET environment variables.\n");
|
|
201
|
+
return envCredentials;
|
|
185
202
|
}
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
// AUTHZ-6.2: exactly one variable set is a misconfiguration, not a silent
|
|
204
|
+
// no-op — name both.
|
|
205
|
+
if (process.env.DOTREQ_PROJECT_ID || process.env.DOTREQ_PROJECT_SECRET) {
|
|
206
|
+
throw new Error("Both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET must be set to authenticate from the environment.");
|
|
207
|
+
}
|
|
208
|
+
// AUTHZ-6.3 / AUTHZ-2: neither a settings file nor environment credentials.
|
|
209
|
+
if (!root) {
|
|
210
|
+
throw new Error('No dotrequirements project found. Run "dotrequirements init" to create one.');
|
|
188
211
|
}
|
|
189
|
-
|
|
212
|
+
throw new Error("Cloud features require authentication. Run 'dotrequirements link'.");
|
|
190
213
|
}
|
|
191
214
|
//# sourceMappingURL=project-settings.js.map
|
package/package.json
CHANGED
package/dist/commands/pull.d.ts
DELETED
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
|