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
@@ -0,0 +1,172 @@
1
+ import { automation } from "@pulumi/pulumi";
2
+ import { type LifecycleAction } from "./actions.js";
3
+ /** Default passphrase for the local secrets provider — see {@link SandboxOptions.passphrase}. */
4
+ export declare const DEFAULT_PASSPHRASE = "sandbox";
5
+ export interface SandboxOptions {
6
+ /**
7
+ * Project name. It becomes the Pulumi project, prefixes the stack name,
8
+ * and seeds {@link SandboxContext.physicalName} — lowercase letters,
9
+ * digits, and dashes.
10
+ */
11
+ name: string;
12
+ /** Explicit developer id; usually left unset and resolved from the environment. */
13
+ devId?: string | undefined;
14
+ /**
15
+ * Refuse to fall back to the `local` developer id. Set this when the team
16
+ * shares a remote backend and accidental stack collisions must be
17
+ * impossible.
18
+ */
19
+ requireDevId?: boolean | undefined;
20
+ /**
21
+ * Optional `KEY=value` file (typically a git-ignored `.env`) consulted for
22
+ * `SANDBOX_DEV_ID` when the environment variable is not set.
23
+ */
24
+ envFile?: string | undefined;
25
+ /**
26
+ * Pulumi backend URL. Defaults to a self-contained `file://` backend under
27
+ * {@link SandboxOptions.homeDir} — no cloud account, no login. Point it at
28
+ * `s3://...` (or any other DIY backend) to share state remotely.
29
+ */
30
+ backendUrl?: string | undefined;
31
+ /**
32
+ * Directory holding sandbox state and the Pulumi work directory. Defaults
33
+ * to `.sandbox` in the package containing the entry script (the nearest
34
+ * directory with a `package.json`, falling back to the entry script's
35
+ * directory) — anchored there rather than to the working directory, so
36
+ * invoking the sandbox from anywhere targets the same state. Add it to
37
+ * `.gitignore`.
38
+ */
39
+ homeDir?: string | undefined;
40
+ /**
41
+ * Passphrase for Pulumi's secrets provider. Local sandboxes hold
42
+ * throwaway development credentials, so a well-known default keeps the
43
+ * zero-configuration promise; override it (or set
44
+ * `PULUMI_CONFIG_PASSPHRASE`) when the backend is shared.
45
+ */
46
+ passphrase?: string | undefined;
47
+ /**
48
+ * Names of provider resources whose backing service lives in a container
49
+ * this sandbox manages — for example `"identity"` when the program
50
+ * declares `new keycloak.Provider("identity", ...)` against a Keycloak
51
+ * container it also creates. These providers get state surgery during
52
+ * destroy (and on create retries), so tearing down or recreating the
53
+ * container never requires talking to the service it hosted.
54
+ */
55
+ containerHostedProviders?: readonly string[] | undefined;
56
+ /**
57
+ * Additional command line verbs, dispatched before any Pulumi machinery
58
+ * is initialized — ideal for fast utilities like opening a shell inside a
59
+ * running container.
60
+ */
61
+ commands?: Record<string, SandboxCommand> | undefined;
62
+ /** Command line arguments; defaults to `process.argv.slice(2)`. */
63
+ argv?: readonly string[] | undefined;
64
+ }
65
+ /**
66
+ * The program's view of the sandbox while resources are being declared.
67
+ *
68
+ * The program runs for operations that need the resource graph — `create`,
69
+ * `preview`, and the create half of `reset`. A `destroy` works from the
70
+ * recorded state and does not execute the program, so no destroy-time
71
+ * guards are needed in it.
72
+ */
73
+ export interface SandboxContext {
74
+ readonly projectName: string;
75
+ readonly devId: string;
76
+ readonly stackName: string;
77
+ /**
78
+ * The lifecycle operation currently executing the program — `create` or
79
+ * `preview`. Use it to confine side effects like writing generated
80
+ * artifacts to real create runs:
81
+ *
82
+ * ```typescript
83
+ * if (context.action === "create") {
84
+ * environment.write("generated/application.env");
85
+ * }
86
+ * ```
87
+ */
88
+ readonly action: LifecycleAction;
89
+ /**
90
+ * Builds a developer-scoped physical resource name:
91
+ * `physicalName("postgres")` → `myproject-postgres-jdoe`. Use it for
92
+ * container names, volume prefixes, and anything else that must not
93
+ * collide between developers on one machine.
94
+ */
95
+ physicalName(...parts: string[]): string;
96
+ }
97
+ /** The context handed to custom commands; no Pulumi machinery is involved. */
98
+ export interface SandboxCommandContext {
99
+ readonly projectName: string;
100
+ readonly devId: string;
101
+ readonly stackName: string;
102
+ /** Arguments after the command verb. */
103
+ readonly argv: readonly string[];
104
+ physicalName(...parts: string[]): string;
105
+ }
106
+ /** A custom command line verb registered through {@link SandboxOptions.commands}. */
107
+ export interface SandboxCommand {
108
+ /** One-line description shown by `help`. */
109
+ description: string;
110
+ /** Runs the command; a returned number becomes the process exit code. */
111
+ run(context: SandboxCommandContext): number | void | Promise<number | void>;
112
+ }
113
+ /** Values returned by the program; they become the stack outputs. */
114
+ export type SandboxOutputs = Record<string, unknown>;
115
+ /** The infrastructure program: a plain Pulumi inline program with a sandbox context. */
116
+ export type SandboxProgram = (context: SandboxContext) => SandboxOutputs | void | Promise<SandboxOutputs | void>;
117
+ /**
118
+ * A fully wired sandbox: the Pulumi stack (file backend, work directory,
119
+ * passphrase) plus the lifecycle operations. Most programs never construct
120
+ * one directly — {@link sandbox} builds it and dispatches the command line —
121
+ * but it is the entry point for programmatic control and tests.
122
+ */
123
+ export declare class Sandbox {
124
+ #private;
125
+ readonly projectName: string;
126
+ readonly devId: string;
127
+ readonly stackName: string;
128
+ /** The underlying automation stack, for operations the lifecycle does not cover. */
129
+ readonly stack: automation.Stack;
130
+ /** @internal Use {@link createSandbox}. */
131
+ constructor(identity: SandboxIdentity, stack: automation.Stack, state: {
132
+ action: LifecycleAction;
133
+ }, containerHostedProviders: readonly string[]);
134
+ /** Runs one lifecycle action against the stack. */
135
+ run(action: LifecycleAction): Promise<void>;
136
+ create(): Promise<void>;
137
+ destroy(): Promise<void>;
138
+ reset(): Promise<void>;
139
+ preview(): Promise<void>;
140
+ cancel(): Promise<void>;
141
+ }
142
+ interface SandboxIdentity {
143
+ projectName: string;
144
+ devId: string;
145
+ stackName: string;
146
+ physicalName(...parts: string[]): string;
147
+ }
148
+ /**
149
+ * Builds a {@link Sandbox} without dispatching any action: resolves the
150
+ * developer identity, prepares the local backend and work directory, and
151
+ * creates or selects the stack with the inline program wired in.
152
+ */
153
+ export declare function createSandbox(options: SandboxOptions, program: SandboxProgram): Promise<Sandbox>;
154
+ /**
155
+ * The complete sandbox entry point: resolves the action from the command
156
+ * line, runs custom commands without touching Pulumi, drives the lifecycle
157
+ * for the rest, and renders every failure as a friendly message plus a
158
+ * non-zero exit code.
159
+ *
160
+ * ```typescript
161
+ * await sandbox({ name: "acme-shop" }, (context) => {
162
+ * // plain Pulumi resources — containers, databases, providers
163
+ * });
164
+ * ```
165
+ *
166
+ * Invoked as `node infra.ts <action>` with `create | destroy | reset |
167
+ * preview | cancel | outputs`, any custom command, `help`, or no action at
168
+ * all for the interactive menu.
169
+ */
170
+ export declare function sandbox(options: SandboxOptions, program: SandboxProgram): Promise<void>;
171
+ export {};
172
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,260 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { automation } from "@pulumi/pulumi";
4
+ import { ACTION_DESCRIPTIONS, LIFECYCLE_ACTIONS, isLifecycleAction } from "./actions.js";
5
+ import { ensureDirectories, fileBackendUrl, resolveDirectories } from "./backend.js";
6
+ import { SandboxConfigurationError, SandboxLockError } from "./errors.js";
7
+ import { resolveDevId } from "./identity.js";
8
+ import { runInteractiveMenu } from "./interactive.js";
9
+ import { cancelStack, createStack, destroyStack, previewStack, printOutputs } from "./lifecycle.js";
10
+ import { bold, cyan, dim, exitQuietlyOnClosedPipe, fail, heading, succeed } from "./terminal.js";
11
+ const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12
+ /** Default passphrase for the local secrets provider — see {@link SandboxOptions.passphrase}. */
13
+ export const DEFAULT_PASSPHRASE = "sandbox";
14
+ /**
15
+ * A fully wired sandbox: the Pulumi stack (file backend, work directory,
16
+ * passphrase) plus the lifecycle operations. Most programs never construct
17
+ * one directly — {@link sandbox} builds it and dispatches the command line —
18
+ * but it is the entry point for programmatic control and tests.
19
+ */
20
+ export class Sandbox {
21
+ projectName;
22
+ devId;
23
+ stackName;
24
+ /** The underlying automation stack, for operations the lifecycle does not cover. */
25
+ stack;
26
+ #host;
27
+ #state;
28
+ /** @internal Use {@link createSandbox}. */
29
+ constructor(identity, stack, state, containerHostedProviders) {
30
+ this.projectName = identity.projectName;
31
+ this.devId = identity.devId;
32
+ this.stackName = identity.stackName;
33
+ this.stack = stack;
34
+ this.#state = state;
35
+ this.#host = { stack, stackName: identity.stackName, containerHostedProviders };
36
+ }
37
+ /** Runs one lifecycle action against the stack. */
38
+ async run(action) {
39
+ switch (action) {
40
+ case "create":
41
+ this.#state.action = "create";
42
+ await createStack(this.#host);
43
+ succeed(`Sandbox ${this.stackName} is ready.`);
44
+ return;
45
+ case "destroy":
46
+ this.#state.action = "destroy";
47
+ await destroyStack(this.#host);
48
+ succeed(`Sandbox ${this.stackName} is gone.`);
49
+ return;
50
+ case "reset":
51
+ await this.run("destroy");
52
+ await this.run("create");
53
+ return;
54
+ case "preview":
55
+ this.#state.action = "preview";
56
+ await previewStack(this.#host);
57
+ return;
58
+ case "cancel":
59
+ await cancelStack(this.#host);
60
+ succeed("State lock released.");
61
+ return;
62
+ case "outputs":
63
+ await printOutputs(this.#host);
64
+ return;
65
+ }
66
+ }
67
+ create() {
68
+ return this.run("create");
69
+ }
70
+ destroy() {
71
+ return this.run("destroy");
72
+ }
73
+ reset() {
74
+ return this.run("reset");
75
+ }
76
+ preview() {
77
+ return this.run("preview");
78
+ }
79
+ cancel() {
80
+ return this.run("cancel");
81
+ }
82
+ }
83
+ function resolveIdentity(options) {
84
+ if (!PROJECT_NAME_PATTERN.test(options.name)) {
85
+ throw new SandboxConfigurationError(`The sandbox name "${options.name}" is not usable in stack and container names.`, ["Use lowercase letters, digits, and dashes, starting with a letter or digit (for example: acme-shop)."]);
86
+ }
87
+ const devId = resolveDevId({
88
+ devId: options.devId,
89
+ envFile: options.envFile,
90
+ require: options.requireDevId,
91
+ });
92
+ return {
93
+ projectName: options.name,
94
+ devId,
95
+ stackName: `${options.name}-${devId}`,
96
+ physicalName: (...parts) => [options.name, ...parts, devId].join("-"),
97
+ };
98
+ }
99
+ /**
100
+ * The default sandbox home: `.sandbox` in the package containing the entry
101
+ * script. Anchoring to the entry script rather than the working directory
102
+ * means `node tool/sandbox.ts destroy` targets the same state from any
103
+ * directory — a sandbox invoked from the wrong place must never conclude
104
+ * there is nothing to destroy.
105
+ */
106
+ function defaultHomeDirectory() {
107
+ const entry = process.argv[1];
108
+ let directory = entry !== undefined ? path.dirname(path.resolve(entry)) : process.cwd();
109
+ let current = directory;
110
+ while (true) {
111
+ if (fs.existsSync(path.join(current, "package.json"))) {
112
+ directory = current;
113
+ break;
114
+ }
115
+ const parent = path.dirname(current);
116
+ if (parent === current) {
117
+ break;
118
+ }
119
+ current = parent;
120
+ }
121
+ return path.join(directory, ".sandbox");
122
+ }
123
+ /**
124
+ * Builds a {@link Sandbox} without dispatching any action: resolves the
125
+ * developer identity, prepares the local backend and work directory, and
126
+ * creates or selects the stack with the inline program wired in.
127
+ */
128
+ export async function createSandbox(options, program) {
129
+ const identity = resolveIdentity(options);
130
+ const directories = resolveDirectories(options.homeDir ?? defaultHomeDirectory(), identity.projectName);
131
+ ensureDirectories(directories);
132
+ const backendUrl = options.backendUrl ?? fileBackendUrl(directories.state);
133
+ const passphrase = options.passphrase ?? process.env.PULUMI_CONFIG_PASSPHRASE ?? DEFAULT_PASSPHRASE;
134
+ const state = { action: "preview" };
135
+ const context = {
136
+ projectName: identity.projectName,
137
+ devId: identity.devId,
138
+ stackName: identity.stackName,
139
+ get action() {
140
+ return state.action;
141
+ },
142
+ physicalName: identity.physicalName,
143
+ };
144
+ try {
145
+ const stack = await automation.LocalWorkspace.createOrSelectStack({
146
+ stackName: identity.stackName,
147
+ projectName: identity.projectName,
148
+ program: async () => (await program(context)) ?? undefined,
149
+ }, {
150
+ workDir: directories.work,
151
+ projectSettings: {
152
+ name: identity.projectName,
153
+ runtime: "nodejs",
154
+ backend: { url: backendUrl },
155
+ },
156
+ envVars: { PULUMI_CONFIG_PASSPHRASE: passphrase },
157
+ });
158
+ return new Sandbox(identity, stack, state, options.containerHostedProviders ?? []);
159
+ }
160
+ catch (error) {
161
+ throw translateWorkspaceError(error);
162
+ }
163
+ }
164
+ /**
165
+ * The complete sandbox entry point: resolves the action from the command
166
+ * line, runs custom commands without touching Pulumi, drives the lifecycle
167
+ * for the rest, and renders every failure as a friendly message plus a
168
+ * non-zero exit code.
169
+ *
170
+ * ```typescript
171
+ * await sandbox({ name: "acme-shop" }, (context) => {
172
+ * // plain Pulumi resources — containers, databases, providers
173
+ * });
174
+ * ```
175
+ *
176
+ * Invoked as `node infra.ts <action>` with `create | destroy | reset |
177
+ * preview | cancel | outputs`, any custom command, `help`, or no action at
178
+ * all for the interactive menu.
179
+ */
180
+ export async function sandbox(options, program) {
181
+ exitQuietlyOnClosedPipe();
182
+ const argv = [...(options.argv ?? process.argv.slice(2))];
183
+ const verb = argv[0] ?? "interactive";
184
+ try {
185
+ if (verb === "help" || verb === "--help" || verb === "-h") {
186
+ printHelp(options);
187
+ return;
188
+ }
189
+ const command = options.commands !== undefined && Object.hasOwn(options.commands, verb) ? options.commands[verb] : undefined;
190
+ if (command !== undefined) {
191
+ const exitCode = await command.run({ ...resolveIdentity(options), argv: argv.slice(1) });
192
+ if (typeof exitCode === "number" && exitCode !== 0) {
193
+ process.exitCode = exitCode;
194
+ }
195
+ return;
196
+ }
197
+ if (verb !== "interactive" && !isLifecycleAction(verb)) {
198
+ throw new SandboxConfigurationError(`Unknown action "${verb}".`, [
199
+ `Available actions: ${[...LIFECYCLE_ACTIONS, ...Object.keys(options.commands ?? {})].join(", ")} — or "help".`,
200
+ ]);
201
+ }
202
+ const instance = await createSandbox(options, program);
203
+ // The `outputs` verb keeps stdout machine-readable: nothing but JSON.
204
+ if (verb !== "outputs") {
205
+ heading(instance.projectName, instance.stackName, verb);
206
+ }
207
+ if (verb === "interactive") {
208
+ await runInteractiveMenu({
209
+ run: (action) => instance.run(action),
210
+ reportFailure,
211
+ });
212
+ return;
213
+ }
214
+ await instance.run(verb);
215
+ }
216
+ catch (error) {
217
+ reportFailure(error);
218
+ process.exitCode = 1;
219
+ }
220
+ }
221
+ function printHelp(options) {
222
+ process.stdout.write(`\n${bold(options.name)} — local development sandbox\n\n`);
223
+ process.stdout.write(`Usage: node <entry-file> ${cyan("<action>")}\n\n`);
224
+ for (const action of LIFECYCLE_ACTIONS) {
225
+ process.stdout.write(` ${bold(action.padEnd(10))} ${dim(ACTION_DESCRIPTIONS[action])}\n`);
226
+ }
227
+ for (const [verb, command] of Object.entries(options.commands ?? {})) {
228
+ process.stdout.write(` ${bold(verb.padEnd(10))} ${dim(command.description)}\n`);
229
+ }
230
+ process.stdout.write(`\nWithout an action, an interactive menu opens.\n`);
231
+ }
232
+ function reportFailure(error) {
233
+ if (error instanceof SandboxConfigurationError) {
234
+ fail(error.message, error.remediation);
235
+ return;
236
+ }
237
+ if (error instanceof SandboxLockError) {
238
+ fail(error.message, ['Release it with the "cancel" action once you are sure no other update is running.']);
239
+ return;
240
+ }
241
+ const message = error instanceof Error ? error.message : String(error);
242
+ // Failures of Pulumi operations arrive as a dump that repeats everything
243
+ // already streamed live; the only new information is the error lines.
244
+ const errorLines = [...new Set(message.split("\n").filter((line) => line.trimStart().startsWith("error:")))];
245
+ if (errorLines.length > 0) {
246
+ fail("The Pulumi operation failed.", errorLines);
247
+ return;
248
+ }
249
+ fail(message);
250
+ }
251
+ function translateWorkspaceError(error) {
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ if (message.includes("ENOENT") && message.toLowerCase().includes("pulumi")) {
254
+ return new SandboxConfigurationError("The Pulumi CLI is not installed (or not on the PATH).", [
255
+ "Install it from https://www.pulumi.com/docs/install/ — no Pulumi account is needed,",
256
+ "the sandbox keeps its state in a local file backend.",
257
+ ]);
258
+ }
259
+ return error;
260
+ }
@@ -0,0 +1,34 @@
1
+ import type { automation } from "@pulumi/pulumi";
2
+ /** What a purge removed from the checkpoint. */
3
+ export interface ProviderPurge {
4
+ /** URNs of the matched provider resources. */
5
+ providerUrns: string[];
6
+ /** Resources removed, providers included. */
7
+ removedResources: number;
8
+ /** Pending operations removed alongside them. */
9
+ removedPendingOperations: number;
10
+ }
11
+ /**
12
+ * Removes the named provider — and every resource that references it — from
13
+ * a deployment body, in place. Returns what was removed so callers can log
14
+ * it or skip the state write when nothing matched.
15
+ *
16
+ * Surviving resources are sanitized as well: any `dependencies`,
17
+ * `propertyDependencies`, `parent`, or `deletedWith` reference to a purged
18
+ * resource is dropped, because the engine refuses to import a deployment
19
+ * that mentions missing resources.
20
+ *
21
+ * Provider resources live at URNs whose type segment ends in
22
+ * `pulumi:providers:<type>` — `parentType$pulumi:providers:<type>` when the
23
+ * provider is declared inside a component resource — followed by the
24
+ * resource name. The match is on that type and the name, never on the stack
25
+ * or project, so the same purge works against any backend layout.
26
+ */
27
+ export declare function removeProviderFromDeployment(deployment: unknown, providerName: string): ProviderPurge;
28
+ /**
29
+ * Exports the stack's state, removes the named provider and its dependents,
30
+ * and imports the filtered state back — only when something actually
31
+ * matched, so healthy stacks never see a state write.
32
+ */
33
+ export declare function purgeProviderFromState(stack: automation.Stack, providerName: string): Promise<ProviderPurge>;
34
+ //# sourceMappingURL=state-surgery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-surgery.d.ts","sourceRoot":"","sources":["../src/state-surgery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAsCjD,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,aAAa,CA6CrG;AAiCD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAOlH"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Removes the named provider — and every resource that references it — from
3
+ * a deployment body, in place. Returns what was removed so callers can log
4
+ * it or skip the state write when nothing matched.
5
+ *
6
+ * Surviving resources are sanitized as well: any `dependencies`,
7
+ * `propertyDependencies`, `parent`, or `deletedWith` reference to a purged
8
+ * resource is dropped, because the engine refuses to import a deployment
9
+ * that mentions missing resources.
10
+ *
11
+ * Provider resources live at URNs whose type segment ends in
12
+ * `pulumi:providers:<type>` — `parentType$pulumi:providers:<type>` when the
13
+ * provider is declared inside a component resource — followed by the
14
+ * resource name. The match is on that type and the name, never on the stack
15
+ * or project, so the same purge works against any backend layout.
16
+ */
17
+ export function removeProviderFromDeployment(deployment, providerName) {
18
+ const body = deployment;
19
+ const providerUrns = new Set((body.resources ?? [])
20
+ .filter((resource) => isProviderUrn(resource.urn, providerName))
21
+ .map((resource) => resource.urn));
22
+ const purge = {
23
+ providerUrns: [...providerUrns],
24
+ removedResources: 0,
25
+ removedPendingOperations: 0,
26
+ };
27
+ if (providerUrns.size === 0) {
28
+ return purge;
29
+ }
30
+ const managedByPurgedProvider = (resource) => {
31
+ if (providerUrns.has(resource.urn)) {
32
+ return true;
33
+ }
34
+ const reference = resource.provider;
35
+ return reference !== undefined && [...providerUrns].some((urn) => reference.startsWith(`${urn}::`));
36
+ };
37
+ const purgedUrns = new Set((body.resources ?? []).filter((resource) => managedByPurgedProvider(resource)).map((resource) => resource.urn));
38
+ if (body.resources) {
39
+ const before = body.resources.length;
40
+ body.resources = body.resources.filter((resource) => !purgedUrns.has(resource.urn));
41
+ purge.removedResources = before - body.resources.length;
42
+ for (const survivor of body.resources) {
43
+ dropReferencesTo(survivor, purgedUrns);
44
+ }
45
+ }
46
+ if (body.pendingOperations) {
47
+ const before = body.pendingOperations.length;
48
+ body.pendingOperations = body.pendingOperations.filter((operation) => !purgedUrns.has(operation.resource.urn) && !managedByPurgedProvider(operation.resource));
49
+ purge.removedPendingOperations = before - body.pendingOperations.length;
50
+ }
51
+ return purge;
52
+ }
53
+ /** Removes every reference a surviving resource holds to the purged URNs. */
54
+ function dropReferencesTo(resource, purgedUrns) {
55
+ if (resource.dependencies) {
56
+ resource.dependencies = resource.dependencies.filter((urn) => !purgedUrns.has(urn));
57
+ }
58
+ if (resource.propertyDependencies) {
59
+ for (const [property, urns] of Object.entries(resource.propertyDependencies)) {
60
+ if (urns) {
61
+ resource.propertyDependencies[property] = urns.filter((urn) => !purgedUrns.has(urn));
62
+ }
63
+ }
64
+ }
65
+ if (resource.parent !== undefined && purgedUrns.has(resource.parent)) {
66
+ delete resource.parent;
67
+ }
68
+ if (resource.deletedWith !== undefined && purgedUrns.has(resource.deletedWith)) {
69
+ delete resource.deletedWith;
70
+ }
71
+ }
72
+ function isProviderUrn(urn, providerName) {
73
+ const segments = urn.split("::");
74
+ if (segments[3] !== providerName) {
75
+ return false;
76
+ }
77
+ // The type segment is `$`-joined with the parent's qualified type when the
78
+ // provider is declared inside a component resource; the provider's own
79
+ // type is always the last element.
80
+ return segments[2]?.split("$").pop()?.startsWith("pulumi:providers:") === true;
81
+ }
82
+ /**
83
+ * Exports the stack's state, removes the named provider and its dependents,
84
+ * and imports the filtered state back — only when something actually
85
+ * matched, so healthy stacks never see a state write.
86
+ */
87
+ export async function purgeProviderFromState(stack, providerName) {
88
+ const exported = await stack.exportStack();
89
+ const purge = removeProviderFromDeployment(exported.deployment, providerName);
90
+ if (purge.removedResources + purge.removedPendingOperations > 0) {
91
+ await stack.importStack(exported);
92
+ }
93
+ return purge;
94
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal terminal presentation layer for the lifecycle dispatcher. Colors
3
+ * are applied only when standard output is an interactive terminal and the
4
+ * `NO_COLOR` convention is respected, so logs captured in files or CI stay
5
+ * clean. This module is internal to the library on purpose: programs should
6
+ * not depend on its exact output.
7
+ */
8
+ export declare const bold: (text: string) => string;
9
+ export declare const dim: (text: string) => string;
10
+ export declare const red: (text: string) => string;
11
+ export declare const green: (text: string) => string;
12
+ export declare const yellow: (text: string) => string;
13
+ export declare const cyan: (text: string) => string;
14
+ /** Whether the Pulumi CLI should be asked for colored output. */
15
+ export declare function colorMode(): "always" | "never";
16
+ /**
17
+ * Makes a closed pipe end the process quietly, the way Unix tools behave
18
+ * under `| head`: without this, Node raises a noisy unhandled EPIPE error.
19
+ * Installed by the command line entry point only — a library consumer's
20
+ * process is not touched.
21
+ */
22
+ export declare function exitQuietlyOnClosedPipe(): void;
23
+ /** Opening line of every run: project, stack, and the action about to run. */
24
+ export declare function heading(project: string, stackName: string, action: string): void;
25
+ /** A lifecycle step about to start, e.g. "refreshing and updating the stack". */
26
+ export declare function step(message: string): void;
27
+ /** A successfully completed lifecycle action. */
28
+ export declare function succeed(message: string): void;
29
+ /** A non-fatal problem the run recovered from or chose to ignore. */
30
+ export declare function warn(message: string): void;
31
+ /** A fatal problem, followed by optional remediation lines. */
32
+ export declare function fail(message: string, remediation?: readonly string[]): void;
33
+ /** Secondary detail under a heading or step. */
34
+ export declare function detail(message: string): void;
35
+ //# sourceMappingURL=terminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,eAAO,MAAM,IAAI,SAJmC,MAAM,KAAK,MAI/B,CAAC;AACjC,eAAO,MAAM,GAAG,SALoC,MAAM,KAAK,MAKhC,CAAC;AAChC,eAAO,MAAM,GAAG,SANoC,MAAM,KAAK,MAM/B,CAAC;AACjC,eAAO,MAAM,KAAK,SAPkC,MAAM,KAAK,MAO7B,CAAC;AACnC,eAAO,MAAM,MAAM,SARiC,MAAM,KAAK,MAQ5B,CAAC;AACpC,eAAO,MAAM,IAAI,SATmC,MAAM,KAAK,MAS9B,CAAC;AAElC,iEAAiE;AACjE,wBAAgB,SAAS,IAAI,QAAQ,GAAG,OAAO,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAS9C;AAED,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,iFAAiF;AACjF,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,iDAAiD;AACjD,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,qEAAqE;AACrE,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,+DAA+D;AAC/D,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,GAAE,SAAS,MAAM,EAAO,GAAG,IAAI,CAQ/E;AAED,gDAAgD;AAChD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5C"}