@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.
- package/dist/__tests__/bootstrap.test.d.ts +2 -0
- package/dist/__tests__/bootstrap.test.js +90 -0
- package/dist/__tests__/dry-run.test.js +21 -9
- package/dist/__tests__/env-writer.test.js +82 -8
- package/dist/bootstrap.d.ts +58 -0
- package/dist/bootstrap.js +221 -0
- package/dist/env-writer.d.ts +14 -11
- package/dist/env-writer.js +30 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/provider.js +30 -48
- package/package.json +8 -3
- package/CLAUDE.md +0 -37
- package/dist/__tests__/dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/dry-run.test.js.map +0 -1
- package/dist/__tests__/env-writer.test.d.ts.map +0 -1
- package/dist/__tests__/env-writer.test.js.map +0 -1
- package/dist/__tests__/lockfile-detect.test.d.ts.map +0 -1
- package/dist/__tests__/lockfile-detect.test.js.map +0 -1
- package/dist/__tests__/port-allocator.test.d.ts.map +0 -1
- package/dist/__tests__/port-allocator.test.js.map +0 -1
- package/dist/__tests__/secret-generator.test.d.ts.map +0 -1
- package/dist/__tests__/secret-generator.test.js.map +0 -1
- package/dist/__tests__/state.test.d.ts.map +0 -1
- package/dist/__tests__/state.test.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/env-writer.d.ts.map +0 -1
- package/dist/env-writer.js.map +0 -1
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lockfile-detect.d.ts.map +0 -1
- package/dist/lockfile-detect.js.map +0 -1
- package/dist/port-allocator.d.ts.map +0 -1
- package/dist/port-allocator.js.map +0 -1
- package/dist/prereqs.d.ts.map +0 -1
- package/dist/prereqs.js.map +0 -1
- package/dist/process-manager.d.ts.map +0 -1
- package/dist/process-manager.js.map +0 -1
- package/dist/provider.d.ts.map +0 -1
- package/dist/provider.js.map +0 -1
- package/dist/resources/index.d.ts.map +0 -1
- package/dist/resources/index.js.map +0 -1
- package/dist/resources/mysql.d.ts.map +0 -1
- package/dist/resources/mysql.js.map +0 -1
- package/dist/resources/postgres.d.ts.map +0 -1
- package/dist/resources/postgres.js.map +0 -1
- package/dist/resources/redis.d.ts.map +0 -1
- package/dist/resources/redis.js.map +0 -1
- package/dist/resources/sqlite.d.ts.map +0 -1
- package/dist/resources/sqlite.js.map +0 -1
- package/dist/resources/types.d.ts.map +0 -1
- package/dist/resources/types.js.map +0 -1
- package/dist/runtimes/bun.d.ts.map +0 -1
- package/dist/runtimes/bun.js.map +0 -1
- package/dist/runtimes/index.d.ts.map +0 -1
- package/dist/runtimes/index.js.map +0 -1
- package/dist/runtimes/node.d.ts.map +0 -1
- package/dist/runtimes/node.js.map +0 -1
- package/dist/runtimes/python.d.ts.map +0 -1
- package/dist/runtimes/python.js.map +0 -1
- package/dist/runtimes/ruby.d.ts.map +0 -1
- package/dist/runtimes/ruby.js.map +0 -1
- package/dist/runtimes/types.d.ts.map +0 -1
- package/dist/runtimes/types.js.map +0 -1
- package/dist/secret-generator.d.ts.map +0 -1
- package/dist/secret-generator.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js.map +0 -1
- package/src/__tests__/dry-run.test.ts +0 -157
- package/src/__tests__/env-writer.test.ts +0 -156
- package/src/__tests__/lockfile-detect.test.ts +0 -74
- package/src/__tests__/port-allocator.test.ts +0 -59
- package/src/__tests__/secret-generator.test.ts +0 -31
- package/src/__tests__/state.test.ts +0 -33
- package/src/cli.ts +0 -78
- package/src/env-writer.ts +0 -231
- package/src/health.ts +0 -80
- package/src/index.ts +0 -9
- package/src/lockfile-detect.ts +0 -48
- package/src/port-allocator.ts +0 -97
- package/src/prereqs.ts +0 -32
- package/src/process-manager.ts +0 -242
- package/src/provider.ts +0 -415
- package/src/resources/index.ts +0 -29
- package/src/resources/mysql.ts +0 -98
- package/src/resources/postgres.ts +0 -133
- package/src/resources/redis.ts +0 -58
- package/src/resources/sqlite.ts +0 -52
- package/src/resources/types.ts +0 -47
- package/src/runtimes/bun.ts +0 -25
- package/src/runtimes/index.ts +0 -24
- package/src/runtimes/node.ts +0 -71
- package/src/runtimes/python.ts +0 -42
- package/src/runtimes/ruby.ts +0 -55
- package/src/runtimes/types.ts +0 -16
- package/src/secret-generator.ts +0 -32
- package/src/shell.ts +0 -70
- package/src/state.ts +0 -92
- package/src/storage.ts +0 -67
- 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";
|
package/src/lockfile-detect.ts
DELETED
|
@@ -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
|
-
}
|
package/src/port-allocator.ts
DELETED
|
@@ -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
|
-
}
|