@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
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
|
-
}
|
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
|
-
}
|