@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.
- package/README.md +230 -62
- package/dist/cli/base-command.d.ts +14 -0
- package/dist/cli/base-command.d.ts.map +1 -0
- package/dist/cli/base-command.js +22 -0
- package/dist/cli/base-command.js.map +1 -0
- package/dist/cli/commands/dir.d.ts +8 -0
- package/dist/cli/commands/dir.d.ts.map +1 -0
- package/dist/cli/commands/dir.js +16 -0
- package/dist/cli/commands/dir.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +23 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +23 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +43 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/machine/list.d.ts +7 -0
- package/dist/cli/commands/machine/list.d.ts.map +1 -0
- package/dist/cli/commands/machine/list.js +12 -0
- package/dist/cli/commands/machine/list.js.map +1 -0
- package/dist/cli/commands/machine/use.d.ts +11 -0
- package/dist/cli/commands/machine/use.d.ts.map +1 -0
- package/dist/cli/commands/machine/use.js +28 -0
- package/dist/cli/commands/machine/use.js.map +1 -0
- package/dist/cli/commands/pull.d.ts +12 -0
- package/dist/cli/commands/pull.d.ts.map +1 -0
- package/dist/cli/commands/pull.js +34 -0
- package/dist/cli/commands/pull.js.map +1 -0
- package/dist/cli/commands/push.d.ts +12 -0
- package/dist/cli/commands/push.d.ts.map +1 -0
- package/dist/cli/commands/push.js +34 -0
- package/dist/cli/commands/push.js.map +1 -0
- package/dist/cli/commands/status.d.ts +11 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +27 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/track.d.ts +16 -0
- package/dist/cli/commands/track.d.ts.map +1 -0
- package/dist/cli/commands/track.js +82 -0
- package/dist/cli/commands/track.js.map +1 -0
- package/dist/cli/commands/untrack.d.ts +11 -0
- package/dist/cli/commands/untrack.d.ts.map +1 -0
- package/dist/cli/commands/untrack.js +28 -0
- package/dist/cli/commands/untrack.js.map +1 -0
- package/dist/config/global-config.d.ts +21 -0
- package/dist/config/global-config.d.ts.map +1 -0
- package/dist/config/global-config.js +106 -0
- package/dist/config/global-config.js.map +1 -0
- package/dist/config/sync.d.ts +96 -0
- package/dist/config/sync.d.ts.map +1 -0
- package/dist/config/sync.js +412 -0
- package/dist/config/sync.js.map +1 -0
- package/dist/config/xdg.d.ts +11 -0
- package/dist/config/xdg.d.ts.map +1 -0
- package/dist/config/xdg.js +79 -0
- package/dist/config/xdg.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -1
- package/dist/index.js.map +1 -0
- package/dist/lib/file-mode.d.ts +3 -0
- package/dist/lib/file-mode.d.ts.map +1 -0
- package/dist/lib/file-mode.js +7 -0
- package/dist/lib/file-mode.js.map +1 -0
- package/dist/lib/output.d.ts +31 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +198 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/path.d.ts +5 -0
- package/dist/lib/path.d.ts.map +1 -0
- package/dist/lib/path.js +25 -0
- package/dist/lib/path.js.map +1 -0
- package/dist/lib/string.d.ts +2 -0
- package/dist/lib/string.d.ts.map +1 -0
- package/dist/lib/string.js +4 -0
- package/dist/lib/string.js.map +1 -0
- package/dist/lib/validation.d.ts +3 -0
- package/dist/lib/validation.d.ts.map +1 -0
- package/dist/lib/validation.js +9 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/services/add.d.ts +20 -0
- package/dist/services/add.d.ts.map +1 -0
- package/dist/services/add.js +161 -0
- package/dist/services/add.js.map +1 -0
- package/dist/services/config-file.d.ts +30 -0
- package/dist/services/config-file.d.ts.map +1 -0
- package/dist/services/config-file.js +34 -0
- package/dist/services/config-file.js.map +1 -0
- package/dist/services/crypto.d.ts +9 -0
- package/dist/services/crypto.d.ts.map +1 -0
- package/dist/services/crypto.js +75 -0
- package/dist/services/crypto.js.map +1 -0
- package/dist/services/doctor.d.ts +16 -0
- package/dist/services/doctor.d.ts.map +1 -0
- package/dist/services/doctor.js +84 -0
- package/dist/services/doctor.js.map +1 -0
- package/dist/services/error.d.ts +14 -0
- package/dist/services/error.d.ts.map +1 -0
- package/dist/services/error.js +38 -0
- package/dist/services/error.js.map +1 -0
- package/dist/services/filesystem.d.ts +15 -0
- package/dist/services/filesystem.d.ts.map +1 -0
- package/dist/services/filesystem.js +113 -0
- package/dist/services/filesystem.js.map +1 -0
- package/dist/services/forget.d.ts +14 -0
- package/dist/services/forget.d.ts.map +1 -0
- package/dist/services/forget.js +124 -0
- package/dist/services/forget.js.map +1 -0
- package/dist/services/git.d.ts +10 -0
- package/dist/services/git.d.ts.map +1 -0
- package/dist/services/git.js +57 -0
- package/dist/services/git.js.map +1 -0
- package/dist/services/init.d.ts +19 -0
- package/dist/services/init.d.ts.map +1 -0
- package/dist/services/init.js +203 -0
- package/dist/services/init.js.map +1 -0
- package/dist/services/local-materialization.d.ts +28 -0
- package/dist/services/local-materialization.d.ts.map +1 -0
- package/dist/services/local-materialization.js +262 -0
- package/dist/services/local-materialization.js.map +1 -0
- package/dist/services/local-snapshot.d.ts +25 -0
- package/dist/services/local-snapshot.d.ts.map +1 -0
- package/dist/services/local-snapshot.js +93 -0
- package/dist/services/local-snapshot.js.map +1 -0
- package/dist/services/machine.d.ts +40 -0
- package/dist/services/machine.d.ts.map +1 -0
- package/dist/services/machine.js +113 -0
- package/dist/services/machine.js.map +1 -0
- package/dist/services/paths.d.ts +12 -0
- package/dist/services/paths.d.ts.map +1 -0
- package/dist/services/paths.js +71 -0
- package/dist/services/paths.js.map +1 -0
- package/dist/services/pull.d.ts +28 -0
- package/dist/services/pull.d.ts.map +1 -0
- package/dist/services/pull.js +67 -0
- package/dist/services/pull.js.map +1 -0
- package/dist/services/push.d.ts +35 -0
- package/dist/services/push.d.ts.map +1 -0
- package/dist/services/push.js +96 -0
- package/dist/services/push.js.map +1 -0
- package/dist/services/repo-artifacts.d.ts +52 -0
- package/dist/services/repo-artifacts.d.ts.map +1 -0
- package/dist/services/repo-artifacts.js +251 -0
- package/dist/services/repo-artifacts.js.map +1 -0
- package/dist/services/repo-snapshot.d.ts +6 -0
- package/dist/services/repo-snapshot.d.ts.map +1 -0
- package/dist/services/repo-snapshot.js +163 -0
- package/dist/services/repo-snapshot.js.map +1 -0
- package/dist/services/runtime.d.ts +40 -0
- package/dist/services/runtime.d.ts.map +1 -0
- package/dist/services/runtime.js +71 -0
- package/dist/services/runtime.js.map +1 -0
- package/dist/services/set.d.ts +38 -0
- package/dist/services/set.d.ts.map +1 -0
- package/dist/services/set.js +184 -0
- package/dist/services/set.js.map +1 -0
- package/dist/services/status.d.ts +30 -0
- package/dist/services/status.d.ts.map +1 -0
- package/dist/services/status.js +35 -0
- package/dist/services/status.js.map +1 -0
- package/package.json +15 -7
- package/src/cli/commands/add.ts +0 -40
- package/src/cli/commands/cd.ts +0 -80
- package/src/cli/commands/doctor.ts +0 -20
- package/src/cli/commands/forget.ts +0 -32
- package/src/cli/commands/index.ts +0 -23
- package/src/cli/commands/init.ts +0 -43
- package/src/cli/commands/list.ts +0 -17
- package/src/cli/commands/pull.ts +0 -31
- package/src/cli/commands/push.ts +0 -31
- package/src/cli/commands/set.ts +0 -47
- package/src/cli/commands/status.ts +0 -18
- package/src/cli/sync-output.test.ts +0 -173
- package/src/cli/sync-output.ts +0 -200
- package/src/config/sync.test.ts +0 -609
- package/src/config/sync.ts +0 -572
- package/src/config/xdg.ts +0 -138
- package/src/lib/string.test.ts +0 -13
- package/src/lib/string.ts +0 -3
- package/src/lib/validation.test.ts +0 -32
- package/src/lib/validation.ts +0 -11
- package/src/services/add.ts +0 -178
- package/src/services/config-file.test.ts +0 -161
- package/src/services/config-file.ts +0 -101
- package/src/services/crypto.test.ts +0 -132
- package/src/services/crypto.ts +0 -83
- package/src/services/doctor.ts +0 -142
- package/src/services/error.ts +0 -6
- package/src/services/filesystem.test.ts +0 -171
- package/src/services/filesystem.ts +0 -183
- package/src/services/forget.ts +0 -261
- package/src/services/git.test.ts +0 -83
- package/src/services/git.ts +0 -74
- package/src/services/init.test.ts +0 -109
- package/src/services/init.ts +0 -244
- package/src/services/list.ts +0 -63
- package/src/services/local-materialization.ts +0 -421
- package/src/services/local-snapshot.ts +0 -173
- package/src/services/paths.test.ts +0 -74
- package/src/services/paths.ts +0 -98
- package/src/services/pull.ts +0 -144
- package/src/services/push.ts +0 -168
- package/src/services/repo-artifacts.ts +0 -262
- package/src/services/repo-snapshot.ts +0 -197
- package/src/services/runtime.ts +0 -57
- package/src/services/set.ts +0 -383
- package/src/services/status.ts +0 -57
- package/src/services/sync.dry-run.test.ts +0 -179
- package/src/services/sync.runtime.test.ts +0 -756
- package/src/services/sync.service.test.ts +0 -1169
- package/src/test/helpers/sync-fixture.ts +0 -47
package/src/services/init.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
createInitialSyncConfig,
|
|
6
|
-
formatSyncConfig,
|
|
7
|
-
parseSyncConfig,
|
|
8
|
-
type ResolvedSyncConfig,
|
|
9
|
-
readSyncConfig,
|
|
10
|
-
resolveSyncArtifactsDirectoryPath,
|
|
11
|
-
} from "#app/config/sync.ts";
|
|
12
|
-
import { resolveConfiguredAbsolutePath } from "#app/config/xdg.ts";
|
|
13
|
-
|
|
14
|
-
import { countConfiguredRules } from "./config-file.ts";
|
|
15
|
-
import {
|
|
16
|
-
createAgeIdentityFile,
|
|
17
|
-
readAgeRecipientsFromIdentityFile,
|
|
18
|
-
} from "./crypto.ts";
|
|
19
|
-
import { DevsyncError } from "./error.ts";
|
|
20
|
-
import { pathExists } from "./filesystem.ts";
|
|
21
|
-
import { ensureRepository, initializeRepository } from "./git.ts";
|
|
22
|
-
import { ensureSyncRepository, type SyncContext } from "./runtime.ts";
|
|
23
|
-
|
|
24
|
-
export type SyncInitRequest = Readonly<{
|
|
25
|
-
identityFile?: string;
|
|
26
|
-
recipients: readonly string[];
|
|
27
|
-
repository?: string;
|
|
28
|
-
}>;
|
|
29
|
-
|
|
30
|
-
export type SyncInitResult = Readonly<{
|
|
31
|
-
alreadyInitialized: boolean;
|
|
32
|
-
configPath: string;
|
|
33
|
-
entryCount: number;
|
|
34
|
-
gitAction: "cloned" | "existing" | "initialized";
|
|
35
|
-
gitSource?: string;
|
|
36
|
-
identityFile: string;
|
|
37
|
-
generatedIdentity: boolean;
|
|
38
|
-
recipientCount: number;
|
|
39
|
-
ruleCount: number;
|
|
40
|
-
syncDirectory: string;
|
|
41
|
-
}>;
|
|
42
|
-
|
|
43
|
-
const defaultSyncIdentityFile = "$XDG_CONFIG_HOME/devsync/age/keys.txt";
|
|
44
|
-
|
|
45
|
-
const normalizeRecipients = (recipients: readonly string[]) => {
|
|
46
|
-
return [
|
|
47
|
-
...new Set(recipients.map((recipient) => recipient.trim()).filter(Boolean)),
|
|
48
|
-
].sort((left, right) => {
|
|
49
|
-
return left.localeCompare(right);
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const resolveInitAgeBootstrap = async (
|
|
54
|
-
request: SyncInitRequest,
|
|
55
|
-
context: Pick<SyncContext, "environment">,
|
|
56
|
-
) => {
|
|
57
|
-
const configuredIdentityFile =
|
|
58
|
-
request.identityFile?.trim() || defaultSyncIdentityFile;
|
|
59
|
-
const identityFile = resolveConfiguredAbsolutePath(
|
|
60
|
-
configuredIdentityFile,
|
|
61
|
-
context.environment,
|
|
62
|
-
);
|
|
63
|
-
const explicitRecipients = normalizeRecipients(request.recipients);
|
|
64
|
-
|
|
65
|
-
if (explicitRecipients.length === 0) {
|
|
66
|
-
if (await pathExists(identityFile)) {
|
|
67
|
-
return {
|
|
68
|
-
configuredIdentityFile,
|
|
69
|
-
generatedIdentity: false,
|
|
70
|
-
recipients: normalizeRecipients(
|
|
71
|
-
await readAgeRecipientsFromIdentityFile(identityFile),
|
|
72
|
-
),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { recipient } = await createAgeIdentityFile(identityFile);
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
configuredIdentityFile,
|
|
80
|
-
generatedIdentity: true,
|
|
81
|
-
recipients: [recipient],
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (await pathExists(identityFile)) {
|
|
86
|
-
return {
|
|
87
|
-
configuredIdentityFile,
|
|
88
|
-
generatedIdentity: false,
|
|
89
|
-
recipients: explicitRecipients,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const { recipient } = await createAgeIdentityFile(identityFile);
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
configuredIdentityFile,
|
|
97
|
-
generatedIdentity: true,
|
|
98
|
-
recipients: normalizeRecipients([...explicitRecipients, recipient]),
|
|
99
|
-
};
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const assertInitRequestMatchesConfig = (
|
|
103
|
-
config: ResolvedSyncConfig,
|
|
104
|
-
request: SyncInitRequest,
|
|
105
|
-
environment: NodeJS.ProcessEnv,
|
|
106
|
-
) => {
|
|
107
|
-
const recipients = normalizeRecipients(request.recipients);
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
recipients.length > 0 &&
|
|
111
|
-
JSON.stringify(recipients) !==
|
|
112
|
-
JSON.stringify(normalizeRecipients(config.age.recipients))
|
|
113
|
-
) {
|
|
114
|
-
throw new DevsyncError(
|
|
115
|
-
"Sync configuration already exists with different recipients.",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
request.identityFile === undefined ||
|
|
121
|
-
request.identityFile.trim() === ""
|
|
122
|
-
) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const resolvedIdentity = resolveConfiguredAbsolutePath(
|
|
127
|
-
request.identityFile,
|
|
128
|
-
environment,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (resolvedIdentity !== config.age.identityFile) {
|
|
132
|
-
throw new DevsyncError(
|
|
133
|
-
"Sync configuration already exists with a different identity file.",
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export const initializeSync = async (
|
|
139
|
-
request: SyncInitRequest,
|
|
140
|
-
context: SyncContext,
|
|
141
|
-
): Promise<SyncInitResult> => {
|
|
142
|
-
const syncDirectory = context.paths.syncDirectory;
|
|
143
|
-
const configPath = context.paths.configPath;
|
|
144
|
-
const configExists = await pathExists(configPath);
|
|
145
|
-
|
|
146
|
-
if (configExists) {
|
|
147
|
-
await ensureSyncRepository(context);
|
|
148
|
-
|
|
149
|
-
const config = await readSyncConfig(syncDirectory, context.environment);
|
|
150
|
-
assertInitRequestMatchesConfig(config, request, context.environment);
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
alreadyInitialized: true,
|
|
154
|
-
configPath,
|
|
155
|
-
entryCount: config.entries.length,
|
|
156
|
-
gitAction: "existing",
|
|
157
|
-
generatedIdentity: false,
|
|
158
|
-
identityFile: config.age.identityFile,
|
|
159
|
-
recipientCount: config.age.recipients.length,
|
|
160
|
-
ruleCount: countConfiguredRules(config),
|
|
161
|
-
syncDirectory,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
await mkdir(dirname(syncDirectory), {
|
|
166
|
-
recursive: true,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
let gitAction: SyncInitResult["gitAction"] = "existing";
|
|
170
|
-
let gitSource: string | undefined;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
await ensureRepository(syncDirectory);
|
|
174
|
-
} catch {
|
|
175
|
-
const syncDirectoryExists = await pathExists(syncDirectory);
|
|
176
|
-
|
|
177
|
-
if (syncDirectoryExists) {
|
|
178
|
-
const entries = await readdir(syncDirectory);
|
|
179
|
-
|
|
180
|
-
if (entries.length > 0) {
|
|
181
|
-
throw new DevsyncError(
|
|
182
|
-
`Sync directory already exists and is not empty: ${syncDirectory}`,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const gitResult = await initializeRepository(
|
|
188
|
-
syncDirectory,
|
|
189
|
-
request.repository?.trim() || undefined,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
gitAction = gitResult.action;
|
|
193
|
-
gitSource = gitResult.source;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
await mkdir(resolveSyncArtifactsDirectoryPath(syncDirectory), {
|
|
197
|
-
recursive: true,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
if (await pathExists(configPath)) {
|
|
201
|
-
const config = await readSyncConfig(syncDirectory, context.environment);
|
|
202
|
-
|
|
203
|
-
assertInitRequestMatchesConfig(config, request, context.environment);
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
alreadyInitialized: true,
|
|
207
|
-
configPath,
|
|
208
|
-
entryCount: config.entries.length,
|
|
209
|
-
gitAction,
|
|
210
|
-
...(gitSource === undefined ? {} : { gitSource }),
|
|
211
|
-
generatedIdentity: false,
|
|
212
|
-
identityFile: config.age.identityFile,
|
|
213
|
-
recipientCount: config.age.recipients.length,
|
|
214
|
-
ruleCount: countConfiguredRules(config),
|
|
215
|
-
syncDirectory,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const ageBootstrap = await resolveInitAgeBootstrap(request, context);
|
|
220
|
-
|
|
221
|
-
const initialConfig = createInitialSyncConfig({
|
|
222
|
-
identityFile: ageBootstrap.configuredIdentityFile,
|
|
223
|
-
recipients: ageBootstrap.recipients,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
parseSyncConfig(initialConfig, context.environment);
|
|
227
|
-
await writeFile(configPath, formatSyncConfig(initialConfig), "utf8");
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
alreadyInitialized: false,
|
|
231
|
-
configPath,
|
|
232
|
-
entryCount: 0,
|
|
233
|
-
gitAction,
|
|
234
|
-
...(gitSource === undefined ? {} : { gitSource }),
|
|
235
|
-
generatedIdentity: ageBootstrap.generatedIdentity,
|
|
236
|
-
identityFile: resolveConfiguredAbsolutePath(
|
|
237
|
-
ageBootstrap.configuredIdentityFile,
|
|
238
|
-
context.environment,
|
|
239
|
-
),
|
|
240
|
-
recipientCount: ageBootstrap.recipients.length,
|
|
241
|
-
ruleCount: 0,
|
|
242
|
-
syncDirectory,
|
|
243
|
-
};
|
|
244
|
-
};
|
package/src/services/list.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatSyncOverrideSelector,
|
|
3
|
-
readSyncConfig,
|
|
4
|
-
type SyncMode,
|
|
5
|
-
} from "#app/config/sync.ts";
|
|
6
|
-
|
|
7
|
-
import { countConfiguredRules } from "./config-file.ts";
|
|
8
|
-
import { ensureSyncRepository, type SyncContext } from "./runtime.ts";
|
|
9
|
-
|
|
10
|
-
export type SyncListOverride = Readonly<{
|
|
11
|
-
mode: SyncMode;
|
|
12
|
-
selector: string;
|
|
13
|
-
}>;
|
|
14
|
-
|
|
15
|
-
export type SyncListEntry = Readonly<{
|
|
16
|
-
kind: "directory" | "file";
|
|
17
|
-
localPath: string;
|
|
18
|
-
mode: SyncMode;
|
|
19
|
-
name: string;
|
|
20
|
-
overrides: readonly SyncListOverride[];
|
|
21
|
-
repoPath: string;
|
|
22
|
-
}>;
|
|
23
|
-
|
|
24
|
-
export type SyncListResult = Readonly<{
|
|
25
|
-
configPath: string;
|
|
26
|
-
entries: readonly SyncListEntry[];
|
|
27
|
-
recipientCount: number;
|
|
28
|
-
ruleCount: number;
|
|
29
|
-
syncDirectory: string;
|
|
30
|
-
}>;
|
|
31
|
-
|
|
32
|
-
export const listSyncConfig = async (
|
|
33
|
-
context: SyncContext,
|
|
34
|
-
): Promise<SyncListResult> => {
|
|
35
|
-
await ensureSyncRepository(context);
|
|
36
|
-
|
|
37
|
-
const config = await readSyncConfig(
|
|
38
|
-
context.paths.syncDirectory,
|
|
39
|
-
context.environment,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
configPath: context.paths.configPath,
|
|
44
|
-
entries: config.entries.map((entry) => {
|
|
45
|
-
return {
|
|
46
|
-
kind: entry.kind,
|
|
47
|
-
localPath: entry.localPath,
|
|
48
|
-
mode: entry.mode,
|
|
49
|
-
name: entry.name,
|
|
50
|
-
overrides: entry.overrides.map((override) => {
|
|
51
|
-
return {
|
|
52
|
-
mode: override.mode,
|
|
53
|
-
selector: formatSyncOverrideSelector(override),
|
|
54
|
-
};
|
|
55
|
-
}),
|
|
56
|
-
repoPath: entry.repoPath,
|
|
57
|
-
};
|
|
58
|
-
}),
|
|
59
|
-
recipientCount: config.age.recipients.length,
|
|
60
|
-
ruleCount: countConfiguredRules(config),
|
|
61
|
-
syncDirectory: context.paths.syncDirectory,
|
|
62
|
-
};
|
|
63
|
-
};
|
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
import { lstat, mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, posix } from "node:path";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
type ResolvedSyncConfig,
|
|
6
|
-
type ResolvedSyncConfigEntry,
|
|
7
|
-
resolveManagedSyncMode,
|
|
8
|
-
} from "#app/config/sync.ts";
|
|
9
|
-
import { DevsyncError } from "./error.ts";
|
|
10
|
-
import {
|
|
11
|
-
copyFilesystemNode,
|
|
12
|
-
getPathStats,
|
|
13
|
-
listDirectoryEntries,
|
|
14
|
-
removePathAtomically,
|
|
15
|
-
replacePathAtomically,
|
|
16
|
-
writeFileNode,
|
|
17
|
-
writeSymlinkNode,
|
|
18
|
-
} from "./filesystem.ts";
|
|
19
|
-
import type { FileLikeSnapshotNode, SnapshotNode } from "./local-snapshot.ts";
|
|
20
|
-
import { buildDirectoryKey } from "./paths.ts";
|
|
21
|
-
|
|
22
|
-
type EntryMaterialization =
|
|
23
|
-
| Readonly<{
|
|
24
|
-
desiredKeys: ReadonlySet<string>;
|
|
25
|
-
type: "absent";
|
|
26
|
-
}>
|
|
27
|
-
| Readonly<{
|
|
28
|
-
desiredKeys: ReadonlySet<string>;
|
|
29
|
-
node: FileLikeSnapshotNode;
|
|
30
|
-
type: "file";
|
|
31
|
-
}>
|
|
32
|
-
| Readonly<{
|
|
33
|
-
desiredKeys: ReadonlySet<string>;
|
|
34
|
-
nodes: ReadonlyMap<string, FileLikeSnapshotNode>;
|
|
35
|
-
type: "directory";
|
|
36
|
-
}>;
|
|
37
|
-
|
|
38
|
-
const copyIgnoredLocalNodesToDirectory = async (
|
|
39
|
-
sourceDirectory: string,
|
|
40
|
-
targetDirectory: string,
|
|
41
|
-
config: ResolvedSyncConfig,
|
|
42
|
-
repoPathPrefix: string,
|
|
43
|
-
): Promise<number> => {
|
|
44
|
-
const stats = await getPathStats(sourceDirectory);
|
|
45
|
-
|
|
46
|
-
if (stats === undefined || !stats.isDirectory()) {
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let copiedNodeCount = 0;
|
|
51
|
-
const entries = await listDirectoryEntries(sourceDirectory);
|
|
52
|
-
const directoryMode = resolveManagedSyncMode(config, repoPathPrefix);
|
|
53
|
-
|
|
54
|
-
if (directoryMode === "ignore") {
|
|
55
|
-
await mkdir(targetDirectory, { recursive: true });
|
|
56
|
-
copiedNodeCount += 1;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const sourcePath = join(sourceDirectory, entry.name);
|
|
61
|
-
const targetPath = join(targetDirectory, entry.name);
|
|
62
|
-
const repoPath = posix.join(repoPathPrefix, entry.name);
|
|
63
|
-
const entryStats = await lstat(sourcePath);
|
|
64
|
-
|
|
65
|
-
if (entryStats.isDirectory()) {
|
|
66
|
-
copiedNodeCount += await copyIgnoredLocalNodesToDirectory(
|
|
67
|
-
sourcePath,
|
|
68
|
-
targetPath,
|
|
69
|
-
config,
|
|
70
|
-
repoPath,
|
|
71
|
-
);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (resolveManagedSyncMode(config, repoPath) !== "ignore") {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
80
|
-
await copyFilesystemNode(sourcePath, targetPath, entryStats);
|
|
81
|
-
copiedNodeCount += 1;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return copiedNodeCount;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const stageAndReplaceFilePath = async (
|
|
88
|
-
targetPath: string,
|
|
89
|
-
node: FileLikeSnapshotNode,
|
|
90
|
-
) => {
|
|
91
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
92
|
-
const stagingDirectory = await mkdtemp(
|
|
93
|
-
join(dirname(targetPath), `.${basename(targetPath)}.devsync-sync-`),
|
|
94
|
-
);
|
|
95
|
-
const stagedPath = join(stagingDirectory, basename(targetPath));
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
if (node.type === "symlink") {
|
|
99
|
-
await symlink(node.linkTarget, stagedPath);
|
|
100
|
-
} else {
|
|
101
|
-
await writeFileNode(stagedPath, node);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
await replacePathAtomically(targetPath, stagedPath);
|
|
105
|
-
} finally {
|
|
106
|
-
await rm(stagingDirectory, { force: true, recursive: true });
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const stageAndReplaceMergedDirectoryPath = async (
|
|
111
|
-
entry: ResolvedSyncConfigEntry,
|
|
112
|
-
config: ResolvedSyncConfig,
|
|
113
|
-
desiredNodes: ReadonlyMap<string, FileLikeSnapshotNode>,
|
|
114
|
-
) => {
|
|
115
|
-
await mkdir(dirname(entry.localPath), { recursive: true });
|
|
116
|
-
const stagingDirectory = await mkdtemp(
|
|
117
|
-
join(
|
|
118
|
-
dirname(entry.localPath),
|
|
119
|
-
`.${basename(entry.localPath)}.devsync-sync-`,
|
|
120
|
-
),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const preservedIgnoredNodeCount = await copyIgnoredLocalNodesToDirectory(
|
|
125
|
-
entry.localPath,
|
|
126
|
-
stagingDirectory,
|
|
127
|
-
config,
|
|
128
|
-
entry.repoPath,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
for (const relativePath of [...desiredNodes.keys()].sort((left, right) => {
|
|
132
|
-
return left.localeCompare(right);
|
|
133
|
-
})) {
|
|
134
|
-
const node = desiredNodes.get(relativePath);
|
|
135
|
-
|
|
136
|
-
if (node === undefined) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const targetNodePath = join(stagingDirectory, ...relativePath.split("/"));
|
|
141
|
-
|
|
142
|
-
if (node.type === "symlink") {
|
|
143
|
-
await writeSymlinkNode(targetNodePath, node.linkTarget);
|
|
144
|
-
} else {
|
|
145
|
-
await writeFileNode(targetNodePath, node);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (preservedIgnoredNodeCount === 0 && desiredNodes.size === 0) {
|
|
150
|
-
await removePathAtomically(entry.localPath);
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await replacePathAtomically(entry.localPath, stagingDirectory);
|
|
156
|
-
} finally {
|
|
157
|
-
await rm(stagingDirectory, { force: true, recursive: true });
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
export const buildEntryMaterialization = (
|
|
162
|
-
entry: ResolvedSyncConfigEntry,
|
|
163
|
-
snapshot: ReadonlyMap<string, SnapshotNode>,
|
|
164
|
-
): EntryMaterialization => {
|
|
165
|
-
if (entry.kind === "file") {
|
|
166
|
-
const node = snapshot.get(entry.repoPath);
|
|
167
|
-
|
|
168
|
-
if (node === undefined) {
|
|
169
|
-
return {
|
|
170
|
-
desiredKeys: new Set<string>(),
|
|
171
|
-
type: "absent",
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (node.type === "directory") {
|
|
176
|
-
throw new DevsyncError(
|
|
177
|
-
`File sync entry resolves to a directory in the repository: ${entry.repoPath}`,
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
desiredKeys: new Set<string>([entry.repoPath]),
|
|
183
|
-
node,
|
|
184
|
-
type: "file",
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const rootNode = snapshot.get(entry.repoPath);
|
|
189
|
-
|
|
190
|
-
if (rootNode !== undefined && rootNode.type !== "directory") {
|
|
191
|
-
throw new DevsyncError(
|
|
192
|
-
`Directory sync entry resolves to a file in the repository: ${entry.repoPath}`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const nodes = new Map<string, FileLikeSnapshotNode>();
|
|
197
|
-
const desiredKeys = new Set<string>();
|
|
198
|
-
|
|
199
|
-
for (const [repoPath, node] of snapshot.entries()) {
|
|
200
|
-
if (!repoPath.startsWith(`${entry.repoPath}/`)) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (node.type === "directory") {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const relativePath = repoPath.slice(entry.repoPath.length + 1);
|
|
209
|
-
|
|
210
|
-
nodes.set(relativePath, node);
|
|
211
|
-
desiredKeys.add(repoPath);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (rootNode === undefined && nodes.size === 0) {
|
|
215
|
-
return {
|
|
216
|
-
desiredKeys,
|
|
217
|
-
type: "absent",
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
desiredKeys.add(buildDirectoryKey(entry.repoPath));
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
desiredKeys,
|
|
225
|
-
nodes,
|
|
226
|
-
type: "directory",
|
|
227
|
-
};
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const collectLocalLeafKeys = async (
|
|
231
|
-
targetPath: string,
|
|
232
|
-
repoPathPrefix: string,
|
|
233
|
-
keys: Set<string>,
|
|
234
|
-
prefix?: string,
|
|
235
|
-
) => {
|
|
236
|
-
const stats = await getPathStats(targetPath);
|
|
237
|
-
|
|
238
|
-
if (stats === undefined) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (!stats.isDirectory()) {
|
|
243
|
-
keys.add(repoPathPrefix);
|
|
244
|
-
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
keys.add(buildDirectoryKey(repoPathPrefix));
|
|
249
|
-
|
|
250
|
-
const entries = await listDirectoryEntries(targetPath);
|
|
251
|
-
|
|
252
|
-
for (const entry of entries) {
|
|
253
|
-
const absolutePath = join(targetPath, entry.name);
|
|
254
|
-
const relativePath =
|
|
255
|
-
prefix === undefined ? entry.name : `${prefix}/${entry.name}`;
|
|
256
|
-
const childStats = await lstat(absolutePath);
|
|
257
|
-
|
|
258
|
-
if (childStats?.isDirectory()) {
|
|
259
|
-
await collectLocalLeafKeys(
|
|
260
|
-
absolutePath,
|
|
261
|
-
repoPathPrefix,
|
|
262
|
-
keys,
|
|
263
|
-
relativePath,
|
|
264
|
-
);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
keys.add(posix.join(repoPathPrefix, relativePath));
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const collectIgnoredLocalKeys = async (
|
|
273
|
-
targetPath: string,
|
|
274
|
-
repoPath: string,
|
|
275
|
-
config: ResolvedSyncConfig,
|
|
276
|
-
keys: Set<string>,
|
|
277
|
-
): Promise<boolean> => {
|
|
278
|
-
const stats = await getPathStats(targetPath);
|
|
279
|
-
|
|
280
|
-
if (stats === undefined) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const mode = resolveManagedSyncMode(config, repoPath);
|
|
285
|
-
|
|
286
|
-
if (!stats.isDirectory()) {
|
|
287
|
-
if (mode !== "ignore") {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
keys.add(repoPath);
|
|
292
|
-
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let preservedIgnoredChildren = mode === "ignore";
|
|
297
|
-
const entries = await listDirectoryEntries(targetPath);
|
|
298
|
-
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
const childPath = join(targetPath, entry.name);
|
|
301
|
-
const childRepoPath = posix.join(repoPath, entry.name);
|
|
302
|
-
|
|
303
|
-
preservedIgnoredChildren =
|
|
304
|
-
(await collectIgnoredLocalKeys(childPath, childRepoPath, config, keys)) ||
|
|
305
|
-
preservedIgnoredChildren;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (mode === "ignore" || preservedIgnoredChildren) {
|
|
309
|
-
keys.add(buildDirectoryKey(repoPath));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return mode === "ignore" || preservedIgnoredChildren;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
export const countDeletedLocalNodes = async (
|
|
316
|
-
entry: ResolvedSyncConfigEntry,
|
|
317
|
-
desiredKeys: ReadonlySet<string>,
|
|
318
|
-
config: ResolvedSyncConfig,
|
|
319
|
-
existingKeys: Set<string> = new Set<string>(),
|
|
320
|
-
) => {
|
|
321
|
-
const preservedIgnoredKeys = new Set<string>();
|
|
322
|
-
|
|
323
|
-
await collectLocalLeafKeys(entry.localPath, entry.repoPath, existingKeys);
|
|
324
|
-
await collectIgnoredLocalKeys(
|
|
325
|
-
entry.localPath,
|
|
326
|
-
entry.repoPath,
|
|
327
|
-
config,
|
|
328
|
-
preservedIgnoredKeys,
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
return [...existingKeys].filter((key) => {
|
|
332
|
-
return !desiredKeys.has(key) && !preservedIgnoredKeys.has(key);
|
|
333
|
-
}).length;
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
export const applyEntryMaterialization = async (
|
|
337
|
-
entry: ResolvedSyncConfigEntry,
|
|
338
|
-
materialization: EntryMaterialization,
|
|
339
|
-
config: ResolvedSyncConfig,
|
|
340
|
-
) => {
|
|
341
|
-
if (
|
|
342
|
-
entry.kind === "file" &&
|
|
343
|
-
resolveManagedSyncMode(config, entry.repoPath) === "ignore"
|
|
344
|
-
) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (materialization.type === "absent") {
|
|
349
|
-
if (entry.kind === "directory") {
|
|
350
|
-
await stageAndReplaceMergedDirectoryPath(entry, config, new Map());
|
|
351
|
-
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
await removePathAtomically(entry.localPath);
|
|
356
|
-
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (materialization.type === "file") {
|
|
361
|
-
await stageAndReplaceFilePath(entry.localPath, materialization.node);
|
|
362
|
-
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
await stageAndReplaceMergedDirectoryPath(
|
|
367
|
-
entry,
|
|
368
|
-
config,
|
|
369
|
-
materialization.nodes,
|
|
370
|
-
);
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
export const buildPullCounts = (
|
|
374
|
-
materializations: readonly EntryMaterialization[],
|
|
375
|
-
) => {
|
|
376
|
-
let decryptedFileCount = 0;
|
|
377
|
-
let directoryCount = 0;
|
|
378
|
-
let plainFileCount = 0;
|
|
379
|
-
let symlinkCount = 0;
|
|
380
|
-
|
|
381
|
-
for (const materialization of materializations) {
|
|
382
|
-
if (materialization === undefined) {
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (materialization.type === "file") {
|
|
387
|
-
if (materialization.node.type === "symlink") {
|
|
388
|
-
symlinkCount += 1;
|
|
389
|
-
} else if (materialization.node.secret) {
|
|
390
|
-
decryptedFileCount += 1;
|
|
391
|
-
} else {
|
|
392
|
-
plainFileCount += 1;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (materialization.type !== "directory") {
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
directoryCount += 1;
|
|
403
|
-
|
|
404
|
-
for (const node of materialization.nodes.values()) {
|
|
405
|
-
if (node.type === "symlink") {
|
|
406
|
-
symlinkCount += 1;
|
|
407
|
-
} else if (node.secret) {
|
|
408
|
-
decryptedFileCount += 1;
|
|
409
|
-
} else {
|
|
410
|
-
plainFileCount += 1;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
decryptedFileCount,
|
|
417
|
-
directoryCount,
|
|
418
|
-
plainFileCount,
|
|
419
|
-
symlinkCount,
|
|
420
|
-
};
|
|
421
|
-
};
|