@launchfile/macos-dev 0.1.4 → 0.2.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 (107) hide show
  1. package/dist/__tests__/bootstrap.test.d.ts +2 -0
  2. package/dist/__tests__/bootstrap.test.js +90 -0
  3. package/dist/__tests__/dry-run.test.js +21 -9
  4. package/dist/__tests__/env-writer.test.js +82 -8
  5. package/dist/bootstrap.d.ts +58 -0
  6. package/dist/bootstrap.js +221 -0
  7. package/dist/env-writer.d.ts +14 -11
  8. package/dist/env-writer.js +30 -3
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +1 -0
  11. package/dist/provider.js +30 -48
  12. package/package.json +8 -3
  13. package/CLAUDE.md +0 -37
  14. package/dist/__tests__/dry-run.test.d.ts.map +0 -1
  15. package/dist/__tests__/dry-run.test.js.map +0 -1
  16. package/dist/__tests__/env-writer.test.d.ts.map +0 -1
  17. package/dist/__tests__/env-writer.test.js.map +0 -1
  18. package/dist/__tests__/lockfile-detect.test.d.ts.map +0 -1
  19. package/dist/__tests__/lockfile-detect.test.js.map +0 -1
  20. package/dist/__tests__/port-allocator.test.d.ts.map +0 -1
  21. package/dist/__tests__/port-allocator.test.js.map +0 -1
  22. package/dist/__tests__/secret-generator.test.d.ts.map +0 -1
  23. package/dist/__tests__/secret-generator.test.js.map +0 -1
  24. package/dist/__tests__/state.test.d.ts.map +0 -1
  25. package/dist/__tests__/state.test.js.map +0 -1
  26. package/dist/cli.d.ts.map +0 -1
  27. package/dist/cli.js.map +0 -1
  28. package/dist/env-writer.d.ts.map +0 -1
  29. package/dist/env-writer.js.map +0 -1
  30. package/dist/health.d.ts.map +0 -1
  31. package/dist/health.js.map +0 -1
  32. package/dist/index.d.ts.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/lockfile-detect.d.ts.map +0 -1
  35. package/dist/lockfile-detect.js.map +0 -1
  36. package/dist/port-allocator.d.ts.map +0 -1
  37. package/dist/port-allocator.js.map +0 -1
  38. package/dist/prereqs.d.ts.map +0 -1
  39. package/dist/prereqs.js.map +0 -1
  40. package/dist/process-manager.d.ts.map +0 -1
  41. package/dist/process-manager.js.map +0 -1
  42. package/dist/provider.d.ts.map +0 -1
  43. package/dist/provider.js.map +0 -1
  44. package/dist/resources/index.d.ts.map +0 -1
  45. package/dist/resources/index.js.map +0 -1
  46. package/dist/resources/mysql.d.ts.map +0 -1
  47. package/dist/resources/mysql.js.map +0 -1
  48. package/dist/resources/postgres.d.ts.map +0 -1
  49. package/dist/resources/postgres.js.map +0 -1
  50. package/dist/resources/redis.d.ts.map +0 -1
  51. package/dist/resources/redis.js.map +0 -1
  52. package/dist/resources/sqlite.d.ts.map +0 -1
  53. package/dist/resources/sqlite.js.map +0 -1
  54. package/dist/resources/types.d.ts.map +0 -1
  55. package/dist/resources/types.js.map +0 -1
  56. package/dist/runtimes/bun.d.ts.map +0 -1
  57. package/dist/runtimes/bun.js.map +0 -1
  58. package/dist/runtimes/index.d.ts.map +0 -1
  59. package/dist/runtimes/index.js.map +0 -1
  60. package/dist/runtimes/node.d.ts.map +0 -1
  61. package/dist/runtimes/node.js.map +0 -1
  62. package/dist/runtimes/python.d.ts.map +0 -1
  63. package/dist/runtimes/python.js.map +0 -1
  64. package/dist/runtimes/ruby.d.ts.map +0 -1
  65. package/dist/runtimes/ruby.js.map +0 -1
  66. package/dist/runtimes/types.d.ts.map +0 -1
  67. package/dist/runtimes/types.js.map +0 -1
  68. package/dist/secret-generator.d.ts.map +0 -1
  69. package/dist/secret-generator.js.map +0 -1
  70. package/dist/shell.d.ts.map +0 -1
  71. package/dist/shell.js.map +0 -1
  72. package/dist/state.d.ts.map +0 -1
  73. package/dist/state.js.map +0 -1
  74. package/dist/storage.d.ts.map +0 -1
  75. package/dist/storage.js.map +0 -1
  76. package/src/__tests__/dry-run.test.ts +0 -157
  77. package/src/__tests__/env-writer.test.ts +0 -156
  78. package/src/__tests__/lockfile-detect.test.ts +0 -74
  79. package/src/__tests__/port-allocator.test.ts +0 -59
  80. package/src/__tests__/secret-generator.test.ts +0 -31
  81. package/src/__tests__/state.test.ts +0 -33
  82. package/src/cli.ts +0 -78
  83. package/src/env-writer.ts +0 -231
  84. package/src/health.ts +0 -80
  85. package/src/index.ts +0 -9
  86. package/src/lockfile-detect.ts +0 -48
  87. package/src/port-allocator.ts +0 -97
  88. package/src/prereqs.ts +0 -32
  89. package/src/process-manager.ts +0 -242
  90. package/src/provider.ts +0 -415
  91. package/src/resources/index.ts +0 -29
  92. package/src/resources/mysql.ts +0 -98
  93. package/src/resources/postgres.ts +0 -133
  94. package/src/resources/redis.ts +0 -58
  95. package/src/resources/sqlite.ts +0 -52
  96. package/src/resources/types.ts +0 -47
  97. package/src/runtimes/bun.ts +0 -25
  98. package/src/runtimes/index.ts +0 -24
  99. package/src/runtimes/node.ts +0 -71
  100. package/src/runtimes/python.ts +0 -42
  101. package/src/runtimes/ruby.ts +0 -55
  102. package/src/runtimes/types.ts +0 -16
  103. package/src/secret-generator.ts +0 -32
  104. package/src/shell.ts +0 -70
  105. package/src/state.ts +0 -92
  106. package/src/storage.ts +0 -67
  107. package/tsconfig.json +0 -18
