@popoverai/dotrequirements 0.27.4 → 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.
Files changed (50) hide show
  1. package/README.md +24 -20
  2. package/dist/cli.js +34 -12
  3. package/dist/commands/aliases.d.ts +26 -0
  4. package/dist/commands/aliases.js +31 -0
  5. package/dist/commands/diff.d.ts +14 -0
  6. package/dist/commands/diff.js +62 -0
  7. package/dist/commands/init.js +5 -5
  8. package/dist/commands/link.d.ts +1 -1
  9. package/dist/commands/link.js +10 -7
  10. package/dist/commands/sync-common.d.ts +21 -0
  11. package/dist/commands/sync-common.js +24 -0
  12. package/dist/commands/sync.d.ts +26 -0
  13. package/dist/commands/sync.js +182 -0
  14. package/dist/convex.d.ts +1 -1
  15. package/dist/convex.js +2 -2
  16. package/dist/push/core.d.ts +18 -116
  17. package/dist/push/core.js +16 -267
  18. package/dist/push/index.d.ts +3 -3
  19. package/dist/push/index.js +4 -4
  20. package/dist/requirements/style-guide.js +3 -3
  21. package/dist/schema/schemas.js +1 -1
  22. package/dist/sync/compare.d.ts +21 -0
  23. package/dist/sync/compare.js +285 -0
  24. package/dist/sync/execute.d.ts +48 -0
  25. package/dist/sync/execute.js +186 -0
  26. package/dist/sync/index.d.ts +21 -0
  27. package/dist/sync/index.js +52 -0
  28. package/dist/sync/local-files.d.ts +26 -0
  29. package/dist/sync/local-files.js +91 -0
  30. package/dist/sync/plan.d.ts +39 -0
  31. package/dist/sync/plan.js +90 -0
  32. package/dist/sync/render.d.ts +17 -0
  33. package/dist/sync/render.js +46 -0
  34. package/dist/sync/segment.d.ts +40 -0
  35. package/dist/sync/segment.js +76 -0
  36. package/dist/sync/snapshot.d.ts +30 -0
  37. package/dist/sync/snapshot.js +118 -0
  38. package/dist/sync/types.d.ts +82 -0
  39. package/dist/sync/types.js +12 -0
  40. package/dist/templates/context-file-section.md +2 -1
  41. package/dist/templates/requirements-readme.js +3 -4
  42. package/dist/templates/requirements-readme.ts +3 -4
  43. package/dist/templates/skills/codebase-to-spec/SKILL.md +2 -2
  44. package/dist/utils/project-settings.d.ts +8 -2
  45. package/dist/utils/project-settings.js +47 -24
  46. package/package.json +1 -1
  47. package/dist/commands/pull.d.ts +0 -8
  48. package/dist/commands/pull.js +0 -230
  49. package/dist/commands/push.d.ts +0 -6
  50. package/dist/commands/push.js +0 -244
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `dotreq sync` — reconcile the repo and the cloud under a direction/authority
3
+ * mode (SYNC-MODE-*). The daily gesture is the unmarked form; flags narrow
4
+ * direction (`--cloud-contributes` / `--repo-contributes`) or assign authority
5
+ * (`--repo-wins` / `--cloud-wins`).
6
+ */
7
+ import * as path from "node:path";
8
+ import * as readline from "node:readline";
9
+ import { executePlan } from "../sync/execute.js";
10
+ import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateLocalDocumentIds, filterByScope, resolutionHint, } from "../sync/index.js";
11
+ import { buildPlan, resolveMode, } from "../sync/plan.js";
12
+ import { brand } from "../utils/brand.js";
13
+ import { resolveCloudAuth } from "./sync-common.js";
14
+ /**
15
+ * The CLI entry point: exits non-zero on unresolved trouble (SYNC-MODE-1.5,
16
+ * SYNC-FAIL-1/4). `init`/`link` call `syncCommand` directly instead — their
17
+ * success is linking/onboarding, and a conflict in one spec file must not fail
18
+ * the whole command (the report lines still print).
19
+ */
20
+ export async function syncCliAction(scope, options) {
21
+ const result = await syncCommand(scope, options);
22
+ if (result === "trouble") {
23
+ process.exitCode = 1;
24
+ }
25
+ }
26
+ export async function syncCommand(scope, options) {
27
+ const workspaceRoot = process.cwd();
28
+ // SHARE-TOKEN-CLI-1.3: a read-only token can only bring requirements down.
29
+ let mode;
30
+ if (options.share) {
31
+ mode = "cloud_contributes";
32
+ if (options.repoWins || options.cloudWins || options.repoContributes) {
33
+ console.log("A share token is read-only — syncing the cloud's requirements down only; other mode flags are ignored.");
34
+ }
35
+ }
36
+ else {
37
+ mode = resolveMode(options);
38
+ }
39
+ const auth = resolveCloudAuth(options);
40
+ const [local, cloud] = await Promise.all([
41
+ acquireLocalSnapshot(workspaceRoot),
42
+ acquireCloudSnapshot(auth),
43
+ ]);
44
+ // SYNC-MODE-4.4 (and 4.4.0 for --yes): an empty repo is indistinguishable from a wrong working
45
+ // directory or a failed checkout — never treat it as intent to delete every
46
+ // cloud document. --yes does not override: it consents to listed deletions,
47
+ // not to this. (Cloud deletion has no recovery path; the mirror-image case
48
+ // — cloud-wins deleting local files — is recoverable via git.)
49
+ if (mode === "repo_wins" &&
50
+ local.documents.length === 0 &&
51
+ cloud.documents.length > 0) {
52
+ throw new Error(`Refusing to sync --repo-wins: no requirement documents found under ${workspaceRoot}, ` +
53
+ `but the cloud project has ${cloud.documents.length}. An empty repo usually means a wrong ` +
54
+ `working directory or a failed checkout, and proceeding would permanently delete every ` +
55
+ `cloud document. If the repo's requirements really are gone on purpose, delete the cloud ` +
56
+ `documents from the web app instead.`);
57
+ }
58
+ // SHARE-TOKEN-CLI-1.2: show whose project this is.
59
+ if (options.share) {
60
+ console.log(`Project: "${cloud.projectName}"`);
61
+ }
62
+ // SYNC-FAIL-3: two files claiming one document id would both write it — abort
63
+ // before any write, naming both files and the shared id.
64
+ const dupes = duplicateLocalDocumentIds(local);
65
+ if (dupes.length > 0) {
66
+ const d = dupes[0];
67
+ throw new Error(`Two files claim the same document ID "${d.documentId}":\n` +
68
+ d.filePaths.map((p) => ` ${p}`).join("\n") +
69
+ `\nEach file must map to its own cloud document. Remove the "id:" line from the copy's frontmatter, then sync again.`);
70
+ }
71
+ const comparison = compareSnapshots(local, cloud);
72
+ const { documents, unmatched } = filterByScope(comparison, scope, workspaceRoot);
73
+ for (const token of unmatched) {
74
+ console.log(`No document matched scope "${token}".`);
75
+ }
76
+ const plan = buildPlan(documents, mode);
77
+ // SYNC-MODE-4: name every deletion before writing anything.
78
+ const deletions = plan.filter((p) => p.action === "cloud_delete" || p.action === "local_delete");
79
+ if (deletions.length > 0) {
80
+ // SYNC-MODE-4.3: a non-interactive run cannot consent — refuse rather than
81
+ // hanging on a prompt that will never answer (or worse, deleting silently).
82
+ if (!options.yes && !process.stdin.isTTY) {
83
+ throw new Error(`This sync would delete ${deletions.length} document(s), and there is no terminal to confirm on. ` +
84
+ `Re-run with --yes to consent to the deletions listed by \`dotreq diff\`.`);
85
+ }
86
+ if (!(await confirmDeletions(deletions, options.yes))) {
87
+ console.log("Sync cancelled.");
88
+ return "cancelled";
89
+ }
90
+ }
91
+ if (plan.every((p) => p.action === "skip")) {
92
+ console.log(`✓ Repo and ${brand} cloud are already in sync.`);
93
+ return "clean";
94
+ }
95
+ console.log(`\nSyncing with ${brand} cloud...`);
96
+ const outcome = await executePlan(plan, cloud, auth, workspaceRoot);
97
+ // Report.
98
+ if (outcome.cloudCreated)
99
+ console.log(` Created in cloud: ${outcome.cloudCreated}`);
100
+ if (outcome.cloudUpdated)
101
+ console.log(` Updated in cloud: ${outcome.cloudUpdated}`);
102
+ if (outcome.cloudDeleted)
103
+ console.log(` Deleted from cloud: ${outcome.cloudDeleted}`);
104
+ if (outcome.localWritten)
105
+ console.log(` Written locally: ${outcome.localWritten}`);
106
+ if (outcome.localDeleted)
107
+ console.log(` Deleted locally: ${outcome.localDeleted}`);
108
+ for (const conflict of outcome.conflicts) {
109
+ console.log(` ! conflict: ${path.basename(conflict.filePath ?? conflict.title)} — ${resolutionHint(conflict)}`);
110
+ }
111
+ for (const invalid of outcome.invalid) {
112
+ console.log(` ✗ invalid file: ${path.basename(invalid.filePath ?? invalid.title)}${invalid.error ? ` — ${invalid.error}` : ""}`);
113
+ }
114
+ for (const err of outcome.errors) {
115
+ console.log(` ✗ failed: ${err.name} — ${err.error}`);
116
+ }
117
+ // IMPORT-3: marker failures are loud but never fatal.
118
+ for (const { name, status } of outcome.importWarnings) {
119
+ if (status === "already_used") {
120
+ console.log(` ⚠ ${name}: 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.`);
121
+ }
122
+ else {
123
+ console.log(` ⚠ ${name}: 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).`);
124
+ }
125
+ }
126
+ // SYNC-FAIL-2.1: saved to cloud, but the local file couldn't be updated —
127
+ // give the re-link recovery step so a retry doesn't mint a duplicate.
128
+ for (const w of outcome.writeBackWarnings) {
129
+ console.log(` ⚠ ${w.name}: saved to the cloud, but the local file could not be updated (${w.error}). ` +
130
+ `To avoid creating a duplicate, add "id: ${w.documentId}" under "document:" in ${w.filePath}, then sync again.`);
131
+ }
132
+ // SYNC-LAND-1: land the user on their synced documents.
133
+ if (outcome.synced.length > 0) {
134
+ console.log(`\nReview in ${brand}:`);
135
+ for (const s of outcome.synced)
136
+ console.log(` ${s.name} → ${s.url}`);
137
+ }
138
+ const hadTrouble = outcome.conflicts.length > 0 ||
139
+ outcome.invalid.length > 0 ||
140
+ outcome.errors.length > 0;
141
+ if (hadTrouble) {
142
+ console.log("\n⚠ Sync completed with unresolved items (see above).");
143
+ }
144
+ else {
145
+ console.log("\n✓ Sync complete.");
146
+ }
147
+ // SHARE-TOKEN-CLI-3: point the onboarding user at full access.
148
+ if (options.share) {
149
+ console.log("\n─────────────────────────────────────────────────────────");
150
+ console.log("Next steps to get full access (sync to cloud, coverage):");
151
+ console.log(" Run: dotrequirements link");
152
+ console.log("─────────────────────────────────────────────────────────");
153
+ }
154
+ return hadTrouble ? "trouble" : "clean";
155
+ }
156
+ async function confirmDeletions(deletions, skip) {
157
+ const cloudDeletes = deletions.filter((d) => d.action === "cloud_delete");
158
+ const localDeletes = deletions.filter((d) => d.action === "local_delete");
159
+ console.log("\nThis will PERMANENTLY delete:");
160
+ for (const d of cloudDeletes) {
161
+ console.log(` from cloud: ${d.doc.title}`);
162
+ }
163
+ for (const d of localDeletes) {
164
+ console.log(` from disk: ${d.doc.filePath ? path.basename(d.doc.filePath) : d.doc.title}`);
165
+ }
166
+ if (skip) {
167
+ console.log("(--yes: proceeding without confirmation)");
168
+ return true;
169
+ }
170
+ const rl = readline.createInterface({
171
+ input: process.stdin,
172
+ output: process.stdout,
173
+ });
174
+ return new Promise((resolve) => {
175
+ rl.question("\nContinue? [y/N] ", (answer) => {
176
+ rl.close();
177
+ const a = answer.trim().toLowerCase();
178
+ resolve(a === "y" || a === "yes");
179
+ });
180
+ });
181
+ }
182
+ //# sourceMappingURL=sync.js.map
package/dist/convex.d.ts CHANGED
@@ -20,11 +20,11 @@ export declare const api: {
20
20
  exportForCli: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
21
21
  exportProjectForCli: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
22
22
  checkDocumentsExist: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
23
- getDocumentsMetadata: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
24
23
  };
