@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.
- package/dist/bootstrap.d.ts +58 -0
- package/dist/bootstrap.js +224 -0
- package/dist/env-writer.d.ts +19 -13
- package/dist/env-writer.js +44 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/process-manager.d.ts +13 -0
- package/dist/process-manager.js +57 -2
- package/dist/process-stopper.d.ts +99 -0
- package/dist/process-stopper.js +165 -0
- package/dist/provider.d.ts +18 -0
- package/dist/provider.js +176 -70
- package/dist/shell.js +1 -1
- package/dist/state.d.ts +28 -0
- package/dist/storage.d.ts +16 -3
- package/dist/storage.js +23 -6
- package/package.json +9 -4
- package/CLAUDE.md +0 -37
- package/dist/__tests__/dry-run.test.d.ts +0 -2
- package/dist/__tests__/dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/dry-run.test.js +0 -129
- package/dist/__tests__/dry-run.test.js.map +0 -1
- package/dist/__tests__/env-writer.test.d.ts +0 -2
- package/dist/__tests__/env-writer.test.d.ts.map +0 -1
- package/dist/__tests__/env-writer.test.js +0 -127
- package/dist/__tests__/env-writer.test.js.map +0 -1
- package/dist/__tests__/lockfile-detect.test.d.ts +0 -2
- package/dist/__tests__/lockfile-detect.test.d.ts.map +0 -1
- package/dist/__tests__/lockfile-detect.test.js +0 -75
- package/dist/__tests__/lockfile-detect.test.js.map +0 -1
- package/dist/__tests__/port-allocator.test.d.ts +0 -2
- package/dist/__tests__/port-allocator.test.d.ts.map +0 -1
- package/dist/__tests__/port-allocator.test.js +0 -53
- package/dist/__tests__/port-allocator.test.js.map +0 -1
- package/dist/__tests__/secret-generator.test.d.ts +0 -2
- package/dist/__tests__/secret-generator.test.d.ts.map +0 -1
- package/dist/__tests__/secret-generator.test.js +0 -26
- package/dist/__tests__/secret-generator.test.js.map +0 -1
- package/dist/__tests__/state.test.d.ts +0 -2
- package/dist/__tests__/state.test.d.ts.map +0 -1
- package/dist/__tests__/state.test.js +0 -30
- 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
package/src/provider.ts
DELETED
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main provider orchestration — the `launch up` sequence.
|
|
3
|
-
*
|
|
4
|
-
* Reads a Launchfile, provisions resources, resolves env vars,
|
|
5
|
-
* installs runtimes, and starts all components.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { readLaunch, type NormalizedLaunch } from "@launchfile/sdk";
|
|
11
|
-
import { checkPrereqs } from "./prereqs.js";
|
|
12
|
-
import { loadState, initState, saveState, ensureDirs } from "./state.js";
|
|
13
|
-
import {
|
|
14
|
-
buildResolverContext,
|
|
15
|
-
resolveComponentEnv,
|
|
16
|
-
generateSecrets,
|
|
17
|
-
resolveGenerators,
|
|
18
|
-
writeEnvFile,
|
|
19
|
-
} from "./env-writer.js";
|
|
20
|
-
import { getProvisioner, type ResourceProperties } from "./resources/index.js";
|
|
21
|
-
import { allocatePorts } from "./port-allocator.js";
|
|
22
|
-
import { getRuntimeInstaller } from "./runtimes/index.js";
|
|
23
|
-
import { detectPackageManager } from "./lockfile-detect.js";
|
|
24
|
-
import { provisionStorage } from "./storage.js";
|
|
25
|
-
import { ProcessManager } from "./process-manager.js";
|
|
26
|
-
import { shell } from "./shell.js";
|
|
27
|
-
|
|
28
|
-
export interface LaunchUpOpts {
|
|
29
|
-
withOptional?: boolean;
|
|
30
|
-
noBuild?: boolean;
|
|
31
|
-
detach?: boolean;
|
|
32
|
-
dryRun?: boolean;
|
|
33
|
-
projectDir?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function launchUp(opts: LaunchUpOpts = {}): Promise<void> {
|
|
37
|
-
const projectDir = opts.projectDir ?? process.cwd();
|
|
38
|
-
|
|
39
|
-
// 1. Check prerequisites
|
|
40
|
-
console.log("Checking prerequisites...");
|
|
41
|
-
const prereqs = await checkPrereqs();
|
|
42
|
-
if (!prereqs.ok) {
|
|
43
|
-
console.error("Missing prerequisites:");
|
|
44
|
-
for (const m of prereqs.missing) console.error(` - ${m}`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 2. Read and parse Launchfile
|
|
49
|
-
console.log("Reading Launchfile...");
|
|
50
|
-
const launchfilePath = join(projectDir, "Launchfile");
|
|
51
|
-
let launchfileContent: string;
|
|
52
|
-
try {
|
|
53
|
-
launchfileContent = await readFile(launchfilePath, "utf8");
|
|
54
|
-
} catch {
|
|
55
|
-
console.error(`No Launchfile found at ${launchfilePath}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const launch = readLaunch(launchfileContent);
|
|
60
|
-
console.log(` App: ${launch.name}`);
|
|
61
|
-
const componentNames = Object.keys(launch.components);
|
|
62
|
-
console.log(` Components: ${componentNames.join(", ")}`);
|
|
63
|
-
|
|
64
|
-
// 3. Load or init state
|
|
65
|
-
let state = await loadState(projectDir);
|
|
66
|
-
if (!state) {
|
|
67
|
-
state = initState(launch.name, launchfileContent);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 4. Ensure directories
|
|
71
|
-
await ensureDirs(projectDir);
|
|
72
|
-
|
|
73
|
-
// 5. Generate secrets
|
|
74
|
-
console.log("Generating secrets...");
|
|
75
|
-
state.secrets = await generateSecrets(launch.secrets, state.secrets);
|
|
76
|
-
|
|
77
|
-
// 6. Provision required resources
|
|
78
|
-
console.log("Provisioning resources...");
|
|
79
|
-
const resourceMap: Record<string, ResourceProperties> = {};
|
|
80
|
-
|
|
81
|
-
for (const [_compName, component] of Object.entries(launch.components)) {
|
|
82
|
-
for (const req of component.requires ?? []) {
|
|
83
|
-
const resourceName = req.name ?? req.type;
|
|
84
|
-
if (resourceMap[resourceName]) continue; // Already provisioned
|
|
85
|
-
|
|
86
|
-
const provisioner = getProvisioner(req.type);
|
|
87
|
-
if (!provisioner) {
|
|
88
|
-
console.warn(` No provisioner for resource type: ${req.type} (skipping)`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (opts.dryRun) {
|
|
93
|
-
console.log(` [dry-run] Would provision ${req.type} as "${resourceName}"`);
|
|
94
|
-
resourceMap[resourceName] = { url: "", host: "localhost", port: 0 }; // placeholder for dedup
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log(` Provisioning ${req.type} (${resourceName})...`);
|
|
99
|
-
const existing = state.resources[resourceName];
|
|
100
|
-
const result = await provisioner.provision(req, { appName: launch.name, projectDir }, existing);
|
|
101
|
-
resourceMap[resourceName] = result.properties;
|
|
102
|
-
state.resources[resourceName] = result.state;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Optional supports resources
|
|
106
|
-
if (opts.withOptional) {
|
|
107
|
-
for (const sup of component.supports ?? []) {
|
|
108
|
-
const resourceName = sup.name ?? sup.type;
|
|
109
|
-
if (resourceMap[resourceName]) continue;
|
|
110
|
-
|
|
111
|
-
const provisioner = getProvisioner(sup.type);
|
|
112
|
-
if (!provisioner) {
|
|
113
|
-
console.log(` Optional resource ${sup.type} not supported (skipping)`);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (opts.dryRun) {
|
|
118
|
-
console.log(` [dry-run] Would provision optional ${sup.type} as "${resourceName}"`);
|
|
119
|
-
resourceMap[resourceName] = { url: "", host: "localhost", port: 0 };
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
console.log(` Provisioning optional ${sup.type} (${resourceName})...`);
|
|
125
|
-
const existing = state.resources[resourceName];
|
|
126
|
-
const result = await provisioner.provision(sup, { appName: launch.name, projectDir }, existing);
|
|
127
|
-
resourceMap[resourceName] = result.properties;
|
|
128
|
-
state.resources[resourceName] = result.state;
|
|
129
|
-
} catch (err) {
|
|
130
|
-
console.log(` Optional resource ${sup.type} failed to provision (skipping): ${err}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 7. Allocate ports
|
|
137
|
-
console.log("Allocating ports...");
|
|
138
|
-
const componentPorts = await allocatePorts(launch.components, launch.name, state.ports);
|
|
139
|
-
state.ports = componentPorts;
|
|
140
|
-
|
|
141
|
-
// 8. Build resolver context
|
|
142
|
-
const context = buildResolverContext(resourceMap, componentPorts, state.secrets);
|
|
143
|
-
|
|
144
|
-
// 9. Install runtimes
|
|
145
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
146
|
-
if (!component.runtime) {
|
|
147
|
-
if (!component.image) {
|
|
148
|
-
console.warn(` [${name}] No runtime declared — cannot run natively`);
|
|
149
|
-
}
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const installer = getRuntimeInstaller(component.runtime);
|
|
154
|
-
if (!installer) {
|
|
155
|
-
console.warn(` [${name}] No installer for runtime: ${component.runtime}`);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (opts.dryRun) {
|
|
160
|
-
console.log(` [dry-run] Would install runtime: ${component.runtime}`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const version = await installer.detectVersion(projectDir);
|
|
165
|
-
if (version) {
|
|
166
|
-
console.log(` [${name}] Runtime ${component.runtime} ${version}`);
|
|
167
|
-
await installer.install(version);
|
|
168
|
-
} else {
|
|
169
|
-
console.log(` [${name}] Runtime ${component.runtime} (using system default)`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 10. Detect package manager
|
|
174
|
-
const pm = await detectPackageManager(projectDir);
|
|
175
|
-
if (pm) {
|
|
176
|
-
console.log(` Package manager: ${pm.name} (${pm.lockfile})`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 11. Create storage directories
|
|
180
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
181
|
-
await provisionStorage(component.storage, name, projectDir);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 12. Resolve env vars and write .env files
|
|
185
|
-
console.log("Resolving environment variables...");
|
|
186
|
-
const allEnvs: Record<string, Record<string, string>> = {};
|
|
187
|
-
const isSingleComponent = componentNames.length === 1 && componentNames[0] === "default";
|
|
188
|
-
|
|
189
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
190
|
-
const env = resolveComponentEnv(component, context, resourceMap);
|
|
191
|
-
await resolveGenerators(component, env);
|
|
192
|
-
|
|
193
|
-
const port = componentPorts[name];
|
|
194
|
-
if (port && !env.PORT) {
|
|
195
|
-
env.PORT = String(port);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
allEnvs[name] = env;
|
|
199
|
-
|
|
200
|
-
if (opts.dryRun) {
|
|
201
|
-
console.log(` [dry-run] Env for ${name}: ${Object.keys(env).join(", ")}`);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (isSingleComponent) {
|
|
206
|
-
await writeEnvFile(join(projectDir, ".env.local"), env);
|
|
207
|
-
console.log(` Wrote .env.local (${Object.keys(env).length} vars)`);
|
|
208
|
-
} else {
|
|
209
|
-
const { mkdir } = await import("node:fs/promises");
|
|
210
|
-
await mkdir(join(projectDir, ".launchfile", "env"), { recursive: true });
|
|
211
|
-
await writeEnvFile(join(projectDir, ".launchfile", "env", `${name}.env`), env);
|
|
212
|
-
console.log(` Wrote .launchfile/env/${name}.env (${Object.keys(env).length} vars)`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 13. Save state before build (in case build fails, we still have resource state)
|
|
217
|
-
await saveState(projectDir, state);
|
|
218
|
-
|
|
219
|
-
if (opts.dryRun) {
|
|
220
|
-
console.log("\n[dry-run] Would now run build, release, and start commands.");
|
|
221
|
-
printSummary(launch, componentPorts, resourceMap);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 14. Run build commands
|
|
226
|
-
if (!opts.noBuild) {
|
|
227
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
228
|
-
const buildCmd = component.commands?.build?.command;
|
|
229
|
-
const cmd = buildCmd ?? pm?.installCommand;
|
|
230
|
-
if (cmd) {
|
|
231
|
-
console.log(`\n [${name}] Building: ${cmd}`);
|
|
232
|
-
await shell(cmd, {
|
|
233
|
-
cwd: projectDir,
|
|
234
|
-
env: allEnvs[name],
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// 15. Run release commands (migrations)
|
|
241
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
242
|
-
const releaseCmd = component.commands?.release?.command;
|
|
243
|
-
if (releaseCmd) {
|
|
244
|
-
console.log(`\n [${name}] Release: ${releaseCmd}`);
|
|
245
|
-
await shell(releaseCmd, {
|
|
246
|
-
cwd: projectDir,
|
|
247
|
-
env: allEnvs[name],
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 16. Start all components
|
|
253
|
-
console.log("\nStarting components...");
|
|
254
|
-
const pm2 = new ProcessManager(projectDir);
|
|
255
|
-
|
|
256
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
257
|
-
const startCmd = component.commands?.start?.command;
|
|
258
|
-
if (!startCmd) {
|
|
259
|
-
console.warn(` [${name}] No start command — skipping`);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
pm2.register(name, {
|
|
264
|
-
command: startCmd,
|
|
265
|
-
env: { ...process.env as Record<string, string>, ...allEnvs[name] },
|
|
266
|
-
cwd: projectDir,
|
|
267
|
-
dependsOn: component.depends_on,
|
|
268
|
-
health: component.health,
|
|
269
|
-
port: componentPorts[name],
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Handle Ctrl+C gracefully
|
|
274
|
-
process.on("SIGINT", async () => {
|
|
275
|
-
console.log("\n\nShutting down...");
|
|
276
|
-
await pm2.stopAll();
|
|
277
|
-
await saveState(projectDir, state!);
|
|
278
|
-
process.exit(0);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
await pm2.startAll();
|
|
282
|
-
|
|
283
|
-
// 17. Print summary
|
|
284
|
-
printSummary(launch, componentPorts, resourceMap);
|
|
285
|
-
|
|
286
|
-
// Save final state
|
|
287
|
-
await saveState(projectDir, state);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function printSummary(
|
|
291
|
-
launch: NormalizedLaunch,
|
|
292
|
-
ports: Record<string, number>,
|
|
293
|
-
resources: Record<string, ResourceProperties>,
|
|
294
|
-
): void {
|
|
295
|
-
console.log("\n" + "=".repeat(60));
|
|
296
|
-
console.log(` ${launch.name} is running`);
|
|
297
|
-
console.log("=".repeat(60));
|
|
298
|
-
|
|
299
|
-
for (const [name, port] of Object.entries(ports)) {
|
|
300
|
-
const component = launch.components[name];
|
|
301
|
-
const exposed = component?.provides?.some((p) => p.exposed);
|
|
302
|
-
const label = exposed ? "(public)" : "(internal)";
|
|
303
|
-
console.log(` ${name}: http://localhost:${port} ${label}`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (Object.keys(resources).length > 0) {
|
|
307
|
-
console.log("\n Resources:");
|
|
308
|
-
for (const [name, props] of Object.entries(resources)) {
|
|
309
|
-
console.log(` ${name}: ${props.url}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
console.log("\n Press Ctrl+C to stop all processes.");
|
|
314
|
-
console.log("=".repeat(60));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export async function launchDown(opts: { destroy?: boolean; projectDir?: string } = {}): Promise<void> {
|
|
318
|
-
const projectDir = opts.projectDir ?? process.cwd();
|
|
319
|
-
const state = await loadState(projectDir);
|
|
320
|
-
if (!state) {
|
|
321
|
-
console.log("No active launch state found.");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (opts.destroy) {
|
|
326
|
-
console.log("Destroying resources...");
|
|
327
|
-
for (const [name, resourceState] of Object.entries(state.resources)) {
|
|
328
|
-
const provisioner = getProvisioner(resourceState.type);
|
|
329
|
-
if (provisioner) {
|
|
330
|
-
console.log(` Destroying ${resourceState.type} (${name})...`);
|
|
331
|
-
await provisioner.destroy(resourceState);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Clean up state
|
|
336
|
-
const { rm } = await import("node:fs/promises");
|
|
337
|
-
await rm(join(projectDir, ".launchfile"), { recursive: true, force: true });
|
|
338
|
-
console.log(" Cleaned up .launchfile/");
|
|
339
|
-
} else {
|
|
340
|
-
console.log("Stopped. Resources are still running (use --destroy to remove them).");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export async function launchStatus(opts: { projectDir?: string } = {}): Promise<void> {
|
|
345
|
-
const projectDir = opts.projectDir ?? process.cwd();
|
|
346
|
-
const state = await loadState(projectDir);
|
|
347
|
-
if (!state) {
|
|
348
|
-
console.log("No active launch state found.");
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
console.log(`App: ${state.appName}`);
|
|
353
|
-
console.log(`Created: ${state.createdAt}`);
|
|
354
|
-
console.log(`Updated: ${state.updatedAt}`);
|
|
355
|
-
|
|
356
|
-
if (Object.keys(state.ports).length > 0) {
|
|
357
|
-
console.log("\nComponents:");
|
|
358
|
-
for (const [name, port] of Object.entries(state.ports)) {
|
|
359
|
-
console.log(` ${name}: http://localhost:${port}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (Object.keys(state.resources).length > 0) {
|
|
364
|
-
console.log("\nResources:");
|
|
365
|
-
for (const [name, res] of Object.entries(state.resources)) {
|
|
366
|
-
const provisioner = getProvisioner(res.type);
|
|
367
|
-
const running = provisioner ? await provisioner.isRunning() : false;
|
|
368
|
-
console.log(` ${name} (${res.type}): ${running ? "running" : "stopped"} on port ${res.port}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export async function launchEnv(opts: { component?: string; projectDir?: string } = {}): Promise<void> {
|
|
374
|
-
const projectDir = opts.projectDir ?? process.cwd();
|
|
375
|
-
|
|
376
|
-
const launchfileContent = await readFile(join(projectDir, "Launchfile"), "utf8");
|
|
377
|
-
const launch = readLaunch(launchfileContent);
|
|
378
|
-
const state = await loadState(projectDir);
|
|
379
|
-
|
|
380
|
-
if (!state) {
|
|
381
|
-
console.log("No active launch state. Run `launch up` first.");
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Rebuild resource map from state
|
|
386
|
-
const resourceMap: Record<string, ResourceProperties> = {};
|
|
387
|
-
for (const [name, res] of Object.entries(state.resources)) {
|
|
388
|
-
const provisioner = getProvisioner(res.type);
|
|
389
|
-
if (provisioner) {
|
|
390
|
-
const result = await provisioner.provision(
|
|
391
|
-
{ type: res.type, name: res.name },
|
|
392
|
-
{ appName: state.appName, projectDir },
|
|
393
|
-
res,
|
|
394
|
-
);
|
|
395
|
-
resourceMap[name] = result.properties;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const context = buildResolverContext(resourceMap, state.ports, state.secrets);
|
|
400
|
-
|
|
401
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
402
|
-
if (opts.component && name !== opts.component) continue;
|
|
403
|
-
|
|
404
|
-
const env = resolveComponentEnv(component, context, resourceMap);
|
|
405
|
-
await resolveGenerators(component, env);
|
|
406
|
-
|
|
407
|
-
const port = state.ports[name];
|
|
408
|
-
if (port && !env.PORT) env.PORT = String(port);
|
|
409
|
-
|
|
410
|
-
console.log(`\n# ${name}`);
|
|
411
|
-
for (const [key, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {
|
|
412
|
-
console.log(`${key}=${value}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
package/src/resources/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resource provisioner registry.
|
|
3
|
-
*
|
|
4
|
-
* Maps resource type names to their provisioner implementations.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ResourceProvisioner } from "./types.js";
|
|
8
|
-
import { PostgresProvisioner } from "./postgres.js";
|
|
9
|
-
import { RedisProvisioner } from "./redis.js";
|
|
10
|
-
import { SqliteProvisioner } from "./sqlite.js";
|
|
11
|
-
import { MysqlProvisioner } from "./mysql.js";
|
|
12
|
-
|
|
13
|
-
const provisioners: Record<string, ResourceProvisioner> = {
|
|
14
|
-
postgres: new PostgresProvisioner(),
|
|
15
|
-
redis: new RedisProvisioner(),
|
|
16
|
-
sqlite: new SqliteProvisioner(),
|
|
17
|
-
mysql: new MysqlProvisioner(),
|
|
18
|
-
mariadb: new MysqlProvisioner(), // MariaDB is MySQL-compatible
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function getProvisioner(type: string): ResourceProvisioner | undefined {
|
|
22
|
-
return provisioners[type];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function supportedResourceTypes(): string[] {
|
|
26
|
-
return Object.keys(provisioners);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type { ResourceProvisioner, ResourceProperties, ProvisionOpts } from "./types.js";
|
package/src/resources/mysql.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MySQL resource provisioner via Homebrew.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
6
|
-
import { shell, shellOk } from "../shell.js";
|
|
7
|
-
import { generatePassword } from "../secret-generator.js";
|
|
8
|
-
import type { ResourceState } from "../state.js";
|
|
9
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_PORT = 3306;
|
|
12
|
-
const DEFAULT_HOST = "localhost";
|
|
13
|
-
|
|
14
|
-
// Security: validate identifiers before interpolating into shell/SQL commands
|
|
15
|
-
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
16
|
-
|
|
17
|
-
export class MysqlProvisioner implements ResourceProvisioner {
|
|
18
|
-
readonly type = "mysql";
|
|
19
|
-
|
|
20
|
-
async isRunning(): Promise<boolean> {
|
|
21
|
-
return shellOk("mysqladmin ping -h localhost --silent");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async provision(
|
|
25
|
-
req: NormalizedRequirement,
|
|
26
|
-
opts: ProvisionOpts,
|
|
27
|
-
existingState?: ResourceState,
|
|
28
|
-
): Promise<{ properties: ResourceProperties; state: ResourceState }> {
|
|
29
|
-
if (!(await this.isRunning())) {
|
|
30
|
-
console.log(" Starting MySQL via brew...");
|
|
31
|
-
const started = await shellOk("brew services start mysql");
|
|
32
|
-
if (!started) {
|
|
33
|
-
await shell("brew install mysql");
|
|
34
|
-
await shell("brew services start mysql");
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const resourceName = req.name ?? req.type;
|
|
39
|
-
const safeName = opts.appName.replace(/-/g, "_");
|
|
40
|
-
const dbName = existingState?.dbName ?? `launchfile_${safeName}`;
|
|
41
|
-
const user = existingState?.user ?? `launchfile_${safeName}`;
|
|
42
|
-
const password = existingState?.password ?? generatePassword();
|
|
43
|
-
const port = DEFAULT_PORT;
|
|
44
|
-
|
|
45
|
-
// Create database and user (idempotent)
|
|
46
|
-
await shell(
|
|
47
|
-
`mysql -h ${DEFAULT_HOST} -u root -e "CREATE DATABASE IF NOT EXISTS \\\`${dbName}\\\`;"`,
|
|
48
|
-
{ allowFailure: true },
|
|
49
|
-
);
|
|
50
|
-
await shell(
|
|
51
|
-
`mysql -h ${DEFAULT_HOST} -u root -e "CREATE USER IF NOT EXISTS '${user}'@'${DEFAULT_HOST}' IDENTIFIED BY '${password}';"`,
|
|
52
|
-
{ allowFailure: true },
|
|
53
|
-
);
|
|
54
|
-
await shell(
|
|
55
|
-
`mysql -h ${DEFAULT_HOST} -u root -e "GRANT ALL PRIVILEGES ON \\\`${dbName}\\\`.* TO '${user}'@'${DEFAULT_HOST}';"`,
|
|
56
|
-
{ allowFailure: true },
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const url = `mysql://${user}:${password}@${DEFAULT_HOST}:${port}/${dbName}`;
|
|
60
|
-
|
|
61
|
-
const properties: ResourceProperties = {
|
|
62
|
-
url,
|
|
63
|
-
host: DEFAULT_HOST,
|
|
64
|
-
port,
|
|
65
|
-
user,
|
|
66
|
-
password,
|
|
67
|
-
name: dbName,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const state: ResourceState = {
|
|
71
|
-
type: "mysql",
|
|
72
|
-
name: resourceName,
|
|
73
|
-
brewService: "mysql",
|
|
74
|
-
port,
|
|
75
|
-
dbName,
|
|
76
|
-
user,
|
|
77
|
-
password,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
return { properties, state };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async destroy(state: ResourceState): Promise<void> {
|
|
84
|
-
// Security: state values come from disk (state.json) — validate before SQL interpolation
|
|
85
|
-
if (state.dbName && SAFE_IDENTIFIER.test(state.dbName)) {
|
|
86
|
-
await shell(
|
|
87
|
-
`mysql -h ${DEFAULT_HOST} -u root -e "DROP DATABASE IF EXISTS \\\`${state.dbName}\\\`;"`,
|
|
88
|
-
{ allowFailure: true },
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
if (state.user && SAFE_IDENTIFIER.test(state.user)) {
|
|
92
|
-
await shell(
|
|
93
|
-
`mysql -h ${DEFAULT_HOST} -u root -e "DROP USER IF EXISTS '${state.user}'@'${DEFAULT_HOST}';"`,
|
|
94
|
-
{ allowFailure: true },
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Postgres resource provisioner via Homebrew.
|
|
3
|
-
*
|
|
4
|
-
* Strategy:
|
|
5
|
-
* - Check if postgres is already running via `brew services list`
|
|
6
|
-
* - If not, install and start via brew
|
|
7
|
-
* - Create an app-specific database and user
|
|
8
|
-
* - Return connection properties
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
12
|
-
import { shell, shellOk } from "../shell.js";
|
|
13
|
-
import { generatePassword } from "../secret-generator.js";
|
|
14
|
-
import type { ResourceState } from "../state.js";
|
|
15
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
16
|
-
|
|
17
|
-
const DEFAULT_PORT = 5432;
|
|
18
|
-
const DEFAULT_HOST = "localhost";
|
|
19
|
-
|
|
20
|
-
// Security: validate identifiers before interpolating into shell/SQL commands.
|
|
21
|
-
// Only alphanumeric + underscore — safe for SQL identifiers and shell args.
|
|
22
|
-
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
23
|
-
|
|
24
|
-
export class PostgresProvisioner implements ResourceProvisioner {
|
|
25
|
-
readonly type = "postgres";
|
|
26
|
-
|
|
27
|
-
async isRunning(): Promise<boolean> {
|
|
28
|
-
return shellOk("pg_isready -q");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async provision(
|
|
32
|
-
req: NormalizedRequirement,
|
|
33
|
-
opts: ProvisionOpts,
|
|
34
|
-
existingState?: ResourceState,
|
|
35
|
-
): Promise<{ properties: ResourceProperties; state: ResourceState }> {
|
|
36
|
-
// Ensure postgres is running
|
|
37
|
-
if (!(await this.isRunning())) {
|
|
38
|
-
console.log(" Starting PostgreSQL via brew...");
|
|
39
|
-
// Try to start; if not installed, install first
|
|
40
|
-
const started = await shellOk("brew services start postgresql");
|
|
41
|
-
if (!started) {
|
|
42
|
-
// Try versioned formula
|
|
43
|
-
await shell("brew install postgresql@16");
|
|
44
|
-
await shell("brew services start postgresql@16");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Wait for ready
|
|
49
|
-
await shell("pg_isready --timeout=10", { allowFailure: true });
|
|
50
|
-
|
|
51
|
-
// Determine database and user names
|
|
52
|
-
const resourceName = req.name ?? req.type;
|
|
53
|
-
const safeName = opts.appName.replace(/-/g, "_");
|
|
54
|
-
const dbName = existingState?.dbName ?? `launchfile_${safeName}`;
|
|
55
|
-
const user = existingState?.user ?? `launchfile_${safeName}`;
|
|
56
|
-
const password = existingState?.password ?? generatePassword();
|
|
57
|
-
const port = DEFAULT_PORT;
|
|
58
|
-
|
|
59
|
-
// Create user (idempotent)
|
|
60
|
-
await shell(
|
|
61
|
-
`psql -h ${DEFAULT_HOST} -p ${port} postgres -c "DO \\$\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user}') THEN CREATE ROLE ${user} WITH LOGIN PASSWORD '${password}' CREATEDB; END IF; END \\$\\$;"`,
|
|
62
|
-
{ allowFailure: true },
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Create database (idempotent)
|
|
66
|
-
const dbExists = await shellOk(
|
|
67
|
-
`psql -h ${DEFAULT_HOST} -p ${port} postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${dbName}'"`,
|
|
68
|
-
);
|
|
69
|
-
if (!dbExists) {
|
|
70
|
-
await shell(
|
|
71
|
-
`createdb -h ${DEFAULT_HOST} -p ${port} -O ${user} ${dbName}`,
|
|
72
|
-
{ allowFailure: true },
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Handle extensions from config
|
|
77
|
-
// Security: extension names come from Launchfile config (untrusted input).
|
|
78
|
-
// Validate each against SAFE_IDENTIFIER before interpolating into SQL.
|
|
79
|
-
const extensions = req.config?.extensions;
|
|
80
|
-
if (Array.isArray(extensions)) {
|
|
81
|
-
for (const ext of extensions) {
|
|
82
|
-
if (typeof ext !== "string") continue;
|
|
83
|
-
if (!SAFE_IDENTIFIER.test(ext)) {
|
|
84
|
-
console.warn(` Skipping invalid extension name: ${ext}`);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
await shell(
|
|
88
|
-
`psql -h ${DEFAULT_HOST} -p ${port} ${dbName} -c "CREATE EXTENSION IF NOT EXISTS \\"${ext}\\";"`,
|
|
89
|
-
{ allowFailure: true },
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const url = `postgresql://${user}:${password}@${DEFAULT_HOST}:${port}/${dbName}`;
|
|
95
|
-
|
|
96
|
-
const properties: ResourceProperties = {
|
|
97
|
-
url,
|
|
98
|
-
host: DEFAULT_HOST,
|
|
99
|
-
port,
|
|
100
|
-
user,
|
|
101
|
-
password,
|
|
102
|
-
name: dbName,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const state: ResourceState = {
|
|
106
|
-
type: "postgres",
|
|
107
|
-
name: resourceName,
|
|
108
|
-
brewService: "postgresql",
|
|
109
|
-
port,
|
|
110
|
-
dbName,
|
|
111
|
-
user,
|
|
112
|
-
password,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return { properties, state };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async destroy(state: ResourceState): Promise<void> {
|
|
119
|
-
// Security: state values come from disk (state.json) — validate before SQL interpolation
|
|
120
|
-
if (state.dbName && SAFE_IDENTIFIER.test(state.dbName)) {
|
|
121
|
-
await shell(`dropdb -h ${DEFAULT_HOST} --if-exists ${state.dbName}`, {
|
|
122
|
-
allowFailure: true,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
if (state.user && SAFE_IDENTIFIER.test(state.user)) {
|
|
126
|
-
await shell(
|
|
127
|
-
`psql -h ${DEFAULT_HOST} postgres -c "DROP ROLE IF EXISTS ${state.user};"`,
|
|
128
|
-
{ allowFailure: true },
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
// Don't stop the brew service — it's shared
|
|
132
|
-
}
|
|
133
|
-
}
|