pulumi-sandbox 0.1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -0
  3. package/dist/actions.d.ts +7 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +14 -0
  6. package/dist/backend.d.ts +40 -0
  7. package/dist/backend.d.ts.map +1 -0
  8. package/dist/backend.js +40 -0
  9. package/dist/docker/exec.d.ts +22 -0
  10. package/dist/docker/exec.d.ts.map +1 -0
  11. package/dist/docker/exec.js +26 -0
  12. package/dist/docker/index.d.ts +12 -0
  13. package/dist/docker/index.d.ts.map +1 -0
  14. package/dist/docker/index.js +8 -0
  15. package/dist/docker/mask-volumes.d.ts +74 -0
  16. package/dist/docker/mask-volumes.d.ts.map +1 -0
  17. package/dist/docker/mask-volumes.js +97 -0
  18. package/dist/docker/shell.d.ts +23 -0
  19. package/dist/docker/shell.d.ts.map +1 -0
  20. package/dist/docker/shell.js +34 -0
  21. package/dist/environment-file.d.ts +58 -0
  22. package/dist/environment-file.d.ts.map +1 -0
  23. package/dist/environment-file.js +129 -0
  24. package/dist/errors.d.ts +43 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +47 -0
  27. package/dist/git.d.ts +11 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +24 -0
  30. package/dist/identity.d.ts +55 -0
  31. package/dist/identity.d.ts.map +1 -0
  32. package/dist/identity.js +94 -0
  33. package/dist/index.d.ts +28 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +19 -0
  36. package/dist/interactive.d.ts +15 -0
  37. package/dist/interactive.d.ts.map +1 -0
  38. package/dist/interactive.js +45 -0
  39. package/dist/lifecycle.d.ts +41 -0
  40. package/dist/lifecycle.d.ts.map +1 -0
  41. package/dist/lifecycle.js +110 -0
  42. package/dist/outputs.d.ts +30 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +65 -0
  45. package/dist/readiness.d.ts +57 -0
  46. package/dist/readiness.d.ts.map +1 -0
  47. package/dist/readiness.js +66 -0
  48. package/dist/sandbox.d.ts +172 -0
  49. package/dist/sandbox.d.ts.map +1 -0
  50. package/dist/sandbox.js +260 -0
  51. package/dist/state-surgery.d.ts +34 -0
  52. package/dist/state-surgery.d.ts.map +1 -0
  53. package/dist/state-surgery.js +94 -0
  54. package/dist/terminal.d.ts +35 -0
  55. package/dist/terminal.d.ts.map +1 -0
  56. package/dist/terminal.js +67 -0
  57. package/package.json +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The pulumi-sandbox contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # Pulumi Sandbox
