pushwork 2.0.0-preview.2 → 2.0.0-preview.3

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 (131) hide show
  1. package/dist/cli/commands.d.ts +71 -0
  2. package/dist/cli/commands.d.ts.map +1 -0
  3. package/dist/cli/commands.js +794 -0
  4. package/dist/cli/commands.js.map +1 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +19 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli.js +54 -108
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.d.ts +58 -0
  12. package/dist/commands.d.ts.map +1 -0
  13. package/dist/commands.js +975 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config/index.d.ts +71 -0
  16. package/dist/config/index.d.ts.map +1 -0
  17. package/dist/config/index.js +314 -0
  18. package/dist/config/index.js.map +1 -0
  19. package/dist/config.d.ts +1 -2
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +1 -2
  22. package/dist/config.js.map +1 -1
  23. package/dist/core/change-detection.d.ts +80 -0
  24. package/dist/core/change-detection.d.ts.map +1 -0
  25. package/dist/core/change-detection.js +560 -0
  26. package/dist/core/change-detection.js.map +1 -0
  27. package/dist/core/config.d.ts +81 -0
  28. package/dist/core/config.d.ts.map +1 -0
  29. package/dist/core/config.js +304 -0
  30. package/dist/core/config.js.map +1 -0
  31. package/dist/core/index.d.ts +6 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/index.js +22 -0
  34. package/dist/core/index.js.map +1 -0
  35. package/dist/core/move-detection.d.ts +34 -0
  36. package/dist/core/move-detection.d.ts.map +1 -0
  37. package/dist/core/move-detection.js +128 -0
  38. package/dist/core/move-detection.js.map +1 -0
  39. package/dist/core/snapshot.d.ts +105 -0
  40. package/dist/core/snapshot.d.ts.map +1 -0
  41. package/dist/core/snapshot.js +254 -0
  42. package/dist/core/snapshot.js.map +1 -0
  43. package/dist/core/sync-engine.d.ts +177 -0
  44. package/dist/core/sync-engine.d.ts.map +1 -0
  45. package/dist/core/sync-engine.js +1471 -0
  46. package/dist/core/sync-engine.js.map +1 -0
  47. package/dist/index.d.ts +2 -4
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -14
  50. package/dist/index.js.map +1 -1
  51. package/dist/pushwork.d.ts +18 -65
  52. package/dist/pushwork.d.ts.map +1 -1
  53. package/dist/pushwork.js +97 -559
  54. package/dist/pushwork.js.map +1 -1
  55. package/dist/snarf.d.ts +21 -0
  56. package/dist/snarf.d.ts.map +1 -0
  57. package/dist/snarf.js +117 -0
  58. package/dist/snarf.js.map +1 -0
  59. package/dist/stash.d.ts +0 -2
  60. package/dist/stash.d.ts.map +1 -1
  61. package/dist/stash.js +0 -1
  62. package/dist/stash.js.map +1 -1
  63. package/dist/types/config.d.ts +102 -0
  64. package/dist/types/config.d.ts.map +1 -0
  65. package/dist/types/config.js +10 -0
  66. package/dist/types/config.js.map +1 -0
  67. package/dist/types/documents.d.ts +88 -0
  68. package/dist/types/documents.d.ts.map +1 -0
  69. package/dist/types/documents.js +23 -0
  70. package/dist/types/documents.js.map +1 -0
  71. package/dist/types/index.d.ts +4 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +20 -0
  74. package/dist/types/index.js.map +1 -0
  75. package/dist/types/snapshot.d.ts +64 -0
  76. package/dist/types/snapshot.d.ts.map +1 -0
  77. package/dist/types/snapshot.js +3 -0
  78. package/dist/types/snapshot.js.map +1 -0
  79. package/dist/utils/content-similarity.d.ts +53 -0
  80. package/dist/utils/content-similarity.d.ts.map +1 -0
  81. package/dist/utils/content-similarity.js +155 -0
  82. package/dist/utils/content-similarity.js.map +1 -0
  83. package/dist/utils/content.d.ts +10 -0
  84. package/dist/utils/content.d.ts.map +1 -0
  85. package/dist/utils/content.js +35 -0
  86. package/dist/utils/content.js.map +1 -0
  87. package/dist/utils/directory.d.ts +24 -0
  88. package/dist/utils/directory.d.ts.map +1 -0
  89. package/dist/utils/directory.js +56 -0
  90. package/dist/utils/directory.js.map +1 -0
  91. package/dist/utils/fs.d.ts +74 -0
  92. package/dist/utils/fs.d.ts.map +1 -0
  93. package/dist/utils/fs.js +298 -0
  94. package/dist/utils/fs.js.map +1 -0
  95. package/dist/utils/index.d.ts +5 -0
  96. package/dist/utils/index.d.ts.map +1 -0
  97. package/dist/utils/index.js +21 -0
  98. package/dist/utils/index.js.map +1 -0
  99. package/dist/utils/mime-types.d.ts +13 -0
  100. package/dist/utils/mime-types.d.ts.map +1 -0
  101. package/dist/utils/mime-types.js +247 -0
  102. package/dist/utils/mime-types.js.map +1 -0
  103. package/dist/utils/network-sync.d.ts +30 -0
  104. package/dist/utils/network-sync.d.ts.map +1 -0
  105. package/dist/utils/network-sync.js +391 -0
  106. package/dist/utils/network-sync.js.map +1 -0
  107. package/dist/utils/node-polyfills.d.ts +9 -0
  108. package/dist/utils/node-polyfills.d.ts.map +1 -0
  109. package/dist/utils/node-polyfills.js +9 -0
  110. package/dist/utils/node-polyfills.js.map +1 -0
  111. package/dist/utils/output.d.ts +129 -0
  112. package/dist/utils/output.d.ts.map +1 -0
  113. package/dist/utils/output.js +375 -0
  114. package/dist/utils/output.js.map +1 -0
  115. package/dist/utils/repo-factory.d.ts +15 -0
  116. package/dist/utils/repo-factory.d.ts.map +1 -0
  117. package/dist/utils/repo-factory.js +156 -0
  118. package/dist/utils/repo-factory.js.map +1 -0
  119. package/dist/utils/string-similarity.d.ts +14 -0
  120. package/dist/utils/string-similarity.d.ts.map +1 -0
  121. package/dist/utils/string-similarity.js +43 -0
  122. package/dist/utils/string-similarity.js.map +1 -0
  123. package/dist/utils/text-diff.d.ts +37 -0
  124. package/dist/utils/text-diff.d.ts.map +1 -0
  125. package/dist/utils/text-diff.js +131 -0
  126. package/dist/utils/text-diff.js.map +1 -0
  127. package/dist/utils/trace.d.ts +19 -0
  128. package/dist/utils/trace.d.ts.map +1 -0
  129. package/dist/utils/trace.js +68 -0
  130. package/dist/utils/trace.js.map +1 -0
  131. package/package.json +1 -1
