@launchfile/macos-dev 0.4.0 → 0.8.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/env-writer.js +4 -2
- package/dist/provider.d.ts +48 -0
- package/dist/provider.js +195 -4
- package/dist/state.d.ts +12 -0
- package/dist/storage.d.ts +10 -2
- package/dist/storage.js +29 -4
- package/package.json +2 -2
package/dist/env-writer.js
CHANGED
|
@@ -26,8 +26,10 @@ export function computeAppProperties(launch, componentPorts) {
|
|
|
26
26
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
27
27
|
// Only endpoints explicitly marked `exposed: true` are reachable from
|
|
28
28
|
// outside the host (D-27), so only they can be the app's public address.
|
|
29
|
-
//
|
|
30
|
-
//
|
|
29
|
+
// This provider has no orchestrator-facing publication channel (#294), so
|
|
30
|
+
// $app.* always comes from its own routing strategy. The docker provider
|
|
31
|
+
// answers by this same rule only when no orchestrator supplies an appUrl
|
|
32
|
+
// (PROVIDERS.md §7).
|
|
31
33
|
const hasExposed = component.provides?.some((p) => p.exposed === true) ?? false;
|
|
32
34
|
if (hasExposed && componentPorts[name]) {
|
|
33
35
|
primaryPort = componentPorts[name];
|
package/dist/provider.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface LaunchUpOpts {
|
|
|
26
26
|
* so both yield the identical running topology (P-5). Empty = all components.
|
|
27
27
|
*/
|
|
28
28
|
components?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Host paths for `content: operator` volumes (D-50 rule 1), keyed as the
|
|
31
|
+
* operator typed them: `<volume>`, or `<component>.<volume>` where a volume
|
|
32
|
+
* name is ambiguous. Relative paths resolve against the current directory.
|
|
33
|
+
*/
|
|
34
|
+
storage?: Record<string, string>;
|
|
29
35
|
}
|
|
30
36
|
/**
|
|
31
37
|
* Components this provider must refuse, mapped to the capabilities it cannot
|
|
@@ -61,6 +67,48 @@ export declare function scheduleWarning(component: string, schedule: string): st
|
|
|
61
67
|
* report success over an empty set.
|
|
62
68
|
*/
|
|
63
69
|
export declare function applyHostCapabilityRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
70
|
+
/**
|
|
71
|
+
* Components this provider must refuse because they require a public HTTPS
|
|
72
|
+
* origin (D-60 rule 5), mapped to the entries it cannot satisfy.
|
|
73
|
+
*
|
|
74
|
+
* This provider has no orchestrator-facing publication channel ([#294]) and no
|
|
75
|
+
* edge of its own, so it can neither provision the origin nor accept a supplied
|
|
76
|
+
* one — it refuses, which PROVIDERS.md §10 item 5 makes conformant. `supports:`
|
|
77
|
+
* entries are not refused: the component runs, degraded.
|
|
78
|
+
*/
|
|
79
|
+
export declare function refusedHttpsOrigins(launch: NormalizedLaunch): Map<string, string[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Remove every component whose required `https-origin` this provider cannot
|
|
82
|
+
* satisfy, and say so on stderr. Same shape and same reason as
|
|
83
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal — a component
|
|
84
|
+
* left in the map goes on to be installed, wired, registered and started, and
|
|
85
|
+
* an app that declared it needs HTTPS would run over plain HTTP behind a
|
|
86
|
+
* message nobody acted on.
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyHttpsOriginRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
89
|
+
/**
|
|
90
|
+
* Components this provider must refuse because native TLS was selected on a
|
|
91
|
+
* listener it cannot activate (D-61 rule 5), mapped to the entries.
|
|
92
|
+
*
|
|
93
|
+
* Selection on this provider is `--with-optional`: it is the only way a
|
|
94
|
+
* `supports:` entry is ever turned on here. Without the flag a certificate
|
|
95
|
+
* binding is simply inactive and the component runs its declared HTTP
|
|
96
|
+
* baseline, which is correct (D-8) — so this returns nothing.
|
|
97
|
+
*
|
|
98
|
+
* With the flag, the operator asked for the optional capability and this
|
|
99
|
+
* provider has no way to deliver it: it has no supplied-resource channel to
|
|
100
|
+
* receive `cert_file`/`key_file` through, and no certificate of its own to
|
|
101
|
+
* offer. Refusing is what PROVIDERS.md §10 item 5 makes conformant; falling
|
|
102
|
+
* back to HTTP on a listener the operator asked to secure is the silent
|
|
103
|
+
* success the decision forbids.
|
|
104
|
+
*/
|
|
105
|
+
export declare function refusedCertificates(launch: NormalizedLaunch, withOptional: boolean): Map<string, string[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Remove every component whose selected certificate binding this provider
|
|
108
|
+
* cannot activate, and say so on stderr. Same shape and same reason as
|
|
109
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal.
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyCertificateRefusals(launch: NormalizedLaunch, withOptional: boolean): "ok" | "none-left";
|
|
64
112
|
export declare function launchUp(opts?: LaunchUpOpts): Promise<void>;
|
|
65
113
|
export declare function launchDown(opts?: {
|
|
66
114
|
destroy?: boolean;
|
package/dist/provider.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Reads a Launchfile, provisions resources, resolves env vars,
|
|
5
5
|
* installs runtimes, and starts all components.
|
|
6
6
|
*/
|
|
7
|
+
import { accessSync, constants as fsConstants } from "node:fs";
|
|
7
8
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, unsuppliedRequiredEnv, } from "@launchfile/sdk";
|
|
9
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
10
|
+
import { CERTIFICATE, certificateBindings, indexOperatorStoragePaths, MissingOperatorStoragePathError, readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, UnboundOperatorStorageError, unsuppliedRequiredEnv, } from "@launchfile/sdk";
|
|
10
11
|
import { checkPrereqs } from "./prereqs.js";
|
|
11
12
|
import { loadState, initState, saveState, ensureDirs } from "./state.js";
|
|
12
13
|
import { buildResolverContext, computeAppProperties, resolveComponentEnv, generateSecrets, resolveGenerators, writeEnvFile, } from "./env-writer.js";
|
|
@@ -44,6 +45,16 @@ function declaredTimeout(timeout, label) {
|
|
|
44
45
|
throw new Error(`${label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
/** Whether a path exists and this process can read it (D-50 rule 2, row 3). */
|
|
49
|
+
function isReadable(path) {
|
|
50
|
+
try {
|
|
51
|
+
accessSync(path, fsConstants.R_OK);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
47
58
|
/**
|
|
48
59
|
* Components this provider must refuse, mapped to the capabilities it cannot
|
|
49
60
|
* grant (D-44, PROVIDERS.md §11). Both spellings fold together so the `host:`
|
|
@@ -110,6 +121,96 @@ export function applyHostCapabilityRefusals(launch) {
|
|
|
110
121
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
111
122
|
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
112
123
|
}
|
|
124
|
+
/** The backing-service type that declares the app's public HTTPS origin (D-60). */
|
|
125
|
+
const HTTPS_ORIGIN = "https-origin";
|
|
126
|
+
/**
|
|
127
|
+
* Components this provider must refuse because they require a public HTTPS
|
|
128
|
+
* origin (D-60 rule 5), mapped to the entries it cannot satisfy.
|
|
129
|
+
*
|
|
130
|
+
* This provider has no orchestrator-facing publication channel ([#294]) and no
|
|
131
|
+
* edge of its own, so it can neither provision the origin nor accept a supplied
|
|
132
|
+
* one — it refuses, which PROVIDERS.md §10 item 5 makes conformant. `supports:`
|
|
133
|
+
* entries are not refused: the component runs, degraded.
|
|
134
|
+
*/
|
|
135
|
+
export function refusedHttpsOrigins(launch) {
|
|
136
|
+
const refused = new Map();
|
|
137
|
+
for (const [name, c] of Object.entries(launch.components)) {
|
|
138
|
+
const entries = (c.requires ?? [])
|
|
139
|
+
.filter((r) => r.type === HTTPS_ORIGIN)
|
|
140
|
+
.map((r) => `${r.name ?? r.type} (endpoint "${r.endpoint ?? "?"}")`);
|
|
141
|
+
if (entries.length > 0)
|
|
142
|
+
refused.set(name, entries);
|
|
143
|
+
}
|
|
144
|
+
return refused;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Remove every component whose required `https-origin` this provider cannot
|
|
148
|
+
* satisfy, and say so on stderr. Same shape and same reason as
|
|
149
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal — a component
|
|
150
|
+
* left in the map goes on to be installed, wired, registered and started, and
|
|
151
|
+
* an app that declared it needs HTTPS would run over plain HTTP behind a
|
|
152
|
+
* message nobody acted on.
|
|
153
|
+
*/
|
|
154
|
+
export function applyHttpsOriginRefusals(launch) {
|
|
155
|
+
const refused = refusedHttpsOrigins(launch);
|
|
156
|
+
for (const [name, entries] of refused) {
|
|
157
|
+
console.error(` Refused: ${name} requires a public HTTPS origin this provider cannot supply ` +
|
|
158
|
+
`(${entries.join("; ")}) — it has no edge and no publication channel — component not started`);
|
|
159
|
+
}
|
|
160
|
+
if (refused.size === 0)
|
|
161
|
+
return "ok";
|
|
162
|
+
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
163
|
+
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Components this provider must refuse because native TLS was selected on a
|
|
167
|
+
* listener it cannot activate (D-61 rule 5), mapped to the entries.
|
|
168
|
+
*
|
|
169
|
+
* Selection on this provider is `--with-optional`: it is the only way a
|
|
170
|
+
* `supports:` entry is ever turned on here. Without the flag a certificate
|
|
171
|
+
* binding is simply inactive and the component runs its declared HTTP
|
|
172
|
+
* baseline, which is correct (D-8) — so this returns nothing.
|
|
173
|
+
*
|
|
174
|
+
* With the flag, the operator asked for the optional capability and this
|
|
175
|
+
* provider has no way to deliver it: it has no supplied-resource channel to
|
|
176
|
+
* receive `cert_file`/`key_file` through, and no certificate of its own to
|
|
177
|
+
* offer. Refusing is what PROVIDERS.md §10 item 5 makes conformant; falling
|
|
178
|
+
* back to HTTP on a listener the operator asked to secure is the silent
|
|
179
|
+
* success the decision forbids.
|
|
180
|
+
*/
|
|
181
|
+
export function refusedCertificates(launch, withOptional) {
|
|
182
|
+
const refused = new Map();
|
|
183
|
+
if (!withOptional)
|
|
184
|
+
return refused;
|
|
185
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
186
|
+
const declared = new Set((component.supports ?? [])
|
|
187
|
+
.filter((s) => !s.host && s.type === CERTIFICATE)
|
|
188
|
+
.map((s) => s.name ?? s.type));
|
|
189
|
+
const entries = certificateBindings(component)
|
|
190
|
+
.filter(({ certificate }) => declared.has(certificate))
|
|
191
|
+
.map(({ entry, certificate }) => `${certificate} (endpoint ${entry.name === undefined ? "(unnamed)" : `"${entry.name}"`})`);
|
|
192
|
+
if (entries.length > 0)
|
|
193
|
+
refused.set(name, entries);
|
|
194
|
+
}
|
|
195
|
+
return refused;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Remove every component whose selected certificate binding this provider
|
|
199
|
+
* cannot activate, and say so on stderr. Same shape and same reason as
|
|
200
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal.
|
|
201
|
+
*/
|
|
202
|
+
export function applyCertificateRefusals(launch, withOptional) {
|
|
203
|
+
const refused = refusedCertificates(launch, withOptional);
|
|
204
|
+
for (const [name, entries] of refused) {
|
|
205
|
+
console.error(` Refused: ${name} selected native TLS and this provider cannot activate it ` +
|
|
206
|
+
`(${entries.join("; ")}) — it has no channel to receive cert_file/key_file — ` +
|
|
207
|
+
"component not started; it never falls back to HTTP");
|
|
208
|
+
}
|
|
209
|
+
if (refused.size === 0)
|
|
210
|
+
return "ok";
|
|
211
|
+
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
212
|
+
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
213
|
+
}
|
|
113
214
|
export async function launchUp(opts = {}) {
|
|
114
215
|
const projectDir = opts.projectDir ?? process.cwd();
|
|
115
216
|
// 1. Check prerequisites
|
|
@@ -166,6 +267,22 @@ export async function launchUp(opts = {}) {
|
|
|
166
267
|
console.error("Every selected component requires a host capability this provider cannot grant.");
|
|
167
268
|
process.exit(1);
|
|
168
269
|
}
|
|
270
|
+
// 2a-bis. A required `https-origin` is refused for the same reason and in
|
|
271
|
+
// the same way (D-60 rule 5, PROVIDERS.md §10 item 5): this provider has
|
|
272
|
+
// no edge and no publication channel, so it cannot satisfy the entry, and
|
|
273
|
+
// starting the component anyway is the silent success the type removes.
|
|
274
|
+
if (applyHttpsOriginRefusals(launch) === "none-left") {
|
|
275
|
+
console.error("Every selected component requires a public HTTPS origin this provider cannot supply.");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
// 2a-ter. A SELECTED certificate binding (`--with-optional`) is refused on
|
|
279
|
+
// the same grounds (D-61 rule 5): no channel to receive the material, so
|
|
280
|
+
// the listener the operator asked to secure would come up cleartext.
|
|
281
|
+
// Unselected, the binding is inactive and the baseline is correct (D-8).
|
|
282
|
+
if (applyCertificateRefusals(launch, opts.withOptional === true) === "none-left") {
|
|
283
|
+
console.error("Every selected component selected native TLS this provider cannot activate.");
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
169
286
|
// An optional capability is not refused — the component runs, degraded.
|
|
170
287
|
for (const [name, c] of Object.entries(launch.components)) {
|
|
171
288
|
for (const sup of c.supports ?? []) {
|
|
@@ -174,6 +291,12 @@ export async function launchUp(opts = {}) {
|
|
|
174
291
|
`${capability}=${String(value)} not granted — running degraded`);
|
|
175
292
|
}
|
|
176
293
|
}
|
|
294
|
+
for (const sup of c.supports ?? []) {
|
|
295
|
+
if (sup.type !== HTTPS_ORIGIN)
|
|
296
|
+
continue;
|
|
297
|
+
console.warn(` Warning: ${name}: optional public HTTPS origin ` +
|
|
298
|
+
`${sup.name ?? sup.type} (endpoint "${sup.endpoint ?? "?"}") not satisfied — running degraded`);
|
|
299
|
+
}
|
|
177
300
|
}
|
|
178
301
|
const componentNames = Object.keys(launch.components);
|
|
179
302
|
// 2b. Source-mode guard (D-38) — fail fast before provisioning anything.
|
|
@@ -245,6 +368,69 @@ export async function launchUp(opts = {}) {
|
|
|
245
368
|
console.error(` ${missingRequired[0].key}=<value> launch up`);
|
|
246
369
|
process.exit(1);
|
|
247
370
|
}
|
|
371
|
+
// 2d. Operator-supplied storage (D-50 rules 1–2), settled here — before
|
|
372
|
+
// state, directories, resources, ports, runtimes or processes exist, and so
|
|
373
|
+
// before `--dry-run` returns. A marked volume with no path, or with one that
|
|
374
|
+
// is not on disk, fails the launch: an empty directory where the operator's
|
|
375
|
+
// library belongs is D-52's fabrication in storage form, and this provider
|
|
376
|
+
// creates neither.
|
|
377
|
+
//
|
|
378
|
+
// Scoped to the components this provider will actually run. An artifact
|
|
379
|
+
// component is warned about and skipped at step 16, so refusing the whole
|
|
380
|
+
// launch over storage it will never read would be a refusal about nothing —
|
|
381
|
+
// the same reason `@launchfile/docker` only examines components it
|
|
382
|
+
// translates. Its volumes simply stay unprovisioned at step 11.
|
|
383
|
+
//
|
|
384
|
+
// Unlike the host-capability refusal above, this throws rather than dropping
|
|
385
|
+
// the component: `@launchfile/docker` fails the whole launch for the same
|
|
386
|
+
// file, and one Launchfile must not yield two topologies (P-5).
|
|
387
|
+
const suppliedStorage = opts.storage
|
|
388
|
+
? Object.fromEntries(Object.entries(opts.storage).map(([key, path]) => [key, resolvePath(path)]))
|
|
389
|
+
: undefined;
|
|
390
|
+
const storageIndex = indexOperatorStoragePaths(launch, suppliedStorage);
|
|
391
|
+
const usedStorageKeys = new Set();
|
|
392
|
+
const unboundVolumes = [];
|
|
393
|
+
const storageBinds = [];
|
|
394
|
+
const operatorStorage = {};
|
|
395
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
396
|
+
if (!isSourceRunnable(component))
|
|
397
|
+
continue;
|
|
398
|
+
for (const [volName, vol] of Object.entries(component.storage ?? {})) {
|
|
399
|
+
if (vol.content !== "operator")
|
|
400
|
+
continue;
|
|
401
|
+
const supplied = storageIndex.lookup(name, volName);
|
|
402
|
+
if (!supplied) {
|
|
403
|
+
unboundVolumes.push({
|
|
404
|
+
component: name,
|
|
405
|
+
volume: volName,
|
|
406
|
+
flag: storageIndex.flagFor(name, volName),
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
usedStorageKeys.add(supplied.key);
|
|
411
|
+
storageBinds.push({
|
|
412
|
+
component: name,
|
|
413
|
+
volume: volName,
|
|
414
|
+
key: supplied.key,
|
|
415
|
+
hostPath: supplied.path,
|
|
416
|
+
containerPath: vol.path,
|
|
417
|
+
});
|
|
418
|
+
(operatorStorage[name] ??= {})[volName] = supplied.path;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (unboundVolumes.length > 0) {
|
|
422
|
+
throw new UnboundOperatorStorageError(unboundVolumes);
|
|
423
|
+
}
|
|
424
|
+
const unreadableBinds = storageBinds.filter((bind) => !isReadable(bind.hostPath));
|
|
425
|
+
if (unreadableBinds.length > 0) {
|
|
426
|
+
throw new MissingOperatorStoragePathError(unreadableBinds);
|
|
427
|
+
}
|
|
428
|
+
// A supplied key that bound nothing would otherwise vanish without a trace,
|
|
429
|
+
// so a typo'd name surfaces here. Row 4 keeps unmarked volumes untouched, so
|
|
430
|
+
// a key naming one is unused too.
|
|
431
|
+
for (const key of storageIndex.unusedKeys(usedStorageKeys)) {
|
|
432
|
+
console.warn(` Warning: --storage ${key} matches no \`content: operator\` volume — ignored`);
|
|
433
|
+
}
|
|
248
434
|
// 3. Load or init state
|
|
249
435
|
let state = await loadState(projectDir);
|
|
250
436
|
if (!state) {
|
|
@@ -348,7 +534,7 @@ export async function launchUp(opts = {}) {
|
|
|
348
534
|
// so it can be injected as $storage.<name>.path (D-39). Scoped per component.
|
|
349
535
|
const componentStorage = {};
|
|
350
536
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
351
|
-
const volumeMap = await provisionStorage(component.storage, name, projectDir);
|
|
537
|
+
const volumeMap = await provisionStorage(component.storage, name, projectDir, operatorStorage[name]);
|
|
352
538
|
const storageCtx = {};
|
|
353
539
|
for (const [volName, localPath] of Object.entries(volumeMap)) {
|
|
354
540
|
storageCtx[volName] = { path: localPath };
|
|
@@ -388,6 +574,9 @@ export async function launchUp(opts = {}) {
|
|
|
388
574
|
}
|
|
389
575
|
}
|
|
390
576
|
// 13. Save state before build (in case build fails, we still have resource state)
|
|
577
|
+
// The bound operator paths ride along so `env` can report the directory the
|
|
578
|
+
// app actually reads (D-50); a later `up` still has to supply them again.
|
|
579
|
+
state.operatorStorage = operatorStorage;
|
|
391
580
|
await saveState(projectDir, state);
|
|
392
581
|
if (opts.dryRun) {
|
|
393
582
|
console.log("\n[dry-run] Would now run build, release, and start commands.");
|
|
@@ -590,8 +779,10 @@ export async function launchEnv(opts = {}) {
|
|
|
590
779
|
continue;
|
|
591
780
|
// Resolved storage paths (D-39) — computed, not provisioned (no mkdir):
|
|
592
781
|
// `launchfile env` only prints, and the dirs already exist from `up`.
|
|
782
|
+
// A `content: operator` volume reads back the host path `up` bound
|
|
783
|
+
// (D-50), so what prints here is what the running app was given.
|
|
593
784
|
const storageCtx = {};
|
|
594
|
-
for (const [volName, localPath] of Object.entries(storagePaths(component.storage, name, projectDir))) {
|
|
785
|
+
for (const [volName, localPath] of Object.entries(storagePaths(component.storage, name, projectDir, state.operatorStorage?.[name]))) {
|
|
595
786
|
storageCtx[volName] = { path: localPath };
|
|
596
787
|
}
|
|
597
788
|
const { env, unsupplied } = resolveComponentEnv(component, context, resourceMap, storageCtx);
|
package/dist/state.d.ts
CHANGED
|
@@ -60,6 +60,18 @@ export interface LaunchState {
|
|
|
60
60
|
* compatibility: older state files omit it and load as a first run.
|
|
61
61
|
*/
|
|
62
62
|
generatedEnv?: Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Host paths bound to `content: operator` volumes on the last successful
|
|
65
|
+
* `up` (D-50), as `component → volume → path`. Recorded so `env` reports
|
|
66
|
+
* the directory the app actually reads, not the `.launchfile/` path an
|
|
67
|
+
* unmarked volume would have taken.
|
|
68
|
+
*
|
|
69
|
+
* It is not a substitute for supplying the paths: a later `up` without them
|
|
70
|
+
* refuses again, matching `@launchfile/docker`, which persists `appUrl` but
|
|
71
|
+
* never its storage paths. Optional for backward compatibility: state files
|
|
72
|
+
* written before this existed omit it.
|
|
73
|
+
*/
|
|
74
|
+
operatorStorage?: Record<string, Record<string, string>>;
|
|
63
75
|
}
|
|
64
76
|
export declare function hashLaunchfile(content: string): string;
|
|
65
77
|
/** Load state from disk, or return null if none exists */
|
package/dist/storage.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Maps named volumes to local directories under .launchfile/, and exposes the
|
|
5
5
|
* resolved local path per volume so the provider can inject it as
|
|
6
6
|
* `$storage.<name>.path` (D-39).
|
|
7
|
+
*
|
|
8
|
+
* A `content: operator` volume (D-50) is the exception: its content comes from
|
|
9
|
+
* a directory the operator already keeps, so this provider names that path and
|
|
10
|
+
* creates nothing. `up` refuses before provisioning when such a volume has no
|
|
11
|
+
* path, or has one that is not there.
|
|
7
12
|
*/
|
|
8
13
|
import type { StorageVolume } from "@launchfile/sdk";
|
|
9
14
|
/**
|
|
@@ -13,15 +18,18 @@ import type { StorageVolume } from "@launchfile/sdk";
|
|
|
13
18
|
* effects, so callers that only need the resolved paths (e.g. `launchfile env`)
|
|
14
19
|
* can use it without provisioning. Persistent volumes live under
|
|
15
20
|
* `.launchfile/storage/<component>/<name>`; ephemeral ones under `.../tmp/...`.
|
|
21
|
+
*
|
|
22
|
+
* `operatorPaths` carries the host paths supplied for this component's
|
|
23
|
+
* `content: operator` volumes, keyed by volume name.
|
|
16
24
|
*/
|
|
17
|
-
export declare function storagePaths(storage: Record<string, StorageVolume> | undefined, componentName: string, projectDir: string): Record<string, string>;
|
|
25
|
+
export declare function storagePaths(storage: Record<string, StorageVolume> | undefined, componentName: string, projectDir: string, operatorPaths?: Readonly<Record<string, string>>): Record<string, string>;
|
|
18
26
|
/**
|
|
19
27
|
* Create the local directories for a component's storage volumes and return the
|
|
20
28
|
* volume-name → local-path map (the home-#3 value the resolver injects as
|
|
21
29
|
* `$storage.<name>.path`, D-39). Previously the return was keyed by container
|
|
22
30
|
* path and discarded at the call site — the path is now captured and delivered.
|
|
23
31
|
*/
|
|
24
|
-
export declare function provisionStorage(storage: Record<string, StorageVolume> | undefined, componentName: string, projectDir: string): Promise<Record<string, string>>;
|
|
32
|
+
export declare function provisionStorage(storage: Record<string, StorageVolume> | undefined, componentName: string, projectDir: string, operatorPaths?: Readonly<Record<string, string>>): Promise<Record<string, string>>;
|
|
25
33
|
/** Clean up ephemeral (non-persistent) storage for a component */
|
|
26
34
|
export declare function cleanEphemeralStorage(storage: Record<string, StorageVolume> | undefined, componentName: string, projectDir: string): Promise<void>;
|
|
27
35
|
/** Clean up all storage (persistent + ephemeral) for a component */
|
package/dist/storage.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Maps named volumes to local directories under .launchfile/, and exposes the
|
|
5
5
|
* resolved local path per volume so the provider can inject it as
|
|
6
6
|
* `$storage.<name>.path` (D-39).
|
|
7
|
+
*
|
|
8
|
+
* A `content: operator` volume (D-50) is the exception: its content comes from
|
|
9
|
+
* a directory the operator already keeps, so this provider names that path and
|
|
10
|
+
* creates nothing. `up` refuses before provisioning when such a volume has no
|
|
11
|
+
* path, or has one that is not there.
|
|
7
12
|
*/
|
|
8
13
|
import { mkdir, rm } from "node:fs/promises";
|
|
9
14
|
import { join } from "node:path";
|
|
@@ -15,12 +20,26 @@ const STATE_DIR = ".launchfile";
|
|
|
15
20
|
* effects, so callers that only need the resolved paths (e.g. `launchfile env`)
|
|
16
21
|
* can use it without provisioning. Persistent volumes live under
|
|
17
22
|
* `.launchfile/storage/<component>/<name>`; ephemeral ones under `.../tmp/...`.
|
|
23
|
+
*
|
|
24
|
+
* `operatorPaths` carries the host paths supplied for this component's
|
|
25
|
+
* `content: operator` volumes, keyed by volume name.
|
|
18
26
|
*/
|
|
19
|
-
export function storagePaths(storage, componentName, projectDir) {
|
|
27
|
+
export function storagePaths(storage, componentName, projectDir, operatorPaths = {}) {
|
|
20
28
|
if (!storage)
|
|
21
29
|
return {};
|
|
22
30
|
const volumeMap = {};
|
|
23
31
|
for (const [name, volume] of Object.entries(storage)) {
|
|
32
|
+
if (volume.content === "operator") {
|
|
33
|
+
// D-50 row 1: the operator's directory IS the volume — this provider
|
|
34
|
+
// runs processes on the host, so binding it means naming that path.
|
|
35
|
+
// A marked volume with nothing supplied is left out of the map
|
|
36
|
+
// entirely rather than handed a `.launchfile/` path the operator's
|
|
37
|
+
// content is not in; `up` refuses that case before it gets here.
|
|
38
|
+
const supplied = operatorPaths[name];
|
|
39
|
+
if (supplied)
|
|
40
|
+
volumeMap[name] = supplied;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
24
43
|
const persistent = volume.persistent !== false; // default true
|
|
25
44
|
const subdir = persistent ? "storage" : "tmp";
|
|
26
45
|
volumeMap[name] = join(projectDir, STATE_DIR, subdir, componentName, name);
|
|
@@ -33,9 +52,15 @@ export function storagePaths(storage, componentName, projectDir) {
|
|
|
33
52
|
* `$storage.<name>.path`, D-39). Previously the return was keyed by container
|
|
34
53
|
* path and discarded at the call site — the path is now captured and delivered.
|
|
35
54
|
*/
|
|
36
|
-
export async function provisionStorage(storage, componentName, projectDir) {
|
|
37
|
-
const volumeMap = storagePaths(storage, componentName, projectDir);
|
|
38
|
-
for (const localPath of Object.
|
|
55
|
+
export async function provisionStorage(storage, componentName, projectDir, operatorPaths = {}) {
|
|
56
|
+
const volumeMap = storagePaths(storage, componentName, projectDir, operatorPaths);
|
|
57
|
+
for (const [name, localPath] of Object.entries(volumeMap)) {
|
|
58
|
+
// A `content: operator` volume is never created (D-50 rule 2, row 3):
|
|
59
|
+
// an empty directory where the operator's library belongs is the
|
|
60
|
+
// failure the marker exists to catch, and minting one here would
|
|
61
|
+
// reintroduce it through the channel's own flag.
|
|
62
|
+
if (storage?.[name]?.content === "operator")
|
|
63
|
+
continue;
|
|
39
64
|
await mkdir(localPath, { recursive: true });
|
|
40
65
|
}
|
|
41
66
|
return volumeMap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchfile/macos-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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,7 +36,7 @@
|
|
|
36
36
|
"directory": "providers/macos-dev"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@launchfile/sdk": "^0.
|
|
39
|
+
"@launchfile/sdk": "^0.8.0",
|
|
40
40
|
"semver": "^7.7.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|