@tinyrack/devsync 1.1.0 → 1.2.2

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 (216) hide show
  1. package/README.md +230 -62
  2. package/dist/cli/base-command.d.ts +14 -0
  3. package/dist/cli/base-command.d.ts.map +1 -0
  4. package/dist/cli/base-command.js +22 -0
  5. package/dist/cli/base-command.js.map +1 -0
  6. package/dist/cli/commands/dir.d.ts +8 -0
  7. package/dist/cli/commands/dir.d.ts.map +1 -0
  8. package/dist/cli/commands/dir.js +16 -0
  9. package/dist/cli/commands/dir.js.map +1 -0
  10. package/dist/cli/commands/doctor.d.ts +8 -0
  11. package/dist/cli/commands/doctor.d.ts.map +1 -0
  12. package/dist/cli/commands/doctor.js +18 -0
  13. package/dist/cli/commands/doctor.js.map +1 -0
  14. package/dist/cli/commands/index.d.ts +23 -0
  15. package/dist/cli/commands/index.d.ts.map +1 -0
  16. package/dist/cli/commands/index.js +23 -0
  17. package/dist/cli/commands/index.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +15 -0
  19. package/dist/cli/commands/init.d.ts.map +1 -0
  20. package/dist/cli/commands/init.js +43 -0
  21. package/dist/cli/commands/init.js.map +1 -0
  22. package/dist/cli/commands/machine/list.d.ts +7 -0
  23. package/dist/cli/commands/machine/list.d.ts.map +1 -0
  24. package/dist/cli/commands/machine/list.js +12 -0
  25. package/dist/cli/commands/machine/list.js.map +1 -0
  26. package/dist/cli/commands/machine/use.d.ts +11 -0
  27. package/dist/cli/commands/machine/use.d.ts.map +1 -0
  28. package/dist/cli/commands/machine/use.js +28 -0
  29. package/dist/cli/commands/machine/use.js.map +1 -0
  30. package/dist/cli/commands/pull.d.ts +12 -0
  31. package/dist/cli/commands/pull.d.ts.map +1 -0
  32. package/dist/cli/commands/pull.js +34 -0
  33. package/dist/cli/commands/pull.js.map +1 -0
  34. package/dist/cli/commands/push.d.ts +12 -0
  35. package/dist/cli/commands/push.d.ts.map +1 -0
  36. package/dist/cli/commands/push.js +34 -0
  37. package/dist/cli/commands/push.js.map +1 -0
  38. package/dist/cli/commands/status.d.ts +11 -0
  39. package/dist/cli/commands/status.d.ts.map +1 -0
  40. package/dist/cli/commands/status.js +27 -0
  41. package/dist/cli/commands/status.js.map +1 -0
  42. package/dist/cli/commands/track.d.ts +16 -0
  43. package/dist/cli/commands/track.d.ts.map +1 -0
  44. package/dist/cli/commands/track.js +82 -0
  45. package/dist/cli/commands/track.js.map +1 -0
  46. package/dist/cli/commands/untrack.d.ts +11 -0
  47. package/dist/cli/commands/untrack.d.ts.map +1 -0
  48. package/dist/cli/commands/untrack.js +28 -0
  49. package/dist/cli/commands/untrack.js.map +1 -0
  50. package/dist/config/global-config.d.ts +21 -0
  51. package/dist/config/global-config.d.ts.map +1 -0
  52. package/dist/config/global-config.js +106 -0
  53. package/dist/config/global-config.js.map +1 -0
  54. package/dist/config/sync.d.ts +96 -0
  55. package/dist/config/sync.d.ts.map +1 -0
  56. package/dist/config/sync.js +412 -0
  57. package/dist/config/sync.js.map +1 -0
  58. package/dist/config/xdg.d.ts +11 -0
  59. package/dist/config/xdg.d.ts.map +1 -0
  60. package/dist/config/xdg.js +79 -0
  61. package/dist/config/xdg.js.map +1 -0
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/{src/index.ts → dist/index.js} +1 -1
  65. package/dist/index.js.map +1 -0
  66. package/dist/lib/file-mode.d.ts +3 -0
  67. package/dist/lib/file-mode.d.ts.map +1 -0
  68. package/dist/lib/file-mode.js +7 -0
  69. package/dist/lib/file-mode.js.map +1 -0
  70. package/dist/lib/output.d.ts +31 -0
  71. package/dist/lib/output.d.ts.map +1 -0
  72. package/dist/lib/output.js +198 -0
  73. package/dist/lib/output.js.map +1 -0
  74. package/dist/lib/path.d.ts +5 -0
  75. package/dist/lib/path.d.ts.map +1 -0
  76. package/dist/lib/path.js +25 -0
  77. package/dist/lib/path.js.map +1 -0
  78. package/dist/lib/string.d.ts +2 -0
  79. package/dist/lib/string.d.ts.map +1 -0
  80. package/dist/lib/string.js +4 -0
  81. package/dist/lib/string.js.map +1 -0
  82. package/dist/lib/validation.d.ts +3 -0
  83. package/dist/lib/validation.d.ts.map +1 -0
  84. package/dist/lib/validation.js +9 -0
  85. package/dist/lib/validation.js.map +1 -0
  86. package/dist/services/add.d.ts +20 -0
  87. package/dist/services/add.d.ts.map +1 -0
  88. package/dist/services/add.js +161 -0
  89. package/dist/services/add.js.map +1 -0
  90. package/dist/services/config-file.d.ts +30 -0
  91. package/dist/services/config-file.d.ts.map +1 -0
  92. package/dist/services/config-file.js +34 -0
  93. package/dist/services/config-file.js.map +1 -0
  94. package/dist/services/crypto.d.ts +9 -0
  95. package/dist/services/crypto.d.ts.map +1 -0
  96. package/dist/services/crypto.js +75 -0
  97. package/dist/services/crypto.js.map +1 -0
  98. package/dist/services/doctor.d.ts +16 -0
  99. package/dist/services/doctor.d.ts.map +1 -0
  100. package/dist/services/doctor.js +84 -0
  101. package/dist/services/doctor.js.map +1 -0
  102. package/dist/services/error.d.ts +14 -0
  103. package/dist/services/error.d.ts.map +1 -0
  104. package/dist/services/error.js +38 -0
  105. package/dist/services/error.js.map +1 -0
  106. package/dist/services/filesystem.d.ts +15 -0
  107. package/dist/services/filesystem.d.ts.map +1 -0
  108. package/dist/services/filesystem.js +113 -0
  109. package/dist/services/filesystem.js.map +1 -0
  110. package/dist/services/forget.d.ts +14 -0
  111. package/dist/services/forget.d.ts.map +1 -0
  112. package/dist/services/forget.js +124 -0
  113. package/dist/services/forget.js.map +1 -0
  114. package/dist/services/git.d.ts +10 -0
  115. package/dist/services/git.d.ts.map +1 -0
  116. package/dist/services/git.js +57 -0
  117. package/dist/services/git.js.map +1 -0
  118. package/dist/services/init.d.ts +19 -0
  119. package/dist/services/init.d.ts.map +1 -0
  120. package/dist/services/init.js +203 -0
  121. package/dist/services/init.js.map +1 -0
  122. package/dist/services/local-materialization.d.ts +28 -0
  123. package/dist/services/local-materialization.d.ts.map +1 -0
  124. package/dist/services/local-materialization.js +262 -0
  125. package/dist/services/local-materialization.js.map +1 -0
  126. package/dist/services/local-snapshot.d.ts +25 -0
  127. package/dist/services/local-snapshot.d.ts.map +1 -0
  128. package/dist/services/local-snapshot.js +93 -0
  129. package/dist/services/local-snapshot.js.map +1 -0
  130. package/dist/services/machine.d.ts +40 -0
  131. package/dist/services/machine.d.ts.map +1 -0
  132. package/dist/services/machine.js +113 -0
  133. package/dist/services/machine.js.map +1 -0
  134. package/dist/services/paths.d.ts +12 -0
  135. package/dist/services/paths.d.ts.map +1 -0
  136. package/dist/services/paths.js +71 -0
  137. package/dist/services/paths.js.map +1 -0
  138. package/dist/services/pull.d.ts +28 -0
  139. package/dist/services/pull.d.ts.map +1 -0
  140. package/dist/services/pull.js +67 -0
  141. package/dist/services/pull.js.map +1 -0
  142. package/dist/services/push.d.ts +35 -0
  143. package/dist/services/push.d.ts.map +1 -0
  144. package/dist/services/push.js +96 -0
  145. package/dist/services/push.js.map +1 -0
  146. package/dist/services/repo-artifacts.d.ts +52 -0
  147. package/dist/services/repo-artifacts.d.ts.map +1 -0
  148. package/dist/services/repo-artifacts.js +251 -0
  149. package/dist/services/repo-artifacts.js.map +1 -0
  150. package/dist/services/repo-snapshot.d.ts +6 -0
  151. package/dist/services/repo-snapshot.d.ts.map +1 -0
  152. package/dist/services/repo-snapshot.js +163 -0
  153. package/dist/services/repo-snapshot.js.map +1 -0
  154. package/dist/services/runtime.d.ts +40 -0
  155. package/dist/services/runtime.d.ts.map +1 -0
  156. package/dist/services/runtime.js +71 -0
  157. package/dist/services/runtime.js.map +1 -0
  158. package/dist/services/set.d.ts +38 -0
  159. package/dist/services/set.d.ts.map +1 -0
  160. package/dist/services/set.js +184 -0
  161. package/dist/services/set.js.map +1 -0
  162. package/dist/services/status.d.ts +30 -0
  163. package/dist/services/status.d.ts.map +1 -0
  164. package/dist/services/status.js +35 -0
  165. package/dist/services/status.js.map +1 -0
  166. package/package.json +15 -7
  167. package/src/cli/commands/add.ts +0 -40
  168. package/src/cli/commands/cd.ts +0 -80
  169. package/src/cli/commands/doctor.ts +0 -20
  170. package/src/cli/commands/forget.ts +0 -32
  171. package/src/cli/commands/index.ts +0 -23
  172. package/src/cli/commands/init.ts +0 -43
  173. package/src/cli/commands/list.ts +0 -17
  174. package/src/cli/commands/pull.ts +0 -31
  175. package/src/cli/commands/push.ts +0 -31
  176. package/src/cli/commands/set.ts +0 -47
  177. package/src/cli/commands/status.ts +0 -18
  178. package/src/cli/sync-output.test.ts +0 -173
  179. package/src/cli/sync-output.ts +0 -200
  180. package/src/config/sync.test.ts +0 -609
  181. package/src/config/sync.ts +0 -572
  182. package/src/config/xdg.ts +0 -138
  183. package/src/lib/string.test.ts +0 -13
  184. package/src/lib/string.ts +0 -3
  185. package/src/lib/validation.test.ts +0 -32
  186. package/src/lib/validation.ts +0 -11
  187. package/src/services/add.ts +0 -178
  188. package/src/services/config-file.test.ts +0 -161
  189. package/src/services/config-file.ts +0 -101
  190. package/src/services/crypto.test.ts +0 -132
  191. package/src/services/crypto.ts +0 -83
  192. package/src/services/doctor.ts +0 -142
  193. package/src/services/error.ts +0 -6
  194. package/src/services/filesystem.test.ts +0 -171
  195. package/src/services/filesystem.ts +0 -183
  196. package/src/services/forget.ts +0 -261
  197. package/src/services/git.test.ts +0 -83
  198. package/src/services/git.ts +0 -74
  199. package/src/services/init.test.ts +0 -109
  200. package/src/services/init.ts +0 -244
  201. package/src/services/list.ts +0 -63
  202. package/src/services/local-materialization.ts +0 -421
  203. package/src/services/local-snapshot.ts +0 -173
  204. package/src/services/paths.test.ts +0 -74
  205. package/src/services/paths.ts +0 -98
  206. package/src/services/pull.ts +0 -144
  207. package/src/services/push.ts +0 -168
  208. package/src/services/repo-artifacts.ts +0 -262
  209. package/src/services/repo-snapshot.ts +0 -197
  210. package/src/services/runtime.ts +0 -57
  211. package/src/services/set.ts +0 -383
  212. package/src/services/status.ts +0 -57
  213. package/src/services/sync.dry-run.test.ts +0 -179
  214. package/src/services/sync.runtime.test.ts +0 -756
  215. package/src/services/sync.service.test.ts +0 -1169
  216. package/src/test/helpers/sync-fixture.ts +0 -47
