@tinyrack/devsync 1.0.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.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/package.json +81 -0
- package/src/cli/commands/add.ts +40 -0
- package/src/cli/commands/cd.ts +80 -0
- package/src/cli/commands/forget.ts +32 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +43 -0
- package/src/cli/commands/pull.ts +31 -0
- package/src/cli/commands/push.ts +31 -0
- package/src/cli/commands/set.ts +47 -0
- package/src/cli/sync-output.ts +129 -0
- package/src/config/sync.ts +572 -0
- package/src/config/xdg.ts +138 -0
- package/src/index.ts +4 -0
- package/src/lib/string.ts +3 -0
- package/src/lib/validation.ts +11 -0
- package/src/services/add.ts +178 -0
- package/src/services/config-file.ts +101 -0
- package/src/services/crypto.ts +83 -0
- package/src/services/error.ts +6 -0
- package/src/services/filesystem.ts +183 -0
- package/src/services/forget.ts +261 -0
- package/src/services/git.ts +74 -0
- package/src/services/init.ts +244 -0
- package/src/services/local-materialization.ts +421 -0
- package/src/services/local-snapshot.ts +173 -0
- package/src/services/paths.ts +98 -0
- package/src/services/pull.ts +75 -0
- package/src/services/push.ts +121 -0
- package/src/services/repo-artifacts.ts +262 -0
- package/src/services/repo-snapshot.ts +197 -0
- package/src/services/runtime.ts +57 -0
- package/src/services/set.ts +383 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, posix } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type ResolvedSyncConfig,
|
|
6
|
+
type ResolvedSyncConfigEntry,
|
|
7
|
+
readSyncConfig,
|
|
8
|
+
resolveSyncArtifactsDirectoryPath,
|
|
9
|
+
} from "#app/config/sync.ts";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createSyncConfigDocument,
|
|
13
|
+
sortSyncConfigEntries,
|
|
14
|
+
writeValidatedSyncConfig,
|
|
15
|
+
} from "./config-file.ts";
|
|
16
|
+
import { DevsyncError } from "./error.ts";
|
|
17
|
+
import {
|
|
18
|
+
getPathStats,
|
|
19
|
+
listDirectoryEntries,
|
|
20
|
+
removePathAtomically,
|
|
21
|
+
} from "./filesystem.ts";
|
|
22
|
+
import {
|
|
23
|
+
isExplicitLocalPath,
|
|
24
|
+
isPathEqualOrNested,
|
|
25
|
+
resolveCommandTargetPath,
|
|
26
|
+
tryNormalizeRepoPathInput,
|
|
27
|
+
} from "./paths.ts";
|
|
28
|
+
import {
|
|
29
|
+
isSecretArtifactPath,
|
|
30
|
+
resolveArtifactRelativePath,
|
|
31
|
+
} from "./repo-artifacts.ts";
|
|
32
|
+
import { ensureSyncRepository, type SyncContext } from "./runtime.ts";
|
|
33
|
+
|
|
34
|
+
export type SyncForgetRequest = Readonly<{
|
|
35
|
+
target: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export type SyncForgetResult = Readonly<{
|
|
39
|
+
configPath: string;
|
|
40
|
+
localPath: string;
|
|
41
|
+
plainArtifactCount: number;
|
|
42
|
+
repoPath: string;
|
|
43
|
+
secretArtifactCount: number;
|
|
44
|
+
syncDirectory: string;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
const findMatchingTrackedEntry = (
|
|
48
|
+
config: ResolvedSyncConfig,
|
|
49
|
+
target: string,
|
|
50
|
+
context: Pick<SyncContext, "cwd" | "environment">,
|
|
51
|
+
) => {
|
|
52
|
+
const trimmedTarget = target.trim();
|
|
53
|
+
const resolvedTargetPath = resolveCommandTargetPath(
|
|
54
|
+
trimmedTarget,
|
|
55
|
+
context.environment,
|
|
56
|
+
context.cwd,
|
|
57
|
+
);
|
|
58
|
+
const byLocalPath = config.entries.find((entry) => {
|
|
59
|
+
return entry.localPath === resolvedTargetPath;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (byLocalPath !== undefined || isExplicitLocalPath(trimmedTarget)) {
|
|
63
|
+
return byLocalPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const normalizedRepoPath = tryNormalizeRepoPathInput(trimmedTarget);
|
|
67
|
+
|
|
68
|
+
if (normalizedRepoPath === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return config.entries.find((entry) => {
|
|
73
|
+
return entry.repoPath === normalizedRepoPath;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const collectRepoArtifactCounts = async (
|
|
78
|
+
targetPath: string,
|
|
79
|
+
counts: {
|
|
80
|
+
plain: number;
|
|
81
|
+
secret: number;
|
|
82
|
+
},
|
|
83
|
+
relativePath: string,
|
|
84
|
+
) => {
|
|
85
|
+
const stats = await getPathStats(targetPath);
|
|
86
|
+
|
|
87
|
+
if (stats === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (stats.isDirectory()) {
|
|
92
|
+
counts.plain += 1;
|
|
93
|
+
|
|
94
|
+
const entries = await listDirectoryEntries(targetPath);
|
|
95
|
+
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
await collectRepoArtifactCounts(
|
|
98
|
+
join(targetPath, entry.name),
|
|
99
|
+
counts,
|
|
100
|
+
posix.join(relativePath, entry.name),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isSecretArtifactPath(relativePath)) {
|
|
108
|
+
counts.secret += 1;
|
|
109
|
+
} else {
|
|
110
|
+
counts.plain += 1;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const collectEntryArtifactCounts = async (
|
|
115
|
+
syncDirectory: string,
|
|
116
|
+
entry: ResolvedSyncConfigEntry,
|
|
117
|
+
) => {
|
|
118
|
+
const artifactsRoot = resolveSyncArtifactsDirectoryPath(syncDirectory);
|
|
119
|
+
const counts = {
|
|
120
|
+
plain: 0,
|
|
121
|
+
secret: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (entry.kind === "directory") {
|
|
125
|
+
await collectRepoArtifactCounts(
|
|
126
|
+
join(artifactsRoot, ...entry.repoPath.split("/")),
|
|
127
|
+
counts,
|
|
128
|
+
entry.repoPath,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
await collectRepoArtifactCounts(
|
|
132
|
+
join(artifactsRoot, ...entry.repoPath.split("/")),
|
|
133
|
+
counts,
|
|
134
|
+
entry.repoPath,
|
|
135
|
+
);
|
|
136
|
+
await collectRepoArtifactCounts(
|
|
137
|
+
join(
|
|
138
|
+
artifactsRoot,
|
|
139
|
+
...resolveArtifactRelativePath({
|
|
140
|
+
category: "secret",
|
|
141
|
+
repoPath: entry.repoPath,
|
|
142
|
+
}).split("/"),
|
|
143
|
+
),
|
|
144
|
+
counts,
|
|
145
|
+
resolveArtifactRelativePath({
|
|
146
|
+
category: "secret",
|
|
147
|
+
repoPath: entry.repoPath,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
plainArtifactCount: counts.plain,
|
|
154
|
+
secretArtifactCount: counts.secret,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const pruneEmptyParentDirectories = async (
|
|
159
|
+
startPath: string,
|
|
160
|
+
rootPath: string,
|
|
161
|
+
) => {
|
|
162
|
+
let currentPath = startPath;
|
|
163
|
+
|
|
164
|
+
while (
|
|
165
|
+
isPathEqualOrNested(currentPath, rootPath) &&
|
|
166
|
+
currentPath !== rootPath
|
|
167
|
+
) {
|
|
168
|
+
const stats = await getPathStats(currentPath);
|
|
169
|
+
|
|
170
|
+
if (stats === undefined) {
|
|
171
|
+
currentPath = dirname(currentPath);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!stats.isDirectory()) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entries = await readdir(currentPath);
|
|
180
|
+
|
|
181
|
+
if (entries.length > 0) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await rm(currentPath, { force: true, recursive: true });
|
|
186
|
+
currentPath = dirname(currentPath);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const removeTrackedEntryArtifacts = async (
|
|
191
|
+
syncDirectory: string,
|
|
192
|
+
entry: ResolvedSyncConfigEntry,
|
|
193
|
+
) => {
|
|
194
|
+
const artifactsRoot = resolveSyncArtifactsDirectoryPath(syncDirectory);
|
|
195
|
+
const plainPath = join(artifactsRoot, ...entry.repoPath.split("/"));
|
|
196
|
+
|
|
197
|
+
await removePathAtomically(plainPath);
|
|
198
|
+
await pruneEmptyParentDirectories(dirname(plainPath), artifactsRoot);
|
|
199
|
+
|
|
200
|
+
if (entry.kind === "directory") {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const secretPath = join(
|
|
205
|
+
artifactsRoot,
|
|
206
|
+
...resolveArtifactRelativePath({
|
|
207
|
+
category: "secret",
|
|
208
|
+
repoPath: entry.repoPath,
|
|
209
|
+
}).split("/"),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
await removePathAtomically(secretPath);
|
|
213
|
+
await pruneEmptyParentDirectories(dirname(secretPath), artifactsRoot);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const forgetSyncTarget = async (
|
|
217
|
+
request: SyncForgetRequest,
|
|
218
|
+
context: SyncContext,
|
|
219
|
+
): Promise<SyncForgetResult> => {
|
|
220
|
+
const target = request.target.trim();
|
|
221
|
+
|
|
222
|
+
if (target.length === 0) {
|
|
223
|
+
throw new DevsyncError("Target path is required.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await ensureSyncRepository(context);
|
|
227
|
+
|
|
228
|
+
const config = await readSyncConfig(
|
|
229
|
+
context.paths.syncDirectory,
|
|
230
|
+
context.environment,
|
|
231
|
+
);
|
|
232
|
+
const entry = findMatchingTrackedEntry(config, target, context);
|
|
233
|
+
|
|
234
|
+
if (entry === undefined) {
|
|
235
|
+
throw new DevsyncError(`No tracked sync entry matches: ${target}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { plainArtifactCount, secretArtifactCount } =
|
|
239
|
+
await collectEntryArtifactCounts(context.paths.syncDirectory, entry);
|
|
240
|
+
const nextConfig = createSyncConfigDocument(config);
|
|
241
|
+
|
|
242
|
+
nextConfig.entries = sortSyncConfigEntries(
|
|
243
|
+
nextConfig.entries.filter((configEntry) => {
|
|
244
|
+
return configEntry.repoPath !== entry.repoPath;
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
await writeValidatedSyncConfig(context.paths.syncDirectory, nextConfig, {
|
|
249
|
+
environment: context.environment,
|
|
250
|
+
});
|
|
251
|
+
await removeTrackedEntryArtifacts(context.paths.syncDirectory, entry);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
configPath: context.paths.configPath,
|
|
255
|
+
localPath: entry.localPath,
|
|
256
|
+
plainArtifactCount,
|
|
257
|
+
repoPath: entry.repoPath,
|
|
258
|
+
secretArtifactCount,
|
|
259
|
+
syncDirectory: context.paths.syncDirectory,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { DevsyncError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
const runGitCommand = async (
|
|
9
|
+
args: readonly string[],
|
|
10
|
+
options?: Readonly<{ cwd?: string }>,
|
|
11
|
+
) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = await execFileAsync("git", [...args], {
|
|
14
|
+
cwd: options?.cwd,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
maxBuffer: 10_000_000,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
stderr: result.stderr,
|
|
21
|
+
stdout: result.stdout,
|
|
22
|
+
};
|
|
23
|
+
} catch (error: unknown) {
|
|
24
|
+
if (error instanceof Error && "stderr" in error) {
|
|
25
|
+
const stderr =
|
|
26
|
+
typeof error.stderr === "string" ? error.stderr.trim() : undefined;
|
|
27
|
+
const stdout =
|
|
28
|
+
"stdout" in error && typeof error.stdout === "string"
|
|
29
|
+
? error.stdout.trim()
|
|
30
|
+
: undefined;
|
|
31
|
+
const message = stderr || stdout || error.message;
|
|
32
|
+
|
|
33
|
+
throw new Error(message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(error instanceof Error ? error.message : "git failed.");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ensureRepository = async (directory: string) => {
|
|
41
|
+
await runGitCommand(["-C", directory, "rev-parse", "--is-inside-work-tree"]);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const initializeRepository = async (
|
|
45
|
+
directory: string,
|
|
46
|
+
source?: string,
|
|
47
|
+
) => {
|
|
48
|
+
if (source === undefined) {
|
|
49
|
+
await runGitCommand(["init", "-b", "main", directory]);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
action: "initialized" as const,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await runGitCommand(["clone", source, directory]);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
action: "cloned" as const,
|
|
60
|
+
source,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const ensureGitRepository = async (syncDirectory: string) => {
|
|
65
|
+
try {
|
|
66
|
+
await ensureRepository(syncDirectory);
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
throw new DevsyncError(
|
|
69
|
+
error instanceof Error
|
|
70
|
+
? `Sync directory is not a git repository: ${error.message}`
|
|
71
|
+
: "Sync directory is not a git repository.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
};
|