everything-dev 1.16.1 → 1.16.2

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.
@@ -0,0 +1,190 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import type { RuntimeConfig } from "../types";
6
+
7
+ const POSTGRES_USER = "everythingdev";
8
+ const POSTGRES_PASSWORD = "everythingdev";
9
+ const API_DATABASE_SECRET = "API_DATABASE_URL";
10
+ const AUTH_DATABASE_SECRET = "AUTH_DATABASE_URL";
11
+ const BASE_DATABASE_PORT = 5434;
12
+
13
+ interface DatabaseSecretConfig {
14
+ secret: string;
15
+ slug: string;
16
+ port: number;
17
+ serviceName: string;
18
+ databaseName: string;
19
+ volumeName: string;
20
+ url: string;
21
+ }
22
+
23
+ function uniqueSecrets(values: Array<string | undefined>): string[] {
24
+ const secrets: string[] = [];
25
+ const seen = new Set<string>();
26
+
27
+ for (const value of values) {
28
+ if (!value || seen.has(value)) continue;
29
+ seen.add(value);
30
+ secrets.push(value);
31
+ }
32
+
33
+ return secrets;
34
+ }
35
+
36
+ function getRuntimeSecrets(runtimeConfig: RuntimeConfig): string[] {
37
+ const pluginSecrets = Object.values(runtimeConfig.plugins ?? {}).flatMap(
38
+ (plugin) => plugin.secrets ?? [],
39
+ );
40
+
41
+ return uniqueSecrets([
42
+ API_DATABASE_SECRET,
43
+ AUTH_DATABASE_SECRET,
44
+ ...(runtimeConfig.api.secrets ?? []),
45
+ ...(runtimeConfig.auth?.secrets ?? []),
46
+ ...pluginSecrets,
47
+ ]);
48
+ }
49
+
50
+ function normalizeDatabaseSlug(secret: string): string {
51
+ return secret.replace(/_DATABASE_URL$/, "").toLowerCase();
52
+ }
53
+
54
+ function buildDatabaseConfigs(secrets: string[]): DatabaseSecretConfig[] {
55
+ const databaseSecrets = uniqueSecrets(
56
+ secrets.filter((secret) => secret.endsWith("_DATABASE_URL")),
57
+ );
58
+
59
+ const additionalSecrets = databaseSecrets
60
+ .filter((secret) => secret !== API_DATABASE_SECRET && secret !== AUTH_DATABASE_SECRET)
61
+ .sort((a, b) => a.localeCompare(b));
62
+
63
+ const orderedSecrets = [API_DATABASE_SECRET, AUTH_DATABASE_SECRET, ...additionalSecrets];
64
+
65
+ return orderedSecrets.map((secret, index) => {
66
+ const slug = normalizeDatabaseSlug(secret);
67
+ const port =
68
+ secret === API_DATABASE_SECRET
69
+ ? 5432
70
+ : secret === AUTH_DATABASE_SECRET
71
+ ? 5433
72
+ : BASE_DATABASE_PORT + index - 2;
73
+
74
+ return {
75
+ secret,
76
+ slug,
77
+ port,
78
+ serviceName: `postgres-${slug.replace(/_/g, "-")}`,
79
+ databaseName: `${slug}_db`,
80
+ volumeName: `postgres_${slug}_data`,
81
+ url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,
82
+ };
83
+ });
84
+ }
85
+
86
+ function defaultSecretValue(
87
+ secret: string,
88
+ databases: Map<string, DatabaseSecretConfig>,
89
+ options: { forExample: boolean },
90
+ ): string {
91
+ if (secret === "BETTER_AUTH_SECRET") {
92
+ return options.forExample ? "" : randomBytes(32).toString("base64url");
93
+ }
94
+
95
+ if (secret === "CORS_ORIGIN") {
96
+ return "http://localhost:3000";
97
+ }
98
+
99
+ return databases.get(secret)?.url ?? "";
100
+ }
101
+
102
+ function renderEnvFile(
103
+ secrets: string[],
104
+ databases: DatabaseSecretConfig[],
105
+ options: { forExample: boolean },
106
+ ): string {
107
+ const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));
108
+ const lines = [
109
+ "# Generated from configured bos secrets",
110
+ "# Update values as needed for your local environment",
111
+ "",
112
+ ];
113
+
114
+ for (const secret of secrets) {
115
+ lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, options)}`);
116
+ }
117
+
118
+ return `${lines.join("\n")}\n`;
119
+ }
120
+
121
+ function renderDockerCompose(databases: DatabaseSecretConfig[]): string {
122
+ const lines = [
123
+ "x-pg-common: &pg-common",
124
+ " image: postgres:17-alpine",
125
+ " environment: &pg-env",
126
+ ` POSTGRES_USER: ${POSTGRES_USER}`,
127
+ ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,
128
+ " healthcheck:",
129
+ ' test: ["CMD-SHELL", "pg_isready -U everythingdev"]',
130
+ " interval: 3s",
131
+ " timeout: 3s",
132
+ " retries: 5",
133
+ "",
134
+ "services:",
135
+ ];
136
+
137
+ for (const database of databases) {
138
+ lines.push(` ${database.serviceName}:`);
139
+ lines.push(" <<: *pg-common");
140
+ lines.push(" environment:");
141
+ lines.push(" <<: *pg-env");
142
+ lines.push(` POSTGRES_DB: ${database.databaseName}`);
143
+ lines.push(" ports:");
144
+ lines.push(` - "${database.port}:5432"`);
145
+ lines.push(" volumes:");
146
+ lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);
147
+ lines.push("");
148
+ }
149
+
150
+ lines.push("volumes:");
151
+ for (const database of databases) {
152
+ lines.push(` ${database.volumeName}:`);
153
+ }
154
+
155
+ return `${lines.join("\n")}\n`;
156
+ }
157
+
158
+ export function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {
159
+ const secrets = getRuntimeSecrets(runtimeConfig);
160
+ const databases = buildDatabaseConfigs(secrets);
161
+ const envExamplePath = join(configDir, ".env.example");
162
+ const dockerComposePath = join(configDir, "docker-compose.yml");
163
+
164
+ writeFileSync(envExamplePath, renderEnvFile(secrets, databases, { forExample: true }));
165
+ writeFileSync(dockerComposePath, renderDockerCompose(databases));
166
+
167
+ return secrets;
168
+ }
169
+
170
+ export function ensureEnvFile(configDir: string): void {
171
+ const envPath = join(configDir, ".env");
172
+ const examplePath = join(configDir, ".env.example");
173
+
174
+ if (existsSync(envPath) || !existsSync(examplePath)) return;
175
+
176
+ const content = readFileSync(examplePath, "utf-8");
177
+ const lines = content.split("\n");
178
+ const secret = randomBytes(32).toString("base64url");
179
+ const updated = lines
180
+ .map((line) => {
181
+ if (/^BETTER_AUTH_SECRET=/.test(line)) {
182
+ return `BETTER_AUTH_SECRET=${secret}`;
183
+ }
184
+ return line;
185
+ })
186
+ .join("\n");
187
+
188
+ writeFileSync(envPath, updated);
189
+ p.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET");
190
+ }
package/src/cli/init.ts CHANGED
@@ -609,6 +609,10 @@ export async function runTypesGen(destination: string): Promise<void> {
609
609
  await execCommand("node_modules/.bin/bos", ["types", "gen"], destination);
610
610
  }
611
611
 
612
+ export async function runDockerComposeUp(destination: string): Promise<void> {
613
+ await execCommand("docker", ["compose", "up", "-d", "--wait"], destination);
614
+ }
615
+
612
616
  const WORKSPACE_LOCAL_PATHS: Record<string, string> = {
613
617
  "everything-dev": "packages/everything-dev",
614
618
  "every-plugin": "packages/every-plugin",
package/src/cli/sync.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { glob } from "glob";
12
+ import { loadConfig } from "../config";
12
13
  import type { SyncOptions, SyncResult } from "../contract";
13
14
  import {
14
15
  isPlainObject as isPlainObjectFromMerge,
@@ -17,6 +18,7 @@ import {
17
18
  } from "../merge";
18
19
  import type { BosPluginRef } from "../types";
19
20
  import { isPathExcluded } from "../utils/path-match";
21
+ import { writeGeneratedInfra } from "./infra";
20
22
  import {
21
23
  personalizeConfig,
22
24
  readTemplatekeep,
@@ -27,10 +29,12 @@ import {
27
29
  import { readSnapshot, writeSnapshot } from "./snapshot";
28
30
 
29
31
  const FRAMEWORK_OWNED_SYNC_FILES = new Set([
32
+ ".env.example",
30
33
  ".gitignore",
31
34
  "biome.json",
32
35
  "bos.config.json",
33
36
  "package.json",
37
+ "docker-compose.yml",
34
38
  ".github/renovate.json",
35
39
  ".github/workflows/ci.yml",
36
40
  ".github/workflows/release-sync.yml",
@@ -494,6 +498,11 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
494
498
  mode: "sync",
495
499
  });
496
500
 
501
+ const syncedConfig = await loadConfig({ cwd: projectDir });
502
+ if (syncedConfig?.runtime) {
503
+ writeGeneratedInfra(projectDir, syncedConfig.runtime);
504
+ }
505
+
497
506
  if (!options.noInstall) {
498
507
  await runBunInstall(projectDir);
499
508
  await runTypesGen(projectDir);
package/src/plugin.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
2
  import { basename, dirname, join, resolve } from "node:path";
4
3
  import * as p from "@clack/prompts";
5
4
  import { Effect } from "effect";
6
5
  import { syncApiContractBridge } from "./api-contract";
7
6
  import { buildRuntimeConfig, detectLocalPackages, prepareDevelopmentRuntimeConfig } from "./app";
7
+ import { ensureEnvFile, writeGeneratedInfra } from "./cli/infra";
8
8
  import {
9
9
  copyFilteredFiles,
10
10
  fetchParentConfig,
@@ -13,6 +13,7 @@ import {
13
13
  readTemplatekeep,
14
14
  resolveSourceDir,
15
15
  runBunInstall,
16
+ runDockerComposeUp,
16
17
  runTypesGen,
17
18
  writeInitSnapshot,
18
19
  } from "./cli/init";
@@ -57,38 +58,6 @@ import { run } from "./utils/run";
57
58
  import { saveBosConfig } from "./utils/save-config";
58
59
  import { colors } from "./utils/theme";
59
60
 
60
- function ensureEnvFile(configDir: string, opts?: { domain?: string }): void {
61
- const envPath = join(configDir, ".env");
62
- const examplePath = join(configDir, ".env.example");
63
-
64
- if (existsSync(envPath)) return;
65
-
66
- if (!existsSync(examplePath)) return;
67
-
68
- const content = readFileSync(examplePath, "utf-8");
69
- const lines = content.split("\n");
70
-
71
- const secret = randomBytes(32).toString("base64url");
72
- const corsOrigin = opts?.domain
73
- ? `http://localhost:3000,https://${opts.domain}`
74
- : "http://localhost:3000";
75
-
76
- const updated = lines
77
- .map((line) => {
78
- if (/^BETTER_AUTH_SECRET=/.test(line)) {
79
- return `BETTER_AUTH_SECRET=${secret}`;
80
- }
81
- if (/^CORS_ORIGIN=/.test(line)) {
82
- return `CORS_ORIGIN=${corsOrigin}`;
83
- }
84
- return line;
85
- })
86
- .join("\n");
87
-
88
- writeFileSync(envPath, updated);
89
- p.log.info(`Created .env from .env.example with generated BETTER_AUTH_SECRET`);
90
- }
91
-
92
61
  const buildCommands: Record<string, { cmd: string; args: string[] }> = {
93
62
  host: { cmd: "bun", args: ["run", "build"] },
94
63
  ui: { cmd: "bun", args: ["run", "build"] },
@@ -1342,6 +1311,7 @@ export default createPlugin({
1342
1311
  }
1343
1312
 
1344
1313
  directory = directory || domain || extendsGateway;
1314
+ const targetDir = resolve(directory);
1345
1315
  plugins = plugins ?? [];
1346
1316
 
1347
1317
  if (!parentConfig) {
@@ -1405,13 +1375,13 @@ export default createPlugin({
1405
1375
  const s = p.spinner();
1406
1376
  s.start("Setting up project");
1407
1377
 
1408
- const filesCopied = await copyFilteredFiles(sourceDir, directory, patterns, {
1378
+ const filesCopied = await copyFilteredFiles(sourceDir, targetDir, patterns, {
1409
1379
  withHost,
1410
1380
  plugins,
1411
1381
  pluginRoutes,
1412
1382
  });
1413
1383
 
1414
- await personalizeConfig(directory, {
1384
+ await personalizeConfig(targetDir, {
1415
1385
  extendsAccount,
1416
1386
  extendsGateway,
1417
1387
  account: account || extendsAccount,
@@ -1422,27 +1392,51 @@ export default createPlugin({
1422
1392
  withHost,
1423
1393
  });
1424
1394
 
1425
- await writeInitSnapshot(directory, extendsAccount, extendsGateway, sourceDir, patterns, {
1395
+ await writeInitSnapshot(targetDir, extendsAccount, extendsGateway, sourceDir, patterns, {
1426
1396
  withHost,
1427
1397
  plugins,
1428
1398
  pluginRoutes,
1429
1399
  });
1430
1400
 
1431
- ensureEnvFile(directory, { domain });
1401
+ const initConfig = await loadConfig({ cwd: targetDir });
1402
+ if (initConfig?.runtime) {
1403
+ writeGeneratedInfra(targetDir, initConfig.runtime);
1404
+ }
1405
+ ensureEnvFile(targetDir);
1432
1406
 
1433
1407
  if (!input.noInstall) {
1434
- await runBunInstall(directory);
1435
- await runTypesGen(directory);
1436
- await generateDatabaseMigrations(directory);
1408
+ await runBunInstall(targetDir);
1409
+ await runTypesGen(targetDir);
1410
+ await generateDatabaseMigrations(targetDir);
1437
1411
  }
1438
1412
 
1439
- const initConfig = await loadConfig({ cwd: directory });
1440
1413
  if (initConfig?.config) {
1441
- await generateCodeArtifacts(directory, initConfig.config);
1414
+ await generateCodeArtifacts(targetDir, initConfig.config);
1442
1415
  }
1443
1416
 
1444
1417
  s.stop("Project initialized");
1445
1418
 
1419
+ if (!input.noInteractive) {
1420
+ const shouldStartDocker = await p.confirm({
1421
+ message: "Run docker compose up -d --wait?",
1422
+ initialValue: true,
1423
+ });
1424
+
1425
+ if (shouldStartDocker === true) {
1426
+ const dockerSpinner = p.spinner();
1427
+ dockerSpinner.start("Starting Docker services");
1428
+ try {
1429
+ await runDockerComposeUp(targetDir);
1430
+ dockerSpinner.stop("Docker services ready");
1431
+ } catch (error) {
1432
+ dockerSpinner.stop("Docker services not started");
1433
+ p.log.warn(
1434
+ `docker compose up -d --wait failed: ${error instanceof Error ? error.message : error}`,
1435
+ );
1436
+ }
1437
+ }
1438
+ }
1439
+
1446
1440
  return {
1447
1441
  status: "initialized" as const,
1448
1442
  directory,