@tinyrack/devsync 1.0.3 → 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 (210) hide show
  1. package/README.md +248 -56
  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/forget.ts +0 -32
  170. package/src/cli/commands/index.ts +0 -17
  171. package/src/cli/commands/init.ts +0 -43
  172. package/src/cli/commands/pull.ts +0 -31
  173. package/src/cli/commands/push.ts +0 -31
  174. package/src/cli/commands/set.ts +0 -47
  175. package/src/cli/sync-output.test.ts +0 -105
  176. package/src/cli/sync-output.ts +0 -129
  177. package/src/config/sync.test.ts +0 -609
  178. package/src/config/sync.ts +0 -572
  179. package/src/config/xdg.ts +0 -138
  180. package/src/lib/string.test.ts +0 -13
  181. package/src/lib/string.ts +0 -3
  182. package/src/lib/validation.test.ts +0 -32
  183. package/src/lib/validation.ts +0 -11
  184. package/src/services/add.ts +0 -178
  185. package/src/services/config-file.test.ts +0 -161
  186. package/src/services/config-file.ts +0 -101
  187. package/src/services/crypto.test.ts +0 -132
  188. package/src/services/crypto.ts +0 -83
  189. package/src/services/error.ts +0 -6
  190. package/src/services/filesystem.test.ts +0 -171
  191. package/src/services/filesystem.ts +0 -183
  192. package/src/services/forget.ts +0 -261
  193. package/src/services/git.test.ts +0 -83
  194. package/src/services/git.ts +0 -74
  195. package/src/services/init.test.ts +0 -109
  196. package/src/services/init.ts +0 -244
  197. package/src/services/local-materialization.ts +0 -421
  198. package/src/services/local-snapshot.ts +0 -173
  199. package/src/services/paths.test.ts +0 -74
  200. package/src/services/paths.ts +0 -98
  201. package/src/services/pull.ts +0 -75
  202. package/src/services/push.ts +0 -121
  203. package/src/services/repo-artifacts.ts +0 -262
  204. package/src/services/repo-snapshot.ts +0 -197
  205. package/src/services/runtime.ts +0 -57
  206. package/src/services/set.ts +0 -383
  207. package/src/services/sync.dry-run.test.ts +0 -179
  208. package/src/services/sync.runtime.test.ts +0 -756
  209. package/src/services/sync.service.test.ts +0 -1025
  210. package/src/test/helpers/sync-fixture.ts +0 -47
