@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
@@ -106,8 +106,10 @@ export function buildRequirementKey(prefix, number) {
106
106
  // Metadata Schema
107
107
  // =============================================================================
108
108
  /**
109
- * Metadata block at the top of every requirements file.
110
- * Only document.title is required for push; everything else is optional or auto-filled.
109
+ * Metadata block at the top of every requirements file. Everything is
110
+ * optional or auto-filled; the document's title lives in the BODY as its
111
+ * leading H1 (SYNC-TITLE-1), with a legacy frontmatter title tolerated as a
112
+ * fallback for files from before that convention.
111
113
  */
112
114
  export const MetadataSchema = z.object({
113
115
  version: z.number().optional(), // Informational - incremented on push
@@ -116,7 +118,7 @@ export const MetadataSchema = z.object({
116
118
  document: z
117
119
  .object({
118
120
  id: z.string().optional(), // Cloud document link - filled by push when creating
119
- title: z.string(), // Required for push
121
+ title: z.string().optional(), // Legacy fallback — the body's leading H1 is the title
120
122
  defaultPrefix: z.string().optional(), // Editor hint for new requirement keys
121
123
  })
122
124
  .optional(),
@@ -136,18 +138,13 @@ export const CONVEX_ID_PATTERN = /^j[a-z0-9]{31}$/;
136
138
  * SYNC-CLI-EDIT-1: Files with valid document.id update existing documents
137
139
  */
138
140
  export function validateForPush(metadata) {
139
- // Must have document section
141
+ // Must have document section. (A title is NOT required — untitled
142
+ // documents are allowed, and the title is derived from the markdown's
143
+ // leading H1 anyway; see docs/working/h1-as-title.md.)
140
144
  if (!metadata.document) {
141
145
  return {
142
146
  valid: false,
143
- reason: "Missing document section. Add a document section with a title to push this file.",
144
- };
145
- }
146
- // Must have title
147
- if (!metadata.document.title || metadata.document.title.trim() === "") {
148
- return {
149
- valid: false,
150
- reason: "Document section missing required title.",
147
+ reason: "Missing document section. Add a document section to sync this file.",
151
148
  };
152
149
  }
153
150
  // Check document.id
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The document title and the markdown's leading H1 are the same thing
3
+ * (DOC-TITLE-3). These helpers hold that contract at the web app's storage
4
+ * boundary: markdown coming *in* from Convex is split into title + body, and
5
+ * markdown going *out* is recomposed — so inside the app, `Content.markdown`
6
+ * is always the body (no leading H1) and `Content.title` carries the title.
7
+ */
8
+ export interface SplitMarkdown {
9
+ /** The leading H1's text, or null when the markdown has no leading H1. */
10
+ title: string | null;
11
+ /** Everything after the leading H1 (or the whole input when there is none). */
12
+ body: string;
13
+ }
14
+ /**
15
+ * Split a leading ATX H1 (`# Title`) off the markdown.
16
+ *
17
+ * "Leading" means the first non-blank line (DOC-TITLE-3.0); an H1 further
18
+ * down is ordinary content and stays in the body (DOC-TITLE-3.4). Markdown
19
+ * allows up to 3 spaces of heading indentation — 4+ is a code block, so it
20
+ * is not a title.
21
+ */
22
+ export declare function splitLeadingH1(markdown: string): SplitMarkdown;
23
+ /**
24
+ * Compose full-document markdown from a title and a body.
25
+ *
26
+ * The title becomes the leading H1 (DOC-TITLE-3.1); an untitled document
27
+ * gets no empty heading line (DOC-TITLE-3.3).
28
+ */
29
+ export declare function composeMarkdownWithTitle(title: string, body: string): string;
30
+ //# sourceMappingURL=title-markdown.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The document title and the markdown's leading H1 are the same thing
3
+ * (DOC-TITLE-3). These helpers hold that contract at the web app's storage
4
+ * boundary: markdown coming *in* from Convex is split into title + body, and
5
+ * markdown going *out* is recomposed — so inside the app, `Content.markdown`
6
+ * is always the body (no leading H1) and `Content.title` carries the title.
7
+ */
8
+ /**
9
+ * Split a leading ATX H1 (`# Title`) off the markdown.
10
+ *
11
+ * "Leading" means the first non-blank line (DOC-TITLE-3.0); an H1 further
12
+ * down is ordinary content and stays in the body (DOC-TITLE-3.4). Markdown
13
+ * allows up to 3 spaces of heading indentation — 4+ is a code block, so it
14
+ * is not a title.
15
+ */
16
+ export function splitLeadingH1(markdown) {
17
+ const match = /^(?:[ \t]*\r?\n)* {0,3}# (.*)(?:\r?\n|$)/.exec(markdown);
18
+ if (!match) {
19
+ return { title: null, body: markdown };
20
+ }
21
+ const body = markdown.slice(match[0].length).replace(/^(\s*\r?\n)+/, "");
22
+ return { title: match[1].trim(), body };
23
+ }
24
+ /**
25
+ * Compose full-document markdown from a title and a body.
26
+ *
27
+ * The title becomes the leading H1 (DOC-TITLE-3.1); an untitled document
28
+ * gets no empty heading line (DOC-TITLE-3.3).
29
+ */
30
+ export function composeMarkdownWithTitle(title, body) {
31
+ const trimmedTitle = title.trim();
32
+ if (!trimmedTitle)
33
+ return body;
34
+ if (!body.trim())
35
+ return `# ${trimmedTitle}\n`;
36
+ return `# ${trimmedTitle}\n\n${body}`;
37
+ }
38
+ //# sourceMappingURL=title-markdown.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The repo/cloud comparator (DIFF-*). A pure function of two snapshots: it
3
+ * reads no disk and makes no network calls. `dotreq diff` renders its result,
4
+ * `dotreq sync` acts on it, CI asserts on it.
5
+ */
6
+ import type { CloudSnapshot, ComparisonResult, LocalSnapshot } from "./types.js";
7
+ /**
8
+ * Two local files carrying the same document id would both pair to one cloud
9
+ * document and both try to write it — last writer wins, silently destroying the
10
+ * first (SYNC-FAIL-3). Return the offending ids with their file paths so the
11
+ * caller can abort before any write.
12
+ */
13
+ export declare function duplicateLocalDocumentIds(local: LocalSnapshot): Array<{
14
+ documentId: string;
15
+ filePaths: string[];
16
+ }>;
17
+ /**
18
+ * Compare the whole repo against the whole cloud, pairing documents by id.
19
+ */
20
+ export declare function compareSnapshots(local: LocalSnapshot, cloud: CloudSnapshot): ComparisonResult;
21
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1,285 @@
1
+ /**
2
+ * The repo/cloud comparator (DIFF-*). A pure function of two snapshots: it
3
+ * reads no disk and makes no network calls. `dotreq diff` renders its result,
4
+ * `dotreq sync` acts on it, CI asserts on it.
5
+ */
6
+ import { normalizeProse, requirementUnitMap, segmentBody, } from "./segment.js";
7
+ /** Whitespace-only normalization for the fast-path body equality check. */
8
+ function normalizeBody(body) {
9
+ return normalizeProse(body);
10
+ }
11
+ /** Do the single-valued document attributes diverge? A changed title or prefix
12
+ * is never an "addition" (both sides always have one), so it forces a conflict.
13
+ * A prefix present on one side only is treated as no divergence — omission is
14
+ * filled by inference (DOC-HEADER-11), not a genuine disagreement. */
15
+ function attributesDiverge(local, cloud) {
16
+ if ((local.title ?? "") !== cloud.title)
17
+ return true;
18
+ if (local.defaultPrefix &&
19
+ cloud.defaultPrefix &&
20
+ local.defaultPrefix !== cloud.defaultPrefix) {
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+ /** Compare two requirement blocks by their position-path unit maps. */
26
+ function compareRequirementBlocks(repoBlock, cloudBlock) {
27
+ let repoMap;
28
+ let cloudMap;
29
+ try {
30
+ repoMap = requirementUnitMap(repoBlock);
31
+ cloudMap = requirementUnitMap(cloudBlock);
32
+ }
33
+ catch {
34
+ // A block that won't parse here can't be classified — treat as diverged so
35
+ // it flags rather than silently syncing. (Both sides are pre-validated in
36
+ // practice, so this is a guard, not a live path.)
37
+ return {
38
+ result: "diverged",
39
+ repoOnlyPositions: [],
40
+ cloudOnlyPositions: [],
41
+ };
42
+ }
43
+ const cloudSubsetOfRepo = [...cloudMap].every(([id, v]) => repoMap.get(id) === v);
44
+ const repoSubsetOfCloud = [...repoMap].every(([id, v]) => cloudMap.get(id) === v);
45
+ const toPosition = (id) => id.replace(/^[^.]*/, "").replace(/^\./, "") || "(root)";
46
+ const repoOnlyPositions = [...repoMap.keys()]
47
+ .filter((id) => repoMap.get(id) !== cloudMap.get(id))
48
+ .map(toPosition);
49
+ const cloudOnlyPositions = [...cloudMap.keys()]
50
+ .filter((id) => cloudMap.get(id) !== repoMap.get(id))
51
+ .map(toPosition);
52
+ let result;
53
+ if (cloudSubsetOfRepo && repoSubsetOfCloud)
54
+ result = "equal";
55
+ else if (cloudSubsetOfRepo)
56
+ result = "repo_extends";
57
+ else if (repoSubsetOfCloud)
58
+ result = "cloud_extends";
59
+ else
60
+ result = "diverged";
61
+ return { result, repoOnlyPositions, cloudOnlyPositions };
62
+ }
63
+ /** Classify a paired document's bodies structurally (DIFF-4). */
64
+ function classifyBodies(repoBody, cloudBody) {
65
+ const repoUnits = segmentBody(repoBody);
66
+ const cloudUnits = segmentBody(cloudBody);
67
+ const details = [];
68
+ // Requirements match by key; index each side.
69
+ const repoReqs = new Map();
70
+ const cloudReqs = new Map();
71
+ for (const u of repoUnits)
72
+ if (u.kind === "requirement")
73
+ repoReqs.set(u.key, u.blockContent);
74
+ for (const u of cloudUnits)
75
+ if (u.kind === "requirement")
76
+ cloudReqs.set(u.key, u.blockContent);
77
+ let anyRepoExtra = false; // repo has something cloud lacks
78
+ let anyCloudExtra = false; // cloud has something repo lacks
79
+ // Matched requirement pairs.
80
+ for (const [key, repoBlock] of repoReqs) {
81
+ const cloudBlock = cloudReqs.get(key);
82
+ if (cloudBlock === undefined) {
83
+ anyRepoExtra = true;
84
+ details.push({ unit: key, verdict: "additions_in_repo" });
85
+ continue;
86
+ }
87
+ const { result, repoOnlyPositions, cloudOnlyPositions } = compareRequirementBlocks(repoBlock, cloudBlock);
88
+ if (result === "repo_extends") {
89
+ anyRepoExtra = true;
90
+ details.push({
91
+ unit: key,
92
+ verdict: "additions_in_repo",
93
+ positions: repoOnlyPositions,
94
+ });
95
+ }
96
+ else if (result === "cloud_extends") {
97
+ anyCloudExtra = true;
98
+ details.push({
99
+ unit: key,
100
+ verdict: "additions_in_cloud",
101
+ positions: cloudOnlyPositions,
102
+ });
103
+ }
104
+ else if (result === "diverged") {
105
+ anyRepoExtra = true;
106
+ anyCloudExtra = true;
107
+ details.push({
108
+ unit: key,
109
+ verdict: "conflict",
110
+ positions: [...new Set([...repoOnlyPositions, ...cloudOnlyPositions])],
111
+ });
112
+ }
113
+ }
114
+ // Cloud-only requirement keys.
115
+ for (const key of cloudReqs.keys()) {
116
+ if (!repoReqs.has(key)) {
117
+ anyCloudExtra = true;
118
+ details.push({ unit: key, verdict: "additions_in_cloud" });
119
+ }
120
+ }
121
+ // Prose: match by exact normalized text (multiset). Unmatched prose on either
122
+ // side is extra material — a reworded passage is unmatched on both.
123
+ const cloudProse = cloudUnits.filter((u) => u.kind === "prose");
124
+ const cloudProseUsed = new Array(cloudProse.length).fill(false);
125
+ for (const u of repoUnits) {
126
+ if (u.kind !== "prose")
127
+ continue;
128
+ const idx = cloudProse.findIndex((c, i) => !cloudProseUsed[i] && c.text === u.text);
129
+ if (idx === -1) {
130
+ anyRepoExtra = true;
131
+ details.push({
132
+ unit: proseLabel(u.text),
133
+ verdict: "additions_in_repo",
134
+ note: "prose",
135
+ });
136
+ }
137
+ else {
138
+ cloudProseUsed[idx] = true;
139
+ }
140
+ }
141
+ cloudProse.forEach((c, i) => {
142
+ if (!cloudProseUsed[i]) {
143
+ anyCloudExtra = true;
144
+ details.push({
145
+ unit: proseLabel(c.text),
146
+ verdict: "additions_in_cloud",
147
+ note: "prose",
148
+ });
149
+ }
150
+ });
151
+ // Order of the units common to both sides must agree; a reorder is a conflict.
152
+ const orderPreserved = matchedOrderAgrees(repoUnits, cloudUnits);
153
+ if (!orderPreserved) {
154
+ anyRepoExtra = true;
155
+ anyCloudExtra = true;
156
+ }
157
+ let verdict;
158
+ if (!anyRepoExtra && !anyCloudExtra)
159
+ verdict = "in_sync";
160
+ else if (anyRepoExtra && !anyCloudExtra)
161
+ verdict = "additions_in_repo";
162
+ else if (anyCloudExtra && !anyRepoExtra)
163
+ verdict = "additions_in_cloud";
164
+ else
165
+ verdict = "conflict";
166
+ return { verdict, details };
167
+ }
168
+ /** The subsequence of unit identities present on both sides must appear in the
169
+ * same relative order. Identity: requirement key, or prose text. */
170
+ function matchedOrderAgrees(repoUnits, cloudUnits) {
171
+ const identity = (u) => u.kind === "requirement" ? `R:${u.key}` : `P:${u.text}`;
172
+ const repoIds = repoUnits.map(identity);
173
+ const cloudIds = cloudUnits.map(identity);
174
+ const cloudIdSet = new Set(cloudIds);
175
+ const common = new Set(repoIds.filter((id) => cloudIdSet.has(id)));
176
+ const repoSeq = repoIds.filter((id) => common.has(id));
177
+ const cloudSeq = cloudIds.filter((id) => common.has(id));
178
+ return (repoSeq.length === cloudSeq.length &&
179
+ repoSeq.every((id, i) => id === cloudSeq[i]));
180
+ }
181
+ function proseLabel(text) {
182
+ const firstLine = text.split("\n")[0].trim();
183
+ return firstLine.length > 40
184
+ ? `${firstLine.slice(0, 40)}…`
185
+ : firstLine || "(prose)";
186
+ }
187
+ /** Compare one document across surfaces. */
188
+ function compareDocument(local, cloud) {
189
+ const base = {
190
+ filePath: local.filePath,
191
+ documentId: cloud.documentId,
192
+ title: cloud.title,
193
+ verdict: "in_sync",
194
+ };
195
+ const diverges = attributesDiverge(local, cloud);
196
+ const body = local.body ?? "";
197
+ // Fast path: whitespace-identical bodies with matching attributes.
198
+ if (!diverges && normalizeBody(body) === normalizeBody(cloud.body)) {
199
+ return { ...base, verdict: "in_sync" };
200
+ }
201
+ const { verdict: bodyVerdict, details } = classifyBodies(body, cloud.body);
202
+ let verdict;
203
+ if (diverges) {
204
+ // A changed single-valued attribute is never additive → conflict.
205
+ verdict = "conflict";
206
+ }
207
+ else {
208
+ verdict = bodyVerdict;
209
+ }
210
+ return {
211
+ ...base,
212
+ verdict,
213
+ details: verdict === "in_sync" ? undefined : details,
214
+ };
215
+ }
216
+ /**
217
+ * Two local files carrying the same document id would both pair to one cloud
218
+ * document and both try to write it — last writer wins, silently destroying the
219
+ * first (SYNC-FAIL-3). Return the offending ids with their file paths so the
220
+ * caller can abort before any write.
221
+ */
222
+ export function duplicateLocalDocumentIds(local) {
223
+ const byId = new Map();
224
+ for (const doc of local.documents) {
225
+ if (!doc.documentId)
226
+ continue;
227
+ const paths = byId.get(doc.documentId) ?? [];
228
+ paths.push(doc.filePath);
229
+ byId.set(doc.documentId, paths);
230
+ }
231
+ return [...byId.entries()]
232
+ .filter(([, paths]) => paths.length > 1)
233
+ .map(([documentId, filePaths]) => ({ documentId, filePaths }));
234
+ }
235
+ /**
236
+ * Compare the whole repo against the whole cloud, pairing documents by id.
237
+ */
238
+ export function compareSnapshots(local, cloud) {
239
+ const cloudById = new Map(cloud.documents.map((d) => [d.documentId, d]));
240
+ const matchedCloudIds = new Set();
241
+ const documents = [];
242
+ for (const doc of local.documents) {
243
+ if (doc.parseError) {
244
+ // An invalid file that still names its document id claims the cloud
245
+ // counterpart: without this, the cloud doc would re-emit as
246
+ // "only_in_cloud" and a sync would write a duplicate local file beside
247
+ // the broken one (then hard-abort every later run on the duplicate id).
248
+ if (doc.documentId)
249
+ matchedCloudIds.add(doc.documentId);
250
+ documents.push({
251
+ filePath: doc.filePath,
252
+ documentId: doc.documentId,
253
+ title: doc.title ?? doc.filePath,
254
+ verdict: "invalid_file",
255
+ error: doc.parseError,
256
+ });
257
+ continue;
258
+ }
259
+ const cloudDoc = doc.documentId ? cloudById.get(doc.documentId) : undefined;
260
+ if (!cloudDoc) {
261
+ // No id, or an id the cloud doesn't have (deleted in cloud, or a stale
262
+ // link) — the document lives only in the repo (SYNC-WEB-DELETE-1.0).
263
+ documents.push({
264
+ filePath: doc.filePath,
265
+ documentId: doc.documentId,
266
+ title: doc.title ?? doc.filePath,
267
+ verdict: "only_in_repo",
268
+ });
269
+ continue;
270
+ }
271
+ matchedCloudIds.add(cloudDoc.documentId);
272
+ documents.push(compareDocument(doc, cloudDoc));
273
+ }
274
+ for (const cloudDoc of cloud.documents) {
275
+ if (!matchedCloudIds.has(cloudDoc.documentId)) {
276
+ documents.push({
277
+ documentId: cloudDoc.documentId,
278
+ title: cloudDoc.title,
279
+ verdict: "only_in_cloud",
280
+ });
281
+ }
282
+ }
283
+ return { documents };
284
+ }
285
+ //# sourceMappingURL=compare.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Executing a sync plan: cloud writes/deletes and local writes/deletes.
3
+ *
4
+ * Cloud writes reuse the push machinery (parse → saveWithRequirements →
5
+ * frontmatter write-back) so document-header/import-provenance fidelity is
6
+ * identical to a repo→cloud push. Local writes reuse local-files.ts.
7
+ */
8
+ import type { PlannedAction } from "./plan.js";
9
+ import type { CloudAuth } from "./snapshot.js";
10
+ import type { CloudSnapshot, DocumentComparison } from "./types.js";
11
+ export interface SyncOutcome {
12
+ cloudCreated: number;
13
+ cloudUpdated: number;
14
+ cloudDeleted: number;
15
+ localWritten: number;
16
+ localDeleted: number;
17
+ conflicts: DocumentComparison[];
18
+ invalid: DocumentComparison[];
19
+ errors: Array<{
20
+ name: string;
21
+ error: string;
22
+ }>;
23
+ /** Synced-to-cloud documents with their web URLs (SYNC-LAND-1). */
24
+ synced: Array<{
25
+ name: string;
26
+ url: string;
27
+ }>;
28
+ /** SYNC-FAIL-2: cloud save succeeded but the local write-back failed. The
29
+ * document is synced; the warning carries the re-link recovery step. */
30
+ writeBackWarnings: Array<{
31
+ name: string;
32
+ filePath: string;
33
+ documentId: string;
34
+ error: string;
35
+ }>;
36
+ /** IMPORT-3: run-marker failures — loud, never fatal, never silent. */
37
+ importWarnings: Array<{
38
+ name: string;
39
+ status: string;
40
+ }>;
41
+ /** SYNC-TITLE-1.3: pushes that changed a document's title. */
42
+ renames: Array<{
43
+ from: string;
44
+ to: string;
45
+ }>;
46
+ }
47
+ /**
48
+ * Execute a plan. `cloud` provides bodies for local writes (keyed by id). Cloud
49
+ * writes require a project slug in `auth`; a read-only share run has none and
50
+ * its plan carries no cloud-writing actions.
51
+ */
52
+ export declare function executePlan(plan: PlannedAction[], cloud: CloudSnapshot, auth: CloudAuth, workspaceRoot: string): Promise<SyncOutcome>;
53
+ //# sourceMappingURL=execute.d.ts.map