@sealant/sdk 0.11.0 → 0.12.1
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.
|
@@ -14,6 +14,7 @@ import { randomUUID } from "node:crypto";
|
|
|
14
14
|
import { SealantError } from "../errors.js";
|
|
15
15
|
import { mapWorkspaceCredentials } from "./credentials.js";
|
|
16
16
|
import { parseTtlSeconds } from "./duration.js";
|
|
17
|
+
import { discoverLinkedWorktreeMetadataMount } from "./linked-worktree.js";
|
|
17
18
|
const sanitizeRepoSlug = (value) => {
|
|
18
19
|
const slug = value
|
|
19
20
|
.toLowerCase()
|
|
@@ -43,6 +44,23 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
43
44
|
.filter((s) => s.length > 0)
|
|
44
45
|
.pop() ?? sourceName;
|
|
45
46
|
const credentials = mapWorkspaceCredentials(options.credentials);
|
|
47
|
+
const linkedWorktreeMount = options.source === undefined ? null : discoverLinkedWorktreeMetadataMount(options.source.path);
|
|
48
|
+
const explicitMounts = options.mounts ?? [];
|
|
49
|
+
const existingMetadataMount = linkedWorktreeMount === null
|
|
50
|
+
? undefined
|
|
51
|
+
: explicitMounts.find((mount) => mount.mountPath === linkedWorktreeMount.mountPath);
|
|
52
|
+
if (linkedWorktreeMount !== null &&
|
|
53
|
+
existingMetadataMount !== undefined &&
|
|
54
|
+
(existingMetadataMount.hostPath !== linkedWorktreeMount.hostPath ||
|
|
55
|
+
existingMetadataMount.readOnly !== false)) {
|
|
56
|
+
throw new SealantError(`Mount path ${linkedWorktreeMount.mountPath} is required for writable linked-worktree Git metadata and conflicts with an explicit mount.`, { code: "linked_worktree_mount_conflict" });
|
|
57
|
+
}
|
|
58
|
+
const mounts = [
|
|
59
|
+
...explicitMounts,
|
|
60
|
+
...(linkedWorktreeMount === null || existingMetadataMount !== undefined
|
|
61
|
+
? []
|
|
62
|
+
: [linkedWorktreeMount]),
|
|
63
|
+
];
|
|
46
64
|
const spec = {
|
|
47
65
|
version: "1",
|
|
48
66
|
sources: {
|
|
@@ -55,10 +73,10 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
55
73
|
...(options.ref === undefined ? {} : { ref: options.ref }),
|
|
56
74
|
}
|
|
57
75
|
: { kind: "mount", hostPath: options.source?.path },
|
|
58
|
-
...(
|
|
76
|
+
...(mounts.length === 0
|
|
59
77
|
? {}
|
|
60
78
|
: {
|
|
61
|
-
mounts:
|
|
79
|
+
mounts: mounts.map((mount) => ({
|
|
62
80
|
hostPath: mount.hostPath,
|
|
63
81
|
mountPath: mount.mountPath,
|
|
64
82
|
// Omitted = the blueprint's default (read-only). Only an explicit choice is sent.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LinkedWorktreeMetadataMount {
|
|
2
|
+
readonly hostPath: string;
|
|
3
|
+
readonly mountPath: string;
|
|
4
|
+
readonly readOnly: false;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A linked Git worktree carries a `.git` POINTER FILE rather than its repository metadata. Docker
|
|
8
|
+
* mounting only the worktree preserves that file but not the absolute host path it names. Discover
|
|
9
|
+
* the shared Git directory so the workspace creator can bind it at the same absolute path inside
|
|
10
|
+
* the container; the pointer then works unchanged and no repository data is copied.
|
|
11
|
+
*/
|
|
12
|
+
export declare const discoverLinkedWorktreeMetadataMount: (sourcePath: string) => LinkedWorktreeMetadataMount | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { SealantError } from "../errors.js";
|
|
4
|
+
const isMissingPath = (cause) => typeof cause === "object" &&
|
|
5
|
+
cause !== null &&
|
|
6
|
+
"code" in cause &&
|
|
7
|
+
(cause.code === "ENOENT" || cause.code === "ENOTDIR");
|
|
8
|
+
const readIfPresent = (file) => {
|
|
9
|
+
try {
|
|
10
|
+
return readFileSync(file, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch (cause) {
|
|
13
|
+
if (isMissingPath(cause))
|
|
14
|
+
return null;
|
|
15
|
+
throw new SealantError(`Could not inspect mounted workspace Git metadata at ${file}.`, {
|
|
16
|
+
code: "mount_source_git_inspection_failed",
|
|
17
|
+
cause,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const requireDirectory = (directory) => {
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(directory).isDirectory())
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
throw new SealantError(`Mounted workspace Git metadata points to an unreadable directory: ${directory}.`, { code: "mount_source_git_metadata_invalid", cause });
|
|
28
|
+
}
|
|
29
|
+
throw new SealantError(`Mounted workspace Git metadata points to a non-directory path: ${directory}.`, { code: "mount_source_git_metadata_invalid" });
|
|
30
|
+
};
|
|
31
|
+
const isWithin = (child, parent) => {
|
|
32
|
+
const fromParent = relative(parent, child);
|
|
33
|
+
return fromParent === "" || (!fromParent.startsWith("..") && !isAbsolute(fromParent));
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A linked Git worktree carries a `.git` POINTER FILE rather than its repository metadata. Docker
|
|
37
|
+
* mounting only the worktree preserves that file but not the absolute host path it names. Discover
|
|
38
|
+
* the shared Git directory so the workspace creator can bind it at the same absolute path inside
|
|
39
|
+
* the container; the pointer then works unchanged and no repository data is copied.
|
|
40
|
+
*/
|
|
41
|
+
export const discoverLinkedWorktreeMetadataMount = (sourcePath) => {
|
|
42
|
+
const dotGit = join(sourcePath, ".git");
|
|
43
|
+
let dotGitStat;
|
|
44
|
+
try {
|
|
45
|
+
dotGitStat = statSync(dotGit);
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
if (isMissingPath(cause))
|
|
49
|
+
return null;
|
|
50
|
+
throw new SealantError(`Could not inspect mounted workspace Git entry at ${dotGit}.`, {
|
|
51
|
+
code: "mount_source_git_inspection_failed",
|
|
52
|
+
cause,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// A normal repository already carries its complete `.git` directory inside the primary mount.
|
|
56
|
+
if (!dotGitStat.isFile())
|
|
57
|
+
return null;
|
|
58
|
+
const pointer = readIfPresent(dotGit);
|
|
59
|
+
const gitDirValue = pointer?.match(/^gitdir:\s*(.+)\s*$/m)?.[1]?.trim();
|
|
60
|
+
// A non-Git folder is still a valid mount source. Only Git's documented pointer shape opts in.
|
|
61
|
+
if (gitDirValue === undefined || gitDirValue === "")
|
|
62
|
+
return null;
|
|
63
|
+
const gitDir = resolve(dirname(dotGit), gitDirValue);
|
|
64
|
+
requireDirectory(gitDir);
|
|
65
|
+
const commonDirValue = readIfPresent(join(gitDir, "commondir"))?.trim();
|
|
66
|
+
const metadataRoot = commonDirValue === undefined || commonDirValue === ""
|
|
67
|
+
? gitDir
|
|
68
|
+
: resolve(gitDir, commonDirValue);
|
|
69
|
+
requireDirectory(metadataRoot);
|
|
70
|
+
// Unusual but already functional: the pointer resolves within the primary mount itself.
|
|
71
|
+
if (isWithin(metadataRoot, sourcePath))
|
|
72
|
+
return null;
|
|
73
|
+
return { hostPath: metadataRoot, mountPath: metadataRoot, readOnly: false };
|
|
74
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -94,6 +94,9 @@ export interface WorkspaceCredentialsOptions {
|
|
|
94
94
|
* A workspace sourced from a CALLER-OWNED host directory instead of a fresh clone. The platform
|
|
95
95
|
* bind-mounts `path` as the workspace working directory and treats it as caller-owned: writes
|
|
96
96
|
* persist across workspace stop/restart/expiry, and the path is never reprovisioned or deleted.
|
|
97
|
+
* When `path` is a linked Git worktree, the SDK also binds its shared Git metadata at the absolute
|
|
98
|
+
* path named by the worktree's `.git` pointer. The metadata remains caller-owned and host-backed;
|
|
99
|
+
* no repository data is copied into container-owned storage.
|
|
97
100
|
* The install must allowlist the path's root (`SEALANT_MOUNT_ALLOWED_STORE_ROOTS`); paths
|
|
98
101
|
* outside the allowlist are rejected at create. Credentials and dotfiles options compose
|
|
99
102
|
* unchanged. Clone-based workspaces remain the right shape for independent verification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"effect": "^4.0.0-beta.85",
|
|
30
|
-
"@sealant/api-contracts": "^0.
|
|
30
|
+
"@sealant/api-contracts": "^0.12.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@effect/vitest": "^4.0.0-beta.85",
|