@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/resources/redis.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redis resource provisioner via Homebrew.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
6
|
-
import { shell, shellOk } from "../shell.js";
|
|
7
|
-
import type { ResourceState } from "../state.js";
|
|
8
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
9
|
-
|
|
10
|
-
const DEFAULT_PORT = 6379;
|
|
11
|
-
const DEFAULT_HOST = "localhost";
|
|
12
|
-
|
|
13
|
-
export class RedisProvisioner implements ResourceProvisioner {
|
|
14
|
-
readonly type = "redis";
|
|
15
|
-
|
|
16
|
-
async isRunning(): Promise<boolean> {
|
|
17
|
-
return shellOk("redis-cli ping");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async provision(
|
|
21
|
-
req: NormalizedRequirement,
|
|
22
|
-
_opts: ProvisionOpts,
|
|
23
|
-
_existingState?: ResourceState,
|
|
24
|
-
): Promise<{ properties: ResourceProperties; state: ResourceState }> {
|
|
25
|
-
if (!(await this.isRunning())) {
|
|
26
|
-
console.log(" Starting Redis via brew...");
|
|
27
|
-
const started = await shellOk("brew services start redis");
|
|
28
|
-
if (!started) {
|
|
29
|
-
await shell("brew install redis");
|
|
30
|
-
await shell("brew services start redis");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const port = DEFAULT_PORT;
|
|
35
|
-
const resourceName = req.name ?? req.type;
|
|
36
|
-
|
|
37
|
-
const properties: ResourceProperties = {
|
|
38
|
-
url: `redis://${DEFAULT_HOST}:${port}/0`,
|
|
39
|
-
host: DEFAULT_HOST,
|
|
40
|
-
port,
|
|
41
|
-
password: "",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const state: ResourceState = {
|
|
45
|
-
type: "redis",
|
|
46
|
-
name: resourceName,
|
|
47
|
-
brewService: "redis",
|
|
48
|
-
port,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return { properties, state };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async destroy(_state: ResourceState): Promise<void> {
|
|
55
|
-
// Redis is shared, don't stop the service
|
|
56
|
-
// Could flush a specific database prefix, but not worth the complexity
|
|
57
|
-
}
|
|
58
|
-
}
|
package/src/resources/sqlite.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite resource provisioner — just creates a directory for the DB file.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
8
|
-
import type { ResourceState } from "../state.js";
|
|
9
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
10
|
-
|
|
11
|
-
export class SqliteProvisioner implements ResourceProvisioner {
|
|
12
|
-
readonly type = "sqlite";
|
|
13
|
-
|
|
14
|
-
async isRunning(): Promise<boolean> {
|
|
15
|
-
return true; // SQLite is always available
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async provision(
|
|
19
|
-
req: NormalizedRequirement,
|
|
20
|
-
opts: ProvisionOpts,
|
|
21
|
-
): Promise<{ properties: ResourceProperties; state: ResourceState }> {
|
|
22
|
-
const resourceName = req.name ?? req.type;
|
|
23
|
-
const safeName = opts.appName.replace(/-/g, "_");
|
|
24
|
-
const dataDir = join(opts.projectDir, ".launchfile", "data", "sqlite");
|
|
25
|
-
await mkdir(dataDir, { recursive: true });
|
|
26
|
-
|
|
27
|
-
const dbPath = join(dataDir, `${safeName}.db`);
|
|
28
|
-
|
|
29
|
-
const properties: ResourceProperties = {
|
|
30
|
-
url: `sqlite://${dbPath}`,
|
|
31
|
-
host: "",
|
|
32
|
-
port: 0,
|
|
33
|
-
path: dbPath,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const state: ResourceState = {
|
|
37
|
-
type: "sqlite",
|
|
38
|
-
name: resourceName,
|
|
39
|
-
port: 0,
|
|
40
|
-
dbName: dbPath,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return { properties, state };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async destroy(state: ResourceState): Promise<void> {
|
|
47
|
-
if (state.dbName) {
|
|
48
|
-
const { rm } = await import("node:fs/promises");
|
|
49
|
-
await rm(state.dbName, { force: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/resources/types.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resource provisioner interface.
|
|
3
|
-
*
|
|
4
|
-
* Each resource type (postgres, redis, etc.) implements this interface
|
|
5
|
-
* to handle install, start, database creation, and teardown.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
9
|
-
import type { ResourceState } from "../state.js";
|
|
10
|
-
|
|
11
|
-
/** Properties that a provisioned resource exposes for expression resolution */
|
|
12
|
-
export interface ResourceProperties {
|
|
13
|
-
url: string;
|
|
14
|
-
host: string;
|
|
15
|
-
port: number;
|
|
16
|
-
user?: string;
|
|
17
|
-
password?: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
path?: string;
|
|
20
|
-
access_key?: string;
|
|
21
|
-
secret_key?: string;
|
|
22
|
-
bucket?: string;
|
|
23
|
-
region?: string;
|
|
24
|
-
[key: string]: string | number | undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ProvisionOpts {
|
|
28
|
-
appName: string;
|
|
29
|
-
projectDir: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ResourceProvisioner {
|
|
33
|
-
readonly type: string;
|
|
34
|
-
|
|
35
|
-
/** Check if the service is already running */
|
|
36
|
-
isRunning(): Promise<boolean>;
|
|
37
|
-
|
|
38
|
-
/** Ensure the service is installed and running, create app-specific resources */
|
|
39
|
-
provision(
|
|
40
|
-
req: NormalizedRequirement,
|
|
41
|
-
opts: ProvisionOpts,
|
|
42
|
-
existingState?: ResourceState,
|
|
43
|
-
): Promise<{ properties: ResourceProperties; state: ResourceState }>;
|
|
44
|
-
|
|
45
|
-
/** Drop app-specific databases/users (destroy mode) */
|
|
46
|
-
destroy(state: ResourceState): Promise<void>;
|
|
47
|
-
}
|
package/src/runtimes/bun.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bun runtime installer via Homebrew.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { shellOk, shell } from "../shell.js";
|
|
6
|
-
import type { RuntimeInstaller } from "./types.js";
|
|
7
|
-
|
|
8
|
-
export class BunInstaller implements RuntimeInstaller {
|
|
9
|
-
readonly runtime = "bun";
|
|
10
|
-
|
|
11
|
-
async detectVersion(_projectDir: string): Promise<string | undefined> {
|
|
12
|
-
// Bun doesn't have a standard version file yet; use whatever is installed
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async install(_version: string): Promise<void> {
|
|
17
|
-
if (!(await shellOk("which bun"))) {
|
|
18
|
-
await shell("brew install bun");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async shellEnv(_version: string): Promise<Record<string, string>> {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|
package/src/runtimes/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime installer registry.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { Runtime } from "@launchfile/sdk";
|
|
6
|
-
import type { RuntimeInstaller } from "./types.js";
|
|
7
|
-
import { NodeInstaller } from "./node.js";
|
|
8
|
-
import { BunInstaller } from "./bun.js";
|
|
9
|
-
import { RubyInstaller } from "./ruby.js";
|
|
10
|
-
import { PythonInstaller } from "./python.js";
|
|
11
|
-
|
|
12
|
-
const installers: Partial<Record<Runtime, RuntimeInstaller>> = {
|
|
13
|
-
node: new NodeInstaller(),
|
|
14
|
-
bun: new BunInstaller(),
|
|
15
|
-
ruby: new RubyInstaller(),
|
|
16
|
-
python: new PythonInstaller(),
|
|
17
|
-
// go, rust, etc. can be added later
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function getRuntimeInstaller(runtime: Runtime): RuntimeInstaller | undefined {
|
|
21
|
-
return installers[runtime];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type { RuntimeInstaller } from "./types.js";
|
package/src/runtimes/node.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node.js runtime installer via fnm (or nvm fallback).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { shell, shellOk } from "../shell.js";
|
|
8
|
-
import type { RuntimeInstaller } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export class NodeInstaller implements RuntimeInstaller {
|
|
11
|
-
readonly runtime = "node";
|
|
12
|
-
|
|
13
|
-
async detectVersion(projectDir: string): Promise<string | undefined> {
|
|
14
|
-
// Priority: .nvmrc > .node-version > package.json engines.node
|
|
15
|
-
for (const file of [".nvmrc", ".node-version"]) {
|
|
16
|
-
try {
|
|
17
|
-
const content = await readFile(join(projectDir, file), "utf8");
|
|
18
|
-
const version = content.trim();
|
|
19
|
-
if (version) return version;
|
|
20
|
-
} catch {
|
|
21
|
-
// File doesn't exist
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const pkg = JSON.parse(await readFile(join(projectDir, "package.json"), "utf8"));
|
|
27
|
-
if (pkg.engines?.node) return pkg.engines.node;
|
|
28
|
-
} catch {
|
|
29
|
-
// No package.json
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async install(version: string): Promise<void> {
|
|
36
|
-
// Prefer fnm (faster), fall back to nvm
|
|
37
|
-
if (await shellOk("which fnm")) {
|
|
38
|
-
await shell(`fnm install ${version}`);
|
|
39
|
-
} else if (await shellOk("which nvm")) {
|
|
40
|
-
await shell(`nvm install ${version}`);
|
|
41
|
-
} else {
|
|
42
|
-
console.log(" Installing fnm via brew...");
|
|
43
|
-
await shell("brew install fnm");
|
|
44
|
-
await shell(`fnm install ${version}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async shellEnv(_version: string): Promise<Record<string, string>> {
|
|
49
|
-
if (await shellOk("which fnm")) {
|
|
50
|
-
try {
|
|
51
|
-
const result = await shell("fnm env --shell bash", { silent: true });
|
|
52
|
-
return parseShellEnv(result.stdout);
|
|
53
|
-
} catch {
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Parse `export KEY="VALUE"` lines from fnm env output */
|
|
62
|
-
function parseShellEnv(output: string): Record<string, string> {
|
|
63
|
-
const env: Record<string, string> = {};
|
|
64
|
-
for (const line of output.split("\n")) {
|
|
65
|
-
const match = /^export\s+(\w+)="?([^"]*)"?/.exec(line);
|
|
66
|
-
if (match) {
|
|
67
|
-
env[match[1]!] = match[2]!;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return env;
|
|
71
|
-
}
|
package/src/runtimes/python.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python runtime installer via pyenv.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { shell, shellOk } from "../shell.js";
|
|
8
|
-
import type { RuntimeInstaller } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export class PythonInstaller implements RuntimeInstaller {
|
|
11
|
-
readonly runtime = "python";
|
|
12
|
-
|
|
13
|
-
async detectVersion(projectDir: string): Promise<string | undefined> {
|
|
14
|
-
try {
|
|
15
|
-
const content = await readFile(join(projectDir, ".python-version"), "utf8");
|
|
16
|
-
return content.trim() || undefined;
|
|
17
|
-
} catch {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async install(version: string): Promise<void> {
|
|
23
|
-
if (!(await shellOk("which pyenv"))) {
|
|
24
|
-
await shell("brew install pyenv");
|
|
25
|
-
}
|
|
26
|
-
if (!(await shellOk(`pyenv versions --bare | grep -q "^${version}$"`))) {
|
|
27
|
-
await shell(`pyenv install ${version}`);
|
|
28
|
-
}
|
|
29
|
-
await shell(`pyenv local ${version}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async shellEnv(_version: string): Promise<Record<string, string>> {
|
|
33
|
-
if (await shellOk("which pyenv")) {
|
|
34
|
-
const result = await shell("pyenv root", { silent: true, allowFailure: true });
|
|
35
|
-
if (result.exitCode === 0) {
|
|
36
|
-
const root = result.stdout.trim();
|
|
37
|
-
return { PATH: `${root}/shims:${process.env.PATH ?? ""}` };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/runtimes/ruby.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ruby runtime installer via rbenv.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { shell, shellOk } from "../shell.js";
|
|
8
|
-
import type { RuntimeInstaller } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export class RubyInstaller implements RuntimeInstaller {
|
|
11
|
-
readonly runtime = "ruby";
|
|
12
|
-
|
|
13
|
-
async detectVersion(projectDir: string): Promise<string | undefined> {
|
|
14
|
-
try {
|
|
15
|
-
const content = await readFile(join(projectDir, ".ruby-version"), "utf8");
|
|
16
|
-
return content.trim() || undefined;
|
|
17
|
-
} catch {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async install(version: string): Promise<void> {
|
|
23
|
-
if (!(await shellOk("which rbenv"))) {
|
|
24
|
-
await shell("brew install rbenv ruby-build");
|
|
25
|
-
}
|
|
26
|
-
const installed = await shellOk(`rbenv versions --bare | grep -q "^${version}$"`);
|
|
27
|
-
if (!installed) {
|
|
28
|
-
await shell(`rbenv install ${version}`);
|
|
29
|
-
}
|
|
30
|
-
await shell(`rbenv local ${version}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async shellEnv(_version: string): Promise<Record<string, string>> {
|
|
34
|
-
if (await shellOk("which rbenv")) {
|
|
35
|
-
try {
|
|
36
|
-
const result = await shell("rbenv init - bash", { silent: true });
|
|
37
|
-
return parseRbenvEnv(result.stdout);
|
|
38
|
-
} catch {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseRbenvEnv(output: string): Record<string, string> {
|
|
47
|
-
const env: Record<string, string> = {};
|
|
48
|
-
for (const line of output.split("\n")) {
|
|
49
|
-
const match = /^export\s+(\w+)="?([^"]*)"?/.exec(line);
|
|
50
|
-
if (match) {
|
|
51
|
-
env[match[1]!] = match[2]!;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return env;
|
|
55
|
-
}
|
package/src/runtimes/types.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime installer interface.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface RuntimeInstaller {
|
|
6
|
-
readonly runtime: string;
|
|
7
|
-
|
|
8
|
-
/** Detect the version from project files (.nvmrc, .ruby-version, etc.) */
|
|
9
|
-
detectVersion(projectDir: string): Promise<string | undefined>;
|
|
10
|
-
|
|
11
|
-
/** Ensure the runtime version is installed */
|
|
12
|
-
install(version: string): Promise<void>;
|
|
13
|
-
|
|
14
|
-
/** Get any shell env modifications needed (PATH, etc.) */
|
|
15
|
-
shellEnv(version: string): Promise<Record<string, string>>;
|
|
16
|
-
}
|
package/src/secret-generator.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secret and value generators for Launchfile env vars.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
6
|
-
import type { Generator } from "@launchfile/sdk";
|
|
7
|
-
import { allocatePort } from "./port-allocator.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate a value based on the generator type.
|
|
11
|
-
*/
|
|
12
|
-
export async function generateValue(
|
|
13
|
-
generator: Generator,
|
|
14
|
-
existingPorts?: Set<number>,
|
|
15
|
-
): Promise<string> {
|
|
16
|
-
switch (generator) {
|
|
17
|
-
case "secret":
|
|
18
|
-
return randomBytes(32).toString("base64url");
|
|
19
|
-
case "uuid":
|
|
20
|
-
return randomUUID();
|
|
21
|
-
case "port": {
|
|
22
|
-
const port = await allocatePort(`generated-${Date.now()}`, existingPorts ?? new Set());
|
|
23
|
-
return String(port);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Generate a password suitable for database users */
|
|
29
|
-
export function generatePassword(): string {
|
|
30
|
-
// URL-safe so it can go in connection strings without encoding
|
|
31
|
-
return randomBytes(24).toString("base64url");
|
|
32
|
-
}
|
package/src/shell.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell execution helper with timeout, logging, and structured results.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { exec as cpExec, type ExecOptions } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
export interface ShellResult {
|
|
8
|
-
exitCode: number;
|
|
9
|
-
stdout: string;
|
|
10
|
-
stderr: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ShellOpts {
|
|
14
|
-
cwd?: string;
|
|
15
|
-
env?: Record<string, string>;
|
|
16
|
-
timeout?: number;
|
|
17
|
-
/** If true, don't log the command being run */
|
|
18
|
-
silent?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Run a shell command and return structured output.
|
|
23
|
-
* Throws on non-zero exit code unless `allowFailure` is set.
|
|
24
|
-
*/
|
|
25
|
-
export async function shell(
|
|
26
|
-
command: string,
|
|
27
|
-
opts: ShellOpts & { allowFailure?: boolean } = {},
|
|
28
|
-
): Promise<ShellResult> {
|
|
29
|
-
if (!opts.silent) {
|
|
30
|
-
console.log(` $ ${command}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const execOpts: ExecOptions = {
|
|
34
|
-
cwd: opts.cwd,
|
|
35
|
-
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
36
|
-
timeout: opts.timeout ?? 120_000,
|
|
37
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
cpExec(command, execOpts, (error, stdout, stderr) => {
|
|
42
|
-
const result: ShellResult = {
|
|
43
|
-
exitCode: error?.code ?? (typeof error?.code === "number" ? error.code : 0),
|
|
44
|
-
stdout: typeof stdout === "string" ? stdout : "",
|
|
45
|
-
stderr: typeof stderr === "string" ? stderr : "",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Node's exec sets error.code to the exit code on non-zero
|
|
49
|
-
if (error && result.exitCode === 0) {
|
|
50
|
-
result.exitCode = 1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (error && !opts.allowFailure) {
|
|
54
|
-
reject(
|
|
55
|
-
Object.assign(new Error(`Command failed: ${command}\n${result.stderr}`), {
|
|
56
|
-
result,
|
|
57
|
-
}),
|
|
58
|
-
);
|
|
59
|
-
} else {
|
|
60
|
-
resolve(result);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Run a command, return true if exit code is 0 */
|
|
67
|
-
export async function shellOk(command: string, opts?: ShellOpts): Promise<boolean> {
|
|
68
|
-
const result = await shell(command, { ...opts, allowFailure: true, silent: true });
|
|
69
|
-
return result.exitCode === 0;
|
|
70
|
-
}
|
package/src/state.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State management for the macOS dev provider.
|
|
3
|
-
*
|
|
4
|
-
* Persists secrets, ports, and resource state in .launchfile/state.json
|
|
5
|
-
* so credentials and ports are stable across restarts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
|
|
12
|
-
export interface ResourceState {
|
|
13
|
-
type: string;
|
|
14
|
-
name: string;
|
|
15
|
-
brewService?: string;
|
|
16
|
-
port: number;
|
|
17
|
-
dbName?: string;
|
|
18
|
-
user?: string;
|
|
19
|
-
password?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface LaunchState {
|
|
23
|
-
version: 1;
|
|
24
|
-
appName: string;
|
|
25
|
-
launchfileHash: string;
|
|
26
|
-
createdAt: string;
|
|
27
|
-
updatedAt: string;
|
|
28
|
-
resources: Record<string, ResourceState>;
|
|
29
|
-
secrets: Record<string, string>;
|
|
30
|
-
ports: Record<string, number>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const STATE_DIR = ".launchfile";
|
|
34
|
-
const STATE_FILE = "state.json";
|
|
35
|
-
|
|
36
|
-
function stateDir(projectDir: string): string {
|
|
37
|
-
return join(projectDir, STATE_DIR);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function statePath(projectDir: string): string {
|
|
41
|
-
return join(stateDir(projectDir), STATE_FILE);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function hashLaunchfile(content: string): string {
|
|
45
|
-
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Load state from disk, or return null if none exists */
|
|
49
|
-
export async function loadState(projectDir: string): Promise<LaunchState | null> {
|
|
50
|
-
try {
|
|
51
|
-
const raw = await readFile(statePath(projectDir), "utf8");
|
|
52
|
-
return JSON.parse(raw) as LaunchState;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Create a fresh state object */
|
|
59
|
-
export function initState(appName: string, launchfileContent: string): LaunchState {
|
|
60
|
-
const now = new Date().toISOString();
|
|
61
|
-
return {
|
|
62
|
-
version: 1,
|
|
63
|
-
appName,
|
|
64
|
-
launchfileHash: hashLaunchfile(launchfileContent),
|
|
65
|
-
createdAt: now,
|
|
66
|
-
updatedAt: now,
|
|
67
|
-
resources: {},
|
|
68
|
-
secrets: {},
|
|
69
|
-
ports: {},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Save state to disk */
|
|
74
|
-
export async function saveState(projectDir: string, state: LaunchState): Promise<void> {
|
|
75
|
-
state.updatedAt = new Date().toISOString();
|
|
76
|
-
// Security: restrict directory/file permissions — state.json contains
|
|
77
|
-
// database passwords and generated secrets in plaintext.
|
|
78
|
-
await mkdir(stateDir(projectDir), { recursive: true, mode: 0o700 });
|
|
79
|
-
await writeFile(statePath(projectDir), JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Ensure .launchfile directories exist */
|
|
83
|
-
export async function ensureDirs(projectDir: string): Promise<void> {
|
|
84
|
-
const dirs = ["storage", "tmp", "logs", "data", "env"];
|
|
85
|
-
// Security: restrict permissions — these dirs contain secrets, logs, env files
|
|
86
|
-
await Promise.all(
|
|
87
|
-
dirs.map((d) => mkdir(join(projectDir, STATE_DIR, d), { recursive: true, mode: 0o700 })),
|
|
88
|
-
);
|
|
89
|
-
// Safety net: write a .gitignore inside .launchfile/ so secrets aren't
|
|
90
|
-
// accidentally committed even if the project's .gitignore doesn't exclude it.
|
|
91
|
-
await writeFile(join(projectDir, STATE_DIR, ".gitignore"), "*\n", { mode: 0o644 });
|
|
92
|
-
}
|
package/src/storage.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local storage provisioning for Launchfile volumes.
|
|
3
|
-
*
|
|
4
|
-
* Maps container paths to local directories under .launchfile/.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import type { StorageVolume } from "@launchfile/sdk";
|
|
10
|
-
|
|
11
|
-
const STATE_DIR = ".launchfile";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Create local directories for a component's storage volumes.
|
|
15
|
-
* Returns a map of container path → local path.
|
|
16
|
-
*/
|
|
17
|
-
export async function provisionStorage(
|
|
18
|
-
storage: Record<string, StorageVolume> | undefined,
|
|
19
|
-
componentName: string,
|
|
20
|
-
projectDir: string,
|
|
21
|
-
): Promise<Record<string, string>> {
|
|
22
|
-
if (!storage) return {};
|
|
23
|
-
|
|
24
|
-
const volumeMap: Record<string, string> = {};
|
|
25
|
-
|
|
26
|
-
for (const [name, volume] of Object.entries(storage)) {
|
|
27
|
-
const persistent = volume.persistent !== false; // default true
|
|
28
|
-
const subdir = persistent ? "storage" : "tmp";
|
|
29
|
-
const localPath = join(projectDir, STATE_DIR, subdir, componentName, name);
|
|
30
|
-
|
|
31
|
-
await mkdir(localPath, { recursive: true });
|
|
32
|
-
volumeMap[volume.path] = localPath;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return volumeMap;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Clean up ephemeral (non-persistent) storage for a component */
|
|
39
|
-
export async function cleanEphemeralStorage(
|
|
40
|
-
storage: Record<string, StorageVolume> | undefined,
|
|
41
|
-
componentName: string,
|
|
42
|
-
projectDir: string,
|
|
43
|
-
): Promise<void> {
|
|
44
|
-
if (!storage) return;
|
|
45
|
-
|
|
46
|
-
for (const [name, volume] of Object.entries(storage)) {
|
|
47
|
-
if (volume.persistent === false) {
|
|
48
|
-
const localPath = join(projectDir, STATE_DIR, "tmp", componentName, name);
|
|
49
|
-
await rm(localPath, { recursive: true, force: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Clean up all storage (persistent + ephemeral) for a component */
|
|
55
|
-
export async function destroyStorage(
|
|
56
|
-
componentName: string,
|
|
57
|
-
projectDir: string,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
await rm(join(projectDir, STATE_DIR, "storage", componentName), {
|
|
60
|
-
recursive: true,
|
|
61
|
-
force: true,
|
|
62
|
-
});
|
|
63
|
-
await rm(join(projectDir, STATE_DIR, "tmp", componentName), {
|
|
64
|
-
recursive: true,
|
|
65
|
-
force: true,
|
|
66
|
-
});
|
|
67
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"noUncheckedIndexedAccess": true,
|
|
8
|
-
"isolatedModules": true,
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"declarationMap": true,
|
|
11
|
-
"sourceMap": true,
|
|
12
|
-
"rootDir": "src",
|
|
13
|
-
"outDir": "dist",
|
|
14
|
-
"types": ["bun-types", "vitest/globals"],
|
|
15
|
-
"skipLibCheck": true
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*.ts"]
|
|
18
|
-
}
|