@launchfile/macos-dev 0.2.0 → 0.4.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 (55) hide show
  1. package/dist/bootstrap.d.ts +56 -8
  2. package/dist/bootstrap.js +170 -93
  3. package/dist/env-writer.d.ts +40 -11
  4. package/dist/env-writer.js +84 -23
  5. package/dist/health.d.ts +8 -2
  6. package/dist/health.js +11 -17
  7. package/dist/prereqs.js +2 -2
  8. package/dist/process-manager.d.ts +13 -0
  9. package/dist/process-manager.js +59 -3
  10. package/dist/process-stopper.d.ts +99 -0
  11. package/dist/process-stopper.js +165 -0
  12. package/dist/provider.d.ts +50 -0
  13. package/dist/provider.js +337 -28
  14. package/dist/redact.d.ts +55 -0
  15. package/dist/redact.js +92 -0
  16. package/dist/resources/identifiers.d.ts +22 -0
  17. package/dist/resources/identifiers.js +31 -0
  18. package/dist/resources/index.d.ts +3 -3
  19. package/dist/resources/index.js +21 -10
  20. package/dist/resources/mysql.d.ts +3 -1
  21. package/dist/resources/mysql.js +49 -11
  22. package/dist/resources/postgres.d.ts +4 -1
  23. package/dist/resources/postgres.js +63 -17
  24. package/dist/resources/redis.d.ts +3 -1
  25. package/dist/resources/redis.js +14 -4
  26. package/dist/resources/sqlite.js +2 -2
  27. package/dist/resources/types.d.ts +21 -3
  28. package/dist/runtimes/bun.js +2 -2
  29. package/dist/runtimes/installed-versions.d.ts +12 -0
  30. package/dist/runtimes/installed-versions.js +21 -0
  31. package/dist/runtimes/node.js +13 -8
  32. package/dist/runtimes/python.js +14 -7
  33. package/dist/runtimes/ruby.js +13 -8
  34. package/dist/secret-generator.js +11 -3
  35. package/dist/shell.d.ts +28 -6
  36. package/dist/shell.js +83 -28
  37. package/dist/state.d.ts +38 -0
  38. package/dist/state.js +12 -1
  39. package/dist/storage.d.ts +16 -3
  40. package/dist/storage.js +23 -6
  41. package/package.json +7 -4
  42. package/dist/__tests__/bootstrap.test.d.ts +0 -2
  43. package/dist/__tests__/bootstrap.test.js +0 -90
  44. package/dist/__tests__/dry-run.test.d.ts +0 -2
  45. package/dist/__tests__/dry-run.test.js +0 -141
  46. package/dist/__tests__/env-writer.test.d.ts +0 -2
  47. package/dist/__tests__/env-writer.test.js +0 -201
  48. package/dist/__tests__/lockfile-detect.test.d.ts +0 -2
  49. package/dist/__tests__/lockfile-detect.test.js +0 -75
  50. package/dist/__tests__/port-allocator.test.d.ts +0 -2
  51. package/dist/__tests__/port-allocator.test.js +0 -53
  52. package/dist/__tests__/secret-generator.test.d.ts +0 -2
  53. package/dist/__tests__/secret-generator.test.js +0 -26
  54. package/dist/__tests__/state.test.d.ts +0 -2
  55. package/dist/__tests__/state.test.js +0 -30
@@ -6,13 +6,21 @@
6
6
  * stdout via regex patterns, re-runnable, and reports failures rather than
7
7
  * deploy-failing. Spec: /spec/SPEC.md § Bootstrap stage.
8
8
  *
9
- * The command is split into argv via whitespace and run through spawn()
10
- * with shell:false to avoid shell-injection exposure. This means shell
11
- * metacharacters (pipes, redirects, &&, quoted args with spaces) are not
12
- * supported — apps that need shell features should wrap them in an
13
- * image-level script and invoke that script.
9
+ * The command runs through a POSIX shell (SPEC.md § Command interpretation,
10
+ * PROVIDERS.md §10 item 11), so `&&`, `;`, pipes, redirection, grouping and
11
+ * variable expansion behave as authors write them. It is passed as a single
12
+ * argv element to `/bin/sh -c` via spawn({ shell: false }) — it is never
13
+ * interpolated into a command string on this side, so the only interpreter
14
+ * that sees it is the `sh` this provider spawns.
14
15
  */
