@launchfile/macos-dev 0.3.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts +56 -8
- package/dist/bootstrap.js +170 -96
- package/dist/env-writer.d.ts +33 -7
- package/dist/env-writer.js +68 -14
- package/dist/health.d.ts +8 -2
- package/dist/health.js +11 -17
- package/dist/prereqs.js +2 -2
- package/dist/process-manager.js +2 -1
- package/dist/provider.d.ts +45 -7
- package/dist/provider.js +296 -32
- package/dist/redact.d.ts +55 -0
- package/dist/redact.js +92 -0
- package/dist/resources/identifiers.d.ts +22 -0
- package/dist/resources/identifiers.js +31 -0
- package/dist/resources/index.d.ts +3 -3
- package/dist/resources/index.js +21 -10
- package/dist/resources/mysql.d.ts +3 -1
- package/dist/resources/mysql.js +49 -11
- package/dist/resources/postgres.d.ts +4 -1
- package/dist/resources/postgres.js +63 -17
- package/dist/resources/redis.d.ts +3 -1
- package/dist/resources/redis.js +14 -4
- package/dist/resources/sqlite.js +2 -2
- package/dist/resources/types.d.ts +21 -3
- package/dist/runtimes/bun.js +2 -2
- package/dist/runtimes/installed-versions.d.ts +12 -0
- package/dist/runtimes/installed-versions.js +21 -0
- package/dist/runtimes/node.js +13 -8
- package/dist/runtimes/python.js +14 -7
- package/dist/runtimes/ruby.js +13 -8
- package/dist/secret-generator.js +11 -3
- package/dist/shell.d.ts +28 -6
- package/dist/shell.js +83 -28
- package/dist/state.d.ts +22 -0
- package/dist/state.js +12 -1
- package/dist/storage.d.ts +10 -2
- package/dist/storage.js +29 -4
- package/package.json +6 -3
package/dist/runtimes/python.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { shell, shellOk } from "../shell.js";
|
|
7
|
+
import { isInstalled } from "./installed-versions.js";
|
|
7
8
|
export class PythonInstaller {
|
|
8
9
|
runtime = "python";
|
|
9
10
|
async detectVersion(projectDir) {
|
|
@@ -16,17 +17,23 @@ export class PythonInstaller {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
async install(version) {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// `version` is the verbatim contents of .python-version in the target repo
|
|
21
|
+
// — untrusted input. It goes in as an argv element so it can never be read
|
|
22
|
+
// as shell syntax.
|
|
23
|
+
if (!(await shellOk("which", ["pyenv"]))) {
|
|
24
|
+
await shell("brew", ["install", "pyenv"]);
|
|
21
25
|
}
|
|
22
|
-
if (!(await
|
|
23
|
-
await shell(
|
|
26
|
+
if (!(await isInstalled("pyenv", version))) {
|
|
27
|
+
await shell("pyenv", ["install", version]);
|
|
24
28
|
}
|
|
25
|
-
await shell(
|
|
29
|
+
await shell("pyenv", ["local", version]);
|
|
26
30
|
}
|
|
27
31
|
async shellEnv(_version) {
|
|
28
|
-
if (await shellOk("which pyenv")) {
|
|
29
|
-
const result = await shell("pyenv root", {
|
|
32
|
+
if (await shellOk("which", ["pyenv"])) {
|
|
33
|
+
const result = await shell("pyenv", ["root"], {
|
|
34
|
+
silent: true,
|
|
35
|
+
allowFailure: true,
|
|
36
|
+
});
|
|
30
37
|
if (result.exitCode === 0) {
|
|
31
38
|
const root = result.stdout.trim();
|
|
32
39
|
return { PATH: `${root}/shims:${process.env.PATH ?? ""}` };
|
package/dist/runtimes/ruby.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { shell, shellOk } from "../shell.js";
|
|
7
|
+
import { isInstalled } from "./installed-versions.js";
|
|
7
8
|
export class RubyInstaller {
|
|
8
9
|
runtime = "ruby";
|
|
9
10
|
async detectVersion(projectDir) {
|
|
@@ -16,19 +17,23 @@ export class RubyInstaller {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
async install(version) {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// `version` is the verbatim contents of .ruby-version in the target repo —
|
|
21
|
+
// untrusted input. It goes in as an argv element so it can never be read
|
|
22
|
+
// as shell syntax.
|
|
23
|
+
if (!(await shellOk("which", ["rbenv"]))) {
|
|
24
|
+
await shell("brew", ["install", "rbenv", "ruby-build"]);
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await shell(`rbenv install ${version}`);
|
|
26
|
+
if (!(await isInstalled("rbenv", version))) {
|
|
27
|
+
await shell("rbenv", ["install", version]);
|
|
25
28
|
}
|
|
26
|
-
await shell(
|
|
29
|
+
await shell("rbenv", ["local", version]);
|
|
27
30
|
}
|
|
28
31
|
async shellEnv(_version) {
|
|
29
|
-
if (await shellOk("which rbenv")) {
|
|
32
|
+
if (await shellOk("which", ["rbenv"])) {
|
|
30
33
|
try {
|
|
31
|
-
const result = await shell("rbenv init - bash", {
|
|
34
|
+
const result = await shell("rbenv", ["init", "-", "bash"], {
|
|
35
|
+
silent: true,
|
|
36
|
+
});
|
|
32
37
|
return parseRbenvEnv(result.stdout);
|
|
33
38
|
}
|
|
34
39
|
catch {
|
package/dist/secret-generator.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { allocatePort } from "./port-allocator.js";
|
|
6
|
+
import { registerSecret } from "./redact.js";
|
|
6
7
|
/**
|
|
7
8
|
* Generate a value based on the generator type.
|
|
8
9
|
*/
|
|
9
10
|
export async function generateValue(generator, existingPorts) {
|
|
10
11
|
switch (generator) {
|
|
11
|
-
case "secret":
|
|
12
|
-
|
|
12
|
+
case "secret": {
|
|
13
|
+
// Spec-defined output: 32 bytes of cryptographically random data,
|
|
14
|
+
// hex-encoded (64 characters). See spec/DESIGN.md D-47.
|
|
15
|
+
const secret = randomBytes(32).toString("hex");
|
|
16
|
+
registerSecret(secret);
|
|
17
|
+
return secret;
|
|
18
|
+
}
|
|
13
19
|
case "uuid":
|
|
14
20
|
return randomUUID();
|
|
15
21
|
case "port": {
|
|
@@ -21,6 +27,8 @@ export async function generateValue(generator, existingPorts) {
|
|
|
21
27
|
/** Generate a password suitable for database users */
|
|
22
28
|
export function generatePassword() {
|
|
23
29
|
// URL-safe so it can go in connection strings without encoding
|
|
24
|
-
|
|
30
|
+
const password = randomBytes(24).toString("base64url");
|
|
31
|
+
registerSecret(password);
|
|
32
|
+
return password;
|
|
25
33
|
}
|
|
26
34
|
//# sourceMappingURL=secret-generator.js.map
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Command execution helpers with timeout, logging, and structured results.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points, and the difference between them is a security boundary:
|
|
5
|
+
*
|
|
6
|
+
* - `shell(cmd, args, opts)` passes arguments straight to the OS via
|
|
7
|
+
* `execFile`. No shell is involved, so metacharacters in an argument are
|
|
8
|
+
* inert. Every command this provider builds itself uses this form — a value
|
|
9
|
+
* spliced into a command string would be CWE-78.
|
|
10
|
+
* - `shellScript(command, opts)` runs a command string through `/bin/sh -c`.
|
|
11
|
+
* It exists only for command strings the Launchfile author wrote for their
|
|
12
|
+
* own app (`commands:`, `health:`, `release:`), where shell syntax is the
|
|
13
|
+
* documented contract. Never build one of these by interpolation.
|
|
3
14
|
*/
|
|
4
15
|
export interface ShellResult {
|
|
5
16
|
exitCode: number;
|
|
@@ -14,12 +25,23 @@ export interface ShellOpts {
|
|
|
14
25
|
silent?: boolean;
|
|
15
26
|
}
|
|
16
27
|
/**
|
|
17
|
-
* Run a
|
|
18
|
-
*
|
|
28
|
+
* Run a command with an argument array. Arguments reach the OS directly, so
|
|
29
|
+
* they are never parsed as shell syntax.
|
|
19
30
|
*/
|
|
20
|
-
export declare function shell(
|
|
31
|
+
export declare function shell(cmd: string, args: string[], opts?: ShellOpts & {
|
|
32
|
+
allowFailure?: boolean;
|
|
33
|
+
}): Promise<ShellResult>;
|
|
34
|
+
/** Run a command with an argument array, return true if exit code is 0. */
|
|
35
|
+
export declare function shellOk(cmd: string, args: string[], opts?: ShellOpts): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Run an author-written command string through `/bin/sh -c`.
|
|
38
|
+
*
|
|
39
|
+
* The shell is the point here: `commands:`, `health:` and `release:` values are
|
|
40
|
+
* documented as shell commands, and app authors rely on pipes, `&&` and
|
|
41
|
+
* variable expansion in them. The string must come from the Launchfile
|
|
42
|
+
* verbatim — building one by interpolating a value makes it injectable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function shellScript(command: string, opts?: ShellOpts & {
|
|
21
45
|
allowFailure?: boolean;
|
|
22
46
|
}): Promise<ShellResult>;
|
|
23
|
-
/** Run a command, return true if exit code is 0 */
|
|
24
|
-
export declare function shellOk(command: string, opts?: ShellOpts): Promise<boolean>;
|
|
25
47
|
//# sourceMappingURL=shell.d.ts.map
|
package/dist/shell.js
CHANGED
|
@@ -1,46 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Command execution helpers with timeout, logging, and structured results.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points, and the difference between them is a security boundary:
|
|
5
|
+
*
|
|
6
|
+
* - `shell(cmd, args, opts)` passes arguments straight to the OS via
|
|
7
|
+
* `execFile`. No shell is involved, so metacharacters in an argument are
|
|
8
|
+
* inert. Every command this provider builds itself uses this form — a value
|
|
9
|
+
* spliced into a command string would be CWE-78.
|
|
10
|
+
* - `shellScript(command, opts)` runs a command string through `/bin/sh -c`.
|
|
11
|
+
* It exists only for command strings the Launchfile author wrote for their
|
|
12
|
+
* own app (`commands:`, `health:`, `release:`), where shell syntax is the
|
|
13
|
+
* documented contract. Never build one of these by interpolation.
|
|
3
14
|
*/
|
|
4
|
-
import { exec as cpExec } from "node:child_process";
|
|
15
|
+
import { exec as cpExec, execFile as cpExecFile, } from "node:child_process";
|
|
16
|
+
import { redactSecrets } from "./redact.js";
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
18
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
19
|
+
function toResult(error, stdout, stderr) {
|
|
20
|
+
const result = {
|
|
21
|
+
exitCode: typeof error?.code === "number" ? error.code : 0,
|
|
22
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
23
|
+
stderr: typeof stderr === "string" ? stderr : "",
|
|
24
|
+
};
|
|
25
|
+
// Node sets error.code to the exit code on non-zero; a signal or a spawn
|
|
26
|
+
// failure leaves it non-numeric, so surface those as a generic failure.
|
|
27
|
+
if (error && result.exitCode === 0)
|
|
28
|
+
result.exitCode = 1;
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function failure(display, result) {
|
|
32
|
+
// The message reaches the user and may be logged by a caller; both the
|
|
33
|
+
// command and the child's stderr can echo a secret back, so neither goes
|
|
34
|
+
// in unscrubbed (D-18, CWE-532).
|
|
35
|
+
return Object.assign(new Error(`Command failed: ${redactSecrets(display)}\n${redactSecrets(result.stderr)}`), { result });
|
|
36
|
+
}
|
|
5
37
|
/**
|
|
6
|
-
* Run a
|
|
7
|
-
*
|
|
38
|
+
* Run a command with an argument array. Arguments reach the OS directly, so
|
|
39
|
+
* they are never parsed as shell syntax.
|
|
8
40
|
*/
|
|
9
|
-
export async function shell(
|
|
41
|
+
export async function shell(cmd, args, opts = {}) {
|
|
42
|
+
const display = [cmd, ...args].join(" ");
|
|
10
43
|
if (!opts.silent) {
|
|
11
|
-
|
|
44
|
+
// An argument can carry a resolved secret (a generated DB password, a
|
|
45
|
+
// credential-bearing URL). Scrub before echo.
|
|
46
|
+
console.log(` $ ${redactSecrets(display)}`);
|
|
12
47
|
}
|
|
13
48
|
const execOpts = {
|
|
14
49
|
cwd: opts.cwd,
|
|
15
50
|
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
16
|
-
timeout: opts.timeout ??
|
|
17
|
-
maxBuffer:
|
|
51
|
+
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
52
|
+
maxBuffer: MAX_BUFFER,
|
|
18
53
|
};
|
|
19
54
|
return new Promise((resolve, reject) => {
|
|
20
|
-
|
|
21
|
-
const result =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
// Node's exec sets error.code to the exit code on non-zero
|
|
27
|
-
if (error && result.exitCode === 0) {
|
|
28
|
-
result.exitCode = 1;
|
|
29
|
-
}
|
|
30
|
-
if (error && !opts.allowFailure) {
|
|
31
|
-
reject(Object.assign(new Error(`Command failed: ${command}\n${result.stderr}`), {
|
|
32
|
-
result,
|
|
33
|
-
}));
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
55
|
+
cpExecFile(cmd, args, execOpts, (error, stdout, stderr) => {
|
|
56
|
+
const result = toResult(error, stdout, stderr);
|
|
57
|
+
if (error && !opts.allowFailure)
|
|
58
|
+
reject(failure(display, result));
|
|
59
|
+
else
|
|
36
60
|
resolve(result);
|
|
37
|
-
}
|
|
38
61
|
});
|
|
39
62
|
});
|
|
40
63
|
}
|
|
41
|
-
/** Run a command, return true if exit code is 0 */
|
|
42
|
-
export async function shellOk(
|
|
43
|
-
const result = await shell(
|
|
64
|
+
/** Run a command with an argument array, return true if exit code is 0. */
|
|
65
|
+
export async function shellOk(cmd, args, opts) {
|
|
66
|
+
const result = await shell(cmd, args, {
|
|
67
|
+
...opts,
|
|
68
|
+
allowFailure: true,
|
|
69
|
+
silent: true,
|
|
70
|
+
});
|
|
44
71
|
return result.exitCode === 0;
|
|
45
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Run an author-written command string through `/bin/sh -c`.
|
|
75
|
+
*
|
|
76
|
+
* The shell is the point here: `commands:`, `health:` and `release:` values are
|
|
77
|
+
* documented as shell commands, and app authors rely on pipes, `&&` and
|
|
78
|
+
* variable expansion in them. The string must come from the Launchfile
|
|
79
|
+
* verbatim — building one by interpolating a value makes it injectable.
|
|
80
|
+
*/
|
|
81
|
+
export async function shellScript(command, opts = {}) {
|
|
82
|
+
if (!opts.silent) {
|
|
83
|
+
console.log(` $ ${redactSecrets(command)}`);
|
|
84
|
+
}
|
|
85
|
+
const execOpts = {
|
|
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
|
+
});
|
|
100
|
+
}
|
|
46
101
|
//# sourceMappingURL=shell.js.map
|
package/dist/state.d.ts
CHANGED
|
@@ -50,6 +50,28 @@ export interface LaunchState {
|
|
|
50
50
|
* persistence existed simply omit this, and `down` tolerates its absence.
|
|
51
51
|
*/
|
|
52
52
|
processes?: Record<string, ProcessState>;
|
|
53
|
+
/**
|
|
54
|
+
* Minted `env:`-level generator values (D-49: generate once, then
|
|
55
|
+
* preserve), keyed `<component>.<ENV_NAME>` — one entry per declaration
|
|
56
|
+
* (D-25), so same-named variables on different components hold independent
|
|
57
|
+
* values. `generator: port` values are never stored here (ports are
|
|
58
|
+
* re-allocated each run). Disjoint from `secrets` on purpose: these names
|
|
59
|
+
* must not become resolvable as `$secrets.<name>`. Optional for backward
|
|
60
|
+
* compatibility: older state files omit it and load as a first run.
|
|
61
|
+
*/
|
|
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>>;
|
|
53
75
|
}
|
|
54
76
|
export declare function hashLaunchfile(content: string): string;
|
|
55
77
|
/** Load state from disk, or return null if none exists */
|
package/dist/state.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
+
import { registerSecrets } from "./redact.js";
|
|
10
11
|
const STATE_DIR = ".launchfile";
|
|
11
12
|
const STATE_FILE = "state.json";
|
|
12
13
|
function stateDir(projectDir) {
|
|
@@ -22,7 +23,17 @@ export function hashLaunchfile(content) {
|
|
|
22
23
|
export async function loadState(projectDir) {
|
|
23
24
|
try {
|
|
24
25
|
const raw = await readFile(statePath(projectDir), "utf8");
|
|
25
|
-
|
|
26
|
+
const state = JSON.parse(raw);
|
|
27
|
+
// Persisted credentials are reused verbatim in provisioning commands and
|
|
28
|
+
// in `$secrets.*` / `$<resource>.*` expressions, so they must be known to
|
|
29
|
+
// the redactor before anything can echo them.
|
|
30
|
+
registerSecrets(Object.values(state.secrets ?? {}));
|
|
31
|
+
registerSecrets(Object.values(state.resources ?? {}).map((r) => r.password));
|
|
32
|
+
// A value minted in an earlier run and merely reused in this one never
|
|
33
|
+
// passes through generateValue()'s registration, so the loader is the
|
|
34
|
+
// only place it can become scrubbable (D-18).
|
|
35
|
+
registerSecrets(Object.values(state.generatedEnv ?? {}));
|
|
36
|
+
return state;
|
|
26
37
|
}
|
|
27
38
|
catch {
|
|
28
39
|
return null;
|
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,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchfile/macos-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "macOS dev provider for Launchfile — run apps locally via brew services and native runtimes",
|
|
5
|
+
"os": [
|
|
6
|
+
"darwin"
|
|
7
|
+
],
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "dist/index.js",
|
|
7
10
|
"types": "dist/index.d.ts",
|
|
@@ -33,14 +36,14 @@
|
|
|
33
36
|
"directory": "providers/macos-dev"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
36
|
-
"@launchfile/sdk": "^0.
|
|
39
|
+
"@launchfile/sdk": "^0.7.0",
|
|
37
40
|
"semver": "^7.7.4"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@biomejs/biome": "^2.4.10",
|
|
41
44
|
"@types/bun": "^1.3.11",
|
|
42
45
|
"@types/semver": "^7.7.1",
|
|
43
|
-
"typescript": "^
|
|
46
|
+
"typescript": "^7.0.2",
|
|
44
47
|
"vitest": "^4.1.3"
|
|
45
48
|
}
|
|
46
49
|
}
|