@@ -1,756 +0,0 @@
1
- import {
2
- chmod,
3
- mkdir,
4
- readFile,
5
- rm,
6
- symlink,
7
- writeFile,
8
- } from "node:fs/promises";
9
- import { dirname, join } from "node:path";
10
-
11
- import { afterEach, describe, expect, it } from "vitest";
12
-
13
- import {
14
- parseSyncConfig,
15
- type ResolvedSyncConfig,
16
- resolveSyncArtifactsDirectoryPath,
17
- type SyncConfig,
18
- syncSecretArtifactSuffix,
19
- } from "#app/config/sync.ts";
20
- import { encryptSecretFile } from "#app/services/crypto.ts";
21
- import {
22
- applyEntryMaterialization,
23
- buildEntryMaterialization,
24
- buildPullCounts,
25
- countDeletedLocalNodes,
26
- } from "#app/services/local-materialization.ts";
27
- import { buildLocalSnapshot } from "#app/services/local-snapshot.ts";
28
- import {
29
- buildRepoArtifacts,
30
- collectExistingArtifactKeys,
31
- writeArtifactsToDirectory,
32
- } from "#app/services/repo-artifacts.ts";
33
- import { buildRepositorySnapshot } from "#app/services/repo-snapshot.ts";
34
- import {
35
- createAgeKeyPair,
36
- createTemporaryDirectory,
37
- writeIdentityFile,
38
- } from "../test/helpers/sync-fixture.ts";
39
-
40
- const temporaryDirectories: string[] = [];
41
-
42
- const createWorkspace = async () => {
43
- const directory = await createTemporaryDirectory("devsync-sync-runtime-");
44
-
45
- temporaryDirectories.push(directory);
46
-
47
- return directory;
48
- };
49
-
50
- const createSyncEnvironment = (
51
- homeDirectory: string,
52
- xdgConfigHome: string,
53
- ): NodeJS.ProcessEnv => {
54
- return {
55
- HOME: homeDirectory,
56
- XDG_CONFIG_HOME: xdgConfigHome,
57
- };
58
- };
59
-
60
- const createResolvedConfig = (
61
- input: Readonly<{
62
- entries: SyncConfig["entries"];
63
- homeDirectory: string;
64
- recipients: readonly string[];
65
- xdgConfigHome: string;
66
- }>,
67
- ): ResolvedSyncConfig => {
68
- return parseSyncConfig(
69
- {
70
- version: 1,
71
- age: {
72
- identityFile: "$XDG_CONFIG_HOME/devsync/age/keys.txt",
73
- recipients: [...input.recipients],
74
- },
75
- entries: input.entries,
76
- },
77
- createSyncEnvironment(input.homeDirectory, input.xdgConfigHome),
78
- );
79
- };
80
-
81
- afterEach(async () => {
82
- while (temporaryDirectories.length > 0) {
83
- const directory = temporaryDirectories.pop();
84
-
85
- if (directory !== undefined) {
86
- await rm(directory, { force: true, recursive: true });
87
- }
88
- }
89
- });
90
-
91
- describe("sync runtime helpers", () => {
92
- it("keeps explicit children under an ignored directory root in the local snapshot", async () => {
93
- const workspace = await createWorkspace();
94
- const homeDirectory = join(workspace, "home");
95
- const xdgConfigHome = join(workspace, "xdg");
96
- const bundleDirectory = join(homeDirectory, "bundle");
97
-
98
- await mkdir(bundleDirectory, { recursive: true });
99
- await writeFile(join(bundleDirectory, "keep.txt"), "keep\n");
100
- await writeFile(join(bundleDirectory, "ignored.txt"), "ignored\n");
101
-
102
- const config = createResolvedConfig({
103
- entries: [
104
- {
105
- kind: "directory",
106
- localPath: "~/bundle",
107
- mode: "ignore",
108
- name: "bundle",
109
- overrides: {
110
- "keep.txt": "normal",
111
- },
112
- repoPath: "bundle",
113
- },
114
- ],
115
- homeDirectory,
116
- recipients: ["age1example"],
117
- xdgConfigHome,
118
- });
119
-
120
- const snapshot = await buildLocalSnapshot(config);
121
-
122
- expect(
123
- [...snapshot.keys()].sort((left, right) => {
124
- return left.localeCompare(right);
125
- }),
126
- ).toEqual(["bundle", "bundle/keep.txt"]);
127
- });
128
-
129
- it("rejects secret symlinks and file-entry kind mismatches in the local snapshot", async () => {
130
- const workspace = await createWorkspace();
131
- const homeDirectory = join(workspace, "home");
132
- const xdgConfigHome = join(workspace, "xdg");
133
- const bundleDirectory = join(homeDirectory, "bundle");
134
-
135
- await mkdir(bundleDirectory, { recursive: true });
136
-
137
- if (process.platform !== "win32") {
138
- await symlink("target.txt", join(bundleDirectory, "token-link"));
139
-
140
- const symlinkConfig = createResolvedConfig({
141
- entries: [
142
- {
143
- kind: "directory",
144
- localPath: "~/bundle",
145
- mode: "normal",
146
- name: "bundle",
147
- overrides: {
148
- "token-link": "secret",
149
- },
150
- repoPath: "bundle",
151
- },
152
- ],
153
- homeDirectory,
154
- recipients: ["age1example"],
155
- xdgConfigHome,
156
- });
157
-
158
- await expect(buildLocalSnapshot(symlinkConfig)).rejects.toThrowError(
159
- /Secret sync paths must be regular files/u,
160
- );
161
- }
162
-
163
- const mismatchConfig = createResolvedConfig({
164
- entries: [
165
- {
166
- kind: "file",
167
- localPath: "~/bundle",
168
- mode: "normal",
169
- name: "bundle",
170
- repoPath: "bundle",
171
- },
172
- ],
173
- homeDirectory,
174
- recipients: ["age1example"],
175
- xdgConfigHome,
176
- });
177
-
178
- await expect(buildLocalSnapshot(mismatchConfig)).rejects.toThrowError(
179
- /expects a file/u,
180
- );
181
- });
182
-
183
- it("rejects local descendants that use the reserved secret suffix", async () => {
184
- const workspace = await createWorkspace();
185
- const homeDirectory = join(workspace, "home");
186
- const xdgConfigHome = join(workspace, "xdg");
187
- const bundleDirectory = join(homeDirectory, "bundle");
188
-
189
- await mkdir(bundleDirectory, { recursive: true });
190
- await writeFile(
191
- join(bundleDirectory, `token.txt${syncSecretArtifactSuffix}`),
192
- "reserved\n",
193
- );
194
-
195
- const config = createResolvedConfig({
196
- entries: [
197
- {
198
- kind: "directory",
199
- localPath: "~/bundle",
200
- mode: "normal",
201
- name: "bundle",
202
- repoPath: "bundle",
203
- },
204
- ],
205
- homeDirectory,
206
- recipients: ["age1example"],
207
- xdgConfigHome,
208
- });
209
-
210
- await expect(buildLocalSnapshot(config)).rejects.toThrowError(
211
- /reserved suffix/u,
212
- );
213
- });
214
-
215
- it("tracks executable secret files on non-Windows platforms", async () => {
216
- if (process.platform === "win32") {
217
- return;
218
- }
219
-
220
- const workspace = await createWorkspace();
221
- const homeDirectory = join(workspace, "home");
222
- const xdgConfigHome = join(workspace, "xdg");
223
- const bundleDirectory = join(homeDirectory, "bundle");
224
- const secretFile = join(bundleDirectory, "secret.sh");
225
-
226
- await mkdir(bundleDirectory, { recursive: true });
227
- await writeFile(secretFile, "#!/bin/sh\necho secret\n");
228
- await chmod(secretFile, 0o755);
229
-
230
- const config = createResolvedConfig({
231
- entries: [
232
- {
233
- kind: "directory",
234
- localPath: "~/bundle",
235
- mode: "normal",
236
- name: "bundle",
237
- overrides: {
238
- "secret.sh": "secret",
239
- },
240
- repoPath: "bundle",
241
- },
242
- ],
243
- homeDirectory,
244
- recipients: ["age1example"],
245
- xdgConfigHome,
246
- });
247
-
248
- const snapshot = await buildLocalSnapshot(config);
249
- const node = snapshot.get("bundle/secret.sh");
250
-
251
- expect(node).toMatchObject({
252
- executable: true,
253
- secret: true,
254
- type: "file",
255
- });
256
- });
257
-
258
- it("builds repo artifacts and collects existing artifact keys in the merged files tree", async () => {
259
- const workspace = await createWorkspace();
260
- const homeDirectory = join(workspace, "home");
261
- const xdgConfigHome = join(workspace, "xdg");
262
- const syncDirectory = join(workspace, "sync");
263
- const ageKeys = await createAgeKeyPair();
264
-
265
- await writeIdentityFile(xdgConfigHome, ageKeys.identity);
266
-
267
- const config = createResolvedConfig({
268
- entries: [
269
- {
270
- kind: "directory",
271
- localPath: "~/bundle",
272
- mode: "normal",
273
- name: "bundle",
274
- overrides: {
275
- "secret.txt": "secret",
276
- },
277
- repoPath: "bundle",
278
- },
279
- ],
280
- homeDirectory,
281
- recipients: [ageKeys.recipient],
282
- xdgConfigHome,
283
- });
284
-
285
- const snapshot = new Map([
286
- ["bundle", { type: "directory" as const }],
287
- [
288
- "bundle/plain.txt",
289
- {
290
- contents: new TextEncoder().encode("plain\n"),
291
- executable: false,
292
- secret: false,
293
- type: "file" as const,
294
- },
295
- ],
296
- [
297
- "bundle/secret.txt",
298
- {
299
- contents: new TextEncoder().encode("secret\n"),
300
- executable: false,
301
- secret: true,
302
- type: "file" as const,
303
- },
304
- ],
305
- [
306
- "bundle/link",
307
- {
308
- linkTarget: "plain.txt",
309
- type: "symlink" as const,
310
- },
311
- ],
312
- ]);
313
-
314
- const artifacts = await buildRepoArtifacts(snapshot, config);
315
-
316
- await writeArtifactsToDirectory(
317
- resolveSyncArtifactsDirectoryPath(syncDirectory),
318
- artifacts,
319
- );
320
-
321
- expect(await collectExistingArtifactKeys(syncDirectory, config)).toEqual(
322
- new Set([
323
- "bundle/",
324
- "bundle/link",
325
- "bundle/plain.txt",
326
- `bundle/secret.txt${syncSecretArtifactSuffix}`,
327
- ]),
328
- );
329
- });
330
-
331
- it("rejects invalid repository secret state and preserves explicit children under ignored roots", async () => {
332
- const workspace = await createWorkspace();
333
- const xdgConfigHome = join(workspace, "xdg");
334
- const syncDirectory = join(workspace, "sync");
335
- const ageKeys = await createAgeKeyPair();
336
-
337
- await writeIdentityFile(xdgConfigHome, ageKeys.identity);
338
-
339
- const writeSecretFile = async (
340
- path: string,
341
- contents = new TextEncoder().encode("secret\n"),
342
- ) => {
343
- await mkdir(dirname(path), { recursive: true });
344
- await writeFile(
345
- path,
346
- await encryptSecretFile(contents, [ageKeys.recipient]),
347
- "utf8",
348
- );
349
- };
350
-
351
- const createConfig = (entries: SyncConfig["entries"]) => {
352
- return createResolvedConfig({
353
- entries,
354
- homeDirectory: join(workspace, "home"),
355
- recipients: [ageKeys.recipient],
356
- xdgConfigHome,
357
- });
358
- };
359
-
360
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
361
- await writeFile(
362
- join(syncDirectory, "files", "bundle", "secret.txt"),
363
- "plain\n",
364
- );
365
-
366
- await expect(
367
- buildRepositorySnapshot(
368
- syncDirectory,
369
- createConfig([
370
- {
371
- kind: "directory",
372
- localPath: "~/bundle",
373
- mode: "normal",
374
- name: "bundle",
375
- overrides: {
376
- "secret.txt": "secret",
377
- },
378
- repoPath: "bundle",
379
- },
380
- ]),
381
- ),
382
- ).rejects.toThrowError(/stored in plain text/u);
383
-
384
- await rm(syncDirectory, { force: true, recursive: true });
385
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
386
- await writeFile(
387
- join(
388
- syncDirectory,
389
- "files",
390
- "bundle",
391
- `plain.txt${syncSecretArtifactSuffix}`,
392
- ),
393
- "ciphertext ignored before decrypt",
394
- "utf8",
395
- );
396
-
397
- await expect(
398
- buildRepositorySnapshot(
399
- syncDirectory,
400
- createConfig([
401
- {
402
- kind: "directory",
403
- localPath: "~/bundle",
404
- mode: "normal",
405
- name: "bundle",
406
- repoPath: "bundle",
407
- },
408
- ]),
409
- ),
410
- ).rejects.toThrowError(/stored in secret form/u);
411
-
412
- await rm(syncDirectory, { force: true, recursive: true });
413
- await mkdir(
414
- join(
415
- syncDirectory,
416
- "files",
417
- "bundle",
418
- `broken${syncSecretArtifactSuffix}`,
419
- ),
420
- { recursive: true },
421
- );
422
- await writeFile(
423
- join(
424
- syncDirectory,
425
- "files",
426
- "bundle",
427
- `broken${syncSecretArtifactSuffix}`,
428
- "value.txt",
429
- ),
430
- "oops\n",
431
- );
432
-
433
- await expect(
434
- buildRepositorySnapshot(
435
- syncDirectory,
436
- createConfig([
437
- {
438
- kind: "directory",
439
- localPath: "~/bundle",
440
- mode: "normal",
441
- name: "bundle",
442
- repoPath: "bundle",
443
- },
444
- ]),
445
- ),
446
- ).rejects.toThrowError(/reserved suffix/u);
447
-
448
- await rm(syncDirectory, { force: true, recursive: true });
449
- await mkdir(join(syncDirectory, "files", "other"), { recursive: true });
450
- await writeFile(
451
- join(
452
- syncDirectory,
453
- "files",
454
- "other",
455
- `token.txt${syncSecretArtifactSuffix}`,
456
- ),
457
- "ignored\n",
458
- "utf8",
459
- );
460
-
461
- await expect(
462
- buildRepositorySnapshot(
463
- syncDirectory,
464
- createConfig([
465
- {
466
- kind: "directory",
467
- localPath: "~/bundle",
468
- mode: "normal",
469
- name: "bundle",
470
- repoPath: "bundle",
471
- },
472
- ]),
473
- ),
474
- ).rejects.toThrowError(/Unmanaged sync path/u);
475
-
476
- if (process.platform !== "win32") {
477
- await rm(syncDirectory, { force: true, recursive: true });
478
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
479
- await symlink(
480
- join(workspace, "target.secret"),
481
- join(
482
- syncDirectory,
483
- "files",
484
- "bundle",
485
- `token.txt${syncSecretArtifactSuffix}`,
486
- ),
487
- );
488
-
489
- await expect(
490
- buildRepositorySnapshot(
491
- syncDirectory,
492
- createConfig([
493
- {
494
- kind: "directory",
495
- localPath: "~/bundle",
496
- mode: "secret",
497
- name: "bundle",
498
- repoPath: "bundle",
499
- },
500
- ]),
501
- ),
502
- ).rejects.toThrowError(/must be regular files, not symlinks/u);
503
- }
504
-
505
- await rm(syncDirectory, { force: true, recursive: true });
506
- await mkdir(join(syncDirectory, "files"), { recursive: true });
507
- await writeFile(
508
- join(syncDirectory, "files", "bundle"),
509
- "not a directory\n",
510
- );
511
-
512
- await expect(
513
- buildRepositorySnapshot(
514
- syncDirectory,
515
- createConfig([
516
- {
517
- kind: "directory",
518
- localPath: "~/bundle",
519
- mode: "normal",
520
- name: "bundle",
521
- repoPath: "bundle",
522
- },
523
- ]),
524
- ),
525
- ).rejects.toThrowError(/is not stored as a directory/u);
526
-
527
- await rm(syncDirectory, { force: true, recursive: true });
528
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
529
- await writeFile(
530
- join(syncDirectory, "files", "bundle", "keep.txt"),
531
- "keep\n",
532
- );
533
-
534
- const snapshot = await buildRepositorySnapshot(
535
- syncDirectory,
536
- createConfig([
537
- {
538
- kind: "directory",
539
- localPath: "~/bundle",
540
- mode: "ignore",
541
- name: "bundle",
542
- overrides: {
543
- "keep.txt": "normal",
544
- },
545
- repoPath: "bundle",
546
- },
547
- ]),
548
- );
549
-
550
- expect(
551
- [...snapshot.keys()].sort((left, right) => {
552
- return left.localeCompare(right);
553
- }),
554
- ).toEqual(["bundle", "bundle/keep.txt"]);
555
-
556
- await rm(syncDirectory, { force: true, recursive: true });
557
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
558
- await writeFile(
559
- join(
560
- syncDirectory,
561
- "files",
562
- "bundle",
563
- `keep.txt${syncSecretArtifactSuffix}`,
564
- ),
565
- "not really ciphertext",
566
- "utf8",
567
- );
568
-
569
- await expect(
570
- buildRepositorySnapshot(
571
- syncDirectory,
572
- createConfig([
573
- {
574
- kind: "directory",
575
- localPath: "~/bundle",
576
- mode: "normal",
577
- name: "bundle",
578
- overrides: {
579
- "keep.txt": "secret",
580
- },
581
- repoPath: "bundle",
582
- },
583
- ]),
584
- ),
585
- ).rejects.toThrowError();
586
-
587
- await rm(syncDirectory, { force: true, recursive: true });
588
- await mkdir(join(syncDirectory, "files", "bundle"), { recursive: true });
589
- await writeSecretFile(
590
- join(
591
- syncDirectory,
592
- "files",
593
- "bundle",
594
- `keep.txt${syncSecretArtifactSuffix}`,
595
- ),
596
- );
597
-
598
- const decrypted = await buildRepositorySnapshot(
599
- syncDirectory,
600
- createConfig([
601
- {
602
- kind: "directory",
603
- localPath: "~/bundle",
604
- mode: "normal",
605
- name: "bundle",
606
- overrides: {
607
- "keep.txt": "secret",
608
- },
609
- repoPath: "bundle",
610
- },
611
- ]),
612
- );
613
-
614
- expect(
615
- new TextDecoder().decode(
616
- (decrypted.get("bundle/keep.txt") as { contents: Uint8Array }).contents,
617
- ),
618
- ).toBe("secret\n");
619
- });
620
-
621
- it("validates materialization conflicts, preserves ignored locals, and counts pull results", async () => {
622
- const workspace = await createWorkspace();
623
- const homeDirectory = join(workspace, "home");
624
- const xdgConfigHome = join(workspace, "xdg");
625
- const bundleDirectory = join(homeDirectory, "bundle");
626
-
627
- await mkdir(bundleDirectory, { recursive: true });
628
- await writeFile(join(bundleDirectory, "keep.txt"), "keep\n");
629
- await writeFile(join(bundleDirectory, "ignored.txt"), "ignored\n");
630
-
631
- const config = createResolvedConfig({
632
- entries: [
633
- {
634
- kind: "directory",
635
- localPath: "~/bundle",
636
- mode: "ignore",
637
- name: "bundle",
638
- overrides: {
639
- "keep.txt": "normal",
640
- },
641
- repoPath: "bundle",
642
- },
643
- {
644
- kind: "file",
645
- localPath: "~/.ignored-file",
646
- mode: "ignore",
647
- name: ".ignored-file",
648
- repoPath: ".ignored-file",
649
- },
650
- ],
651
- homeDirectory,
652
- recipients: ["age1example"],
653
- xdgConfigHome,
654
- });
655
- const bundleEntry = config.entries.find((entry) => {
656
- return entry.repoPath === "bundle";
657
- });
658
- const ignoredFileEntry = config.entries.find((entry) => {
659
- return entry.repoPath === ".ignored-file";
660
- });
661
-
662
- if (bundleEntry === undefined || ignoredFileEntry === undefined) {
663
- throw new Error("Expected test sync entries to be present.");
664
- }
665
-
666
- await writeFile(join(homeDirectory, ".ignored-file"), "leave me alone\n");
667
-
668
- expect(() => {
669
- buildEntryMaterialization(
670
- ignoredFileEntry,
671
- new Map([[".ignored-file", { type: "directory" as const }]]),
672
- );
673
- }).toThrowError(/resolves to a directory/u);
674
-
675
- expect(() => {
676
- buildEntryMaterialization(
677
- bundleEntry,
678
- new Map([
679
- [
680
- "bundle",
681
- {
682
- contents: new TextEncoder().encode("wrong"),
683
- executable: false,
684
- secret: false,
685
- type: "file" as const,
686
- },
687
- ],
688
- ]),
689
- );
690
- }).toThrowError(/resolves to a file/u);
691
-
692
- expect(
693
- await countDeletedLocalNodes(
694
- bundleEntry,
695
- new Set(["bundle/", "bundle/keep.txt"]),
696
- config,
697
- ),
698
- ).toBe(0);
699
-
700
- await applyEntryMaterialization(
701
- ignoredFileEntry,
702
- {
703
- desiredKeys: new Set<string>(),
704
- type: "absent",
705
- },
706
- config,
707
- );
708
-
709
- expect(await readFile(join(homeDirectory, ".ignored-file"), "utf8")).toBe(
710
- "leave me alone\n",
711
- );
712
-
713
- expect(
714
- buildPullCounts([
715
- {
716
- desiredKeys: new Set(["plain.txt"]),
717
- node: {
718
- contents: new TextEncoder().encode("plain"),
719
- executable: false,
720
- secret: false,
721
- type: "file",
722
- },
723
- type: "file",
724
- },
725
- {
726
- desiredKeys: new Set(["secret.txt"]),
727
- node: {
728
- contents: new TextEncoder().encode("secret"),
729
- executable: false,
730
- secret: true,
731
- type: "file",
732
- },
733
- type: "file",
734
- },
735
- {
736
- desiredKeys: new Set(["bundle/", "bundle/link"]),
737
- nodes: new Map([
738
- [
739
- "link",
740
- {
741
- linkTarget: "plain.txt",
742
- type: "symlink",
743
- },
744
- ],
745
- ]),
746
- type: "directory",
747
- },
748
- ]),
749
- ).toEqual({
750
- decryptedFileCount: 1,
751
- directoryCount: 1,
752
- plainFileCount: 1,
753
- symlinkCount: 1,
754
- });
755
- });
756
- });