{"_id":"@cgardev/pulumi-sandbox","_rev":"3-5ee0e335464ad3d7e6cad4024aa52064","name":"@cgardev/pulumi-sandbox","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.1":{"name":"@cgardev/pulumi-sandbox","version":"0.1.1","keywords":["pulumi","automation-api","sandbox","local-development","docker","infrastructure-as-code","developer-experience"],"author":"Cristian García","license":"MIT","_id":"@cgardev/pulumi-sandbox@0.1.1","maintainers":[{"name":"cgardev","email":"cgardev@gmail.com"}],"homepage":"https://github.com/cgardev/pulumi-sandbox#readme","bugs":{"url":"https://github.com/cgardev/pulumi-sandbox/issues"},"dist":{"shasum":"c4990d21dce6f15f0f639220c5935b8d85d4160e","tarball":"https://registry.npmjs.org/@cgardev/pulumi-sandbox/-/pulumi-sandbox-0.1.1.tgz","fileCount":57,"integrity":"sha512-Z3bke9SK11LFADret/v6/WZZaZDVQ2eDKO0ImD6fkqvmj+LhymPgY0/Ro+RexJ6Rrar5UAQhgNmNND1j+9K/cA==","signatures":[{"sig":"MEUCIGpkHta4DicmMkudO3IW/rqb/OaHAlks7Jmv7nOLAB0cAiEAuE7ucMkiFzILtLkmTOTVkmjtDuGYjU0NHGEzsgZTcsM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115389},"type":"module","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./docker":{"types":"./dist/docker/index.d.ts","default":"./dist/docker/index.js"}},"scripts":{"test":"vitest run","build":"tsc --project tsconfig.build.json","check":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"cgardev","email":"cgardev@gmail.com"},"repository":{"url":"git+https://github.com/cgardev/pulumi-sandbox.git","type":"git"},"description":"Local development sandboxes as code: a batteries-included lifecycle harness for Pulumi inline programs that provision docker-based infrastructure on the developer machine. Unofficial: not affiliated with or endorsed by Pulumi Corporation.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.8","typescript":"^6.0.3","@types/node":"^25.9.2","@pulumi/pulumi":"^3.245.0"},"peerDependencies":{"@pulumi/pulumi":"^3.245.0"},"_npmOperationalInternal":{"tmp":"tmp/pulumi-sandbox_0.1.1_1781048233592_0.10211156384252762","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@cgardev/pulumi-sandbox","version":"0.1.2","keywords":["pulumi","automation-api","sandbox","local-development","docker","infrastructure-as-code","developer-experience"],"author":"Cristian García","license":"MIT","_id":"@cgardev/pulumi-sandbox@0.1.2","maintainers":[{"name":"cgardev","email":"cgardev@gmail.com"}],"homepage":"https://github.com/cgardev/pulumi-sandbox#readme","bugs":{"url":"https://github.com/cgardev/pulumi-sandbox/issues"},"dist":{"shasum":"ee4486e598cfffd276d69b1897095f643f759eb3","tarball":"https://registry.npmjs.org/@cgardev/pulumi-sandbox/-/pulumi-sandbox-0.1.2.tgz","fileCount":57,"integrity":"sha512-xGNjE7JjviZe+SgtsG4jPipntzjxT5h5QPqyWAXfznT0tQVUzl2PCoHVR3S7O01kkEejA+h/j/F9M1v4Xc+i+A==","signatures":[{"sig":"MEUCIQDmsP1u+WhW+mD5j5hBmcwFTQOWv2Y8iNmvt089fZLQbQIgFB3xIiE0lpDDgItszvxLKx+2vo24gk4/mz8T8Arl9bk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115672},"type":"module","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./docker":{"types":"./dist/docker/index.d.ts","default":"./dist/docker/index.js"}},"scripts":{"test":"vitest run","build":"tsc --project tsconfig.build.json","check":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"cgardev","email":"cgardev@gmail.com"},"repository":{"url":"git+https://github.com/cgardev/pulumi-sandbox.git","type":"git"},"description":"Local development sandboxes as code: a batteries-included lifecycle harness for Pulumi inline programs that provision docker-based infrastructure on the developer machine. Unofficial: not affiliated with or endorsed by Pulumi Corporation.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.8","typescript":"^6.0.3","@types/node":"^25.9.2","@pulumi/pulumi":"^3.245.0"},"peerDependencies":{"@pulumi/pulumi":"^3.245.0"},"_npmOperationalInternal":{"tmp":"tmp/pulumi-sandbox_0.1.2_1781048772233_0.39233112506586676","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@cgardev/pulumi-sandbox","version":"0.2.0","description":"Local development sandboxes as code: a batteries-included lifecycle harness for Pulumi inline programs that provision docker-based infrastructure on the developer machine. Unofficial: not affiliated with or endorsed by Pulumi Corporation.","keywords":["pulumi","automation-api","sandbox","local-development","docker","infrastructure-as-code","developer-experience"],"license":"MIT","author":{"name":"Cristian García"},"repository":{"type":"git","url":"git+https://github.com/cgardev/pulumi-sandbox.git"},"bugs":{"url":"https://github.com/cgardev/pulumi-sandbox/issues"},"homepage":"https://github.com/cgardev/pulumi-sandbox#readme","type":"module","engines":{"node":">=24"},"packageManager":"pnpm@11.5.2","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./docker":{"types":"./dist/docker/index.d.ts","default":"./dist/docker/index.js"},"./plugins/intellij":{"types":"./dist/plugins/intellij.d.ts","default":"./dist/plugins/intellij.js"},"./plugins/bookmarks":{"types":"./dist/plugins/bookmarks.d.ts","default":"./dist/plugins/bookmarks.js"}},"sideEffects":false,"scripts":{"build":"tsc --project tsconfig.build.json","prepack":"pnpm build","check":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"@pulumi/pulumi":"^3.245.0"},"devDependencies":{"@pulumi/pulumi":"^3.245.0","@types/node":"^25.9.2","typescript":"^6.0.3","vitest":"^4.1.8"},"gitHead":"8ecc3c3bb369743d2f463d0080b9890e9dc67496","_id":"@cgardev/pulumi-sandbox@0.2.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-eYj+GuGg/VMmCQqLwkMu4RS/C47QmQ7iX2ax4j8aZLQnt5CEuvH/h9Nc3djhFPwdVVmYeuJBR8FmvoltELWizw==","shasum":"c2f5cfb33330eea1e5d36b5d15e4fc9bec072d4d","tarball":"https://registry.npmjs.org/@cgardev/pulumi-sandbox/-/pulumi-sandbox-0.2.0.tgz","fileCount":63,"unpackedSize":141234,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cgardev%2fpulumi-sandbox@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD5bsq9vNAeWxOJKxbQlG90sX+sO4T3RXZZBmqJbzWX4wIhANrRx/myO0IVQ6D8Ch5UtTXes5UUc8VcpL9wmTqnEPDe"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5a53101b-09eb-44b7-9a9d-eeb0a15bafde"}},"directories":{},"maintainers":[{"name":"cgardev","email":"cgardev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pulumi-sandbox_0.2.0_1781109147329_0.8169235843306117"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-09T23:37:13.369Z","modified":"2026-06-10T16:32:27.824Z","0.1.1":"2026-06-09T23:37:13.771Z","0.1.2":"2026-06-09T23:46:12.369Z","0.2.0":"2026-06-10T16:32:27.493Z"},"bugs":{"url":"https://github.com/cgardev/pulumi-sandbox/issues"},"author":{"name":"Cristian García"},"license":"MIT","homepage":"https://github.com/cgardev/pulumi-sandbox#readme","keywords":["pulumi","automation-api","sandbox","local-development","docker","infrastructure-as-code","developer-experience"],"repository":{"type":"git","url":"git+https://github.com/cgardev/pulumi-sandbox.git"},"description":"Local development sandboxes as code: a batteries-included lifecycle harness for Pulumi inline programs that provision docker-based infrastructure on the developer machine. Unofficial: not affiliated with or endorsed by Pulumi Corporation.","maintainers":[{"name":"cgardev","email":"cgardev@gmail.com"}],"readme":"# Pulumi Sandbox\n\n[![ci](https://github.com/cgardev/pulumi-sandbox/actions/workflows/ci.yml/badge.svg)](https://github.com/cgardev/pulumi-sandbox/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/%40cgardev%2Fpulumi-sandbox)](https://www.npmjs.com/package/@cgardev/pulumi-sandbox)\n[![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n\nLocal development sandboxes as code.\n\n> Unofficial project, not affiliated with or endorsed by Pulumi Corporation.\n> Pulumi is a trademark of Pulumi Corporation.\n\nYou write a plain [Pulumi](https://www.pulumi.com) program describing what your\napplication needs on a developer machine. This library wraps it in a complete,\nper-developer sandbox lifecycle:\n\n```typescript\n// src/sandbox.ts\nimport * as docker from \"@pulumi/docker\";\nimport { sandbox } from \"@cgardev/pulumi-sandbox\";\n\nawait sandbox({ name: \"shop\" }, (context) => {\n  const postgres = new docker.RemoteImage(\"postgres\", { name: \"postgres:18\" });\n\n  new docker.Container(\"database\", {\n    image: postgres.imageId,\n    name: context.physicalName(\"database\"),\n    ports: [{ internal: 5432, external: 25432 }],\n    envs: [\"POSTGRES_USER=dev\", \"POSTGRES_PASSWORD=dev\", \"POSTGRES_DB=shop\"],\n  });\n});\n```\n\n```bash\nnode src/sandbox.ts create     # provision (or update) the sandbox\nnode src/sandbox.ts destroy    # tear it down\nnode src/sandbox.ts            # interactive menu\n```\n\nState lives in a git-ignored `.sandbox/` directory, on Pulumi's local file\nbackend. There is no account to create and nothing to log into. On Node.js 24\nor newer the TypeScript entry point runs as-is, without a build step.\n\nThe complete, runnable version of this program — including the real-world\nflags the snippet above leaves out — is\n[`examples/getting-started`](examples/getting-started); the other\n[examples](examples) scale the same loop up to one database per service, a\nKeycloak realm, and a containerized development workspace.\n\n## Why\n\nEvery project that manages its development environment through Pulumi's\nAutomation API ends up writing the same harness: an entry script that parses\n`create` and `destroy` from argv, a file backend with the URL quirk that\nmakes it work on Windows, per-developer stack names so two people on one\nmachine don't fight over container names, and some recovery path for the day\na colleague deletes a container by hand and Pulumi state stops matching\nreality. I had copied that glue between three repositories before pulling it\nout into this library.\n\nThe program stays plain Pulumi. The library only provides what goes around it.\n\n## Why not docker-compose?\n\nA compose file describes containers. A sandbox program can also wait for\nKeycloak to finish booting before creating realms in it, render the generated\nclient secret into the `.env` file your application loads, and derive a\ncontainer for every module it finds in your repository. And since it is\nTypeScript, a topology with one database per service and a port plan is a\nfunction and a loop rather than a wall of YAML.\n\n## What it does\n\n- Lifecycle CLI: `create`, `destroy`, `reset`, `preview`, `cancel`,\n  `outputs`, `help`, and an interactive menu when no action is given. You can\n  register your own verbs as well.\n- Self-contained local state under `.sandbox/`. Point `backendUrl` at\n  `s3://...` later if the team wants shared state; nothing else changes.\n- A developer id (`SANDBOX_DEV_ID`, falling back to `local`) suffixes the\n  stack name and every physical resource name. Set `requireDevId: true` when\n  a collision would actually hurt.\n- Refresh is folded into create and destroy, so resources deleted behind\n  Pulumi's back drop out of state instead of failing the run.\n- State surgery for providers whose backing service runs in a container the\n  sandbox itself manages. This is the part nobody misses until the first\n  wedged stack; see below.\n- Helpers for the boring parts: `EnvironmentFile`, `deepResolve`,\n  `readyWhenHttp`, `findGitRoot`, and a `/docker` module with `attachShell`,\n  `dockerExec` and mask-volume discovery.\n- Plugins that project the sandbox onto developer tooling: IntelliJ data\n  sources for the provisioned databases, Chrome bookmarks for the exposed\n  consoles.\n\nThe core has zero runtime dependencies. `@pulumi/pulumi` is the only peer\ndependency, and the library knows nothing about any particular database,\nbuild tool or identity server. Your program carries the specifics.\n\n## Requirements\n\n- Node.js >= 24\n- The [Pulumi CLI](https://www.pulumi.com/docs/install/) on the PATH (no account needed)\n- Docker, when the program manages containers\n\n```bash\npnpm add @cgardev/pulumi-sandbox @pulumi/pulumi\n# plus the providers your program uses, e.g. for containers:\npnpm add @pulumi/docker\n```\n\n## The lifecycle\n\n| Action     | What happens                                                                                  |\n|:-----------|:----------------------------------------------------------------------------------------------|\n| `create`   | `up` with refresh folded in; on failure, purge container-hosted provider state and retry once |\n| `destroy`  | Purge container-hosted provider state, then `destroy` with refresh folded in                  |\n| `reset`    | `destroy` followed by `create`, in one process                                                |\n| `preview`  | Diffed preview of what `create` would change                                                  |\n| `cancel`   | Release a stuck state lock left by an interrupted run                                         |\n| `outputs`  | Print the stack outputs as JSON                                                               |\n| *(none)*   | Interactive menu over all of the above                                                        |\n\nIf another process holds the state lock you get a hint to run `cancel`, not a\nstack trace. A `reset` that hits the lock during its destroy half stops\nthere; it does not continue into create as if nothing happened.\n\n## The context\n\nThe program receives a context describing the run:\n\n```typescript\nawait sandbox({ name: \"shop\" }, (context) => {\n  context.devId;                       // \"jdoe\", the resolved developer id\n  context.stackName;                   // \"shop-jdoe\"\n  context.physicalName(\"orders-db\");   // \"shop-orders-db-jdoe\"\n  context.action;                      // the lifecycle operation executing the program\n});\n```\n\n`physicalName` is what keeps container, network and volume names from\ncolliding between developers. `context.action` is either `create` or\n`preview`, and is mostly useful for confining side effects (like writing\ngenerated files) to real create runs. The program only executes for\noperations that need the resource graph. A `destroy` works from recorded\nstate and never runs it, so destroy-time guards are unnecessary.\n\nReturning a record from the program publishes it as stack outputs:\n\n```typescript\nawait sandbox({ name: \"shop\" }, () => {\n  return { adminUrl: \"http://localhost:25080\" };\n});\n```\n\n## Container-hosted providers\n\nSome providers manage resources that live inside a container the sandbox\nitself runs: realms inside a Keycloak container, schemas inside a database\ncontainer. Pulumi has no idea the realm dies with the container. Once the\ncontainer is gone, every refresh, update and destroy aborts while\ninitializing a provider whose service no longer exists, and the stack is\nwedged.\n\nDeclare such providers and the lifecycle deals with it:\n\n```typescript\nawait sandbox(\n  { name: \"shop\", containerHostedProviders: [\"identity\"] },\n  () => {\n    const identity = new IdentityServer(/* keycloak container + sidecar */);\n\n    const provider = new keycloak.Provider(\"identity\", {\n      url: identity.readyUrl,  // configures itself only after boot\n      // ...\n    });\n    new keycloak.Realm(\"shop\", { realm: \"shop\" }, {\n      provider,\n      retainOnDelete: true,            // safety net for partial destroys\n      deletedWith: identity.container, // documents the dependency\n    });\n  },\n);\n```\n\nOn destroy, the provider and everything it manages are removed from state\nbefore the plan runs. The container teardown wipes the actual data anyway,\nso nothing needs to talk to the doomed service. On create, a failed update\npurges the same providers and retries once, which recovers sandboxes whose\ncontainers were removed out of band. Any provider resource name can be\nlisted, and `purgeProviderFromState` is exported if you need the raw\noperation.\n\n## Generating configuration for applications\n\nA sandbox is only useful once an application runs against it.\n`EnvironmentFile` accumulates variables in ordered, blank-line-separated\ngroups, and `deepResolve` collapses a tree of Pulumi outputs into one\nconcrete value, so generated credentials end up in the same file as static\nports:\n\n```typescript\nimport { EnvironmentFile, deepResolve } from \"@cgardev/pulumi-sandbox\";\n\nconst environment = new EnvironmentFile([\n  { ORDERS_DATABASE_URL: ordersDatabase.connectionUri },\n  { SMTP_HOST: \"localhost\", SMTP_PORT: 25025 },\n]);\n\nif (context.action === \"create\") {\n  deepResolve({ secret: ordersApi.clientSecret }).apply(({ secret }) => {\n    environment.add({ OIDC_CLIENT_SECRET: secret });\n    environment.write(\"generated/orders.env\");\n  });\n}\n```\n\nPassing `undefined`, `null`, or an unresolved Pulumi output throws with the\noffending key. A poisoned value in a generated `.env` costs far more\ndebugging time than an exception at render time.\n\n## Custom commands\n\nVerbs beyond the lifecycle dispatch before any Pulumi machinery starts, so\nthey are instant:\n\n```typescript\nimport { attachShell } from \"@cgardev/pulumi-sandbox/docker\";\n\nawait sandbox(\n  {\n    name: \"workspace\",\n    commands: {\n      shell: {\n        description: \"Open a shell inside the workspace container\",\n        run: ({ physicalName, argv }) => attachShell(physicalName(\"dev\"), { shell: argv[0] }),\n      },\n    },\n  },\n  program,\n);\n```\n\n## Plugins\n\nThe `plugins/` modules generate artifacts for the tools around the sandbox.\nThey never touch Pulumi — pass them resolved outputs, typically from a\n`deepResolve(...).apply(...)` confined to create runs:\n\n```typescript\nimport { writeIntellijDataSources } from \"@cgardev/pulumi-sandbox/plugins/intellij\";\nimport { writeChromeBookmarks } from \"@cgardev/pulumi-sandbox/plugins/bookmarks\";\n\nwriteIntellijDataSources(\".idea\", [\n  { name: \"orders\", jdbcUrl: \"jdbc:postgresql://localhost:25432/orders\", userName: \"dev\", password: \"dev\" },\n]);\n\nwriteChromeBookmarks(\"generated\", [\n  { name: \"Mail catcher\", url: \"http://localhost:25080\", group: \"Tools\" },\n  { name: \"Identity console\", url: \"http://localhost:25081\", group: \"Tools\" },\n], \"Shop Sandbox\");\n```\n\nThe IntelliJ plugin writes both halves of a data source\n(`dataSources.xml` and `dataSources.local.xml`) with UUIDs derived from the\ndata-source name, so the IDE's introspection cache survives sandbox resets.\nPassing a `password` embeds it in the JDBC URL — IntelliJ then connects\nwithout prompting, which is only acceptable for throwaway sandbox\ncredentials. The bookmarks plugin renders a `bookmarks.html` importable via\n`chrome://bookmarks` → Import bookmarks.\n\n## Examples\n\n| Example                                        | Shows                                                                                   |\n|:-----------------------------------------------|:----------------------------------------------------------------------------------------|\n| [`getting-started`](examples/getting-started)  | One database and one generated `.env`; the minimal loop                                 |\n| [`multi-service`](examples/multi-service)      | Databases per service, mail catcher, Keycloak realm via a container-hosted provider     |\n| [`dev-workspace`](examples/dev-workspace)      | A containerized development environment with rule-discovered mask volumes and a `shell` command |\n\n## Where state lives\n\n```\n.sandbox/             # add to .gitignore\n├── state/            # the file:// backend: checkpoints, history, backups\n└── work/<project>/   # the generated Pulumi project and per-stack settings\n```\n\nBy default `.sandbox/` sits in the package containing the entry script, not\nin the current working directory, so invoking the sandbox from anywhere\ntargets the same state. Override the location with `homeDir`.\n\nThe `file://` URL is built in the one form Pulumi's DIY backend accepts on\nboth Windows and POSIX (`fileBackendUrl`). The same entry point works for\nthe whole team.\n\n## Caveats\n\nEach of these is a deliberate trade-off, not an oversight. Knowing the\nreasoning makes the way out obvious.\n\n### `destroy` deletes your data\n\n`destroy` removes named volumes; database contents and caches go with them.\nThat is what makes `reset` trustworthy — a sandbox that preserved data\nacross resets would hand you stale state precisely when you asked for a\nclean environment. Two consequences for how you write the program: anything\nthe sandbox needs on every boot (schemas, seed users, realms) belongs in\nthe program itself, where `create` rebuilds it; anything that must survive\n(fixtures you edit by hand, a download cache that is expensive to refill)\nbelongs in a bind-mounted directory, which Pulumi never owned and therefore\nnever deletes.\n\n### The default secrets passphrase is well known\n\nPulumi encrypts stack secrets with a passphrase, and this library defaults\nit to the literal string `sandbox`. A random per-machine passphrase would\nbe security theater: it would have to be stored next to the state it\nprotects, and the secrets in question are throwaway development credentials\nfor services on localhost. What the well-known default buys is the\nzero-configuration promise — clone, `create`, no prompt. The moment state\nleaves the developer machine the math changes: when you point `backendUrl`\nat shared storage, set `passphrase` (or the `PULUMI_CONFIG_PASSPHRASE`\nenvironment variable) to a real secret.\n\n### `.sandbox/` grows over time\n\nThe file backend keeps every checkpoint, plus history and backups, and\nnever prunes them. That history is what makes recovery from interrupted\nruns possible, so the library does not clean it behind your back. The cost\nis disk space, nothing else; the directory is git-ignored and machine\nlocal. After a `destroy`, deleting `.sandbox/` entirely is always safe and\ngives a clean slate.\n\n### `onReady` hooks must be idempotent\n\n`readyWhenHttp` lives inside the resource graph, and Pulumi re-evaluates\nthe graph on every update — so once its gate resolves, the probe and its\n`onReady` hook run again on each subsequent `create`. There is no reliable\n\"first boot only\" signal that survives both an in-place update and a\ncontainer recreated out of band, so rather than pretending to have one the\nlibrary makes the contract explicit: write hooks that are no-ops against an\nalready-configured service. In practice this is easy — `dockerExec`\nshrugging off an \"already exists\" error is the common case. Previews never\nprobe and never run hooks, because a preview must not stall on a stopped\ncontainer or cause side effects.\n\n## API reference\n\nGrouped by what you are trying to do. A typical program touches `sandbox()`,\n`EnvironmentFile`, `deepResolve`, and perhaps `readyWhenHttp`; the rest is\nexported for the day you build tooling *around* the sandbox instead of\ninside it.\n\n### Entry points\n\n`sandbox(options, program)` — the complete entry point, and for most\nprojects the only import. It resolves the developer identity, dispatches\nthe command line (lifecycle actions, custom commands, `help`, or the\ninteractive menu when no action is given), and renders every failure as a\nfriendly message with remediation steps and a non-zero exit code. Your\nentry script is one call:\n\n```typescript\nawait sandbox({ name: \"shop\" }, (context) => {\n  // plain Pulumi resources\n});\n```\n\n`createSandbox(options, program)` → `Sandbox` — the same fully wired stack\nwith no command line attached. Use it where code drives the lifecycle\ninstead of a developer typing verbs: integration tests, CI smoke runs, a\nlarger task runner that embeds the sandbox. The instance has `create()`,\n`destroy()`, `reset()`, `preview()`, and `cancel()` methods, and exposes\nthe underlying `automation.Stack` for operations the lifecycle does not\ncover:\n\n```typescript\nconst instance = await createSandbox({ name: \"shop\" }, program);\nawait instance.create();\ntry {\n  await runSmokeTests();\n} finally {\n  await instance.destroy();\n}\n```\n\n### Turning outputs into application configuration\n\nThe resource graph knows the ports, endpoints, and generated credentials;\nyour application reads a flat `.env` file. These two close that gap — the\nworked example is in\n[Generating configuration for applications](#generating-configuration-for-applications).\n\n`deepResolve(value)` — collapses a plain structure with Pulumi outputs\nnested anywhere inside (objects, arrays, promises) into a single output of\nthe fully concrete shape. It exists because collecting a dozen values from\ndifferent resources with raw `apply` calls turns into a pyramid; with\n`deepResolve` you assemble the structure once and consume it in one\nclosure. Class instances and circular references are rejected with the\noffending path named, since Pulumi would otherwise flatten them silently.\n\n`EnvironmentFile` — an ordered, incrementally built `.env` file. Each\n`add({ ... })` call starts a blank-line-separated group; re-adding a key\noverwrites it in place, so the file layout stays stable across runs. An\nempty string renders as a commented-out `# KEY=` line — present for\ndiscoverability, inactive for the loader. Passing `undefined`, `null`, or\nan unresolved output throws immediately with the offending key: a loud\nfailure at render time beats debugging an application that read a poisoned\nvalue. `write(path)` creates parent directories and writes the file;\n`values()` returns the same effective variables as an object for in-process\nuse.\n\n### Waiting for services to boot\n\n`readyWhenHttp(gate, url, options)` — returns an output that resolves to\n`url` only after the endpoint behind it responds, gated on another resource\n(typically the container serving the endpoint) being scheduled first.\nAnything consuming the returned output — a provider, a dependent resource —\nis therefore held back until the service has actually booted. The optional\n`onReady` hook is the place for imperative post-boot configuration; pair it\nwith `dockerExec`. Previews resolve immediately, without probing and\nwithout side effects.\n\n`waitForHttp(url, options)` — the raw probe underneath: polls until the\nendpoint responds or the timeout elapses (default 180 seconds, every 2\nseconds) and returns whether it became ready. It deliberately never throws —\non timeout it warns and returns `false`, so flows that race a disappearing\ncontainer (destroy, refresh) degrade to a warning instead of wedging. By\ndefault any HTTP status counts as ready, even a 403, because a status line\nproves the server is up — which is all a boot probe needs; pass\n`expect: \"ok\"` to require a 2xx.\n\n### Developer identity and configuration files\n\n`resolveDevId(options)` — the exact resolution the sandbox itself performs:\nthe explicit option, then the `SANDBOX_DEV_ID` environment variable, then\nthe optional `envFile`, then `\"local\"` — or an error when `require: true`.\nExported so external tooling (a script that computes container names, a\ncleanup job) can agree with the sandbox about whose resources it is\ntouching. The constants `DEV_ID_VARIABLE` and `DEFAULT_DEV_ID` are exported\nalongside it.\n\n`readEnvFile(path)` / `parseEnvFile(content)` — minimal `KEY=value`\nparsing: blank lines and `#` comments are skipped, the first `=` splits key\nfrom value, both sides are trimmed. No quoting, no interpolation —\ndeliberately less than dotenv, so sandbox configuration files stay\ntrivially predictable. `readEnvFile` returns `undefined` for a missing\nfile, keeping \"not configured\" distinguishable from \"empty\".\n\n`booleanFlag(value, defaultValue)` — interprets `true`/`false`, `1`/`0`,\n`yes`/`no`, and `on`/`off` case-insensitively; anything else, including a\nmissing value, yields the default. For feature toggles read from the\nenvironment or an `envFile`.\n\n### State surgery\n\nThe high-level switch is the `containerHostedProviders` option (see\n[Container-hosted providers](#container-hosted-providers)); these are the\nraw operations underneath it, exported for custom recovery tooling.\n\n`purgeProviderFromState(stack, providerName)` — exports the stack's state,\nremoves the named provider and every resource it manages, and imports the\nresult back. The import only happens when something actually matched, so a\nhealthy stack never sees a state write. Returns a summary of what was\nremoved.\n\n`removeProviderFromDeployment(deployment, providerName)` — the pure,\nin-memory half: mutates an exported deployment body in place and also drops\nevery dangling reference (dependencies, parents, `deletedWith`) to the\npurged resources, because the engine refuses to import a deployment that\nmentions missing resources. Useful for testing recovery logic and for\ninspecting state offline.\n\n### Paths and plumbing\n\n`fileBackendUrl(absoluteDirectory)` — builds the one `file://` URL form\nthat Pulumi's DIY backend accepts on both Windows and POSIX. Node's own\n`pathToFileURL` produces `file:///D:/...`, which the backend mis-parses on\nWindows into `file:///D:/D:/...`; this helper exists so nobody has to\nrediscover that bug.\n\n`resolveDirectories(homeDir, projectName)` / `ensureDirectories(dirs)` —\ncompute and create the `.sandbox/` layout: `state/` shared per home,\n`work/<project>/` scoped per project. Exported for tools that need to\nlocate sandbox state — a cleanup script, a disk usage report — without\nhardcoding the layout.\n\n`findGitRoot(startDirectory?)` — walks upward until a `.git` entry appears\nand returns that directory, or `undefined` outside a repository. Both\n`.git` directories and `.git` pointer files count, so worktrees and\nsubmodules work. The natural anchor for artifacts that belong at the\nrepository root — the `.idea` directory the IntelliJ plugin writes to, for\nexample.\n\n### Errors\n\nEvery library error extends `SandboxError`, and the library throws instead\nof ever calling `process.exit` itself — so the same functions behave under\ntests and inside larger tools, and `sandbox()` is the single place where\nerrors become terminal output and an exit code. When embedding, catch the\nsubtypes: `SandboxConfigurationError` (the machine or the options are\nincomplete; carries `remediation` lines to show the developer),\n`SandboxLockError` (another process holds the state lock; resolved by the\n`cancel` action), and `EnvironmentFileError` (a value could not be rendered\ninto an environment file).\n\n### Docker utilities — `@cgardev/pulumi-sandbox/docker`\n\nHost-side helpers that shell out to the `docker` CLI. Nothing here imports\nPulumi, and `@pulumi/docker` is not required.\n\n`attachShell(containerName, options)` — the interactive\n`docker exec -it <container> <shell>` a developer would type by hand,\nreturning the shell's exit code. Built to back a custom `shell` command —\nsee [Custom commands](#custom-commands).\n\n`dockerExec(containerName, command, options)` — runs a command inside a\nrunning container, for the post-boot configuration no provider covers:\nunlocking an admin API, creating a seed user, flipping a development-only\nsetting. On failure it warns and returns `false` instead of throwing\n(override with `warnOnly: false`), because these calls usually run inside\nreadiness chains where a throw would wedge destroy and refresh. Commands\nmust be idempotent — see the `onReady` caveat above.\n\n`discoverMaskVolumes(root, { containerRoot, rules })` — for containerized\ndevelopment environments where the repository is bind-mounted into the\ncontainer: walks the tree and applies caller-supplied rules (\"a directory\ncontaining `package.json` gets its `node_modules` masked\") to produce the\ncontainer-local volumes that keep host build artifacts and container build\nartifacts separate. Rule-driven precisely so the library stays ignorant of\nany particular build tool — your rules carry that knowledge.\n\n### Plugins — `@cgardev/pulumi-sandbox/plugins/*`\n\nGenerators for the tools around the sandbox. They consume resolved outputs\nand never import Pulumi; the worked example is in [Plugins](#plugins).\n\n`plugins/intellij` — `writeIntellijDataSources(ideaDir, definitions)`\nwrites both halves of an IntelliJ data source (`dataSources.xml` and\n`dataSources.local.xml`) for the Postgres databases the sandbox provisions.\nUUIDs are derived deterministically from each data-source name, so the\nIDE's introspection cache survives regeneration; an optional password is\nembedded into the JDBC URL so IntelliJ connects without prompting.\n`renderDataSourcesXml` and `renderDataSourcesLocalXml` return the same\ndocuments as strings.\n\n`plugins/bookmarks` — `writeChromeBookmarks(outputDir, entries,\nrootFolder?)` renders the consoles and dashboards the sandbox exposes as a\n`bookmarks.html` in the Netscape bookmark format Chrome imports\n(`chrome://bookmarks` → Import bookmarks), with entries grouped into\nsub-folders. `renderChromeBookmarksHtml` returns the document as a string.\n\n## Development\n\n```bash\npnpm install\npnpm build      # compile to dist/\npnpm check      # type-check sources and tests\npnpm test       # vitest\n```\n\n## License\n\n[MIT](LICENSE)\n","readmeFilename":"README.md"}