@@ -1,197 +0,0 @@
1
- import { lstat, readFile, readlink } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- import {
5
- findOwningSyncEntry,
6
- type ResolvedSyncConfig,
7
- resolveManagedSyncMode,
8
- resolveSyncArtifactsDirectoryPath,
9
- } from "#app/config/sync.ts";
10
-
11
- import { decryptSecretFile } from "./crypto.ts";
12
- import { DevsyncError } from "./error.ts";
13
- import {
14
- getPathStats,
15
- isExecutableMode,
16
- listDirectoryEntries,
17
- pathExists,
18
- } from "./filesystem.ts";
19
- import { addSnapshotNode, type SnapshotNode } from "./local-snapshot.ts";
20
- import {
21
- assertStorageSafeRepoPath,
22
- isSecretArtifactPath,
23
- stripSecretArtifactSuffix,
24
- } from "./repo-artifacts.ts";
25
-
26
- const readPlainSnapshotNode = async (
27
- absolutePath: string,
28
- repoPath: string,
29
- config: ResolvedSyncConfig,
30
- snapshot: Map<string, SnapshotNode>,
31
- ) => {
32
- assertStorageSafeRepoPath(repoPath);
33
- const mode = resolveManagedSyncMode(config, repoPath);
34
-
35
- if (mode === "ignore") {
36
- return;
37
- }
38
-
39
- if (findOwningSyncEntry(config, repoPath) === undefined) {
40
- throw new DevsyncError(
41
- `Unmanaged plain sync path found in repository: ${repoPath}`,
42
- );
43
- }
44
-
45
- if (mode === "secret") {
46
- throw new DevsyncError(
47
- `Secret sync path is stored in plain text in the repository: ${repoPath}`,
48
- );
49
- }
50
-
51
- const stats = await lstat(absolutePath);
52
-
53
- if (stats.isSymbolicLink()) {
54
- addSnapshotNode(snapshot, repoPath, {
55
- linkTarget: await readlink(absolutePath),
56
- type: "symlink",
57
- });
58
-
59
- return;
60
- }
61
-
62
- if (!stats.isFile()) {
63
- throw new DevsyncError(
64
- `Unsupported plain repository entry: ${absolutePath}`,
65
- );
66
- }
67
-
68
- addSnapshotNode(snapshot, repoPath, {
69
- contents: await readFile(absolutePath),
70
- executable: isExecutableMode(stats.mode),
71
- secret: false,
72
- type: "file",
73
- });
74
- };
75
-
76
- const readRepositoryTree = async (
77
- rootDirectory: string,
78
- config: ResolvedSyncConfig,
79
- snapshot: Map<string, SnapshotNode>,
80
- prefix?: string,
81
- ) => {
82
- if (!(await pathExists(rootDirectory))) {
83
- return;
84
- }
85
-
86
- const entries = await listDirectoryEntries(rootDirectory);
87
-
88
- for (const entry of entries) {
89
- const absolutePath = join(rootDirectory, entry.name);
90
- const relativePath =
91
- prefix === undefined ? entry.name : `${prefix}/${entry.name}`;
92
- const stats = await lstat(absolutePath);
93
-
94
- if (stats.isDirectory()) {
95
- assertStorageSafeRepoPath(relativePath);
96
- await readRepositoryTree(absolutePath, config, snapshot, relativePath);
97
- continue;
98
- }
99
-
100
- if (stats.isSymbolicLink()) {
101
- if (isSecretArtifactPath(relativePath)) {
102
- throw new DevsyncError(
103
- `Secret repository entries must be regular files, not symlinks: ${relativePath}`,
104
- );
105
- }
106
-
107
- await readPlainSnapshotNode(absolutePath, relativePath, config, snapshot);
108
- continue;
109
- }
110
-
111
- if (isSecretArtifactPath(relativePath)) {
112
- const repoPath = stripSecretArtifactSuffix(relativePath);
113
-
114
- if (repoPath === undefined || repoPath.length === 0) {
115
- throw new DevsyncError(
116
- `Secret repository files must include a path before ${relativePath}`,
117
- );
118
- }
119
-
120
- assertStorageSafeRepoPath(repoPath);
121
- const mode = resolveManagedSyncMode(config, repoPath);
122
-
123
- if (findOwningSyncEntry(config, repoPath) === undefined) {
124
- throw new DevsyncError(
125
- `Unmanaged secret sync path found in repository: ${repoPath}`,
126
- );
127
- }
128
-
129
- if (mode === "ignore") {
130
- continue;
131
- }
132
-
133
- if (mode !== "secret") {
134
- throw new DevsyncError(
135
- `Plain sync path is stored in secret form in the repository: ${repoPath}`,
136
- );
137
- }
138
-
139
- addSnapshotNode(snapshot, repoPath, {
140
- contents: await decryptSecretFile(
141
- await readFile(absolutePath, "utf8"),
142
- config.age.identityFile,
143
- ),
144
- executable: isExecutableMode(stats.mode),
145
- secret: true,
146
- type: "file",
147
- });
148
- continue;
149
- }
150
-
151
- if (!stats.isFile()) {
152
- throw new DevsyncError(
153
- `Unsupported plain repository entry: ${absolutePath}`,
154
- );
155
- }
156
-
157
- await readPlainSnapshotNode(absolutePath, relativePath, config, snapshot);
158
- }
159
- };
160
-
161
- export const buildRepositorySnapshot = async (
162
- syncDirectory: string,
163
- config: ResolvedSyncConfig,
164
- ) => {
165
- const snapshot = new Map<string, SnapshotNode>();
166
- const artifactsDirectory = resolveSyncArtifactsDirectoryPath(syncDirectory);
167
-
168
- await readRepositoryTree(artifactsDirectory, config, snapshot);
169
-
170
- for (const entry of config.entries) {
171
- if (entry.kind !== "directory") {
172
- continue;
173
- }
174
-
175
- const artifactPath = join(artifactsDirectory, ...entry.repoPath.split("/"));
176
- const stats = await getPathStats(artifactPath);
177
-
178
- if (stats !== undefined && !stats.isDirectory()) {
179
- throw new DevsyncError(
180
- `Directory sync entry is not stored as a directory in the repository: ${entry.repoPath}`,
181
- );
182
- }
183
-
184
- const mode = resolveManagedSyncMode(config, entry.repoPath);
185
- const hasTrackedChildren = [...snapshot.keys()].some((repoPath) => {
186
- return repoPath.startsWith(`${entry.repoPath}/`);
187
- });
188
-
189
- if (stats?.isDirectory() && (mode !== "ignore" || hasTrackedChildren)) {
190
- addSnapshotNode(snapshot, entry.repoPath, {
191
- type: "directory",
192
- });
193
- }
194
- }
195
-
196
- return snapshot;
197
- };
@@ -1,57 +0,0 @@
1
- import {
2
- resolveSyncArtifactsDirectoryPath,
3
- resolveSyncConfigFilePath,
4
- } from "#app/config/sync.ts";
5
- import {
6
- resolveDevsyncSyncDirectory,
7
- resolveHomeDirectory,
8
- } from "#app/config/xdg.ts";
9
-
10
- import { ensureGitRepository } from "./git.ts";
11
-
12
- export type SyncPaths = Readonly<{
13
- artifactsDirectory: string;
14
- configPath: string;
15
- homeDirectory: string;
16
- syncDirectory: string;
17
- }>;
18
-
19
- export type SyncContext = Readonly<{
20
- cwd: string;
21
- environment: NodeJS.ProcessEnv;
22
- paths: SyncPaths;
23
- }>;
24
-
25
- export const createSyncPaths = (
26
- environment: NodeJS.ProcessEnv = process.env,
27
- ): SyncPaths => {
28
- const syncDirectory = resolveDevsyncSyncDirectory(environment);
29
-
30
- return {
31
- artifactsDirectory: resolveSyncArtifactsDirectoryPath(syncDirectory),
32
- configPath: resolveSyncConfigFilePath(syncDirectory),
33
- homeDirectory: resolveHomeDirectory(environment),
34
- syncDirectory,
35
- };
36
- };
37
-
38
- export const createSyncContext = (
39
- options: Readonly<{
40
- cwd?: string;
41
- environment?: NodeJS.ProcessEnv;
42
- }> = {},
43
- ): SyncContext => {
44
- const environment = options.environment ?? process.env;
45
-
46
- return {
47
- cwd: options.cwd ?? process.cwd(),
48
- environment,
49
- paths: createSyncPaths(environment),
50
- };
51
- };
52
-
53
- export const ensureSyncRepository = async (
54
- context: Pick<SyncContext, "paths">,
55
- ) => {
56
- await ensureGitRepository(context.paths.syncDirectory);
57
- };
@@ -1,383 +0,0 @@
1
- import { join } from "node:path";
2
-
3
- import {
4
- findOwningSyncEntry,
5
- type ResolvedSyncConfigEntry,
6
- type ResolvedSyncOverride,
7
- readSyncConfig,
8
- resolveEntryRelativeRepoPath,
9
- resolveRelativeSyncMode,
10
- type SyncMode,
11
- } from "#app/config/sync.ts";
12
-
13
- import {
14
- createSyncConfigDocument,
15
- createSyncConfigDocumentEntry,
16
- sortSyncConfigEntries,
17
- writeValidatedSyncConfig,
18
- } from "./config-file.ts";
19
- import { DevsyncError } from "./error.ts";
20
- import { getPathStats } from "./filesystem.ts";
21
- import {
22
- buildRepoPathWithinRoot,
23
- isExplicitLocalPath,
24
- resolveCommandTargetPath,
25
- tryBuildRepoPathWithinRoot,
26
- tryNormalizeRepoPathInput,
27
- } from "./paths.ts";
28
- import { ensureSyncRepository, type SyncContext } from "./runtime.ts";
29
-
30
- export type SyncSetRequest = Readonly<{
31
- recursive: boolean;
32
- state: SyncMode;
33
- target: string;
34
- }>;
35
-
36
- type SyncSetScope = "default" | "exact" | "subtree";
37
- type SyncSetAction = "added" | "removed" | "unchanged" | "updated";
38
-
39
- export type SyncSetResult = Readonly<{
40
- action: SyncSetAction;
41
- configPath: string;
42
- entryRepoPath: string;
43
- localPath: string;
44
- mode: SyncMode;
45
- repoPath: string;
46
- scope: SyncSetScope;
47
- syncDirectory: string;
48
- }>;
49
-
50
- const resolveTargetPath = async (
51
- target: string,
52
- entry: ResolvedSyncConfigEntry,
53
- context: Pick<SyncContext, "cwd" | "environment" | "paths">,
54
- ) => {
55
- if (isExplicitLocalPath(target)) {
56
- const localPath = resolveCommandTargetPath(
57
- target,
58
- context.environment,
59
- context.cwd,
60
- );
61
- const stats = await getPathStats(localPath);
62
-
63
- if (stats === undefined) {
64
- throw new DevsyncError(`Sync set target does not exist: ${localPath}`);
65
- }
66
-
67
- return {
68
- localPath,
69
- repoPath: buildRepoPathWithinRoot(
70
- localPath,
71
- context.paths.homeDirectory,
72
- "Sync set target",
73
- ),
74
- stats,
75
- };
76
- }
77
-
78
- const repoPath = tryNormalizeRepoPathInput(target);
79
-
80
- if (repoPath === undefined) {
81
- throw new DevsyncError(
82
- `Sync set target must be a local path or repository path: ${target}`,
83
- );
84
- }
85
-
86
- const relativePath = resolveEntryRelativeRepoPath(entry, repoPath);
87
- const localPath =
88
- relativePath === undefined || relativePath === ""
89
- ? entry.localPath
90
- : join(entry.localPath, ...relativePath.split("/"));
91
-
92
- return {
93
- localPath,
94
- repoPath,
95
- stats: await getPathStats(localPath),
96
- };
97
- };
98
-
99
- const resolveSetTarget = async (
100
- target: string,
101
- config: Awaited<ReturnType<typeof readSyncConfig>>,
102
- context: Pick<SyncContext, "cwd" | "environment" | "paths">,
103
- ) => {
104
- const trimmedTarget = target.trim();
105
-
106
- if (trimmedTarget.length === 0) {
107
- throw new DevsyncError("Target path is required.");
108
- }
109
-
110
- const homeDirectory = context.paths.homeDirectory;
111
- const explicitLocalPath = isExplicitLocalPath(trimmedTarget);
112
- const localTargetPath = resolveCommandTargetPath(
113
- trimmedTarget,
114
- context.environment,
115
- context.cwd,
116
- );
117
- const localRepoPath = explicitLocalPath
118
- ? buildRepoPathWithinRoot(localTargetPath, homeDirectory, "Sync set target")
119
- : tryBuildRepoPathWithinRoot(
120
- localTargetPath,
121
- homeDirectory,
122
- "Sync set target",
123
- );
124
-
125
- if (localRepoPath !== undefined) {
126
- const localStats = await getPathStats(localTargetPath);
127
-
128
- if (explicitLocalPath && localStats === undefined) {
129
- throw new DevsyncError(
130
- `Sync set target does not exist: ${localTargetPath}`,
131
- );
132
- }
133
-
134
- const entry = findOwningSyncEntry(config, localRepoPath);
135
-
136
- if (entry?.kind === "directory") {
137
- const relativePath = resolveEntryRelativeRepoPath(entry, localRepoPath);
138
-
139
- if (relativePath !== undefined) {
140
- return {
141
- entry,
142
- localPath: localTargetPath,
143
- relativePath,
144
- repoPath: localRepoPath,
145
- stats: localStats,
146
- };
147
- }
148
- }
149
-
150
- if (explicitLocalPath) {
151
- throw new DevsyncError(
152
- `Sync set target must be inside a tracked directory entry: ${trimmedTarget}`,
153
- );
154
- }
155
- }
156
-
157
- const repoPath = tryNormalizeRepoPathInput(trimmedTarget);
158
-
159
- if (repoPath === undefined) {
160
- throw new DevsyncError(
161
- `Sync set target must be a local path or repository path: ${trimmedTarget}`,
162
- );
163
- }
164
-
165
- const entry = findOwningSyncEntry(config, repoPath);
166
-
167
- if (entry === undefined || entry.kind !== "directory") {
168
- throw new DevsyncError(
169
- `Sync set target must be inside a tracked directory entry: ${trimmedTarget}`,
170
- );
171
- }
172
-
173
- const resolvedTarget = await resolveTargetPath(trimmedTarget, entry, context);
174
- const relativePath = resolveEntryRelativeRepoPath(
175
- entry,
176
- resolvedTarget.repoPath,
177
- );
178
-
179
- if (relativePath === undefined) {
180
- throw new DevsyncError(
181
- `Sync set target must be inside a tracked directory entry: ${trimmedTarget}`,
182
- );
183
- }
184
-
185
- return {
186
- entry,
187
- localPath: resolvedTarget.localPath,
188
- relativePath,
189
- repoPath: resolvedTarget.repoPath,
190
- stats: resolvedTarget.stats,
191
- };
192
- };
193
-
194
- const updateEntryMode = (
195
- entry: ResolvedSyncConfigEntry,
196
- mode: SyncMode,
197
- ): {
198
- action: SyncSetAction;
199
- entry: ResolvedSyncConfigEntry;
200
- } => {
201
- if (entry.mode === mode) {
202
- return {
203
- action: "unchanged",
204
- entry,
205
- };
206
- }
207
-
208
- return {
209
- action: "updated",
210
- entry: {
211
- ...entry,
212
- mode,
213
- },
214
- };
215
- };
216
-
217
- const updateChildOverride = (
218
- entry: ResolvedSyncConfigEntry,
219
- input: Readonly<{
220
- match: Extract<SyncSetScope, "exact" | "subtree">;
221
- mode: SyncMode;
222
- relativePath: string;
223
- }>,
224
- ): {
225
- action: SyncSetAction;
226
- entry: ResolvedSyncConfigEntry;
227
- } => {
228
- const existingOverride = entry.overrides.find((override) => {
229
- return (
230
- override.match === input.match && override.path === input.relativePath
231
- );
232
- });
233
- const remainingOverrides = entry.overrides.filter((override) => {
234
- return !(
235
- override.match === input.match && override.path === input.relativePath
236
- );
237
- });
238
- const inheritedMode = resolveRelativeSyncMode(
239
- entry.mode,
240
- remainingOverrides,
241
- input.relativePath,
242
- );
243
-
244
- if (input.mode === inheritedMode) {
245
- if (existingOverride === undefined) {
246
- return {
247
- action: "unchanged",
248
- entry,
249
- };
250
- }
251
-
252
- return {
253
- action: "removed",
254
- entry: {
255
- ...entry,
256
- overrides: remainingOverrides,
257
- },
258
- };
259
- }
260
-
261
- if (existingOverride?.mode === input.mode) {
262
- return {
263
- action: "unchanged",
264
- entry,
265
- };
266
- }
267
-
268
- const nextOverride = {
269
- match: input.match,
270
- mode: input.mode,
271
- path: input.relativePath,
272
- } satisfies ResolvedSyncOverride;
273
-
274
- return {
275
- action: existingOverride === undefined ? "added" : "updated",
276
- entry: {
277
- ...entry,
278
- overrides: [...remainingOverrides, nextOverride],
279
- },
280
- };
281
- };
282
-
283
- export const setSyncTargetMode = async (
284
- request: SyncSetRequest,
285
- context: SyncContext,
286
- ): Promise<SyncSetResult> => {
287
- await ensureSyncRepository(context);
288
-
289
- const config = await readSyncConfig(
290
- context.paths.syncDirectory,
291
- context.environment,
292
- );
293
- const target = await resolveSetTarget(request.target, config, context);
294
-
295
- if (target.relativePath === "") {
296
- if (!request.recursive) {
297
- throw new DevsyncError(
298
- "Tracked directory roots require --recursive to update the entry mode.",
299
- );
300
- }
301
-
302
- const update = updateEntryMode(target.entry, request.state);
303
- const nextConfig = createSyncConfigDocument(config);
304
-
305
- nextConfig.entries = sortSyncConfigEntries(
306
- nextConfig.entries.map((entry) => {
307
- if (entry.repoPath !== target.entry.repoPath) {
308
- return entry;
309
- }
310
-
311
- return createSyncConfigDocumentEntry(update.entry);
312
- }),
313
- );
314
-
315
- if (update.action !== "unchanged") {
316
- await writeValidatedSyncConfig(context.paths.syncDirectory, nextConfig, {
317
- environment: context.environment,
318
- });
319
- }
320
-
321
- return {
322
- action: update.action,
323
- configPath: context.paths.configPath,
324
- entryRepoPath: target.entry.repoPath,
325
- localPath: target.localPath,
326
- mode: request.state,
327
- repoPath: target.repoPath,
328
- scope: "default",
329
- syncDirectory: context.paths.syncDirectory,
330
- };
331
- }
332
-
333
- if (target.stats?.isDirectory() && !request.recursive) {
334
- throw new DevsyncError(
335
- "Directory targets require --recursive. Use a file path for exact overrides.",
336
- );
337
- }
338
-
339
- if (
340
- request.recursive &&
341
- target.stats !== undefined &&
342
- !target.stats.isDirectory()
343
- ) {
344
- throw new DevsyncError(
345
- "--recursive can only be used with directories or tracked directory roots.",
346
- );
347
- }
348
-
349
- const scope = request.recursive ? "subtree" : "exact";
350
- const update = updateChildOverride(target.entry, {
351
- match: scope,
352
- mode: request.state,
353
- relativePath: target.relativePath,
354
- });
355
- const nextConfig = createSyncConfigDocument(config);
356
-
357
- nextConfig.entries = sortSyncConfigEntries(
358
- nextConfig.entries.map((entry) => {
359
- if (entry.repoPath !== target.entry.repoPath) {
360
- return entry;
361
- }
362
-
363
- return createSyncConfigDocumentEntry(update.entry);
364
- }),
365
- );
366
-
367
- if (update.action !== "unchanged") {
368
- await writeValidatedSyncConfig(context.paths.syncDirectory, nextConfig, {
369
- environment: context.environment,
370
- });
371
- }
372
-
373
- return {
374
- action: update.action,
375
- configPath: context.paths.configPath,
376
- entryRepoPath: target.entry.repoPath,
377
- localPath: target.localPath,
378
- mode: request.state,
379
- repoPath: target.repoPath,
380
- scope,
381
- syncDirectory: context.paths.syncDirectory,
382
- };
383
- };