2
+
3
+ [![ci](https://github.com/cgardev/pulumi-sandbox/actions/workflows/ci.yml/badge.svg)](https://github.com/cgardev/pulumi-sandbox/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/pulumi-sandbox)](https://www.npmjs.com/package/pulumi-sandbox)
5
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
7
+ **Local development sandboxes as code.**
8
+
9
+ Write a plain [Pulumi](https://www.pulumi.com) program describing the local
10
+ infrastructure your application needs — containers, databases, message
11
+ brokers, identity servers — and get a complete, per-developer sandbox
12
+ lifecycle around it. No backend account, no YAML, no glue scripts.
13
+
14
+ ```typescript
15
+ // src/sandbox.ts
16
+ import * as docker from "@pulumi/docker";
17
+ import { sandbox } from "pulumi-sandbox";
18
+
19
+ await sandbox({ name: "shop" }, (context) => {
20
+ const image = new docker.RemoteImage("postgres", { name: "postgres:18", keepLocally: true });
21
+
22
+ new docker.Container("database", {
23
+ image: image.imageId,
24
+ name: context.physicalName("database"),
25
+ ports: [{ internal: 5432, external: 25432 }],
26
+ envs: ["POSTGRES_USER=dev", "POSTGRES_PASSWORD=dev", "POSTGRES_DB=shop"],
27
+ mustRun: true,
28
+ }, { deleteBeforeReplace: true });
29
+ });
30
+ ```
31
+
32
+ ```bash
33
+ node src/sandbox.ts create # provision (or update) the sandbox
34
+ node src/sandbox.ts destroy # tear it down
35
+ node src/sandbox.ts # interactive menu
36
+ ```
37
+
38
+ That is the entire setup. State lives in a git-ignored `.sandbox/` directory
39
+ on the local `file://` backend; with Node.js 24+ the TypeScript entry point
40
+ runs directly, no build step involved.
41
+
42
+ ## Why not docker-compose?
43
+
44
+ A compose file describes containers. A sandbox program describes an
45
+ *environment*: it can wait for a server to boot before configuring realms
46
+ inside it, generate credentials and render them into the `.env` files your
47
+ applications load, derive a container per module of your repository, and
48
+ reuse every Pulumi provider in existence. With the full expressiveness of
49
+ TypeScript — loops, functions, composition — complex topologies (one database
50
+ per service, port plans, cross-service wiring) stay readable.
51
+
52
+ This library supplies everything *around* that program, so a project's
53
+ infrastructure entry point contains nothing but infrastructure.
54
+
55
+ ## What you get
56
+
57
+ - **Zero-configuration state.** A self-contained `file://` backend under
58
+ `.sandbox/` — no Pulumi account, no cloud bucket, nothing to log into.
59
+ Point `backendUrl` at `s3://...` later if the team wants shared state.
60
+ - **Per-developer isolation.** A developer id (from `SANDBOX_DEV_ID`, an
61
+ optional `.env` file, or the `local` default) suffixes the stack and every
62
+ physical resource name, so two developers on one machine — or two checkouts
63
+ given distinct `SANDBOX_DEV_ID` values — never collide.
64
+ - **A complete lifecycle.** `create`, `destroy`, `reset`, `preview`,
65
+ `cancel`, `outputs`, `help`, an interactive menu, and your own custom
66
+ commands — with readable output and honest exit codes.
67
+ - **A lifecycle that survives reality.** Refresh is folded into `create` and
68
+ `destroy`, so containers killed by hand drop out of state instead of
69
+ failing the run. Providers hosted *inside* sandbox-managed containers
70
+ (Keycloak realms, database schemas) get state surgery on destroy and a
71
+ purge-and-retry on create — see below.
72
+ - **Developer-experience helpers.** `EnvironmentFile` renders ordered,
73
+ grouped `.env` files; `deepResolve` turns a tree of Pulumi outputs into one
74
+ concrete value; `readyWhenHttp` gates providers on a service actually
75
+ booting; `findGitRoot` anchors paths; `pulumi-sandbox/docker` adds
76
+ `attachShell`, `dockerExec`, and rule-driven mask-volume discovery.
77
+ - **A tiny, generic core.** ESM, fully typed, zero runtime dependencies, and
78
+ `@pulumi/pulumi` as the only peer dependency. The library has no knowledge
79
+ of any particular database, build tool, or identity server — your program
80
+ and your rules carry the specifics.
81
+
82
+ ## Requirements
83
+
84
+ - Node.js >= 24
85
+ - The [Pulumi CLI](https://www.pulumi.com/docs/install/) on the PATH (no account needed)
86
+ - Docker, when the program manages containers
87
+
88
+ ```bash
89
+ pnpm add pulumi-sandbox @pulumi/pulumi
90
+ # plus the providers your program uses, e.g. for containers:
91
+ pnpm add @pulumi/docker
92
+ ```
93
+
94
+ ## The lifecycle
95
+
96
+ | Action | What happens |
97
+ |:-----------|:----------------------------------------------------------------------------------------------|
98
+ | `create` | `up` with refresh folded in; on failure, purge container-hosted provider state and retry once |
99
+ | `destroy` | Purge container-hosted provider state, then `destroy` with refresh folded in |
100
+ | `reset` | `destroy` followed by `create`, in one process |
101
+ | `preview` | Diffed preview of what `create` would change |
102
+ | `cancel` | Release a stuck state lock left by an interrupted run |
103
+ | `outputs` | Print the stack outputs as JSON |
104
+ | *(none)* | Interactive menu over all of the above |
105
+
106
+ A concurrent-update collision is reported as a hint to run `cancel`, never as
107
+ a stack trace — and a `reset` whose destroy half hits the lock stops instead
108
+ of silently proceeding.
109
+
110
+ ## The context
111
+
112
+ The program receives a context describing the run:
113
+
114
+ ```typescript
115
+ await sandbox({ name: "shop" }, (context) => {
116
+ context.devId; // "jdoe" — the resolved developer id
117
+ context.stackName; // "shop-jdoe"
118
+ context.physicalName("orders-db"); // "shop-orders-db-jdoe"
119
+ context.action; // the lifecycle operation executing the program
120
+ });
121
+ ```
122
+
123
+ `physicalName` keeps container, network, and volume names collision-free per
124
+ developer. `action` is the operation currently executing the program —
125
+ `create` or `preview` — and gates side effects like writing generated
126
+ artifacts. The program only runs for operations that need the resource
127
+ graph; a `destroy` works from the recorded state and never executes it, so
128
+ programs need no destroy-time guards.
129
+
130
+ Returning a record from the program publishes it as stack outputs:
131
+
132
+ ```typescript
133
+ await sandbox({ name: "shop" }, () => {
134
+ return { adminUrl: "http://localhost:25080" };
135
+ });
136
+ ```
137
+
138
+ ## Container-hosted providers
139
+
140
+ Some providers manage resources *inside* a container the sandbox itself
141
+ runs — realms inside a Keycloak container, schemas inside a database
142
+ container. Pulumi treats those resources as independent of the container, so
143
+ when the container disappears (a destroy, or a developer's `docker rm`), any
144
+ refresh, update, or destroy aborts while initializing a provider whose
145
+ service no longer exists.
146
+
147
+ Declare such providers and the lifecycle handles the rest:
148
+
149
+ ```typescript
150
+ await sandbox(
151
+ { name: "shop", containerHostedProviders: ["identity"] },
152
+ () => {
153
+ const identity = new IdentityServer(/* keycloak container + sidecar */);
154
+
155
+ const provider = new keycloak.Provider("identity", {
156
+ url: identity.readyUrl, // configures itself only after boot
157
+ // ...
158
+ });
159
+ new keycloak.Realm("shop", { realm: "shop" }, {
160
+ provider,
161
+ retainOnDelete: true, // safety net for partial destroys
162
+ deletedWith: identity.container, // documents the dependency
163
+ });
164
+ },
165
+ );
166
+ ```
167
+
168
+ On `destroy`, the provider and everything it manages are removed from state
169
+ before the plan runs — the container teardown wipes them physically, so
170
+ nothing needs to talk to the doomed service. On `create`, a failed update
171
+ triggers the same purge and a single retry, which recovers sandboxes whose
172
+ containers were removed out-of-band. The mechanism is generic: any provider
173
+ resource name can be listed, and `purgeProviderFromState` is exported for
174
+ custom flows.
175
+
176
+ ## Rendering configuration for applications
177
+
178
+ Sandboxes exist so applications can run against them. `EnvironmentFile`
179
+ accumulates variables in ordered, blank-line-separated groups; `deepResolve`
180
+ collapses any tree of Pulumi outputs into one concrete value, so generated
181
+ credentials land in the same file as static ports:
182
+
183
+ ```typescript
184
+ import { EnvironmentFile, deepResolve } from "pulumi-sandbox";
185
+
186
+ const environment = new EnvironmentFile([
187
+ { ORDERS_DATABASE_URL: ordersDatabase.connectionUri },
188
+ { SMTP_HOST: "localhost", SMTP_PORT: 25025 },
189
+ ]);
190
+
191
+ if (context.action === "create") {
192
+ deepResolve({ secret: ordersApi.clientSecret }).apply(({ secret }) => {
193
+ environment.add({ OIDC_CLIENT_SECRET: secret });
194
+ environment.write("generated/orders.env");
195
+ });
196
+ }
197
+ ```
198
+
199
+ An `undefined` or `null` value throws immediately with the offending key —
200
+ a loud failure beats a poisoned environment file.
201
+
202
+ ## Custom commands
203
+
204
+ Verbs beyond the lifecycle dispatch before any Pulumi machinery starts, so
205
+ they stay instant:
206
+
207
+ ```typescript
208
+ import { attachShell } from "pulumi-sandbox/docker";
209
+
210
+ await sandbox(
211
+ {
212
+ name: "workspace",
213
+ commands: {
214
+ shell: {
215
+ description: "Open a shell inside the workspace container",
216
+ run: ({ physicalName, argv }) => attachShell(physicalName("dev"), { shell: argv[0] }),
217
+ },
218
+ },
219
+ },
220
+ program,
221
+ );
222
+ ```
223
+
224
+ ## Examples
225
+
226
+ | Example | Shows |
227
+ |:-----------------------------------------------|:----------------------------------------------------------------------------------------|
228
+ | [`getting-started`](examples/getting-started) | One database, one generated `.env` — the minimal loop |
229
+ | [`multi-service`](examples/multi-service) | Databases per service, mail catcher, Keycloak realm via a container-hosted provider |
230
+ | [`dev-workspace`](examples/dev-workspace) | A containerized development environment with rule-discovered mask volumes and a `shell` command |
231
+
232
+ ## State layout and portability
233
+
234
+ ```
235
+ .sandbox/ # add to .gitignore
236
+ ├── state/ # the file:// backend: checkpoints, history, backups
237
+ └── work/<project>/ # the generated Pulumi project and per-stack settings
238
+ ```
239
+
240
+ By default `.sandbox/` lives in the package containing the entry script —
241
+ anchored there rather than to the working directory, so invoking the sandbox
242
+ from anywhere targets the same state. Override the location with `homeDir`.
243
+
244
+ Secrets on the local backend are encrypted with a well-known default
245
+ passphrase (`sandbox`) to keep the zero-configuration promise — local
246
+ sandboxes hold throwaway development credentials. Override `passphrase` or
247
+ set `PULUMI_CONFIG_PASSPHRASE` when pointing at a shared backend.
248
+
249
+ The `file://` URL is built in the one form Pulumi's DIY backend accepts on
250
+ both Windows and POSIX (`fileBackendUrl`), so the same entry point works for
251
+ the whole team.
252
+
253
+ ## API overview
254
+
255
+ Core (`pulumi-sandbox`):
256
+
257
+ - `sandbox(options, program)` — the complete entry point: dispatch, lifecycle, error rendering
258
+ - `createSandbox(options, program)` / `Sandbox` — programmatic control, the underlying `automation.Stack` included
259
+ - `EnvironmentFile`, `deepResolve`, `waitForHttp`, `readyWhenHttp`, `findGitRoot`
260
+ - `resolveDevId`, `parseEnvFile`, `readEnvFile`, `booleanFlag`
261
+ - `purgeProviderFromState`, `removeProviderFromDeployment`
262
+ - `fileBackendUrl`, `resolveDirectories`, `ensureDirectories`
263
+ - `SandboxError`, `SandboxConfigurationError`, `SandboxLockError`, `EnvironmentFileError`
264
+
265
+ Docker utilities (`pulumi-sandbox/docker`, host-side, no `@pulumi/docker` required):
266
+
267
+ - `attachShell(containerName, options)` — interactive `docker exec`
268
+ - `dockerExec(containerName, command, options)` — idempotent post-boot configuration
269
+ - `discoverMaskVolumes(root, { containerRoot, rules })` — rule-driven discovery of directories to mask with container-local volumes
270
+
271
+ ## Development
272
+
273
+ ```bash
274
+ pnpm install
275
+ pnpm build # compile to dist/
276
+ pnpm check # type-check sources and tests
277
+ pnpm test # vitest
278
+ ```
279
+
280
+ ## License
281
+
282
+ [MIT](LICENSE)
@@ -0,0 +1,7 @@
1
+ /** Lifecycle verbs understood by every sandbox. */
2
+ export declare const LIFECYCLE_ACTIONS: readonly ["create", "destroy", "reset", "preview", "cancel", "outputs"];
3
+ export type LifecycleAction = (typeof LIFECYCLE_ACTIONS)[number];
4
+ /** One-line description per lifecycle verb, shared by `help` and the interactive menu. */
5
+ export declare const ACTION_DESCRIPTIONS: Record<LifecycleAction, string>;
6
+ export declare function isLifecycleAction(verb: string): verb is LifecycleAction;
7
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,yEAA0E,CAAC;AAEzG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAO/D,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,eAAe,CAEvE"}
@@ -0,0 +1,14 @@
1
+ /** Lifecycle verbs understood by every sandbox. */
2
+ export const LIFECYCLE_ACTIONS = ["create", "destroy", "reset", "preview", "cancel", "outputs"];
3
+ /** One-line description per lifecycle verb, shared by `help` and the interactive menu. */
4
+ export const ACTION_DESCRIPTIONS = {
5
+ create: "Provision the sandbox, or update it to match the program",
6
+ destroy: "Tear down every resource the sandbox manages",
7
+ reset: "Destroy, then create — a clean slate in one command",
8
+ preview: "Show what create would change, without changing it",
9
+ cancel: "Release a stuck state lock left by an interrupted run",
10
+ outputs: "Print the stack outputs as JSON",
11
+ };
12
+ export function isLifecycleAction(verb) {
13
+ return LIFECYCLE_ACTIONS.includes(verb);
14
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Builds the `file://` URL for Pulumi's DIY (self-managed) backend from an
3
+ * absolute directory path.
4
+ *
5
+ * Node's `pathToFileURL` produces a triple-slash URL (`file:///D:/...`) that
6
+ * the DIY backend mis-parses on Windows — it re-prepends the drive, yielding
7
+ * `file:///D:/D:/...` and a "filename ... syntax is incorrect" error. The
8
+ * form Pulumi accepts on Windows is `file://D:/forward/slash/path`.
9
+ * Prefixing the absolute path (with forward slashes) with `file://` gives
10
+ * exactly that on Windows and the correct `file:///absolute/path` on POSIX,
11
+ * where the path already starts with `/`.
12
+ */
13
+ export declare function fileBackendUrl(absoluteDirectory: string): string;
14
+ /**
15
+ * On-disk layout of a local sandbox. Everything lives under a single home
16
+ * directory (default `.sandbox/`, git-ignored) so a checkout can be cleaned
17
+ * with one deletion and nothing machine-specific is ever committed.
18
+ */
19
+ export interface SandboxDirectories {
20
+ /** Root of the sandbox-managed files. */
21
+ home: string;
22
+ /** Pulumi DIY backend storage — checkpoints, history, backups, locks. */
23
+ state: string;
24
+ /** Pulumi work directory — the generated project and per-stack settings. */
25
+ work: string;
26
+ }
27
+ /**
28
+ * Resolves the directory layout under the given sandbox home directory. The
29
+ * work directory is scoped per project so two sandboxes sharing a home never
30
+ * fight over the generated project file; the state directory is shared, as
31
+ * the DIY backend already namespaces checkpoints by project.
32
+ */
33
+ export declare function resolveDirectories(homeDirectory: string, projectName: string): SandboxDirectories;
34
+ /**
35
+ * Creates the sandbox directories. Both must exist before the Pulumi
36
+ * workspace is constructed: the file backend needs somewhere to write its
37
+ * checkpoint, and the work directory receives the generated project file.
38
+ */
39
+ export declare function ensureDirectories(directories: SandboxDirectories): void;
40
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAOjG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,kBAAkB,GAAG,IAAI,CAGvE"}
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ /**
4
+ * Builds the `file://` URL for Pulumi's DIY (self-managed) backend from an
5
+ * absolute directory path.
6
+ *
7
+ * Node's `pathToFileURL` produces a triple-slash URL (`file:///D:/...`) that
8
+ * the DIY backend mis-parses on Windows — it re-prepends the drive, yielding
9
+ * `file:///D:/D:/...` and a "filename ... syntax is incorrect" error. The
10
+ * form Pulumi accepts on Windows is `file://D:/forward/slash/path`.
11
+ * Prefixing the absolute path (with forward slashes) with `file://` gives
12
+ * exactly that on Windows and the correct `file:///absolute/path` on POSIX,
13
+ * where the path already starts with `/`.
14
+ */
15
+ export function fileBackendUrl(absoluteDirectory) {
16
+ return `file://${absoluteDirectory.replace(/\\/g, "/")}`;
17
+ }
18
+ /**
19
+ * Resolves the directory layout under the given sandbox home directory. The
20
+ * work directory is scoped per project so two sandboxes sharing a home never
21
+ * fight over the generated project file; the state directory is shared, as
22
+ * the DIY backend already namespaces checkpoints by project.
23
+ */
24
+ export function resolveDirectories(homeDirectory, projectName) {
25
+ const home = path.resolve(homeDirectory);
26
+ return {
27
+ home,
28
+ state: path.join(home, "state"),
29
+ work: path.join(home, "work", projectName),
30
+ };
31
+ }
32
+ /**
33
+ * Creates the sandbox directories. Both must exist before the Pulumi
34
+ * workspace is constructed: the file backend needs somewhere to write its
35
+ * checkpoint, and the work directory receives the generated project file.
36
+ */
37
+ export function ensureDirectories(directories) {
38
+ fs.mkdirSync(directories.state, { recursive: true });
39
+ fs.mkdirSync(directories.work, { recursive: true });
40
+ }
@@ -0,0 +1,22 @@
1
+ export interface DockerExecOptions {
2
+ /**
3
+ * Warn and return `false` instead of throwing when the command fails.
4
+ * This is the right mode for post-boot configuration hooks: the service
5
+ * that actually needs the configuration will surface a clear error at its
6
+ * own layer, while a throw here would wedge destroy and refresh flows.
7
+ * Default: `true`.
8
+ */
9
+ warnOnly?: boolean | undefined;
10
+ }
11
+ /**
12
+ * Runs a command inside a running container via `docker exec`, for
13
+ * imperative post-boot configuration that no provider covers — unlocking an
14
+ * admin API, creating a seed user, flipping a development-only setting.
15
+ * Returns whether the command succeeded.
16
+ *
17
+ * Commands must be idempotent: readiness chains re-execute on every Pulumi
18
+ * operation, so the same configuration may run against an already-configured
19
+ * container.
20
+ */
21
+ export declare function dockerExec(containerName: string, command: readonly string[], options?: DockerExecOptions): boolean;
22
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/docker/exec.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAatH"}
@@ -0,0 +1,26 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { warn } from "../terminal.js";
3
+ /**
4
+ * Runs a command inside a running container via `docker exec`, for
5
+ * imperative post-boot configuration that no provider covers — unlocking an
6
+ * admin API, creating a seed user, flipping a development-only setting.
7
+ * Returns whether the command succeeded.
8
+ *
9
+ * Commands must be idempotent: readiness chains re-execute on every Pulumi
10
+ * operation, so the same configuration may run against an already-configured
11
+ * container.
12
+ */
13
+ export function dockerExec(containerName, command, options = {}) {
14
+ try {
15
+ execFileSync("docker", ["exec", containerName, ...command], { stdio: "pipe" });
16
+ return true;
17
+ }
18
+ catch (error) {
19
+ const detail = error.stderr?.toString().trim() || error.message;
20
+ if (options.warnOnly ?? true) {
21
+ warn(`docker exec in ${containerName} failed: ${detail}`);
22
+ return false;
23
+ }
24
+ throw error;
25
+ }
26
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host-side docker utilities — attaching shells, executing post-boot
3
+ * configuration, and discovering mask volumes. Nothing in this module
4
+ * touches Pulumi; it complements the resources a program declares.
5
+ */
6
+ export { attachShell } from "./shell.js";
7
+ export type { AttachShellOptions } from "./shell.js";
8
+ export { dockerExec } from "./exec.js";
9
+ export type { DockerExecOptions } from "./exec.js";
10
+ export { discoverMaskVolumes } from "./mask-volumes.js";
11
+ export type { MaskVolume, MaskRule, DiscoverMaskVolumesOptions } from "./mask-volumes.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/docker/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Host-side docker utilities — attaching shells, executing post-boot
3
+ * configuration, and discovering mask volumes. Nothing in this module
4
+ * touches Pulumi; it complements the resources a program declares.
5
+ */
6
+ export { attachShell } from "./shell.js";
7
+ export { dockerExec } from "./exec.js";
8
+ export { discoverMaskVolumes } from "./mask-volumes.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A named volume layered over a bind mount, identified by a stable key. The
3
+ * key is meant to be combined with a per-developer volume prefix into the
4
+ * physical volume name.
5
+ */
6
+ export interface MaskVolume {
7
+ /** Stable, human-readable identifier derived from the rule and the repository path. */
8
+ key: string;
9
+ /** Where the volume mounts inside the container. */
10
+ containerPath: string;
11
+ }
12
+ /**
13
+ * Declares which directories a project tree generates locally and must
14
+ * therefore be masked. A rule fires in every walked directory containing one
15
+ * of its marker files; the rule's `mask` entries (and whatever `expand`
16
+ * derives from the marker's content) become container-local volumes.
17
+ *
18
+ * The library has no knowledge of any build tool — rules carry all of it.
19
+ * A JVM-and-Node repository, for example, is fully described by:
20
+ *
21
+ * ```typescript
22
+ * const rules: MaskRule[] = [
23
+ * { prefix: "build", markers: ["build.gradle.kts", "settings.gradle.kts"], mask: ["build"] },
24
+ * { prefix: "gradle", markers: ["settings.gradle.kts"], mask: [".gradle"] },
25
+ * { prefix: "node-modules", markers: ["package.json"], mask: ["node_modules"] },
26
+ * ];
27
+ * ```
28
+ */
29
+ export interface MaskRule {
30
+ /** Prefix for the volume keys this rule produces. */
31
+ prefix: string;
32
+ /** File names whose presence marks a directory as governed by this rule. */
33
+ markers: readonly string[];
34
+ /** Directories to mask, relative to the marked directory. */
35
+ mask: readonly string[];
36
+ /**
37
+ * Optional expansion hook: derives additional directories to mask from the
38
+ * content of the matched marker file — for build systems whose
39
+ * configuration references sibling project roots. Returned paths are
40
+ * resolved against the marker's directory and must stay inside the walked
41
+ * root; anything outside the bind mount cannot be masked and is skipped
42
+ * with a warning.
43
+ */
44
+ expand?: ((markerPath: string, content: string) => readonly string[]) | undefined;
45
+ }
46
+ export interface DiscoverMaskVolumesOptions {
47
+ /** Where the repository is bind-mounted inside the container, e.g. `/workspace`. */
48
+ containerRoot: string;
49
+ /** The rules describing which directories to mask. */
50
+ rules: readonly MaskRule[];
51
+ /**
52
+ * Directory names never descended into, on top of the directories actually
53
+ * masked and dot-directories (always skipped). Masking already prevents
54
+ * descending into a masked directory, but only where its rule fired — an
55
+ * unrelated directory that merely shares the name is still walked.
56
+ */
57
+ prune?: readonly string[] | undefined;
58
+ }
59
+ /**
60
+ * Walks a repository and returns one container-local mask volume for every
61
+ * directory the given rules mark as locally generated — directories whose
62
+ * contents must not round-trip between the host bind mount and the
63
+ * container, which is essential when host and container run different
64
+ * platforms (a Windows host building inside a Linux container, for example).
65
+ *
66
+ * Detection keys off the marker files — which exist from the first
67
+ * checkout — never off the masked directories themselves, so a directory
68
+ * that does not exist yet is still masked: docker creates the empty volume
69
+ * on first start and the first in-container build writes there, never back
70
+ * to the host. Add a module to the repository and its masks appear on the
71
+ * next `create`, with no list to maintain.
72
+ */
73
+ export declare function discoverMaskVolumes(rootDirectory: string, options: DiscoverMaskVolumesOptions): MaskVolume[];
74
+ //# sourceMappingURL=mask-volumes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask-volumes.d.ts","sourceRoot":"","sources":["../../src/docker/mask-volumes.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,QAAQ;IACvB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IAEf,4EAA4E;IAC5E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAE3B,6DAA6D;IAC7D,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAExB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,SAAS,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,0BAA0B;IACzC,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;IAEtB,sDAAsD;IACtD,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAE3B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACvC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,GAAG,UAAU,EAAE,CAgF5G"}