@launchfile/macos-dev 0.12.0 → 0.14.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 +69 -1
- package/dist/provider.js +201 -56
- package/dist/redact.js +13 -1
- package/dist/shell.js +15 -23
- package/dist/state.d.ts +26 -0
- package/package.json +6 -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.
|
|
@@ -184,7 +186,73 @@ export declare function applyResourceUseRefusals(launch: NormalizedLaunch): "ok"
|
|
|
184
186
|
* URL (D-58) the names are whoever routes that URL's to send here (rule 7).
|
|
185
187
|
*/
|
|
186
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>;
|
|
187
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
|
+
* `verb` is the phrase between the label and the address; a dry run passes
|
|
250
|
+
* "would be reachable at" because nothing has started.
|
|
251
|
+
* Pure, so the placement of the supplied URL is testable without a launch.
|
|
252
|
+
*/
|
|
253
|
+
export declare function summaryLines(appName: string, ports: Record<string, number>, publication?: PrintedPublication, verb?: string): string[];
|
|
254
|
+
/** The "Components:" lines `status` prints, one per `ports` key. */
|
|
255
|
+
export declare function statusLines(ports: Record<string, number>, publication?: PrintedPublication): string[];
|
|
188
256
|
export declare function launchDown(opts?: {
|
|
189
257
|
destroy?: boolean;
|
|
190
258
|
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 { AT_APP_HOST, atDeclarations, atEntryLabel, 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))) {
|
|
@@ -343,6 +348,67 @@ function printAtReports(reports) {
|
|
|
343
348
|
for (const report of reports)
|
|
344
349
|
console.warn(` Warning: ${report}`);
|
|
345
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
|
+
}
|
|
346
412
|
export async function launchUp(opts = {}) {
|
|
347
413
|
const projectDir = opts.projectDir ?? process.cwd();
|
|
348
414
|
// Publication context (D-58): validated and normalized before anything is
|
|
@@ -392,6 +458,28 @@ export async function launchUp(opts = {}) {
|
|
|
392
458
|
const startSet = new Set(selection.start);
|
|
393
459
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => startSet.has(n)));
|
|
394
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);
|
|
395
483
|
// 2a. Host capabilities are granted or refused, never provisioned (D-44,
|
|
396
484
|
// PROVIDERS.md §11). This provider runs processes directly on the host and
|
|
397
485
|
// grants none of them, so a component with a required capability is
|
|
@@ -407,21 +495,8 @@ export async function launchUp(opts = {}) {
|
|
|
407
495
|
// 2a-bis. A required `https-origin` (D-60 rule 5, PROVIDERS.md §10 item 5)
|
|
408
496
|
// is satisfied by the publication context when its scheme is https —
|
|
409
497
|
// supplied on this run, or recorded by an earlier one and preserved on
|
|
410
|
-
// omission
|
|
411
|
-
//
|
|
412
|
-
// Otherwise it is refused in the same way as a host capability: starting
|
|
413
|
-
// the component anyway is the silent success the type removes.
|
|
414
|
-
let state = await loadState(projectDir);
|
|
415
|
-
if (!state) {
|
|
416
|
-
state = initState(launch.name, launchfileContent);
|
|
417
|
-
}
|
|
418
|
-
// Publication context (D-58), the same preservation rule the docker provider
|
|
419
|
-
// applies: a supplied value replaces the recorded one — the derived $app.*
|
|
420
|
-
// env recomputes below from it (D-49) — while omission keeps what is
|
|
421
|
-
// recorded, so a later plain `up` cannot silently flip a proxied deployment
|
|
422
|
-
// back to localhost.
|
|
423
|
-
if (suppliedAppUrl !== undefined)
|
|
424
|
-
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.
|
|
425
500
|
if (applyHttpsOriginRefusals(launch, state.appUrl) === "none-left") {
|
|
426
501
|
console.error("Every selected component requires a public HTTPS origin this provider cannot supply.");
|
|
427
502
|
process.exit(1);
|
|
@@ -467,9 +542,22 @@ export async function launchUp(opts = {}) {
|
|
|
467
542
|
}
|
|
468
543
|
// The optional mood of `https-origin` (D-60 rule 6): satisfied, it is
|
|
469
544
|
// wired like any other resource at step 8; unsatisfied, the component
|
|
470
|
-
// still runs, its set_env is absent, and the shortfall is named.
|
|
471
|
-
|
|
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
|
+
}
|
|
472
559
|
continue;
|
|
560
|
+
}
|
|
473
561
|
for (const sup of c.supports ?? []) {
|
|
474
562
|
if (sup.type !== HTTPS_ORIGIN)
|
|
475
563
|
continue;
|
|
@@ -610,7 +698,7 @@ export async function launchUp(opts = {}) {
|
|
|
610
698
|
for (const key of storageIndex.unusedKeys(usedStorageKeys)) {
|
|
611
699
|
console.warn(` Warning: --storage ${key} matches no \`content: operator\` volume — ignored`);
|
|
612
700
|
}
|
|
613
|
-
// 3. State: loaded
|
|
701
|
+
// 3. State: loaded ahead of step 2a, where the refusals read it.
|
|
614
702
|
// 4. Ensure directories
|
|
615
703
|
await ensureDirs(projectDir);
|
|
616
704
|
// 5. Generate secrets
|
|
@@ -712,10 +800,17 @@ export async function launchUp(opts = {}) {
|
|
|
712
800
|
// 7. Allocate ports
|
|
713
801
|
const componentPorts = await allocatePorts(launch.components, launch.name, state.ports);
|
|
714
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);
|
|
715
808
|
// 8. Build resolver context (including $app.* properties from D-33). A
|
|
716
809
|
// satisfied `https-origin` registers `url` — the same string as `$app.url`
|
|
717
810
|
// (D-60 rule 4) — so its set_env resolves like any provisioned resource's.
|
|
718
|
-
|
|
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);
|
|
719
814
|
// `$app.endpoints.<name>.*` resolves "" here (D-63 rule 4, #294). Said
|
|
720
815
|
// once per `up`, and only when the file asks, so the empty value is not a
|
|
721
816
|
// silent one (PROVIDERS.md §10 item 8).
|
|
@@ -803,25 +898,20 @@ export async function launchUp(opts = {}) {
|
|
|
803
898
|
await saveState(projectDir, state);
|
|
804
899
|
if (opts.dryRun) {
|
|
805
900
|
console.log("\n[dry-run] Would now run build, release, and start commands.");
|
|
806
|
-
printSummary(launch, componentPorts,
|
|
901
|
+
printSummary(launch.name, componentPorts, state, "would be reachable at");
|
|
807
902
|
return;
|
|
808
903
|
}
|
|
809
|
-
// 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.
|
|
810
907
|
if (!opts.noBuild) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
env: allEnvs[name],
|
|
819
|
-
// Installs/compiles routinely exceed the 2-minute shell default;
|
|
820
|
-
// honor a declared timeout, else allow 10 minutes.
|
|
821
|
-
timeout: declaredTimeout(prepare?.timeout, `prepare [${name}]`) ?? 600_000,
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
}
|
|
908
|
+
await runSourcePrepare(launch, {
|
|
909
|
+
projectDir,
|
|
910
|
+
state,
|
|
911
|
+
envs: allEnvs,
|
|
912
|
+
fallbackCommand: pm?.installCommand,
|
|
913
|
+
labelComponents: componentNames.length > 1,
|
|
914
|
+
});
|
|
825
915
|
}
|
|
826
916
|
// 15. Run release commands (migrations) \u2014 mode-invariant (D-38)
|
|
827
917
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
@@ -865,24 +955,79 @@ export async function launchUp(opts = {}) {
|
|
|
865
955
|
await saveState(projectDir, finalState);
|
|
866
956
|
process.exit(0);
|
|
867
957
|
});
|
|
868
|
-
|
|
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
|
+
}
|
|
869
987
|
console.log("");
|
|
870
988
|
console.log(` \u2713 All components started`);
|
|
871
|
-
// Record spawned pids so `launch down` can stop them from another shell or
|
|
872
|
-
// after this foreground session ends (closes #49). Backward compatible: the
|
|
873
|
-
// field is optional and absent in pre-existing state files.
|
|
874
|
-
state.processes = pm2.getRecordedProcesses();
|
|
875
989
|
// 17. Print summary
|
|
876
|
-
printSummary(launch, componentPorts,
|
|
990
|
+
printSummary(launch.name, componentPorts, state);
|
|
877
991
|
printAtReports(atReports(launch, componentPorts, suppliedAppUrl));
|
|
878
|
-
// Save final state (now including recorded pids)
|
|
879
|
-
await saveState(projectDir, state);
|
|
880
992
|
}
|
|
881
|
-
|
|
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
|
+
* `verb` is the phrase between the label and the address; a dry run passes
|
|
1014
|
+
* "would be reachable at" because nothing has started.
|
|
1015
|
+
* Pure, so the placement of the supplied URL is testable without a launch.
|
|
1016
|
+
*/
|
|
1017
|
+
export function summaryLines(appName, ports, publication, verb = "is running at") {
|
|
1018
|
+
return Object.entries(ports).map(([name, port]) => {
|
|
1019
|
+
const label = name === "default" ? appName : name;
|
|
1020
|
+
return ` ${label} ${verb} ${componentAddress(name, port, publication)}`;
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/** The "Components:" lines `status` prints, one per `ports` key. */
|
|
1024
|
+
export function statusLines(ports, publication) {
|
|
1025
|
+
return Object.entries(ports).map(([name, port]) => ` ${name}: ${componentAddress(name, port, publication)}`);
|
|
1026
|
+
}
|
|
1027
|
+
function printSummary(appName, ports, publication, verb) {
|
|
882
1028
|
console.log("");
|
|
883
|
-
for (const
|
|
884
|
-
|
|
885
|
-
console.log(` ${label} is running at http://localhost:${port}`);
|
|
1029
|
+
for (const line of summaryLines(appName, ports, publication, verb)) {
|
|
1030
|
+
console.log(line);
|
|
886
1031
|
}
|
|
887
1032
|
console.log("\n Press Ctrl+C to stop all processes.");
|
|
888
1033
|
}
|
|
@@ -957,8 +1102,8 @@ export async function launchStatus(opts = {}) {
|
|
|
957
1102
|
console.log(`Updated: ${state.updatedAt}`);
|
|
958
1103
|
if (Object.keys(state.ports).length > 0) {
|
|
959
1104
|
console.log("\nComponents:");
|
|
960
|
-
for (const
|
|
961
|
-
console.log(
|
|
1105
|
+
for (const line of statusLines(state.ports, state)) {
|
|
1106
|
+
console.log(line);
|
|
962
1107
|
}
|
|
963
1108
|
}
|
|
964
1109
|
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/shell.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* own app (`commands:`, `health:`, `release:`), where shell syntax is the
|
|
13
13
|
* documented contract. Never build one of these by interpolation.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { execFile as cpExecFile, } from "node:child_process";
|
|
16
16
|
import { redactSecrets } from "./redact.js";
|
|
17
17
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
18
18
|
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
@@ -35,11 +35,10 @@ function failure(display, result) {
|
|
|
35
35
|
return Object.assign(new Error(`Command failed: ${redactSecrets(display)}\n${redactSecrets(result.stderr)}`), { result });
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Spawn `cmd` with `args` via `execFile` and settle with a structured result.
|
|
39
|
+
* `display` is what the user sees echoed and what a failure message names.
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
const display = [cmd, ...args].join(" ");
|
|
41
|
+
function run(cmd, args, display, opts) {
|
|
43
42
|
if (!opts.silent) {
|
|
44
43
|
// An argument can carry a resolved secret (a generated DB password, a
|
|
45
44
|
// credential-bearing URL). Scrub before echo.
|
|
@@ -61,6 +60,13 @@ export async function shell(cmd, args, opts = {}) {
|
|
|
61
60
|
});
|
|
62
61
|
});
|
|
63
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Run a command with an argument array. Arguments reach the OS directly, so
|
|
65
|
+
* they are never parsed as shell syntax.
|
|
66
|
+
*/
|
|
67
|
+
export async function shell(cmd, args, opts = {}) {
|
|
68
|
+
return run(cmd, args, [cmd, ...args].join(" "), opts);
|
|
69
|
+
}
|
|
64
70
|
/** Run a command with an argument array, return true if exit code is 0. */
|
|
65
71
|
export async function shellOk(cmd, args, opts) {
|
|
66
72
|
const result = await shell(cmd, args, {
|
|
@@ -79,23 +85,9 @@ export async function shellOk(cmd, args, opts) {
|
|
|
79
85
|
* verbatim — building one by interpolating a value makes it injectable.
|
|
80
86
|
*/
|
|
81
87
|
export async function shellScript(command, opts = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cwd: opts.cwd,
|
|
87
|
-
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
88
|
-
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
89
|
-
maxBuffer: MAX_BUFFER,
|
|
90
|
-
};
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
cpExec(command, execOpts, (error, stdout, stderr) => {
|
|
93
|
-
const result = toResult(error, stdout, stderr);
|
|
94
|
-
if (error && !opts.allowFailure)
|
|
95
|
-
reject(failure(command, result));
|
|
96
|
-
else
|
|
97
|
-
resolve(result);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
88
|
+
// The command string is one argv element of an explicit `/bin/sh -c`, the
|
|
89
|
+
// same shape `@launchfile/docker` uses. The shell parses the author's
|
|
90
|
+
// string; nothing this provider adds is ever spliced into it.
|
|
91
|
+
return run("/bin/sh", ["-c", command], command, opts);
|
|
100
92
|
}
|
|
101
93
|
//# sourceMappingURL=shell.js.map
|
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.14.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,15 @@
|
|
|
36
36
|
"directory": "providers/macos-dev"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@launchfile/sdk": "^0.
|
|
39
|
+
"@launchfile/sdk": "^0.14.0",
|
|
40
40
|
"semver": "^7.7.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@biomejs/biome": "^2.
|
|
44
|
-
"@
|
|
43
|
+
"@biomejs/biome": "^2.5.15",
|
|
44
|
+
"@launchfile/docker": "^0.14.0",
|
|
45
|
+
"@types/bun": "^1.4.2",
|
|
45
46
|
"@types/semver": "^7.7.1",
|
|
46
47
|
"typescript": "^7.0.2",
|
|
47
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^5.0.3"
|
|
48
49
|
}
|
|
49
50
|
}
|