@tinyrack/devsync 1.1.0 → 1.3.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 (220) 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/platform.d.ts +11 -0
  55. package/dist/config/platform.d.ts.map +1 -0
  56. package/dist/config/platform.js +19 -0
  57. package/dist/config/platform.js.map +1 -0
  58. package/dist/config/sync.d.ts +107 -0
  59. package/dist/config/sync.d.ts.map +1 -0
  60. package/dist/config/sync.js +424 -0
  61. package/dist/config/sync.js.map +1 -0
  62. package/dist/config/xdg.d.ts +14 -0
  63. package/dist/config/xdg.d.ts.map +1 -0
  64. package/dist/config/xdg.js +102 -0
  65. package/dist/config/xdg.js.map +1 -0
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/{src/index.ts → dist/index.js} +1 -1
  69. package/dist/index.js.map +1 -0
  70. package/dist/lib/file-mode.d.ts +3 -0
  71. package/dist/lib/file-mode.d.ts.map +1 -0
  72. package/dist/lib/file-mode.js +7 -0
  73. package/dist/lib/file-mode.js.map +1 -0
  74. package/dist/lib/output.d.ts +31 -0
  75. package/dist/lib/output.d.ts.map +1 -0
  76. package/dist/lib/output.js +198 -0
  77. package/dist/lib/output.js.map +1 -0
  78. package/dist/lib/path.d.ts +5 -0
  79. package/dist/lib/path.d.ts.map +1 -0
  80. package/dist/lib/path.js +25 -0
  81. package/dist/lib/path.js.map +1 -0
  82. package/dist/lib/string.d.ts +2 -0
  83. package/dist/lib/string.d.ts.map +1 -0
  84. package/dist/lib/string.js +4 -0
  85. package/dist/lib/string.js.map +1 -0
  86. package/dist/lib/validation.d.ts +3 -0
  87. package/dist/lib/validation.d.ts.map +1 -0
  88. package/dist/lib/validation.js +9 -0
  89. package/dist/lib/validation.js.map +1 -0
  90. package/dist/services/add.d.ts +20 -0
  91. package/dist/services/add.d.ts.map +1 -0
  92. package/dist/services/add.js +161 -0
  93. package/dist/services/add.js.map +1 -0
  94. package/dist/services/config-file.d.ts +45 -0
  95. package/dist/services/config-file.d.ts.map +1 -0
  96. package/dist/services/config-file.js +35 -0
  97. package/dist/services/config-file.js.map +1 -0
  98. package/dist/services/crypto.d.ts +9 -0
  99. package/dist/services/crypto.d.ts.map +1 -0
  100. package/dist/services/crypto.js +75 -0
  101. package/dist/services/crypto.js.map +1 -0
  102. package/dist/services/doctor.d.ts +16 -0
  103. package/dist/services/doctor.d.ts.map +1 -0
  104. package/dist/services/doctor.js +84 -0
  105. package/dist/services/doctor.js.map +1 -0
  106. package/dist/services/error.d.ts +14 -0
  107. package/dist/services/error.d.ts.map +1 -0
  108. package/dist/services/error.js +38 -0
  109. package/dist/services/error.js.map +1 -0
  110. package/dist/services/filesystem.d.ts +15 -0
  111. package/dist/services/filesystem.d.ts.map +1 -0
  112. package/dist/services/filesystem.js +113 -0
  113. package/dist/services/filesystem.js.map +1 -0
  114. package/dist/services/forget.d.ts +14 -0
  115. package/dist/services/forget.d.ts.map +1 -0
  116. package/dist/services/forget.js +124 -0
  117. package/dist/services/forget.js.map +1 -0
  118. package/dist/services/git.d.ts +10 -0
  119. package/dist/services/git.d.ts.map +1 -0
  120. package/dist/services/git.js +57 -0
  121. package/dist/services/git.js.map +1 -0
  122. package/dist/services/init.d.ts +19 -0
  123. package/dist/services/init.d.ts.map +1 -0
  124. package/dist/services/init.js +203 -0
  125. package/dist/services/init.js.map +1 -0
  126. package/dist/services/local-materialization.d.ts +28 -0
  127. package/dist/services/local-materialization.d.ts.map +1 -0
  128. package/dist/services/local-materialization.js +262 -0
  129. package/dist/services/local-materialization.js.map +1 -0
  130. package/dist/services/local-snapshot.d.ts +25 -0
  131. package/dist/services/local-snapshot.d.ts.map +1 -0
  132. package/dist/services/local-snapshot.js +93 -0
  133. package/dist/services/local-snapshot.js.map +1 -0
  134. package/dist/services/machine.d.ts +40 -0
  135. package/dist/services/machine.d.ts.map +1 -0
  136. package/dist/services/machine.js +113 -0
  137. package/dist/services/machine.js.map +1 -0
  138. package/dist/services/paths.d.ts +13 -0
  139. package/dist/services/paths.d.ts.map +1 -0
  140. package/dist/services/paths.js +71 -0
  141. package/dist/services/paths.js.map +1 -0
  142. package/dist/services/pull.d.ts +28 -0
  143. package/dist/services/pull.d.ts.map +1 -0
  144. package/dist/services/pull.js +67 -0
  145. package/dist/services/pull.js.map +1 -0
  146. package/dist/services/push.d.ts +35 -0
  147. package/dist/services/push.d.ts.map +1 -0
  148. package/dist/services/push.js +96 -0
  149. package/dist/services/push.js.map +1 -0
  150. package/dist/services/repo-artifacts.d.ts +52 -0
  151. package/dist/services/repo-artifacts.d.ts.map +1 -0
  152. package/dist/services/repo-artifacts.js +251 -0
  153. package/dist/services/repo-artifacts.js.map +1 -0
  154. package/dist/services/repo-snapshot.d.ts +6 -0
  155. package/dist/services/repo-snapshot.d.ts.map +1 -0
  156. package/dist/services/repo-snapshot.js +163 -0
  157. package/dist/services/repo-snapshot.js.map +1 -0
  158. package/dist/services/runtime.d.ts +40 -0
  159. package/dist/services/runtime.d.ts.map +1 -0
  160. package/dist/services/runtime.js +71 -0
  161. package/dist/services/runtime.js.map +1 -0
  162. package/dist/services/set.d.ts +38 -0
  163. package/dist/services/set.d.ts.map +1 -0
  164. package/dist/services/set.js +184 -0
  165. package/dist/services/set.js.map +1 -0
  166. package/dist/services/status.d.ts +30 -0
  167. package/dist/services/status.d.ts.map +1 -0
  168. package/dist/services/status.js +35 -0
  169. package/dist/services/status.js.map +1 -0
  170. package/package.json +15 -7
  171. package/src/cli/commands/add.ts +0 -40
  172. package/src/cli/commands/cd.ts +0 -80
  173. package/src/cli/commands/doctor.ts +0 -20
  174. package/src/cli/commands/forget.ts +0 -32
  175. package/src/cli/commands/index.ts +0 -23
  176. package/src/cli/commands/init.ts +0 -43
  177. package/src/cli/commands/list.ts +0 -17
  178. package/src/cli/commands/pull.ts +0 -31
  179. package/src/cli/commands/push.ts +0 -31
  180. package/src/cli/commands/set.ts +0 -47
  181. package/src/cli/commands/status.ts +0 -18
  182. package/src/cli/sync-output.test.ts +0 -173
  183. package/src/cli/sync-output.ts +0 -200
  184. package/src/config/sync.test.ts +0 -609
  185. package/src/config/sync.ts +0 -572
  186. package/src/config/xdg.ts +0 -138
  187. package/src/lib/string.test.ts +0 -13
  188. package/src/lib/string.ts +0 -3
  189. package/src/lib/validation.test.ts +0 -32
  190. package/src/lib/validation.ts +0 -11
  191. package/src/services/add.ts +0 -178
  192. package/src/services/config-file.test.ts +0 -161
  193. package/src/services/config-file.ts +0 -101
  194. package/src/services/crypto.test.ts +0 -132
  195. package/src/services/crypto.ts +0 -83
  196. package/src/services/doctor.ts +0 -142
  197. package/src/services/error.ts +0 -6
  198. package/src/services/filesystem.test.ts +0 -171
  199. package/src/services/filesystem.ts +0 -183
  200. package/src/services/forget.ts +0 -261
  201. package/src/services/git.test.ts +0 -83
  202. package/src/services/git.ts +0 -74
  203. package/src/services/init.test.ts +0 -109
  204. package/src/services/init.ts +0 -244
  205. package/src/services/list.ts +0 -63
  206. package/src/services/local-materialization.ts +0 -421
  207. package/src/services/local-snapshot.ts +0 -173
  208. package/src/services/paths.test.ts +0 -74
  209. package/src/services/paths.ts +0 -98
  210. package/src/services/pull.ts +0 -144
  211. package/src/services/push.ts +0 -168
  212. package/src/services/repo-artifacts.ts +0 -262
  213. package/src/services/repo-snapshot.ts +0 -197
  214. package/src/services/runtime.ts +0 -57
  215. package/src/services/set.ts +0 -383
  216. package/src/services/status.ts +0 -57
  217. package/src/services/sync.dry-run.test.ts +0 -179
  218. package/src/services/sync.runtime.test.ts +0 -756
  219. package/src/services/sync.service.test.ts +0 -1169
  220. package/src/test/helpers/sync-fixture.ts +0 -47