package/dist/pushwork.js CHANGED
@@ -33,7 +33,6 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.deleteBranchFile = void 0;
37
36
  exports.init = init;
38
37
  exports.clone = clone;
39
38
  exports.url = url;
@@ -42,35 +41,26 @@ exports.nuclearizeRepo = nuclearizeRepo;
42
41
  exports.save = save;
43
42
  exports.status = status;
44
43
  exports.diff = diff;
45
- exports.listBranches = listBranches;
46
- exports.currentBranch = currentBranch;
47
- exports.createBranch = createBranch;
48
- exports.previewMerge = previewMerge;
49
- exports.mergeBranch = mergeBranch;
50
44
  exports.cutWorkdir = cutWorkdir;
51
- exports.pasteStash = pasteStash;
52
- exports.showStashes = showStashes;
53
- exports.switchBranch = switchBranch;
45
+ exports.pasteSnarf = pasteSnarf;
46
+ exports.showSnarfs = showSnarfs;
54
47
  const fs = __importStar(require("fs/promises"));
55
48
  const path = __importStar(require("path"));
56
49
  const Automerge = __importStar(require("@automerge/automerge"));
57
50
  const automerge_repo_1 = require("@automerge/automerge-repo");
58
51
  const config_js_1 = require("./config.js");
59
- const branches_js_1 = require("./branches.js");
60
- Object.defineProperty(exports, "deleteBranchFile", { enumerable: true, get: function () { return branches_js_1.deleteBranchFile; } });
61
52
  const ignore_js_1 = require("./ignore.js");
62
53
  const fs_tree_js_1 = require("./fs-tree.js");
63
54
  const log_js_1 = require("./log.js");
64
55
  const repo_js_1 = require("./repo.js");
65
- const stash_js_1 = require("./stash.js");
56
+ const snarf_js_1 = require("./snarf.js");
66
57
  const index_js_1 = require("./shapes/index.js");
67
58
  const dlog = (0, log_js_1.log)("pushwork");
68
59
  const DEFAULT_ARTIFACT_DIRECTORIES = ["dist"];
69
60
  async function init(opts) {
70
61
  const root = path.resolve(opts.dir);
71
- const useBranches = opts.branches ?? true;
72
62
  const online = opts.online ?? true;
73
- dlog("init root=%s backend=%s shape=%s branches=%s online=%s", root, opts.backend, opts.shape, useBranches, online);
63
+ dlog("init root=%s backend=%s shape=%s online=%s", root, opts.backend, opts.shape, online);
74
64
  if (await (0, config_js_1.configExists)(root)) {
75
65
  throw new Error(`pushwork already initialized at ${root}`);
76
66
  }
@@ -93,29 +83,15 @@ async function init(opts) {
93
83
  stampLastSyncAt(folderHandle);
94
84
  await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 10000 });
95
85
  }
96
- let rootUrl = folderUrl;
97
- if (useBranches) {
98
- const branchesHandle = repo.create({
99
- "@patchwork": { type: "branches", ...(title ? { title } : {}) },
100
- branches: { [branches_js_1.DEFAULT_BRANCH]: folderUrl },
101
- });
102
- if (online) {
103
- await (0, repo_js_1.waitForSync)(branchesHandle, { minMs: 1500, idleMs: 1500, maxMs: 10000 });
104
- }
105
- rootUrl = branchesHandle.url;
106
- dlog("init wrapped in BranchesDoc=%s", rootUrl);
107
- await (0, branches_js_1.writeBranchFile)(root, branches_js_1.DEFAULT_BRANCH);
108
- }
109
86
  await (0, config_js_1.writeConfig)(root, {
110
87
  version: config_js_1.CONFIG_VERSION,
111
- rootUrl,
88
+ rootUrl: folderUrl,
112
89
  backend: opts.backend,
113
90
  shape: opts.shape,
114
91
  artifactDirectories: artifactDirs,
115
- branches: useBranches,
116
92
  });
117
- dlog("init complete: rootUrl=%s", rootUrl);
118
- return rootUrl;
93
+ dlog("init complete: rootUrl=%s", folderUrl);
94
+ return folderUrl;
119
95
  }
