@launchfile/macos-dev 0.11.0 → 0.13.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 +7 -0
- package/dist/bootstrap.js +28 -4
- package/dist/cli-args.d.ts +24 -0
- package/dist/cli-args.js +51 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +21 -2
- package/dist/env-writer.d.ts +46 -3
- package/dist/env-writer.js +83 -25
- package/dist/health.d.ts +22 -1
- package/dist/health.js +36 -1
- package/dist/https-origin.d.ts +46 -10
- package/dist/https-origin.js +47 -16
- package/dist/lockfile-detect.d.ts +8 -0
- package/dist/lockfile-detect.js +10 -0
- package/dist/prepare-fingerprint.d.ts +23 -0
- package/dist/prepare-fingerprint.js +63 -0
- package/dist/process-manager.d.ts +50 -2
- package/dist/process-manager.js +188 -40
- package/dist/provider.d.ts +80 -1
- package/dist/provider.js +242 -56
- package/dist/redact.js +13 -1
- package/dist/state.d.ts +26 -0
- package/package.json +5 -5
package/dist/provider.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* installs runtimes, and starts all components.
|
|
6
6
|
*/
|
|
7
7
|
import { type NormalizedLaunch, type NormalizedComponent } from "@launchfile/sdk";
|
|
8
|
+
import { type LaunchState } from "./state.js";
|
|
8
9
|
/**
|
|
9
10
|
* This provider runs apps from source. A component is source-runnable when
|
|
10
11
|
* {@link resolveSourceRunCommand} (D-38) resolves a command — declares `dev`,
|
|
@@ -159,7 +160,8 @@ export declare function applyResourceTypeRefusals(launch: NormalizedLaunch): "ok
|
|
|
159
160
|
* use its provisioner cannot cover (SPEC.md § Resource uses, D-56 rule 1,
|
|
160
161
|
* D-64), mapped to `<entry>: <use>` lines. Graded after
|
|
161
162
|
* {@link refusedResourceTypes}: a type with no provisioner is refused on the
|
|
162
|
-
* type, so only entries whose type this provider stands up
|
|
163
|
+
* type, so only entries whose type this provider stands up, or accepts
|
|
164
|
+
* through the publication context (`https-origin`), reach here. A
|
|
163
165
|
* token this provider does not recognise is uncovered — no provider can
|
|
164
166
|
* claim to cover a use it does not know. `supports:` entries are optional
|
|
165
167
|
* (D-8) and are not graded here.
|
|
@@ -171,7 +173,84 @@ export declare function refusedResourceUses(launch: NormalizedLaunch): Map<strin
|
|
|
171
173
|
* removal IS the refusal.
|
|
172
174
|
*/
|
|
173
175
|
export declare function applyResourceUseRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
176
|
+
/**
|
|
177
|
+
* The mandatory report for every `provides` entry that declares `at:` (D-68
|
|
178
|
+
* rule 5), one line per declaring entry. This provider starts each process on
|
|
179
|
+
* a local port with nothing in front that routes by host name, so every
|
|
180
|
+
* request reaches the listener with its `Host` intact and only name resolution
|
|
181
|
+
* is left to the operator. It launches and reports — never a silent launch,
|
|
182
|
+
* and never a refusal.
|
|
183
|
+
*
|
|
184
|
+
* `ports` maps a component to the local port its process listens on; an
|
|
185
|
+
* absent entry means the port is not known yet. Under a supplied publication
|
|
186
|
+
* URL (D-58) the names are whoever routes that URL's to send here (rule 7).
|
|
187
|
+
*/
|
|
188
|
+
export declare function atReports(launch: NormalizedLaunch, ports?: Record<string, number>, suppliedAppUrl?: string): string[];
|
|
189
|
+
/** What {@link runSourcePrepare} needs from the surrounding `up`. */
|
|
190
|
+
export interface SourcePrepareContext {
|
|
191
|
+
projectDir: string;
|
|
192
|
+
/** Mutated in place: successful runs are recorded under `state.prepared`. */
|
|
193
|
+
state: LaunchState;
|
|
194
|
+
/** Resolved environment per component, as `prepare` should see it. */
|
|
195
|
+
envs: Record<string, Record<string, string>>;
|
|
196
|
+
/** Package-manager install command used where a component declares none. */
|
|
197
|
+
fallbackCommand?: string;
|
|
198
|
+
/** Prefix progress lines with the component name (multi-component apps). */
|
|
199
|
+
labelComponents?: boolean;
|
|
200
|
+
/** Runs one command. Injected so tests can observe without a real shell. */
|
|
201
|
+
run?: (command: string, opts: {
|
|
202
|
+
cwd: string;
|
|
203
|
+
env?: Record<string, string>;
|
|
204
|
+
timeout?: number;
|
|
205
|
+
}) => Promise<unknown>;
|
|
206
|
+
/** Persists state whenever a component's `state.prepared` record changes. */
|
|
207
|
+
save?: (projectDir: string, state: LaunchState) => Promise<void>;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Run the source-mode prepare slot (`install ?? build`, D-38) for every
|
|
211
|
+
* component that declares one, **on demand**: on first launch, and afterwards
|
|
212
|
+
* only when the prepare inputs changed.
|
|
213
|
+
*
|
|
214
|
+
* The demand signal is {@link prepareFingerprint} — the command plus the
|
|
215
|
+
* dependency manifests and lockfiles in its working directory. `state.prepared`
|
|
216
|
+
* holds the fingerprint of each component's last successful run, so an
|
|
217
|
+
* unchanged component is skipped rather than reinstalled on every `up`.
|
|
218
|
+
*
|
|
219
|
+
* Components resolving to the same working directory and command share one
|
|
220
|
+
* prepare, so it runs once per `up` (D-38) even on a first launch, when neither
|
|
221
|
+
* has a recorded fingerprint yet.
|
|
222
|
+
*
|
|
223
|
+
* A failing command throws, which fails the launch (SPEC.md § Failure
|
|
224
|
+
* semantics): the prepare slot fails the invocation. Nothing is recorded for
|
|
225
|
+
* it, so the next `up` retries.
|
|
226
|
+
*/
|
|
227
|
+
export declare function runSourcePrepare(launch: NormalizedLaunch, ctx: SourcePrepareContext): Promise<void>;
|
|
174
228
|
export declare function launchUp(opts?: LaunchUpOpts): Promise<void>;
|
|
229
|
+
/**
|
|
230
|
+
* What a printout reads to place the orchestrator-supplied publication URL
|
|
231
|
+
* (D-58): the URL, and the `ports` key it asserts. One URL asserts the
|
|
232
|
+
* primary endpoint only (D-58 rule 4), so every other key keeps this
|
|
233
|
+
* provider's own address; with no URL every key does.
|
|
234
|
+
*/
|
|
235
|
+
export type PrintedPublication = Pick<LaunchState, "appUrl" | "primaryEndpoint">;
|
|
236
|
+
/**
|
|
237
|
+
* The address to print for one `ports` key — the single definition `up` and
|
|
238
|
+
* `status` share. The supplied publication URL on the primary endpoint's key,
|
|
239
|
+
* as stored (`normalizeAppUrl` ran when `up` recorded it; nothing runs again
|
|
240
|
+
* here); this provider's own `http://localhost:<port>` on every other key,
|
|
241
|
+
* and on every key when no URL is supplied — byte-identical to a run with
|
|
242
|
+
* none. `up` records `primaryEndpoint` only for an `http`/`https` primary or
|
|
243
|
+
* one a declared `https-origin` names, so a positional `ws`/`tcp`/`udp`/`grpc`
|
|
244
|
+
* primary keeps this provider's own form (§7, `printedPrimaryEndpoint`).
|
|
245
|
+
*/
|
|
246
|
+
export declare function componentAddress(key: string, port: number, publication?: PrintedPublication): string;
|
|
247
|
+
/**
|
|
248
|
+
* The "<component> is running at …" lines `up` prints, one per `ports` key.
|
|
249
|
+
* Pure, so the placement of the supplied URL is testable without a launch.
|
|
250
|
+
*/
|
|
251
|
+
export declare function summaryLines(appName: string, ports: Record<string, number>, publication?: PrintedPublication): string[];
|
|
252
|
+
/** The "Components:" lines `status` prints, one per `ports` key. */
|
|
253
|
+
export declare function statusLines(ports: Record<string, number>, publication?: PrintedPublication): string[];
|
|
175
254
|
export declare function launchDown(opts?: {
|
|
176
255
|
destroy?: boolean;
|
|
177
256
|
projectDir?: string;
|
package/dist/provider.js
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
import { accessSync, constants as fsConstants } from "node:fs";
|
|
8
8
|
import { readFile } from "node:fs/promises";
|
|
9
9
|
import { join, resolve as resolvePath } from "node:path";
|
|
10
|
-
import { CERTIFICATE, certificateBindings, indexOperatorStoragePaths, MissingOperatorStoragePathError, normalizeAppUrl, readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, UnboundOperatorStorageError, unsuppliedRequiredEnv, appEndpointReferences, useKeys, } from "@launchfile/sdk";
|
|
10
|
+
import { AT_APP_HOST, atDeclarations, atEntryLabel, buildLaunchErrorContext, CERTIFICATE, certificateBindings, indexOperatorStoragePaths, LaunchError, MissingOperatorStoragePathError, normalizeAppUrl, readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, UnboundOperatorStorageError, unsuppliedRequiredEnv, appEndpointReferences, useKeys, } from "@launchfile/sdk";
|
|
11
11
|
import { checkPrereqs } from "./prereqs.js";
|
|
12
|
-
import { HTTPS_ORIGIN, httpsOriginSatisfied, httpsOriginShortfall, } from "./https-origin.js";
|
|
13
|
-
import { loadState, initState, saveState, ensureDirs, withRecordedDbIndexes } from "./state.js";
|
|
14
|
-
import { declaredUses, registerResource, resolveComponentEnv, resolverContextFor, resourceMapFromState, generateSecrets, resolveGenerators, writeEnvFile, } from "./env-writer.js";
|
|
12
|
+
import { declaredPrimary, HTTPS_ORIGIN, httpsOriginSatisfied, httpsOriginShortfall, uncoveredOriginUses, } from "./https-origin.js";
|
|
13
|
+
import { loadState, initState, saveState, ensureDirs, withRecordedDbIndexes, } from "./state.js";
|
|
14
|
+
import { declaredUses, printedPrimaryEndpoint, registerResource, resolveComponentEnv, resolverContextFor, resourceMapFromState, generateSecrets, resolveGenerators, writeEnvFile, } from "./env-writer.js";
|
|
15
15
|
import { allocateDbIndexes, getProvisioner, namedDatabases, uncoveredUses, } from "./resources/index.js";
|
|
16
16
|
import { allocatePorts } from "./port-allocator.js";
|
|
17
17
|
import { getRuntimeInstaller } from "./runtimes/index.js";
|
|
18
18
|
import { detectPackageManager } from "./lockfile-detect.js";
|
|
19
|
+
import { prepareFingerprint } from "./prepare-fingerprint.js";
|
|
19
20
|
import { provisionStorage, storagePaths } from "./storage.js";
|
|
20
|
-
import { ProcessManager } from "./process-manager.js";
|
|
21
|
+
import { HealthGateError, ProcessManager } from "./process-manager.js";
|
|
22
|
+
import { redactSecrets } from "./redact.js";
|
|
21
23
|
import { stopRecordedProcesses } from "./process-stopper.js";
|
|
22
24
|
import { shellScript } from "./shell.js";
|
|
23
25
|
import { parseDuration } from "./bootstrap.js";
|
|
@@ -266,7 +268,8 @@ export function applyResourceTypeRefusals(launch) {
|
|
|
266
268
|
* use its provisioner cannot cover (SPEC.md § Resource uses, D-56 rule 1,
|
|
267
269
|
* D-64), mapped to `<entry>: <use>` lines. Graded after
|
|
268
270
|
* {@link refusedResourceTypes}: a type with no provisioner is refused on the
|
|
269
|
-
* type, so only entries whose type this provider stands up
|
|
271
|
+
* type, so only entries whose type this provider stands up, or accepts
|
|
272
|
+
* through the publication context (`https-origin`), reach here. A
|
|
270
273
|
* token this provider does not recognise is uncovered — no provider can
|
|
271
274
|
* claim to cover a use it does not know. `supports:` entries are optional
|
|
272
275
|
* (D-8) and are not graded here.
|
|
@@ -276,7 +279,9 @@ export function refusedResourceUses(launch) {
|
|
|
276
279
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
277
280
|
const entries = [];
|
|
278
281
|
for (const req of component.requires ?? []) {
|
|
279
|
-
if (req.host ||
|
|
282
|
+
if (req.host || !req.uses)
|
|
283
|
+
continue;
|
|
284
|
+
if (req.type !== HTTPS_ORIGIN && !getProvisioner(req.type))
|
|
280
285
|
continue;
|
|
281
286
|
const resourceName = req.name ?? req.type;
|
|
282
287
|
for (const use of uncoveredUses(req.type, useKeys(req.uses))) {
|
|
@@ -306,6 +311,104 @@ export function applyResourceUseRefusals(launch) {
|
|
|
306
311
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
307
312
|
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
308
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The mandatory report for every `provides` entry that declares `at:` (D-68
|
|
316
|
+
* rule 5), one line per declaring entry. This provider starts each process on
|
|
317
|
+
* a local port with nothing in front that routes by host name, so every
|
|
318
|
+
* request reaches the listener with its `Host` intact and only name resolution
|
|
319
|
+
* is left to the operator. It launches and reports — never a silent launch,
|
|
320
|
+
* and never a refusal.
|
|
321
|
+
*
|
|
322
|
+
* `ports` maps a component to the local port its process listens on; an
|
|
323
|
+
* absent entry means the port is not known yet. Under a supplied publication
|
|
324
|
+
* URL (D-58) the names are whoever routes that URL's to send here (rule 7).
|
|
325
|
+
*/
|
|
326
|
+
export function atReports(launch, ports = {}, suppliedAppUrl) {
|
|
327
|
+
const host = suppliedAppUrl === undefined ? "localhost" : new URL(suppliedAppUrl).hostname;
|
|
328
|
+
return atDeclarations(launch).map((declaration) => {
|
|
329
|
+
const names = declaration.values
|
|
330
|
+
.map((value) => (value === AT_APP_HOST ? host : `${value}.${host}`))
|
|
331
|
+
.join(", ");
|
|
332
|
+
const head = `${atEntryLabel(declaration)} answers at ${names} (\`at:\`, D-68)`;
|
|
333
|
+
if (suppliedAppUrl !== undefined) {
|
|
334
|
+
return (`${head} — this provider sets up no host names, and the supplied publication URL ` +
|
|
335
|
+
`(${suppliedAppUrl}) says nothing about them; whatever routes that URL must send each ` +
|
|
336
|
+
"name to this endpoint with the requested `Host` intact");
|
|
337
|
+
}
|
|
338
|
+
const port = ports[declaration.component];
|
|
339
|
+
const target = port === undefined ? "the component's local port" : `localhost:${port}`;
|
|
340
|
+
return (`${head} — this provider starts the process on a local port and sets up no host names. ` +
|
|
341
|
+
`Every request that reaches ${target} reaches the listener with its \`Host\` intact, so ` +
|
|
342
|
+
"map each name that does not resolve to this machine (hosts file or DNS), or use a " +
|
|
343
|
+
"provider that routes host names");
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/** Print {@link atReports} the way this provider prints every warning. */
|
|
347
|
+
function printAtReports(reports) {
|
|
348
|
+
for (const report of reports)
|
|
349
|
+
console.warn(` Warning: ${report}`);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Run the source-mode prepare slot (`install ?? build`, D-38) for every
|
|
353
|
+
* component that declares one, **on demand**: on first launch, and afterwards
|
|
354
|
+
* only when the prepare inputs changed.
|
|
355
|
+
*
|
|
356
|
+
* The demand signal is {@link prepareFingerprint} — the command plus the
|
|
357
|
+
* dependency manifests and lockfiles in its working directory. `state.prepared`
|
|
358
|
+
* holds the fingerprint of each component's last successful run, so an
|
|
359
|
+
* unchanged component is skipped rather than reinstalled on every `up`.
|
|
360
|
+
*
|
|
361
|
+
* Components resolving to the same working directory and command share one
|
|
362
|
+
* prepare, so it runs once per `up` (D-38) even on a first launch, when neither
|
|
363
|
+
* has a recorded fingerprint yet.
|
|
364
|
+
*
|
|
365
|
+
* A failing command throws, which fails the launch (SPEC.md § Failure
|
|
366
|
+
* semantics): the prepare slot fails the invocation. Nothing is recorded for
|
|
367
|
+
* it, so the next `up` retries.
|
|
368
|
+
*/
|
|
369
|
+
export async function runSourcePrepare(launch, ctx) {
|
|
370
|
+
const run = ctx.run ?? shellScript;
|
|
371
|
+
const save = ctx.save ?? saveState;
|
|
372
|
+
ctx.state.prepared ??= {};
|
|
373
|
+
const prepared = ctx.state.prepared;
|
|
374
|
+
const ranThisUp = new Set();
|
|
375
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
376
|
+
const prepare = resolveSourcePrepareCommand(component);
|
|
377
|
+
const command = prepare?.command ?? ctx.fallbackCommand;
|
|
378
|
+
if (!command)
|
|
379
|
+
continue;
|
|
380
|
+
const label = ctx.labelComponents ? ` [${name}]` : "";
|
|
381
|
+
const sourceDir = join(component.source ?? component.build?.context ?? ".");
|
|
382
|
+
const cwd = join(ctx.projectDir, sourceDir);
|
|
383
|
+
// Identifies a shared prepare: same directory, same command.
|
|
384
|
+
const sharedKey = `${sourceDir}\u0000${command}`;
|
|
385
|
+
const fingerprint = await prepareFingerprint(cwd, command);
|
|
386
|
+
if (prepared[name] === fingerprint || ranThisUp.has(sharedKey)) {
|
|
387
|
+
// A component sharing this prepare is covered by an up-to-date record
|
|
388
|
+
// as much as by a run this `up`.
|
|
389
|
+
ranThisUp.add(sharedKey);
|
|
390
|
+
if (prepared[name] !== fingerprint) {
|
|
391
|
+
prepared[name] = fingerprint;
|
|
392
|
+
await save(ctx.projectDir, ctx.state);
|
|
393
|
+
}
|
|
394
|
+
console.log(` \u2713 Prepare up to date${label} (no dependency change)`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
console.log(` \u2193 Preparing${label}...`);
|
|
398
|
+
await run(command, {
|
|
399
|
+
cwd,
|
|
400
|
+
env: ctx.envs[name],
|
|
401
|
+
// Installs/compiles routinely exceed the 2-minute shell default;
|
|
402
|
+
// honor a declared timeout, else allow 10 minutes.
|
|
403
|
+
timeout: declaredTimeout(prepare?.timeout, `prepare [${name}]`) ?? 600_000,
|
|
404
|
+
});
|
|
405
|
+
ranThisUp.add(sharedKey);
|
|
406
|
+
// Recorded as each command succeeds: a later component can fail the
|
|
407
|
+
// launch, and that must not discard the record of work already done.
|
|
408
|
+
prepared[name] = fingerprint;
|
|
409
|
+
await save(ctx.projectDir, ctx.state);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
309
412
|
export async function launchUp(opts = {}) {
|
|
310
413
|
const projectDir = opts.projectDir ?? process.cwd();
|
|
311
414
|
// Publication context (D-58): validated and normalized before anything is
|
|
@@ -355,6 +458,28 @@ export async function launchUp(opts = {}) {
|
|
|
355
458
|
const startSet = new Set(selection.start);
|
|
356
459
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => startSet.has(n)));
|
|
357
460
|
}
|
|
461
|
+
// State is loaded ahead of every refusal (loading writes nothing; the
|
|
462
|
+
// first write is `ensureDirs`): the https-origin refusal at 2a-bis reads
|
|
463
|
+
// the recorded publication context, and the declared primary below is
|
|
464
|
+
// read against it.
|
|
465
|
+
let state = await loadState(projectDir);
|
|
466
|
+
if (!state) {
|
|
467
|
+
state = initState(launch.name, launchfileContent);
|
|
468
|
+
}
|
|
469
|
+
// Publication context (D-58), the same preservation rule the docker provider
|
|
470
|
+
// applies: a supplied value replaces the recorded one — the derived $app.*
|
|
471
|
+
// env recomputes below from it (D-49) — while omission keeps what is
|
|
472
|
+
// recorded, so a later plain `up` cannot silently flip a proxied deployment
|
|
473
|
+
// back to localhost.
|
|
474
|
+
if (suppliedAppUrl !== undefined)
|
|
475
|
+
state.appUrl = suppliedAppUrl;
|
|
476
|
+
// The primary an `https-origin` entry declares (D-60 rule 3), read now,
|
|
477
|
+
// while the start-set is whole: the refusals below remove a refused
|
|
478
|
+
// component from `launch.components`, and a primary read after that
|
|
479
|
+
// would fall back to a surviving sibling. Declaration fixes the primary;
|
|
480
|
+
// a refused one keeps its place and resolves the empty address at step
|
|
481
|
+
// 8 (D-72).
|
|
482
|
+
const primary = declaredPrimary(launch, state.appUrl);
|
|
358
483
|
// 2a. Host capabilities are granted or refused, never provisioned (D-44,
|
|
359
484
|
// PROVIDERS.md §11). This provider runs processes directly on the host and
|
|
360
485
|
// grants none of them, so a component with a required capability is
|
|
@@ -370,21 +495,8 @@ export async function launchUp(opts = {}) {
|
|
|
370
495
|
// 2a-bis. A required `https-origin` (D-60 rule 5, PROVIDERS.md §10 item 5)
|
|
371
496
|
// is satisfied by the publication context when its scheme is https —
|
|
372
497
|
// supplied on this run, or recorded by an earlier one and preserved on
|
|
373
|
-
// omission
|
|
374
|
-
//
|
|
375
|
-
// Otherwise it is refused in the same way as a host capability: starting
|
|
376
|
-
// the component anyway is the silent success the type removes.
|
|
377
|
-
let state = await loadState(projectDir);
|
|
378
|
-
if (!state) {
|
|
379
|
-
state = initState(launch.name, launchfileContent);
|
|
380
|
-
}
|
|
381
|
-
// Publication context (D-58), the same preservation rule the docker provider
|
|
382
|
-
// applies: a supplied value replaces the recorded one — the derived $app.*
|
|
383
|
-
// env recomputes below from it (D-49) — while omission keeps what is
|
|
384
|
-
// recorded, so a later plain `up` cannot silently flip a proxied deployment
|
|
385
|
-
// back to localhost.
|
|
386
|
-
if (suppliedAppUrl !== undefined)
|
|
387
|
-
state.appUrl = suppliedAppUrl;
|
|
498
|
+
// omission. Otherwise it is refused in the same way as a host capability:
|
|
499
|
+
// starting the component anyway is the silent success the type removes.
|
|
388
500
|
if (applyHttpsOriginRefusals(launch, state.appUrl) === "none-left") {
|
|
389
501
|
console.error("Every selected component requires a public HTTPS origin this provider cannot supply.");
|
|
390
502
|
process.exit(1);
|
|
@@ -415,6 +527,11 @@ export async function launchUp(opts = {}) {
|
|
|
415
527
|
console.error("Every selected component requires a use of a resource this provider cannot cover.");
|
|
416
528
|
process.exit(1);
|
|
417
529
|
}
|
|
530
|
+
// 2a-sexies. A `provides` entry declaring `at:` is reported, not refused
|
|
531
|
+
// (D-68 rule 5). The report belongs at the end of provisioning, after the
|
|
532
|
+
// summary; a dry run never gets there, so it prints here.
|
|
533
|
+
if (opts.dryRun)
|
|
534
|
+
printAtReports(atReports(launch, {}, suppliedAppUrl));
|
|
418
535
|
// An optional capability is not refused — the component runs, degraded.
|
|
419
536
|
for (const [name, c] of Object.entries(launch.components)) {
|
|
420
537
|
for (const sup of c.supports ?? []) {
|
|
@@ -425,9 +542,22 @@ export async function launchUp(opts = {}) {
|
|
|
425
542
|
}
|
|
426
543
|
// The optional mood of `https-origin` (D-60 rule 6): satisfied, it is
|
|
427
544
|
// wired like any other resource at step 8; unsatisfied, the component
|
|
428
|
-
// still runs, its set_env is absent, and the shortfall is named.
|
|
429
|
-
|
|
545
|
+
// still runs, its set_env is absent, and the shortfall is named. A
|
|
546
|
+
// declared use this provider cannot cover leaves a satisfied entry
|
|
547
|
+
// unfulfilled the same way (D-65 rule 4).
|
|
548
|
+
if (httpsOriginSatisfied(state.appUrl)) {
|
|
549
|
+
for (const sup of c.supports ?? []) {
|
|
550
|
+
if (sup.type !== HTTPS_ORIGIN)
|
|
551
|
+
continue;
|
|
552
|
+
const uncovered = uncoveredOriginUses(sup);
|
|
553
|
+
if (uncovered.length === 0)
|
|
554
|
+
continue;
|
|
555
|
+
console.warn(` Warning: ${name}: optional public HTTPS origin ${sup.name ?? sup.type} not satisfied — ` +
|
|
556
|
+
`this provider does not cover ${uncovered.length === 1 ? "a use" : "uses"} it declares ` +
|
|
557
|
+
`(${uncovered.join(", ")}); running degraded`);
|
|
558
|
+
}
|
|
430
559
|
continue;
|
|
560
|
+
}
|
|
431
561
|
for (const sup of c.supports ?? []) {
|
|
432
562
|
if (sup.type !== HTTPS_ORIGIN)
|
|
433
563
|
continue;
|
|
@@ -568,7 +698,7 @@ export async function launchUp(opts = {}) {
|
|
|
568
698
|
for (const key of storageIndex.unusedKeys(usedStorageKeys)) {
|
|
569
699
|
console.warn(` Warning: --storage ${key} matches no \`content: operator\` volume — ignored`);
|
|
570
700
|
}
|
|
571
|
-
// 3. State: loaded
|
|
701
|
+
// 3. State: loaded ahead of step 2a, where the refusals read it.
|
|
572
702
|
// 4. Ensure directories
|
|
573
703
|
await ensureDirs(projectDir);
|
|
574
704
|
// 5. Generate secrets
|
|
@@ -670,10 +800,17 @@ export async function launchUp(opts = {}) {
|
|
|
670
800
|
// 7. Allocate ports
|
|
671
801
|
const componentPorts = await allocatePorts(launch.components, launch.name, state.ports);
|
|
672
802
|
state.ports = componentPorts;
|
|
803
|
+
// The key the printouts place a supplied publication URL on (§7, D-58
|
|
804
|
+
// rules 2 and 4), recorded so `status` places it without the Launchfile.
|
|
805
|
+
// `primary` is the declared primary as read before the refusals, so a
|
|
806
|
+
// refused one places the URL on no key rather than a sibling's (D-72).
|
|
807
|
+
state.primaryEndpoint = printedPrimaryEndpoint(launch, componentPorts, state.appUrl, primary);
|
|
673
808
|
// 8. Build resolver context (including $app.* properties from D-33). A
|
|
674
809
|
// satisfied `https-origin` registers `url` — the same string as `$app.url`
|
|
675
810
|
// (D-60 rule 4) — so its set_env resolves like any provisioned resource's.
|
|
676
|
-
|
|
811
|
+
// `primary` is the declared primary as read before the refusals, so a
|
|
812
|
+
// refused one resolves the empty address rather than a sibling's (D-72).
|
|
813
|
+
const context = resolverContextFor(launch, resourceMap, state, primary);
|
|
677
814
|
// `$app.endpoints.<name>.*` resolves "" here (D-63 rule 4, #294). Said
|
|
678
815
|
// once per `up`, and only when the file asks, so the empty value is not a
|
|
679
816
|
// silent one (PROVIDERS.md §10 item 8).
|
|
@@ -761,25 +898,20 @@ export async function launchUp(opts = {}) {
|
|
|
761
898
|
await saveState(projectDir, state);
|
|
762
899
|
if (opts.dryRun) {
|
|
763
900
|
console.log("\n[dry-run] Would now run build, release, and start commands.");
|
|
764
|
-
printSummary(launch, componentPorts,
|
|
901
|
+
printSummary(launch.name, componentPorts, state);
|
|
765
902
|
return;
|
|
766
903
|
}
|
|
767
|
-
// 14. Run source-mode prepare \u2014 `install ?? build` (D-38), on demand
|
|
904
|
+
// 14. Run source-mode prepare \u2014 `install ?? build` (D-38), on demand.
|
|
905
|
+
// `--no-build` stays the explicit opt-out; it records nothing, since nothing
|
|
906
|
+
// ran, so a later `up` without it still prepares.
|
|
768
907
|
if (!opts.noBuild) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
env: allEnvs[name],
|
|
777
|
-
// Installs/compiles routinely exceed the 2-minute shell default;
|
|
778
|
-
// honor a declared timeout, else allow 10 minutes.
|
|
779
|
-
timeout: declaredTimeout(prepare?.timeout, `prepare [${name}]`) ?? 600_000,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
908
|
+
await runSourcePrepare(launch, {
|
|
909
|
+
projectDir,
|
|
910
|
+
state,
|
|
911
|
+
envs: allEnvs,
|
|
912
|
+
fallbackCommand: pm?.installCommand,
|
|
913
|
+
labelComponents: componentNames.length > 1,
|
|
914
|
+
});
|
|
783
915
|
}
|
|
784
916
|
// 15. Run release commands (migrations) \u2014 mode-invariant (D-38)
|
|
785
917
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
@@ -823,23 +955,77 @@ export async function launchUp(opts = {}) {
|
|
|
823
955
|
await saveState(projectDir, finalState);
|
|
824
956
|
process.exit(0);
|
|
825
957
|
});
|
|
826
|
-
|
|
958
|
+
try {
|
|
959
|
+
await pm2.startAll();
|
|
960
|
+
}
|
|
961
|
+
catch (err) {
|
|
962
|
+
// A failed health gate fails the invocation with the `health` phase
|
|
963
|
+
// (SPEC.md § Failure semantics), the same record the docker provider
|
|
964
|
+
// raises: the CLI registers the deployment as unhealthy so `down`
|
|
965
|
+
// reaches the processes left running, and `diagnose` finds the record.
|
|
966
|
+
if (err instanceof HealthGateError) {
|
|
967
|
+
throw new LaunchError(buildLaunchErrorContext({
|
|
968
|
+
phase: "health",
|
|
969
|
+
provider: "macos-dev",
|
|
970
|
+
key: launch.name,
|
|
971
|
+
app: launch.name,
|
|
972
|
+
message: err.message,
|
|
973
|
+
}, redactSecrets));
|
|
974
|
+
}
|
|
975
|
+
throw err;
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
// Record spawned pids so `launch down` can stop them from another shell or
|
|
979
|
+
// after this foreground session ends (closes #49). Backward compatible: the
|
|
980
|
+
// field is optional and absent in pre-existing state files. Recorded on
|
|
981
|
+
// the failure path too: a component that never became healthy fails the
|
|
982
|
+
// invocation (SPEC.md § Failure semantics) but its process is left
|
|
983
|
+
// running, and `status`/`logs`/`down` must still reach it.
|
|
984
|
+
state.processes = pm2.getRecordedProcesses();
|
|
985
|
+
await saveState(projectDir, state);
|
|
986
|
+
}
|
|
827
987
|
console.log("");
|
|
828
988
|
console.log(` \u2713 All components started`);
|
|
829
|
-
// Record spawned pids so `launch down` can stop them from another shell or
|
|
830
|
-
// after this foreground session ends (closes #49). Backward compatible: the
|
|
831
|
-
// field is optional and absent in pre-existing state files.
|
|
832
|
-
state.processes = pm2.getRecordedProcesses();
|
|
833
989
|
// 17. Print summary
|
|
834
|
-
printSummary(launch, componentPorts,
|
|
835
|
-
|
|
836
|
-
|
|
990
|
+
printSummary(launch.name, componentPorts, state);
|
|
991
|
+
printAtReports(atReports(launch, componentPorts, suppliedAppUrl));
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* The address to print for one `ports` key — the single definition `up` and
|
|
995
|
+
* `status` share. The supplied publication URL on the primary endpoint's key,
|
|
996
|
+
* as stored (`normalizeAppUrl` ran when `up` recorded it; nothing runs again
|
|
997
|
+
* here); this provider's own `http://localhost:<port>` on every other key,
|
|
998
|
+
* and on every key when no URL is supplied — byte-identical to a run with
|
|
999
|
+
* none. `up` records `primaryEndpoint` only for an `http`/`https` primary or
|
|
1000
|
+
* one a declared `https-origin` names, so a positional `ws`/`tcp`/`udp`/`grpc`
|
|
1001
|
+
* primary keeps this provider's own form (§7, `printedPrimaryEndpoint`).
|
|
1002
|
+
*/
|
|
1003
|
+
export function componentAddress(key, port, publication) {
|
|
1004
|
+
if (publication?.appUrl !== undefined &&
|
|
1005
|
+
publication.primaryEndpoint !== undefined &&
|
|
1006
|
+
key === publication.primaryEndpoint) {
|
|
1007
|
+
return publication.appUrl;
|
|
1008
|
+
}
|
|
1009
|
+
return `http://localhost:${port}`;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* The "<component> is running at …" lines `up` prints, one per `ports` key.
|
|
1013
|
+
* Pure, so the placement of the supplied URL is testable without a launch.
|
|
1014
|
+
*/
|
|
1015
|
+
export function summaryLines(appName, ports, publication) {
|
|
1016
|
+
return Object.entries(ports).map(([name, port]) => {
|
|
1017
|
+
const label = name === "default" ? appName : name;
|
|
1018
|
+
return ` ${label} is running at ${componentAddress(name, port, publication)}`;
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
/** The "Components:" lines `status` prints, one per `ports` key. */
|
|
1022
|
+
export function statusLines(ports, publication) {
|
|
1023
|
+
return Object.entries(ports).map(([name, port]) => ` ${name}: ${componentAddress(name, port, publication)}`);
|
|
837
1024
|
}
|
|
838
|
-
function printSummary(
|
|
1025
|
+
function printSummary(appName, ports, publication) {
|
|
839
1026
|
console.log("");
|
|
840
|
-
for (const
|
|
841
|
-
|
|
842
|
-
console.log(` ${label} is running at http://localhost:${port}`);
|
|
1027
|
+
for (const line of summaryLines(appName, ports, publication)) {
|
|
1028
|
+
console.log(line);
|
|
843
1029
|
}
|
|
844
1030
|
console.log("\n Press Ctrl+C to stop all processes.");
|
|
845
1031
|
}
|
|
@@ -914,8 +1100,8 @@ export async function launchStatus(opts = {}) {
|
|
|
914
1100
|
console.log(`Updated: ${state.updatedAt}`);
|
|
915
1101
|
if (Object.keys(state.ports).length > 0) {
|
|
916
1102
|
console.log("\nComponents:");
|
|
917
|
-
for (const
|
|
918
|
-
console.log(
|
|
1103
|
+
for (const line of statusLines(state.ports, state)) {
|
|
1104
|
+
console.log(line);
|
|
919
1105
|
}
|
|
920
1106
|
}
|
|
921
1107
|
if (Object.keys(state.resources).length > 0) {
|
package/dist/redact.js
CHANGED
|
@@ -73,7 +73,19 @@ export function clearRegisteredSecrets() {
|
|
|
73
73
|
// `scheme://user:password@host` — the password group is everything between the
|
|
74
74
|
// first `:` after the userinfo and the `@`. Userinfo cannot contain `/`, `@`,
|
|
75
75
|
// or whitespace, which bounds the match to a single URL.
|
|
76
|
-
|
|
76
|
+
//
|
|
77
|
+
// The scheme repetition is bounded rather than `*`: unbounded, every starting
|
|
78
|
+
// offset in a long run of scheme-legal characters rescans that whole run
|
|
79
|
+
// looking for `://`, which is quadratic in the input and lets a log line DoS
|
|
80
|
+
// the redactor that is supposed to protect it (CWE-1333).
|
|
81
|
+
//
|
|
82
|
+
// The bound excludes no URL, and not because schemes are short — RFC 3986 sets
|
|
83
|
+
// no ceiling, and `microsoft.windows.camera.multipicker` is 36 characters. It
|
|
84
|
+
// is because the pattern is unanchored: against a longer scheme the match
|
|
85
|
+
// simply starts further into it and the password still redacts. Raising the
|
|
86
|
+
// bound to "fit the longest scheme" would restore the quadratic scan for no
|
|
87
|
+
// gain.
|
|
88
|
+
const CREDENTIAL_URL = /([a-zA-Z][a-zA-Z0-9+.-]{0,31}:\/\/[^\s/:@]+:)([^\s/@]+)(@)/g;
|
|
77
89
|
/**
|
|
78
90
|
* Scrub registered secrets and URL-embedded credentials out of `text`.
|
|
79
91
|
*
|
package/dist/state.d.ts
CHANGED
|
@@ -108,6 +108,32 @@ export interface LaunchState {
|
|
|
108
108
|
* answers.
|
|
109
109
|
*/
|
|
110
110
|
appUrl?: string;
|
|
111
|
+
/**
|
|
112
|
+
* The `ports` key — this provider allocates one port per component, so a
|
|
113
|
+
* component name — of the app's primary endpoint, the one `$app.*` reads,
|
|
114
|
+
* when a declared `https-origin` names that endpoint (any protocol, D-60
|
|
115
|
+
* rule 4) or its effective listener is `http` or `https`; absent for a
|
|
116
|
+
* positional `ws`/`tcp`/`udp`/`grpc` primary (§7, D-58 rule 2). See
|
|
117
|
+
* `printedPrimaryEndpoint`. Recorded at `up` beside `appUrl`
|
|
118
|
+
* so `status`, which never reads the Launchfile, prints the supplied URL on
|
|
119
|
+
* that one key and no other (D-58 rule 4). Optional for backward
|
|
120
|
+
* compatibility: a state file without it prints this provider's own address
|
|
121
|
+
* on every key.
|
|
122
|
+
*/
|
|
123
|
+
primaryEndpoint?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Fingerprint of the prepare inputs (`install ?? build` command plus the
|
|
126
|
+
* dependency manifests and lockfiles in its working directory) at the last
|
|
127
|
+
* successful prepare, keyed by component name. It is what makes prepare run
|
|
128
|
+
* on demand rather than on every `up` (D-38): a component whose current
|
|
129
|
+
* fingerprint matches its recorded one has nothing to install.
|
|
130
|
+
*
|
|
131
|
+
* An entry is written only after its command exits zero, so a failed prepare
|
|
132
|
+
* is retried on the next `up`. Optional for backward compatibility: a state
|
|
133
|
+
* file written before this existed has no entries, so the next `up` prepares
|
|
134
|
+
* every component once and records them.
|
|
135
|
+
*/
|
|
136
|
+
prepared?: Record<string, string>;
|
|
111
137
|
}
|
|
112
138
|
export declare function hashLaunchfile(content: string): string;
|
|
113
139
|
/** Load state from disk, or return null if none exists */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchfile/macos-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "macOS dev provider for Launchfile — run apps locally via brew services and native runtimes",
|
|
5
5
|
"os": [
|
|
6
6
|
"darwin"
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"directory": "providers/macos-dev"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@launchfile/sdk": "^0.
|
|
39
|
+
"@launchfile/sdk": "^0.13.0",
|
|
40
40
|
"semver": "^7.7.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@biomejs/biome": "^2.
|
|
44
|
-
"@types/bun": "^1.
|
|
43
|
+
"@biomejs/biome": "^2.5.14",
|
|
44
|
+
"@types/bun": "^1.4.2",
|
|
45
45
|
"@types/semver": "^7.7.1",
|
|
46
46
|
"typescript": "^7.0.2",
|
|
47
|
-
"vitest": "^
|
|
47
|
+
"vitest": "^5.0.2"
|
|
48
48
|
}
|
|
49
49
|
}
|