15
- import { type CaptureEntry } from "@launchfile/sdk";
16
+ import { parseDurationMs, type CaptureEntry, type NormalizedLaunch, type ResolverContext } from "@launchfile/sdk";
17
+ /** Default budget for a bootstrap command when no `timeout` is declared. */
18
+ export declare const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 120000;
19
+ /**
20
+ * The interpreter every bootstrap command runs under. `/bin/sh` is the POSIX
21
+ * shell guaranteed present on macOS.
22
+ */
23
+ export declare const BOOTSTRAP_SHELL = "/bin/sh";
16
24
  /**
17
25
  * Result of running one bootstrap command. Captures may be empty even on
18
26
  * success (the command may not produce matching output), and may be
@@ -38,8 +46,47 @@ export interface BootstrapResult {
38
46
  * Exported for unit testing.
39
47
  */
40
48
  export declare function extractCaptures(stdout: string, captures: Record<string, CaptureEntry>): Record<string, string>;
41
- /** Parse a simple duration string like "5m", "30s", "1h" into milliseconds. Exported for unit testing. */
42
- export declare function parseDuration(s: string): number;
49
+ /**
50
+ * Parse a simple duration string like "5m", "30s", "1h" into milliseconds
51
+ * using the ratified grammar (D-48). Throws on an unparseable value —
52
+ * PROVIDERS.md §10.10 forbids silently substituting a default. Re-exported
53
+ * for unit testing.
54
+ */
55
+ export declare const parseDuration: typeof parseDurationMs;
56
+ /** One planned bootstrap execution, in component declaration order. */
57
+ export interface BootstrapPlanItem {
58
+ component: string;
59
+ /** The $-resolved command string. */
60
+ command: string;
61
+ /** `["/bin/sh", "-c", command]` — the command as a single argv element. */
62
+ argv: string[];
63
+ timeoutMs: number;
64
+ capture?: Record<string, CaptureEntry>;
65
+ /**
66
+ * Set when the item cannot run at all (empty command, unparseable
67
+ * timeout). Bootstrap failures are reported, never thrown
68
+ * (SPEC.md § Failure semantics), so a bad item stays in the plan and
69
+ * carries its own diagnosis.
70
+ */
71
+ error?: string;
72
+ }
73
+ /**
74
+ * Build the bootstrap plan for a launch. Pure — no I/O — so selection,
75
+ * expression resolution, argv shape and timeout handling are unit-testable.
76
+ */
77
+ export declare function planBootstraps(launch: NormalizedLaunch, context: ResolverContext, opts?: {
78
+ component?: string;
79
+ }): BootstrapPlanItem[];
80
+ /** Minimal exec contract so tests can inject a fake runner. */
81
+ export type BootstrapExec = (cmd: string, args: string[], opts: {
82
+ cwd: string;
83
+ env: Record<string, string>;
84
+ timeoutMs: number;
85
+ }) => Promise<{
86
+ exitCode: number;
87
+ stdout: string;
88
+ stderr: string;
89
+ }>;
43
90
  /**
44
91
  * Public entry point for `launch bootstrap`. Loads the Launchfile, rebuilds
45
92
  * the resolver context from persisted state (so $app.url resolves to the
@@ -54,5 +101,6 @@ export declare function parseDuration(s: string): number;
54
101
  export declare function launchBootstrap(opts?: {
55
102
  component?: string;
56
103
  projectDir?: string;
104
+ exec?: BootstrapExec;
57
105
  }): Promise<BootstrapResult[]>;
58
106
  //# sourceMappingURL=bootstrap.d.ts.map
package/dist/bootstrap.js CHANGED
@@ -6,19 +6,28 @@
6
6
  * stdout via regex patterns, re-runnable, and reports failures rather than
7
7
  * deploy-failing. Spec: /spec/SPEC.md § Bootstrap stage.
8
8
  *
9
- * The command is split into argv via whitespace and run through spawn()
10
- * with shell:false to avoid shell-injection exposure. This means shell
11
- * metacharacters (pipes, redirects, &&, quoted args with spaces) are not
12
- * supported — apps that need shell features should wrap them in an
13
- * image-level script and invoke that script.
9
+ * The command runs through a POSIX shell (SPEC.md § Command interpretation,
10
+ * PROVIDERS.md §10 item 11), so `&&`, `;`, pipes, redirection, grouping and
11
+ * variable expansion behave as authors write them. It is passed as a single
12
+ * argv element to `/bin/sh -c` via spawn({ shell: false }) — it is never
13
+ * interpolated into a command string on this side, so the only interpreter
14
+ * that sees it is the `sh` this provider spawns.
14
15
  */