25
24
  mutations: {
26
25
  create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
27
26
  update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
27
+ deleteForCli: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
28
28
  };
29
29
  saveWithRequirements: {
30
30
  saveWithRequirements: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
package/dist/convex.js CHANGED
@@ -24,12 +24,12 @@ export const api = {
24
24
  exportForCli: query("documents/queries:exportForCli"),
25
25
  exportProjectForCli: query("documents/queries:exportProjectForCli"),
26
26
  checkDocumentsExist: query("documents/queries:checkDocumentsExist"),
27
- // SYNC-CLI-EDIT-2: Get document metadata for conflict detection
28
- getDocumentsMetadata: query("documents/queries:getDocumentsMetadata"),
29
27
  },
30
28
  mutations: {
31
29
  create: mutation("documents/mutations:create"),
32
30
  update: mutation("documents/mutations:update"),
31
+ // SYNC-MODE-3: delete a cloud document from the CLI (repo-wins)
32
+ deleteForCli: mutation("documents/mutations:deleteForCli"),
33
33
  },
34
34
  // SYNC-ARCH-1: Save document with requirements derivation
35
35
  saveWithRequirements: {
@@ -1,22 +1,12 @@
1
1
  /**
2
- * Shared push logic for CLI and MCP.
3
- *
4
- * This module contains the core push functionality that both the CLI push command
5
- * and MCP push_requirements tool use. It handles:
6
- * - Parsing local files
7
- * - Dry run validation with Convex
8
- * - Conflict detection
9
- * - Executing the push
10
- * - Updating local files with document IDs and timestamps
2
+ * Shared parse-and-write-back primitives for syncing local requirements files
3
+ * to the cloud. The comparator-driven sync path (`packages/cli/src/sync/`)
4
+ * consumes these; the former dry-run/execute push pipeline was retired with the
5
+ * `push` command (its behavior now lives in the sync executor).
11
6
  */
12
7
  import type { Metadata } from "../schema/index.js";
13
- export interface PushCredentials {
14
- projectId: string;
15
- projectSecret: string;
16
- convexUrl: string;
17
- }
18
8
  /**
19
- * Parsed file ready for push.
9
+ * Parsed file ready to sync.
20
10
  * SYNC-ARCH-1: Documents are the sync unit, not individual requirements.
21
11
  */
22
12
  export interface ParsedFile {
@@ -28,111 +18,31 @@ export interface ParsedFile {
28
18
  requirementCount: number;
29
19
  /**
30
20
  * DOC-HEADER-14: the frontmatter exactly as the user wrote it, including
31
- * keys the schema doesn't recognize — merged back on the post-push rewrite
21
+ * keys the schema doesn't recognize — merged back on the post-sync rewrite
32
22
  * so those keys survive.
33
23
  */
34
24
  rawFrontmatter?: Record<string, unknown>;
35
25
  /** DOC-HEADER-11.4: true when defaultPrefix was inferred from the first requirement rather than read from frontmatter */
36
26
  inferredDefaultPrefix?: boolean;
37
27
  }
38
- /**
39
- * Cloud document metadata for conflict detection.
40
- * SYNC-CLI-EDIT-2: Compare pulledAt with cloud updatedAt.
41
- */
42
- export interface CloudDocumentMetadata {
43
- documentId: string;
44
- version: number;
45
- updatedAt: number;
46
- }
47
- /**
48
- * Dry run result from saveWithRequirements.
49
- */
50
- export interface DryRunResultItem {
51
- dryRun: true;
52
- action: "create" | "update" | "not_found" | "invalid";
53
- documentId?: string;
54
- title?: string;
55
- warning?: string;
56
- error?: string;
57
- }
58
- /**
59
- * File with its dry run result.
60
- */
61
- export interface FileWithDryRun {
62
- file: ParsedFile;
63
- result: DryRunResultItem;
64
- }
65
- /**
66
- * Conflict information for a file.
67
- */
68
- export interface ConflictInfo {
69
- item: FileWithDryRun;
70
- cloudMeta: CloudDocumentMetadata;
71
- }
72
- /**
73
- * Result of the dry run phase.
74
- */
75
- export interface DryRunResult {
76
- updates: FileWithDryRun[];
77
- creates: FileWithDryRun[];
78
- notFound: FileWithDryRun[];
79
- invalid: FileWithDryRun[];
80
- conflicts: ConflictInfo[];
81
- totalRequirements: number;
82
- }
83
- /**
84
- * Outcome of run-marker handling for one pushed file (IMPORT-2/3).
85
- * Mirrors the server's ImportStatus.
86
- */
87
- export type ImportStatus = "imported" | "invalid_marker" | "unsupported_version" | "already_used" | "ignored_update" | "none";
88
28
  /** Base URL of the web app, where synced documents are reviewed. */
89
29
  export declare const WEB_APP_URL = "https://app.dotrequirements.io";
90
- /**
91
- * Result of the execute phase.
92
- */
93
- export interface PushResult {
94
- created: number;
95
- updated: number;
96
- /** filePath disambiguates when two pushed files share a basename */
97
- errors: Array<{
98
- fileName: string;
99
- filePath: string;
100
- error: string;
101
- }>;
102
- /** IMPORT-3: marker failures that must be surfaced to the user, per file */
103
- importWarnings: Array<{
104
- fileName: string;
105
- status: Extract<ImportStatus, "invalid_marker" | "unsupported_version" | "already_used">;
106
- }>;
107
- /** SYNC-LAND-1: each successfully synced document, with its web URL */
108
- synced: Array<{
109
- fileName: string;
110
- documentId: string;
111
- url: string;
112
- }>;
113
- /**
114
- * SYNC-FAIL-2: cloud save succeeded but the local file write-back failed.
115
- * These documents are synced (counted in created/updated and listed in
116
- * `synced`) — the warning carries what the user needs to re-link the file
117
- * without creating a duplicate.
118
- */
119
- writeBackWarnings: Array<{
120
- fileName: string;
121
- filePath: string;
122
- documentId: string;
123
- error: string;
124
- }>;
125
- }
126
30
  /**
127
31
  * Extract markdown content from a file, stripping YAML frontmatter.
128
32
  */
129
33
  export declare function extractMarkdownContent(rawContent: string): string;
130
34
  /**
131
- * Parse files for push. Returns parsed files with metadata and content.
35
+ * DOC-HEADER-14: merge the validated (and sync-updated) metadata over the raw
36
+ * frontmatter so unrecognized keys survive the rewrite while the fields the
37
+ * sync owns (document ID, pulledAt, defaultPrefix, version) stay updated.
38
+ */
39
+ export declare function mergeMetadataWithRawFrontmatter(metadata: Metadata, rawFrontmatter: Record<string, unknown> | undefined): Metadata;
40
+ /**
41
+ * Parse files for sync. Returns parsed files with metadata and content.
132
42
  *
133
43
  * Throws (via `parseRequirementsFromFile`) if any file is syntactically
134
44
  * invalid. Callers that need one bad file not to abort the batch should parse
135
- * files individually and collect failures (see the MCP push handler, #45).
45
+ * files individually and collect failures.
136
46
  */
137
47
  export declare function parseFilesForPush(filePaths: string[]): {
138
48
  parsedFiles: ParsedFile[];
@@ -145,11 +55,11 @@ export interface ParseFailure {
145
55
  /**
146
56
  * SYNC-FAIL-4.0: parse files one at a time so a single invalid file cannot
147
57
  * abort the batch — its failure is collected per file while the valid files
148
- * still parse. Shared by the CLI push command and the MCP push handler (#45)
149
- * so their isolation semantics cannot drift.
58
+ * still parse.
150
59
  *
151
60
  * Note: cross-file checks that need the whole batch (e.g. SYNC-FAIL-3
152
- * duplicate document IDs) are enforced downstream in dryRunPush, not here.
61
+ * duplicate document IDs) are enforced by the sync path against the local
62
+ * snapshot, not here.
153
63
  */
154
64
  export declare function parseFilesForPushIndividually(filePaths: string[]): {
155
65
  parsedFiles: ParsedFile[];
@@ -157,17 +67,9 @@ export declare function parseFilesForPushIndividually(filePaths: string[]): {
157
67
  parseFailures: ParseFailure[];
158
68
  };
159
69
  /**
160
- * SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both push
70
+ * SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both sync
161
71
  * as updates to one cloud document — last writer wins and the first spec is
162
72
  * silently destroyed. Abort instead, naming both files and the shared id.
163
73
  */
164
74
  export declare function assertNoDuplicateDocumentIds(parsedFiles: ParsedFile[]): void;
165
- /**
166
- * Execute dry run phase: validate all files against Convex and detect conflicts.
167
- */
168
- export declare function dryRunPush(parsedFiles: ParsedFile[], credentials: PushCredentials): Promise<DryRunResult>;
169
- /**
170
- * Execute the push: save all pushable files to Convex and update local files.
171
- */
172
- export declare function executePush(dryRunResult: DryRunResult, credentials: PushCredentials): Promise<PushResult>;
173
75
  //# sourceMappingURL=core.d.ts.map