@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.
- package/README.md +248 -56
- 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/forget.ts +0 -32
- package/src/cli/commands/index.ts +0 -17
- package/src/cli/commands/init.ts +0 -43
- 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/sync-output.test.ts +0 -105
- package/src/cli/sync-output.ts +0 -129
- 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/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/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 -75
- package/src/services/push.ts +0 -121
- 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/sync.dry-run.test.ts +0 -179
- package/src/services/sync.runtime.test.ts +0 -756
- package/src/services/sync.service.test.ts +0 -1025
- package/src/test/helpers/sync-fixture.ts +0 -47
package/src/services/error.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmod,
|
|
3
|
-
lstat,
|
|
4
|
-
mkdir,
|
|
5
|
-
readFile,
|
|
6
|
-
readlink,
|
|
7
|
-
rm,
|
|
8
|
-
symlink,
|
|
9
|
-
writeFile,
|
|
10
|
-
} from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
|
|
13
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
buildExecutableMode,
|
|
17
|
-
copyFilesystemNode,
|
|
18
|
-
getPathStats,
|
|
19
|
-
isExecutableMode,
|
|
20
|
-
listDirectoryEntries,
|
|
21
|
-
pathExists,
|
|
22
|
-
removePathAtomically,
|
|
23
|
-
replacePathAtomically,
|
|
24
|
-
writeFileNode,
|
|
25
|
-
writeSymlinkNode,
|
|
26
|
-
writeTextFileAtomically,
|
|
27
|
-
} from "#app/services/filesystem.ts";
|
|
28
|
-
import { createTemporaryDirectory } from "../test/helpers/sync-fixture.ts";
|
|
29
|
-
|
|
30
|
-
const temporaryDirectories: string[] = [];
|
|
31
|
-
|
|
32
|
-
const createWorkspace = async () => {
|
|
33
|
-
const directory = await createTemporaryDirectory("devsync-filesystem-");
|
|
34
|
-
|
|
35
|
-
temporaryDirectories.push(directory);
|
|
36
|
-
|
|
37
|
-
return directory;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
afterEach(async () => {
|
|
41
|
-
while (temporaryDirectories.length > 0) {
|
|
42
|
-
const directory = temporaryDirectories.pop();
|
|
43
|
-
|
|
44
|
-
if (directory !== undefined) {
|
|
45
|
-
await rm(directory, { force: true, recursive: true });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("filesystem helpers", () => {
|
|
51
|
-
it("checks path existence and missing stats", async () => {
|
|
52
|
-
const workspace = await createWorkspace();
|
|
53
|
-
const filePath = join(workspace, "value.txt");
|
|
54
|
-
|
|
55
|
-
expect(await pathExists(filePath)).toBe(false);
|
|
56
|
-
expect(await getPathStats(filePath)).toBeUndefined();
|
|
57
|
-
|
|
58
|
-
await writeFile(filePath, "value\n", "utf8");
|
|
59
|
-
|
|
60
|
-
expect(await pathExists(filePath)).toBe(true);
|
|
61
|
-
expect((await getPathStats(filePath))?.isFile()).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("lists directory entries in sorted order", async () => {
|
|
65
|
-
const workspace = await createWorkspace();
|
|
66
|
-
|
|
67
|
-
await mkdir(join(workspace, "b"), { recursive: true });
|
|
68
|
-
await writeFile(join(workspace, "c.txt"), "c\n", "utf8");
|
|
69
|
-
await writeFile(join(workspace, "a.txt"), "a\n", "utf8");
|
|
70
|
-
|
|
71
|
-
const entries = await listDirectoryEntries(workspace);
|
|
72
|
-
|
|
73
|
-
expect(entries.map((entry) => entry.name)).toEqual(["a.txt", "b", "c.txt"]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("builds and detects executable modes", async () => {
|
|
77
|
-
expect(buildExecutableMode(true)).toBe(0o755);
|
|
78
|
-
expect(buildExecutableMode(false)).toBe(0o644);
|
|
79
|
-
expect(isExecutableMode(0o100755)).toBe(true);
|
|
80
|
-
expect(isExecutableMode(0o100644)).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("writes regular files and preserves executable bits", async () => {
|
|
84
|
-
if (process.platform === "win32") {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const workspace = await createWorkspace();
|
|
89
|
-
const filePath = join(workspace, "bin", "tool.sh");
|
|
90
|
-
|
|
91
|
-
await writeFileNode(filePath, {
|
|
92
|
-
contents: "#!/bin/sh\nexit 0\n",
|
|
93
|
-
executable: true,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(await readFile(filePath, "utf8")).toContain("#!/bin/sh");
|
|
97
|
-
expect(isExecutableMode((await lstat(filePath)).mode)).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("writes symlinks after removing existing content", async () => {
|
|
101
|
-
if (process.platform === "win32") {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const workspace = await createWorkspace();
|
|
106
|
-
const linkPath = join(workspace, "links", "current");
|
|
107
|
-
|
|
108
|
-
await mkdir(join(workspace, "links"), { recursive: true });
|
|
109
|
-
await writeFile(linkPath, "old\n", "utf8");
|
|
110
|
-
await writeSymlinkNode(linkPath, "../target.txt");
|
|
111
|
-
|
|
112
|
-
expect(await readlink(linkPath)).toBe("../target.txt");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("copies regular files and symlinks", async () => {
|
|
116
|
-
if (process.platform === "win32") {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const workspace = await createWorkspace();
|
|
121
|
-
const sourceDirectory = join(workspace, "source");
|
|
122
|
-
const targetDirectory = join(workspace, "target");
|
|
123
|
-
const filePath = join(sourceDirectory, "nested", "value.txt");
|
|
124
|
-
const linkPath = join(sourceDirectory, "nested", "value-link");
|
|
125
|
-
|
|
126
|
-
await mkdir(join(sourceDirectory, "nested"), { recursive: true });
|
|
127
|
-
await writeFile(filePath, "payload\n", "utf8");
|
|
128
|
-
await chmod(filePath, 0o755);
|
|
129
|
-
await symlink("value.txt", linkPath);
|
|
130
|
-
|
|
131
|
-
await copyFilesystemNode(sourceDirectory, targetDirectory);
|
|
132
|
-
|
|
133
|
-
expect(
|
|
134
|
-
await readFile(join(targetDirectory, "nested", "value.txt"), "utf8"),
|
|
135
|
-
).toBe("payload\n");
|
|
136
|
-
expect(await readlink(join(targetDirectory, "nested", "value-link"))).toBe(
|
|
137
|
-
"value.txt",
|
|
138
|
-
);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("replaces and removes paths atomically", async () => {
|
|
142
|
-
const workspace = await createWorkspace();
|
|
143
|
-
const targetPath = join(workspace, "config.json");
|
|
144
|
-
const stagedPath = join(workspace, "next.json");
|
|
145
|
-
|
|
146
|
-
await writeFile(targetPath, "old\n", "utf8");
|
|
147
|
-
await writeFile(stagedPath, "new\n", "utf8");
|
|
148
|
-
|
|
149
|
-
await replacePathAtomically(targetPath, stagedPath);
|
|
150
|
-
|
|
151
|
-
expect(await readFile(targetPath, "utf8")).toBe("new\n");
|
|
152
|
-
expect(await pathExists(stagedPath)).toBe(false);
|
|
153
|
-
|
|
154
|
-
await removePathAtomically(targetPath);
|
|
155
|
-
|
|
156
|
-
expect(await pathExists(targetPath)).toBe(false);
|
|
157
|
-
await removePathAtomically(targetPath);
|
|
158
|
-
expect(await pathExists(targetPath)).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("writes text files atomically for create and overwrite flows", async () => {
|
|
162
|
-
const workspace = await createWorkspace();
|
|
163
|
-
const targetPath = join(workspace, "nested", "config.json");
|
|
164
|
-
|
|
165
|
-
await writeTextFileAtomically(targetPath, "first\n");
|
|
166
|
-
expect(await readFile(targetPath, "utf8")).toBe("first\n");
|
|
167
|
-
|
|
168
|
-
await writeTextFileAtomically(targetPath, "second\n");
|
|
169
|
-
expect(await readFile(targetPath, "utf8")).toBe("second\n");
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
access,
|
|
4
|
-
chmod,
|
|
5
|
-
lstat,
|
|
6
|
-
mkdir,
|
|
7
|
-
mkdtemp,
|
|
8
|
-
readdir,
|
|
9
|
-
readFile,
|
|
10
|
-
readlink,
|
|
11
|
-
rename,
|
|
12
|
-
rm,
|
|
13
|
-
symlink,
|
|
14
|
-
writeFile,
|
|
15
|
-
} from "node:fs/promises";
|
|
16
|
-
import { basename, dirname, join } from "node:path";
|
|
17
|
-
|
|
18
|
-
import { DevsyncError } from "./error.ts";
|
|
19
|
-
|
|
20
|
-
export const pathExists = async (path: string) => {
|
|
21
|
-
try {
|
|
22
|
-
await access(path);
|
|
23
|
-
|
|
24
|
-
return true;
|
|
25
|
-
} catch (error: unknown) {
|
|
26
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const getPathStats = async (path: string) => {
|
|
35
|
-
try {
|
|
36
|
-
return await lstat(path);
|
|
37
|
-
} catch (error: unknown) {
|
|
38
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const listDirectoryEntries = async (path: string) => {
|
|
47
|
-
const entries = await readdir(path, { withFileTypes: true });
|
|
48
|
-
|
|
49
|
-
return entries.sort((left, right) => {
|
|
50
|
-
return left.name.localeCompare(right.name);
|
|
51
|
-
});
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const buildExecutableMode = (executable: boolean) => {
|
|
55
|
-
return executable ? 0o755 : 0o644;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const isExecutableMode = (mode: number | bigint) => {
|
|
59
|
-
return (Number(mode) & 0o111) !== 0;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const writeFileNode = async (
|
|
63
|
-
path: string,
|
|
64
|
-
node: Readonly<{
|
|
65
|
-
contents: string | Uint8Array;
|
|
66
|
-
executable: boolean;
|
|
67
|
-
}>,
|
|
68
|
-
) => {
|
|
69
|
-
await mkdir(dirname(path), { recursive: true });
|
|
70
|
-
await writeFile(path, node.contents);
|
|
71
|
-
await chmod(path, buildExecutableMode(node.executable));
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const writeSymlinkNode = async (path: string, linkTarget: string) => {
|
|
75
|
-
await mkdir(dirname(path), { recursive: true });
|
|
76
|
-
await rm(path, { force: true, recursive: true });
|
|
77
|
-
await symlink(linkTarget, path);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const copyFilesystemNode = async (
|
|
81
|
-
sourcePath: string,
|
|
82
|
-
targetPath: string,
|
|
83
|
-
stats?: Awaited<ReturnType<typeof lstat>>,
|
|
84
|
-
) => {
|
|
85
|
-
const sourceStats = stats ?? (await lstat(sourcePath));
|
|
86
|
-
|
|
87
|
-
if (sourceStats.isDirectory()) {
|
|
88
|
-
await mkdir(targetPath, { recursive: true });
|
|
89
|
-
|
|
90
|
-
const entries = await listDirectoryEntries(sourcePath);
|
|
91
|
-
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
await copyFilesystemNode(
|
|
94
|
-
join(sourcePath, entry.name),
|
|
95
|
-
join(targetPath, entry.name),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (sourceStats.isSymbolicLink()) {
|
|
103
|
-
await writeSymlinkNode(targetPath, await readlink(sourcePath));
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!sourceStats.isFile()) {
|
|
109
|
-
throw new DevsyncError(`Unsupported filesystem entry: ${sourcePath}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
await writeFileNode(targetPath, {
|
|
113
|
-
contents: await readFile(sourcePath),
|
|
114
|
-
executable: isExecutableMode(sourceStats.mode),
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const replacePathAtomically = async (
|
|
119
|
-
targetPath: string,
|
|
120
|
-
nextPath: string,
|
|
121
|
-
) => {
|
|
122
|
-
const backupPath = join(
|
|
123
|
-
dirname(targetPath),
|
|
124
|
-
`.${basename(targetPath)}.devsync-sync-backup-${randomUUID()}`,
|
|
125
|
-
);
|
|
126
|
-
const existingStats = await getPathStats(targetPath);
|
|
127
|
-
let targetMoved = false;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
if (existingStats !== undefined) {
|
|
131
|
-
await rename(targetPath, backupPath);
|
|
132
|
-
targetMoved = true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
await rename(nextPath, targetPath);
|
|
136
|
-
|
|
137
|
-
if (targetMoved) {
|
|
138
|
-
await rm(backupPath, { force: true, recursive: true });
|
|
139
|
-
}
|
|
140
|
-
} catch (error: unknown) {
|
|
141
|
-
if (targetMoved && !(await pathExists(targetPath))) {
|
|
142
|
-
await rename(backupPath, targetPath).catch(() => {});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
throw error;
|
|
146
|
-
} finally {
|
|
147
|
-
await rm(backupPath, { force: true, recursive: true }).catch(() => {});
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const removePathAtomically = async (targetPath: string) => {
|
|
152
|
-
const stats = await getPathStats(targetPath);
|
|
153
|
-
|
|
154
|
-
if (stats === undefined) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const backupPath = join(
|
|
159
|
-
dirname(targetPath),
|
|
160
|
-
`.${basename(targetPath)}.devsync-sync-remove-${randomUUID()}`,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
await rename(targetPath, backupPath);
|
|
164
|
-
await rm(backupPath, { force: true, recursive: true });
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
export const writeTextFileAtomically = async (
|
|
168
|
-
targetPath: string,
|
|
169
|
-
contents: string,
|
|
170
|
-
) => {
|
|
171
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
172
|
-
const stagingDirectory = await mkdtemp(
|
|
173
|
-
join(dirname(targetPath), `.${basename(targetPath)}.devsync-sync-`),
|
|
174
|
-
);
|
|
175
|
-
const stagedPath = join(stagingDirectory, basename(targetPath));
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await writeFile(stagedPath, contents, "utf8");
|
|
179
|
-
await replacePathAtomically(targetPath, stagedPath);
|
|
180
|
-
} finally {
|
|
181
|
-
await rm(stagingDirectory, { force: true, recursive: true });
|
|
182
|
-
}
|
|
183
|
-
};
|
package/src/services/forget.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
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
|
-
};
|
package/src/services/git.test.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { DevsyncError } from "#app/services/error.ts";
|
|
7
|
-
import {
|
|
8
|
-
ensureGitRepository,
|
|
9
|
-
ensureRepository,
|
|
10
|
-
initializeRepository,
|
|
11
|
-
} from "#app/services/git.ts";
|
|
12
|
-
import {
|
|
13
|
-
createTemporaryDirectory,
|
|
14
|
-
runGit,
|
|
15
|
-
} from "../test/helpers/sync-fixture.ts";
|
|
16
|
-
|
|
17
|
-
const temporaryDirectories: string[] = [];
|
|
18
|
-
|
|
19
|
-
const createWorkspace = async () => {
|
|
20
|
-
const directory = await createTemporaryDirectory("devsync-git-");
|
|
21
|
-
|
|
22
|
-
temporaryDirectories.push(directory);
|
|
23
|
-
|
|
24
|
-
return directory;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
while (temporaryDirectories.length > 0) {
|
|
29
|
-
const directory = temporaryDirectories.pop();
|
|
30
|
-
|
|
31
|
-
if (directory !== undefined) {
|
|
32
|
-
await rm(directory, { force: true, recursive: true });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("git helpers", () => {
|
|
38
|
-
it("initializes a repository with a main branch", async () => {
|
|
39
|
-
const workspace = await createWorkspace();
|
|
40
|
-
const repositoryPath = join(workspace, "sync");
|
|
41
|
-
|
|
42
|
-
await expect(initializeRepository(repositoryPath)).resolves.toEqual({
|
|
43
|
-
action: "initialized",
|
|
44
|
-
});
|
|
45
|
-
await expect(ensureRepository(repositoryPath)).resolves.toBeUndefined();
|
|
46
|
-
await expect(
|
|
47
|
-
runGit(["-C", repositoryPath, "symbolic-ref", "--short", "HEAD"]),
|
|
48
|
-
).resolves.toMatchObject({
|
|
49
|
-
stdout: "main\n",
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("clones an existing repository and reports the source", async () => {
|
|
54
|
-
const workspace = await createWorkspace();
|
|
55
|
-
const sourcePath = join(workspace, "source");
|
|
56
|
-
const targetPath = join(workspace, "clone");
|
|
57
|
-
|
|
58
|
-
await runGit(["init", "-b", "main", sourcePath], workspace);
|
|
59
|
-
|
|
60
|
-
await expect(initializeRepository(targetPath, sourcePath)).resolves.toEqual(
|
|
61
|
-
{
|
|
62
|
-
action: "cloned",
|
|
63
|
-
source: sourcePath,
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
await expect(ensureRepository(targetPath)).resolves.toBeUndefined();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("wraps missing git repositories in a DevsyncError", async () => {
|
|
70
|
-
const workspace = await createWorkspace();
|
|
71
|
-
const missingRepositoryPath = join(workspace, "not-a-repo");
|
|
72
|
-
|
|
73
|
-
await expect(
|
|
74
|
-
ensureRepository(missingRepositoryPath),
|
|
75
|
-
).rejects.toThrowError();
|
|
76
|
-
await expect(
|
|
77
|
-
ensureGitRepository(missingRepositoryPath),
|
|
78
|
-
).rejects.toThrowError(DevsyncError);
|
|
79
|
-
await expect(
|
|
80
|
-
ensureGitRepository(missingRepositoryPath),
|
|
81
|
-
).rejects.toThrowError(/Sync directory is not a git repository/u);
|
|
82
|
-
});
|
|
83
|
-
});
|