@@ -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
- }
package/src/health.ts DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * Health check polling for components.
3
- */
4
-
5
- import type { NormalizedHealth } from "@launchfile/sdk";
6
- import { shell } from "./shell.js";
7
-
8
- /** Parse a duration string like "30s", "1m", "500ms" to milliseconds */
9
- export function parseDuration(duration: string): number {
10
- const match = /^(\d+)(ms|s|m)$/.exec(duration);
11
- if (!match) return 0;
12
- const value = Number.parseInt(match[1]!, 10);
13
- switch (match[2]) {
14
- case "ms":
15
- return value;
16
- case "s":
17
- return value * 1000;
18
- case "m":
19
- return value * 60_000;
20
- default:
21
- return 0;
22
- }
23
- }
24
-
25
- /**
26
- * Wait for a component to become healthy.
27
- * Returns true if healthy, false if timed out.
28
- */
29
- export async function waitForHealthy(
30
- name: string,
31
- health: NormalizedHealth,
32
- port: number,
33
- overallTimeout: number = 60_000,
34
- ): Promise<boolean> {
35
- const startPeriod = parseDuration(health.start_period ?? "0s");
36
- const interval = parseDuration(health.interval ?? "3s");
37
- const checkTimeout = parseDuration(health.timeout ?? "5s");
38
-
39
- // Wait for start period
40
- if (startPeriod > 0) {
41
- console.log(` [${name}] Waiting ${health.start_period} start period...`);
42
- await sleep(startPeriod);
43
- }
44
-
45
- const deadline = Date.now() + overallTimeout;
46
-
47
- while (Date.now() < deadline) {
48
- try {
49
- if (health.path) {
50
- const resp = await fetch(`http://localhost:${port}${health.path}`, {
51
- signal: AbortSignal.timeout(checkTimeout),
52
- });
53
- if (resp.ok) {
54
- console.log(` [${name}] Healthy`);
55
- return true;
56
- }
57
- } else if (health.command) {
58
- await shell(health.command, { timeout: checkTimeout, silent: true });
59
- console.log(` [${name}] Healthy`);
60
- return true;
61
- } else {
62
- // No specific check — try connecting to the port
63
- const resp = await fetch(`http://localhost:${port}/`, {
64
- signal: AbortSignal.timeout(checkTimeout),
65
- }).catch(() => null);
66
- if (resp) return true;
67
- }
68
- } catch {
69
- // Expected — service not ready yet
70
- }
71
- await sleep(interval);
72
- }
73
-
74
- console.error(` [${name}] Health check timed out after ${overallTimeout}ms`);
75
- return false;
76
- }
77
-
78
- function sleep(ms: number): Promise<void> {
79
- return new Promise((resolve) => setTimeout(resolve, ms));
80
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * macOS Dev Provider for Launchfile.
3
- *
4
- * Run apps locally on macOS with brew services for databases
5
- * and native runtimes for the app itself.
6
- */
7
-
8
- export { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
9
- export type { LaunchUpOpts } from "./provider.js";
@@ -1,48 +0,0 @@
1
- /**
2
- * Detect the package manager from lockfile presence.
3
- */
4
-
5
- import { access } from "node:fs/promises";
6
- import { join } from "node:path";
7
-
8
- export interface PackageManager {
9
- name: string;
10
- installCommand: string;
11
- lockfile: string;
12
- }
13
-
14
- const LOCKFILE_ORDER: PackageManager[] = [
15
- { name: "bun", installCommand: "bun install", lockfile: "bun.lockb" },
16
- { name: "bun", installCommand: "bun install", lockfile: "bun.lock" },
17
- { name: "pnpm", installCommand: "pnpm install", lockfile: "pnpm-lock.yaml" },
18
- { name: "yarn", installCommand: "yarn install", lockfile: "yarn.lock" },
19
- { name: "npm", installCommand: "npm install", lockfile: "package-lock.json" },
20
- { name: "bundler", installCommand: "bundle install", lockfile: "Gemfile.lock" },
21
- { name: "go", installCommand: "go mod download", lockfile: "go.sum" },
22
- { name: "cargo", installCommand: "cargo build", lockfile: "Cargo.lock" },
23
- { name: "pip", installCommand: "pip install -r requirements.txt", lockfile: "requirements.txt" },
24
- { name: "poetry", installCommand: "poetry install", lockfile: "poetry.lock" },
25
- { name: "uv", installCommand: "uv sync", lockfile: "uv.lock" },
26
- ];
27
-
28
- async function fileExists(path: string): Promise<boolean> {
29
- try {
30
- await access(path);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
36
-
37
- /**
38
- * Detect the package manager from lockfile presence in a directory.
39
- * Returns the first match in priority order, or null.
40
- */
41
- export async function detectPackageManager(projectDir: string): Promise<PackageManager | null> {
42
- for (const pm of LOCKFILE_ORDER) {
43
- if (await fileExists(join(projectDir, pm.lockfile))) {
44
- return pm;
45
- }
46
- }
47
- return null;
48
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Deterministic port allocation for Launchfile apps.
3
- *
4
- * Ports are derived from a hash of the app name so they're stable across runs
5
- * and machines. If a port is in use, we scan upward for the next free one.
6
- */
7
-
8
- import { createServer } from "node:net";
9
-
10
- /** Base range for allocated ports */
11
- const PORT_RANGE_START = 10_000;
12
- const PORT_RANGE_SIZE = 10_000;
13
-
14
- /** Simple string hash → number in a range */
15
- function hashToRange(input: string, rangeSize: number): number {
16
- let hash = 0;
17
- for (let i = 0; i < input.length; i++) {
18
- hash = (hash * 31 + input.charCodeAt(i)) | 0;
19
- }
20
- return Math.abs(hash) % rangeSize;
21
- }
22
-
23
- /** Check if a port is available by attempting to listen on it */
24
- async function isPortFree(port: number): Promise<boolean> {
25
- return new Promise((resolve) => {
26
- const server = createServer();
27
- server.once("error", () => resolve(false));
28
- server.once("listening", () => {
29
- server.close(() => resolve(true));
30
- });
31
- server.listen(port, "127.0.0.1");
32
- });
33
- }
34
-
35
- /**
36
- * Allocate a port for a given key (e.g., "myapp" or "myapp:backend").
37
- * Uses deterministic hashing for stability, with fallback scan.
38
- */
39
- export async function allocatePort(
40
- key: string,
41
- existingPorts: Set<number>,
42
- ): Promise<number> {
43
- const preferred = PORT_RANGE_START + hashToRange(key, PORT_RANGE_SIZE);
44
-
45
- // Try the preferred port first
46
- if (!existingPorts.has(preferred) && (await isPortFree(preferred))) {
47
- return preferred;
48
- }
49
-
50
- // Scan upward for the next free port
51
- for (let offset = 1; offset < PORT_RANGE_SIZE; offset++) {
52
- const candidate = PORT_RANGE_START + ((preferred - PORT_RANGE_START + offset) % PORT_RANGE_SIZE);
53
- if (!existingPorts.has(candidate) && (await isPortFree(candidate))) {
54
- return candidate;
55
- }
56
- }
57
-
58
- throw new Error(`No free port found in range ${PORT_RANGE_START}–${PORT_RANGE_START + PORT_RANGE_SIZE}`);
59
- }
60
-
61
- /**
62
- * Allocate ports for all components in a launch config.
63
- * Returns a map of component name → port.
64
- */
65
- export async function allocatePorts(
66
- components: Record<string, { provides?: Array<{ port: number }> }>,
67
- appName: string,
68
- savedPorts?: Record<string, number>,
69
- ): Promise<Record<string, number>> {
70
- const allocated = new Set<number>();
71
- const result: Record<string, number> = {};
72
-
73
- for (const [name, component] of Object.entries(components)) {
74
- // Reuse saved port if still free
75
- const saved = savedPorts?.[name];
76
- if (saved && (await isPortFree(saved))) {
77
- result[name] = saved;
78
- allocated.add(saved);
79
- continue;
80
- }
81
-
82
- // Use the component's declared port if free
83
- const declaredPort = component.provides?.[0]?.port;
84
- if (declaredPort && !allocated.has(declaredPort) && (await isPortFree(declaredPort))) {
85
- result[name] = declaredPort;
86
- allocated.add(declaredPort);
87
- continue;
88
- }
89
-
90
- // Fall back to deterministic allocation
91
- const port = await allocatePort(`${appName}:${name}`, allocated);
92
- result[name] = port;
93
- allocated.add(port);
94
- }
95
-
96
- return result;
97
- }
package/src/prereqs.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * Prerequisite checks for the macOS dev provider.
3
- */
4
-
5
- import { shellOk } from "./shell.js";
6
-
7
- export interface PrereqResult {
8
- ok: boolean;
9
- missing: string[];
10
- warnings: string[];
11
- }
12
-
13
- export async function checkPrereqs(): Promise<PrereqResult> {
14
- const missing: string[] = [];
15
- const warnings: string[] = [];
16
-
17
- // Homebrew is required
18
- if (!(await shellOk("which brew"))) {
19
- missing.push("Homebrew — install from https://brew.sh");
20
- }
21
-
22
- // Git is required (for cloning)
23
- if (!(await shellOk("which git"))) {
24
- missing.push("git — install via: brew install git");
25
- }
26
-
27
- return {
28
- ok: missing.length === 0,
29
- missing,
30
- warnings,
31
- };
32
- }