pi-microsandbox 0.2.0 → 0.3.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.
@@ -12,8 +12,8 @@ import type {
12
12
 
13
13
  // --------------------------------------------------------------- identity ----
14
14
 
15
- export const STATE_SCHEMA_VERSION = 1;
16
- export const LOCKFILE_VERSION = 1;
15
+ export const STATE_SCHEMA_VERSION = 2;
16
+ export const LOCKFILE_VERSION = 2;
17
17
 
18
18
  export function resourceId(sessionId: string): string {
19
19
  return createHash("sha256").update(sessionId).digest("hex").slice(0, 20);
@@ -24,14 +24,7 @@ export function displayId(sessionId: string): string {
24
24
  export function sandboxNameFor(sessionId: string): string {
25
25
  return `pi-msb-${resourceId(sessionId)}`;
26
26
  }
27
- export function volumeNameFor(sessionId: string): string {
28
- return `pi-msb-vol-${resourceId(sessionId)}`;
29
- }
30
-
31
27
  // ---------------------------------------------------------------- config ----
32
-
33
- export type StorageMode = "git" | "direct" | "none";
34
- export type ConfigStorageMode = "auto" | StorageMode;
35
28
  export type NetworkMode = "default" | "open" | "allowlist" | "deny";
36
29
  export type FallbackMode = "block" | "host";
37
30
  export type BootstrapTools = "auto" | boolean;
@@ -73,11 +66,6 @@ export interface Config {
73
66
  replace: boolean;
74
67
  replaceTimeoutMs: number;
75
68
  sandboxName: string | null;
76
- mode: ConfigStorageMode;
77
- cloneBranch: "current" | string;
78
- cloneDepth: number | "unlimited";
79
- shallowArchive: boolean;
80
- volumeQuotaMiB: number;
81
69
  network: NetworkConfig;
82
70
  docker: DockerConfig;
83
71
  secrets: SecretConfig[];
@@ -137,31 +125,21 @@ export const LABEL_KEYS = {
137
125
  managed: "pi-msb.managed",
138
126
  schema: "pi-msb.schema",
139
127
  session: "pi-msb.session",
140
- mode: "pi-msb.mode",
141
128
  cwd: "pi-msb.cwd",
129
+ root: "pi-msb.root",
130
+ guestRoot: "pi-msb.guest-root",
142
131
  pid: "pi-msb.pid",
143
- volume: "pi-msb.volume",
144
132
  image: "pi-msb.image",
145
133
  keep: "pi-msb.keep",
146
- seedBranch: "pi-msb.seed-branch",
147
- seedSha: "pi-msb.seed-sha",
148
134
  } as const;
149
135
 
150
136
  export interface SandboxLabelInput {
151
137
  sessionId: string;
152
- mode: StorageMode;
153
138
  cwd: string;
139
+ root: string;
140
+ guestRoot: string;
154
141
  pid: number;
155
142
  image: string;
156
- volumeName?: string;
157
- seedBranch?: string | null;
158
- seedSha?: string | null;
159
- }
160
- export interface VolumeLabelInput {
161
- sessionId: string;
162
- cwd: string;
163
- seedBranch?: string | null;
164
- seedSha?: string | null;
165
143
  }
166
144
  export interface ManagedSandboxRecord {
167
145
  name: string;
@@ -171,29 +149,18 @@ export interface ManagedSandboxRecord {
171
149
  }
172
150
  export interface ValidatedManagedSandbox extends ManagedSandboxRecord {
173
151
  sessionId: string;
174
- mode: StorageMode;
175
152
  cwd: string;
176
- volumeName?: string;
177
- }
178
- export interface VolumeRecord {
179
- name: string;
180
- hostPath: string;
181
- labels: Record<string, string>;
182
- kind?: string;
183
- usedBytes?: number;
184
- createdAt?: number;
153
+ root: string;
154
+ guestRoot: string;
185
155
  }
186
156
  export interface PersistedSandboxState {
187
157
  version: typeof STATE_SCHEMA_VERSION;
188
158
  sessionId: string;
189
159
  sandboxName: string;
190
- mode: StorageMode;
191
160
  cwd: string;
161
+ root: string;
162
+ guestRoot: string;
192
163
  image: string;
193
- volumeName?: string;
194
- volumeHostPath?: string;
195
- seedBranch?: string | null;
196
- seedSha?: string | null;
197
164
  enabled: boolean;
198
165
  createdAt: number;
199
166
  }
@@ -204,9 +171,8 @@ export interface LockInfo {
204
171
  version: typeof LOCKFILE_VERSION;
205
172
  sessionId: string;
206
173
  sandboxName: string;
207
- volumeName?: string;
208
- mode: StorageMode;
209
174
  cwd: string;
175
+ root: string;
210
176
  pid: number;
211
177
  createdAt: number;
212
178
  }
@@ -218,52 +184,18 @@ export interface LocksPort {
218
184
  tryAcquire(sessionId: string): Promise<LockHandle | null>;
219
185
  }
220
186
 
221
- // ------------------------------------------------------------- git/storage ----
187
+ // -------------------------------------------------------------- workspace ----
222
188
 
223
- export interface GitRepoInfo {
224
- isGitRepo: boolean;
225
- /** Canonical host source for direct binds; guest paths remain lexical. */
226
- hostCwd?: string;
227
- /** Canonical host source used only for Git reads and seed bundle creation. */
228
- repoRoot: string | null;
229
- /** Lexical guest namespace where the retained volume must be mounted. */
230
- guestRepoRoot?: string | null;
231
- branch: string | null;
232
- headSha: string | null;
233
- unborn: boolean;
234
- isLinkedWorktree: boolean;
235
- }
236
- export interface GitSeedBundle {
237
- hostPath: string;
238
- branch: string | null;
239
- headSha: string;
240
- cleanup(): Promise<void>;
241
- }
242
- export interface GitVolumePlan {
243
- kind: "git-volume";
244
- sessionId: string;
245
- volumeName: string;
246
- volumeQuotaMiB: number;
247
- repoRoot: string;
248
- mountGuestPath: string;
249
- workdir: string;
250
- branch: string | null;
251
- headSha: string | null;
252
- unborn: boolean;
253
- depth: number | "unlimited";
254
- seedRequired: boolean;
255
- }
256
- export type StoragePlan =
257
- | GitVolumePlan
258
- | { kind: "direct-mount"; hostPath: string; guestPath: string; workdir: string }
259
- | { kind: "none"; guestPath: string; workdir: string };
260
- export interface PreparedStorage {
261
- plan: StoragePlan;
262
- volume?: VolumeRecord;
263
- bundle?: GitSeedBundle | null;
264
- createdVolume: boolean;
265
- }
266
- export interface SeedResult { headSha: string | null }
189
+ /** One path-preserving read/write bind used by every sandbox. */
190
+ export interface Workspace {
191
+ /** Canonical host directory supplied to the bind mount. */
192
+ hostRoot: string;
193
+ /** Lexical absolute path used as the guest mount point. */
194
+ guestRoot: string;
195
+ /** Original lexical absolute cwd retained as the sandbox workdir. */
196
+ cwd: string;
197
+ fromGit: boolean;
198
+ }
267
199
 
268
200
  // ------------------------------------------------------------- transport ----
269
201
 
@@ -306,8 +238,6 @@ export interface SandboxTransport {
306
238
  exists(path: string): Promise<boolean>;
307
239
  stat(path: string): Promise<StatResult>;
308
240
  list(path: string): Promise<FsEntry[]>;
309
- copyFromHost(hostPath: string, guestPath: string): Promise<void>;
310
- copyToHost(guestPath: string, hostPath: string): Promise<void>;
311
241
  exec(command: string, args: string[], options?: ExecOptions): Promise<TransportExecResult>;
312
242
  execStream(command: string, args: string[], options?: ExecStreamOptions): Promise<{ exitCode: number }>;
313
243
  dispose(): Promise<void>;
@@ -382,14 +312,10 @@ export interface RuntimePreparation {
382
312
  export interface SandboxInfo {
383
313
  name: string;
384
314
  displayId: string;
385
- mode: StorageMode;
386
315
  image: string;
387
316
  pid: number;
388
317
  cwd: string;
389
- volumeName?: string;
390
- volumeHostPath?: string;
391
- seedBranch?: string | null;
392
- seedSha?: string | null;
318
+ root: string;
393
319
  createdAt: number;
394
320
  docker: DockerCapabilityStatus;
395
321
  }
@@ -401,6 +327,7 @@ export interface RuntimeState {
401
327
  export interface BootRequest {
402
328
  sessionId: string;
403
329
  cwd: string;
330
+ workspace: Workspace;
404
331
  config: Config;
405
332
  restored: PersistedSandboxState | null;
406
333
  }
@@ -429,22 +356,11 @@ export type ExecFn = (
429
356
 
430
357
  // ------------------------------------------------------- command/control ----
431
358
 
432
- export interface ExportResult { source: string; destination: string }
433
359
  export interface MsbControl {
434
360
  getState(): RuntimeState;
435
361
  setEnabled(enabled: boolean): Promise<void>;
436
362
  reload(): Promise<void>;
437
363
  pruneNow(): Promise<PruneReport>;
438
- listVolumes(): Promise<VolumeRecord[]>;
439
- describeVolume(name: string): Promise<{
440
- volume: VolumeRecord;
441
- branch?: string;
442
- lastCommit?: string;
443
- dirtyCount?: number;
444
- mounted: boolean;
445
- }>;
446
- removeVolume(name: string): Promise<void>;
447
- exportPaths(paths: string[], destination?: string): Promise<ExportResult[]>;
448
364
  getLogs(tailLines?: number): Promise<string>;
449
365
  getEffectiveConfig(): ResolvedConfig;
450
366
  getEffectiveConfigToml(): string;
@@ -0,0 +1,126 @@
1
+ import { execFile } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ import type { ExecFn, Workspace } from "./types.ts";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ interface ExecFileFailure {
11
+ code?: number | string;
12
+ killed?: boolean;
13
+ signal?: string | null;
14
+ stdout?: string | Buffer;
15
+ stderr?: string | Buffer;
16
+ message?: string;
17
+ }
18
+
19
+ const defaultExec: ExecFn = async (command, args, options = {}) => {
20
+ try {
21
+ const result = await execFileAsync(command, args, {
22
+ cwd: options.cwd,
23
+ env: { ...process.env, ...options.env, LC_ALL: "C" },
24
+ timeout: options.timeout === undefined ? undefined : options.timeout * 1000,
25
+ maxBuffer: 1024 * 1024,
26
+ encoding: "utf8",
27
+ });
28
+ return { stdout: String(result.stdout), stderr: String(result.stderr), code: 0 };
29
+ } catch (error: unknown) {
30
+ const failure = error as ExecFileFailure;
31
+ return {
32
+ stdout: String(failure.stdout ?? ""),
33
+ stderr: String(failure.stderr ?? failure.message ?? ""),
34
+ code: typeof failure.code === "number" ? failure.code : 127,
35
+ killed: Boolean(failure.killed || failure.signal),
36
+ };
37
+ }
38
+ };
39
+
40
+ async function lexicalRoot(cwd: string, canonicalRoot: string): Promise<string | null> {
41
+ let candidate = cwd;
42
+ while (true) {
43
+ let canonicalCandidate: string;
44
+ try {
45
+ canonicalCandidate = await realpath(candidate);
46
+ } catch (error) {
47
+ throw new Error(`workspace path cannot be canonicalized: ${candidate}`, { cause: error });
48
+ }
49
+ if (canonicalCandidate === canonicalRoot) return candidate;
50
+ const parent = dirname(candidate);
51
+ if (parent === candidate) return null;
52
+ candidate = parent;
53
+ }
54
+ }
55
+
56
+ function isNotRepository(code: number, stderr: string): boolean {
57
+ return code === 128 && /(?:^|\n)fatal: (?:not a git (?:repository|work tree)|this operation must be run in a work tree)(?:\s|$)/i.test(stderr);
58
+ }
59
+
60
+ const PROTECTED_GUEST_PATHS = [
61
+ "/bin",
62
+ "/dev",
63
+ "/etc/ld.so.cache",
64
+ "/etc/ld.so.preload",
65
+ "/lib",
66
+ "/lib64",
67
+ "/proc",
68
+ "/sbin",
69
+ "/sys",
70
+ "/usr",
71
+ "/run",
72
+ "/var/run",
73
+ "/var/lib/docker",
74
+ ] as const;
75
+
76
+ function containsPath(parent: string, child: string): boolean {
77
+ const value = relative(parent, child);
78
+ return value === "" || (value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value));
79
+ }
80
+
81
+ function assertSafeGuestRoot(root: string): void {
82
+ if (PROTECTED_GUEST_PATHS.some((path) => containsPath(root, path) || containsPath(path, root))) {
83
+ throw new Error(`workspace root cannot safely be mounted at the guest path: ${root}`);
84
+ }
85
+ }
86
+
87
+ /** Discover the single path-preserving project bind for a configure/boot cycle. */
88
+ export async function discoverWorkspace(cwd: string, exec: ExecFn = defaultExec): Promise<Workspace> {
89
+ const lexicalCwd = resolve(cwd);
90
+ let canonicalCwd: string;
91
+ try {
92
+ canonicalCwd = await realpath(lexicalCwd);
93
+ } catch (error) {
94
+ throw new Error(`workspace cwd cannot be canonicalized: ${lexicalCwd}`, { cause: error });
95
+ }
96
+
97
+ const result = await exec("git", ["-C", lexicalCwd, "rev-parse", "--show-toplevel"]);
98
+ if (result.killed) throw new Error("Git workspace discovery was interrupted");
99
+ if (result.code !== 0) {
100
+ if (isNotRepository(result.code, result.stderr)) {
101
+ assertSafeGuestRoot(lexicalCwd);
102
+ return { hostRoot: canonicalCwd, guestRoot: lexicalCwd, cwd: lexicalCwd, fromGit: false };
103
+ }
104
+ const detail = result.stderr.trim() || `git exited with code ${result.code}`;
105
+ throw new Error(`Git workspace discovery failed: ${detail}`);
106
+ }
107
+
108
+ const output = result.stdout.replace(/\r?\n$/, "");
109
+ if (!output || output.includes("\0") || output.includes("\n") || output.includes("\r") || !isAbsolute(output)) {
110
+ throw new Error("Git workspace discovery returned an invalid repository root");
111
+ }
112
+
113
+ let hostRoot: string;
114
+ try {
115
+ hostRoot = await realpath(output);
116
+ } catch (error) {
117
+ throw new Error(`Git workspace root cannot be canonicalized: ${output}`, { cause: error });
118
+ }
119
+ const guestRoot = await lexicalRoot(lexicalCwd, hostRoot);
120
+ if (guestRoot === null) {
121
+ throw new Error("Git workspace root cannot preserve the cwd guest path; unsupported symlink topology");
122
+ }
123
+ assertSafeGuestRoot(guestRoot);
124
+
125
+ return { hostRoot, guestRoot, cwd: lexicalCwd, fromGit: true };
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-microsandbox",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Microsandbox-backed isolation for Pi coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,256 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { chmod, mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { promisify } from "node:util";
7
-
8
- import type { ExecFn, GitRepoInfo, GitSeedBundle } from "./types.ts";
9
-
10
- const execFileAsync = promisify(execFile);
11
- const SEED_REF = "refs/pi-msb/seed";
12
-
13
- interface ExecFileFailure {
14
- code?: number | string;
15
- killed?: boolean;
16
- signal?: string | null;
17
- stdout?: string | Buffer;
18
- stderr?: string | Buffer;
19
- message?: string;
20
- }
21
-
22
- /** Execute a program without giving it a shell or interpolating its arguments. */
23
- const defaultExec: ExecFn = async (command, args, options = {}) => {
24
- try {
25
- const result = await execFileAsync(command, args, {
26
- cwd: options.cwd,
27
- env: options.env,
28
- timeout: options.timeout === undefined ? undefined : options.timeout * 1000,
29
- maxBuffer: 16 * 1024 * 1024,
30
- encoding: "utf8",
31
- });
32
- return {
33
- stdout: String(result.stdout),
34
- stderr: String(result.stderr),
35
- code: 0,
36
- };
37
- } catch (error: unknown) {
38
- const failure = error as ExecFileFailure;
39
- const numericCode = typeof failure.code === "number" ? failure.code : 127;
40
- return {
41
- stdout: String(failure.stdout ?? ""),
42
- stderr: String(failure.stderr ?? failure.message ?? ""),
43
- code: numericCode,
44
- killed: Boolean(failure.killed || failure.signal),
45
- };
46
- }
47
- };
48
-
49
- const emptyRepo = (hostCwd?: string): GitRepoInfo => ({
50
- isGitRepo: false,
51
- ...(hostCwd ? { hostCwd } : {}),
52
- repoRoot: null,
53
- branch: null,
54
- headSha: null,
55
- unborn: false,
56
- isLinkedWorktree: false,
57
- });
58
-
59
- function output(result: Awaited<ReturnType<ExecFn>>): string {
60
- return result.stdout.trim();
61
- }
62
-
63
- function failureMessage(context: string, result: Awaited<ReturnType<ExecFn>>): Error {
64
- const detail = result.stderr.trim() || `git exited with code ${result.code}`;
65
- return new Error(`${context}: ${detail}`);
66
- }
67
-
68
- async function canonicalPath(value: string): Promise<string> {
69
- try {
70
- return await realpath(value);
71
- } catch {
72
- return resolve(value);
73
- }
74
- }
75
-
76
- function pathFromGitResult(value: string, base: string): string {
77
- return isAbsolute(value) ? value : resolve(base, value);
78
- }
79
-
80
- async function lexicalRepoRoot(requestedCwd: string, canonicalRepoRoot: string): Promise<string | null> {
81
- let candidate = requestedCwd;
82
- while (true) {
83
- if (await canonicalPath(candidate) === canonicalRepoRoot) return candidate;
84
- const parent = dirname(candidate);
85
- if (parent === candidate) return null;
86
- candidate = parent;
87
- }
88
- }
89
-
90
- /**
91
- * Return the immutable source state of cwd. A directory outside Git is an ordinary
92
- * result rather than an error, which lets storage mode `auto` remain a direct alias.
93
- */
94
- export async function detectGitRepo(cwd: string, exec: ExecFn = defaultExec): Promise<GitRepoInfo> {
95
- const requestedCwd = resolve(cwd);
96
- const hostCwd = await canonicalPath(requestedCwd);
97
- const topLevel = await exec("git", ["-C", requestedCwd, "rev-parse", "--show-toplevel"]);
98
- if (topLevel.code !== 0 || topLevel.killed || !output(topLevel)) return emptyRepo(hostCwd);
99
-
100
- const repoRoot = await canonicalPath(output(topLevel));
101
- const guestRepoRoot = await lexicalRepoRoot(requestedCwd, repoRoot);
102
- const branchResult = await exec("git", [
103
- "-C",
104
- repoRoot,
105
- "symbolic-ref",
106
- "--quiet",
107
- "--short",
108
- "HEAD",
109
- ]);
110
- const branch = branchResult.code === 0 && !branchResult.killed && output(branchResult)
111
- ? output(branchResult)
112
- : null;
113
-
114
- const headResult = await exec("git", ["-C", repoRoot, "rev-parse", "--verify", "HEAD"]);
115
- const headSha = headResult.code === 0 && !headResult.killed && output(headResult)
116
- ? output(headResult)
117
- : null;
118
-
119
- // In a linked worktree --git-dir points at .git/worktrees/<name>, while
120
- // --git-common-dir points at the shared .git directory.
121
- const gitDirResult = await exec("git", ["-C", repoRoot, "rev-parse", "--git-dir"]);
122
- const commonDirResult = await exec("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"]);
123
- let isLinkedWorktree = false;
124
- if (gitDirResult.code === 0 && commonDirResult.code === 0) {
125
- const gitDir = await canonicalPath(pathFromGitResult(output(gitDirResult), repoRoot));
126
- const commonDir = await canonicalPath(pathFromGitResult(output(commonDirResult), repoRoot));
127
- isLinkedWorktree = gitDir !== commonDir;
128
- }
129
-
130
- return {
131
- isGitRepo: true,
132
- hostCwd,
133
- repoRoot,
134
- guestRepoRoot,
135
- branch,
136
- headSha,
137
- unborn: headSha === null && branch !== null,
138
- isLinkedWorktree,
139
- };
140
- }
141
-
142
- function bundleFailure(context: string, result: Awaited<ReturnType<ExecFn>>): never {
143
- throw failureMessage(context, result);
144
- }
145
-
146
- async function checkedGit(
147
- exec: ExecFn,
148
- args: string[],
149
- context: string,
150
- ): Promise<Awaited<ReturnType<ExecFn>>> {
151
- const result = await exec("git", args);
152
- if (result.code !== 0 || result.killed) bundleFailure(context, result);
153
- return result;
154
- }
155
-
156
- /**
157
- * Make a disposable bundle from committed objects only. The temporary bare clone
158
- * deliberately has no worktree, so ignored, modified, and untracked files cannot
159
- * enter the seed.
160
- */
161
- export async function createSeedBundle(
162
- info: GitRepoInfo,
163
- options: {
164
- branch: "current" | string;
165
- depth: number | "unlimited";
166
- tempRoot?: string;
167
- exec?: ExecFn;
168
- },
169
- ): Promise<GitSeedBundle | null> {
170
- if (!info.isGitRepo || info.unborn || !info.repoRoot || !info.headSha) return null;
171
- if (options.depth !== "unlimited" && (!Number.isInteger(options.depth) || options.depth < 1)) {
172
- throw new Error(`invalid Git bundle depth: ${String(options.depth)}`);
173
- }
174
-
175
- const exec = options.exec ?? defaultExec;
176
- const parent = resolve(options.tempRoot ?? tmpdir());
177
- await mkdir(parent, { recursive: true, mode: 0o700 });
178
- const tempDir = await mkdtemp(join(parent, "pi-msb-git-"));
179
- await chmod(tempDir, 0o700);
180
- let cleaned = false;
181
- const cleanup = async (): Promise<void> => {
182
- if (cleaned) return;
183
- cleaned = true;
184
- await rm(tempDir, { recursive: true, force: true });
185
- };
186
-
187
- try {
188
- const bareName = "source.git";
189
- const barePath = join(tempDir, bareName);
190
- const bundlePath = join(tempDir, "seed.bundle");
191
- const source = fileUrl(info.repoRoot);
192
- const depthArgs = options.depth === "unlimited" ? [] : ["--depth", String(options.depth)];
193
-
194
- // A file:// URL forces Git's local transport while keeping the source repo
195
- // read-only. --bare means no checkout, filters, or worktree are involved.
196
- await checkedGit(
197
- exec,
198
- ["-C", tempDir, "clone", "--bare", "--no-tags", ...depthArgs, source, bareName],
199
- "creating temporary bare clone",
200
- );
201
-
202
- const objectCheck = await exec("git", ["-C", barePath, "cat-file", "-e", `${info.headSha}^{commit}`]);
203
- if (objectCheck.code !== 0 || objectCheck.killed) {
204
- // A branch may have moved between detection and clone. Fetch the captured
205
- // object explicitly; never fall back to whatever HEAD the clone obtained.
206
- await checkedGit(
207
- exec,
208
- ["-C", barePath, "fetch", "--no-tags", ...depthArgs, source, info.headSha],
209
- `fetching captured HEAD ${info.headSha}`,
210
- );
211
- }
212
-
213
- const verifiedObject = await exec("git", ["-C", barePath, "cat-file", "-e", `${info.headSha}^{commit}`]);
214
- if (verifiedObject.code !== 0 || verifiedObject.killed) {
215
- bundleFailure(`captured HEAD ${info.headSha} is not present in temporary clone`, verifiedObject);
216
- }
217
-
218
- await checkedGit(
219
- exec,
220
- ["-C", barePath, "update-ref", SEED_REF, info.headSha],
221
- "updating temporary seed ref",
222
- );
223
- await checkedGit(
224
- exec,
225
- ["-C", barePath, "bundle", "create", bundlePath, SEED_REF],
226
- "creating Git bundle",
227
- );
228
- await checkedGit(exec, ["-C", barePath, "bundle", "verify", bundlePath], "verifying Git bundle");
229
- const heads = await checkedGit(
230
- exec,
231
- ["-C", barePath, "bundle", "list-heads", bundlePath],
232
- "listing Git bundle heads",
233
- );
234
- const expectedHead = heads.stdout
235
- .split(/\r?\n/)
236
- .map((line) => line.trim().split(/\s+/))
237
- .find(([sha, ref]) => sha === info.headSha && ref === SEED_REF);
238
- if (!expectedHead) {
239
- throw new Error(`Git bundle did not contain captured HEAD ${info.headSha} at ${SEED_REF}`);
240
- }
241
-
242
- return {
243
- hostPath: bundlePath,
244
- branch: options.branch === "current" ? info.branch : options.branch,
245
- headSha: info.headSha,
246
- cleanup,
247
- };
248
- } catch (error: unknown) {
249
- await cleanup().catch(() => undefined);
250
- throw error;
251
- }
252
- }
253
-
254
- export function fileUrl(path: string): string {
255
- return pathToFileURL(path).href;
256
- }