@procedures/snapshot 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +737 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +16 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +18 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +15 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +99 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/snapshot/archive-io.d.ts +12 -0
  14. package/dist/procedures/snapshot/archive-io.d.ts.map +1 -0
  15. package/dist/procedures/snapshot/archive-io.js +29 -0
  16. package/dist/procedures/snapshot/archive-io.js.map +1 -0
  17. package/dist/procedures/snapshot/commands.d.ts +15 -0
  18. package/dist/procedures/snapshot/commands.d.ts.map +1 -0
  19. package/dist/procedures/snapshot/commands.js +32 -0
  20. package/dist/procedures/snapshot/commands.js.map +1 -0
  21. package/dist/procedures/snapshot/create.d.ts +12 -0
  22. package/dist/procedures/snapshot/create.d.ts.map +1 -0
  23. package/dist/procedures/snapshot/create.js +279 -0
  24. package/dist/procedures/snapshot/create.js.map +1 -0
  25. package/dist/procedures/snapshot/delete.d.ts +12 -0
  26. package/dist/procedures/snapshot/delete.d.ts.map +1 -0
  27. package/dist/procedures/snapshot/delete.js +31 -0
  28. package/dist/procedures/snapshot/delete.js.map +1 -0
  29. package/dist/procedures/snapshot/diff.d.ts +12 -0
  30. package/dist/procedures/snapshot/diff.d.ts.map +1 -0
  31. package/dist/procedures/snapshot/diff.js +113 -0
  32. package/dist/procedures/snapshot/diff.js.map +1 -0
  33. package/dist/procedures/snapshot/index.d.ts +9 -0
  34. package/dist/procedures/snapshot/index.d.ts.map +1 -0
  35. package/dist/procedures/snapshot/index.js +9 -0
  36. package/dist/procedures/snapshot/index.js.map +1 -0
  37. package/dist/procedures/snapshot/list.d.ts +12 -0
  38. package/dist/procedures/snapshot/list.d.ts.map +1 -0
  39. package/dist/procedures/snapshot/list.js +57 -0
  40. package/dist/procedures/snapshot/list.js.map +1 -0
  41. package/dist/procedures/snapshot/names.d.ts +10 -0
  42. package/dist/procedures/snapshot/names.d.ts.map +1 -0
  43. package/dist/procedures/snapshot/names.js +20 -0
  44. package/dist/procedures/snapshot/names.js.map +1 -0
  45. package/dist/procedures/snapshot/restore.d.ts +12 -0
  46. package/dist/procedures/snapshot/restore.d.ts.map +1 -0
  47. package/dist/procedures/snapshot/restore.js +131 -0
  48. package/dist/procedures/snapshot/restore.js.map +1 -0
  49. package/dist/procedures/snapshot/s3-lookup.d.ts +39 -0
  50. package/dist/procedures/snapshot/s3-lookup.d.ts.map +1 -0
  51. package/dist/procedures/snapshot/s3-lookup.js +57 -0
  52. package/dist/procedures/snapshot/s3-lookup.js.map +1 -0
  53. package/dist/register.d.ts +8 -0
  54. package/dist/register.d.ts.map +1 -0
  55. package/dist/register.js +14 -0
  56. package/dist/register.js.map +1 -0
  57. package/dist/types.d.ts +193 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +168 -0
  60. package/dist/types.js.map +1 -0
  61. package/package.json +84 -3
  62. package/src/index.ts +52 -0
  63. package/src/module.ts +136 -0
  64. package/src/procedures/snapshot/archive-io.ts +31 -0
  65. package/src/procedures/snapshot/commands.ts +34 -0
  66. package/src/procedures/snapshot/create.ts +354 -0
  67. package/src/procedures/snapshot/delete.ts +40 -0
  68. package/src/procedures/snapshot/diff.ts +147 -0
  69. package/src/procedures/snapshot/index.ts +9 -0
  70. package/src/procedures/snapshot/list.ts +80 -0
  71. package/src/procedures/snapshot/names.ts +24 -0
  72. package/src/procedures/snapshot/restore.ts +166 -0
  73. package/src/procedures/snapshot/s3-lookup.ts +88 -0
  74. package/src/register.ts +17 -0
  75. package/src/types.ts +381 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-io.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/archive-io.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,+CAA+C;AAC/C,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9D;AAED,yDAAyD;AACzD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9F"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * File access for snapshot archives without the whole archive in memory.
