@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment a document body into an ordered sequence of units — prose passages
|
|
3
|
+
* and requirement blocks, interleaved (the comparator's structural input).
|
|
4
|
+
*
|
|
5
|
+
* A requirement's identity is its key; a prose passage's identity is its
|
|
6
|
+
* normalized text (prose has no stable id, so a reworded passage reads as a
|
|
7
|
+
* removal-plus-addition — a conflict, per DIFF-4.3).
|
|
8
|
+
*/
|
|
9
|
+
import { getAllRequirements, parseRequirementBlock } from "../schema/index.js";
|
|
10
|
+
/** Matches a fenced dotrequirements block; group 1 is its inner content. */
|
|
11
|
+
const FENCE = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
12
|
+
/**
|
|
13
|
+
* Normalize prose for comparison: LF line endings, trailing whitespace
|
|
14
|
+
* stripped per line, runs of blank lines collapsed, ends trimmed. Two prose
|
|
15
|
+
* passages that differ only in incidental whitespace compare equal, so a
|
|
16
|
+
* whitespace-only reflow does not read as a conflict.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeProse(text) {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/\r\n/g, "\n")
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map((line) => line.replace(/\s+$/, ""))
|
|
23
|
+
.join("\n")
|
|
24
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Split a body into its ordered units. Prose between (and around) fences
|
|
29
|
+
* becomes prose units when non-empty after normalization; each fence becomes a
|
|
30
|
+
* requirement unit keyed by its first-line KEY.
|
|
31
|
+
*/
|
|
32
|
+
export function segmentBody(body) {
|
|
33
|
+
const normalized = body.replace(/\r\n/g, "\n");
|
|
34
|
+
const units = [];
|
|
35
|
+
let lastIndex = 0;
|
|
36
|
+
FENCE.lastIndex = 0;
|
|
37
|
+
let match = FENCE.exec(normalized);
|
|
38
|
+
while (match !== null) {
|
|
39
|
+
const prose = normalizeProse(normalized.slice(lastIndex, match.index));
|
|
40
|
+
if (prose)
|
|
41
|
+
units.push({ kind: "prose", text: prose });
|
|
42
|
+
const blockContent = match[1].trim();
|
|
43
|
+
const keyMatch = blockContent.match(/^([\w-]+):/);
|
|
44
|
+
// A block whose first line isn't KEY: is malformed; parsing rejects it
|
|
45
|
+
// upstream, so segmentation never sees it in the compare path. Fall back to
|
|
46
|
+
// the leading text as a key so segmentation stays total regardless.
|
|
47
|
+
const key = (keyMatch ? keyMatch[1] : blockContent.slice(0, 24)).toUpperCase();
|
|
48
|
+
units.push({ kind: "requirement", key, blockContent });
|
|
49
|
+
lastIndex = FENCE.lastIndex;
|
|
50
|
+
match = FENCE.exec(normalized);
|
|
51
|
+
}
|
|
52
|
+
const tail = normalizeProse(normalized.slice(lastIndex));
|
|
53
|
+
if (tail)
|
|
54
|
+
units.push({ kind: "prose", text: tail });
|
|
55
|
+
return units;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Flatten a requirement block into a map from position-path id (e.g. "AUTH-1",
|
|
59
|
+
* "AUTH-1.0", "AUTH-1.2.1") to a canonical label+content string.
|
|
60
|
+
*
|
|
61
|
+
* Because the id encodes the position, a criterion inserted mid-list shifts the
|
|
62
|
+
* ids of everything after it — so an insertion cannot subset-match the original
|
|
63
|
+
* (DIFF-4.2), while an appended criterion adds a new id without disturbing the
|
|
64
|
+
* others (DIFF-4.1).
|
|
65
|
+
*/
|
|
66
|
+
export function requirementUnitMap(blockContent) {
|
|
67
|
+
const key = blockContent.match(/^([\w-]+):/)?.[1] ?? "";
|
|
68
|
+
const tree = parseRequirementBlock(key, blockContent);
|
|
69
|
+
const map = new Map();
|
|
70
|
+
for (const node of getAllRequirements([tree])) {
|
|
71
|
+
// A NUL (\u0000) separates the label from content so a label change reads as a change.
|
|
72
|
+
map.set(node.id.toUpperCase(), `${node.label}\u0000${node.content}`);
|
|
73
|
+
}
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=segment.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot acquisition for the comparator — kept separate from comparison so
|
|
3
|
+
* the comparator stays a pure function (compare.ts). The local half reads the
|
|
4
|
+
* workspace; the cloud half is one exportProjectForCli query.
|
|
5
|
+
*/
|
|
6
|
+
import type { CloudSnapshot, LocalDocument, LocalSnapshot } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Read and classify one requirements file into a LocalDocument. A file whose
|
|
9
|
+
* body has a structural error (duplicate key/position, orphaned position) is
|
|
10
|
+
* returned with `parseError` set — the comparator reports it as invalid_file
|
|
11
|
+
* (SYNC-FAIL-4) — while its frontmatter title, if readable, is kept for display.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readLocalDocument(filePath: string): LocalDocument;
|
|
14
|
+
/**
|
|
15
|
+
* Build the local snapshot from the workspace, or from an explicit set of file
|
|
16
|
+
* paths (scope). Files are always read fresh from disk.
|
|
17
|
+
*/
|
|
18
|
+
export declare function acquireLocalSnapshot(workspaceRoot: string, filePaths?: string[]): Promise<LocalSnapshot>;
|
|
19
|
+
export interface CloudAuth {
|
|
20
|
+
/** projectSlug + projectSecret for full access, or a bare share token. */
|
|
21
|
+
projectSlug?: string;
|
|
22
|
+
projectSecret: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the cloud snapshot with one exportProjectForCli query. A share token is
|
|
26
|
+
* passed as the projectSecret with no slug (the read-only wrapper resolves the
|
|
27
|
+
* project from the token) — the read-only path never writes back.
|
|
28
|
+
*/
|
|
29
|
+
export declare function acquireCloudSnapshot(auth: CloudAuth): Promise<CloudSnapshot>;
|
|
30
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot acquisition for the comparator — kept separate from comparison so
|
|
3
|
+
* the comparator stays a pure function (compare.ts). The local half reads the
|
|
4
|
+
* workspace; the cloud half is one exportProjectForCli query.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
import { getConvexUrl } from "../config.js";
|
|
10
|
+
import { api } from "../convex.js";
|
|
11
|
+
import { findRequirementsFiles } from "../requirements/index.js";
|
|
12
|
+
import { splitLeadingH1, validateMetadata } from "../schema/index.js";
|
|
13
|
+
import { parseRequirementBlocksFromMarkdown, splitFrontmatter, stripFrontmatterBlock, } from "../schema/parser-core.js";
|
|
14
|
+
/**
|
|
15
|
+
* Read and classify one requirements file into a LocalDocument. A file whose
|
|
16
|
+
* body has a structural error (duplicate key/position, orphaned position) is
|
|
17
|
+
* returned with `parseError` set — the comparator reports it as invalid_file
|
|
18
|
+
* (SYNC-FAIL-4) — while its frontmatter title, if readable, is kept for display.
|
|
19
|
+
*/
|
|
20
|
+
export function readLocalDocument(filePath) {
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
filePath,
|
|
28
|
+
parseError: `Could not read file: ${err.message}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Frontmatter first, so a body-parse failure still yields id/title.
|
|
32
|
+
let documentId;
|
|
33
|
+
let title;
|
|
34
|
+
let defaultPrefix;
|
|
35
|
+
let frontmatterError;
|
|
36
|
+
const split = splitFrontmatter(raw);
|
|
37
|
+
if (split) {
|
|
38
|
+
// Pull id/title from the raw YAML before schema validation: an invalid
|
|
39
|
+
// frontmatter must not cost the file its identity, or its cloud
|
|
40
|
+
// counterpart re-pairs as "only_in_repo" and a sync mints a duplicate
|
|
41
|
+
// cloud document.
|
|
42
|
+
let rawMeta;
|
|
43
|
+
try {
|
|
44
|
+
rawMeta = YAML.parse(split.yaml);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
frontmatterError = `Invalid YAML frontmatter: ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
|
|
50
|
+
const rawDoc = rawMeta.document;
|
|
51
|
+
if (rawDoc && typeof rawDoc === "object" && !Array.isArray(rawDoc)) {
|
|
52
|
+
const d = rawDoc;
|
|
53
|
+
if (typeof d.id === "string")
|
|
54
|
+
documentId = d.id;
|
|
55
|
+
if (typeof d.title === "string")
|
|
56
|
+
title = d.title;
|
|
57
|
+
if (typeof d.defaultPrefix === "string")
|
|
58
|
+
defaultPrefix = d.defaultPrefix;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (rawMeta !== undefined && !frontmatterError) {
|
|
62
|
+
try {
|
|
63
|
+
validateMetadata(rawMeta);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Schema-invalid frontmatter flags the file (invalid_file) instead of
|
|
67
|
+
// proceeding with half-metadata — it must never sync as if unlinked.
|
|
68
|
+
frontmatterError = `Invalid frontmatter: ${err.message}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (frontmatterError) {
|
|
73
|
+
return { filePath, documentId, title, parseError: frontmatterError };
|
|
74
|
+
}
|
|
75
|
+
const body = stripFrontmatterBlock(raw);
|
|
76
|
+
try {
|
|
77
|
+
parseRequirementBlocksFromMarkdown(body);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { filePath, documentId, title, parseError: err.message };
|
|
81
|
+
}
|
|
82
|
+
// SYNC-TITLE-1: the body's leading H1 is the document's title. A legacy
|
|
83
|
+
// frontmatter title stands only when the body has none (SYNC-TITLE-1.1);
|
|
84
|
+
// with neither, the document is untitled (SYNC-TITLE-1.4).
|
|
85
|
+
const derivedTitle = splitLeadingH1(body).title ?? title;
|
|
86
|
+
return { filePath, documentId, title: derivedTitle, defaultPrefix, body };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the local snapshot from the workspace, or from an explicit set of file
|
|
90
|
+
* paths (scope). Files are always read fresh from disk.
|
|
91
|
+
*/
|
|
92
|
+
export async function acquireLocalSnapshot(workspaceRoot, filePaths) {
|
|
93
|
+
const paths = filePaths ?? (await findRequirementsFiles(workspaceRoot));
|
|
94
|
+
return { documents: paths.map(readLocalDocument) };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the cloud snapshot with one exportProjectForCli query. A share token is
|
|
98
|
+
* passed as the projectSecret with no slug (the read-only wrapper resolves the
|
|
99
|
+
* project from the token) — the read-only path never writes back.
|
|
100
|
+
*/
|
|
101
|
+
export async function acquireCloudSnapshot(auth) {
|
|
102
|
+
const client = new ConvexHttpClient(getConvexUrl());
|
|
103
|
+
const projectAuth = auth.projectSlug
|
|
104
|
+
? { projectSlug: auth.projectSlug, projectSecret: auth.projectSecret }
|
|
105
|
+
: { projectSecret: auth.projectSecret };
|
|
106
|
+
const result = (await client.query(api.documents.queries.exportProjectForCli, {
|
|
107
|
+
projectAuth,
|
|
108
|
+
}));
|
|
109
|
+
return {
|
|
110
|
+
projectName: result.projectName,
|
|
111
|
+
projectSlug: result.projectSlug,
|
|
112
|
+
documents: result.documents.map((d) => ({
|
|
113
|
+
documentId: d.documentId,
|
|
114
|
+
title: d.title,
|
|
115
|
+
defaultPrefix: d.defaultPrefix,
|
|
116
|
+
body: d.markdownContent,
|
|
117
|
+
version: d.version,
|
|
118
|
+
updatedAt: d.updatedAt,
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for the repo/cloud comparator (DIFF-*, SYNC-*).
|
|
3
|
+
*
|
|
4
|
+
* One vocabulary, no translation layer: the verdict identifiers here are the
|
|
5
|
+
* words the CLI prints. `verdictLabel` only reshapes an identifier into its
|
|
6
|
+
* spaced form ("additions_in_repo" -> "additions in repo"); it never renames.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Per-document verdict from comparing the repo and the cloud (DIFF-1.0).
|
|
10
|
+
*/
|
|
11
|
+
export type Verdict = "in_sync" | "additions_in_repo" | "additions_in_cloud" | "conflict" | "only_in_repo" | "only_in_cloud" | "invalid_file";
|
|
12
|
+
/** The user-facing spelling of a verdict — same words, spaced. */
|
|
13
|
+
export declare function verdictLabel(verdict: Verdict): string;
|
|
14
|
+
/**
|
|
15
|
+
* One document as it exists locally: a `.requirements.md` file. When the file
|
|
16
|
+
* cannot be parsed, `parseError` is set and the other content fields are absent
|
|
17
|
+
* — the comparator reports it as `invalid_file` (SYNC-FAIL-4).
|
|
18
|
+
*/
|
|
19
|
+
export interface LocalDocument {
|
|
20
|
+
filePath: string;
|
|
21
|
+
/** document.id from frontmatter, when the file is linked to a cloud document. */
|
|
22
|
+
documentId?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
defaultPrefix?: string;
|
|
25
|
+
/** Frontmatter-stripped markdown body (the comparison unit, SYNC-ARCH-1). */
|
|
26
|
+
body?: string;
|
|
27
|
+
parseError?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One published document as it exists in the cloud, from exportProjectForCli.
|
|
31
|
+
*/
|
|
32
|
+
export interface CloudDocument {
|
|
33
|
+
documentId: string;
|
|
34
|
+
title: string;
|
|
35
|
+
defaultPrefix?: string;
|
|
36
|
+
/** Published markdownContent (SYNC-ARCH-1). */
|
|
37
|
+
body: string;
|
|
38
|
+
version: number;
|
|
39
|
+
updatedAt: number;
|
|
40
|
+
}
|
|
41
|
+
export interface LocalSnapshot {
|
|
42
|
+
documents: LocalDocument[];
|
|
43
|
+
}
|
|
44
|
+
export interface CloudSnapshot {
|
|
45
|
+
projectName: string;
|
|
46
|
+
projectSlug: string;
|
|
47
|
+
documents: CloudDocument[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A single point of difference within a paired document, for verbose
|
|
51
|
+
* document-scope diff (DIFF-2). `verdict` reuses the document vocabulary at
|
|
52
|
+
* unit granularity — the verdicts are fractal.
|
|
53
|
+
*/
|
|
54
|
+
export interface UnitDetail {
|
|
55
|
+
/** Requirement key, or a short prose descriptor. */
|
|
56
|
+
unit: string;
|
|
57
|
+
verdict: "additions_in_repo" | "additions_in_cloud" | "conflict";
|
|
58
|
+
/** Criterion positions new on one side, when the difference is criterion-level. */
|
|
59
|
+
positions?: string[];
|
|
60
|
+
note?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The comparison of one document across the two surfaces. Identity for
|
|
64
|
+
* scoping/output comes from documentId / filePath / title directly.
|
|
65
|
+
*/
|
|
66
|
+
export interface DocumentComparison {
|
|
67
|
+
verdict: Verdict;
|
|
68
|
+
/** File path when the document exists locally. */
|
|
69
|
+
filePath?: string;
|
|
70
|
+
/** Cloud document id when the document exists in the cloud. */
|
|
71
|
+
documentId?: string;
|
|
72
|
+
/** Display title (from whichever side has the document). */
|
|
73
|
+
title: string;
|
|
74
|
+
/** Parse error message when verdict is invalid_file. */
|
|
75
|
+
error?: string;
|
|
76
|
+
/** Unit-level breakdown, populated for verbose diff of non-in-sync documents. */
|
|
77
|
+
details?: UnitDetail[];
|
|
78
|
+
}
|
|
79
|
+
export interface ComparisonResult {
|
|
80
|
+
documents: DocumentComparison[];
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for the repo/cloud comparator (DIFF-*, SYNC-*).
|
|
3
|
+
*
|
|
4
|
+
* One vocabulary, no translation layer: the verdict identifiers here are the
|
|
5
|
+
* words the CLI prints. `verdictLabel` only reshapes an identifier into its
|
|
6
|
+
* spaced form ("additions_in_repo" -> "additions in repo"); it never renames.
|
|
7
|
+
*/
|
|
8
|
+
/** The user-facing spelling of a verdict — same words, spaced. */
|
|
9
|
+
export function verdictLabel(verdict) {
|
|
10
|
+
return verdict.replace(/_/g, " ");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -57,7 +57,8 @@ test(requirement('REQ-ID.0'), () => { /* test specific criterion */ });
|
|
|
57
57
|
**Authoring:**
|
|
58
58
|
- `dotreq create-requirement-document [path]` - Print the template and style guide
|
|
59
59
|
- `dotreq validate [glob]` - Check syntax (works offline)
|
|
60
|
-
- `dotreq
|
|
60
|
+
- `dotreq diff [scope...]` - Show how the repo and the cloud differ (read-only)
|
|
61
|
+
- `dotreq sync [scope...]` - Reconcile the repo and the cloud (both contribute by default)
|
|
61
62
|
|
|
62
63
|
**Review (judgment runs in the dispatched subagent):**
|
|
63
64
|
- `dotreq style-check <file>` - Emits the style guide + content for the reviewer to judge (`--source cloud` for hosted review)
|
|
@@ -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.
|
|
@@ -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