@doubledigit/cli 0.1.0 → 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/LICENSE +1 -1
- package/dist/codegen.d.ts +6 -2
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +81 -13
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +36 -3
- package/dist/commands/create.js +2 -2
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +24 -11
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +46 -12
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +163 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +6 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/lib/marketplace-schema.d.ts +24 -24
- package/dist/lib/onboarding.d.ts +3 -3
- package/dist/lib/onboarding.d.ts.map +1 -1
- package/dist/lib/onboarding.js +282 -237
- package/dist/lib/package-entry.d.ts +6 -0
- package/dist/lib/package-entry.d.ts.map +1 -0
- package/dist/lib/package-entry.js +63 -0
- package/dist/lib/scoped-migrations.d.ts +11 -0
- package/dist/lib/scoped-migrations.d.ts.map +1 -0
- package/dist/lib/scoped-migrations.js +156 -0
- package/dist/lib/validators.d.ts.map +1 -1
- package/dist/lib/validators.js +12 -49
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/scanner.d.ts +4 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +21 -0
- package/package.json +10 -4
package/dist/lib/onboarding.js
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import net from
|
|
3
|
-
import os from
|
|
4
|
-
import path from
|
|
5
|
-
import { randomBytes } from
|
|
6
|
-
import { execFileSync, spawn } from
|
|
7
|
-
import { createRequire } from
|
|
8
|
-
import { createInterface } from
|
|
9
|
-
import { pathToFileURL } from
|
|
10
|
-
export const DEFAULT_DATABASE_URL =
|
|
11
|
-
export const DEFAULT_APP_URL = 'http://localhost:3000';
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
export const DEFAULT_DATABASE_URL = "postgresql://doubledigit:doubledigit@localhost:5432/doubledigit";
|
|
12
11
|
export const DEFAULT_EMBEDDED_POSTGRES_PORT = 54329;
|
|
13
|
-
export const DEFAULT_EMBEDDED_POSTGRES_INSTANCE_ID =
|
|
14
|
-
export const DEFAULT_EMBEDDED_POSTGRES_HOME = path.join(os.homedir(),
|
|
15
|
-
export const DEFAULT_EMBEDDED_POSTGRES_DATA_DIR = path.join(DEFAULT_EMBEDDED_POSTGRES_HOME,
|
|
12
|
+
export const DEFAULT_EMBEDDED_POSTGRES_INSTANCE_ID = "default";
|
|
13
|
+
export const DEFAULT_EMBEDDED_POSTGRES_HOME = path.join(os.homedir(), ".doubledigit");
|
|
14
|
+
export const DEFAULT_EMBEDDED_POSTGRES_DATA_DIR = path.join(DEFAULT_EMBEDDED_POSTGRES_HOME, "instances", DEFAULT_EMBEDDED_POSTGRES_INSTANCE_ID, "db");
|
|
16
15
|
const DEFAULT_DOCKER_POSTGRES_PORT = 5432;
|
|
17
16
|
const EMBEDDED_POSTGRES_LOOKAHEAD = 20;
|
|
18
|
-
const EMBEDDED_POSTGRES_USER =
|
|
19
|
-
const EMBEDDED_POSTGRES_PASSWORD =
|
|
20
|
-
const EMBEDDED_POSTGRES_DATABASE =
|
|
17
|
+
const EMBEDDED_POSTGRES_USER = "doubledigit";
|
|
18
|
+
const EMBEDDED_POSTGRES_PASSWORD = "doubledigit";
|
|
19
|
+
const EMBEDDED_POSTGRES_DATABASE = "doubledigit";
|
|
21
20
|
const EMBEDDED_LOG_BUFFER_LIMIT = 40;
|
|
22
|
-
const DEFAULT_APP_PORT =
|
|
21
|
+
const DEFAULT_APP_PORT = 3111;
|
|
23
22
|
const APP_PORT_LOOKAHEAD = 20;
|
|
24
23
|
const INITIAL_DATABASE_URL = process.env.DATABASE_URL?.trim();
|
|
25
24
|
const INITIAL_DATABASE_MODE = process.env.DD_DATABASE_MODE?.trim();
|
|
25
|
+
const buildAppUrl = (port) => `http://localhost:${port}`;
|
|
26
|
+
export const DEFAULT_APP_URL = buildAppUrl(DEFAULT_APP_PORT);
|
|
26
27
|
/**
|
|
27
28
|
* Snapshot of shell-originated environment at module load time.
|
|
28
29
|
* Values present here must never be overwritten by .env file loading —
|
|
@@ -35,24 +36,24 @@ for (const key of Object.keys(process.env)) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
const REQUIRED_ENV_KEYS = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
"DATABASE_URL",
|
|
40
|
+
"PAYLOAD_SECRET",
|
|
41
|
+
"BETTER_AUTH_SECRET",
|
|
42
|
+
"BETTER_AUTH_URL",
|
|
42
43
|
];
|
|
43
44
|
export function getEnvPath(paths) {
|
|
44
|
-
return path.join(paths.mainAppDir,
|
|
45
|
+
return path.join(paths.mainAppDir, ".env");
|
|
45
46
|
}
|
|
46
47
|
export function getEnvExamplePath(paths) {
|
|
47
|
-
return path.join(paths.root,
|
|
48
|
+
return path.join(paths.root, ".env.example");
|
|
48
49
|
}
|
|
49
50
|
export function getGeneratedTypesPath(paths) {
|
|
50
|
-
return path.join(paths.root,
|
|
51
|
+
return path.join(paths.root, "packages", "shared", "src", "types", "payload-types.ts");
|
|
51
52
|
}
|
|
52
53
|
export function commandExists(command) {
|
|
53
|
-
const lookup = process.platform ===
|
|
54
|
+
const lookup = process.platform === "win32" ? "where" : "which";
|
|
54
55
|
try {
|
|
55
|
-
execFileSync(lookup, [command], { stdio:
|
|
56
|
+
execFileSync(lookup, [command], { stdio: "pipe" });
|
|
56
57
|
return true;
|
|
57
58
|
}
|
|
58
59
|
catch {
|
|
@@ -60,11 +61,11 @@ export function commandExists(command) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
export function dockerAvailable() {
|
|
63
|
-
if (!commandExists(
|
|
64
|
+
if (!commandExists("docker")) {
|
|
64
65
|
return false;
|
|
65
66
|
}
|
|
66
67
|
try {
|
|
67
|
-
execFileSync(
|
|
68
|
+
execFileSync("docker", ["info"], { stdio: "pipe" });
|
|
68
69
|
return true;
|
|
69
70
|
}
|
|
70
71
|
catch {
|
|
@@ -75,7 +76,7 @@ export function runChecked(command, args, cwd, label, envOverrides = {}) {
|
|
|
75
76
|
try {
|
|
76
77
|
execFileSync(command, args, {
|
|
77
78
|
cwd,
|
|
78
|
-
stdio:
|
|
79
|
+
stdio: "inherit",
|
|
79
80
|
env: {
|
|
80
81
|
...process.env,
|
|
81
82
|
...envOverrides,
|
|
@@ -90,8 +91,8 @@ export function captureCommand(command, args, cwd, envOverrides = {}) {
|
|
|
90
91
|
try {
|
|
91
92
|
const output = execFileSync(command, args, {
|
|
92
93
|
cwd,
|
|
93
|
-
stdio:
|
|
94
|
-
encoding:
|
|
94
|
+
stdio: "pipe",
|
|
95
|
+
encoding: "utf8",
|
|
95
96
|
env: {
|
|
96
97
|
...process.env,
|
|
97
98
|
...envOverrides,
|
|
@@ -100,15 +101,15 @@ export function captureCommand(command, args, cwd, envOverrides = {}) {
|
|
|
100
101
|
return { ok: true, output };
|
|
101
102
|
}
|
|
102
103
|
catch (error) {
|
|
103
|
-
const stdout = typeof error.stdout ===
|
|
104
|
+
const stdout = typeof error.stdout === "string"
|
|
104
105
|
? error.stdout
|
|
105
|
-
:
|
|
106
|
-
const stderr = typeof error.stderr ===
|
|
106
|
+
: "";
|
|
107
|
+
const stderr = typeof error.stderr === "string"
|
|
107
108
|
? error.stderr
|
|
108
|
-
:
|
|
109
|
+
: "";
|
|
109
110
|
return {
|
|
110
111
|
ok: false,
|
|
111
|
-
output: [stdout, stderr].filter(Boolean).join(
|
|
112
|
+
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -117,13 +118,13 @@ export function readEnvFile(filePath) {
|
|
|
117
118
|
return {};
|
|
118
119
|
}
|
|
119
120
|
const result = {};
|
|
120
|
-
const content = fs.readFileSync(filePath,
|
|
121
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
121
122
|
for (const line of content.split(/\r?\n/)) {
|
|
122
123
|
const trimmed = line.trim();
|
|
123
|
-
if (!trimmed || trimmed.startsWith(
|
|
124
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
124
125
|
continue;
|
|
125
126
|
}
|
|
126
|
-
const separator = trimmed.indexOf(
|
|
127
|
+
const separator = trimmed.indexOf("=");
|
|
127
128
|
if (separator === -1) {
|
|
128
129
|
continue;
|
|
129
130
|
}
|
|
@@ -134,10 +135,10 @@ export function readEnvFile(filePath) {
|
|
|
134
135
|
return result;
|
|
135
136
|
}
|
|
136
137
|
function escapeRegExp(value) {
|
|
137
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g,
|
|
138
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
138
139
|
}
|
|
139
140
|
function upsertEnvValue(content, key, value) {
|
|
140
|
-
const pattern = new RegExp(`^${escapeRegExp(key)}=.*$`,
|
|
141
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
|
|
141
142
|
if (pattern.test(content)) {
|
|
142
143
|
return content.replace(pattern, `${key}=${value}`);
|
|
143
144
|
}
|
|
@@ -145,11 +146,11 @@ function upsertEnvValue(content, key, value) {
|
|
|
145
146
|
return `${trimmed}\n${key}=${value}\n`;
|
|
146
147
|
}
|
|
147
148
|
function removeEnvValue(content, key) {
|
|
148
|
-
const pattern = new RegExp(`^${escapeRegExp(key)}=.*(?:\\r?\\n)?`,
|
|
149
|
-
return content.replace(pattern,
|
|
149
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}=.*(?:\\r?\\n)?`, "m");
|
|
150
|
+
return content.replace(pattern, "");
|
|
150
151
|
}
|
|
151
152
|
function generateSecret() {
|
|
152
|
-
return randomBytes(32).toString(
|
|
153
|
+
return randomBytes(32).toString("base64");
|
|
153
154
|
}
|
|
154
155
|
function buildManagedDatabaseUrl(port, database = EMBEDDED_POSTGRES_DATABASE) {
|
|
155
156
|
return `postgresql://${EMBEDDED_POSTGRES_USER}:${EMBEDDED_POSTGRES_PASSWORD}@127.0.0.1:${port}/${database}`;
|
|
@@ -165,22 +166,25 @@ function parsePositiveInt(value) {
|
|
|
165
166
|
return parsed;
|
|
166
167
|
}
|
|
167
168
|
function expandHomePrefix(value) {
|
|
168
|
-
if (value ===
|
|
169
|
+
if (value === "~") {
|
|
169
170
|
return os.homedir();
|
|
170
171
|
}
|
|
171
|
-
if (value.startsWith(
|
|
172
|
+
if (value.startsWith("~/")) {
|
|
172
173
|
return path.resolve(os.homedir(), value.slice(2));
|
|
173
174
|
}
|
|
174
175
|
return value;
|
|
175
176
|
}
|
|
176
177
|
function resolveEmbeddedDataDir(env) {
|
|
177
|
-
return path.resolve(expandHomePrefix(env.DD_EMBEDDED_POSTGRES_DATA_DIR?.trim() ||
|
|
178
|
+
return path.resolve(expandHomePrefix(env.DD_EMBEDDED_POSTGRES_DATA_DIR?.trim() ||
|
|
179
|
+
DEFAULT_EMBEDDED_POSTGRES_DATA_DIR));
|
|
178
180
|
}
|
|
179
181
|
function resolveEmbeddedPort(env) {
|
|
180
|
-
return parsePositiveInt(env.DD_EMBEDDED_POSTGRES_PORT) ||
|
|
182
|
+
return (parsePositiveInt(env.DD_EMBEDDED_POSTGRES_PORT) ||
|
|
183
|
+
DEFAULT_EMBEDDED_POSTGRES_PORT);
|
|
181
184
|
}
|
|
182
185
|
function resolveDockerPort(env) {
|
|
183
|
-
return parsePositiveInt(env.DD_DOCKER_POSTGRES_PORT) ||
|
|
186
|
+
return (parsePositiveInt(env.DD_DOCKER_POSTGRES_PORT) ||
|
|
187
|
+
DEFAULT_DOCKER_POSTGRES_PORT);
|
|
184
188
|
}
|
|
185
189
|
function applyEnvToProcess(env) {
|
|
186
190
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -197,32 +201,32 @@ function isLegacyLocalDatabaseUrl(value) {
|
|
|
197
201
|
return value?.trim() === DEFAULT_DATABASE_URL;
|
|
198
202
|
}
|
|
199
203
|
function isLoopbackHost(host) {
|
|
200
|
-
return host ===
|
|
204
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
201
205
|
}
|
|
202
206
|
function hasEmbeddedRuntimeHints(env) {
|
|
203
|
-
return Boolean(env.DD_EMBEDDED_POSTGRES_PORT?.trim()
|
|
204
|
-
|
|
207
|
+
return Boolean(env.DD_EMBEDDED_POSTGRES_PORT?.trim() ||
|
|
208
|
+
env.DD_EMBEDDED_POSTGRES_DATA_DIR?.trim());
|
|
205
209
|
}
|
|
206
210
|
function hasDockerRuntimeHints(env) {
|
|
207
|
-
return Boolean(env.DD_DATABASE_MODE?.trim() ===
|
|
208
|
-
|
|
211
|
+
return Boolean(env.DD_DATABASE_MODE?.trim() === "docker" ||
|
|
212
|
+
env.DD_DOCKER_POSTGRES_PORT?.trim());
|
|
209
213
|
}
|
|
210
214
|
function canonicalManagedDatabaseUrl(databaseUrl) {
|
|
211
215
|
try {
|
|
212
216
|
const url = new URL(databaseUrl);
|
|
213
|
-
if (url.protocol !==
|
|
217
|
+
if (url.protocol !== "postgresql:" && url.protocol !== "postgres:") {
|
|
214
218
|
return undefined;
|
|
215
219
|
}
|
|
216
220
|
if (!isLoopbackHost(url.hostname) || url.search || url.hash) {
|
|
217
221
|
return undefined;
|
|
218
222
|
}
|
|
219
223
|
const port = parsePositiveInt(url.port) || DEFAULT_DOCKER_POSTGRES_PORT;
|
|
220
|
-
const database = url.pathname.replace(/^\/+/,
|
|
224
|
+
const database = url.pathname.replace(/^\/+/, "");
|
|
221
225
|
const username = decodeURIComponent(url.username);
|
|
222
226
|
const password = decodeURIComponent(url.password);
|
|
223
|
-
if (username !== EMBEDDED_POSTGRES_USER
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
if (username !== EMBEDDED_POSTGRES_USER ||
|
|
228
|
+
password !== EMBEDDED_POSTGRES_PASSWORD ||
|
|
229
|
+
database !== EMBEDDED_POSTGRES_DATABASE) {
|
|
226
230
|
return undefined;
|
|
227
231
|
}
|
|
228
232
|
return buildManagedDatabaseUrl(port, database);
|
|
@@ -235,26 +239,28 @@ function isEmbeddedManagedDatabaseUrl(databaseUrl, env) {
|
|
|
235
239
|
if (!databaseUrl || !hasEmbeddedRuntimeHints(env)) {
|
|
236
240
|
return false;
|
|
237
241
|
}
|
|
238
|
-
return canonicalManagedDatabaseUrl(databaseUrl) ===
|
|
242
|
+
return (canonicalManagedDatabaseUrl(databaseUrl) ===
|
|
243
|
+
buildManagedDatabaseUrl(resolveEmbeddedPort(env)));
|
|
239
244
|
}
|
|
240
245
|
function isDockerManagedDatabaseUrl(databaseUrl, env) {
|
|
241
246
|
if (!databaseUrl || !hasDockerRuntimeHints(env)) {
|
|
242
247
|
return false;
|
|
243
248
|
}
|
|
244
|
-
return canonicalManagedDatabaseUrl(databaseUrl) ===
|
|
249
|
+
return (canonicalManagedDatabaseUrl(databaseUrl) ===
|
|
250
|
+
buildManagedDatabaseUrl(resolveDockerPort(env)));
|
|
245
251
|
}
|
|
246
252
|
function hasExplicitExternalDatabaseUrl(databaseUrl, env) {
|
|
247
253
|
const trimmed = databaseUrl?.trim();
|
|
248
|
-
return Boolean(trimmed
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
return Boolean(trimmed &&
|
|
255
|
+
!isLegacyLocalDatabaseUrl(trimmed) &&
|
|
256
|
+
!isEmbeddedManagedDatabaseUrl(trimmed, env) &&
|
|
257
|
+
!isDockerManagedDatabaseUrl(trimmed, env));
|
|
252
258
|
}
|
|
253
259
|
export function isPlaceholderValue(key, value) {
|
|
254
260
|
if (!value) {
|
|
255
261
|
return true;
|
|
256
262
|
}
|
|
257
|
-
if (value.includes(
|
|
263
|
+
if (value.includes("change-me")) {
|
|
258
264
|
return true;
|
|
259
265
|
}
|
|
260
266
|
return false;
|
|
@@ -271,52 +277,56 @@ export function ensureLocalEnv(paths, options = {}) {
|
|
|
271
277
|
fs.copyFileSync(envExamplePath, envPath);
|
|
272
278
|
created = true;
|
|
273
279
|
}
|
|
274
|
-
let content = fs.readFileSync(envPath,
|
|
280
|
+
let content = fs.readFileSync(envPath, "utf-8");
|
|
275
281
|
const current = readEnvFile(envPath);
|
|
276
282
|
const updates = {};
|
|
277
283
|
const removals = new Set();
|
|
278
284
|
const currentDatabaseMode = current.DD_DATABASE_MODE?.trim();
|
|
279
285
|
const currentDatabaseUrl = current.DATABASE_URL?.trim();
|
|
280
|
-
if (!options.databaseMode && currentDatabaseMode ===
|
|
281
|
-
removals.add(
|
|
282
|
-
}
|
|
283
|
-
if (!options.databaseUrl
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
removals.add(
|
|
287
|
-
}
|
|
288
|
-
if (!options.databaseMode
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
updates.DD_DATABASE_MODE =
|
|
292
|
-
}
|
|
293
|
-
if (!options.databaseMode
|
|
294
|
-
&&
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
removals.add(
|
|
298
|
-
}
|
|
299
|
-
const normalizedCurrentDatabaseMode = removals.has(
|
|
286
|
+
if (!options.databaseMode && currentDatabaseMode === "external") {
|
|
287
|
+
removals.add("DD_DATABASE_MODE");
|
|
288
|
+
}
|
|
289
|
+
if (!options.databaseUrl &&
|
|
290
|
+
(isEmbeddedManagedDatabaseUrl(currentDatabaseUrl, current) ||
|
|
291
|
+
isDockerManagedDatabaseUrl(currentDatabaseUrl, current))) {
|
|
292
|
+
removals.add("DATABASE_URL");
|
|
293
|
+
}
|
|
294
|
+
if (!options.databaseMode &&
|
|
295
|
+
isEmbeddedManagedDatabaseUrl(current.DATABASE_URL, current) &&
|
|
296
|
+
currentDatabaseMode !== "embedded") {
|
|
297
|
+
updates.DD_DATABASE_MODE = "embedded";
|
|
298
|
+
}
|
|
299
|
+
if (!options.databaseMode &&
|
|
300
|
+
currentDatabaseMode &&
|
|
301
|
+
currentDatabaseMode !== "external" &&
|
|
302
|
+
hasExplicitExternalDatabaseUrl(current.DATABASE_URL, current)) {
|
|
303
|
+
removals.add("DD_DATABASE_MODE");
|
|
304
|
+
}
|
|
305
|
+
const normalizedCurrentDatabaseMode = removals.has("DD_DATABASE_MODE")
|
|
300
306
|
? undefined
|
|
301
307
|
: currentDatabaseMode;
|
|
302
|
-
const effectiveDatabaseMode = options.databaseMode ||
|
|
303
|
-
|
|
308
|
+
const effectiveDatabaseMode = options.databaseMode ||
|
|
309
|
+
updates.DD_DATABASE_MODE ||
|
|
310
|
+
normalizedCurrentDatabaseMode;
|
|
311
|
+
if (isPlaceholderValue("PAYLOAD_SECRET", current.PAYLOAD_SECRET)) {
|
|
304
312
|
updates.PAYLOAD_SECRET = generateSecret();
|
|
305
313
|
}
|
|
306
|
-
if (isPlaceholderValue(
|
|
314
|
+
if (isPlaceholderValue("BETTER_AUTH_SECRET", current.BETTER_AUTH_SECRET)) {
|
|
307
315
|
updates.BETTER_AUTH_SECRET = generateSecret();
|
|
308
316
|
}
|
|
309
|
-
if (isPlaceholderValue(
|
|
317
|
+
if (isPlaceholderValue("BETTER_AUTH_URL", current.BETTER_AUTH_URL)) {
|
|
310
318
|
updates.BETTER_AUTH_URL = DEFAULT_APP_URL;
|
|
311
319
|
}
|
|
312
|
-
if (!effectiveDatabaseMode &&
|
|
313
|
-
|
|
320
|
+
if (!effectiveDatabaseMode &&
|
|
321
|
+
(!current.DATABASE_URL?.trim() ||
|
|
322
|
+
isLegacyLocalDatabaseUrl(current.DATABASE_URL))) {
|
|
323
|
+
updates.DD_DATABASE_MODE = "embedded";
|
|
314
324
|
}
|
|
315
325
|
if (options.databaseMode) {
|
|
316
326
|
updates.DD_DATABASE_MODE = options.databaseMode;
|
|
317
327
|
}
|
|
318
328
|
const resolvedDatabaseMode = options.databaseMode || updates.DD_DATABASE_MODE || currentDatabaseMode;
|
|
319
|
-
if (resolvedDatabaseMode ===
|
|
329
|
+
if (resolvedDatabaseMode === "embedded") {
|
|
320
330
|
if (options.embeddedPort) {
|
|
321
331
|
updates.DD_EMBEDDED_POSTGRES_PORT = String(options.embeddedPort);
|
|
322
332
|
}
|
|
@@ -327,10 +337,11 @@ export function ensureLocalEnv(paths, options = {}) {
|
|
|
327
337
|
updates.DD_EMBEDDED_POSTGRES_DATA_DIR = options.embeddedDataDir;
|
|
328
338
|
}
|
|
329
339
|
else if (!current.DD_EMBEDDED_POSTGRES_DATA_DIR?.trim()) {
|
|
330
|
-
updates.DD_EMBEDDED_POSTGRES_DATA_DIR =
|
|
340
|
+
updates.DD_EMBEDDED_POSTGRES_DATA_DIR =
|
|
341
|
+
DEFAULT_EMBEDDED_POSTGRES_DATA_DIR;
|
|
331
342
|
}
|
|
332
343
|
}
|
|
333
|
-
if (resolvedDatabaseMode ===
|
|
344
|
+
if (resolvedDatabaseMode === "docker") {
|
|
334
345
|
if (options.dockerPort) {
|
|
335
346
|
updates.DD_DOCKER_POSTGRES_PORT = String(options.dockerPort);
|
|
336
347
|
}
|
|
@@ -347,8 +358,8 @@ export function ensureLocalEnv(paths, options = {}) {
|
|
|
347
358
|
for (const [key, value] of Object.entries(updates)) {
|
|
348
359
|
content = upsertEnvValue(content, key, value);
|
|
349
360
|
}
|
|
350
|
-
if (Object.keys(updates).length > 0) {
|
|
351
|
-
fs.writeFileSync(envPath, content,
|
|
361
|
+
if (Object.keys(updates).length > 0 || removals.size > 0) {
|
|
362
|
+
fs.writeFileSync(envPath, content, "utf-8");
|
|
352
363
|
}
|
|
353
364
|
const env = readEnvFile(envPath);
|
|
354
365
|
applyEnvToProcess(env);
|
|
@@ -368,12 +379,12 @@ export function inspectLocalEnv(paths) {
|
|
|
368
379
|
};
|
|
369
380
|
}
|
|
370
381
|
export function parseDatabaseUrl(databaseUrl) {
|
|
371
|
-
const normalized = databaseUrl.replace(/^postgres:\/\//,
|
|
382
|
+
const normalized = databaseUrl.replace(/^postgres:\/\//, "postgresql://");
|
|
372
383
|
const parsed = new URL(normalized);
|
|
373
384
|
return {
|
|
374
|
-
host: parsed.hostname ||
|
|
385
|
+
host: parsed.hostname || "localhost",
|
|
375
386
|
port: parsed.port ? Number(parsed.port) : 5432,
|
|
376
|
-
database: parsed.pathname.replace(/^\//,
|
|
387
|
+
database: parsed.pathname.replace(/^\//, ""),
|
|
377
388
|
username: parsed.username || undefined,
|
|
378
389
|
};
|
|
379
390
|
}
|
|
@@ -391,15 +402,15 @@ export async function checkDatabaseReachability(databaseUrl) {
|
|
|
391
402
|
resolve(true);
|
|
392
403
|
});
|
|
393
404
|
socket.setTimeout(1500);
|
|
394
|
-
socket.on(
|
|
395
|
-
socket.on(
|
|
405
|
+
socket.on("error", () => resolve(false));
|
|
406
|
+
socket.on("timeout", () => {
|
|
396
407
|
socket.destroy();
|
|
397
408
|
resolve(false);
|
|
398
409
|
});
|
|
399
410
|
});
|
|
400
411
|
}
|
|
401
412
|
export async function confirmYesNo(message) {
|
|
402
|
-
if (!process.stdin.isTTY || process.env.CI ===
|
|
413
|
+
if (!process.stdin.isTTY || process.env.CI === "true") {
|
|
403
414
|
return true;
|
|
404
415
|
}
|
|
405
416
|
const rl = createInterface({
|
|
@@ -409,7 +420,7 @@ export async function confirmYesNo(message) {
|
|
|
409
420
|
try {
|
|
410
421
|
const answer = await rl.question(`${message} [Y/n] `);
|
|
411
422
|
const normalized = answer.trim().toLowerCase();
|
|
412
|
-
return normalized ===
|
|
423
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
413
424
|
}
|
|
414
425
|
finally {
|
|
415
426
|
rl.close();
|
|
@@ -417,7 +428,17 @@ export async function confirmYesNo(message) {
|
|
|
417
428
|
}
|
|
418
429
|
export async function waitForDockerDatabase(paths, envOverrides = {}) {
|
|
419
430
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
420
|
-
const result = captureCommand(
|
|
431
|
+
const result = captureCommand("docker", [
|
|
432
|
+
"compose",
|
|
433
|
+
"exec",
|
|
434
|
+
"db",
|
|
435
|
+
"pg_isready",
|
|
436
|
+
"-U",
|
|
437
|
+
"doubledigit",
|
|
438
|
+
"-d",
|
|
439
|
+
"doubledigit",
|
|
440
|
+
"-q",
|
|
441
|
+
], paths.root, envOverrides);
|
|
421
442
|
if (result.ok) {
|
|
422
443
|
return true;
|
|
423
444
|
}
|
|
@@ -426,7 +447,8 @@ export async function waitForDockerDatabase(paths, envOverrides = {}) {
|
|
|
426
447
|
return false;
|
|
427
448
|
}
|
|
428
449
|
export async function startDevServer(paths, envOverrides = {}) {
|
|
429
|
-
const preferredPort = parsePositiveInt(process.env.APP_PORT || envOverrides.APP_PORT) ||
|
|
450
|
+
const preferredPort = parsePositiveInt(process.env.APP_PORT || envOverrides.APP_PORT) ||
|
|
451
|
+
DEFAULT_APP_PORT;
|
|
430
452
|
let port;
|
|
431
453
|
try {
|
|
432
454
|
port = await findAvailablePort(preferredPort, APP_PORT_LOOKAHEAD);
|
|
@@ -437,44 +459,54 @@ export async function startDevServer(paths, envOverrides = {}) {
|
|
|
437
459
|
if (port !== preferredPort) {
|
|
438
460
|
console.log(`⚠ Port ${preferredPort} is busy — using port ${port} instead.`);
|
|
439
461
|
}
|
|
440
|
-
const betterAuthUrl =
|
|
441
|
-
const child = spawn(
|
|
462
|
+
const betterAuthUrl = buildAppUrl(port);
|
|
463
|
+
const child = spawn("pnpm", [
|
|
464
|
+
"--dir",
|
|
465
|
+
"apps/main-app",
|
|
466
|
+
"exec",
|
|
467
|
+
"next",
|
|
468
|
+
"dev",
|
|
469
|
+
"--turbo",
|
|
470
|
+
"--port",
|
|
471
|
+
String(port),
|
|
472
|
+
], {
|
|
442
473
|
cwd: paths.root,
|
|
443
|
-
stdio:
|
|
474
|
+
stdio: "inherit",
|
|
444
475
|
env: {
|
|
445
476
|
...process.env,
|
|
446
477
|
...envOverrides,
|
|
447
478
|
PORT: String(port),
|
|
479
|
+
APP_PORT: String(port),
|
|
448
480
|
BETTER_AUTH_URL: betterAuthUrl,
|
|
449
481
|
},
|
|
450
|
-
shell: process.platform ===
|
|
482
|
+
shell: process.platform === "win32",
|
|
451
483
|
});
|
|
452
|
-
child.on(
|
|
484
|
+
child.on("exit", (code) => {
|
|
453
485
|
process.exit(code ?? 0);
|
|
454
486
|
});
|
|
455
|
-
process.on(
|
|
456
|
-
child.kill(
|
|
487
|
+
process.on("SIGINT", () => {
|
|
488
|
+
child.kill("SIGINT");
|
|
457
489
|
});
|
|
458
|
-
process.on(
|
|
459
|
-
child.kill(
|
|
490
|
+
process.on("SIGTERM", () => {
|
|
491
|
+
child.kill("SIGTERM");
|
|
460
492
|
});
|
|
461
493
|
}
|
|
462
494
|
export function printNextSteps() {
|
|
463
|
-
console.log(
|
|
464
|
-
console.log(
|
|
465
|
-
console.log(
|
|
466
|
-
console.log(
|
|
467
|
-
console.log(
|
|
495
|
+
console.log("\n✅ Setup complete! Next steps:\n");
|
|
496
|
+
console.log(" pnpm dev # start the development server");
|
|
497
|
+
console.log(` open ${DEFAULT_APP_URL}`);
|
|
498
|
+
console.log(` open ${buildAppUrl(DEFAULT_APP_PORT)}/admin`);
|
|
499
|
+
console.log("");
|
|
468
500
|
}
|
|
469
501
|
function createEmbeddedPostgresLogBuffer(limit = EMBEDDED_LOG_BUFFER_LIMIT) {
|
|
470
502
|
const recentLogs = [];
|
|
471
503
|
return {
|
|
472
504
|
append(message) {
|
|
473
|
-
const text = typeof message ===
|
|
505
|
+
const text = typeof message === "string"
|
|
474
506
|
? message
|
|
475
507
|
: message instanceof Error
|
|
476
508
|
? message.message
|
|
477
|
-
: String(message ??
|
|
509
|
+
: String(message ?? "");
|
|
478
510
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
479
511
|
const line = rawLine.trim();
|
|
480
512
|
if (!line) {
|
|
@@ -499,29 +531,29 @@ function summarizeEmbeddedPostgresLogs(recentLogs) {
|
|
|
499
531
|
.slice(-8)
|
|
500
532
|
.map((line) => line.trim())
|
|
501
533
|
.filter(Boolean)
|
|
502
|
-
.join(
|
|
534
|
+
.join(" | ");
|
|
503
535
|
}
|
|
504
536
|
function formatEmbeddedPostgresError(error, fallbackMessage, recentLogs) {
|
|
505
537
|
const base = error instanceof Error
|
|
506
538
|
? error.message
|
|
507
539
|
: `${fallbackMessage}: ${String(error ?? fallbackMessage)}`;
|
|
508
540
|
const parts = [base];
|
|
509
|
-
const haystack = recentLogs.join(
|
|
510
|
-
if (haystack.includes(
|
|
511
|
-
parts.push(
|
|
541
|
+
const haystack = recentLogs.join("\n").toLowerCase();
|
|
542
|
+
if (haystack.includes("could not create shared memory segment")) {
|
|
543
|
+
parts.push("Embedded PostgreSQL could not allocate shared memory. Stop other local PostgreSQL servers or raise the host shared-memory limits, then retry.");
|
|
512
544
|
}
|
|
513
545
|
const recentSummary = summarizeEmbeddedPostgresLogs(recentLogs);
|
|
514
546
|
if (recentSummary) {
|
|
515
547
|
parts.push(`Recent embedded Postgres logs: ${recentSummary}`);
|
|
516
548
|
}
|
|
517
|
-
return new Error(parts.join(
|
|
549
|
+
return new Error(parts.join(" "));
|
|
518
550
|
}
|
|
519
551
|
export function resolveDatabasePreference(env) {
|
|
520
552
|
const shellDatabaseUrl = INITIAL_DATABASE_URL;
|
|
521
553
|
if (shellDatabaseUrl) {
|
|
522
554
|
return {
|
|
523
|
-
mode:
|
|
524
|
-
reason:
|
|
555
|
+
mode: "external",
|
|
556
|
+
reason: "DATABASE_URL",
|
|
525
557
|
databaseUrl: shellDatabaseUrl,
|
|
526
558
|
};
|
|
527
559
|
}
|
|
@@ -531,89 +563,94 @@ export function resolveDatabasePreference(env) {
|
|
|
531
563
|
const configuredExternalDatabaseUrl = hasExplicitExternalDatabaseUrl(configuredDatabaseUrl, env);
|
|
532
564
|
if (configuredExternalDatabaseUrl) {
|
|
533
565
|
return {
|
|
534
|
-
mode:
|
|
535
|
-
reason:
|
|
566
|
+
mode: "external",
|
|
567
|
+
reason: "apps/main-app/.env",
|
|
536
568
|
databaseUrl: configuredDatabaseUrl,
|
|
537
569
|
};
|
|
538
570
|
}
|
|
539
|
-
if (modeHint ===
|
|
571
|
+
if (modeHint === "external" &&
|
|
572
|
+
(configuredDatabaseUrl || INITIAL_DATABASE_MODE)) {
|
|
540
573
|
if (!INITIAL_DATABASE_MODE && embeddedManagedDatabaseUrl) {
|
|
541
574
|
return {
|
|
542
|
-
mode:
|
|
543
|
-
reason:
|
|
575
|
+
mode: "embedded",
|
|
576
|
+
reason: "embedded-managed-url",
|
|
544
577
|
databaseUrl: configuredDatabaseUrl,
|
|
545
578
|
};
|
|
546
579
|
}
|
|
547
580
|
return {
|
|
548
|
-
mode:
|
|
549
|
-
reason:
|
|
581
|
+
mode: "external",
|
|
582
|
+
reason: "DD_DATABASE_MODE",
|
|
550
583
|
databaseUrl: configuredDatabaseUrl,
|
|
551
584
|
};
|
|
552
585
|
}
|
|
553
|
-
if (modeHint ===
|
|
586
|
+
if (modeHint === "docker") {
|
|
554
587
|
return {
|
|
555
|
-
mode:
|
|
556
|
-
reason:
|
|
557
|
-
databaseUrl: configuredDatabaseUrl ||
|
|
588
|
+
mode: "docker",
|
|
589
|
+
reason: "DD_DATABASE_MODE",
|
|
590
|
+
databaseUrl: configuredDatabaseUrl ||
|
|
591
|
+
buildManagedDatabaseUrl(resolveDockerPort(env)),
|
|
558
592
|
};
|
|
559
593
|
}
|
|
560
|
-
if (modeHint ===
|
|
594
|
+
if (modeHint === "embedded") {
|
|
561
595
|
return {
|
|
562
|
-
mode:
|
|
563
|
-
reason:
|
|
564
|
-
databaseUrl: configuredDatabaseUrl ||
|
|
596
|
+
mode: "embedded",
|
|
597
|
+
reason: "DD_DATABASE_MODE",
|
|
598
|
+
databaseUrl: configuredDatabaseUrl ||
|
|
599
|
+
buildManagedDatabaseUrl(resolveEmbeddedPort(env)),
|
|
565
600
|
};
|
|
566
601
|
}
|
|
567
602
|
if (embeddedManagedDatabaseUrl) {
|
|
568
603
|
return {
|
|
569
|
-
mode:
|
|
570
|
-
reason:
|
|
604
|
+
mode: "embedded",
|
|
605
|
+
reason: "embedded-managed-url",
|
|
571
606
|
databaseUrl: configuredDatabaseUrl,
|
|
572
607
|
};
|
|
573
608
|
}
|
|
574
|
-
if (configuredDatabaseUrl &&
|
|
609
|
+
if (configuredDatabaseUrl &&
|
|
610
|
+
!isLegacyLocalDatabaseUrl(configuredDatabaseUrl)) {
|
|
575
611
|
return {
|
|
576
|
-
mode:
|
|
577
|
-
reason:
|
|
612
|
+
mode: "external",
|
|
613
|
+
reason: "apps/main-app/.env",
|
|
578
614
|
databaseUrl: configuredDatabaseUrl,
|
|
579
615
|
};
|
|
580
616
|
}
|
|
581
617
|
return {
|
|
582
|
-
mode:
|
|
583
|
-
reason: configuredDatabaseUrl ?
|
|
584
|
-
databaseUrl: configuredDatabaseUrl ||
|
|
618
|
+
mode: "embedded",
|
|
619
|
+
reason: configuredDatabaseUrl ? "legacy-default" : "no-database-url",
|
|
620
|
+
databaseUrl: configuredDatabaseUrl ||
|
|
621
|
+
buildManagedDatabaseUrl(resolveEmbeddedPort(env)),
|
|
585
622
|
};
|
|
586
623
|
}
|
|
587
624
|
function getEmbeddedPostgresPlatformPackageName() {
|
|
588
625
|
switch (process.platform) {
|
|
589
|
-
case
|
|
590
|
-
if (process.arch ===
|
|
591
|
-
return
|
|
626
|
+
case "darwin":
|
|
627
|
+
if (process.arch === "arm64") {
|
|
628
|
+
return "@embedded-postgres/darwin-arm64";
|
|
592
629
|
}
|
|
593
|
-
if (process.arch ===
|
|
594
|
-
return
|
|
630
|
+
if (process.arch === "x64") {
|
|
631
|
+
return "@embedded-postgres/darwin-x64";
|
|
595
632
|
}
|
|
596
633
|
break;
|
|
597
|
-
case
|
|
598
|
-
if (process.arch ===
|
|
599
|
-
return
|
|
634
|
+
case "linux":
|
|
635
|
+
if (process.arch === "arm") {
|
|
636
|
+
return "@embedded-postgres/linux-arm";
|
|
600
637
|
}
|
|
601
|
-
if (process.arch ===
|
|
602
|
-
return
|
|
638
|
+
if (process.arch === "arm64") {
|
|
639
|
+
return "@embedded-postgres/linux-arm64";
|
|
603
640
|
}
|
|
604
|
-
if (process.arch ===
|
|
605
|
-
return
|
|
641
|
+
if (process.arch === "ia32") {
|
|
642
|
+
return "@embedded-postgres/linux-ia32";
|
|
606
643
|
}
|
|
607
|
-
if (process.arch ===
|
|
608
|
-
return
|
|
644
|
+
if (process.arch === "ppc64") {
|
|
645
|
+
return "@embedded-postgres/linux-ppc64";
|
|
609
646
|
}
|
|
610
|
-
if (process.arch ===
|
|
611
|
-
return
|
|
647
|
+
if (process.arch === "x64") {
|
|
648
|
+
return "@embedded-postgres/linux-x64";
|
|
612
649
|
}
|
|
613
650
|
break;
|
|
614
|
-
case
|
|
615
|
-
if (process.arch ===
|
|
616
|
-
return
|
|
651
|
+
case "win32":
|
|
652
|
+
if (process.arch === "x64") {
|
|
653
|
+
return "@embedded-postgres/windows-x64";
|
|
617
654
|
}
|
|
618
655
|
break;
|
|
619
656
|
default:
|
|
@@ -623,15 +660,15 @@ function getEmbeddedPostgresPlatformPackageName() {
|
|
|
623
660
|
}
|
|
624
661
|
function hydrateEmbeddedPostgresSymlinks() {
|
|
625
662
|
const resolver = createRequire(import.meta.url);
|
|
626
|
-
const embeddedEntry = resolver.resolve(
|
|
663
|
+
const embeddedEntry = resolver.resolve("embedded-postgres");
|
|
627
664
|
const embeddedRequire = createRequire(embeddedEntry);
|
|
628
665
|
const packageEntryPath = embeddedRequire.resolve(getEmbeddedPostgresPlatformPackageName());
|
|
629
666
|
const packageRoot = path.dirname(path.dirname(packageEntryPath));
|
|
630
|
-
const symlinkFile = path.join(packageRoot,
|
|
667
|
+
const symlinkFile = path.join(packageRoot, "native", "pg-symlinks.json");
|
|
631
668
|
if (!fs.existsSync(symlinkFile)) {
|
|
632
669
|
return;
|
|
633
670
|
}
|
|
634
|
-
const symlinks = JSON.parse(fs.readFileSync(symlinkFile,
|
|
671
|
+
const symlinks = JSON.parse(fs.readFileSync(symlinkFile, "utf8"));
|
|
635
672
|
for (const { source, target } of symlinks) {
|
|
636
673
|
const sourcePath = path.resolve(packageRoot, source);
|
|
637
674
|
const targetPath = path.resolve(packageRoot, target);
|
|
@@ -646,11 +683,11 @@ function hydrateEmbeddedPostgresSymlinks() {
|
|
|
646
683
|
async function loadEmbeddedPostgresBinaries() {
|
|
647
684
|
hydrateEmbeddedPostgresSymlinks();
|
|
648
685
|
const resolver = createRequire(import.meta.url);
|
|
649
|
-
const embeddedEntry = resolver.resolve(
|
|
686
|
+
const embeddedEntry = resolver.resolve("embedded-postgres");
|
|
650
687
|
const embeddedRequire = createRequire(embeddedEntry);
|
|
651
688
|
const packageEntryPath = embeddedRequire.resolve(getEmbeddedPostgresPlatformPackageName());
|
|
652
689
|
const packageRoot = path.dirname(path.dirname(packageEntryPath));
|
|
653
|
-
const mod = await import(pathToFileURL(packageEntryPath).href);
|
|
690
|
+
const mod = (await import(pathToFileURL(packageEntryPath).href));
|
|
654
691
|
return {
|
|
655
692
|
packageRoot,
|
|
656
693
|
initdb: mod.initdb,
|
|
@@ -659,8 +696,8 @@ async function loadEmbeddedPostgresBinaries() {
|
|
|
659
696
|
};
|
|
660
697
|
}
|
|
661
698
|
async function createPgClient(connectionString) {
|
|
662
|
-
const pgMod = await import(
|
|
663
|
-
const pg =
|
|
699
|
+
const pgMod = await import("pg");
|
|
700
|
+
const pg = "default" in pgMod ? pgMod.default : pgMod;
|
|
664
701
|
return new pg.Client({ connectionString });
|
|
665
702
|
}
|
|
666
703
|
function ensureExecutable(filePath) {
|
|
@@ -671,8 +708,10 @@ function ensureExecutable(filePath) {
|
|
|
671
708
|
}
|
|
672
709
|
}
|
|
673
710
|
function createEmbeddedPasswordFile() {
|
|
674
|
-
const passwordFile = path.join(os.tmpdir(), `dd-embedded-postgres-${randomBytes(6).toString(
|
|
675
|
-
fs.writeFileSync(passwordFile, `${EMBEDDED_POSTGRES_PASSWORD}\n`, {
|
|
711
|
+
const passwordFile = path.join(os.tmpdir(), `dd-embedded-postgres-${randomBytes(6).toString("hex")}.txt`);
|
|
712
|
+
fs.writeFileSync(passwordFile, `${EMBEDDED_POSTGRES_PASSWORD}\n`, {
|
|
713
|
+
mode: 0o600,
|
|
714
|
+
});
|
|
676
715
|
return passwordFile;
|
|
677
716
|
}
|
|
678
717
|
function appendEmbeddedOutput(logBuffer, output) {
|
|
@@ -681,9 +720,9 @@ function appendEmbeddedOutput(logBuffer, output) {
|
|
|
681
720
|
}
|
|
682
721
|
}
|
|
683
722
|
function getEmbeddedPostgresLogPath(dataDir) {
|
|
684
|
-
const logsDir = path.join(path.dirname(dataDir),
|
|
723
|
+
const logsDir = path.join(path.dirname(dataDir), "logs");
|
|
685
724
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
686
|
-
return path.join(logsDir,
|
|
725
|
+
return path.join(logsDir, "postgres.log");
|
|
687
726
|
}
|
|
688
727
|
async function getPostgresDataDirectory(connectionString) {
|
|
689
728
|
const client = await createPgClient(connectionString);
|
|
@@ -691,7 +730,7 @@ async function getPostgresDataDirectory(connectionString) {
|
|
|
691
730
|
try {
|
|
692
731
|
const result = await client.query(`SELECT current_setting('data_directory') AS data_directory`);
|
|
693
732
|
const dataDirectory = result.rows[0]?.data_directory;
|
|
694
|
-
return typeof dataDirectory ===
|
|
733
|
+
return typeof dataDirectory === "string" ? dataDirectory : null;
|
|
695
734
|
}
|
|
696
735
|
finally {
|
|
697
736
|
await client.end().catch(() => undefined);
|
|
@@ -701,13 +740,13 @@ async function ensurePostgresDatabase(adminConnectionString, databaseName) {
|
|
|
701
740
|
const client = await createPgClient(adminConnectionString);
|
|
702
741
|
await client.connect();
|
|
703
742
|
try {
|
|
704
|
-
const lookup = await client.query(
|
|
743
|
+
const lookup = await client.query("SELECT 1 FROM pg_database WHERE datname = $1 LIMIT 1", [databaseName]);
|
|
705
744
|
if (lookup.rowCount > 0) {
|
|
706
|
-
return
|
|
745
|
+
return "existing";
|
|
707
746
|
}
|
|
708
747
|
const safeDatabaseName = databaseName.replace(/"/g, '""');
|
|
709
748
|
await client.query(`CREATE DATABASE "${safeDatabaseName}"`);
|
|
710
|
-
return
|
|
749
|
+
return "created";
|
|
711
750
|
}
|
|
712
751
|
finally {
|
|
713
752
|
await client.end().catch(() => undefined);
|
|
@@ -718,7 +757,7 @@ function readRunningPostmasterPid(postmasterPidFile) {
|
|
|
718
757
|
return null;
|
|
719
758
|
}
|
|
720
759
|
try {
|
|
721
|
-
const pid = Number(fs.readFileSync(postmasterPidFile,
|
|
760
|
+
const pid = Number(fs.readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim());
|
|
722
761
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
723
762
|
return null;
|
|
724
763
|
}
|
|
@@ -734,7 +773,7 @@ function readPostmasterPort(postmasterPidFile) {
|
|
|
734
773
|
return null;
|
|
735
774
|
}
|
|
736
775
|
try {
|
|
737
|
-
const port = Number(fs.readFileSync(postmasterPidFile,
|
|
776
|
+
const port = Number(fs.readFileSync(postmasterPidFile, "utf8").split("\n")[3]?.trim());
|
|
738
777
|
return Number.isInteger(port) && port > 0 ? port : null;
|
|
739
778
|
}
|
|
740
779
|
catch {
|
|
@@ -745,10 +784,10 @@ async function isPortInUse(port) {
|
|
|
745
784
|
return await new Promise((resolve) => {
|
|
746
785
|
const server = net.createServer();
|
|
747
786
|
server.unref();
|
|
748
|
-
server.once(
|
|
749
|
-
resolve(error.code ===
|
|
787
|
+
server.once("error", (error) => {
|
|
788
|
+
resolve(error.code === "EADDRINUSE");
|
|
750
789
|
});
|
|
751
|
-
server.listen(port,
|
|
790
|
+
server.listen(port, "127.0.0.1", () => {
|
|
752
791
|
server.close();
|
|
753
792
|
resolve(false);
|
|
754
793
|
});
|
|
@@ -767,20 +806,21 @@ async function ensureEmbeddedDatabaseRuntime(env) {
|
|
|
767
806
|
const binaries = await loadEmbeddedPostgresBinaries();
|
|
768
807
|
const dataDir = resolveEmbeddedDataDir(env);
|
|
769
808
|
const preferredPort = resolveEmbeddedPort(env);
|
|
770
|
-
const postmasterPidFile = path.join(dataDir,
|
|
771
|
-
const pgVersionFile = path.join(dataDir,
|
|
809
|
+
const postmasterPidFile = path.join(dataDir, "postmaster.pid");
|
|
810
|
+
const pgVersionFile = path.join(dataDir, "PG_VERSION");
|
|
772
811
|
const runningPid = readRunningPostmasterPid(postmasterPidFile);
|
|
773
812
|
const runningPort = readPostmasterPort(postmasterPidFile);
|
|
774
|
-
const adminUrl = buildManagedDatabaseUrl(preferredPort,
|
|
813
|
+
const adminUrl = buildManagedDatabaseUrl(preferredPort, "postgres");
|
|
775
814
|
const logBuffer = createEmbeddedPostgresLogBuffer();
|
|
776
815
|
fs.mkdirSync(path.dirname(dataDir), { recursive: true });
|
|
777
816
|
if (!runningPid && fs.existsSync(pgVersionFile)) {
|
|
778
817
|
try {
|
|
779
818
|
const actualDataDir = await getPostgresDataDirectory(adminUrl);
|
|
780
|
-
if (actualDataDir &&
|
|
819
|
+
if (actualDataDir &&
|
|
820
|
+
path.resolve(actualDataDir) === path.resolve(dataDir)) {
|
|
781
821
|
await ensurePostgresDatabase(adminUrl, EMBEDDED_POSTGRES_DATABASE);
|
|
782
822
|
return {
|
|
783
|
-
mode:
|
|
823
|
+
mode: "embedded",
|
|
784
824
|
databaseUrl: buildManagedDatabaseUrl(preferredPort),
|
|
785
825
|
source: `embedded-postgres@${preferredPort}`,
|
|
786
826
|
reachable: true,
|
|
@@ -795,10 +835,10 @@ async function ensureEmbeddedDatabaseRuntime(env) {
|
|
|
795
835
|
}
|
|
796
836
|
if (runningPid) {
|
|
797
837
|
const port = runningPort || preferredPort;
|
|
798
|
-
const runningAdminUrl = buildManagedDatabaseUrl(port,
|
|
838
|
+
const runningAdminUrl = buildManagedDatabaseUrl(port, "postgres");
|
|
799
839
|
await ensurePostgresDatabase(runningAdminUrl, EMBEDDED_POSTGRES_DATABASE);
|
|
800
840
|
return {
|
|
801
|
-
mode:
|
|
841
|
+
mode: "embedded",
|
|
802
842
|
databaseUrl: buildManagedDatabaseUrl(port),
|
|
803
843
|
source: `embedded-postgres@${port}`,
|
|
804
844
|
reachable: true,
|
|
@@ -814,18 +854,18 @@ async function ensureEmbeddedDatabaseRuntime(env) {
|
|
|
814
854
|
try {
|
|
815
855
|
const initResult = captureCommand(binaries.initdb, [
|
|
816
856
|
`--pgdata=${dataDir}`,
|
|
817
|
-
|
|
857
|
+
"--auth=password",
|
|
818
858
|
`--username=${EMBEDDED_POSTGRES_USER}`,
|
|
819
859
|
`--pwfile=${passwordFile}`,
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
860
|
+
"--lc-messages=C",
|
|
861
|
+
"--encoding=UTF8",
|
|
862
|
+
"--locale=C",
|
|
823
863
|
], binaries.packageRoot, {
|
|
824
|
-
LC_MESSAGES:
|
|
864
|
+
LC_MESSAGES: "C",
|
|
825
865
|
});
|
|
826
866
|
appendEmbeddedOutput(logBuffer, initResult.output);
|
|
827
867
|
if (!initResult.ok) {
|
|
828
|
-
throw new Error(initResult.output ||
|
|
868
|
+
throw new Error(initResult.output || "initdb failed");
|
|
829
869
|
}
|
|
830
870
|
}
|
|
831
871
|
catch (error) {
|
|
@@ -840,23 +880,23 @@ async function ensureEmbeddedDatabaseRuntime(env) {
|
|
|
840
880
|
}
|
|
841
881
|
const logPath = getEmbeddedPostgresLogPath(dataDir);
|
|
842
882
|
try {
|
|
843
|
-
const startResult = captureCommand(binaries.pgCtl, [
|
|
844
|
-
LC_MESSAGES:
|
|
883
|
+
const startResult = captureCommand(binaries.pgCtl, ["-D", dataDir, "-l", logPath, "-w", "start", "-o", `-p ${selectedPort}`], binaries.packageRoot, {
|
|
884
|
+
LC_MESSAGES: "C",
|
|
845
885
|
});
|
|
846
886
|
appendEmbeddedOutput(logBuffer, startResult.output);
|
|
847
887
|
if (fs.existsSync(logPath)) {
|
|
848
|
-
appendEmbeddedOutput(logBuffer, fs.readFileSync(logPath,
|
|
888
|
+
appendEmbeddedOutput(logBuffer, fs.readFileSync(logPath, "utf8"));
|
|
849
889
|
}
|
|
850
890
|
if (!startResult.ok) {
|
|
851
|
-
throw new Error(startResult.output ||
|
|
891
|
+
throw new Error(startResult.output || "pg_ctl start failed");
|
|
852
892
|
}
|
|
853
893
|
}
|
|
854
894
|
catch (error) {
|
|
855
895
|
throw formatEmbeddedPostgresError(error, `Failed to start embedded PostgreSQL on port ${selectedPort}`, logBuffer.getRecentLogs());
|
|
856
896
|
}
|
|
857
|
-
await ensurePostgresDatabase(buildManagedDatabaseUrl(selectedPort,
|
|
897
|
+
await ensurePostgresDatabase(buildManagedDatabaseUrl(selectedPort, "postgres"), EMBEDDED_POSTGRES_DATABASE);
|
|
858
898
|
return {
|
|
859
|
-
mode:
|
|
899
|
+
mode: "embedded",
|
|
860
900
|
databaseUrl: buildManagedDatabaseUrl(selectedPort),
|
|
861
901
|
source: `embedded-postgres@${selectedPort}`,
|
|
862
902
|
reachable: true,
|
|
@@ -870,15 +910,15 @@ async function ensureDockerDatabaseRuntime(paths, env) {
|
|
|
870
910
|
const envOverrides = {
|
|
871
911
|
DB_PORT: String(selectedPort),
|
|
872
912
|
};
|
|
873
|
-
runChecked(
|
|
913
|
+
runChecked("docker", ["compose", "up", "-d", "db"], paths.root, "Docker db startup", envOverrides);
|
|
874
914
|
const ready = await waitForDockerDatabase(paths, envOverrides);
|
|
875
915
|
if (!ready) {
|
|
876
|
-
throw new Error(
|
|
916
|
+
throw new Error("Docker PostgreSQL did not become ready in time.");
|
|
877
917
|
}
|
|
878
918
|
return {
|
|
879
|
-
mode:
|
|
919
|
+
mode: "docker",
|
|
880
920
|
databaseUrl: buildManagedDatabaseUrl(selectedPort),
|
|
881
|
-
source:
|
|
921
|
+
source: "docker-compose",
|
|
882
922
|
reachable: true,
|
|
883
923
|
dockerPort: selectedPort,
|
|
884
924
|
};
|
|
@@ -898,25 +938,26 @@ export async function probeEmbeddedPostgresSupport() {
|
|
|
898
938
|
export async function ensureDatabaseReady(paths, options = {}) {
|
|
899
939
|
const env = readEnvFile(getEnvPath(paths));
|
|
900
940
|
const preference = resolveDatabasePreference(env);
|
|
901
|
-
if (preference.mode ===
|
|
941
|
+
if (preference.mode === "external") {
|
|
902
942
|
if (!preference.databaseUrl) {
|
|
903
|
-
throw new Error(
|
|
943
|
+
throw new Error("DD_DATABASE_MODE=external requires DATABASE_URL to be set.");
|
|
904
944
|
}
|
|
905
945
|
const reachable = await checkDatabaseReachability(preference.databaseUrl);
|
|
906
946
|
if (!reachable) {
|
|
907
|
-
throw new Error(
|
|
947
|
+
throw new Error("Database is not reachable. Start PostgreSQL or update apps/main-app/.env first.");
|
|
908
948
|
}
|
|
909
949
|
return {
|
|
910
|
-
mode:
|
|
950
|
+
mode: "external",
|
|
911
951
|
databaseUrl: preference.databaseUrl,
|
|
912
952
|
source: preference.reason,
|
|
913
953
|
reachable,
|
|
914
954
|
};
|
|
915
955
|
}
|
|
916
|
-
if (preference.mode ===
|
|
917
|
-
if (preference.databaseUrl &&
|
|
956
|
+
if (preference.mode === "docker") {
|
|
957
|
+
if (preference.databaseUrl &&
|
|
958
|
+
(await checkDatabaseReachability(preference.databaseUrl))) {
|
|
918
959
|
return {
|
|
919
|
-
mode:
|
|
960
|
+
mode: "docker",
|
|
920
961
|
databaseUrl: preference.databaseUrl,
|
|
921
962
|
source: preference.reason,
|
|
922
963
|
reachable: true,
|
|
@@ -924,17 +965,18 @@ export async function ensureDatabaseReady(paths, options = {}) {
|
|
|
924
965
|
};
|
|
925
966
|
}
|
|
926
967
|
if (!dockerAvailable()) {
|
|
927
|
-
throw new Error(
|
|
968
|
+
throw new Error("Docker is not available, so the configured Docker database cannot be started.");
|
|
928
969
|
}
|
|
929
970
|
return ensureDockerDatabaseRuntime(paths, env);
|
|
930
971
|
}
|
|
931
|
-
if (preference.databaseUrl &&
|
|
972
|
+
if (preference.databaseUrl &&
|
|
973
|
+
isLegacyLocalDatabaseUrl(preference.databaseUrl)) {
|
|
932
974
|
const reachable = await checkDatabaseReachability(preference.databaseUrl);
|
|
933
975
|
if (reachable) {
|
|
934
976
|
return {
|
|
935
|
-
mode:
|
|
977
|
+
mode: "external",
|
|
936
978
|
databaseUrl: preference.databaseUrl,
|
|
937
|
-
source:
|
|
979
|
+
source: "legacy-localhost",
|
|
938
980
|
reachable: true,
|
|
939
981
|
};
|
|
940
982
|
}
|
|
@@ -946,7 +988,7 @@ export async function ensureDatabaseReady(paths, options = {}) {
|
|
|
946
988
|
if (options.allowDockerFallback && dockerAvailable()) {
|
|
947
989
|
const useDocker = options.yes
|
|
948
990
|
? true
|
|
949
|
-
: await confirmYesNo(
|
|
991
|
+
: await confirmYesNo("Embedded PostgreSQL failed. Start PostgreSQL via Docker instead?");
|
|
950
992
|
if (useDocker) {
|
|
951
993
|
return ensureDockerDatabaseRuntime(paths, env);
|
|
952
994
|
}
|
|
@@ -955,23 +997,23 @@ export async function ensureDatabaseReady(paths, options = {}) {
|
|
|
955
997
|
}
|
|
956
998
|
}
|
|
957
999
|
export function localDependenciesInstalled(paths) {
|
|
958
|
-
return fs.existsSync(path.join(paths.root,
|
|
959
|
-
|
|
1000
|
+
return (fs.existsSync(path.join(paths.root, "packages", "cli", "node_modules", "embedded-postgres")) &&
|
|
1001
|
+
fs.existsSync(path.join(paths.root, "packages", "cli", "node_modules", "pg")));
|
|
960
1002
|
}
|
|
961
1003
|
export function databaseRuntimeToEnvOptions(runtime) {
|
|
962
1004
|
const options = {};
|
|
963
|
-
if (runtime.mode ===
|
|
1005
|
+
if (runtime.mode === "embedded") {
|
|
964
1006
|
options.databaseMode = runtime.mode;
|
|
965
1007
|
options.embeddedDataDir = runtime.embeddedDataDir;
|
|
966
1008
|
options.embeddedPort = runtime.embeddedPort;
|
|
967
1009
|
return options;
|
|
968
1010
|
}
|
|
969
|
-
if (runtime.mode ===
|
|
1011
|
+
if (runtime.mode === "docker") {
|
|
970
1012
|
options.databaseMode = runtime.mode;
|
|
971
1013
|
options.dockerPort = runtime.dockerPort;
|
|
972
1014
|
return options;
|
|
973
1015
|
}
|
|
974
|
-
if (runtime.source !==
|
|
1016
|
+
if (runtime.source !== "DATABASE_URL") {
|
|
975
1017
|
options.databaseUrl = runtime.databaseUrl;
|
|
976
1018
|
}
|
|
977
1019
|
return options;
|
|
@@ -989,15 +1031,18 @@ export function trimOutput(output, maxLines = 12) {
|
|
|
989
1031
|
.map((line) => line.trimEnd())
|
|
990
1032
|
.filter(Boolean);
|
|
991
1033
|
if (lines.length <= maxLines) {
|
|
992
|
-
return lines.join(
|
|
1034
|
+
return lines.join("\n");
|
|
993
1035
|
}
|
|
994
|
-
return lines.slice(-maxLines).join(
|
|
1036
|
+
return lines.slice(-maxLines).join("\n");
|
|
995
1037
|
}
|
|
996
1038
|
export function requiredEnvKeysMissing(env) {
|
|
997
1039
|
const databasePreference = resolveDatabasePreference(env);
|
|
998
1040
|
return REQUIRED_ENV_KEYS.filter((key) => {
|
|
999
|
-
if (key ===
|
|
1000
|
-
return !env[key] &&
|
|
1041
|
+
if (key === "DATABASE_URL") {
|
|
1042
|
+
return (!env[key] &&
|
|
1043
|
+
databasePreference.mode !== "embedded" &&
|
|
1044
|
+
databasePreference.mode !== "docker" &&
|
|
1045
|
+
databasePreference.reason !== "DATABASE_URL");
|
|
1001
1046
|
}
|
|
1002
1047
|
return !env[key];
|
|
1003
1048
|
});
|