@sealant/sdk 0.25.2 → 0.26.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.
- package/dist/effect/api-client.d.ts +32 -0
- package/dist/effect/operations.d.ts +10 -0
- package/dist/effect/operations.js +1 -0
- package/dist/facade/workspace.js +9 -1
- package/dist/internal/blueprint.js +13 -4
- package/dist/internal/linked-worktree.d.ts +1 -0
- package/dist/types.d.ts +38 -2
- package/package.json +2 -2
|
@@ -1080,6 +1080,22 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1080
1080
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1081
1081
|
};
|
|
1082
1082
|
readonly workspaces: {
|
|
1083
|
+
readonly bindWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1084
|
+
readonly params: {
|
|
1085
|
+
readonly workspaceId: string;
|
|
1086
|
+
};
|
|
1087
|
+
readonly payload: {
|
|
1088
|
+
readonly ownerUserId: string;
|
|
1089
|
+
readonly mountPath?: string | undefined;
|
|
1090
|
+
readonly subpath: string;
|
|
1091
|
+
};
|
|
1092
|
+
readonly responseMode?: Mode;
|
|
1093
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
1094
|
+
readonly binds: readonly {
|
|
1095
|
+
readonly mountPath: string;
|
|
1096
|
+
readonly subpath: string;
|
|
1097
|
+
}[];
|
|
1098
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1083
1099
|
readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1084
1100
|
readonly headers: {
|
|
1085
1101
|
readonly "idempotency-key"?: string | undefined;
|
|
@@ -2450,6 +2466,22 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2450
2466
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2451
2467
|
};
|
|
2452
2468
|
readonly workspaces: {
|
|
2469
|
+
readonly bindWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2470
|
+
readonly params: {
|
|
2471
|
+
readonly workspaceId: string;
|
|
2472
|
+
};
|
|
2473
|
+
readonly payload: {
|
|
2474
|
+
readonly ownerUserId: string;
|
|
2475
|
+
readonly mountPath?: string | undefined;
|
|
2476
|
+
readonly subpath: string;
|
|
2477
|
+
};
|
|
2478
|
+
readonly responseMode?: Mode;
|
|
2479
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
2480
|
+
readonly binds: readonly {
|
|
2481
|
+
readonly mountPath: string;
|
|
2482
|
+
readonly subpath: string;
|
|
2483
|
+
}[];
|
|
2484
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2453
2485
|
readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2454
2486
|
readonly headers: {
|
|
2455
2487
|
readonly "idempotency-key"?: string | undefined;
|
|
@@ -135,6 +135,16 @@ export declare const execWorkspaceOp: (workspaceId: string, payload: {
|
|
|
135
135
|
readonly createdAt: string;
|
|
136
136
|
readonly updatedAt: string;
|
|
137
137
|
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
|
|
138
|
+
export declare const bindWorkspaceOp: (workspaceId: string, payload: {
|
|
139
|
+
readonly ownerUserId: string;
|
|
140
|
+
readonly mountPath?: string | undefined;
|
|
141
|
+
readonly subpath: string;
|
|
142
|
+
}) => Effect.Effect<{
|
|
143
|
+
readonly binds: readonly {
|
|
144
|
+
readonly mountPath: string;
|
|
145
|
+
readonly subpath: string;
|
|
146
|
+
}[];
|
|
147
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
|
|
138
148
|
export declare const stopWorkspaceOp: (workspaceId: string, payload: {
|
|
139
149
|
readonly ownerUserId: string;
|
|
140
150
|
}) => Effect.Effect<{
|
|
@@ -11,6 +11,7 @@ export const getWorkspaceOp = (workspaceId, ownerUserId) => Effect.flatMap(Seala
|
|
|
11
11
|
}));
|
|
12
12
|
export const listWorkspacesOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.listWorkspaces({ query }));
|
|
13
13
|
export const execWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.execWorkspace({ params: { workspaceId }, payload }));
|
|
14
|
+
export const bindWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.bindWorkspace({ params: { workspaceId }, payload }));
|
|
14
15
|
export const stopWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.stopWorkspace({ params: { workspaceId }, payload }));
|
|
15
16
|
export const restartWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.restartWorkspace({ params: { workspaceId }, payload }));
|
|
16
17
|
export const expireWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.expireWorkspace({ params: { workspaceId }, payload }));
|
package/dist/facade/workspace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execWorkspace } from "../effect/exec-workspace.js";
|
|
2
|
-
import { createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
2
|
+
import { bindWorkspaceOp, createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
3
3
|
import { SealantError, SealantNotImplementedError } from "../errors.js";
|
|
4
4
|
import { parseTtlSeconds } from "../internal/duration.js";
|
|
5
5
|
import { makeInteractiveSession } from "./session.js";
|
|
@@ -120,6 +120,14 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
120
120
|
harness,
|
|
121
121
|
sessions,
|
|
122
122
|
exec: (argv, options) => execWorkspace(ctx, init, argv, options),
|
|
123
|
+
bind: async (options) => {
|
|
124
|
+
const result = await ctx.runtime.run(bindWorkspaceOp(init.id, {
|
|
125
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
126
|
+
...(options.mountPath === undefined ? {} : { mountPath: options.mountPath }),
|
|
127
|
+
subpath: options.subpath,
|
|
128
|
+
}));
|
|
129
|
+
return result.binds.map((bind) => ({ mountPath: bind.mountPath, subpath: bind.subpath }));
|
|
130
|
+
},
|
|
123
131
|
// Poll-backed lifecycle stream: emit a coarse event on each status transition until the workspace
|
|
124
132
|
// reaches a terminal/ready state. Swaps to SSE over Postgres LISTEN/NOTIFY in Stage 5 (same shape).
|
|
125
133
|
events: () => {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Lowers the fluent `create({ repository, harness })` options into the existing
|
|
3
3
|
* `createWorkspaceRequestSchema` the control plane accepts — entirely client-side, so the slice needs
|
|
4
4
|
* no contract change. The public `repository` is the SOURCE git repo (it becomes
|
|
5
|
-
* `spec.sources.workspace.url`); the contract's `repository`/`tag`
|
|
5
|
+
* `spec.sources.workspace.url`); the contract's `repository`/`tag` name the create for the build
|
|
6
|
+
* job's own records — the worker publishes the image under plan-keyed coordinates (one repository
|
|
7
|
+
* per OS family, one tag per plan hash), so this name never reaches the registry — and
|
|
6
8
|
* we derive. `customization.enableSealantd` is forced on (it bakes + launches the daemon the run path
|
|
7
9
|
* connects to), the runtime target is `auto` (the deployment's default adapter — Docker on self-host, Kubernetes
|
|
8
10
|
* when the worker is configured for a cluster), and the
|
|
@@ -54,13 +56,17 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
54
56
|
(options.dotfiles.archives === undefined || options.dotfiles.archives.length === 0)) {
|
|
55
57
|
throw new SealantError("`dotfiles` requires a `repository`, at least one entry in `archives`, or both.", { code: "invalid_create_options" });
|
|
56
58
|
}
|
|
57
|
-
const sourceName = options.repository ??
|
|
59
|
+
const sourceName = options.repository ??
|
|
60
|
+
(options.source?.kind === "standby" ? options.source.rootPath : options.source?.path) ??
|
|
61
|
+
"workspace";
|
|
58
62
|
const tail = sourceName
|
|
59
63
|
.split("/")
|
|
60
64
|
.filter((s) => s.length > 0)
|
|
61
65
|
.pop() ?? sourceName;
|
|
62
66
|
const credentials = mapWorkspaceCredentials(options.credentials);
|
|
63
|
-
const linkedWorktreeMount = options.source === undefined
|
|
67
|
+
const linkedWorktreeMount = options.source === undefined || options.source.kind === "standby"
|
|
68
|
+
? null
|
|
69
|
+
: discoverLinkedWorktreeMetadataMount(options.source.path);
|
|
64
70
|
const explicitMounts = options.mounts ?? [];
|
|
65
71
|
const existingMetadataMount = linkedWorktreeMount === null
|
|
66
72
|
? undefined
|
|
@@ -142,7 +148,9 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
142
148
|
// Omitted ref = the repository's default branch, resolved by the clone itself.
|
|
143
149
|
...(options.ref === undefined ? {} : { ref: options.ref }),
|
|
144
150
|
}
|
|
145
|
-
:
|
|
151
|
+
: options.source?.kind === "standby"
|
|
152
|
+
? { kind: "standby", rootPath: options.source.rootPath }
|
|
153
|
+
: { kind: "mount", hostPath: options.source?.path },
|
|
146
154
|
...(dotfilesRepository === undefined
|
|
147
155
|
? {}
|
|
148
156
|
: {
|
|
@@ -166,6 +174,7 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
166
174
|
mountPath: mount.mountPath,
|
|
167
175
|
// Omitted = the blueprint's default (read-only). Only an explicit choice is sent.
|
|
168
176
|
...(mount.readOnly === undefined ? {} : { readOnly: mount.readOnly }),
|
|
177
|
+
...(mount.bindable === undefined ? {} : { bindable: mount.bindable }),
|
|
169
178
|
})),
|
|
170
179
|
}),
|
|
171
180
|
},
|
|
@@ -2,6 +2,7 @@ export interface LinkedWorktreeMetadataMount {
|
|
|
2
2
|
readonly hostPath: string;
|
|
3
3
|
readonly mountPath: string;
|
|
4
4
|
readonly readOnly: false;
|
|
5
|
+
readonly bindable?: false;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* A linked Git worktree carries a `.git` POINTER FILE rather than its repository metadata. Docker
|
package/dist/types.d.ts
CHANGED
|
@@ -117,6 +117,19 @@ export interface WorkspaceMountSource {
|
|
|
117
117
|
/** Absolute, normalized host path (no `..` segments). */
|
|
118
118
|
readonly path: string;
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* A STANDBY workspace (sealantd ADR-0014): the caller-owned ROOT directory — a project's
|
|
122
|
+
* worktrees directory — is mounted hidden, and the working directory does not exist until
|
|
123
|
+
* `workspace.bind({ subpath })` points it at one of the root's subdirectories. This is what lets a
|
|
124
|
+
* pool of ready workspaces serve ANY worktree of a project, and lets one workspace be re-pointed.
|
|
125
|
+
* Same allowlist as a mount source. The worktrees' shared git metadata (the bare repository the
|
|
126
|
+
* `.git` files point at) must ride as an explicit extra mount at its own absolute path.
|
|
127
|
+
*/
|
|
128
|
+
export interface WorkspaceStandbySource {
|
|
129
|
+
readonly kind: "standby";
|
|
130
|
+
/** Absolute, normalized host path of the root (no `..` segments). */
|
|
131
|
+
readonly rootPath: string;
|
|
132
|
+
}
|
|
120
133
|
/**
|
|
121
134
|
* An ADDITIONAL caller-owned host directory bind-mounted beside the primary source — sibling
|
|
122
135
|
* repositories, reference clones, scratch material the workspace should see without adopting.
|
|
@@ -132,6 +145,23 @@ export interface WorkspaceExtraMount {
|
|
|
132
145
|
readonly mountPath: string;
|
|
133
146
|
/** Defaults to `true`. Pass `false` deliberately — writes to extra mounts are unrecorded. */
|
|
134
147
|
readonly readOnly?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Bindable (sealantd ADR-0014): `hostPath` is a ROOT mounted hidden, and `mountPath` becomes a
|
|
150
|
+
* symlink `workspace.bind({ mountPath, subpath })` points at one of the root's subdirectories.
|
|
151
|
+
* A sibling repository's worktrees directory, bound to one worktree at first use.
|
|
152
|
+
*/
|
|
153
|
+
readonly bindable?: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** One live binding: `mountPath` points at `<root>/<subpath>`. */
|
|
156
|
+
export interface WorkspaceBind {
|
|
157
|
+
readonly mountPath: string;
|
|
158
|
+
readonly subpath: string;
|
|
159
|
+
}
|
|
160
|
+
export interface WorkspaceBindOptions {
|
|
161
|
+
/** The bindable mount to point; defaults to the working directory (a standby source). */
|
|
162
|
+
readonly mountPath?: string;
|
|
163
|
+
/** Relative path under the mount's root; an empty string unbinds. */
|
|
164
|
+
readonly subpath: string;
|
|
135
165
|
}
|
|
136
166
|
/** How a dotfiles tree is applied inside the workspace. */
|
|
137
167
|
export type WorkspaceDotfilesManager = "auto" | "chezmoi" | "stow" | "copy";
|
|
@@ -200,8 +230,8 @@ export interface CreateOptions {
|
|
|
200
230
|
* Exactly one of `repository` or `source` must be provided.
|
|
201
231
|
*/
|
|
202
232
|
readonly repository?: string;
|
|
203
|
-
/** Alternative to `repository`: source the workspace from a caller-owned mount. */
|
|
204
|
-
readonly source?: WorkspaceMountSource;
|
|
233
|
+
/** Alternative to `repository`: source the workspace from a caller-owned mount, or a standby root. */
|
|
234
|
+
readonly source?: WorkspaceMountSource | WorkspaceStandbySource;
|
|
205
235
|
/** Additional read-only-by-default mounts beside the primary source (see `WorkspaceExtraMount`). */
|
|
206
236
|
readonly mounts?: readonly WorkspaceExtraMount[];
|
|
207
237
|
/** The harness to run inside the workspace. */
|
|
@@ -341,6 +371,12 @@ export interface Workspace {
|
|
|
341
371
|
* run record like any other process. `argv[0]` is the executable, the rest its arguments.
|
|
342
372
|
*/
|
|
343
373
|
exec(argv: readonly string[], options?: WorkspaceExecOptions): Promise<WorkspaceExecResult>;
|
|
374
|
+
/**
|
|
375
|
+
* Point a standby working directory, or a bindable extra mount, at one subdirectory of its root
|
|
376
|
+
* (sealantd ADR-0014). Synchronous: resolves once the daemon applied it. Returns every live
|
|
377
|
+
* binding, which each relaunch re-applies.
|
|
378
|
+
*/
|
|
379
|
+
bind(options: WorkspaceBindOptions): Promise<readonly WorkspaceBind[]>;
|
|
344
380
|
/** Interactive PTY sessions: open new ones, reattach to existing ones by id. */
|
|
345
381
|
readonly sessions: WorkspaceSessions;
|
|
346
382
|
/** Lifecycle events as an async stream. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sealant/api-contracts": "^0.
|
|
29
|
+
"@sealant/api-contracts": "^0.26.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@effect/vitest": "4.0.0-beta.85",
|