15
16
  import { readFile } from "node:fs/promises";
16
17
  import { join } from "node:path";
17
18
  import { spawn } from "node:child_process";
18
- import { readLaunch, resolveExpression, } from "@launchfile/sdk";
19
- import { loadState } from "./state.js";
19
+ import { parseDurationMs, readLaunch, resolveExpression, } from "@launchfile/sdk";
20
+ import { loadState, saveState } from "./state.js";
20
21
  import { buildResolverContext, computeAppProperties, resolveComponentEnv, resolveGenerators, } from "./env-writer.js";
22
+ import { redactSecrets } from "./redact.js";
21
23
  import { getProvisioner } from "./resources/index.js";
24
+ /** Default budget for a bootstrap command when no `timeout` is declared. */
25
+ export const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 120_000;
26
+ /**
27
+ * The interpreter every bootstrap command runs under. `/bin/sh` is the POSIX
28
+ * shell guaranteed present on macOS.
29
+ */
30
+ export const BOOTSTRAP_SHELL = "/bin/sh";
22
31
  /**
23
32
  * Strip ANSI escape sequences from captured stdout before regex matching.
24
33
  * CLI tools that detect a TTY will emit color codes that would otherwise
@@ -51,73 +60,117 @@ export function extractCaptures(stdout, captures) {
51
60
  }
52
61
  return result;
53
62
  }
54
- /** Parse a simple duration string like "5m", "30s", "1h" into milliseconds. Exported for unit testing. */
55
- export function parseDuration(s) {
56
- const match = /^(\d+)\s*(ms|s|m|h)$/.exec(s.trim());
57
- if (!match)
58
- return 120_000;
59
- const n = Number.parseInt(match[1], 10);
60
- switch (match[2]) {
61
- case "ms": return n;
62
- case "s": return n * 1000;
63
- case "m": return n * 60 * 1000;
64
- case "h": return n * 60 * 60 * 1000;
65
- default: return 120_000;
63
+ /**
64
+ * Parse a simple duration string like "5m", "30s", "1h" into milliseconds
65
+ * using the ratified grammar (D-48). Throws on an unparseable value —
66
+ * PROVIDERS.md §10.10 forbids silently substituting a default. Re-exported
67
+ * for unit testing.
68
+ */
69
+ export const parseDuration = parseDurationMs;
70
+ /**
71
+ * Build the bootstrap plan for a launch. Pure — no I/O — so selection,
72
+ * expression resolution, argv shape and timeout handling are unit-testable.
73
+ */
74
+ export function planBootstraps(launch, context, opts = {}) {
75
+ const plan = [];
76
+ for (const [name, component] of Object.entries(launch.components)) {
77
+ if (opts.component && name !== opts.component)
78
+ continue;
79
+ // `bootstrap` is mode-invariant (D-38): the same command runs from
80
+ // source or artifact. A path or binary that differs by mode belongs in
81
+ // storage:/env/PATH, not a separate command.
82
+ const bootstrap = component.commands?.bootstrap;
83
+ if (!bootstrap)
84
+ continue;
85
+ // Resolve $-expressions in the command string (e.g. $app.url) at
86
+ // invocation time. Bootstrap runs after start, so the resolved URL
87
+ // already reflects the actual allocated port. A `$$` escape becomes a
88
+ // literal `$` here and reaches the shell intact (SPEC.md § References).
89
+ const command = resolveExpression(bootstrap.command, context);
90
+ const base = {
91
+ component: name,
92
+ command,
93
+ // `/bin/sh -c <command>` — one argv element, so the shell interprets
94
+ // the command and nothing else (SPEC.md § Command interpretation).
95
+ argv: [BOOTSTRAP_SHELL, "-c", command],
96
+ capture: bootstrap.capture,
97
+ };
98
+ if (command.trim() === "") {
99
+ plan.push({
100
+ ...base,
101
+ timeoutMs: DEFAULT_BOOTSTRAP_TIMEOUT_MS,
102
+ error: "empty command",
103
+ });
104
+ continue;
105
+ }
106
+ let timeoutMs = DEFAULT_BOOTSTRAP_TIMEOUT_MS;
107
+ if (bootstrap.timeout !== undefined) {
108
+ try {
109
+ timeoutMs = parseDuration(bootstrap.timeout);
110
+ }
111
+ catch (err) {
112
+ // An unparseable timeout is surfaced, never silently replaced
113
+ // with a default (PROVIDERS.md §10.10).
114
+ plan.push({
115
+ ...base,
116
+ timeoutMs: DEFAULT_BOOTSTRAP_TIMEOUT_MS,
117
+ error: err instanceof Error ? err.message : String(err),
118
+ });
119
+ continue;
120
+ }
121
+ }
122
+ plan.push({ ...base, timeoutMs });
66
123
  }
124
+ return plan;
67
125
  }
68
126
  /**
69
- * Run one command using argv-split (no-shell) execution. Returns the
127
+ * Run one command. `cmd`/`args` cross the process boundary as an argv vector
128
+ * with shell:false — the command string is a single element of it, never
129
+ * interpolated into a string another shell would re-parse. Returns the
70
130
  * structured result; does not throw on command failure.
71
131
  */
72
- async function runOnce(command, opts) {
73
- const parts = command.trim().split(/\s+/).filter(Boolean);
74
- if (parts.length === 0) {
75
- return { exitCode: 1, stdout: "", stderr: "empty command" };
76
- }
77
- const [file, ...args] = parts;
78
- return new Promise((resolveP) => {
79
- const child = spawn(file, args, {
80
- cwd: opts.cwd,
81
- env: { ...process.env, ...opts.env },
82
- shell: false,
83
- stdio: ["ignore", "pipe", "pipe"],
84
- });
85
- let stdout = "";
86
- let stderr = "";
87
- let settled = false;
88
- const timer = setTimeout(() => {
89
- if (!settled) {
90
- settled = true;
91
- child.kill("SIGTERM");
92
- resolveP({
93
- exitCode: 124,
94
- stdout,
95
- stderr: stderr + `\n(killed after ${opts.timeoutMs}ms timeout)`,
96
- });
97
- }
98
- }, opts.timeoutMs);
99
- child.stdout?.on("data", (chunk) => {
100
- stdout += String(chunk);
101
- });
102
- child.stderr?.on("data", (chunk) => {
103
- stderr += String(chunk);
104
- });
105
- child.on("error", (err) => {
106
- if (!settled) {
107
- settled = true;
108
- clearTimeout(timer);
109
- resolveP({ exitCode: 1, stdout, stderr: `${stderr}\n${err.message}` });
110
- }
111
- });
112
- child.on("close", (code) => {
113
- if (!settled) {
114
- settled = true;
115
- clearTimeout(timer);
116
- resolveP({ exitCode: code ?? 1, stdout, stderr });
117
- }
118
- });
132
+ const defaultExec = (cmd, args, opts) => new Promise((resolveP) => {
133
+ const child = spawn(cmd, args, {
134
+ cwd: opts.cwd,
135
+ env: { ...process.env, ...opts.env },
136
+ shell: false,
137
+ stdio: ["ignore", "pipe", "pipe"],
119
138
  });
120
- }
139
+ let stdout = "";
140
+ let stderr = "";
141
+ let settled = false;
142
+ const timer = setTimeout(() => {
143
+ if (!settled) {
144
+ settled = true;
145
+ child.kill("SIGTERM");
146
+ resolveP({
147
+ exitCode: 124,
148
+ stdout,
149
+ stderr: stderr + `\n(killed after ${opts.timeoutMs}ms timeout)`,
150
+ });
151
+ }
152
+ }, opts.timeoutMs);
153
+ child.stdout?.on("data", (chunk) => {
154
+ stdout += String(chunk);
155
+ });
156
+ child.stderr?.on("data", (chunk) => {
157
+ stderr += String(chunk);
158
+ });
159
+ child.on("error", (err) => {
160
+ if (!settled) {
161
+ settled = true;
162
+ clearTimeout(timer);
163
+ resolveP({ exitCode: 1, stdout, stderr: `${stderr}\n${err.message}` });
164
+ }
165
+ });
166
+ child.on("close", (code) => {
167
+ if (!settled) {
168
+ settled = true;
169
+ clearTimeout(timer);
170
+ resolveP({ exitCode: code ?? 1, stdout, stderr });
171
+ }
172
+ });
173
+ });
121
174
  /**
122
175
  * Public entry point for `launch bootstrap`. Loads the Launchfile, rebuilds
123
176
  * the resolver context from persisted state (so $app.url resolves to the
@@ -154,50 +207,74 @@ export async function launchBootstrap(opts = {}) {
154
207
  }
155
208
  const appProperties = computeAppProperties(launch, state.ports);
156
209
  const context = buildResolverContext(resourceMap, state.ports, state.secrets, appProperties);
210
+ const exec = opts.exec ?? defaultExec;
211
+ const plan = planBootstraps(launch, context, { component: opts.component });
157
212
  const results = [];
158
- for (const [name, component] of Object.entries(launch.components)) {
159
- if (opts.component && name !== opts.component)
160
- continue;
161
- const bootstrap = component.commands?.bootstrap;
162
- if (!bootstrap)
213
+ for (const item of plan) {
214
+ const name = item.component;
215
+ // Bootstrap failures are reported, not thrown (SPEC.md \u00a7 Failure
216
+ // semantics) \u2014 an unparseable timeout is surfaced the same way,
217
+ // never silently replaced with a default (PROVIDERS.md \u00a710.10).
218
+ if (item.error) {
219
+ console.error(` \u2717 Bootstrap [${name}]: ${item.error}`);
220
+ results.push({
221
+ component: name,
222
+ command: item.command,
223
+ ok: false,
224
+ exitCode: 1,
225
+ captures: {},
226
+ captureMeta: item.capture ?? {},
227
+ stdout: "",
228
+ stderr: item.error,
229
+ });
163
230
  continue;
164
- // Resolve $-expressions in the command string (e.g. $app.url) at
165
- // invocation time. Bootstrap runs after start, so the resolved URL
166
- // already reflects the actual allocated port.
167
- const resolvedCommand = resolveExpression(bootstrap.command, context);
231
+ }
168
232
  // Resolve env vars so the subprocess gets the same environment as
169
- // the running component.
170
- const env = resolveComponentEnv(component, context, resourceMap);
171
- await resolveGenerators(component, env);
233
+ // the running component. Minted generator values come from the store
234
+ // `up` persists (D-49); a value minted here (a generator declared
235
+ // after the last `up`) is persisted before the command runs, so
236
+ // `up`/`env`/`bootstrap` keep agreeing on it.
237
+ const component = launch.components[name];
238
+ const { env, unsupplied } = resolveComponentEnv(component, context, resourceMap);
239
+ const minted = await resolveGenerators(component, env, name, (state.generatedEnv ??= {}));
240
+ if (minted)
241
+ await saveState(projectDir, state);
242
+ // `up` took its `required:` values from the launching environment; read the
243
+ // same channel so bootstrap really does see the running component's env.
244
+ for (const { key } of unsupplied) {
245
+ const supplied = process.env[key];
246
+ if (supplied !== undefined)
247
+ env[key] = supplied;
248
+ }
172
249
  const port = state.ports[name];
173
250
  if (port && !env.PORT)
174
251
  env.PORT = String(port);
175
252
  console.log(`\n \u2193 Bootstrap [${name}]`);
176
- console.log(` $ ${resolvedCommand}`);
177
- const { exitCode, stdout, stderr } = await runOnce(resolvedCommand, {
253
+ // `$secrets.*` / `$<resource>.password` / `$<resource>.url` resolve to live
254
+ // credentials here, so the echoed command is scrubbed (CWE-532).
255
+ console.log(` $ ${redactSecrets(item.command)}`);
256
+ const [file, ...args] = item.argv;
257
+ const { exitCode, stdout, stderr } = await exec(file, args, {
178
258
  cwd: projectDir,
179
259
  env,
180
- timeoutMs: bootstrap.timeout ? parseDuration(bootstrap.timeout) : 120_000,
260
+ timeoutMs: item.timeoutMs,
181
261
  });
182
- const captures = bootstrap.capture
183
- ? extractCaptures(stdout, bootstrap.capture)
184
- : {};
185
- const result = {
262
+ const captures = item.capture ? extractCaptures(stdout, item.capture) : {};
263
+ results.push({
186
264
  component: name,
187
- command: resolvedCommand,
265
+ command: item.command,
188
266
  ok: exitCode === 0,
189
267
  exitCode,
190
268
  captures,
191
- captureMeta: bootstrap.capture ?? {},
269
+ captureMeta: item.capture ?? {},
192
270
  stdout,
193
271
  stderr,
194
- };
195
- results.push(result);
272
+ });
196
273
  // Print captures inline so the user sees them immediately.
197
274
  if (Object.keys(captures).length > 0) {
198
275
  console.log("\n Captured:");
199
276
  for (const [key, value] of Object.entries(captures)) {
200
- const meta = bootstrap.capture?.[key];
277
+ const meta = item.capture?.[key];
201
278
  const displayValue = meta?.sensitive ? "***" : value;
202
279
  const desc = meta?.description ? ` — ${meta.description}` : "";
203
280
  console.log(` ${key}: ${displayValue}${desc}`);
@@ -206,7 +283,7 @@ export async function launchBootstrap(opts = {}) {
206
283
  if (exitCode !== 0) {
207
284
  console.error(` \u2717 Bootstrap [${name}] failed with exit code ${exitCode}`);
208
285
  if (stderr)
209
- console.error(stderr);
286
+ console.error(redactSecrets(stderr));
210
287
  }
211
288
  else {
212
289
  console.log(` \u2713 Bootstrap [${name}] complete`);
@@ -4,14 +4,17 @@
4
4
  * Connects provisioned resource properties to the SDK's expression resolver,
5
5
  * then writes the results to .env files.
6
6
  */
7
- import { type NormalizedComponent, type NormalizedLaunch, type ResolverContext, type Secret } from "@launchfile/sdk";
7
+ import { type NormalizedComponent, type NormalizedLaunch, type ResolverContext, type Secret, type UnsuppliedRequiredEnv } from "@launchfile/sdk";
8
8
  import type { ResourceProperties } from "./resources/types.js";
9
- export type { ResolverContext };
9
+ export type { ResolverContext, UnsuppliedRequiredEnv };
10
10
  /**
11
- * Compute the $app.* property set for a Launchfile under the macos-dev
12
- * provider. The app's "primary" port comes from the first component (in
13
- * declaration order) that has at least one `exposed: true` provides entry.
14
- * Apps with no exposed component get `port: 0` and `url: ""`.
11
+ * Compute the $app.* property set (D-33, D-35) for a Launchfile under the
12
+ * macos-dev provider. The app's "primary" port comes from the first component
13
+ * (in declaration order) that has at least one `exposed: true` provides entry.
14
+ * The `authority`/`scheme`/`tls` trio is derived from the resulting URL via the
15
+ * SDK so split-field tokens (e.g. `CMD_DOMAIN: $app.authority`) resolve.
16
+ * Apps with no exposed component get `port: 0` and `url: ""` (and empty
17
+ * authority/scheme/tls).
15
18
  *
16
19
  * For multi-exposed-component apps that need a specific component's URL,
17
20
  * use `$components.<name>.url` instead — `$app.*` always points at the
@@ -23,19 +26,45 @@ export declare function computeAppProperties(launch: NormalizedLaunch, component
23
26
  * secrets, and (D-33) the platform-injected app properties.
24
27
  */
25
28
  export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>): ResolverContext;
29
+ /**
30
+ * The resolved environment for one component, plus what the file did not supply.
31
+ */
32
+ export interface ComponentEnvResult {
33
+ /** Keys that actually arrived. An unsupplied `required:` key is ABSENT, never `""`. */
34
+ env: Record<string, string>;
35
+ /**
36
+ * `required:` variables with no `generator:`, no `default:`, and no
37
+ * `set_env:` binding that injected (D-52, PROVIDERS.md §10 rule 8). The
38
+ * caller decides: `up` reads its operator channel then fails by name, `env`
39
+ * reports them without breaking `eval`.
40
+ */
41
+ unsupplied: UnsuppliedRequiredEnv[];
42
+ }
26
43
  /**
27
44
  * Resolve all environment variables for a single component.
28
45
  */
29
- export declare function resolveComponentEnv(component: NormalizedComponent, context: ResolverContext, resourceMap: Record<string, ResourceProperties>): Record<string, string>;
46
+ export declare function resolveComponentEnv(component: NormalizedComponent, context: ResolverContext, resourceMap: Record<string, ResourceProperties>, storage?: Record<string, Record<string, string>>): ComponentEnvResult;
30
47
  /**
31
48
  * Generate all app-wide secrets, reusing values from state when available.
32
49
  */
33
50
  export declare function generateSecrets(secretDefs: Record<string, Secret> | undefined, existingSecrets: Record<string, string>): Promise<Record<string, string>>;
34
51
  /**
35
- * Generate values for env vars that have generators.
36
- * Mutates the env record in place.
52
+ * Resolve values for env vars that declare generators, preserving minted
53
+ * values across runs (D-49: generate once, then preserve).
54
+ *
55
+ * A `secret` or `uuid` value is read from `generatedEnv` when present, and
56
+ * minted and written into it when absent. The store is keyed
57
+ * `<component>.<ENV_NAME>` — one entry per declaration (D-25), so two
58
+ * components declaring the same variable name hold independent values.
59
+ * `generator: port` is exempt: ports have their own preserved home
60
+ * (`state.ports`) and allocator, and a preserved port produces a bind
61
+ * conflict rather than continuity.
62
+ *
63
+ * Mutates `env` and `generatedEnv` in place. Returns true when a new value
64
+ * was minted into `generatedEnv` — the caller must then persist the state
65
+ * before handing the value to anything, so every site that mints persists.
37
66
  */
38
- export declare function resolveGenerators(component: NormalizedComponent, env: Record<string, string>): Promise<void>;
67
+ export declare function resolveGenerators(component: NormalizedComponent, env: Record<string, string>, componentName: string, generatedEnv: Record<string, string>): Promise<boolean>;
39
68
  /**
40
69
  * Write resolved env vars to a .env file.
41
70
  */
@@ -45,5 +74,5 @@ export declare function writeEnvFile(filePath: string, env: Record<string, strin
45
74
  * Single-component → .env.local at project root.
46
75
  * Multi-component → .launchfile/env/<component>.env per component.
47
76
  */
48
- export declare function writeAllEnvFiles(launch: NormalizedLaunch, context: ResolverContext, resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, projectDir: string): Promise<Record<string, Record<string, string>>>;
77
+ export declare function writeAllEnvFiles(launch: NormalizedLaunch, context: ResolverContext, resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, projectDir: string, generatedEnv: Record<string, string>): Promise<Record<string, Record<string, string>>>;
49
78
  //# sourceMappingURL=env-writer.d.ts.map