@sealant/sdk 0.25.1 → 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.
@@ -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 }));
@@ -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` are the OCI push coordinates, which
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 ?? options.source?.path ?? "workspace";
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 ? null : discoverLinkedWorktreeMetadataMount(options.source.path);
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
- : { kind: "mount", hostPath: options.source?.path },
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.25.1",
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.25.1"
29
+ "@sealant/api-contracts": "^0.26.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@effect/vitest": "4.0.0-beta.85",