@launchfile/macos-dev 0.1.4 → 0.3.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.
Files changed (124) hide show
  1. package/dist/bootstrap.d.ts +58 -0
  2. package/dist/bootstrap.js +224 -0
  3. package/dist/env-writer.d.ts +19 -13
  4. package/dist/env-writer.js +44 -8
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +1 -0
  7. package/dist/process-manager.d.ts +13 -0
  8. package/dist/process-manager.js +57 -2
  9. package/dist/process-stopper.d.ts +99 -0
  10. package/dist/process-stopper.js +165 -0
  11. package/dist/provider.d.ts +18 -0
  12. package/dist/provider.js +176 -70
  13. package/dist/shell.js +1 -1
  14. package/dist/state.d.ts +28 -0
  15. package/dist/storage.d.ts +16 -3
  16. package/dist/storage.js +23 -6
  17. package/package.json +9 -4
  18. package/CLAUDE.md +0 -37
  19. package/dist/__tests__/dry-run.test.d.ts +0 -2
  20. package/dist/__tests__/dry-run.test.d.ts.map +0 -1
  21. package/dist/__tests__/dry-run.test.js +0 -129
  22. package/dist/__tests__/dry-run.test.js.map +0 -1
  23. package/dist/__tests__/env-writer.test.d.ts +0 -2
  24. package/dist/__tests__/env-writer.test.d.ts.map +0 -1
  25. package/dist/__tests__/env-writer.test.js +0 -127
  26. package/dist/__tests__/env-writer.test.js.map +0 -1
  27. package/dist/__tests__/lockfile-detect.test.d.ts +0 -2
  28. package/dist/__tests__/lockfile-detect.test.d.ts.map +0 -1
  29. package/dist/__tests__/lockfile-detect.test.js +0 -75
  30. package/dist/__tests__/lockfile-detect.test.js.map +0 -1
  31. package/dist/__tests__/port-allocator.test.d.ts +0 -2
  32. package/dist/__tests__/port-allocator.test.d.ts.map +0 -1
  33. package/dist/__tests__/port-allocator.test.js +0 -53
  34. package/dist/__tests__/port-allocator.test.js.map +0 -1
  35. package/dist/__tests__/secret-generator.test.d.ts +0 -2
  36. package/dist/__tests__/secret-generator.test.d.ts.map +0 -1
  37. package/dist/__tests__/secret-generator.test.js +0 -26
  38. package/dist/__tests__/secret-generator.test.js.map +0 -1
  39. package/dist/__tests__/state.test.d.ts +0 -2
  40. package/dist/__tests__/state.test.d.ts.map +0 -1
  41. package/dist/__tests__/state.test.js +0 -30
  42. package/dist/__tests__/state.test.js.map +0 -1
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js.map +0 -1
  45. package/dist/env-writer.d.ts.map +0 -1
  46. package/dist/env-writer.js.map +0 -1
  47. package/dist/health.d.ts.map +0 -1
  48. package/dist/health.js.map +0 -1
  49. package/dist/index.d.ts.map +0 -1
  50. package/dist/index.js.map +0 -1
  51. package/dist/lockfile-detect.d.ts.map +0 -1
  52. package/dist/lockfile-detect.js.map +0 -1
  53. package/dist/port-allocator.d.ts.map +0 -1
  54. package/dist/port-allocator.js.map +0 -1
  55. package/dist/prereqs.d.ts.map +0 -1
  56. package/dist/prereqs.js.map +0 -1
  57. package/dist/process-manager.d.ts.map +0 -1
  58. package/dist/process-manager.js.map +0 -1
  59. package/dist/provider.d.ts.map +0 -1
  60. package/dist/provider.js.map +0 -1
  61. package/dist/resources/index.d.ts.map +0 -1
  62. package/dist/resources/index.js.map +0 -1
  63. package/dist/resources/mysql.d.ts.map +0 -1
  64. package/dist/resources/mysql.js.map +0 -1
  65. package/dist/resources/postgres.d.ts.map +0 -1
  66. package/dist/resources/postgres.js.map +0 -1
  67. package/dist/resources/redis.d.ts.map +0 -1
  68. package/dist/resources/redis.js.map +0 -1
  69. package/dist/resources/sqlite.d.ts.map +0 -1
  70. package/dist/resources/sqlite.js.map +0 -1
  71. package/dist/resources/types.d.ts.map +0 -1
  72. package/dist/resources/types.js.map +0 -1
  73. package/dist/runtimes/bun.d.ts.map +0 -1
  74. package/dist/runtimes/bun.js.map +0 -1
  75. package/dist/runtimes/index.d.ts.map +0 -1
  76. package/dist/runtimes/index.js.map +0 -1
  77. package/dist/runtimes/node.d.ts.map +0 -1
  78. package/dist/runtimes/node.js.map +0 -1
  79. package/dist/runtimes/python.d.ts.map +0 -1
  80. package/dist/runtimes/python.js.map +0 -1
  81. package/dist/runtimes/ruby.d.ts.map +0 -1
  82. package/dist/runtimes/ruby.js.map +0 -1
  83. package/dist/runtimes/types.d.ts.map +0 -1
  84. package/dist/runtimes/types.js.map +0 -1
  85. package/dist/secret-generator.d.ts.map +0 -1
  86. package/dist/secret-generator.js.map +0 -1
  87. package/dist/shell.d.ts.map +0 -1
  88. package/dist/shell.js.map +0 -1
  89. package/dist/state.d.ts.map +0 -1
  90. package/dist/state.js.map +0 -1
  91. package/dist/storage.d.ts.map +0 -1
  92. package/dist/storage.js.map +0 -1
  93. package/src/__tests__/dry-run.test.ts +0 -157
  94. package/src/__tests__/env-writer.test.ts +0 -156
  95. package/src/__tests__/lockfile-detect.test.ts +0 -74
  96. package/src/__tests__/port-allocator.test.ts +0 -59
  97. package/src/__tests__/secret-generator.test.ts +0 -31
  98. package/src/__tests__/state.test.ts +0 -33
  99. package/src/cli.ts +0 -78
  100. package/src/env-writer.ts +0 -231
  101. package/src/health.ts +0 -80
  102. package/src/index.ts +0 -9
  103. package/src/lockfile-detect.ts +0 -48
  104. package/src/port-allocator.ts +0 -97
  105. package/src/prereqs.ts +0 -32
  106. package/src/process-manager.ts +0 -242
  107. package/src/provider.ts +0 -415
  108. package/src/resources/index.ts +0 -29
  109. package/src/resources/mysql.ts +0 -98
  110. package/src/resources/postgres.ts +0 -133
  111. package/src/resources/redis.ts +0 -58
  112. package/src/resources/sqlite.ts +0 -52
  113. package/src/resources/types.ts +0 -47
  114. package/src/runtimes/bun.ts +0 -25
  115. package/src/runtimes/index.ts +0 -24
  116. package/src/runtimes/node.ts +0 -71
  117. package/src/runtimes/python.ts +0 -42
  118. package/src/runtimes/ruby.ts +0 -55
  119. package/src/runtimes/types.ts +0 -16
  120. package/src/secret-generator.ts +0 -32
  121. package/src/shell.ts +0 -70
  122. package/src/state.ts +0 -92
  123. package/src/storage.ts +0 -67
  124. package/tsconfig.json +0 -18
