pi-microsandbox 0.1.1 → 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,20 +24,18 @@ 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;
38
31
  export type PullPolicy = "always" | "if-missing" | "never";
39
32
  export type MountType = "dir" | "file" | "named" | "tmpfs";
33
+ export type DockerMode = "auto" | "require" | "disabled";
40
34
 
35
+ export interface DockerConfig {
36
+ mode: DockerMode;
37
+ startupTimeoutMs: number;
38
+ }
41
39
  export interface NetworkConfig {
42
40
  mode: NetworkMode;
43
41
  allowHosts: string[];
@@ -68,12 +66,8 @@ export interface Config {
68
66
  replace: boolean;
69
67
  replaceTimeoutMs: number;
70
68
  sandboxName: string | null;
71
- mode: ConfigStorageMode;
72
- cloneBranch: "current" | string;
73
- cloneDepth: number | "unlimited";
74
- shallowArchive: boolean;
75
- volumeQuotaMiB: number;
76
69
  network: NetworkConfig;
70
+ docker: DockerConfig;
77
71
  secrets: SecretConfig[];
78
72
  mounts: MountConfig[];
79
73
  blockThirdParty: boolean;
@@ -131,31 +125,21 @@ export const LABEL_KEYS = {
131
125
  managed: "pi-msb.managed",
132
126
  schema: "pi-msb.schema",
133
127
  session: "pi-msb.session",
134
- mode: "pi-msb.mode",
135
128
  cwd: "pi-msb.cwd",
129
+ root: "pi-msb.root",
130
+ guestRoot: "pi-msb.guest-root",
136
131
  pid: "pi-msb.pid",
137
- volume: "pi-msb.volume",
138
132
  image: "pi-msb.image",
139
133
  keep: "pi-msb.keep",
140
- seedBranch: "pi-msb.seed-branch",
141
- seedSha: "pi-msb.seed-sha",
142
134
  } as const;
143
135
 
144
136
  export interface SandboxLabelInput {
145
137
  sessionId: string;
146
- mode: StorageMode;
147
138
  cwd: string;
139
+ root: string;
140
+ guestRoot: string;
148
141
  pid: number;
149
142
  image: string;
150
- volumeName?: string;
151
- seedBranch?: string | null;
152
- seedSha?: string | null;
153
- }
154
- export interface VolumeLabelInput {
155
- sessionId: string;
156
- cwd: string;
157
- seedBranch?: string | null;
158
- seedSha?: string | null;
159
143
  }
160
144
  export interface ManagedSandboxRecord {
161
145
  name: string;
@@ -165,29 +149,18 @@ export interface ManagedSandboxRecord {
165
149
  }
166
150
  export interface ValidatedManagedSandbox extends ManagedSandboxRecord {
167
151
  sessionId: string;
168
- mode: StorageMode;
169
152
  cwd: string;
170
- volumeName?: string;
171
- }
172
- export interface VolumeRecord {
173
- name: string;
174
- hostPath: string;
175
- labels: Record<string, string>;
176
- kind?: string;
177
- usedBytes?: number;
178
- createdAt?: number;
153
+ root: string;
154
+ guestRoot: string;
179
155
  }
180
156
  export interface PersistedSandboxState {
181
157
  version: typeof STATE_SCHEMA_VERSION;
182
158
  sessionId: string;
183
159
  sandboxName: string;
184
- mode: StorageMode;
185
160
  cwd: string;
161
+ root: string;
162
+ guestRoot: string;
186
163
  image: string;
187
- volumeName?: string;
188
- volumeHostPath?: string;
189
- seedBranch?: string | null;
190
- seedSha?: string | null;
191
164
  enabled: boolean;
192
165
  createdAt: number;
193
166
  }
@@ -198,9 +171,8 @@ export interface LockInfo {
198
171
  version: typeof LOCKFILE_VERSION;
199
172
  sessionId: string;
200
173
  sandboxName: string;
201
- volumeName?: string;
202
- mode: StorageMode;
203
174
  cwd: string;
175
+ root: string;
204
176
  pid: number;
205
177
  createdAt: number;
206
178
  }
@@ -212,52 +184,18 @@ export interface LocksPort {
212
184
  tryAcquire(sessionId: string): Promise<LockHandle | null>;
213
185
  }
214
186
 
215
- // ------------------------------------------------------------- git/storage ----
187
+ // -------------------------------------------------------------- workspace ----
216
188
 
217
- export interface GitRepoInfo {
218
- isGitRepo: boolean;
219
- /** Canonical host source for direct binds; guest paths remain lexical. */
220
- hostCwd?: string;
221
- /** Canonical host source used only for Git reads and seed bundle creation. */
222
- repoRoot: string | null;
223
- /** Lexical guest namespace where the retained volume must be mounted. */
224
- guestRepoRoot?: string | null;
225
- branch: string | null;
226
- headSha: string | null;
227
- unborn: boolean;
228
- isLinkedWorktree: boolean;
229
- }
230
- export interface GitSeedBundle {
231
- hostPath: string;
232
- branch: string | null;
233
- headSha: string;
234
- cleanup(): Promise<void>;
235
- }
236
- export interface GitVolumePlan {
237
- kind: "git-volume";
238
- sessionId: string;
239
- volumeName: string;
240
- volumeQuotaMiB: number;
241
- repoRoot: string;
242
- mountGuestPath: string;
243
- workdir: string;
244
- branch: string | null;
245
- headSha: string | null;
246
- unborn: boolean;
247
- depth: number | "unlimited";
248
- seedRequired: boolean;
249
- }
250
- export type StoragePlan =
251
- | GitVolumePlan
252
- | { kind: "direct-mount"; hostPath: string; guestPath: string; workdir: string }
253
- | { kind: "none"; guestPath: string; workdir: string };
254
- export interface PreparedStorage {
255
- plan: StoragePlan;
256
- volume?: VolumeRecord;
257
- bundle?: GitSeedBundle | null;
258
- createdVolume: boolean;
259
- }
260
- 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
+ }
261
199
 
262
200
  // ------------------------------------------------------------- transport ----
263
201
 
@@ -300,8 +238,6 @@ export interface SandboxTransport {
300
238
  exists(path: string): Promise<boolean>;
301
239
  stat(path: string): Promise<StatResult>;
302
240
  list(path: string): Promise<FsEntry[]>;
303
- copyFromHost(hostPath: string, guestPath: string): Promise<void>;
304
- copyToHost(guestPath: string, hostPath: string): Promise<void>;
305
241
  exec(command: string, args: string[], options?: ExecOptions): Promise<TransportExecResult>;
306
242
  execStream(command: string, args: string[], options?: ExecStreamOptions): Promise<{ exitCode: number }>;
307
243
  dispose(): Promise<void>;
@@ -362,18 +298,26 @@ export type RuntimeStatus =
362
298
  | "off"
363
299
  | "host-fallback"
364
300
  | "disabled";
301
+ export type DockerReadiness = "ready" | "missing" | "unavailable" | "disabled";
302
+ export interface DockerCapabilityStatus {
303
+ mode: DockerMode;
304
+ readiness: DockerReadiness;
305
+ version?: string;
306
+ storageDriver?: string;
307
+ reason?: string;
308
+ }
309
+ export interface RuntimePreparation {
310
+ docker: DockerCapabilityStatus;
311
+ }
365
312
  export interface SandboxInfo {
366
313
  name: string;
367
314
  displayId: string;
368
- mode: StorageMode;
369
315
  image: string;
370
316
  pid: number;
371
317
  cwd: string;
372
- volumeName?: string;
373
- volumeHostPath?: string;
374
- seedBranch?: string | null;
375
- seedSha?: string | null;
318
+ root: string;
376
319
  createdAt: number;
320
+ docker: DockerCapabilityStatus;
377
321
  }
378
322
  export interface RuntimeState {
379
323
  status: RuntimeStatus;
@@ -383,6 +327,7 @@ export interface RuntimeState {
383
327
  export interface BootRequest {
384
328
  sessionId: string;
385
329
  cwd: string;
330
+ workspace: Workspace;
386
331
  config: Config;
387
332
  restored: PersistedSandboxState | null;
388
333
  }
@@ -411,22 +356,11 @@ export type ExecFn = (
411
356
 
412
357
  // ------------------------------------------------------- command/control ----
413
358
 
414
- export interface ExportResult { source: string; destination: string }
415
359
  export interface MsbControl {
416
360
  getState(): RuntimeState;
417
361
  setEnabled(enabled: boolean): Promise<void>;
418
362
  reload(): Promise<void>;
419
363
  pruneNow(): Promise<PruneReport>;
420
- listVolumes(): Promise<VolumeRecord[]>;
421
- describeVolume(name: string): Promise<{
422
- volume: VolumeRecord;
423
- branch?: string;
424
- lastCommit?: string;
425
- dirtyCount?: number;
426
- mounted: boolean;
427
- }>;
428
- removeVolume(name: string): Promise<void>;
429
- exportPaths(paths: string[], destination?: string): Promise<ExportResult[]>;
430
364
  getLogs(tailLines?: number): Promise<string>;
431
365
  getEffectiveConfig(): ResolvedConfig;
432
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.1.1",
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
- }