@rehearsal-db/core 0.1.0-beta.4 → 0.1.0-beta.6
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/CHANGELOG.md +55 -1
- package/COMPATIBILITY.md +5 -0
- package/README.md +105 -368
- package/SECURITY.md +2 -1
- package/SUPPORT.md +5 -0
- package/docs/adapters.md +5 -0
- package/docs/baselines.md +81 -22
- package/docs/commands.md +76 -64
- package/docs/configuration.md +47 -4
- package/docs/getting-started.md +87 -218
- package/docs/glossary.md +7 -8
- package/docs/sanitization.md +8 -5
- package/docs/security-model.md +5 -1
- package/docs/troubleshooting.md +46 -1
- package/docs/tutorial.md +70 -59
- package/package.json +5 -3
- package/scripts/lib/rehearsal/baseline_preparation.mjs +138 -23
- package/scripts/lib/rehearsal/configuration.d.mts +18 -3
- package/scripts/lib/rehearsal/configuration.mjs +227 -56
- package/scripts/lib/rehearsal/diagnostics.mjs +4 -2
- package/scripts/lib/rehearsal/input_discovery.mjs +155 -0
- package/scripts/lib/rehearsal/plan.mjs +85 -10
- package/scripts/lib/rehearsal/policy_review.mjs +45 -5
- package/scripts/lib/rehearsal/runtime_restore.mjs +6 -2
- package/scripts/lib/rehearsal/setup.mjs +60 -28
- package/scripts/lib/rehearsal/support_report.mjs +102 -0
- package/scripts/lib/runtime/postgresql_runtime.mjs +772 -0
- package/scripts/lib/runtime/runtime_target.mjs +41 -0
- package/scripts/lib/runtime/supabase_runtime.mjs +790 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +6 -785
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +522 -149
|
@@ -105,6 +105,14 @@ const resultCheck = async ({ id, label, run, remediation }) => {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
const assertRuntimeConfigMatches = async ({ config, paths }) => {
|
|
108
|
+
if (config.runtime.target === "postgresql") {
|
|
109
|
+
if (paths.rehearsalConfig !== null || paths.supabaseWorkdir !== null) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"The PostgreSQL target unexpectedly resolved Supabase paths.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return `isolated PostgreSQL image ${config.postgresql.image} and a loopback database port are configured`;
|
|
115
|
+
}
|
|
108
116
|
const source = await readFile(paths.rehearsalConfig, "utf8");
|
|
109
117
|
const expected = [
|
|
110
118
|
["project_id", config.runtime.projectId],
|
|
@@ -217,7 +225,8 @@ export const buildRehearsalPlan = async (options = {}) => {
|
|
|
217
225
|
project: config.project.name,
|
|
218
226
|
},
|
|
219
227
|
environment: {
|
|
220
|
-
|
|
228
|
+
target: config.runtime.target,
|
|
229
|
+
kind: `isolated_local_${config.runtime.target}`,
|
|
221
230
|
applicationUrl: config.runtime.applicationUrl,
|
|
222
231
|
projectId: config.runtime.projectId,
|
|
223
232
|
ports: config.runtime.ports,
|
|
@@ -226,7 +235,7 @@ export const buildRehearsalPlan = async (options = {}) => {
|
|
|
226
235
|
authenticationProviders: config.safety.authenticationProviders,
|
|
227
236
|
barriers: [
|
|
228
237
|
"versioned configuration accepts loopback hosts only",
|
|
229
|
-
|
|
238
|
+
`runtime uses a dedicated local ${config.runtime.target === "supabase" ? "Supabase workdir" : "PostgreSQL container and volume"} and project id`,
|
|
230
239
|
"child processes receive an allowlisted environment",
|
|
231
240
|
"application egress policy denies hosted targets and side effects",
|
|
232
241
|
...(config.safety.authenticationProviders.length
|
|
@@ -382,14 +391,37 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
382
391
|
remediation: "Use npm for the initial Rehearsal contract.",
|
|
383
392
|
},
|
|
384
393
|
{
|
|
385
|
-
id:
|
|
386
|
-
|
|
394
|
+
id:
|
|
395
|
+
config.runtime.target === "supabase"
|
|
396
|
+
? "supabase-cli"
|
|
397
|
+
: "postgresql-image",
|
|
398
|
+
label:
|
|
399
|
+
config.runtime.target === "supabase"
|
|
400
|
+
? "Supabase CLI"
|
|
401
|
+
: "PostgreSQL image",
|
|
387
402
|
run: async () => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
if (config.runtime.target === "supabase") {
|
|
404
|
+
const result = commandAvailable("supabase");
|
|
405
|
+
if (!result.available)
|
|
406
|
+
throw new Error("Supabase CLI is unavailable.");
|
|
407
|
+
return result.version;
|
|
408
|
+
}
|
|
409
|
+
const result = commandAvailable("docker", [
|
|
410
|
+
"image",
|
|
411
|
+
"inspect",
|
|
412
|
+
config.postgresql.image,
|
|
413
|
+
]);
|
|
414
|
+
if (!result.available) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`PostgreSQL image ${config.postgresql.image} is unavailable locally.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return config.postgresql.image;
|
|
391
420
|
},
|
|
392
|
-
remediation:
|
|
421
|
+
remediation:
|
|
422
|
+
config.runtime.target === "supabase"
|
|
423
|
+
? "Install the Supabase CLI in the project and retry."
|
|
424
|
+
: `Pull and review ${config.postgresql.image}, then retry.`,
|
|
393
425
|
},
|
|
394
426
|
{
|
|
395
427
|
id: "docker",
|
|
@@ -412,13 +444,17 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
412
444
|
run: async () => {
|
|
413
445
|
await Promise.all([
|
|
414
446
|
verifyPath(paths.migrationDirectory, "directory"),
|
|
415
|
-
|
|
447
|
+
...(paths.rehearsalConfig
|
|
448
|
+
? [verifyPath(paths.rehearsalConfig, "file")]
|
|
449
|
+
: []),
|
|
416
450
|
verifyPath(paths.sanitizationPolicy, "file"),
|
|
417
451
|
...(paths.runtimeAdapter
|
|
418
452
|
? [verifyPath(paths.runtimeAdapter, "file")]
|
|
419
453
|
: []),
|
|
420
454
|
]);
|
|
421
|
-
return
|
|
455
|
+
return config.runtime.target === "supabase"
|
|
456
|
+
? "migrations, local runtime config, and sanitization policy exist"
|
|
457
|
+
: "migrations and sanitization policy exist";
|
|
422
458
|
},
|
|
423
459
|
remediation:
|
|
424
460
|
"Correct the missing project path in the Rehearsal configuration.",
|
|
@@ -446,6 +482,9 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
446
482
|
id: "service-environment",
|
|
447
483
|
label: "Local service credentials",
|
|
448
484
|
run: async () => {
|
|
485
|
+
if (config.runtime.target === "postgresql") {
|
|
486
|
+
return "no local service credentials required";
|
|
487
|
+
}
|
|
449
488
|
const environment = await readRehearsalServiceEnvironment({
|
|
450
489
|
path: paths.serviceEnvironment,
|
|
451
490
|
keys: config.supabase.serviceEnvironmentVariables,
|
|
@@ -472,6 +511,42 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
472
511
|
);
|
|
473
512
|
const occupied = availability.filter((entry) => !entry.available);
|
|
474
513
|
if (occupied.length) {
|
|
514
|
+
if (config.runtime.target === "postgresql") {
|
|
515
|
+
const status = spawnSync(
|
|
516
|
+
"docker",
|
|
517
|
+
[
|
|
518
|
+
"ps",
|
|
519
|
+
"--filter",
|
|
520
|
+
`label=com.rehearsal-db.project=${config.runtime.projectId}`,
|
|
521
|
+
"--format",
|
|
522
|
+
"{{.Ports}}",
|
|
523
|
+
],
|
|
524
|
+
{
|
|
525
|
+
cwd: projectRoot,
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
env: Object.fromEntries(
|
|
528
|
+
["HOME", "LANG", "LC_ALL", "PATH", "SHELL", "TMPDIR"].flatMap(
|
|
529
|
+
(key) =>
|
|
530
|
+
process.env[key] === undefined
|
|
531
|
+
? []
|
|
532
|
+
: [[key, process.env[key]]],
|
|
533
|
+
),
|
|
534
|
+
),
|
|
535
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
if (
|
|
539
|
+
status.status !== 0 ||
|
|
540
|
+
!String(status.stdout).includes(
|
|
541
|
+
`127.0.0.1:${config.runtime.ports.database}->5432/tcp`,
|
|
542
|
+
)
|
|
543
|
+
) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Configured database port is occupied by an unverified process: database=${config.runtime.ports.database}.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return "occupied only by the configured local Rehearsal runtime";
|
|
549
|
+
}
|
|
475
550
|
const serviceEnvironment = await readRehearsalServiceEnvironment({
|
|
476
551
|
path: paths.serviceEnvironment,
|
|
477
552
|
keys: config.supabase.serviceEnvironmentVariables,
|
|
@@ -7,6 +7,31 @@
|
|
|
7
7
|
import { readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
|
|
9
9
|
|
|
10
|
+
export const SAFE_COLUMN_PRESET = Object.freeze({
|
|
11
|
+
action: "REPLACE WITH SYNTHETIC",
|
|
12
|
+
generated: "NEVER",
|
|
13
|
+
identity: "NO",
|
|
14
|
+
foreignKey: null,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const createSafeTablePreset = (columns) =>
|
|
18
|
+
Object.fromEntries(
|
|
19
|
+
columns.map((column) => [
|
|
20
|
+
typeof column === "string" ? column : column.name,
|
|
21
|
+
{ ...SAFE_COLUMN_PRESET },
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const suggestPolicyExceptionColumns = (columns) =>
|
|
26
|
+
columns
|
|
27
|
+
.map((column) => (typeof column === "string" ? column : column.name))
|
|
28
|
+
.filter(
|
|
29
|
+
(column) =>
|
|
30
|
+
column === "id" ||
|
|
31
|
+
column.endsWith("_id") ||
|
|
32
|
+
["created_at", "updated_at", "deleted_at"].includes(column),
|
|
33
|
+
);
|
|
34
|
+
|
|
10
35
|
export const readReviewablePolicyDraft = async (path) => {
|
|
11
36
|
const source = await readFile(path, "utf8");
|
|
12
37
|
const draft = JSON.parse(source);
|
|
@@ -18,18 +43,33 @@ export const readReviewablePolicyDraft = async (path) => {
|
|
|
18
43
|
return { source, draft };
|
|
19
44
|
};
|
|
20
45
|
|
|
21
|
-
export const completePolicyDraft = async ({
|
|
46
|
+
export const completePolicyDraft = async ({
|
|
47
|
+
draft,
|
|
48
|
+
reviewColumn,
|
|
49
|
+
reviewTable,
|
|
50
|
+
}) => {
|
|
22
51
|
if (draft?.draft !== true || !Array.isArray(draft.tables)) {
|
|
23
52
|
throw new Error("A generated REVIEW REQUIRED policy draft is required.");
|
|
24
53
|
}
|
|
25
54
|
const tables = [];
|
|
26
55
|
for (const table of draft.tables) {
|
|
56
|
+
const columnNames = (table.columns ?? []).map((column) => column.name);
|
|
57
|
+
const tableDecisions = reviewTable
|
|
58
|
+
? await reviewTable({ table: table.name, columns: columnNames })
|
|
59
|
+
: {};
|
|
27
60
|
const columns = [];
|
|
28
61
|
for (const column of table.columns ?? []) {
|
|
29
|
-
const reviewed =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
const reviewed =
|
|
63
|
+
tableDecisions?.[column.name] ??
|
|
64
|
+
(await reviewColumn?.({
|
|
65
|
+
table: table.name,
|
|
66
|
+
column: column.name,
|
|
67
|
+
}));
|
|
68
|
+
if (!reviewed) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Policy review did not classify ${table.name}.${column.name}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
33
73
|
columns.push({ name: column.name, ...reviewed });
|
|
34
74
|
}
|
|
35
75
|
tables.push({ ...table, columns });
|
|
@@ -236,7 +236,11 @@ const buildForeignKeyChecks = (manifest) =>
|
|
|
236
236
|
})
|
|
237
237
|
.join("\n\t");
|
|
238
238
|
|
|
239
|
-
export const buildRestoreSqlSuffix = ({
|
|
239
|
+
export const buildRestoreSqlSuffix = ({
|
|
240
|
+
manifest,
|
|
241
|
+
tableCounts,
|
|
242
|
+
restoreSupabaseAuth = true,
|
|
243
|
+
}) => {
|
|
240
244
|
if (!Array.isArray(manifest?.tables) || !tableCounts) {
|
|
241
245
|
throw new Error("A reviewed manifest and exact row counts are required.");
|
|
242
246
|
}
|
|
@@ -252,7 +256,7 @@ truncate table ${manifest.tables
|
|
|
252
256
|
.map((table) => `public.${quoteIdentifier(table.name)}`)
|
|
253
257
|
.join(", ")}
|
|
254
258
|
restart identity cascade;
|
|
255
|
-
${buildAuthRestoreSql(manifest)}
|
|
259
|
+
${restoreSupabaseAuth ? buildAuthRestoreSql(manifest) : ""}
|
|
256
260
|
${tables.map(buildTableRestoreSql).join("\n")}
|
|
257
261
|
${tables.map(buildIdentitySequenceSql).filter(Boolean).join("\n")}
|
|
258
262
|
set local session_replication_role = origin;
|
|
@@ -9,7 +9,9 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
9
9
|
import {
|
|
10
10
|
inspectDetectedProject,
|
|
11
11
|
renderDetectedConfig,
|
|
12
|
+
renderDetectedPostgresqlConfig,
|
|
12
13
|
} from "./configuration.mjs";
|
|
14
|
+
import { resolveRuntimeTarget } from "../runtime/runtime_target.mjs";
|
|
13
15
|
|
|
14
16
|
const CONFIG_PATH = "rehearsal.config.mjs";
|
|
15
17
|
const LOCAL_SUPABASE_CONFIG_PATH =
|
|
@@ -209,20 +211,26 @@ const assertCreateTargetAvailable = async (file) => {
|
|
|
209
211
|
export const planRehearsalSetup = async ({
|
|
210
212
|
projectRoot = process.cwd(),
|
|
211
213
|
isPortAvailable,
|
|
214
|
+
target = "supabase",
|
|
212
215
|
} = {}) => {
|
|
213
216
|
const root = resolve(projectRoot);
|
|
217
|
+
const runtimeTarget = resolveRuntimeTarget(target).id;
|
|
214
218
|
const detected = await inspectDetectedProject({ projectRoot: root });
|
|
215
219
|
const ports = await findAvailableRehearsalPorts({
|
|
216
220
|
...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
|
|
217
221
|
});
|
|
218
222
|
const applicationUrl = "http://localhost:5175";
|
|
219
223
|
const projectId = `${detected.projectName}-rehearsal`;
|
|
220
|
-
const sourceSupabaseConfig =
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
const sourceSupabaseConfig =
|
|
225
|
+
runtimeTarget === "supabase"
|
|
226
|
+
? await readOptionalFile(join(root, "supabase/config.toml"))
|
|
227
|
+
: null;
|
|
223
228
|
const originalGitignore = await readOptionalFile(join(root, GITIGNORE_PATH));
|
|
224
229
|
const nextGitignore = appendGitignoreEntries(originalGitignore);
|
|
225
|
-
for (const path of [
|
|
230
|
+
for (const path of [
|
|
231
|
+
CONFIG_PATH,
|
|
232
|
+
...(runtimeTarget === "supabase" ? [LOCAL_SUPABASE_CONFIG_PATH] : []),
|
|
233
|
+
]) {
|
|
226
234
|
if (await pathExists(join(root, path))) {
|
|
227
235
|
throw new Error(
|
|
228
236
|
`Rehearsal setup will not overwrite the existing file at ${path}.`,
|
|
@@ -235,23 +243,34 @@ export const planRehearsalSetup = async ({
|
|
|
235
243
|
absolutePath: join(root, CONFIG_PATH),
|
|
236
244
|
action: "create",
|
|
237
245
|
mode: 0o600,
|
|
238
|
-
content:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
content: renderSafeLocalSupabaseConfig({
|
|
249
|
-
projectId,
|
|
250
|
-
applicationUrl,
|
|
251
|
-
ports,
|
|
252
|
-
databaseMajorVersion: databaseMajorVersionFrom(sourceSupabaseConfig),
|
|
253
|
-
}),
|
|
246
|
+
content:
|
|
247
|
+
runtimeTarget === "supabase"
|
|
248
|
+
? renderDetectedConfig(detected, {
|
|
249
|
+
applicationUrl,
|
|
250
|
+
ports,
|
|
251
|
+
})
|
|
252
|
+
: renderDetectedPostgresqlConfig(detected, {
|
|
253
|
+
applicationUrl,
|
|
254
|
+
databasePort: ports.database,
|
|
255
|
+
}),
|
|
254
256
|
},
|
|
257
|
+
...(runtimeTarget === "supabase"
|
|
258
|
+
? [
|
|
259
|
+
{
|
|
260
|
+
path: LOCAL_SUPABASE_CONFIG_PATH,
|
|
261
|
+
absolutePath: join(root, LOCAL_SUPABASE_CONFIG_PATH),
|
|
262
|
+
action: "create",
|
|
263
|
+
mode: 0o600,
|
|
264
|
+
content: renderSafeLocalSupabaseConfig({
|
|
265
|
+
projectId,
|
|
266
|
+
applicationUrl,
|
|
267
|
+
ports,
|
|
268
|
+
databaseMajorVersion:
|
|
269
|
+
databaseMajorVersionFrom(sourceSupabaseConfig),
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
: []),
|
|
255
274
|
{
|
|
256
275
|
path: GITIGNORE_PATH,
|
|
257
276
|
absolutePath: join(root, GITIGNORE_PATH),
|
|
@@ -270,15 +289,24 @@ export const planRehearsalSetup = async ({
|
|
|
270
289
|
projectRoot: root,
|
|
271
290
|
project: detected.projectName,
|
|
272
291
|
projectId,
|
|
292
|
+
target: runtimeTarget,
|
|
273
293
|
applicationUrl,
|
|
274
294
|
detected,
|
|
275
|
-
ports,
|
|
295
|
+
ports: runtimeTarget === "supabase" ? ports : { database: ports.database },
|
|
276
296
|
isPortAvailable: isPortAvailable ?? isRehearsalPortAvailable,
|
|
277
297
|
files,
|
|
278
298
|
safety: [
|
|
279
299
|
"hosted access remains disabled",
|
|
280
|
-
|
|
281
|
-
|
|
300
|
+
runtimeTarget === "supabase"
|
|
301
|
+
? "the Supabase runtime binds to dedicated local ports"
|
|
302
|
+
: "PostgreSQL binds only to a dedicated loopback port",
|
|
303
|
+
...(runtimeTarget === "supabase"
|
|
304
|
+
? [
|
|
305
|
+
"external identity providers, realtime, edge functions, and analytics start disabled",
|
|
306
|
+
]
|
|
307
|
+
: [
|
|
308
|
+
"the runtime uses an already-installed official PostgreSQL image and never pulls implicitly",
|
|
309
|
+
]),
|
|
282
310
|
"configuration files are never overwritten and .gitignore updates are concurrency-checked",
|
|
283
311
|
],
|
|
284
312
|
};
|
|
@@ -331,13 +359,17 @@ export const summarizeRehearsalSetup = (plan, { mode }) => ({
|
|
|
331
359
|
mode,
|
|
332
360
|
project: plan.project,
|
|
333
361
|
projectId: plan.projectId,
|
|
362
|
+
target: plan.target,
|
|
334
363
|
applicationUrl: plan.applicationUrl,
|
|
335
364
|
detected: plan.detected,
|
|
336
|
-
ports:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
365
|
+
ports:
|
|
366
|
+
plan.target === "supabase"
|
|
367
|
+
? {
|
|
368
|
+
api: plan.ports.api,
|
|
369
|
+
database: plan.ports.database,
|
|
370
|
+
studio: plan.ports.studio,
|
|
371
|
+
}
|
|
372
|
+
: { database: plan.ports.database },
|
|
341
373
|
files: plan.files.map((file) => ({ path: file.path, action: file.action })),
|
|
342
374
|
safety: plan.safety,
|
|
343
375
|
nextAction:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a copy-ready, data-free support report. The report intentionally omits
|
|
3
|
+
* project names, paths, command output, environment-variable names, row data,
|
|
4
|
+
* migration contents, and baseline identifiers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { arch, platform, release } from "node:os";
|
|
9
|
+
import { REHEARSAL_VERSION, redactDiagnosticValue } from "./diagnostics.mjs";
|
|
10
|
+
import { runRehearsalDoctor } from "./plan.mjs";
|
|
11
|
+
|
|
12
|
+
export const REHEARSAL_SUPPORT_URL =
|
|
13
|
+
"https://github.com/Ddupasquier/rehearsal-db/issues/new?template=bug.yml";
|
|
14
|
+
|
|
15
|
+
const safeEnvironment = () =>
|
|
16
|
+
Object.fromEntries(
|
|
17
|
+
["HOME", "LANG", "LC_ALL", "PATH", "SHELL", "TMPDIR"].flatMap((key) =>
|
|
18
|
+
process.env[key] === undefined ? [] : [[key, process.env[key]]],
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const runVersionCommand = (command, args) =>
|
|
23
|
+
spawnSync(command, args, {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
env: safeEnvironment(),
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const inspectTool = ({ command, args, runCommand }) => {
|
|
31
|
+
const result = runCommand(command, args);
|
|
32
|
+
if (result.error?.code === "ENOENT") return { status: "not_installed" };
|
|
33
|
+
if (result.status !== 0) return { status: "unavailable" };
|
|
34
|
+
const version = String(result.stdout || result.stderr)
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\r?\n/u)[0]
|
|
37
|
+
.slice(0, 200);
|
|
38
|
+
return {
|
|
39
|
+
status: "available",
|
|
40
|
+
version: redactDiagnosticValue(version || "version not reported"),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const collectRehearsalSupportReport = async (
|
|
45
|
+
options = {},
|
|
46
|
+
{
|
|
47
|
+
runDoctor = runRehearsalDoctor,
|
|
48
|
+
runCommand = runVersionCommand,
|
|
49
|
+
system = {
|
|
50
|
+
platform: platform(),
|
|
51
|
+
release: release(),
|
|
52
|
+
architecture: arch(),
|
|
53
|
+
node: process.versions.node,
|
|
54
|
+
},
|
|
55
|
+
} = {},
|
|
56
|
+
) => {
|
|
57
|
+
const doctor = await runDoctor(options);
|
|
58
|
+
return {
|
|
59
|
+
rehearsalVersion: REHEARSAL_VERSION,
|
|
60
|
+
system,
|
|
61
|
+
tools: {
|
|
62
|
+
npm: inspectTool({ command: "npm", args: ["--version"], runCommand }),
|
|
63
|
+
supabase: inspectTool({
|
|
64
|
+
command: "supabase",
|
|
65
|
+
args: ["--version"],
|
|
66
|
+
runCommand,
|
|
67
|
+
}),
|
|
68
|
+
dockerClient: inspectTool({
|
|
69
|
+
command: "docker",
|
|
70
|
+
args: ["--version"],
|
|
71
|
+
runCommand,
|
|
72
|
+
}),
|
|
73
|
+
dockerServer: inspectTool({
|
|
74
|
+
command: "docker",
|
|
75
|
+
args: ["info", "--format", "{{.ServerVersion}}"],
|
|
76
|
+
runCommand,
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
readiness: {
|
|
80
|
+
state: doctor.state,
|
|
81
|
+
checks: doctor.checks.map(({ id, label, status, remediation }) => ({
|
|
82
|
+
id,
|
|
83
|
+
label,
|
|
84
|
+
status,
|
|
85
|
+
...(status === "fail" && remediation ? { remediation } : {}),
|
|
86
|
+
})),
|
|
87
|
+
quarantinedHostedVariableCount:
|
|
88
|
+
doctor.ambientHostedVariables?.presentButQuarantined.length ?? 0,
|
|
89
|
+
},
|
|
90
|
+
privacy: {
|
|
91
|
+
omits: [
|
|
92
|
+
"row values",
|
|
93
|
+
"credentials",
|
|
94
|
+
"project paths",
|
|
95
|
+
"migration SQL",
|
|
96
|
+
"baseline identifiers",
|
|
97
|
+
],
|
|
98
|
+
reviewBeforeSharing: true,
|
|
99
|
+
},
|
|
100
|
+
supportUrl: REHEARSAL_SUPPORT_URL,
|
|
101
|
+
};
|
|
102
|
+
};
|