@@ -1,156 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- buildResolverContext,
4
- resolveComponentEnv,
5
- generateSecrets,
6
- } from "../env-writer.js";
7
- import type { NormalizedComponent, Secret } from "@launchfile/sdk";
8
- import type { ResourceProperties } from "../resources/types.js";
9
-
10
- describe("buildResolverContext", () => {
11
- it("builds context from resources, ports, and secrets", () => {
12
- const resources: Record<string, ResourceProperties> = {
13
- postgres: {
14
- url: "postgresql://user:pass@localhost:5432/mydb",
15
- host: "localhost",
16
- port: 5432,
17
- user: "user",
18
- password: "pass",
19
- name: "mydb",
20
- },
21
- };
22
- const ports = { backend: 3000, frontend: 3001 };
23
- const secrets = { "jwt-secret": "abc123" };
24
-
25
- const ctx = buildResolverContext(resources, ports, secrets);
26
-
27
- expect(ctx.components?.backend).toEqual({
28
- url: "http://localhost:3000",
29
- host: "localhost",
30
- port: 3000,
31
- });
32
- expect(ctx.resources?.postgres?.url).toBe("postgresql://user:pass@localhost:5432/mydb");
33
- expect(ctx.secrets?.["jwt-secret"]).toBe("abc123");
34
- });
35
- });
36
-
37
- describe("resolveComponentEnv", () => {
38
- it("resolves set_env from requires", () => {
39
- const component: NormalizedComponent = {
40
- requires: [
41
- {
42
- type: "postgres",
43
- set_env: {
44
- DATABASE_URL: "$url",
45
- DB_HOST: "$host",
46
- },
47
- },
48
- ],
49
- };
50
- const resourceMap: Record<string, ResourceProperties> = {
51
- postgres: {
52
- url: "postgresql://user:pass@localhost:5432/mydb",
53
- host: "localhost",
54
- port: 5432,
55
- },
56
- };
57
- const context = buildResolverContext(resourceMap, {}, {});
58
-
59
- const env = resolveComponentEnv(component, context, resourceMap);
60
-
61
- expect(env.DATABASE_URL).toBe("postgresql://user:pass@localhost:5432/mydb");
62
- expect(env.DB_HOST).toBe("localhost");
63
- });
64
-
65
- it("resolves component-level env defaults", () => {
66
- const component: NormalizedComponent = {
67
- env: {
68
- PORT: { default: "8080" },
69
- LOG_LEVEL: { default: "info" },
70
- },
71
- };
72
- const context = buildResolverContext({}, {}, {});
73
-
74
- const env = resolveComponentEnv(component, context, {});
75
-
76
- expect(env.PORT).toBe("8080");
77
- expect(env.LOG_LEVEL).toBe("info");
78
- });
79
-
80
- it("resolves cross-component references", () => {
81
- const component: NormalizedComponent = {
82
- env: {
83
- BACKEND_URL: { default: "$components.backend.url" },
84
- },
85
- };
86
- const context = buildResolverContext({}, { backend: 3000 }, {});
87
-
88
- const env = resolveComponentEnv(component, context, {});
89
-
90
- expect(env.BACKEND_URL).toBe("http://localhost:3000");
91
- });
92
-
93
- it("resolves secrets references", () => {
94
- const component: NormalizedComponent = {
95
- env: {
96
- SECRET_KEY: { default: "$secrets.my-key" },
97
- },
98
- };
99
- const context = buildResolverContext({}, {}, { "my-key": "super-secret" });
100
-
101
- const env = resolveComponentEnv(component, context, {});
102
-
103
- expect(env.SECRET_KEY).toBe("super-secret");
104
- });
105
-
106
- it("set_env takes precedence over component env", () => {
107
- const component: NormalizedComponent = {
108
- requires: [
109
- {
110
- type: "postgres",
111
- set_env: { DATABASE_URL: "$url" },
112
- },
113
- ],
114
- env: {
115
- DATABASE_URL: { default: "fallback" },
116
- },
117
- };
118
- const resourceMap: Record<string, ResourceProperties> = {
119
- postgres: {
120
- url: "postgresql://real-url",
121
- host: "localhost",
122
- port: 5432,
123
- },
124
- };
125
- const context = buildResolverContext(resourceMap, {}, {});
126
-
127
- const env = resolveComponentEnv(component, context, resourceMap);
128
-
129
- expect(env.DATABASE_URL).toBe("postgresql://real-url");
130
- });
131
- });
132
-
133
- describe("generateSecrets", () => {
134
- it("generates new secrets", async () => {
135
- const defs: Record<string, Secret> = {
136
- "jwt-secret": { generator: "secret" },
137
- "app-uuid": { generator: "uuid" },
138
- };
139
-
140
- const secrets = await generateSecrets(defs, {});
141
-
142
- expect(secrets["jwt-secret"]).toBeTruthy();
143
- expect(secrets["app-uuid"]).toMatch(/^[0-9a-f-]+$/);
144
- });
145
-
146
- it("reuses existing secrets", async () => {
147
- const defs: Record<string, Secret> = {
148
- "jwt-secret": { generator: "secret" },
149
- };
150
- const existing = { "jwt-secret": "keep-this" };
151
-
152
- const secrets = await generateSecrets(defs, existing);
153
-
154
- expect(secrets["jwt-secret"]).toBe("keep-this");
155
- });
156
- });
@@ -1,74 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { detectPackageManager } from "../lockfile-detect.js";
3
- import { mkdtemp, writeFile, rm } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
-
7
- describe("detectPackageManager", () => {
8
- it("returns null for empty directory", async () => {
9
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
10
- try {
11
- const pm = await detectPackageManager(dir);
12
- expect(pm).toBeNull();
13
- } finally {
14
- await rm(dir, { recursive: true });
15
- }
16
- });
17
-
18
- it("detects bun from bun.lockb", async () => {
19
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
20
- try {
21
- await writeFile(join(dir, "bun.lockb"), "");
22
- const pm = await detectPackageManager(dir);
23
- expect(pm?.name).toBe("bun");
24
- expect(pm?.installCommand).toBe("bun install");
25
- } finally {
26
- await rm(dir, { recursive: true });
27
- }
28
- });
29
-
30
- it("detects yarn from yarn.lock", async () => {
31
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
32
- try {
33
- await writeFile(join(dir, "yarn.lock"), "");
34
- const pm = await detectPackageManager(dir);
35
- expect(pm?.name).toBe("yarn");
36
- } finally {
37
- await rm(dir, { recursive: true });
38
- }
39
- });
40
-
41
- it("detects pnpm from pnpm-lock.yaml", async () => {
42
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
43
- try {
44
- await writeFile(join(dir, "pnpm-lock.yaml"), "");
45
- const pm = await detectPackageManager(dir);
46
- expect(pm?.name).toBe("pnpm");
47
- } finally {
48
- await rm(dir, { recursive: true });
49
- }
50
- });
51
-
52
- it("detects bundler from Gemfile.lock", async () => {
53
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
54
- try {
55
- await writeFile(join(dir, "Gemfile.lock"), "");
56
- const pm = await detectPackageManager(dir);
57
- expect(pm?.name).toBe("bundler");
58
- } finally {
59
- await rm(dir, { recursive: true });
60
- }
61
- });
62
-
63
- it("prefers bun over yarn when both present", async () => {
64
- const dir = await mkdtemp(join(tmpdir(), "lf-test-"));
65
- try {
66
- await writeFile(join(dir, "bun.lockb"), "");
67
- await writeFile(join(dir, "yarn.lock"), "");
68
- const pm = await detectPackageManager(dir);
69
- expect(pm?.name).toBe("bun");
70
- } finally {
71
- await rm(dir, { recursive: true });
72
- }
73
- });
74
- });
@@ -1,59 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { allocatePort, allocatePorts } from "../port-allocator.js";
3
-
4
- describe("allocatePort", () => {
5
- it("returns a port in the expected range", async () => {
6
- const port = await allocatePort("test-app", new Set());
7
- expect(port).toBeGreaterThanOrEqual(10_000);
8
- expect(port).toBeLessThan(20_000);
9
- });
10
-
11
- it("is deterministic for the same key", async () => {
12
- const port1 = await allocatePort("my-app", new Set());
13
- const port2 = await allocatePort("my-app", new Set());
14
- expect(port1).toBe(port2);
15
- });
16
-
17
- it("avoids ports already in use", async () => {
18
- const port1 = await allocatePort("collision-test", new Set());
19
- const port2 = await allocatePort("collision-test", new Set([port1]));
20
- expect(port2).not.toBe(port1);
21
- });
22
-
23
- it("produces different ports for different keys", async () => {
24
- const port1 = await allocatePort("app-a", new Set());
25
- const port2 = await allocatePort("app-b", new Set());
26
- // Not guaranteed but overwhelmingly likely for different inputs
27
- expect(port1).not.toBe(port2);
28
- });
29
- });
30
-
31
- describe("allocatePorts", () => {
32
- it("allocates ports for all components", async () => {
33
- const components = {
34
- backend: { provides: [{ port: 3000 }] },
35
- frontend: { provides: [{ port: 3001 }] },
36
- };
37
- const ports = await allocatePorts(components, "test-app");
38
- expect(ports.backend).toBeDefined();
39
- expect(ports.frontend).toBeDefined();
40
- expect(ports.backend).not.toBe(ports.frontend);
41
- });
42
-
43
- it("prefers the declared port if free", async () => {
44
- const components = {
45
- api: { provides: [{ port: 4567 }] },
46
- };
47
- const ports = await allocatePorts(components, "test-app");
48
- expect(ports.api).toBe(4567);
49
- });
50
-
51
- it("reuses saved ports when available", async () => {
52
- const components = {
53
- api: { provides: [{ port: 3000 }] },
54
- };
55
- const saved = { api: 9876 };
56
- const ports = await allocatePorts(components, "test-app", saved);
57
- expect(ports.api).toBe(9876);
58
- });
59
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { generateValue, generatePassword } from "../secret-generator.js";
3
-
4
- describe("generateValue", () => {
5
- it("generates a base64url secret", async () => {
6
- const value = await generateValue("secret");
7
- expect(value).toMatch(/^[A-Za-z0-9_-]+$/);
8
- expect(value.length).toBeGreaterThan(20);
9
- });
10
-
11
- it("generates a UUID", async () => {
12
- const value = await generateValue("uuid");
13
- expect(value).toMatch(
14
- /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
15
- );
16
- });
17
-
18
- it("generates unique values each time", async () => {
19
- const a = await generateValue("secret");
20
- const b = await generateValue("secret");
21
- expect(a).not.toBe(b);
22
- });
23
- });
24
-
25
- describe("generatePassword", () => {
26
- it("generates a URL-safe password", () => {
27
- const pw = generatePassword();
28
- expect(pw).toMatch(/^[A-Za-z0-9_-]+$/);
29
- expect(pw.length).toBeGreaterThan(10);
30
- });
31
- });
@@ -1,33 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { initState, hashLaunchfile } from "../state.js";
3
-
4
- describe("initState", () => {
5
- it("creates a state with correct app name", () => {
6
- const state = initState("my-app", "name: my-app");
7
- expect(state.appName).toBe("my-app");
8
- expect(state.version).toBe(1);
9
- expect(state.resources).toEqual({});
10
- expect(state.secrets).toEqual({});
11
- expect(state.ports).toEqual({});
12
- });
13
-
14
- it("hashes launchfile content", () => {
15
- const state = initState("app", "some yaml content");
16
- expect(state.launchfileHash).toBeTruthy();
17
- expect(state.launchfileHash.length).toBe(16);
18
- });
19
- });
20
-
21
- describe("hashLaunchfile", () => {
22
- it("is deterministic", () => {
23
- const a = hashLaunchfile("hello");
24
- const b = hashLaunchfile("hello");
25
- expect(a).toBe(b);
26
- });
27
-
28
- it("differs for different content", () => {
29
- const a = hashLaunchfile("hello");
30
- const b = hashLaunchfile("world");
31
- expect(a).not.toBe(b);
32
- });
33
- });
package/src/cli.ts DELETED
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * CLI entry point for the macOS dev provider.
4
- *
5
- * Usage:
6
- * launch up [--with-optional] [--no-build] [--dry-run]
7
- * launch down [--destroy]
8
- * launch status
9
- * launch env [component]
10
- */
11
-
12
- import { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
13
-
14
- const args = process.argv.slice(2);
15
- const command = args[0];
16
-
17
- function hasFlag(flag: string): boolean {
18
- return args.includes(`--${flag}`);
19
- }
20
-
21
- function getArg(index: number): string | undefined {
22
- return args[index];
23
- }
24
-
25
- async function main(): Promise<void> {
26
- switch (command) {
27
- case "up":
28
- await launchUp({
29
- withOptional: hasFlag("with-optional"),
30
- noBuild: hasFlag("no-build"),
31
- dryRun: hasFlag("dry-run"),
32
- detach: hasFlag("detach"),
33
- });
34
- break;
35
-
36
- case "down":
37
- await launchDown({
38
- destroy: hasFlag("destroy"),
39
- });
40
- break;
41
-
42
- case "status":
43
- await launchStatus();
44
- break;
45
-
46
- case "env":
47
- await launchEnv({
48
- component: getArg(1),
49
- });
50
- break;
51
-
52
- default:
53
- console.log(`launch — macOS dev provider for Launchfile
54
-
55
- Usage:
56
- launch up [--with-optional] [--no-build] [--dry-run]
57
- Provision resources, install deps, and start the app.
58
-
59
- launch down [--destroy]
60
- Stop processes. --destroy also drops databases and cleans up.
61
-
62
- launch status
63
- Show status of components and resources.
64
-
65
- launch env [component]
66
- Print resolved environment variables.
67
- `);
68
- if (command && command !== "help" && command !== "--help") {
69
- console.error(`Unknown command: ${command}`);
70
- process.exit(1);
71
- }
72
- }
73
- }
74
-
75
- main().catch((err) => {
76
- console.error(err.message ?? err);
77
- process.exit(1);
78
- });
package/src/env-writer.ts DELETED
@@ -1,231 +0,0 @@
1
- /**
2
- * Environment variable resolver and writer.
3
- *
4
- * Connects provisioned resource properties to the SDK's expression resolver,
5
- * then writes the results to .env files.
6
- */
7
-
8
- import { writeFile, mkdir } from "node:fs/promises";
9
- import { join } from "node:path";
10
- import {
11
- resolveExpression,
12
- isExpression,
13
- type NormalizedComponent,
14
- type NormalizedLaunch,
15
- type Secret,
16
- } from "@launchfile/sdk";
17
- import type { ResourceProperties } from "./resources/types.js";
18
- import { generateValue } from "./secret-generator.js";
19
-
20
- /**
21
- * Context for the SDK's resolveExpression().
22
- * Mirrors the interface from sdk/src/resolver.ts since it's not exported.
23
- */
24
- export interface ResolverContext {
25
- resource?: Record<string, string | number>;
26
- resources?: Record<string, Record<string, string | number>>;
27
- components?: Record<string, Record<string, string | number>>;
28
- secrets?: Record<string, string>;
29
- }
30
-
31
- /**
32
- * Build a ResolverContext from provisioned resources, component ports, and secrets.
33
- */
34
- export function buildResolverContext(
35
- resourceMap: Record<string, ResourceProperties>,
36
- componentPorts: Record<string, number>,
37
- secrets: Record<string, string>,
38
- ): ResolverContext {
39
- // Build components map from ports
40
- const components: Record<string, Record<string, string | number>> = {};
41
- for (const [name, port] of Object.entries(componentPorts)) {
42
- components[name] = {
43
- url: `http://localhost:${port}`,
44
- host: "localhost",
45
- port,
46
- };
47
- }
48
-
49
- // Build named resources map
50
- const resources: Record<string, Record<string, string | number>> = {};
51
- for (const [name, props] of Object.entries(resourceMap)) {
52
- const record: Record<string, string | number> = {};
53
- for (const [k, v] of Object.entries(props)) {
54
- if (v !== undefined) {
55
- record[k] = v;
56
- }
57
- }
58
- resources[name] = record;
59
- }
60
-
61
- return { resources, components, secrets };
62
- }
63
-
64
- /**
65
- * Resolve all environment variables for a single component.
66
- */
67
- export function resolveComponentEnv(
68
- component: NormalizedComponent,
69
- context: ResolverContext,
70
- resourceMap: Record<string, ResourceProperties>,
71
- ): Record<string, string> {
72
- const env: Record<string, string> = {};
73
-
74
- // 1. Resolve set_env from requires
75
- for (const req of component.requires ?? []) {
76
- const resourceName = req.name ?? req.type;
77
- const props = resourceMap[resourceName];
78
- if (!req.set_env || !props) continue;
79
-
80
- // Build resource-scoped context (enclosing resource)
81
- const resourceRecord: Record<string, string | number> = {};
82
- for (const [k, v] of Object.entries(props)) {
83
- if (v !== undefined) resourceRecord[k] = v;
84
- }
85
- const scopedContext: ResolverContext = {
86
- ...context,
87
- resource: resourceRecord,
88
- };
89
-
90
- for (const [envKey, expr] of Object.entries(req.set_env)) {
91
- env[envKey] = resolveExpression(expr, scopedContext);
92
- }
93
- }
94
-
95
- // 2. Resolve set_env from supports (only if resource was provisioned)
96
- for (const sup of component.supports ?? []) {
97
- const resourceName = sup.name ?? sup.type;
98
- const props = resourceMap[resourceName];
99
- if (!sup.set_env || !props) continue;
100
-
101
- const resourceRecord: Record<string, string | number> = {};
102
- for (const [k, v] of Object.entries(props)) {
103
- if (v !== undefined) resourceRecord[k] = v;
104
- }
105
- const scopedContext: ResolverContext = {
106
- ...context,
107
- resource: resourceRecord,
108
- };
109
-
110
- for (const [envKey, expr] of Object.entries(sup.set_env)) {
111
- env[envKey] = resolveExpression(expr, scopedContext);
112
- }
113
- }
114
-
115
- // 3. Resolve component-level env vars
116
- if (component.env) {
117
- for (const [key, envVar] of Object.entries(component.env)) {
118
- if (env[key] !== undefined) continue; // set_env takes precedence
119
-
120
- if (envVar.default !== undefined) {
121
- const defaultStr = String(envVar.default);
122
- if (isExpression(defaultStr)) {
123
- env[key] = resolveExpression(defaultStr, context);
124
- } else {
125
- env[key] = defaultStr;
126
- }
127
- }
128
- // required + no default + no generator → left unset (provider should prompt)
129
- }
130
- }
131
-
132
- return env;
133
- }
134
-
135
- /**
136
- * Generate all app-wide secrets, reusing values from state when available.
137
- */
138
- export async function generateSecrets(
139
- secretDefs: Record<string, Secret> | undefined,
140
- existingSecrets: Record<string, string>,
141
- ): Promise<Record<string, string>> {
142
- if (!secretDefs) return { ...existingSecrets };
143
-
144
- const secrets = { ...existingSecrets };
145
- for (const [name, def] of Object.entries(secretDefs)) {
146
- if (!secrets[name]) {
147
- secrets[name] = await generateValue(def.generator);
148
- }
149
- }
150
- return secrets;
151
- }
152
-
153
- /**
154
- * Generate values for env vars that have generators.
155
- * Mutates the env record in place.
156
- */
157
- export async function resolveGenerators(
158
- component: NormalizedComponent,
159
- env: Record<string, string>,
160
- ): Promise<void> {
161
- if (!component.env) return;
162
-
163
- for (const [key, envVar] of Object.entries(component.env)) {
164
- if (env[key] !== undefined) continue;
165
- if (envVar.generator) {
166
- env[key] = await generateValue(envVar.generator);
167
- }
168
- }
169
- }
170
-
171
- /**
172
- * Write resolved env vars to a .env file.
173
- */
174
- export async function writeEnvFile(
175
- filePath: string,
176
- env: Record<string, string>,
177
- ): Promise<void> {
178
- const lines = Object.entries(env)
179
- .sort(([a], [b]) => a.localeCompare(b))
180
- .map(([key, value]) => {
181
- // Quote values that contain spaces, #, or newlines
182
- if (/[\s#\n]/.test(value)) {
183
- return `${key}="${value.replace(/"/g, '\\"')}"`;
184
- }
185
- return `${key}=${value}`;
186
- });
187
-
188
- const content = `# Generated by launch up — do not edit manually\n${lines.join("\n")}\n`;
189
- // Security: env files contain database URLs with passwords and generated secrets
190
- await writeFile(filePath, content, { mode: 0o600 });
191
- }
192
-
193
- /**
194
- * Resolve and write env files for all components.
195
- * Single-component → .env.local at project root.
196
- * Multi-component → .launchfile/env/<component>.env per component.
197
- */
198
- export async function writeAllEnvFiles(
199
- launch: NormalizedLaunch,
200
- context: ResolverContext,
201
- resourceMap: Record<string, ResourceProperties>,
202
- componentPorts: Record<string, number>,
203
- projectDir: string,
204
- ): Promise<Record<string, Record<string, string>>> {
205
- const allEnvs: Record<string, Record<string, string>> = {};
206
- const componentNames = Object.keys(launch.components);
207
- const isSingleComponent = componentNames.length === 1 && componentNames[0] === "default";
208
-
209
- for (const [name, component] of Object.entries(launch.components)) {
210
- const env = resolveComponentEnv(component, context, resourceMap);
211
- await resolveGenerators(component, env);
212
-
213
- // Inject PORT if not already set and component has provides
214
- const port = componentPorts[name];
215
- if (port && !env.PORT) {
216
- env.PORT = String(port);
217
- }
218
-
219
- allEnvs[name] = env;
220
-
221
- if (isSingleComponent) {
222
- await writeEnvFile(join(projectDir, ".env.local"), env);
223
- } else {
224
- const envDir = join(projectDir, ".launchfile", "env");
225
- await mkdir(envDir, { recursive: true });
226
- await writeEnvFile(join(envDir, `${name}.env`), env);
227
- }
228
- }
229
-
230
- return allEnvs;
231
- }