@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.
Files changed (65) hide show
  1. package/README.md +24 -20
  2. package/dist/cli.js +34 -12
  3. package/dist/codebase-to-spec/compose.d.ts +3 -2
  4. package/dist/codebase-to-spec/compose.js +3 -3
  5. package/dist/commands/aliases.d.ts +26 -0
  6. package/dist/commands/aliases.js +31 -0
  7. package/dist/commands/diff.d.ts +14 -0
  8. package/dist/commands/diff.js +62 -0
  9. package/dist/commands/init.js +5 -5
  10. package/dist/commands/link.d.ts +1 -1
  11. package/dist/commands/link.js +10 -7
  12. package/dist/commands/sync-common.d.ts +21 -0
  13. package/dist/commands/sync-common.js +24 -0
  14. package/dist/commands/sync.d.ts +26 -0
  15. package/dist/commands/sync.js +187 -0
  16. package/dist/convex.d.ts +1 -1
  17. package/dist/convex.js +2 -2
  18. package/dist/push/core.d.ts +18 -116
  19. package/dist/push/core.js +23 -267
  20. package/dist/push/index.d.ts +3 -3
  21. package/dist/push/index.js +4 -4
  22. package/dist/requirements/index.d.ts +2 -0
  23. package/dist/requirements/index.js +6 -1
  24. package/dist/requirements/style-guide.js +4 -4
  25. package/dist/schema/browser.d.ts +1 -0
  26. package/dist/schema/browser.js +3 -0
  27. package/dist/schema/index.d.ts +1 -0
  28. package/dist/schema/index.js +1 -0
  29. package/dist/schema/parser.d.ts +3 -0
  30. package/dist/schema/parser.js +1 -1
  31. package/dist/schema/schemas.d.ts +24 -22
  32. package/dist/schema/schemas.js +9 -12
  33. package/dist/schema/title-markdown.d.ts +30 -0
  34. package/dist/schema/title-markdown.js +38 -0
  35. package/dist/sync/compare.d.ts +21 -0
  36. package/dist/sync/compare.js +285 -0
  37. package/dist/sync/execute.d.ts +53 -0
  38. package/dist/sync/execute.js +225 -0
  39. package/dist/sync/index.d.ts +21 -0
  40. package/dist/sync/index.js +52 -0
  41. package/dist/sync/local-files.d.ts +26 -0
  42. package/dist/sync/local-files.js +93 -0
  43. package/dist/sync/plan.d.ts +39 -0
  44. package/dist/sync/plan.js +90 -0
  45. package/dist/sync/render.d.ts +17 -0
  46. package/dist/sync/render.js +46 -0
  47. package/dist/sync/segment.d.ts +40 -0
  48. package/dist/sync/segment.js +76 -0
  49. package/dist/sync/snapshot.d.ts +30 -0
  50. package/dist/sync/snapshot.js +122 -0
  51. package/dist/sync/types.d.ts +82 -0
  52. package/dist/sync/types.js +12 -0
  53. package/dist/templates/context-file-section.md +2 -1
  54. package/dist/templates/example-requirements.js +0 -2
  55. package/dist/templates/example-requirements.ts +0 -2
  56. package/dist/templates/requirements-readme.js +3 -4
  57. package/dist/templates/requirements-readme.ts +3 -4
  58. package/dist/templates/skills/codebase-to-spec/SKILL.md +2 -2
  59. package/dist/utils/project-settings.d.ts +8 -2
  60. package/dist/utils/project-settings.js +47 -24
  61. package/package.json +1 -1
  62. package/dist/commands/pull.d.ts +0 -8
  63. package/dist/commands/pull.js +0 -230
  64. package/dist/commands/push.d.ts +0 -6
  65. 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 push [file]` - Sync to cloud (shows diff preview, then confirms)
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)
@@ -10,8 +10,6 @@ export function generateExampleRequirements(projectId = "local") {
10
10
  projectId: ${projectId}
11
11
  pulledAt: ${now}
12
12
  version: 1
13
- document:
14
- title: "Example Requirements"
15
13
  ---
16
14
 
17
15
  # Example Requirements
@@ -13,8 +13,6 @@ export function generateExampleRequirements(
13
13
  projectId: ${projectId}
14
14
  pulledAt: ${now}
15
15
  version: 1
16
- document:
17
- title: "Example Requirements"
18
16
  ---
19
17
 
20
18
  # Example Requirements
@@ -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
- - Generated via \`dotrequirements pull\` from dot•requirements cloud
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 pull\` - Sync requirements from cloud
25
- - \`dotrequirements push\` - Push local requirements to cloud
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
- - Generated via \`dotrequirements pull\` from dot•requirements cloud
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 pull\` - Sync requirements from cloud
25
- - \`dotrequirements push\` - Push local requirements to cloud
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}} push --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 `sharePullCommand` 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.
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
- * Get project credentials, throwing helpful error if not available
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
- // AUTHZ-6.0 / 6.1: with the explicit flag, env credentials replace
174
- // file-based discovery entirely
175
- if (authFromEnv) {
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("--auth-from-env requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set.");
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
- const info = getProjectInfo(startDir);
183
- if (!info) {
184
- throw new Error('No dotrequirements project found. Run "dotrequirements init" to create one.');
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
- if (!info.credentials) {
187
- throw new Error('Project is not connected to cloud. Run "dotrequirements link" to connect.');
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
- return info.credentials;
212
+ throw new Error("Cloud features require authentication. Run 'dotrequirements link'.");
190
213
  }
191
214
  //# sourceMappingURL=project-settings.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.27.4",
3
+ "version": "0.29.0",
4
4
  "description": "Requirements tracking CLI and test harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +0,0 @@
1
- interface PullOptions {
2
- project?: string;
3
- document?: string;
4
- share?: string;
5
- }
6
- export declare function pullCommand(options: PullOptions): Promise<void>;
7
- export {};
8
- //# sourceMappingURL=pull.d.ts.map