@@ -1,572 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { isAbsolute, join, posix, relative } from "node:path";
3
-
4
- import { z } from "zod";
5
- import {
6
- resolveConfiguredAbsolutePath,
7
- resolveDevsyncSyncDirectory,
8
- resolveHomeConfiguredAbsolutePath,
9
- resolveHomeDirectory,
10
- } from "#app/config/xdg.ts";
11
- import { ensureTrailingNewline } from "#app/lib/string.ts";
12
- import { formatInputIssues } from "#app/lib/validation.ts";
13
- import { DevsyncError } from "#app/services/error.ts";
14
- import { doPathsOverlap } from "#app/services/paths.ts";
15
-
16
- export const syncConfigFileName = "config.json";
17
- export const syncArtifactsDirectoryName = "files";
18
- export const syncSecretArtifactSuffix = ".devsync.secret";
19
-
20
- const syncEntryKinds = ["file", "directory"] as const;
21
- export const syncModes = ["normal", "secret", "ignore"] as const;
22
-
23
- const requiredTrimmedStringSchema = z
24
- .string()
25
- .trim()
26
- .min(1, "Value must not be empty.");
27
-
28
- const syncConfigEntrySchema = z
29
- .object({
30
- kind: z.enum(syncEntryKinds),
31
- localPath: requiredTrimmedStringSchema,
32
- mode: z.enum(syncModes),
33
- name: requiredTrimmedStringSchema,
34
- overrides: z
35
- .record(requiredTrimmedStringSchema, z.enum(syncModes))
36
- .optional(),
37
- repoPath: requiredTrimmedStringSchema,
38
- })
39
- .strict();
40
-
41
- const syncConfigSchema = z
42
- .object({
43
- version: z.literal(1),
44
- age: z
45
- .object({
46
- recipients: z
47
- .array(requiredTrimmedStringSchema)
48
- .min(1, "At least one age recipient is required."),
49
- identityFile: requiredTrimmedStringSchema,
50
- })
51
- .strict(),
52
- entries: z.array(syncConfigEntrySchema),
53
- })
54
- .strict();
55
-
56
- export type SyncConfigEntryKind = (typeof syncEntryKinds)[number];
57
- export type SyncMode = (typeof syncModes)[number];
58
- export type SyncConfig = z.infer<typeof syncConfigSchema>;
59
- export type SyncOverrideMatch = "exact" | "subtree";
60
-
61
- export type ResolvedSyncOverride = Readonly<{
62
- match: SyncOverrideMatch;
63
- mode: SyncMode;
64
- path: string;
65
- }>;
66
-
67
- export type ResolvedSyncConfigEntry = Readonly<{
68
- configuredLocalPath: string;
69
- kind: SyncConfigEntryKind;
70
- localPath: string;
71
- mode: SyncMode;
72
- name: string;
73
- overrides: readonly ResolvedSyncOverride[];
74
- repoPath: string;
75
- }>;
76
-
77
- export type ResolvedSyncConfig = Readonly<{
78
- age: Readonly<{
79
- configuredIdentityFile: string;
80
- identityFile: string;
81
- recipients: readonly string[];
82
- }>;
83
- entries: readonly ResolvedSyncConfigEntry[];
84
- version: 1;
85
- }>;
86
-
87
- export const normalizeSyncRepoPath = (value: string) => {
88
- const normalizedValue = posix.normalize(value.replaceAll("\\", "/"));
89
-
90
- if (
91
- normalizedValue === "" ||
92
- normalizedValue === "." ||
93
- normalizedValue.startsWith("../") ||
94
- normalizedValue.includes("/../") ||
95
- normalizedValue.startsWith("/")
96
- ) {
97
- throw new DevsyncError(
98
- `Repository path must be a relative POSIX path without '..': ${value}`,
99
- );
100
- }
101
-
102
- if (hasReservedSyncArtifactSuffixSegment(normalizedValue)) {
103
- throw new DevsyncError(
104
- `Repository path must not use the reserved suffix ${syncSecretArtifactSuffix}: ${value}`,
105
- );
106
- }
107
-
108
- return normalizedValue;
109
- };
110
-
111
- export const normalizeSyncOverridePath = (
112
- value: string,
113
- description = "Override path",
114
- ) => {
115
- const posixValue = value.replaceAll("\\", "/");
116
-
117
- if (
118
- posixValue === "" ||
119
- posixValue === "." ||
120
- posixValue === ".." ||
121
- posixValue.startsWith("../") ||
122
- posixValue.includes("/../") ||
123
- posixValue.startsWith("/")
124
- ) {
125
- throw new DevsyncError(
126
- `${description} must be a relative POSIX path without '..': ${value}`,
127
- );
128
- }
129
-
130
- const normalizedValue = posix.normalize(posixValue);
131
-
132
- if (
133
- normalizedValue === "" ||
134
- normalizedValue === "." ||
135
- normalizedValue === ".." ||
136
- normalizedValue.startsWith("../") ||
137
- normalizedValue.includes("/../") ||
138
- normalizedValue.startsWith("/")
139
- ) {
140
- throw new DevsyncError(
141
- `${description} must be a relative POSIX path without '..': ${value}`,
142
- );
143
- }
144
-
145
- if (hasReservedSyncArtifactSuffixSegment(normalizedValue)) {
146
- throw new DevsyncError(
147
- `${description} must not use the reserved suffix ${syncSecretArtifactSuffix}: ${value}`,
148
- );
149
- }
150
-
151
- return normalizedValue;
152
- };
153
-
154
- export const hasReservedSyncArtifactSuffixSegment = (value: string) => {
155
- return value
156
- .replaceAll("\\", "/")
157
- .split("/")
158
- .some((segment) => segment.endsWith(syncSecretArtifactSuffix));
159
- };
160
-
161
- export const normalizeSyncOverrideSelector = (
162
- value: string,
163
- description = "Override selector",
164
- ): ResolvedSyncOverride => {
165
- const posixValue = value.replaceAll("\\", "/");
166
- const match = posixValue.endsWith("/") ? "subtree" : "exact";
167
- const trimmedValue =
168
- match === "subtree" ? posixValue.replace(/\/+$/u, "") : posixValue;
169
-
170
- return {
171
- match,
172
- mode: "normal",
173
- path: normalizeSyncOverridePath(trimmedValue, description),
174
- };
175
- };
176
-
177
- export const formatSyncOverrideSelector = (
178
- override: Pick<ResolvedSyncOverride, "match" | "path">,
179
- ) => {
180
- return override.match === "subtree" ? `${override.path}/` : override.path;
181
- };
182
-
183
- export const findOwningSyncEntry = (
184
- config: Pick<ResolvedSyncConfig, "entries">,
185
- repoPath: string,
186
- ): ResolvedSyncConfigEntry | undefined => {
187
- return config.entries.find((entry) => {
188
- return (
189
- entry.repoPath === repoPath ||
190
- (entry.kind === "directory" && repoPath.startsWith(`${entry.repoPath}/`))
191
- );
192
- });
193
- };
194
-
195
- export const resolveEntryRelativeRepoPath = (
196
- entry: Pick<ResolvedSyncConfigEntry, "kind" | "repoPath">,
197
- repoPath: string,
198
- ) => {
199
- if (entry.kind === "file") {
200
- return repoPath === entry.repoPath ? "" : undefined;
201
- }
202
-
203
- if (repoPath === entry.repoPath) {
204
- return "";
205
- }
206
-
207
- if (!repoPath.startsWith(`${entry.repoPath}/`)) {
208
- return undefined;
209
- }
210
-
211
- return repoPath.slice(entry.repoPath.length + 1);
212
- };
213
-
214
- const getRulePathDepth = (path: string) => {
215
- return path.split("/").length;
216
- };
217
-
218
- const compareOverrideSpecificity = (
219
- left: Pick<ResolvedSyncOverride, "match" | "path">,
220
- right: Pick<ResolvedSyncOverride, "match" | "path">,
221
- ) => {
222
- const depthComparison =
223
- getRulePathDepth(right.path) - getRulePathDepth(left.path);
224
-
225
- if (depthComparison !== 0) {
226
- return depthComparison;
227
- }
228
-
229
- if (left.match === right.match) {
230
- return 0;
231
- }
232
-
233
- return left.match === "exact" ? -1 : 1;
234
- };
235
-
236
- const matchesOverride = (
237
- override: Pick<ResolvedSyncOverride, "match" | "path">,
238
- relativePath: string,
239
- ) => {
240
- if (relativePath === "") {
241
- return false;
242
- }
243
-
244
- if (override.match === "exact") {
245
- return override.path === relativePath;
246
- }
247
-
248
- return (
249
- override.path === relativePath ||
250
- relativePath.startsWith(`${override.path}/`)
251
- );
252
- };
253
-
254
- export const resolveRelativeSyncMode = (
255
- mode: SyncMode,
256
- overrides: readonly Pick<ResolvedSyncOverride, "match" | "mode" | "path">[],
257
- relativePath: string,
258
- ) => {
259
- if (relativePath === "") {
260
- return mode;
261
- }
262
-
263
- const matchingOverride = [...overrides]
264
- .filter((override) => {
265
- return matchesOverride(override, relativePath);
266
- })
267
- .sort(compareOverrideSpecificity)[0];
268
-
269
- return matchingOverride?.mode ?? mode;
270
- };
271
-
272
- const resolveSyncEntryLocalPath = (
273
- value: string,
274
- environment: NodeJS.ProcessEnv,
275
- ) => {
276
- const homeDirectory = resolveHomeDirectory(environment);
277
- let resolvedLocalPath: string;
278
-
279
- try {
280
- resolvedLocalPath = resolveHomeConfiguredAbsolutePath(value, environment);
281
- } catch (error: unknown) {
282
- throw new DevsyncError(
283
- error instanceof Error
284
- ? error.message
285
- : `Invalid sync entry local path: ${value}`,
286
- );
287
- }
288
-
289
- const relativePath = relative(homeDirectory, resolvedLocalPath);
290
-
291
- if (relativePath === "") {
292
- throw new DevsyncError(
293
- `Sync entry local path must be inside ${homeDirectory}, not the home directory itself: ${value}`,
294
- );
295
- }
296
-
297
- if (
298
- isAbsolute(relativePath) ||
299
- relativePath.startsWith("..") ||
300
- relativePath === ".."
301
- ) {
302
- throw new DevsyncError(
303
- `Sync entry local path must be inside ${homeDirectory}: ${value}`,
304
- );
305
- }
306
-
307
- return resolvedLocalPath;
308
- };
309
-
310
- const resolveConfiguredIdentityFile = (
311
- value: string,
312
- environment: NodeJS.ProcessEnv,
313
- ) => {
314
- try {
315
- return resolveConfiguredAbsolutePath(value, environment);
316
- } catch (error: unknown) {
317
- throw new DevsyncError(
318
- error instanceof Error
319
- ? error.message
320
- : `Invalid sync age identity file path: ${value}`,
321
- );
322
- }
323
- };
324
-
325
- const validateUniqueNames = (entries: readonly ResolvedSyncConfigEntry[]) => {
326
- const seenNames = new Set<string>();
327
-
328
- for (const entry of entries) {
329
- if (seenNames.has(entry.name)) {
330
- throw new DevsyncError(`Duplicate sync entry name: ${entry.name}`);
331
- }
332
-
333
- seenNames.add(entry.name);
334
- }
335
- };
336
-
337
- const validatePathOverlaps = (
338
- entries: readonly ResolvedSyncConfigEntry[],
339
- property: "localPath" | "repoPath",
340
- description: string,
341
- ) => {
342
- for (let index = 0; index < entries.length; index += 1) {
343
- const currentEntry = entries[index];
344
-
345
- if (currentEntry === undefined) {
346
- continue;
347
- }
348
-
349
- for (
350
- let otherIndex = index + 1;
351
- otherIndex < entries.length;
352
- otherIndex += 1
353
- ) {
354
- const otherEntry = entries[otherIndex];
355
-
356
- if (otherEntry === undefined) {
357
- continue;
358
- }
359
-
360
- const currentValue = currentEntry[property];
361
- const otherValue = otherEntry[property];
362
- const overlaps =
363
- property === "repoPath"
364
- ? currentValue === otherValue ||
365
- currentValue.startsWith(`${otherValue}/`) ||
366
- otherValue.startsWith(`${currentValue}/`)
367
- : doPathsOverlap(currentValue, otherValue);
368
-
369
- if (overlaps) {
370
- throw new DevsyncError(
371
- `${description} paths must not overlap: ${currentEntry.name} (${currentValue}) and ${otherEntry.name} (${otherValue})`,
372
- );
373
- }
374
- }
375
- }
376
- };
377
-
378
- const validateOverrides = (entry: ResolvedSyncConfigEntry) => {
379
- if (entry.kind === "file" && entry.overrides.length > 0) {
380
- throw new DevsyncError(
381
- `File sync entries must not define overrides: ${entry.name}`,
382
- );
383
- }
384
-
385
- const seenOverrides = new Set<string>();
386
-
387
- for (const override of entry.overrides) {
388
- const key = formatSyncOverrideSelector(override);
389
-
390
- if (seenOverrides.has(key)) {
391
- throw new DevsyncError(
392
- `Duplicate sync override for ${entry.name}: ${key}`,
393
- );
394
- }
395
-
396
- seenOverrides.add(key);
397
- }
398
- };
399
-
400
- export const parseSyncConfig = (
401
- input: unknown,
402
- environment: NodeJS.ProcessEnv = process.env,
403
- ): ResolvedSyncConfig => {
404
- const result = syncConfigSchema.safeParse(input);
405
-
406
- if (!result.success) {
407
- throw new DevsyncError(formatInputIssues(result.error.issues));
408
- }
409
-
410
- const entries = result.data.entries.map((entry) => {
411
- const resolvedEntry = {
412
- configuredLocalPath: entry.localPath,
413
- kind: entry.kind,
414
- localPath: resolveSyncEntryLocalPath(entry.localPath, environment),
415
- mode: entry.mode,
416
- name: entry.name,
417
- overrides: Object.entries(entry.overrides ?? {}).map(
418
- ([selector, overrideMode]) => {
419
- const normalizedSelector = normalizeSyncOverrideSelector(
420
- selector,
421
- "Entry override selector",
422
- );
423
-
424
- return {
425
- ...normalizedSelector,
426
- mode: overrideMode,
427
- } satisfies ResolvedSyncOverride;
428
- },
429
- ),
430
- repoPath: normalizeSyncRepoPath(entry.repoPath),
431
- } satisfies ResolvedSyncConfigEntry;
432
-
433
- validateOverrides(resolvedEntry);
434
-
435
- return resolvedEntry;
436
- });
437
-
438
- validateUniqueNames(entries);
439
- validatePathOverlaps(entries, "repoPath", "Repository");
440
- validatePathOverlaps(entries, "localPath", "Local");
441
-
442
- return {
443
- age: {
444
- configuredIdentityFile: result.data.age.identityFile,
445
- identityFile: resolveConfiguredIdentityFile(
446
- result.data.age.identityFile,
447
- environment,
448
- ),
449
- recipients: [...new Set(result.data.age.recipients)],
450
- },
451
- entries,
452
- version: 1,
453
- };
454
- };
455
-
456
- export const createInitialSyncConfig = (input: {
457
- identityFile: string;
458
- recipients: readonly string[];
459
- }): SyncConfig => {
460
- return {
461
- version: 1,
462
- age: {
463
- identityFile: input.identityFile,
464
- recipients: [
465
- ...new Set(input.recipients.map((recipient) => recipient.trim())),
466
- ],
467
- },
468
- entries: [],
469
- };
470
- };
471
-
472
- export const formatSyncConfig = (config: SyncConfig) => {
473
- return ensureTrailingNewline(JSON.stringify(config, null, 2));
474
- };
475
-
476
- export const resolveSyncConfigPath = (
477
- environment: NodeJS.ProcessEnv = process.env,
478
- ) => {
479
- return posix.join(
480
- resolveDevsyncSyncDirectory(environment).replaceAll("\\", "/"),
481
- syncConfigFileName,
482
- );
483
- };
484
-
485
- export const resolveSyncConfigFilePath = (
486
- syncDirectory: string = resolveDevsyncSyncDirectory(),
487
- ) => {
488
- return join(syncDirectory, syncConfigFileName);
489
- };
490
-
491
- export const resolveSyncArtifactsDirectoryPath = (syncDirectory: string) => {
492
- return join(syncDirectory, syncArtifactsDirectoryName);
493
- };
494
-
495
- export const readSyncConfig = async (
496
- syncDirectory: string = resolveDevsyncSyncDirectory(),
497
- environment: NodeJS.ProcessEnv = process.env,
498
- ) => {
499
- try {
500
- const contents = await readFile(
501
- resolveSyncConfigFilePath(syncDirectory),
502
- "utf8",
503
- );
504
-
505
- return parseSyncConfig(JSON.parse(contents) as unknown, environment);
506
- } catch (error: unknown) {
507
- if (error instanceof DevsyncError) {
508
- throw error;
509
- }
510
-
511
- if (error instanceof SyntaxError) {
512
- throw new DevsyncError(
513
- `Sync configuration is not valid JSON: ${error.message}`,
514
- );
515
- }
516
-
517
- throw new DevsyncError(
518
- error instanceof Error
519
- ? error.message
520
- : "Failed to read sync configuration.",
521
- );
522
- }
523
- };
524
-
525
- export const resolveSyncMode = (
526
- config: ResolvedSyncConfig,
527
- repoPath: string,
528
- ): SyncMode | undefined => {
529
- const entry = findOwningSyncEntry(config, repoPath);
530
-
531
- if (entry === undefined) {
532
- return undefined;
533
- }
534
-
535
- const relativePath = resolveEntryRelativeRepoPath(entry, repoPath);
536
-
537
- if (relativePath === undefined) {
538
- return undefined;
539
- }
540
-
541
- return resolveRelativeSyncMode(entry.mode, entry.overrides, relativePath);
542
- };
543
-
544
- export const isIgnoredSyncPath = (
545
- config: ResolvedSyncConfig,
546
- repoPath: string,
547
- ) => {
548
- return resolveSyncMode(config, repoPath) === "ignore";
549
- };
550
-
551
- export const isSecretSyncPath = (
552
- config: ResolvedSyncConfig,
553
- repoPath: string,
554
- ) => {
555
- return resolveSyncMode(config, repoPath) === "secret";
556
- };
557
-
558
- export const resolveManagedSyncMode = (
559
- config: ResolvedSyncConfig,
560
- repoPath: string,
561
- context?: string,
562
- ) => {
563
- const mode = resolveSyncMode(config, repoPath);
564
-
565
- if (mode === undefined) {
566
- throw new DevsyncError(
567
- `Unmanaged sync path${context ? ` found ${context}` : ""}: ${repoPath}`,
568
- );
569
- }
570
-
571
- return mode;
572
- };
package/src/config/xdg.ts DELETED
@@ -1,138 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { isAbsolute, resolve } from "node:path";
3
-
4
- const readTrimmedEnvironmentValue = (
5
- environment: NodeJS.ProcessEnv,
6
- key: string,
7
- ) => {
8
- const value = environment[key];
9
-
10
- if (value === undefined) {
11
- return undefined;
12
- }
13
-
14
- const trimmedValue = value.trim();
15
-
16
- return trimmedValue === "" ? undefined : trimmedValue;
17
- };
18
-
19
- const bracedXdgConfigHomeToken = "$" + "{XDG_CONFIG_HOME}";
20
- const bracedXdgConfigHomePrefix = `${bracedXdgConfigHomeToken}/`;
21
-
22
- export const resolveHomeDirectory = (
23
- environment: NodeJS.ProcessEnv = process.env,
24
- ) => {
25
- const configuredValue = readTrimmedEnvironmentValue(environment, "HOME");
26
-
27
- if (configuredValue !== undefined) {
28
- return resolve(configuredValue);
29
- }
30
-
31
- return resolve(homedir());
32
- };
33
-
34
- export const resolveXdgConfigHome = (
35
- environment: NodeJS.ProcessEnv = process.env,
36
- ) => {
37
- const configuredValue = readTrimmedEnvironmentValue(
38
- environment,
39
- "XDG_CONFIG_HOME",
40
- );
41
-
42
- if (configuredValue !== undefined) {
43
- return resolve(configuredValue);
44
- }
45
-
46
- return resolve(resolveHomeDirectory(environment), ".config");
47
- };
48
-
49
- export const resolveDevsyncConfigDirectory = (
50
- environment: NodeJS.ProcessEnv = process.env,
51
- ) => {
52
- return resolve(resolveXdgConfigHome(environment), "devsync");
53
- };
54
-
55
- export const resolveDevsyncSyncDirectory = (
56
- environment: NodeJS.ProcessEnv = process.env,
57
- ) => {
58
- return resolve(resolveDevsyncConfigDirectory(environment), "sync");
59
- };
60
-
61
- export const resolveDevsyncAgeDirectory = (
62
- environment: NodeJS.ProcessEnv = process.env,
63
- ) => {
64
- return resolve(resolveDevsyncConfigDirectory(environment), "age");
65
- };
66
-
67
- export const expandHomePath = (
68
- value: string,
69
- environment: NodeJS.ProcessEnv = process.env,
70
- ) => {
71
- let expandedValue = value.trim();
72
-
73
- if (expandedValue === "~") {
74
- expandedValue = resolveHomeDirectory(environment);
75
- } else if (expandedValue.startsWith("~/")) {
76
- expandedValue = resolve(
77
- resolveHomeDirectory(environment),
78
- expandedValue.slice(2),
79
- );
80
- }
81
-
82
- return expandedValue;
83
- };
84
-
85
- export const expandConfiguredPath = (
86
- value: string,
87
- environment: NodeJS.ProcessEnv = process.env,
88
- ) => {
89
- let expandedValue = expandHomePath(value, environment);
90
-
91
- if (expandedValue === "$XDG_CONFIG_HOME") {
92
- expandedValue = resolveXdgConfigHome(environment);
93
- } else if (expandedValue.startsWith("$XDG_CONFIG_HOME/")) {
94
- expandedValue = resolve(
95
- resolveXdgConfigHome(environment),
96
- expandedValue.slice("$XDG_CONFIG_HOME/".length),
97
- );
98
- } else if (expandedValue === bracedXdgConfigHomeToken) {
99
- expandedValue = resolveXdgConfigHome(environment);
100
- } else if (expandedValue.startsWith(bracedXdgConfigHomePrefix)) {
101
- expandedValue = resolve(
102
- resolveXdgConfigHome(environment),
103
- expandedValue.slice(bracedXdgConfigHomePrefix.length),
104
- );
105
- }
106
-
107
- return expandedValue;
108
- };
109
-
110
- export const resolveConfiguredAbsolutePath = (
111
- value: string,
112
- environment: NodeJS.ProcessEnv = process.env,
113
- ) => {
114
- const expandedValue = expandConfiguredPath(value, environment);
115
-
116
- if (!isAbsolute(expandedValue)) {
117
- throw new Error(
118
- `Configured path must be absolute or start with ~ or $XDG_CONFIG_HOME: ${value}`,
119
- );
120
- }
121
-
122
- return resolve(expandedValue);
123
- };
124
-
125
- export const resolveHomeConfiguredAbsolutePath = (
126
- value: string,
127
- environment: NodeJS.ProcessEnv = process.env,
128
- ) => {
129
- const expandedValue = expandHomePath(value, environment);
130
-
131
- if (!isAbsolute(expandedValue)) {
132
- throw new Error(
133
- `Configured path must be absolute or start with ~: ${value}`,
134
- );
135
- }
136
-
137
- return resolve(expandedValue);
138
- };
@@ -1,13 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { ensureTrailingNewline } from "#app/lib/string.ts";
4
-
5
- describe("string helpers", () => {
6
- it("adds a trailing newline when missing", () => {
7
- expect(ensureTrailingNewline("value")).toBe("value\n");
8
- });
9
-
10
- it("preserves an existing trailing newline", () => {
11
- expect(ensureTrailingNewline("value\n")).toBe("value\n");
12
- });
13
- });