120
96
  finally {
121
97
  await repo.shutdown();
@@ -137,36 +113,36 @@ async function clone(opts) {
137
113
  const repo = await (0, repo_js_1.openRepo)(opts.backend, (0, config_js_1.storageDir)(root), { offline: !online });
138
114
  try {
139
115
  const shape = await (0, index_js_1.resolveShape)(opts.shape);
140
- const rootHandle = await repo.find(opts.url);
116
+ let folderHandle = await repo.find(opts.url);
141
117
  if (online) {
142
- await (0, repo_js_1.waitForSync)(rootHandle, { idleMs: 1500, maxMs: 15000 });
118
+ await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
143
119
  }
144
- const docType = (0, branches_js_1.detectDocType)(rootHandle.doc());
145
- dlog("clone detected docType=%s", docType);
146
- let useBranches = false;
147
- let folderHandle = rootHandle;
148
- if (docType === "branches") {
149
- useBranches = true;
150
- const branchName = opts.branch ?? branches_js_1.DEFAULT_BRANCH;
151
- folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
120
+ let storedUrl = opts.url;
121
+ const branchesDoc = asBranchesDoc(folderHandle.doc());
122
+ if (branchesDoc) {
123
+ if (!opts.onBranchesDoc) {
124
+ throw new Error(`URL ${opts.url} is a legacy branches doc; pushwork no longer supports branches. Provide an onBranchesDoc callback (or use the CLI, which will prompt you to pick a branch).`);
125
+ }
126
+ const branches = Object.entries(branchesDoc.branches).map(([name, url]) => ({ name, url }));
127
+ const chosenUrl = await opts.onBranchesDoc({
128
+ title: branchesDoc.title,
129
+ branches,
130
+ });
131
+ dlog("clone branches doc → chose %s", chosenUrl);
132
+ folderHandle = await repo.find(chosenUrl);
152
133
  if (online) {
153
134
  await (0, repo_js_1.waitForSync)(folderHandle, { idleMs: 1500, maxMs: 15000 });
154
135
  }
155
- await (0, branches_js_1.writeBranchFile)(root, branchName);
156
- dlog("clone branch=%s folder=%s", branchName, folderHandle.url);
157
- }
158
- else if (opts.branch) {
159
- throw new Error(`--branch passed but root doc is not a branches doc (type=${docType})`);
136
+ storedUrl = chosenUrl;
160
137
  }
161
138
  const tree = await shape.decode({ repo, root: folderHandle });
162
139
  await materializeTree(repo, root, tree);
163
140
  await (0, config_js_1.writeConfig)(root, {
164
141
  version: config_js_1.CONFIG_VERSION,
165
- rootUrl: opts.url,
142
+ rootUrl: storedUrl,
166
143
  backend: opts.backend,
167
144
  shape: opts.shape,
168
145
  artifactDirectories: artifactDirs,
169
- branches: useBranches,
170
146
  });
171
147
  dlog("clone complete");
172
148
  }
@@ -174,6 +150,22 @@ async function clone(opts) {
174
150
  await repo.shutdown();
175
151
  }
176
152
  }
153
+ function asBranchesDoc(doc) {
154
+ if (!doc || typeof doc !== "object")
155
+ return null;
156
+ const meta = doc["@patchwork"];
157
+ if (!meta || typeof meta !== "object")
158
+ return null;
159
+ if (meta.type !== "branches")
160
+ return null;
161
+ const branches = doc.branches;
162
+ if (!branches || typeof branches !== "object")
163
+ return null;
164
+ return {
165
+ title: meta.title,
166
+ branches: branches,
167
+ };
168
+ }
177
169
  async function url(cwd) {
178
170
  const config = await (0, config_js_1.readConfig)(path.resolve(cwd));
179
171
  return config.rootUrl;
@@ -185,11 +177,10 @@ async function sync(cwd, opts = {}) {
185
177
  await commitWorkdir(cwd, { online: true });
186
178
  }
187
179
  /**
188
- * Re-create every Automerge doc this repo references — every UnixFileEntry,
189
- * every folder/directory doc, and (when in branches mode) the BranchesDoc
190
- * with brand-new URLs and no shared history with the originals. Updates
191
- * `config.json` to point at the new root. Offline; the next sync publishes
192
- * the new docs to the server.
180
+ * Re-create every Automerge doc this repo references — every UnixFileEntry
181
+ * and the folder/directory doc with brand-new URLs and no shared history
182
+ * with the originals. Updates `config.json` to point at the new root.
183
+ * Offline; the next sync publishes the new docs to the server.
193
184
  *
194
185
  * Use sparingly: this orphans the previous URLs from this repo's perspective.
195
186
  * Anyone who had cloned the old URL keeps working from those docs; this
@@ -198,58 +189,34 @@ async function sync(cwd, opts = {}) {
198
189
  async function nuclearizeRepo(cwd) {
199
190
  const root = path.resolve(cwd);
200
191
  const config = await (0, config_js_1.readConfig)(root);
201
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
202
- dlog("nuclear root=%s branches=%s current=%s", root, config.branches, branchName);
192
+ dlog("nuclear root=%s", root);
203
193
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
204
194
  try {
205
195
  const shape = await (0, index_js_1.resolveShape)(config.shape);
206
- const oldRootHandle = await repo.find(config.rootUrl);
196
+ const oldFolder = await repo.find(config.rootUrl);
207
197
  const title = path.basename(root) || undefined;
208
- const rebuildFolder = async (oldFolderUrl, folderTitle) => {
209
- const oldFolder = await repo.find(oldFolderUrl);
210
- const oldTree = await shape.decode({ repo, root: oldFolder });
211
- // For each leaf: read content, create a fresh UnixFileEntry doc.
212
- const newTree = (0, index_js_1.newDir)();
213
- for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(oldTree)) {
214
- const bare = (0, index_js_1.stripHeads)(fileUrl);
215
- const oldFileHandle = await repo.find(bare);
216
- const oldDoc = oldFileHandle.doc();
217
- const newFileHandle = repo.create({
218
- "@patchwork": { type: "file" },
219
- name: oldDoc.name,
220
- extension: oldDoc.extension,
221
- mimeType: oldDoc.mimeType,
222
- content: oldDoc.content,
223
- });
224
- let finalUrl = newFileHandle.url;
225
- if ((0, automerge_repo_1.parseAutomergeUrl)(fileUrl).heads) {
226
- finalUrl = (0, index_js_1.pinUrl)(newFileHandle);
227
- }
228
- (0, index_js_1.setFileAt)(newTree, posixPath.split("/").filter(Boolean), finalUrl);
229
- }
230
- // Encode without previousRoot → fresh folder/directory doc URL.
231
- return shape.encode({ repo, tree: newTree, title: folderTitle });
232
- };
233
- let newRootUrl;
234
- if (config.branches && (0, branches_js_1.isBranchesDoc)(oldRootHandle.doc())) {
235
- const oldDoc = oldRootHandle.doc();
236
- const newBranches = {};
237
- for (const [name, oldFolderUrl] of Object.entries(oldDoc.branches)) {
238
- newBranches[name] = await rebuildFolder(oldFolderUrl, title);
239
- dlog("nuclear rebuilt branch %s → %s", name, newBranches[name]);
240
- }
241
- const newRoot = repo.create({
242
- "@patchwork": {
243
- type: "branches",
244
- ...(title ? { title } : {}),
245
- },
246
- branches: newBranches,
198
+ const oldTree = await shape.decode({ repo, root: oldFolder });
199
+ // For each leaf: read content, create a fresh UnixFileEntry doc.
200
+ const newTree = (0, index_js_1.newDir)();
201
+ for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(oldTree)) {
202
+ const bare = (0, index_js_1.stripHeads)(fileUrl);
203
+ const oldFileHandle = await repo.find(bare);
204
+ const oldDoc = oldFileHandle.doc();
205
+ const newFileHandle = repo.create({
206
+ "@patchwork": { type: "file" },
207
+ name: oldDoc.name,
208
+ extension: oldDoc.extension,
209
+ mimeType: oldDoc.mimeType,
210
+ content: oldDoc.content,
247
211
  });
248
- newRootUrl = newRoot.url;
249
- }
250
- else {
251
- newRootUrl = await rebuildFolder(config.rootUrl, title);
212
+ let finalUrl = newFileHandle.url;
213
+ if ((0, index_js_1.isInArtifactDir)(posixPath, config.artifactDirectories)) {
214
+ finalUrl = (0, index_js_1.pinUrl)(newFileHandle);
215
+ }
216
+ (0, index_js_1.setFileAt)(newTree, posixPath.split("/").filter(Boolean), finalUrl);
252
217
  }
218
+ // Encode without previousRoot → fresh folder/directory doc URL.
219
+ const newRootUrl = await shape.encode({ repo, tree: newTree, title });
253
220
  dlog("nuclear new rootUrl=%s", newRootUrl);
254
221
  await (0, config_js_1.writeConfig)(root, { ...config, rootUrl: newRootUrl });
255
222
  }
@@ -263,28 +230,13 @@ async function save(cwd) {
263
230
  async function commitWorkdir(cwd, { online }) {
264
231
  const root = path.resolve(cwd);
265
232
  const config = await (0, config_js_1.readConfig)(root);
266
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
267
- dlog("commit online=%s root=%s branch=%s", online, root, branchName);
233
+ dlog("commit online=%s root=%s", online, root);
268
234
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), {
269
235
  offline: !online,
270
236
  });
271
237
  try {
272
238
  const shape = await (0, index_js_1.resolveShape)(config.shape);
273
- const rootHandle = await repo.find(config.rootUrl);
274
- // In branches mode + online, touch every branch's folder doc so the
275
- // network adapter announces them. Without this, a branch created
276
- // offline (`pushwork branch X`) is never pushed to the server, even
277
- // though its entry is in the BranchesDoc.
278
- const otherBranchHandles = [];
279
- if (online && config.branches && (0, branches_js_1.isBranchesDoc)(rootHandle.doc())) {
280
- const doc = rootHandle.doc();
281
- for (const [name, url] of Object.entries(doc.branches)) {
282
- if (name === branchName)
283
- continue;
284
- otherBranchHandles.push(await repo.find(url));
285
- }
286
- }
287
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
239
+ const folderHandle = await repo.find(config.rootUrl);
288
240
  const previousTree = await shape.decode({ repo, root: folderHandle });
289
241
  const previousFiles = await readFileBytes(repo, previousTree);
290
242
  const ig = await (0, ignore_js_1.loadIgnore)(root);
@@ -300,21 +252,11 @@ async function commitWorkdir(cwd, { online }) {
300
252
  // working tree changed — a sync is also a checkpoint that "we
301
253
  // reconciled with the server at this time."
302
254
  stampLastSyncAt(folderHandle);
303
- // Wait for the current branch's folder, the BranchesDoc itself
304
- // (when in branches mode), and any other branch folder docs to
305
- // flush. The maxMs is generous so a brand-new offline-created
306
- // branch reliably propagates.
307
255
  await (0, repo_js_1.waitForSync)(folderHandle, {
308
256
  minMs: 3000,
309
257
  idleMs: 1500,
310
258
  maxMs: 15000,
311
259
  });
312
- if (config.branches) {
313
- await (0, repo_js_1.waitForSync)(rootHandle, { idleMs: 1500, maxMs: 10000 });
314
- }
315
- for (const h of otherBranchHandles) {
316
- await (0, repo_js_1.waitForSync)(h, { idleMs: 1500, maxMs: 10000 });
317
- }
318
260
  }
319
261
  const finalTree = await shape.decode({ repo, root: folderHandle });
320
262
  await materializeTree(repo, root, finalTree);
@@ -327,18 +269,16 @@ async function commitWorkdir(cwd, { online }) {
327
269
  async function status(cwd) {
328
270
  const root = path.resolve(cwd);
329
271
  const config = await (0, config_js_1.readConfig)(root);
330
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
331
272
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
332
273
  try {
333
274
  const shape = await (0, index_js_1.resolveShape)(config.shape);
334
- const rootHandle = await repo.find(config.rootUrl);
335
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
275
+ const folderHandle = await repo.find(config.rootUrl);
336
276
  const previousTree = await shape.decode({ repo, root: folderHandle });
337
277
  const previousFiles = await readFileBytes(repo, previousTree);
338
278
  const ig = await (0, ignore_js_1.loadIgnore)(root);
339
279
  const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
340
280
  const diff = computeDiff(previousFiles, fsFiles);
341
- return { branch: branchName, diff };
281
+ return { diff };
342
282
  }
343
283
  finally {
344
284
  await repo.shutdown();
@@ -347,12 +287,10 @@ async function status(cwd) {
347
287
  async function diff(cwd, limitToPath) {
348
288
  const root = path.resolve(cwd);
349
289
  const config = await (0, config_js_1.readConfig)(root);
350
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
351
290
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
352
291
  try {
353
292
  const shape = await (0, index_js_1.resolveShape)(config.shape);
354
- const rootHandle = await repo.find(config.rootUrl);
355
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
293
+ const folderHandle = await repo.find(config.rootUrl);
356
294
  const previousTree = await shape.decode({ repo, root: folderHandle });
357
295
  const previousFiles = await readFileBytes(repo, previousTree);
358
296
  const ig = await (0, ignore_js_1.loadIgnore)(root);
@@ -381,309 +319,19 @@ async function diff(cwd, limitToPath) {
381
319
  await repo.shutdown();
382
320
  }
383
321
  }
384
- async function listBranches(cwd) {
385
- const root = path.resolve(cwd);
386
- const config = await (0, config_js_1.readConfig)(root);
387
- if (!config.branches) {
388
- throw new Error("pushwork repo has no branches");
389
- }
390
- const current = await (0, branches_js_1.readBranchFile)(root);
391
- const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
392
- try {
393
- const rootHandle = await repo.find(config.rootUrl);
394
- const doc = rootHandle.doc();
395
- if (!(0, branches_js_1.isBranchesDoc)(doc)) {
396
- throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
397
- }
398
- return { current, names: (0, branches_js_1.listBranchNames)(doc) };
399
- }
400
- finally {
401
- await repo.shutdown();
402
- }
403
- }
404
- async function currentBranch(cwd) {
405
- const root = path.resolve(cwd);
406
- const config = await (0, config_js_1.readConfig)(root);
407
- if (!config.branches)
408
- return null;
409
- return (0, branches_js_1.readBranchFile)(root);
410
- }
411
- async function createBranch(cwd, name) {
412
- if (!name)
413
- throw new Error("branch name is required");
414
- if (name.includes("/") || name.includes("\\")) {
415
- throw new Error("branch name may not contain slashes");
416
- }
417
- const root = path.resolve(cwd);
418
- const config = await (0, config_js_1.readConfig)(root);
419
- if (!config.branches)
420
- throw new Error("pushwork repo has no branches");
421
- const currentName = await (0, branches_js_1.readBranchFile)(root);
422
- if (!currentName)
423
- throw new Error("no current branch is set");
424
- const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
425
- try {
426
- const shape = await (0, index_js_1.resolveShape)(config.shape);
427
- const rootHandle = await repo.find(config.rootUrl);
428
- const doc = rootHandle.doc();
429
- if (!(0, branches_js_1.isBranchesDoc)(doc)) {
430
- throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
431
- }
432
- if (doc.branches[name]) {
433
- throw new Error(`branch "${name}" already exists`);
434
- }
435
- const sourceUrl = doc.branches[currentName];
436
- if (!sourceUrl) {
437
- throw new Error(`current branch "${currentName}" not found in branches doc`);
438
- }
439
- const sourceHandle = await repo.find(sourceUrl);
440
- // Clone the folder doc.
441
- const clonedFolder = repo.clone(sourceHandle);
442
- dlog("createBranch %s cloned folder %s → %s", name, sourceUrl, clonedFolder.url);
443
- // Deep-clone every file doc the source folder references, then rewrite
444
- // the cloned folder's leaves to point at the new file URLs. Without
445
- // this step both branches would alias the same UnixFileEntry docs and
446
- // editing one branch would silently mutate the other.
447
- const sourceTree = await shape.decode({ repo, root: sourceHandle });
448
- const fileUrlRemap = new Map();
449
- for (const [, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
450
- const bare = (0, index_js_1.stripHeads)(fileUrl);
451
- if (fileUrlRemap.has(bare))
452
- continue;
453
- const orig = await repo.find(bare);
454
- const cloned = repo.clone(orig);
455
- fileUrlRemap.set(bare, cloned.url);
456
- dlog("createBranch cloned file %s → %s", bare, cloned.url);
457
- }
458
- const newTree = (0, index_js_1.newDir)();
459
- for (const [posixPath, fileUrl] of (0, index_js_1.flattenLeaves)(sourceTree)) {
460
- const bare = (0, index_js_1.stripHeads)(fileUrl);
461
- const remappedBare = fileUrlRemap.get(bare);
462
- if (!remappedBare)
463
- continue;
464
- const parsed = (0, automerge_repo_1.parseAutomergeUrl)(fileUrl);
465
- // Preserve heads-pinning if the source URL was pinned.
466
- let finalUrl = remappedBare;
467
- if (parsed.heads) {
468
- const newHandle = await repo.find(remappedBare);
469
- finalUrl = (0, index_js_1.pinUrl)(newHandle);
470
- }
471
- const segments = posixPath.split("/").filter(Boolean);
472
- (0, index_js_1.setFileAt)(newTree, segments, finalUrl);
473
- }
474
- await shape.encode({ repo, tree: newTree, previousRoot: clonedFolder });
475
- rootHandle.change((d) => {
476
- d.branches[name] = clonedFolder.url;
477
- });
478
- // Switch to the new branch. The deep clone has identical content to the
479
- // source, so the working tree on disk is already correct — we just
480
- // update .pushwork/branch.
481
- await (0, branches_js_1.writeBranchFile)(root, name);
482
- dlog("createBranch switched to %s", name);
483
- return clonedFolder.url;
484
- }
485
- finally {
486
- await repo.shutdown();
487
- }
488
- }
489
- /**
490
- * Apply changes from `source` branch onto the current branch.
491
- *
492
- * For each path:
493
- * - In both branches: their UnixFileEntry docs share Automerge history (deep
494
- * cloned at branch creation), so we Automerge-merge source's content into
495
- * target's. Concurrent edits are CRDT-merged inside each file doc.
496
- * - Only in source: deep-clone the source's file doc into a new doc and add
497
- * it to target's folder. Editing on either branch afterward stays isolated.
498
- * - Only in target: untouched. We don't propagate deletions from source — the
499
- * user can do that explicitly.
500
- *
501
- * Refuses if the working tree has uncommitted changes against the current
502
- * branch (run `pushwork save` first). Offline only — propagation happens on
503
- * the next `pushwork sync`.
504
- */
505
- /**
506
- * Compute what `merge <source>` would do without mutating any docs or the
507
- * working tree. For paths in both branches we apply the merge to a *clone*
508
- * of the target's file doc to learn the merged bytes; for paths only in
509
- * source we just read source's bytes.
510
- */
511
- async function previewMerge(cwd, source) {
512
- if (!source)
513
- throw new Error("source branch name is required");
514
- const root = path.resolve(cwd);
515
- const config = await (0, config_js_1.readConfig)(root);
516
- if (!config.branches)
517
- throw new Error("pushwork repo has no branches");
518
- const targetName = await (0, branches_js_1.readBranchFile)(root);
519
- if (!targetName)
520
- throw new Error("no current branch is set");
521
- if (source === targetName) {
522
- throw new Error(`cannot merge "${source}" into itself`);
523
- }
524
- const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
525
- try {
526
- const shape = await (0, index_js_1.resolveShape)(config.shape);
527
- const rootHandle = await repo.find(config.rootUrl);
528
- const branchesDoc = rootHandle.doc();
529
- if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
530
- throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
531
- }
532
- if (!branchesDoc.branches[source]) {
533
- throw new Error(`source branch "${source}" does not exist`);
534
- }
535
- const targetFolder = await repo.find(branchesDoc.branches[targetName]);
536
- const sourceFolder = await repo.find(branchesDoc.branches[source]);
537
- const tTree = await shape.decode({ repo, root: targetFolder });
538
- const sTree = await shape.decode({ repo, root: sourceFolder });
539
- const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
540
- const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
541
- const entries = [];
542
- for (const [posixPath, sUrl] of sLeaves) {
543
- const tUrl = tLeaves.get(posixPath);
544
- const sBare = (0, index_js_1.stripHeads)(sUrl);
545
- const sHandle = await repo.find(sBare);
546
- if (!tUrl) {
547
- entries.push({
548
- path: posixPath,
549
- kind: "added",
550
- after: (0, index_js_1.contentToBytes)(sHandle.doc().content),
551
- });
552
- continue;
553
- }
554
- const tBare = (0, index_js_1.stripHeads)(tUrl);
555
- if (tBare === sBare)
556
- continue;
557
- const tHandle = await repo.find(tBare);
558
- const before = (0, index_js_1.contentToBytes)(tHandle.doc().content);
559
- // Compute merge result without touching the target doc.
560
- const merged = Automerge.merge(Automerge.clone(tHandle.doc()), Automerge.clone(sHandle.doc()));
561
- const after = (0, index_js_1.contentToBytes)(merged.content);
562
- if ((0, fs_tree_js_1.byteEq)(before, after))
563
- continue;
564
- entries.push({ path: posixPath, kind: "merged", before, after });
565
- }
566
- entries.sort((a, b) => a.path.localeCompare(b.path));
567
- return { source, target: targetName, entries };
568
- }
569
- finally {
570
- await repo.shutdown();
571
- }
572
- }
573
- async function mergeBranch(cwd, source) {
574
- if (!source)
575
- throw new Error("source branch name is required");
576
- const root = path.resolve(cwd);
577
- const config = await (0, config_js_1.readConfig)(root);
578
- if (!config.branches)
579
- throw new Error("pushwork repo has no branches");
580
- const targetName = await (0, branches_js_1.readBranchFile)(root);
581
- if (!targetName)
582
- throw new Error("no current branch is set");
583
- if (source === targetName) {
584
- throw new Error(`cannot merge "${source}" into itself`);
585
- }
586
- dlog("merge source=%s target=%s", source, targetName);
587
- const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
588
- try {
589
- const shape = await (0, index_js_1.resolveShape)(config.shape);
590
- const rootHandle = await repo.find(config.rootUrl);
591
- const branchesDoc = rootHandle.doc();
592
- if (!(0, branches_js_1.isBranchesDoc)(branchesDoc)) {
593
- throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
594
- }
595
- if (!branchesDoc.branches[source]) {
596
- throw new Error(`source branch "${source}" does not exist`);
597
- }
598
- const targetUrl = branchesDoc.branches[targetName];
599
- const sourceUrl = branchesDoc.branches[source];
600
- const targetFolder = await repo.find(targetUrl);
601
- const sourceFolder = await repo.find(sourceUrl);
602
- // Refuse on dirty working tree (mirror switchBranch policy).
603
- const tFiles = await readFileBytes(repo, await shape.decode({ repo, root: targetFolder }));
604
- const ig = await (0, ignore_js_1.loadIgnore)(root);
605
- const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
606
- const dirty = computeDiff(tFiles, fsFiles);
607
- if (dirty.added.length || dirty.modified.length || dirty.deleted.length) {
608
- throw new Error(`refusing to merge: working tree has uncommitted changes on branch "${targetName}". run \`pushwork save\` first.`);
609
- }
610
- const tTree = await shape.decode({ repo, root: targetFolder });
611
- const sTree = await shape.decode({ repo, root: sourceFolder });
612
- const tLeaves = (0, index_js_1.flattenLeaves)(tTree);
613
- const sLeaves = (0, index_js_1.flattenLeaves)(sTree);
614
- const merged = [];
615
- const added = [];
616
- // For paths in both: merge file docs in place.
617
- for (const [posixPath, sUrl] of sLeaves) {
618
- const tUrl = tLeaves.get(posixPath);
619
- if (!tUrl)
620
- continue;
621
- const tBare = (0, index_js_1.stripHeads)(tUrl);
622
- const sBare = (0, index_js_1.stripHeads)(sUrl);
623
- if (tBare === sBare) {
624
- // Same file doc identity (shared) — already in sync, nothing to do.
625
- continue;
626
- }
627
- const tHandle = await repo.find(tBare);
628
- const sHandle = await repo.find(sBare);
629
- tHandle.update((d) => Automerge.merge(d, Automerge.clone(sHandle.doc())));
630
- merged.push(posixPath);
631
- dlog("merge merged file at %s (%s ← %s)", posixPath, tBare, sBare);
632
- }
633
- // For paths only in source: deep-clone source's file doc, add to target's folder.
634
- const newLeaves = new Map();
635
- for (const [posixPath, sUrl] of sLeaves) {
636
- if (tLeaves.has(posixPath))
637
- continue;
638
- const sBare = (0, index_js_1.stripHeads)(sUrl);
639
- const sHandle = await repo.find(sBare);
640
- const cloned = repo.clone(sHandle);
641
- let finalUrl = cloned.url;
642
- const parsed = (0, automerge_repo_1.parseAutomergeUrl)(sUrl);
643
- if (parsed.heads) {
644
- finalUrl = (0, index_js_1.pinUrl)(cloned);
645
- }
646
- newLeaves.set(posixPath, finalUrl);
647
- added.push(posixPath);
648
- dlog("merge added %s url=%s", posixPath, finalUrl);
649
- }
650
- if (newLeaves.size > 0) {
651
- // Build a tree for the encode call: existing target leaves + new ones.
652
- const nextTree = (0, index_js_1.newDir)();
653
- for (const [p, url] of tLeaves) {
654
- (0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
655
- }
656
- for (const [p, url] of newLeaves) {
657
- (0, index_js_1.setFileAt)(nextTree, p.split("/").filter(Boolean), url);
658
- }
659
- await shape.encode({ repo, tree: nextTree, previousRoot: targetFolder });
660
- }
661
- // Materialize current branch (target) onto disk to reflect the merge.
662
- const finalTree = await shape.decode({ repo, root: targetFolder });
663
- await materializeTree(repo, root, finalTree);
664
- merged.sort();
665
- added.sort();
666
- return { source, target: targetName, merged, added };
667
- }
668
- finally {
669
- await repo.shutdown();
670
- }
671
- }
672
322
  /**
673
- * Capture the working tree's changes against the current branch's saved
674
- * state into a local stash, then reset the working tree to the saved state.
675
- * Stashes live in `.pushwork/stash.json` and are never synced.
323
+ * Capture the working tree's changes against the saved state into a local
324
+ * snarf, then reset the working tree to the saved state. Snarfs live in
325
+ * `.pushwork/snarf/` and are never synced.
676
326
  */
677
327
  async function cutWorkdir(cwd, opts = {}) {
678
328
  const root = path.resolve(cwd);
679
329
  const config = await (0, config_js_1.readConfig)(root);
680
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
681
- dlog("cut root=%s branch=%s name=%s", root, branchName, opts.name ?? "(unnamed)");
330
+ dlog("cut root=%s name=%s", root, opts.name ?? "(unnamed)");
682
331
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
683
332
  try {
684
333
  const shape = await (0, index_js_1.resolveShape)(config.shape);
685
- const rootHandle = await repo.find(config.rootUrl);
686
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
334
+ const folderHandle = await repo.find(config.rootUrl);
687
335
  const previousTree = await shape.decode({ repo, root: folderHandle });
688
336
  const previousFiles = await readFileBytes(repo, previousTree);
689
337
  const ig = await (0, ignore_js_1.loadIgnore)(root);
@@ -692,13 +340,13 @@ async function cutWorkdir(cwd, opts = {}) {
692
340
  for (const [p, bytes] of fsFiles) {
693
341
  const prev = previousFiles.get(p);
694
342
  if (!prev) {
695
- entries.push({ path: p, kind: "added", contentBase64: (0, stash_js_1.encodeBytes)(bytes) });
343
+ entries.push({ path: p, kind: "added", contentBase64: (0, snarf_js_1.encodeBytes)(bytes) });
696
344
  }
697
345
  else if (!(0, fs_tree_js_1.byteEq)(prev.bytes, bytes)) {
698
346
  entries.push({
699
347
  path: p,
700
348
  kind: "modified",
701
- contentBase64: (0, stash_js_1.encodeBytes)(bytes),
349
+ contentBase64: (0, snarf_js_1.encodeBytes)(bytes),
702
350
  });
703
351
  }
704
352
  }
@@ -710,35 +358,32 @@ async function cutWorkdir(cwd, opts = {}) {
710
358
  throw new Error("nothing to cut: working tree clean");
711
359
  }
712
360
  entries.sort((a, b) => a.path.localeCompare(b.path));
713
- const stash = await (0, stash_js_1.appendStash)(root, {
361
+ const snarf = await (0, snarf_js_1.appendSnarf)(root, {
714
362
  name: opts.name,
715
- branch: branchName,
716
363
  entries,
717
364
  });
718
- // Reset working tree to the branch's saved state.
365
+ // Reset working tree to the saved state.
719
366
  await materializeTree(repo, root, previousTree);
720
- dlog("cut complete id=%d entries=%d", stash.id, entries.length);
721
- return { id: stash.id, entries: entries.length };
367
+ dlog("cut complete id=%d entries=%d", snarf.id, entries.length);
368
+ return { id: snarf.id, entries: entries.length };
722
369
  }
723
370
  finally {
724
371
  await repo.shutdown();
725
372
  }
726
373
  }
727
374
  /**
728
- * Apply a stash on top of the current working tree, then remove the stash
375
+ * Apply a snarf on top of the current working tree, then remove the snarf
729
376
  * entry. Refuses if the working tree has uncommitted changes (caller can
730
377
  * `pushwork save` or `pushwork cut` first).
731
378
  */
732
- async function pasteStash(cwd, selector) {
379
+ async function pasteSnarf(cwd, selector) {
733
380
  const root = path.resolve(cwd);
734
381
  const config = await (0, config_js_1.readConfig)(root);
735
- const branchName = config.branches ? await (0, branches_js_1.readBranchFile)(root) : null;
736
- // Check the working tree is clean against the current branch state.
382
+ // Check the working tree is clean against the saved state.
737
383
  const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
738
384
  try {
739
385
  const shape = await (0, index_js_1.resolveShape)(config.shape);
740
- const rootHandle = await repo.find(config.rootUrl);
741
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, branchName);
386
+ const folderHandle = await repo.find(config.rootUrl);
742
387
  const previousTree = await shape.decode({ repo, root: folderHandle });
743
388
  const previousFiles = await readFileBytes(repo, previousTree);
744
389
  const ig = await (0, ignore_js_1.loadIgnore)(root);
@@ -751,13 +396,13 @@ async function pasteStash(cwd, selector) {
751
396
  finally {
752
397
  await repo.shutdown();
753
398
  }
754
- const stash = await (0, stash_js_1.takeStash)(root, selector);
755
- if (!stash) {
399
+ const snarf = await (0, snarf_js_1.takeSnarf)(root, selector);
400
+ if (!snarf) {
756
401
  throw new Error(selector
757
- ? `no stash matches "${selector}"`
758
- : "nothing to paste: no stashes");
402
+ ? `no snarf matches "${selector}"`
403
+ : "nothing to paste: no snarfs");
759
404
  }
760
- for (const entry of stash.entries) {
405
+ for (const entry of snarf.entries) {
761
406
  const target = path.join(root, fromPosix(entry.path));
762
407
  if (entry.kind === "deleted") {
763
408
  try {
@@ -769,118 +414,15 @@ async function pasteStash(cwd, selector) {
769
414
  await pruneEmptyDirs(root, path.dirname(fromPosix(entry.path)));
770
415
  }
771
416
  else if (entry.contentBase64 != null) {
772
- const bytes = (0, stash_js_1.decodeBytes)(entry.contentBase64);
417
+ const bytes = (0, snarf_js_1.decodeBytes)(entry.contentBase64);
773
418
  await (0, fs_tree_js_1.writeFileAtomic)(target, bytes);
774
419
  }
775
420
  }
776
- dlog("paste complete id=%d entries=%d", stash.id, stash.entries.length);
777
- return { id: stash.id, name: stash.name, entries: stash.entries.length };
421
+ dlog("paste complete id=%d entries=%d", snarf.id, snarf.entries.length);
422
+ return { id: snarf.id, name: snarf.name, entries: snarf.entries.length };
778
423
  }
779
- async function showStashes(cwd) {
780
- return (0, stash_js_1.listStashes)(path.resolve(cwd));
781
- }
782
- async function switchBranch(cwd, name) {
783
- if (!name)
784
- throw new Error("branch name is required");
785
- const root = path.resolve(cwd);
786
- const config = await (0, config_js_1.readConfig)(root);
787
- if (!config.branches)
788
- throw new Error("pushwork repo has no branches");
789
- const currentName = await (0, branches_js_1.readBranchFile)(root);
790
- const repo = await (0, repo_js_1.openRepo)(config.backend, (0, config_js_1.storageDir)(root), { offline: true });
791
- try {
792
- const shape = await (0, index_js_1.resolveShape)(config.shape);
793
- const rootHandle = await repo.find(config.rootUrl);
794
- const doc = rootHandle.doc();
795
- if (!(0, branches_js_1.isBranchesDoc)(doc)) {
796
- throw new Error(`root doc at ${config.rootUrl} is not a branches doc`);
797
- }
798
- if (!doc.branches[name]) {
799
- throw new Error(`branch "${name}" does not exist`);
800
- }
801
- const stranded = !!currentName && !doc.branches[currentName];
802
- // Refuse if the working dir has uncommitted changes against the current
803
- // branch. The user can `pushwork save` to commit, or `pushwork cut` +
804
- // `pushwork paste` to carry the changes across the switch.
805
- if (currentName && !stranded) {
806
- const folderHandle = await (0, branches_js_1.resolveEffectiveRoot)(repo, rootHandle, currentName);
807
- const previousTree = await shape.decode({ repo, root: folderHandle });
808
- const previousFiles = await readFileBytes(repo, previousTree);
809
- const ig = await (0, ignore_js_1.loadIgnore)(root);
810
- const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
811
- const d = computeDiff(previousFiles, fsFiles);
812
- if (d.added.length || d.modified.length || d.deleted.length) {
813
- throw new Error(`refusing to switch: working tree has uncommitted changes on branch "${currentName}". run \`pushwork save\` to commit, or \`pushwork cut\` + \`pushwork switch ${name}\` + \`pushwork paste\` to carry them across.`);
814
- }
815
- }
816
- // Materialize from the new branch.
817
- const newFolder = await repo.find(doc.branches[name]);
818
- const tree = await shape.decode({ repo, root: newFolder });
819
- // Stranded: the current branch is gone, so we have no reference for a
820
- // dirty check. Auto-cut working changes against the destination branch,
821
- // materialize, then auto-paste so the user's work survives the switch.
822
- let strandedStashId = null;
823
- if (stranded) {
824
- const newFiles = await readFileBytes(repo, tree);
825
- const ig = await (0, ignore_js_1.loadIgnore)(root);
826
- const fsFiles = await (0, fs_tree_js_1.walkDir)(root, ig);
827
- const entries = [];
828
- for (const [p, bytes] of fsFiles) {
829
- const prev = newFiles.get(p);
830
- if (!prev) {
831
- entries.push({ path: p, kind: "added", contentBase64: (0, stash_js_1.encodeBytes)(bytes) });
832
- }
833
- else if (!(0, fs_tree_js_1.byteEq)(prev.bytes, bytes)) {
834
- entries.push({ path: p, kind: "modified", contentBase64: (0, stash_js_1.encodeBytes)(bytes) });
835
- }
836
- }
837
- for (const [p] of newFiles) {
838
- if (!fsFiles.has(p))
839
- entries.push({ path: p, kind: "deleted" });
840
- }
841
- if (entries.length > 0) {
842
- entries.sort((a, b) => a.path.localeCompare(b.path));
843
- const stash = await (0, stash_js_1.appendStash)(root, {
844
- name: `stranded-from-${currentName}`,
845
- branch: currentName,
846
- entries,
847
- });
848
- strandedStashId = stash.id;
849
- process.stderr.write(`warning: branch "${currentName}" no longer exists; auto-cut ${entries.length} entr${entries.length === 1 ? "y" : "ies"} as stash #${stash.id} and will auto-paste after switch\n`);
850
- }
851
- else {
852
- process.stderr.write(`warning: branch "${currentName}" no longer exists; switching (working tree already matches "${name}")\n`);
853
- }
854
- }
855
- await materializeTree(repo, root, tree);
856
- await (0, branches_js_1.writeBranchFile)(root, name);
857
- dlog("switch → %s", name);
858
- if (strandedStashId != null) {
859
- const stash = await (0, stash_js_1.takeStash)(root, String(strandedStashId));
860
- if (stash) {
861
- for (const entry of stash.entries) {
862
- const target = path.join(root, fromPosix(entry.path));
863
- if (entry.kind === "deleted") {
864
- try {
865
- await fs.unlink(target);
866
- }
867
- catch {
868
- // already gone
869
- }
870
- await pruneEmptyDirs(root, path.dirname(fromPosix(entry.path)));
871
- }
872
- else if (entry.contentBase64 != null) {
873
- const bytes = (0, stash_js_1.decodeBytes)(entry.contentBase64);
874
- await (0, fs_tree_js_1.writeFileAtomic)(target, bytes);
875
- }
876
- }
877
- dlog("stranded auto-paste applied stash #%d (%d entries)", stash.id, stash.entries.length);
878
- }
879
- }
880
- }
881
- finally {
882
- await repo.shutdown();
883
- }
424
+ async function showSnarfs(cwd) {
425
+ return (0, snarf_js_1.listSnarfs)(path.resolve(cwd));
884
426
  }
885
427
  function stampLastSyncAt(handle) {
886
428
  handle.change((d) => {
@@ -938,17 +480,13 @@ async function pushFiles(repo, fsFiles, previous, artifactDirs) {
938
480
  }
939
481
  else if (prev) {
940
482
  // Changed path: mutate the existing file doc in place. This keeps
941
- // the file URL stable within a branch and avoids the propagation
483
+ // the file URL stable across edits and avoids the propagation
942
484
  // race where a brand-new file doc URL is referenced by the folder
943
485
  // before its bytes have reached the sync server.
944
486
  //
945
487
  // For string content (text files) we use Automerge.updateText so
946
488
  // concurrent character-level edits merge correctly. Bytes and
947
489
  // ImmutableString are atomic — last writer wins on the field.
948
- //
949
- // Branch isolation is enforced separately: `createBranch` deep
950
- // clones every file doc the source branch references, so two
951
- // branches never share a UnixFileEntry doc identity.
952
490
  const refreshUrl = (0, index_js_1.stripHeads)(prev.url);
953
491
  const handle = await repo.find(refreshUrl);
954
492
  handle.change((d) => {