@launchfile/macos-dev 0.3.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.
- package/dist/bootstrap.d.ts +56 -8
- package/dist/bootstrap.js +170 -96
- package/dist/env-writer.d.ts +33 -7
- package/dist/env-writer.js +66 -14
- package/dist/health.d.ts +8 -2
- package/dist/health.js +11 -17
- package/dist/prereqs.js +2 -2
- package/dist/process-manager.js +2 -1
- package/dist/provider.d.ts +39 -7
- package/dist/provider.js +214 -29
- package/dist/redact.d.ts +55 -0
- package/dist/redact.js +92 -0
- package/dist/resources/identifiers.d.ts +22 -0
- package/dist/resources/identifiers.js +31 -0
- package/dist/resources/index.d.ts +3 -3
- package/dist/resources/index.js +21 -10
- package/dist/resources/mysql.d.ts +3 -1
- package/dist/resources/mysql.js +49 -11
- package/dist/resources/postgres.d.ts +4 -1
- package/dist/resources/postgres.js +63 -17
- package/dist/resources/redis.d.ts +3 -1
- package/dist/resources/redis.js +14 -4
- package/dist/resources/sqlite.js +2 -2
- package/dist/resources/types.d.ts +21 -3
- package/dist/runtimes/bun.js +2 -2
- package/dist/runtimes/installed-versions.d.ts +12 -0
- package/dist/runtimes/installed-versions.js +21 -0
- package/dist/runtimes/node.js +13 -8
- package/dist/runtimes/python.js +14 -7
- package/dist/runtimes/ruby.js +13 -8
- package/dist/secret-generator.js +11 -3
- package/dist/shell.d.ts +28 -6
- package/dist/shell.js +83 -28
- package/dist/state.d.ts +10 -0
- package/dist/state.js +12 -1
- package/package.json +6 -3
package/dist/bootstrap.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
42
|
-
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,53 +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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
});
|
|
166
230
|
continue;
|
|
167
|
-
|
|
168
|
-
// invocation time. Bootstrap runs after start, so the resolved URL
|
|
169
|
-
// already reflects the actual allocated port.
|
|
170
|
-
const resolvedCommand = resolveExpression(bootstrap.command, context);
|
|
231
|
+
}
|
|
171
232
|
// Resolve env vars so the subprocess gets the same environment as
|
|
172
|
-
// the running component.
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
175
249
|
const port = state.ports[name];
|
|
176
250
|
if (port && !env.PORT)
|
|
177
251
|
env.PORT = String(port);
|
|
178
252
|
console.log(`\n \u2193 Bootstrap [${name}]`);
|
|
179
|
-
|
|
180
|
-
|
|
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, {
|
|
181
258
|
cwd: projectDir,
|
|
182
259
|
env,
|
|
183
|
-
timeoutMs:
|
|
260
|
+
timeoutMs: item.timeoutMs,
|
|
184
261
|
});
|
|
185
|
-
const captures =
|
|
186
|
-
|
|
187
|
-
: {};
|
|
188
|
-
const result = {
|
|
262
|
+
const captures = item.capture ? extractCaptures(stdout, item.capture) : {};
|
|
263
|
+
results.push({
|
|
189
264
|
component: name,
|
|
190
|
-
command:
|
|
265
|
+
command: item.command,
|
|
191
266
|
ok: exitCode === 0,
|
|
192
267
|
exitCode,
|
|
193
268
|
captures,
|
|
194
|
-
captureMeta:
|
|
269
|
+
captureMeta: item.capture ?? {},
|
|
195
270
|
stdout,
|
|
196
271
|
stderr,
|
|
197
|
-
};
|
|
198
|
-
results.push(result);
|
|
272
|
+
});
|
|
199
273
|
// Print captures inline so the user sees them immediately.
|
|
200
274
|
if (Object.keys(captures).length > 0) {
|
|
201
275
|
console.log("\n Captured:");
|
|
202
276
|
for (const [key, value] of Object.entries(captures)) {
|
|
203
|
-
const meta =
|
|
277
|
+
const meta = item.capture?.[key];
|
|
204
278
|
const displayValue = meta?.sensitive ? "***" : value;
|
|
205
279
|
const desc = meta?.description ? ` — ${meta.description}` : "";
|
|
206
280
|
console.log(` ${key}: ${displayValue}${desc}`);
|
|
@@ -209,7 +283,7 @@ export async function launchBootstrap(opts = {}) {
|
|
|
209
283
|
if (exitCode !== 0) {
|
|
210
284
|
console.error(` \u2717 Bootstrap [${name}] failed with exit code ${exitCode}`);
|
|
211
285
|
if (stderr)
|
|
212
|
-
console.error(stderr);
|
|
286
|
+
console.error(redactSecrets(stderr));
|
|
213
287
|
}
|
|
214
288
|
else {
|
|
215
289
|
console.log(` \u2713 Bootstrap [${name}] complete`);
|
package/dist/env-writer.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
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
11
|
* Compute the $app.* property set (D-33, D-35) for a Launchfile under the
|
|
12
12
|
* macos-dev provider. The app's "primary" port comes from the first component
|
|
@@ -26,19 +26,45 @@ export declare function computeAppProperties(launch: NormalizedLaunch, component
|
|
|
26
26
|
* secrets, and (D-33) the platform-injected app properties.
|
|
27
27
|
*/
|
|
28
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
|
+
}
|
|
29
43
|
/**
|
|
30
44
|
* Resolve all environment variables for a single component.
|
|
31
45
|
*/
|
|
32
|
-
export declare function resolveComponentEnv(component: NormalizedComponent, context: ResolverContext, resourceMap: Record<string, ResourceProperties>, storage?: Record<string, Record<string, string>>):
|
|
46
|
+
export declare function resolveComponentEnv(component: NormalizedComponent, context: ResolverContext, resourceMap: Record<string, ResourceProperties>, storage?: Record<string, Record<string, string>>): ComponentEnvResult;
|
|
33
47
|
/**
|
|
34
48
|
* Generate all app-wide secrets, reusing values from state when available.
|
|
35
49
|
*/
|
|
36
50
|
export declare function generateSecrets(secretDefs: Record<string, Secret> | undefined, existingSecrets: Record<string, string>): Promise<Record<string, string>>;
|
|
37
51
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
40
66
|
*/
|
|
41
|
-
export declare function resolveGenerators(component: NormalizedComponent, env: Record<string, string>): Promise<
|
|
67
|
+
export declare function resolveGenerators(component: NormalizedComponent, env: Record<string, string>, componentName: string, generatedEnv: Record<string, string>): Promise<boolean>;
|
|
42
68
|
/**
|
|
43
69
|
* Write resolved env vars to a .env file.
|
|
44
70
|
*/
|
|
@@ -48,5 +74,5 @@ export declare function writeEnvFile(filePath: string, env: Record<string, strin
|
|
|
48
74
|
* Single-component → .env.local at project root.
|
|
49
75
|
* Multi-component → .launchfile/env/<component>.env per component.
|
|
50
76
|
*/
|
|
51
|
-
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>>>;
|
|
52
78
|
//# sourceMappingURL=env-writer.d.ts.map
|
package/dist/env-writer.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { deriveAppUrlProperties, resolveExpression, isExpression, } from "@launchfile/sdk";
|
|
9
|
+
import { deriveAppUrlProperties, resolveExpression, isExpression, unsuppliedRequiredEnv, } from "@launchfile/sdk";
|
|
10
10
|
import { generateValue } from "./secret-generator.js";
|
|
11
11
|
/**
|
|
12
12
|
* Compute the $app.* property set (D-33, D-35) for a Launchfile under the
|
|
@@ -24,7 +24,11 @@ import { generateValue } from "./secret-generator.js";
|
|
|
24
24
|
export function computeAppProperties(launch, componentPorts) {
|
|
25
25
|
let primaryPort = 0;
|
|
26
26
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
27
|
-
|
|
27
|
+
// Only endpoints explicitly marked `exposed: true` are reachable from
|
|
28
|
+
// outside the host (D-27), so only they can be the app's public address.
|
|
29
|
+
// The docker provider derives `$app.*` by the same rule — `$app.url` is a
|
|
30
|
+
// portable value and the two providers must not disagree on it (P-5).
|
|
31
|
+
const hasExposed = component.provides?.some((p) => p.exposed === true) ?? false;
|
|
28
32
|
if (hasExposed && componentPorts[name]) {
|
|
29
33
|
primaryPort = componentPorts[name];
|
|
30
34
|
break;
|
|
@@ -77,6 +81,11 @@ export function resolveComponentEnv(component, context, resourceMap, storage) {
|
|
|
77
81
|
const ctx = storage ? { ...context, storage } : context;
|
|
78
82
|
// 1. Resolve set_env from requires
|
|
79
83
|
for (const req of component.requires ?? []) {
|
|
84
|
+
// A host capability (D-44) is never provisioned, so it has no properties
|
|
85
|
+
// to resolve against. An ungranted capability's set_env vars are omitted
|
|
86
|
+
// rather than resolved to empty strings.
|
|
87
|
+
if (req.host)
|
|
88
|
+
continue;
|
|
80
89
|
const resourceName = req.name ?? req.type;
|
|
81
90
|
const props = resourceMap[resourceName];
|
|
82
91
|
if (!req.set_env || !props)
|
|
@@ -97,6 +106,8 @@ export function resolveComponentEnv(component, context, resourceMap, storage) {
|
|
|
97
106
|
}
|
|
98
107
|
// 2. Resolve set_env from supports (only if resource was provisioned)
|
|
99
108
|
for (const sup of component.supports ?? []) {
|
|
109
|
+
if (sup.host)
|
|
110
|
+
continue; // capability, not a backing service (D-44)
|
|
100
111
|
const resourceName = sup.name ?? sup.type;
|
|
101
112
|
const props = resourceMap[resourceName];
|
|
102
113
|
if (!sup.set_env || !props)
|
|
@@ -119,6 +130,12 @@ export function resolveComponentEnv(component, context, resourceMap, storage) {
|
|
|
119
130
|
for (const [key, envVar] of Object.entries(component.env)) {
|
|
120
131
|
if (env[key] !== undefined)
|
|
121
132
|
continue; // set_env takes precedence
|
|
133
|
+
// A generator outranks a default (D-49 provenance precedence). Filling
|
|
134
|
+
// the default here would win by arriving first — resolveGenerators
|
|
135
|
+
// skips any key already set — and this provider would mint nothing
|
|
136
|
+
// where docker and aws mint a secret, for the same file.
|
|
137
|
+
if (envVar.generator)
|
|
138
|
+
continue;
|
|
122
139
|
if (envVar.default !== undefined) {
|
|
123
140
|
const defaultStr = String(envVar.default);
|
|
124
141
|
if (isExpression(defaultStr)) {
|
|
@@ -128,10 +145,15 @@ export function resolveComponentEnv(component, context, resourceMap, storage) {
|
|
|
128
145
|
env[key] = defaultStr;
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
|
-
// required + no default + no generator → left unset (provider should prompt)
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
|
-
|
|
150
|
+
// A `required:` var nothing above yielded is left ABSENT and reported, not
|
|
151
|
+
// silently dropped (D-52, PROVIDERS.md §10 rule 8). `generator:` keys were
|
|
152
|
+
// skipped just above but are not unsupplied — `resolveGenerators` mints them
|
|
153
|
+
// — and the shared predicate excludes them for that reason. The test runs
|
|
154
|
+
// after the `set_env` loops so it measures arrival, not declaration.
|
|
155
|
+
const unsupplied = unsuppliedRequiredEnv(component, Object.keys(env));
|
|
156
|
+
return { env, unsupplied };
|
|
135
157
|
}
|
|
136
158
|
/**
|
|
137
159
|
* Generate all app-wide secrets, reusing values from state when available.
|
|
@@ -148,19 +170,46 @@ export async function generateSecrets(secretDefs, existingSecrets) {
|
|
|
148
170
|
return secrets;
|
|
149
171
|
}
|
|
150
172
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
173
|
+
* Resolve values for env vars that declare generators, preserving minted
|
|
174
|
+
* values across runs (D-49: generate once, then preserve).
|
|
175
|
+
*
|
|
176
|
+
* A `secret` or `uuid` value is read from `generatedEnv` when present, and
|
|
177
|
+
* minted and written into it when absent. The store is keyed
|
|
178
|
+
* `<component>.<ENV_NAME>` — one entry per declaration (D-25), so two
|
|
179
|
+
* components declaring the same variable name hold independent values.
|
|
180
|
+
* `generator: port` is exempt: ports have their own preserved home
|
|
181
|
+
* (`state.ports`) and allocator, and a preserved port produces a bind
|
|
182
|
+
* conflict rather than continuity.
|
|
183
|
+
*
|
|
184
|
+
* Mutates `env` and `generatedEnv` in place. Returns true when a new value
|
|
185
|
+
* was minted into `generatedEnv` — the caller must then persist the state
|
|
186
|
+
* before handing the value to anything, so every site that mints persists.
|
|
153
187
|
*/
|
|
154
|
-
export async function resolveGenerators(component, env) {
|
|
188
|
+
export async function resolveGenerators(component, env, componentName, generatedEnv) {
|
|
155
189
|
if (!component.env)
|
|
156
|
-
return;
|
|
190
|
+
return false;
|
|
191
|
+
let minted = false;
|
|
157
192
|
for (const [key, envVar] of Object.entries(component.env)) {
|
|
158
193
|
if (env[key] !== undefined)
|
|
159
194
|
continue;
|
|
160
|
-
if (envVar.generator)
|
|
195
|
+
if (!envVar.generator)
|
|
196
|
+
continue;
|
|
197
|
+
if (envVar.generator === "port") {
|
|
161
198
|
env[key] = await generateValue(envVar.generator);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const stateKey = `${componentName}.${key}`;
|
|
202
|
+
const existing = generatedEnv[stateKey];
|
|
203
|
+
if (existing !== undefined) {
|
|
204
|
+
env[key] = existing;
|
|
205
|
+
continue;
|
|
162
206
|
}
|
|
207
|
+
const value = await generateValue(envVar.generator);
|
|
208
|
+
env[key] = value;
|
|
209
|
+
generatedEnv[stateKey] = value;
|
|
210
|
+
minted = true;
|
|
163
211
|
}
|
|
212
|
+
return minted;
|
|
164
213
|
}
|
|
165
214
|
/**
|
|
166
215
|
* Write resolved env vars to a .env file.
|
|
@@ -169,9 +218,12 @@ export async function writeEnvFile(filePath, env) {
|
|
|
169
218
|
const lines = Object.entries(env)
|
|
170
219
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
171
220
|
.map(([key, value]) => {
|
|
172
|
-
// Quote values that contain spaces, #, or newlines
|
|
221
|
+
// Quote values that contain spaces, #, or newlines. Escape backslashes
|
|
222
|
+
// first, then quotes — otherwise a value containing a backslash would
|
|
223
|
+
// produce broken or injectable quoting (CWE-116 incomplete escaping).
|
|
173
224
|
if (/[\s#\n]/.test(value)) {
|
|
174
|
-
|
|
225
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
226
|
+
return `${key}="${escaped}"`;
|
|
175
227
|
}
|
|
176
228
|
return `${key}=${value}`;
|
|
177
229
|
});
|
|
@@ -184,13 +236,13 @@ export async function writeEnvFile(filePath, env) {
|
|
|
184
236
|
* Single-component → .env.local at project root.
|
|
185
237
|
* Multi-component → .launchfile/env/<component>.env per component.
|
|
186
238
|
*/
|
|
187
|
-
export async function writeAllEnvFiles(launch, context, resourceMap, componentPorts, projectDir) {
|
|
239
|
+
export async function writeAllEnvFiles(launch, context, resourceMap, componentPorts, projectDir, generatedEnv) {
|
|
188
240
|
const allEnvs = {};
|
|
189
241
|
const componentNames = Object.keys(launch.components);
|
|
190
242
|
const isSingleComponent = componentNames.length === 1 && componentNames[0] === "default";
|
|
191
243
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
192
|
-
const env = resolveComponentEnv(component, context, resourceMap);
|
|
193
|
-
await resolveGenerators(component, env);
|
|
244
|
+
const { env } = resolveComponentEnv(component, context, resourceMap);
|
|
245
|
+
await resolveGenerators(component, env, name, generatedEnv);
|
|
194
246
|
// Inject PORT if not already set and component has provides
|
|
195
247
|
const port = componentPorts[name];
|
|
196
248
|
if (port && !env.PORT) {
|
package/dist/health.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Health check polling for components.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
5
|
-
/**
|
|
4
|
+
import { type NormalizedHealth } from "@launchfile/sdk";
|
|
5
|
+
/**
|
|
6
|
+
* Parse a health duration against the ratified grammar (D-48). Throws on an
|
|
7
|
+
* unparseable value — PROVIDERS.md §10.10 forbids silently substituting a
|
|
8
|
+
* default, and the previous local parser did exactly that in the worst
|
|
9
|
+
* possible way: it accepted no `h` unit and returned 0, so a spec-valid
|
|
10
|
+
* `interval: "1h"` became a zero-length poll that could never pass.
|
|
11
|
+
*/
|
|
6
12
|
export declare function parseDuration(duration: string): number;
|
|
7
13
|
/**
|
|
8
14
|
* Wait for a component to become healthy.
|