3
+ *
4
+ * Before, create and restore read each archive into memory (for the checksum and the upload).
5
+ * An archive over 2 GiB failed, and the peak was about 2.3 times the archive (deep dive DATA-19).
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { createReadStream } from "node:fs";
9
+ /** The SHA-256 of a file, read as a stream. */
10
+ export async function sha256File(path) {
11
+ const hash = createHash("sha256");
12
+ for await (const chunk of createReadStream(path)) {
13
+ hash.update(chunk);
14
+ }
15
+ return hash.digest("hex");
16
+ }
17
+ /** The bytes of a file from `offset`, at most `size`. */
18
+ export async function readPart(file, offset, size) {
19
+ const buffer = Buffer.alloc(size);
20
+ let filled = 0;
21
+ while (filled < size) {
22
+ const { bytesRead } = await file.read(buffer, filled, size - filled, offset + filled);
23
+ if (bytesRead === 0)
24
+ break;
25
+ filled += bytesRead;
26
+ }
27
+ return buffer.subarray(0, filled);
28
+ }
29
+ //# sourceMappingURL=archive-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-io.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/archive-io.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAG3C,+CAA+C;AAC/C,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,KAAe,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAgB,EAAE,MAAc,EAAE,IAAY;IAC3E,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,MAAM,GAAG,IAAI,EAAE,CAAC;QACrB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;QACtF,IAAI,SAAS,KAAK,CAAC;YAAE,MAAM;QAC3B,MAAM,IAAI,SAAS,CAAC;IACtB,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The programs that the snapshot procedures run: git and pnpm, through `runCommand`.
3
+ *
4
+ * `runCommand` is async, kills the process tree when the signal aborts, and runs no shell.
5
+ * Before, the procedures used `execSync`: it blocked the event loop of the host for each call,
6
+ * ignored the signal, and passed the command line to a shell.
7
+ */
8
+ /**
9
+ * This function runs `git <args>` in `cwd` and gives its trimmed output. It gives `undefined`
10
+ * when git fails, when it cannot start, or when the signal aborts.
11
+ */
12
+ export declare function gitText(args: readonly string[], cwd: string, signal?: AbortSignal): Promise<string | undefined>;
13
+ /** This function gives the version of pnpm, or "unknown" when pnpm cannot run. */
14
+ export declare function pnpmVersion(signal?: AbortSignal): Promise<string>;
15
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGrH;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CASvE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The programs that the snapshot procedures run: git and pnpm, through `runCommand`.
3
+ *
4
+ * `runCommand` is async, kills the process tree when the signal aborts, and runs no shell.
5
+ * Before, the procedures used `execSync`: it blocked the event loop of the host for each call,
6
+ * ignored the signal, and passed the command line to a shell.
7
+ */
8
+ import { runCommand } from "@procedures/shell/command";
9
+ import { resolvePnpm } from "@procedures/pnpm";
10
+ /** The longest time of one git or pnpm query, in milliseconds. */
11
+ const QUERY_TIMEOUT_MS = 30_000;
12
+ /**
13
+ * This function runs `git <args>` in `cwd` and gives its trimmed output. It gives `undefined`
14
+ * when git fails, when it cannot start, or when the signal aborts.
15
+ */
16
+ export async function gitText(args, cwd, signal) {
17
+ const result = await runCommand("git", { args, cwd, signal, timeout: QUERY_TIMEOUT_MS });
18
+ return result.success ? result.stdout.trim() : undefined;
19
+ }
20
+ /** This function gives the version of pnpm, or "unknown" when pnpm cannot run. */
21
+ export async function pnpmVersion(signal) {
22
+ let pnpm;
23
+ try {
24
+ pnpm = resolvePnpm();
25
+ }
26
+ catch {
27
+ return "unknown";
28
+ }
29
+ const result = await runCommand(pnpm.command, { args: [...pnpm.prefix, "--version"], signal, timeout: QUERY_TIMEOUT_MS });
30
+ return result.success ? result.stdout.trim() : "unknown";
31
+ }
32
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,kEAAkE;AAClE,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAuB,EAAE,GAAW,EAAE,MAAoB;IACtF,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACzF,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAoB;IACpD,IAAI,IAA2C,CAAC;IAChD,IAAI,CAAC;QACH,IAAI,GAAG,WAAW,EAAE,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC1H,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * snapshot.create procedure
3
+ *
4
+ * Create an environment snapshot and upload to S3.
5
+ */
6
+ import type { ProcedureContext } from "@procedures/client";
7
+ import type { SnapshotCreateInput, SnapshotCreateOutput } from "../../types.js";
8
+ /**
9
+ * Create a snapshot of the environment
10
+ */
11
+ export declare function snapshotCreate(input: SnapshotCreateInput, ctx: ProcedureContext): Promise<SnapshotCreateOutput>;
12
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/create.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAI3D,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EAIrB,MAAM,gBAAgB,CAAC;AAKxB;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,oBAAoB,CAAC,CAgG/B"}
@@ -0,0 +1,279 @@
1
+ /**
2
+ * snapshot.create procedure
3
+ *
4
+ * Create an environment snapshot and upload to S3.
5
+ */
6
+ import { existsSync, mkdtempSync, rmSync, readFileSync, writeFileSync, statSync } from "node:fs";
7
+ import { open } from "node:fs/promises";
8
+ import { join, basename, dirname, resolve } from "node:path";
9
+ import { tmpdir, hostname } from "node:os";
10
+ import { randomUUID } from "node:crypto";
11
+ import * as tar from "tar";
12
+ import { assertSnapshotName, isExcluded } from "./names.js";
13
+ import { readPart, sha256File } from "./archive-io.js";
14
+ import { gitText, pnpmVersion } from "./commands.js";
15
+ /** The size of each part of a multipart upload (the S3 minimum) */
16
+ const PART_SIZE = 5 * 1024 * 1024;
17
+ /**
18
+ * Create a snapshot of the environment
19
+ */
20
+ export async function snapshotCreate(input, ctx) {
21
+ // A plain name: it goes into S3 keys and the snapshot id (a "/" made ids that restore,
22
+ // diff and delete never found: deep dive DATA-1, DATA-16)
23
+ assertSnapshotName("name", input.name);
24
+ const id = `${input.name}-${Date.now()}-${randomUUID().slice(0, 8)}`;
25
+ const workDir = mkdtempSync(join(tmpdir(), "snapshot-"));
26
+ try {
27
+ // Get repository paths to snapshot
28
+ const repoPaths = input.paths || (await getEcosystemPaths(ctx));
29
+ // Gather repository info
30
+ const repositories = await Promise.all(repoPaths.map((path) => getRepositoryInfo(path, ctx)));
31
+ // Get environment info
32
+ const environment = await getEnvironmentInfo(ctx.signal);
33
+ // Create metadata
34
+ const metadata = {
35
+ id,
36
+ name: input.name,
37
+ preset: input.preset,
38
+ createdAt: new Date().toISOString(),
39
+ environment,
40
+ repositories,
41
+ checksum: "", // Will be calculated after archive creation
42
+ archiveSize: 0,
43
+ description: input.description,
44
+ };
45
+ // Write metadata to work directory
46
+ const metadataPath = join(workDir, "metadata.json");
47
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
48
+ // Create archive based on preset
49
+ const archivePath = join(workDir, `${id}.tar.gz`);
50
+ await createArchive(archivePath, repoPaths, input.preset, workDir);
51
+ // Calculate checksum (a stream: the archive is never in memory as a whole)
52
+ const checksum = await sha256File(archivePath);
53
+ const archiveSize = statSync(archivePath).size;
54
+ // Update metadata with checksum
55
+ metadata.checksum = checksum;
56
+ metadata.archiveSize = archiveSize;
57
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
58
+ // Upload to S3
59
+ const uploadStart = Date.now();
60
+ const s3Key = `snapshots/${input.name}/${id}.tar.gz`;
61
+ // Use multipart upload for large files (> 5MB). The parts are read from the file, one at a
62
+ // time (deep dive DATA-19).
63
+ if (archiveSize > PART_SIZE) {
64
+ await multipartUpload(ctx, input.bucket, s3Key, archivePath, archiveSize);
65
+ }
66
+ else {
67
+ await ctx.client.call(["s3", "upload"], {
68
+ bucket: input.bucket,
69
+ key: s3Key,
70
+ body: readFileSync(archivePath).toString("base64"),
71
+ base64: true,
72
+ contentType: "application/gzip",
73
+ metadata: {
74
+ snapshotId: id,
75
+ snapshotName: input.name,
76
+ preset: input.preset,
77
+ checksum,
78
+ },
79
+ });
80
+ }
81
+ // Also upload metadata separately for quick listing
82
+ await ctx.client.call(["s3", "upload"], {
83
+ bucket: input.bucket,
84
+ key: `snapshots/${input.name}/${id}.metadata.json`,
85
+ body: JSON.stringify(metadata, null, 2),
86
+ contentType: "application/json",
87
+ });
88
+ const uploadDuration = Date.now() - uploadStart;
89
+ return {
90
+ id,
91
+ location: `s3://${input.bucket}/${s3Key}`,
92
+ metadata,
93
+ uploadDuration,
94
+ };
95
+ }
96
+ finally {
97
+ // Cleanup work directory
98
+ if (existsSync(workDir)) {
99
+ rmSync(workDir, { recursive: true, force: true });
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * Get ecosystem repository paths from manifest
105
+ */
106
+ async function getEcosystemPaths(_ctx) {
107
+ // TODO: Read from ecosystem manifest
108
+ // For now, return current working directory
109
+ return [process.cwd()];
110
+ }
111
+ /**
112
+ * Get repository info for a path
113
+ */
114
+ async function getRepositoryInfo(repoPath, ctx) {
115
+ try {
116
+ const result = await ctx.client.call(["git", "status"], { cwd: repoPath });
117
+ // The commit hash ("" for a repository with no commit) and the remote URL (none without "origin")
118
+ const commit = (await gitText(["rev-parse", "HEAD"], repoPath, ctx.signal)) ?? "";
119
+ const remoteUrl = await gitText(["remote", "get-url", "origin"], repoPath, ctx.signal);
120
+ // Get stash count
121
+ let stashCount = 0;
122
+ try {
123
+ const stashList = await ctx.client.call(["git", "stash", "list"], { cwd: repoPath });
124
+ stashCount = stashList.count;
125
+ }
126
+ catch {
127
+ // No stashes
128
+ }
129
+ // Get package name from package.json
130
+ let name = basename(repoPath);
131
+ try {
132
+ const pkgPath = join(repoPath, "package.json");
133
+ if (existsSync(pkgPath)) {
134
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
135
+ name = pkg.name || name;
136
+ }
137
+ }
138
+ catch {
139
+ // Use directory name
140
+ }
141
+ return {
142
+ path: repoPath,
143
+ name,
144
+ branch: result.branch,
145
+ commit,
146
+ dirty: !result.clean,
147
+ stashCount,
148
+ remoteUrl,
149
+ ahead: result.ahead,
150
+ behind: result.behind,
151
+ };
152
+ }
153
+ catch (error) {
154
+ // Not a git repo or error
155
+ return {
156
+ path: repoPath,
157
+ name: basename(repoPath),
158
+ branch: "",
159
+ commit: "",
160
+ dirty: false,
161
+ stashCount: 0,
162
+ ahead: 0,
163
+ behind: 0,
164
+ };
165
+ }
166
+ }
167
+ /**
168
+ * Get environment info
169
+ */
170
+ async function getEnvironmentInfo(signal) {
171
+ return {
172
+ os: process.platform,
173
+ nodeVersion: process.version,
174
+ pnpmVersion: await pnpmVersion(signal),
175
+ username: process.env["USER"] || process.env["USERNAME"] || "unknown",
176
+ hostname: hostname(),
177
+ };
178
+ }
179
+ /**
180
+ * Create tar archive based on preset
181
+ */
182
+ async function createArchive(archivePath, repoPaths, preset, workDir) {
183
+ // Only archive repositories that actually exist on disk.
184
+ const existingRepos = repoPaths.filter((p) => existsSync(p));
185
+ // tar.create accepts a single `cwd`, and each entry is stored relative to it.
186
+ // To store each repository as a top-level `<basename>/...` entry we must run
187
+ // with `cwd` set to the directory that contains the repositories (the
188
+ // ecosystem root), not the fresh temp `workDir` the repos are not inside.
189
+ const parents = new Set(existingRepos.map((p) => resolve(dirname(p))));
190
+ if (parents.size > 1) {
191
+ throw new Error(`snapshot.create requires all repositories to share a parent directory; got multiple roots: ${[
192
+ ...parents,
193
+ ].join(", ")}`);
194
+ }
195
+ const cwd = existingRepos.length > 0 ? resolve(dirname(existingRepos[0])) : workDir;
196
+ // Create gzip options based on preset
197
+ const gzipOpts = { level: preset === "heavy" ? 6 : 9 };
198
+ // Build exclude patterns based on preset
199
+ const excludePatterns = [];
200
+ if (preset === "light") {
201
+ // Exclude node_modules and pnpm store
202
+ excludePatterns.push("node_modules", ".pnpm-store", "dist", "*.log");
203
+ }
204
+ else if (preset === "medium") {
205
+ // Include node_modules but exclude pnpm store
206
+ excludePatterns.push(".pnpm-store", "*.log");
207
+ }
208
+ // heavy preset includes everything
209
+ await tar.create({
210
+ gzip: gzipOpts,
211
+ file: archivePath,
212
+ cwd,
213
+ // Whole path segments, and "*.ext" for file endings. (Before, `path.includes("dist")`
214
+ // left out src/distance.ts, and "*.log" matched nothing: deep dive DATA-3.)
215
+ filter: (path) => !isExcluded(path, excludePatterns),
216
+ }, existingRepos.map((p) => basename(p)));
217
+ }
218
+ /**
219
+ * Multipart upload for large files. Each part is read from the file when it goes up.
220
+ */
221
+ async function multipartUpload(ctx, bucket, key, archivePath, archiveSize) {
222
+ // Initialize multipart upload
223
+ const initResult = await ctx.client.call(["s3", "multipart", "init"], {
224
+ bucket,
225
+ key,
226
+ contentType: "application/gzip",
227
+ });
228
+ const uploadId = initResult.uploadId;
229
+ const parts = [];
230
+ const file = await open(archivePath, "r");
231
+ try {
232
+ // Upload parts
233
+ let partNumber = 1;
234
+ for (let offset = 0; offset < archiveSize; offset += PART_SIZE) {
235
+ const chunk = await readPart(file, offset, PART_SIZE);
236
+ const partResult = await ctx.client.call(["s3", "multipart", "upload"], {
237
+ bucket,
238
+ key,
239
+ uploadId,
240
+ partNumber,
241
+ body: chunk.toString("base64"),
242
+ });
243
+ parts.push({
244
+ etag: partResult.etag,
245
+ partNumber: partResult.partNumber,
246
+ });
247
+ partNumber++;
248
+ }
249
+ // Complete multipart upload
250
+ await ctx.client.call(["s3", "multipart", "complete"], {
251
+ bucket,
252
+ key,
253
+ uploadId,
254
+ parts,
255
+ });
256
+ }
257
+ catch (error) {
258
+ // Abort on failure. The error of the upload stays the error: before, a failed abort
259
+ // replaced it.
260
+ try {
261
+ await ctx.client.call(["s3", "multipart", "abort"], {
262
+ bucket,
263
+ key,
264
+ uploadId,
265
+ });
266
+ }
267
+ catch (abortError) {
268
+ throw new Error(`${messageOf(error)} (the abort of upload ${uploadId} also failed: ${messageOf(abortError)})`, { cause: error });
269
+ }
270
+ throw error;
271
+ }
272
+ finally {
273
+ await file.close();
274
+ }
275
+ }
276
+ function messageOf(error) {
277
+ return error instanceof Error ? error.message : String(error);
278
+ }
279
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/create.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * snapshot.delete procedure
3
+ *
4
+ * Delete a snapshot from S3.
5
+ */
6
+ import type { ProcedureContext } from "@procedures/client";
7
+ import type { SnapshotDeleteInput, SnapshotDeleteOutput } from "../../types.js";
8
+ /**
9
+ * Delete a snapshot from S3
10
+ */
11
+ export declare function snapshotDelete(input: SnapshotDeleteInput, ctx: ProcedureContext): Promise<SnapshotDeleteOutput>;
12
+ //# sourceMappingURL=delete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/delete.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAGhF;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,oBAAoB,CAAC,CAuB/B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * snapshot.delete procedure
3
+ *
4
+ * Delete a snapshot from S3.
5
+ */
6
+ import { listAllObjects, findSnapshotKeys } from "./s3-lookup.js";
7
+ /**
8
+ * Delete a snapshot from S3
9
+ */
10
+ export async function snapshotDelete(input, ctx) {
11
+ // Find all files for this snapshot ID (paginated listing, exact basename
12
+ // match). Exact matching is critical here: a substring match could delete a
13
+ // different snapshot whose id contains this id as a substring.
14
+ const objects = await listAllObjects(ctx, input.bucket, "snapshots/");
15
+ const snapshotKeys = findSnapshotKeys(objects, input.id);
16
+ if (snapshotKeys.length === 0) {
17
+ throw new Error(`Snapshot not found: ${input.id}`);
18
+ }
19
+ // Delete each file
20
+ for (const key of snapshotKeys) {
21
+ await ctx.client.call(["s3", "delete"], {
22
+ bucket: input.bucket,
23
+ key,
24
+ });
25
+ }
26
+ return {
27
+ deleted: true,
28
+ id: input.id,
29
+ };
30
+ }
31
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/delete.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElE;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAA0B,EAC1B,GAAqB;IAErB,yEAAyE;IACzE,4EAA4E;IAC5E,+DAA+D;IAC/D,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACtE,MAAM,YAAY,GAAG,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;IAEzD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,mBAAmB;IACnB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE;YACtC,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,GAAG;SACJ,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,OAAO,EAAE,IAAI;QACb,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * snapshot.diff procedure
3
+ *
4
+ * Compare current state against a snapshot.
5
+ */
6
+ import type { ProcedureContext } from "@procedures/client";
7
+ import type { SnapshotDiffInput, SnapshotDiffOutput } from "../../types.js";
8
+ /**
9
+ * Compare current state against a snapshot
10
+ */
11
+ export declare function snapshotDiff(input: SnapshotDiffInput, ctx: ProcedureContext): Promise<SnapshotDiffOutput>;
12
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/diff.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAGnB,MAAM,gBAAgB,CAAC;AAGxB;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,kBAAkB,CAAC,CA0H7B"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * snapshot.diff procedure
3
+ *
4
+ * Compare current state against a snapshot.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { basename } from "node:path";
8
+ import { gitText } from "./commands.js";
9
+ import { listAllObjects, findSnapshotKey } from "./s3-lookup.js";
10
+ /**
11
+ * Compare current state against a snapshot
12
+ */
13
+ export async function snapshotDiff(input, ctx) {
14
+ // Download snapshot metadata (paginated listing, exact basename match).
15
+ const objects = await listAllObjects(ctx, input.bucket, "snapshots/");
16
+ const metadataKey = findSnapshotKey(objects, input.id, ".metadata.json");
17
+ if (!metadataKey) {
18
+ throw new Error(`Snapshot not found: ${input.id}`);
19
+ }
20
+ const metadataResult = await ctx.client.call(["s3", "download"], {
21
+ bucket: input.bucket,
22
+ key: metadataKey,
23
+ encoding: "utf8",
24
+ });
25
+ const snapshotMetadata = JSON.parse(metadataResult.body);
26
+ // Compare each repository
27
+ const repositories = [];
28
+ let totalFilesChanged = 0;
29
+ let reposChanged = 0;
30
+ const pathsToCheck = input.paths || snapshotMetadata.repositories.map((r) => r.path);
31
+ for (const repoPath of pathsToCheck) {
32
+ const snapshotRepo = snapshotMetadata.repositories.find((r) => r.path === repoPath || r.name === basename(repoPath));
33
+ if (!snapshotRepo)
34
+ continue;
35
+ const diff = {
36
+ path: repoPath,
37
+ filesChanged: 0,
38
+ newStashes: 0,
39
+ };
40
+ if (!existsSync(repoPath)) {
41
+ // Repository was deleted
42
+ diff.filesChanged = -1; // Indicates missing
43
+ repositories.push(diff);
44
+ reposChanged++;
45
+ continue;
46
+ }
47
+ try {
48
+ // Get current branch (a failure is an error of this repository: the catch below)
49
+ const currentBranch = await gitText(["rev-parse", "--abbrev-ref", "HEAD"], repoPath, ctx.signal);
50
+ if (currentBranch === undefined)
51
+ throw new Error(`git cannot read the branch of ${repoPath}`);
52
+ if (currentBranch !== snapshotRepo.branch) {
53
+ diff.branchDiff = {
54
+ current: currentBranch,
55
+ snapshot: snapshotRepo.branch,
56
+ };
57
+ }
58
+ // Get current commit
59
+ const currentCommit = await gitText(["rev-parse", "HEAD"], repoPath, ctx.signal);
60
+ if (currentCommit === undefined)
61
+ throw new Error(`git cannot read the commit of ${repoPath}`);
62
+ if (currentCommit !== snapshotRepo.commit) {
63
+ diff.commitDiff = {
64
+ current: currentCommit,
65
+ snapshot: snapshotRepo.commit,
66
+ };
67
+ // Count files changed since snapshot commit.
68
+ // The commit comes from snapshot metadata downloaded from S3: accept only a hex
69
+ // object name, and pass it as an argument (no shell), so it cannot inject a command.
70
+ try {
71
+ if (!/^[0-9a-f]{7,64}$/i.test(snapshotRepo.commit)) {
72
+ throw new Error(`Not a commit hash: ${snapshotRepo.commit}`);
73
+ }
74
+ const diffStat = await gitText(["diff", "--stat", `${snapshotRepo.commit}..HEAD`], repoPath, ctx.signal);
75
+ if (diffStat === undefined)
76
+ throw new Error(`git cannot compare ${snapshotRepo.commit} with HEAD`);
77
+ // Count lines that represent files (contain |)
78
+ diff.filesChanged = diffStat.split("\n").filter((l) => l.includes("|")).length;
79
+ totalFilesChanged += diff.filesChanged;
80
+ }
81
+ catch {
82
+ // Snapshot commit may not exist locally
83
+ diff.filesChanged = -1;
84
+ }
85
+ }
86
+ // Check for new stashes
87
+ const stashResult = await ctx.client.call(["git", "stash", "list"], { cwd: repoPath });
88
+ diff.newStashes = stashResult.count - snapshotRepo.stashCount;
89
+ if (diff.branchDiff ||
90
+ diff.commitDiff ||
91
+ diff.filesChanged > 0 ||
92
+ diff.newStashes > 0) {
93
+ reposChanged++;
94
+ }
95
+ }
96
+ catch {
97
+ // Not a git repo or error
98
+ diff.filesChanged = -1;
99
+ reposChanged++;
100
+ }
101
+ repositories.push(diff);
102
+ }
103
+ return {
104
+ snapshotMetadata,
105
+ repositories,
106
+ summary: {
107
+ reposChanged,
108
+ totalFilesChanged,
109
+ isMatch: reposChanged === 0,
110
+ },
111
+ };
112
+ }
113
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Snapshot Procedures Index
3
+ */
4
+ export { snapshotCreate } from "./create.js";
5
+ export { snapshotList } from "./list.js";
6
+ export { snapshotRestore } from "./restore.js";
7
+ export { snapshotDiff } from "./diff.js";
8
+ export { snapshotDelete } from "./delete.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Snapshot Procedures Index
3
+ */
4
+ export { snapshotCreate } from "./create.js";
5
+ export { snapshotList } from "./list.js";
6
+ export { snapshotRestore } from "./restore.js";
7
+ export { snapshotDiff } from "./diff.js";
8
+ export { snapshotDelete } from "./delete.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/procedures/snapshot/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * snapshot.list procedure
3
+ *
4
+ * List available snapshots in S3 bucket.
5
+ */
6
+ import type { ProcedureContext } from "@procedures/client";
7
+ import type { SnapshotListInput, SnapshotListOutput } from "../../types.js";
8
+ /**
9
+ * List snapshots in S3 bucket
10
+ */
11
+ export declare function snapshotList(input: SnapshotListInput, ctx: ProcedureContext): Promise<SnapshotListOutput>;
12
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/procedures/snapshot/list.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAGnB,MAAM,gBAAgB,CAAC;AAGxB;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAoD7B"}