@percepta/create 4.1.15 → 4.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.
Files changed (38) hide show
  1. package/dist/index.js +24 -152
  2. package/dist/index.js.map +1 -1
  3. package/dist/{register-app-BeSQEsel.js → register-app-BtvxQeo0.js} +91 -1
  4. package/dist/register-app-BtvxQeo0.js.map +1 -0
  5. package/package.json +5 -2
  6. package/template-versions.json +2 -2
  7. package/templates/monorepo/README.md +28 -1
  8. package/templates/monorepo/auth/package.json +1 -1
  9. package/templates/monorepo/auth/src/auth.ts +7 -103
  10. package/templates/monorepo/authentik/blueprints/local-dev.yaml +123 -0
  11. package/templates/monorepo/authentik/initdb/00-authentik.sql +5 -0
  12. package/templates/monorepo/docker-compose.yml +70 -0
  13. package/templates/monorepo/oxlint.config.ts.template +5 -1
  14. package/templates/monorepo/package.json.template +2 -1
  15. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +22 -89
  16. package/templates/webapp/AGENTS.md +4 -4
  17. package/templates/webapp/README.md +22 -33
  18. package/templates/webapp/agent-skills/langfuse.md +8 -11
  19. package/templates/webapp/agent-skills/oneshot.md +1 -1
  20. package/templates/webapp/e2e/rbac.spec.ts +28 -4
  21. package/templates/webapp/env.example.template +8 -12
  22. package/templates/webapp/package.json.template +4 -5
  23. package/templates/webapp/scripts/seed.ts +28 -52
  24. package/templates/webapp/scripts/with-local-env.ts +12 -64
  25. package/templates/webapp/src/app/(auth)/auth/signin/SignInForm.tsx +59 -0
  26. package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +3 -3
  27. package/templates/webapp/src/drizzle/db.ts +5 -9
  28. package/templates/webapp/src/instrumentation.ts +5 -72
  29. package/templates/webapp/src/lib/auth/index.ts +1 -2
  30. package/templates/webapp/src/services/DatabaseService.ts +3 -51
  31. package/templates/webapp/src/services/observability/initFaro.ts +5 -17
  32. package/dist/register-app-BeSQEsel.js.map +0 -1
  33. package/templates/monorepo/scripts/setup-local-databases.mjs +0 -183
  34. package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +0 -179
  35. package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +0 -135
  36. package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +0 -53
  37. package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +0 -25
  38. package/templates/webapp/src/lib/auth/app-auth-mode.ts +0 -20
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { Button } from "@percepta/design";
4
+ import { ArrowRight } from "lucide-react";
5
+ import { useSearchParams } from "next/navigation";
6
+ import { useCallback, useState } from "react";
7
+ import { toast } from "sonner";
8
+ import { authClient } from "../../../../lib/auth-client";
9
+
10
+ export function SignInForm() {
11
+ const searchParams = useSearchParams();
12
+ const [isSubmitting, setIsSubmitting] = useState(false);
13
+
14
+ const callbackUrl = searchParams.get("callbackUrl") ?? "/";
15
+
16
+ const signIn = useCallback(async (): Promise<void> => {
17
+ setIsSubmitting(true);
18
+ try {
19
+ const { error } = await authClient.signIn.oauth2({
20
+ providerId: "authentik",
21
+ callbackURL: callbackUrl,
22
+ });
23
+
24
+ if (error != null) {
25
+ toast.error(error.message ?? "Unable to sign in with Authentik.");
26
+ }
27
+ } finally {
28
+ setIsSubmitting(false);
29
+ }
30
+ }, [callbackUrl]);
31
+
32
+ return (
33
+ <div className="grid gap-8">
34
+ <div className="grid gap-3">
35
+ <p className="app-auth-kicker">
36
+ <span>01</span>
37
+ <span>Sign in</span>
38
+ </p>
39
+ <h1 className="app-auth-title">Welcome back.</h1>
40
+ <p className="app-auth-copy text-sm">
41
+ Use your Authentik account to continue.
42
+ </p>
43
+ </div>
44
+ <div className="app-auth-form">
45
+ <div className="flex justify-end">
46
+ <Button
47
+ className="app-auth-submit"
48
+ type="button"
49
+ loading={isSubmitting}
50
+ onClick={signIn}
51
+ >
52
+ <span>Continue with Authentik</span>
53
+ <ArrowRight aria-hidden={true} />
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -1,8 +1,8 @@
1
1
  import type { Metadata } from "next";
2
2
  import { redirect } from "next/navigation";
3
3
  import { Suspense } from "react";
4
- import { AUTH_MODE, getServerSession } from "../../../../lib/auth";
5
- import { CredentialsSignInForm } from "./CredentialsSignInForm";
4
+ import { getServerSession } from "../../../../lib/auth";
5
+ import { SignInForm } from "./SignInForm";
6
6
 
7
7
  export const metadata: Metadata = {
8
8
  title: "Sign In — __APP_TITLE__",
@@ -21,7 +21,7 @@ export default async function SignInPage() {
21
21
  <p className="text-center text-sm text-muted-foreground">Loading…</p>
22
22
  }
23
23
  >
24
- <CredentialsSignInForm authMode={AUTH_MODE} />
24
+ <SignInForm />
25
25
  </Suspense>
26
26
  );
27
27
  }
@@ -1,5 +1,5 @@
1
- import { createPgPool, readDatabaseConfig } from "@percepta/database";
2
- import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
1
+ import { createDrizzlePgDatabase } from "@percepta/database";
2
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import type { Pool } from "pg";
4
4
  import { getEnvConfig } from "../config/getEnvConfig";
5
5
 
@@ -7,11 +7,7 @@ export const { client, db } = createDb();
7
7
 
8
8
  function createDb(): { client: Pool; db: NodePgDatabase } {
9
9
  const { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv } = getEnvConfig();
10
- const pool = createPgPool(
11
- readDatabaseConfig({
12
- env: { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv },
13
- }),
14
- );
15
-
16
- return { client: pool, db: drizzle(pool) };
10
+ return createDrizzlePgDatabase({
11
+ env: { DATABASE_URL: databaseUrl, NODE_ENV: nodeEnv },
12
+ });
17
13
  }
@@ -1,76 +1,9 @@
1
- import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
2
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
3
- import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
4
- import { createLangfuseSpanProcessor } from "@percepta/ai";
5
- import { compact } from "lodash-es";
1
+ import { startPerceptaNodeTelemetry } from "@percepta/ai";
6
2
  import { getEnvConfig } from "./config/getEnvConfig";
7
3
  import { getLogger } from "./services/logger/AppLogger";
8
4
 
9
- type SpanProcessor = tracing.SpanProcessor;
10
-
11
- function setDefaultOpenTelemetryEnv(): void {
12
- const { DEPLOYMENT_ENVIRONMENT: deploymentEnvironment, NODE_ENV: nodeEnv } =
13
- getEnvConfig();
14
-
15
- process.env.OTEL_SERVICE_NAME ??= "__APP_NAME__";
16
- process.env.OTEL_RESOURCE_ATTRIBUTES ??= `deployment.environment=${deploymentEnvironment ?? nodeEnv}`;
17
- }
18
-
19
- function getOtlpTracesEndpoint(): string | undefined {
20
- const {
21
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: tracesEndpoint,
22
- OTEL_EXPORTER_OTLP_ENDPOINT: baseEndpoint,
23
- } = getEnvConfig();
24
-
25
- if (tracesEndpoint) return tracesEndpoint;
26
-
27
- if (!baseEndpoint) return undefined;
28
-
29
- return `${baseEndpoint.replace(/\/$/, "")}/v1/traces`;
30
- }
31
-
32
- function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
33
- const { OTEL_TRACES_EXPORTER: tracesExporter } = getEnvConfig();
34
- if (tracesExporter === "none") {
35
- getLogger().debug(
36
- undefined,
37
- "OTEL_TRACES_EXPORTER=none. Skipping OTLP trace export.",
38
- );
39
- return undefined;
40
- }
41
-
42
- const tracesEndpoint = getOtlpTracesEndpoint();
43
- if (!tracesEndpoint) {
44
- getLogger().debug(
45
- undefined,
46
- "No OTLP trace endpoint found. Skipping OTLP trace export.",
47
- );
48
- return undefined;
49
- }
50
-
51
- getLogger().debug(
52
- { safe: { tracesEndpoint } },
53
- "Registering OTLP trace exporter.",
54
- );
55
- return new tracing.BatchSpanProcessor(
56
- new OTLPTraceExporter({ url: tracesEndpoint }),
57
- );
58
- }
59
-
60
- function getLangfuseSpanProcessor(): SpanProcessor | undefined {
61
- return createLangfuseSpanProcessor(getEnvConfig(), getLogger());
62
- }
63
-
64
- setDefaultOpenTelemetryEnv();
65
-
66
- const spanProcessors: tracing.SpanProcessor[] = compact([
67
- getOtlpSpanProcessor(),
68
- getLangfuseSpanProcessor(),
69
- ]);
70
-
71
- const sdk = new NodeSDK({
72
- spanProcessors,
73
- instrumentations: [getNodeAutoInstrumentations()],
5
+ startPerceptaNodeTelemetry({
6
+ appName: "__APP_NAME__",
7
+ getEnv: getEnvConfig,
8
+ getLogger,
74
9
  });
75
-
76
- sdk.start();
@@ -1,8 +1,7 @@
1
1
  import { auth, type BetterAuthSession } from "@__REPO_NAME__/auth";
2
2
  import { headers } from "next/headers";
3
- import { AUTH_MODE, type AuthMode } from "./app-auth-mode";
4
3
 
5
- export { AUTH_MODE, auth, type AuthMode, type BetterAuthSession };
4
+ export { auth, type BetterAuthSession };
6
5
 
7
6
  export async function getServerSession() {
8
7
  return auth.api.getSession({
@@ -1,53 +1,5 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import type { NodePgDatabase } from "drizzle-orm/node-postgres";
1
+ import { createTransactionalDatabaseServiceFactory } from "@percepta/database";
3
2
  import { db } from "../drizzle/db";
4
3
 
5
- export class DatabaseService {
6
- private static SINGLETON: DatabaseService | undefined;
7
- public static create(): DatabaseService {
8
- if (DatabaseService.SINGLETON == null) {
9
- DatabaseService.SINGLETON = new DatabaseService(db);
10
- }
11
-
12
- return DatabaseService.SINGLETON;
13
- }
14
-
15
- private transactionAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
16
-
17
- private constructor(private database: NodePgDatabase) {}
18
-
19
- public async createTransaction<TReturn>(
20
- callback: (txn: NodePgDatabase) => Promise<TReturn>,
21
- ): Promise<TReturn> {
22
- const currentContext = this.transactionAsyncLocalStorage.getStore();
23
- if (currentContext != null) {
24
- const { txn } = currentContext;
25
-
26
- // Already in a transaction.
27
- return callback(txn);
28
- }
29
-
30
- return this.database.transaction((txn) => {
31
- return this.transactionAsyncLocalStorage.run<Promise<TReturn>>(
32
- { txn },
33
- () => callback(txn),
34
- );
35
- });
36
- }
37
-
38
- public getDatabase(): Database {
39
- const context = this.transactionAsyncLocalStorage.getStore();
40
- if (context == null) {
41
- return this.database;
42
- }
43
-
44
- const { txn } = context;
45
- return txn;
46
- }
47
- }
48
-
49
- type Database = Omit<NodePgDatabase, "transaction">;
50
-
51
- interface LocalStorage {
52
- txn: NodePgDatabase;
53
- }
4
+ export const DatabaseService = createTransactionalDatabaseServiceFactory(db);
5
+ export type DatabaseService = ReturnType<typeof DatabaseService.create>;
@@ -1,22 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { TracingInstrumentation } from "@grafana/faro-web-tracing";
4
- import { createFaroInstance } from "@percepta/next-utils/faro";
3
+ import { createPerceptaFaroInstance } from "@percepta/next-utils/faro";
5
4
  import { getClientEnvConfig } from "../../config/clientEnvConfig";
6
5
 
7
- const {
8
- FARO_COLLECTOR_URL,
9
- FARO_APP_NAME,
10
- FARO_APP_VERSION,
11
- FARO_APP_ENVIRONMENT,
12
- } = getClientEnvConfig();
13
-
14
- export const faroInstance = createFaroInstance({
15
- collectorUrl: FARO_COLLECTOR_URL,
16
- app: {
17
- name: FARO_APP_NAME,
18
- version: FARO_APP_VERSION,
19
- environment: FARO_APP_ENVIRONMENT,
20
- },
21
- extraInstrumentations: [new TracingInstrumentation()],
6
+ export const faroInstance = createPerceptaFaroInstance({
7
+ defaultAppName: "__APP_NAME__",
8
+ defaultEnvironment: process.env.NODE_ENV,
9
+ env: getClientEnvConfig(),
22
10
  });
@@ -1 +0,0 @@
1
- {"version":3,"file":"register-app-BeSQEsel.js","names":[],"sources":["../src/commands/infra/register-app.ts"],"sourcesContent":["import path from \"node:path\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { isMap, isSeq, parseDocument } from \"yaml\";\nimport {\n toKebabCase,\n toSnakeCase,\n toTitleCase,\n} from \"../../utils/case-converters.js\";\nimport { detectMonorepo } from \"../../utils/detect-monorepo.js\";\nimport { validateProjectName } from \"../../utils/validate.js\";\nimport { readWorkspaceManifest } from \"../../utils/workspace-manifest.js\";\nimport {\n createInfraGitHubApi,\n createOrUpdateInfraPullRequestFiles,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n type InfraPullRequestFile,\n resolveGitHubToken,\n} from \"./github.js\";\n\nconst OS_POSTGRESQL_TERRAFORM_SUFFIX = \"postgresql-terraform\";\nconst LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS = \"os-postgresql-terraform\";\nconst OS_POSTGRESQL_TERRAFORM_SERVICES = new Set([\n \"os-postgresql-terraform-aws\",\n \"os-postgresql-terraform-azure\",\n]);\nconst OS_BLUEPRINT_INPUT_GROUPS = [\n {\n name: \"general\",\n displayName: \"General\",\n description: \"Shared OS infrastructure settings.\",\n },\n {\n name: \"applications\",\n displayName: \"Applications\",\n description: \"Generated OS webapp settings.\",\n },\n {\n name: \"aws_postgresql\",\n displayName: \"AWS PostgreSQL\",\n description: \"AWS Aurora PostgreSQL settings.\",\n condition: '{{ eq EnvironmentProviderType \"aws\" }}',\n },\n {\n name: \"azure_postgresql\",\n displayName: \"Azure PostgreSQL\",\n description: \"Azure PostgreSQL Flexible Server settings.\",\n condition: '{{ eq EnvironmentProviderType \"azure\" }}',\n },\n];\n\nexport interface RegisterAppResult {\n appName: string;\n blueprintName: string;\n blueprintPath: string;\n branchName: string;\n customerSlug: string;\n pullRequestUrl: string | null;\n repository: typeof INFRA_REPOSITORY;\n status: \"already_registered\" | \"created_pr\" | \"updated_pr\";\n servicePath: string;\n targetPath: string;\n}\n\nexport async function registerApp(\n appNameInput: string,\n args: {\n cwd?: string;\n github?: InfraGitHubApi;\n } = {},\n): Promise<RegisterAppResult> {\n const appName = normalizeAppName(appNameInput);\n const cwd = args.cwd ?? process.cwd();\n const monorepoContext = await detectMonorepo(cwd);\n if (!monorepoContext.found || !monorepoContext.rootDir) {\n throw new Error(\n \"Run this command from a Mosaic customer monorepo with a .mosaic-workspace.json file.\",\n );\n }\n\n const workspaceManifest = await readWorkspaceManifest(\n monorepoContext.rootDir,\n );\n const customerSlug = workspaceManifest?.customerSlug;\n if (!customerSlug) {\n throw new Error(\n \".mosaic-workspace.json is missing customerSlug. Recreate the monorepo with a current @percepta/create.\",\n );\n }\n\n const github = args.github ?? createInfraGitHubApi(resolveGitHubToken());\n const blueprintName = `${customerSlug}-os`;\n const branchName = `blueberry/register-${customerSlug}-${appName}`;\n const blueprintPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n const servicePath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"services\",\n `${appName}.service.yaml`,\n ].join(\"/\");\n\n const mainBlueprintFile = await github.getFile(\n blueprintPath,\n INFRA_BASE_BRANCH,\n );\n if (!mainBlueprintFile) {\n throw new Error(\n `${blueprintPath} does not exist in ${INFRA_REPOSITORY}. Run \\`pnpm mosaic infra register-os-blueprint\\` and merge that infra PR first.`,\n );\n }\n\n const mainServiceFile = await github.getFile(servicePath, INFRA_BASE_BRANCH);\n const serviceContent =\n mainServiceFile == null\n ? await readLocalServiceDefinition(monorepoContext.rootDir, appName)\n : null;\n const blueprintContent = registerAppInBlueprint(\n mainBlueprintFile.content,\n appName,\n );\n\n const files: InfraPullRequestFile[] = [];\n if (blueprintContent !== mainBlueprintFile.content) {\n files.push({\n baseFileSha: mainBlueprintFile.sha,\n content: blueprintContent,\n message: `Register ${appName} in ${blueprintName}`,\n path: blueprintPath,\n });\n }\n if (serviceContent != null) {\n files.push({\n content: serviceContent,\n message: `Register ${appName} service`,\n path: servicePath,\n });\n }\n\n if (files.length === 0) {\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n servicePath,\n targetPath: blueprintPath,\n };\n }\n\n const pullRequest = await createOrUpdateInfraPullRequestFiles({\n branchName,\n github,\n files,\n title: `Register ${appName} app`,\n body: [\n `Registers the ${appName} service and deployment in ${blueprintName}.`,\n \"\",\n \"Generated by `mosaic infra register-app`.\",\n ].join(\"\\n\"),\n });\n\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n servicePath,\n targetPath: blueprintPath,\n };\n}\n\nexport async function registerAppCommand(appName: string): Promise<void> {\n try {\n const result = await registerApp(appName);\n\n if (result.status === \"already_registered\") {\n console.log(\n chalk.green(\"✔\"),\n `${result.appName} is already registered in ${result.repository} at`,\n chalk.cyan(result.targetPath),\n );\n return;\n }\n\n const verb =\n result.status === \"created_pr\" ? \"Created\" : \"Updated existing\";\n console.log(\n chalk.green(\"✔\"),\n `${verb} infra PR for ${result.appName}:`,\n chalk.cyan(result.pullRequestUrl),\n );\n } catch (error) {\n console.error(chalk.red(\"Error:\"), (error as Error).message);\n process.exit(1);\n }\n}\n\nexport function addAppDatabaseToBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: false,\n appInputs: false,\n });\n}\n\nexport function registerAppInBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: true,\n appInputs: true,\n });\n}\n\nfunction updateBlueprint(\n blueprintContent: string,\n appName: string,\n options: {\n appDatabase: boolean;\n appInstallation: boolean;\n appInputs: boolean;\n },\n): string {\n const document = parseDocument(blueprintContent);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid OS blueprint YAML: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const spec = document.get(\"spec\", true);\n if (!isMap(spec)) {\n throw new Error(\"OS blueprint must include a spec map.\");\n }\n\n let changed = false;\n const inputs = spec.get(\"inputs\", true);\n if (!isSeq(inputs)) {\n throw new Error(\"OS blueprint spec.inputs must be a sequence.\");\n }\n\n changed = ensureInputGroups(document, spec) || changed;\n\n if (options.appInputs) {\n changed =\n addAppInput(document, inputs, renderIngressDomainInput()) || changed;\n changed =\n addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n }\n\n if (options.appDatabase) {\n changed = addAppDatabase(document, inputs, appName) || changed;\n }\n\n if (options.appInstallation) {\n const installations = spec.get(\"installations\", true);\n if (!isSeq(installations)) {\n throw new Error(\"OS blueprint spec.installations must be a sequence.\");\n }\n const postgresqlInstallationName = getOsPostgresqlInstallationName(\n getBlueprintName(document),\n );\n changed =\n ensureOsPostgresqlInstallationAlias(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n ensureAppInstallationPostgresqlOutputRefs(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n addAppInstallation(\n document,\n installations,\n appName,\n postgresqlInstallationName,\n ) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\n}\n\nfunction ensureInputGroups(\n document: ReturnType<typeof parseDocument>,\n spec: {\n get(key: string, keepScalar?: true): unknown;\n set(key: string, value: unknown): void;\n },\n): boolean {\n let changed = false;\n const inputGroups = spec.get(\"inputGroups\", true);\n\n if (inputGroups == null) {\n spec.set(\"inputGroups\", document.createNode(OS_BLUEPRINT_INPUT_GROUPS));\n return true;\n }\n\n if (!isSeq(inputGroups)) {\n throw new Error(\"OS blueprint spec.inputGroups must be a sequence.\");\n }\n\n for (const group of OS_BLUEPRINT_INPUT_GROUPS) {\n const exists = inputGroups.items.some(\n (item) => isMap(item) && item.get(\"name\") === group.name,\n );\n if (exists) continue;\n\n inputGroups.add(document.createNode(group));\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureOsPostgresqlInstallationAlias(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n\n const service = installation.get(\"service\");\n if (\n typeof service !== \"string\" ||\n !OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n ) {\n continue;\n }\n\n if (installation.get(\"name\") === postgresqlInstallationName) continue;\n\n installation.set(\"name\", postgresqlInstallationName);\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureAppInstallationPostgresqlOutputRefs(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n if (isOsPostgresqlInstallation(installation)) continue;\n\n const env = installation.get(\"env\", true);\n if (!isSeq(env)) continue;\n\n for (const envVar of env.items) {\n if (!isMap(envVar)) continue;\n\n const valueFromOutput = envVar.get(\"valueFromOutput\", true);\n if (!isMap(valueFromOutput)) continue;\n if (\n valueFromOutput.get(\"serviceInstallation\") !==\n LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS\n ) {\n continue;\n }\n\n const outputName = valueFromOutput.get(\"name\");\n if (\n typeof outputName !== \"string\" ||\n (outputName !== \"auth_database_url\" &&\n !outputName.startsWith(\"app_database_urls.\"))\n ) {\n continue;\n }\n\n valueFromOutput.set(\"serviceInstallation\", postgresqlInstallationName);\n changed = true;\n }\n }\n\n return changed;\n}\n\nfunction isOsPostgresqlInstallation(installation: {\n get(key: string, keepScalar?: true): unknown;\n}): boolean {\n const service = installation.get(\"service\");\n return (\n typeof service === \"string\" && OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n );\n}\n\nfunction getBlueprintName(document: ReturnType<typeof parseDocument>): string {\n const metadata = document.get(\"metadata\", true);\n if (!isMap(metadata)) {\n throw new Error(\"OS blueprint must include a metadata map.\");\n }\n\n const name = metadata.get(\"name\");\n if (typeof name !== \"string\" || name.length === 0) {\n throw new Error(\"OS blueprint metadata.name must be a non-empty string.\");\n }\n\n return name;\n}\n\nfunction getOsPostgresqlInstallationName(blueprintName: string): string {\n return `${blueprintName}-${OS_POSTGRESQL_TERRAFORM_SUFFIX}`;\n}\n\nfunction addAppInput(\n document: ReturnType<typeof parseDocument>,\n inputs: { add(value: unknown): void; items: unknown[] },\n input: Record<string, unknown> & { name: string },\n): boolean {\n if (\n inputs.items.some((item) => isMap(item) && item.get(\"name\") === input.name)\n ) {\n return false;\n }\n\n inputs.add(document.createNode(input));\n return true;\n}\n\nfunction addAppDatabase(\n document: ReturnType<typeof parseDocument>,\n inputs: { items: unknown[] },\n appName: string,\n): boolean {\n const appDatabasesInput = inputs.items.find(\n (item) => isMap(item) && item.get(\"name\") === \"app_databases\",\n );\n if (!isMap(appDatabasesInput)) {\n throw new Error(\"OS blueprint must include an app_databases input.\");\n }\n\n const defaultValue = appDatabasesInput.get(\"default\", true);\n if (!isMap(defaultValue)) {\n throw new Error(\"OS blueprint app_databases default must be a map.\");\n }\n\n if (defaultValue.has(appName)) return false;\n\n defaultValue.flow = false;\n const appDatabaseValue = document.createNode({\n schema_name: toSnakeCase(appName),\n });\n defaultValue.set(appName, appDatabaseValue);\n return true;\n}\n\nfunction addAppInstallation(\n document: ReturnType<typeof parseDocument>,\n installations: { add(value: unknown): void; items: unknown[] },\n appName: string,\n postgresqlInstallationName: string,\n): boolean {\n if (\n installations.items.some(\n (item) => isMap(item) && item.get(\"service\") === appName,\n )\n ) {\n return false;\n }\n\n installations.add(\n document.createNode({\n service: appName,\n env: renderAppInstallationEnv(appName, postgresqlInstallationName),\n config: renderAppInstallationConfig(appName),\n }),\n );\n return true;\n}\n\nfunction renderIngressDomainInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: ingressDomainInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Ingress Domain\",\n description: \"Shared ingress domain for generated OS webapps.\",\n default: '{{ default \"example.local\" .ryvn.env.state.public_domain.name }}',\n };\n}\n\nfunction renderBetterAuthSecretInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: betterAuthSecretInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: `${toTitleCase(appName)} Better Auth Secret`,\n description: `Generated Better Auth signing secret for ${appName}.`,\n hidden: true,\n generated: {\n type: \"random-bytes\",\n length: 32,\n },\n };\n}\n\nfunction renderLangfusePublicKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfusePublicKeyInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Langfuse Public Key\",\n description:\n \"Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export.\",\n default: \"\",\n };\n}\n\nfunction renderLangfuseSecretKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfuseSecretKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Langfuse Secret Key\",\n description:\n \"Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export.\",\n condition: `{{ ne (input \"${langfusePublicKeyInputName()}\") \"\" }}`,\n };\n}\n\nfunction renderInngestEventKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestEventKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Event Key\",\n description: \"Shared Inngest event key for generated OS webapps.\",\n };\n}\n\nfunction renderInngestSigningKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestSigningKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Signing Key\",\n description: \"Shared Inngest signing key for generated OS webapps.\",\n };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n postgresqlInstallationName: string,\n): Array<Record<string, unknown>> {\n const appInternalEndpoint = `http://${appName}-web-server.{{ .ryvn.env.name }}.svc.cluster.local:3000/api/inngest`;\n\n return [\n {\n key: \"DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: `app_database_urls.${appName}`,\n },\n },\n {\n key: \"AUTH_DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: \"auth_database_url\",\n },\n },\n {\n key: \"DATABASE_SCHEMA\",\n value: toSnakeCase(appName),\n },\n {\n key: \"INGRESS_DOMAIN\",\n valueFromInput: {\n name: ingressDomainInputName(),\n },\n },\n {\n key: \"APP_BASE_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"BETTER_AUTH_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"DEPLOYMENT_ENVIRONMENT\",\n value: \"{{ .ryvn.env.name }}\",\n },\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"INNGEST_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"inngest_base_url\",\n },\n },\n {\n key: \"INNGEST_EVENT_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestEventKeyInputName(),\n },\n },\n {\n key: \"INNGEST_SIGNING_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestSigningKeyInputName(),\n },\n },\n {\n key: \"INNGEST_APP_URL\",\n value: appInternalEndpoint,\n },\n {\n key: \"INNGEST_SERVE_HOST\",\n value: appInternalEndpoint,\n },\n {\n key: \"LANGFUSE_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"langfuse_internal_url\",\n },\n },\n {\n key: \"LANGFUSE_PUBLIC_KEY\",\n valueFromInput: {\n name: langfusePublicKeyInputName(),\n },\n },\n {\n key: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(),\n },\n },\n {\n key: \"OTEL_EXPORTER_OTLP_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"otel_exporter_otlp_endpoint\",\n },\n },\n {\n key: \"SPICEDB_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_endpoint\",\n },\n },\n {\n key: \"SPICEDB_PRESHARED_KEY\",\n isSecret: true,\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_preshared_key\",\n },\n },\n {\n key: \"SPICEDB_INSECURE\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_insecure\",\n },\n },\n ];\n}\n\nfunction renderAppInstallationConfig(appName: string): string {\n const appHost = `${appName}.{{ input \"${ingressDomainInputName()}\" }}`;\n\n return [\n \"replicaCount: 1\",\n \"\",\n \"service:\",\n \" port: 3000\",\n \"\",\n \"livenessEnabled: true\",\n \"readinessEnabled: true\",\n \"startupEnabled: true\",\n \"\",\n \"resources:\",\n \" requests:\",\n ' cpu: \"100m\"',\n \" memory: 256Mi\",\n \" limits:\",\n ' cpu: \"500m\"',\n \" memory: 512Mi\",\n \"\",\n \"ingress:\",\n \" enabled: true\",\n \" className: external-nginx\",\n \" annotations:\",\n \" cert-manager.io/cluster-issuer: external-issuer\",\n ' nginx.ingress.kubernetes.io/ssl-redirect: \"true\"',\n \" hosts:\",\n ` - host: '${appHost}'`,\n \" paths:\",\n \" - path: /\",\n \" pathType: Prefix\",\n \" tls:\",\n ` - secretName: ${appName}-tls`,\n \" hosts:\",\n ` - '${appHost}'`,\n \"\",\n ].join(\"\\n\");\n}\n\nasync function readLocalServiceDefinition(\n monorepoRoot: string,\n appName: string,\n): Promise<string> {\n const serviceDefinitionPath = path.join(\n monorepoRoot,\n \"packages\",\n appName,\n \"deploy\",\n \"ryvn\",\n `${appName}.service.yaml`,\n );\n if (!(await fs.pathExists(serviceDefinitionPath))) {\n throw new Error(\n `${serviceDefinitionPath} does not exist. Add the app's Ryvn service definition before registering it in infra.`,\n );\n }\n\n const content = await fs.readFile(serviceDefinitionPath, \"utf-8\");\n validateLocalServiceDefinition(content, appName, serviceDefinitionPath);\n return content.endsWith(\"\\n\") ? content : `${content}\\n`;\n}\n\nfunction validateLocalServiceDefinition(\n content: string,\n appName: string,\n serviceDefinitionPath: string,\n): void {\n const document = parseDocument(content);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid Ryvn service YAML at ${serviceDefinitionPath}: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const service = document.toJS() as {\n kind?: unknown;\n metadata?: { name?: unknown };\n };\n if (service.kind !== \"Service\" || service.metadata?.name !== appName) {\n throw new Error(\n `${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`,\n );\n }\n}\n\nfunction ingressDomainInputName(): string {\n return \"ingress_domain\";\n}\n\nfunction betterAuthSecretInputName(appName: string): string {\n return `${toSnakeCase(appName)}_better_auth_secret`;\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_key\";\n}\n\nfunction inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction normalizeAppName(appNameInput: string): string {\n const appName = toKebabCase(appNameInput);\n const validation = validateProjectName(appName);\n if (!validation.valid) {\n throw new Error(`Invalid app name: ${validation.error}`);\n }\n return appName;\n}\n"],"mappings":"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ACE;EACF,SAAS;CACX;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aACE;EACF,WAAW,iBAAiB,2BAA2B,EAAE;CAC3D;AACF;AAEA,SAAS,6BAEP;CACA,OAAO;EACL,MAAM,yBAAyB;EAC/B,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,yBACP,SACA,4BACgC;CAChC,MAAM,sBAAsB,UAAU,QAAQ;CAE9C,OAAO;EACL;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM,qBAAqB;GAC7B;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,OAAO,YAAY,OAAO;EAC5B;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,uBAAuB,EAC/B;EACF;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,OAAO,EACzC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,yBAAyB,EACjC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;CACF;AACF;AAEA,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,UAAU,GAAG,QAAQ,aAAa,uBAAuB,EAAE;CAEjE,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAgB,QAAQ;EACxB;EACA;EACA;EACA;EACA,qBAAqB,QAAQ;EAC7B;EACA,cAAc,QAAQ;EACtB;CACF,EAAE,KAAK,IAAI;AACb;AAEA,eAAe,2BACb,cACA,SACiB;CACjB,MAAM,wBAAwB,KAAK,KACjC,cACA,YACA,SACA,UACA,QACA,GAAG,QAAQ,cACb;CACA,IAAI,CAAE,MAAM,GAAG,WAAW,qBAAqB,GAC7C,MAAM,IAAI,MACR,GAAG,sBAAsB,uFAC3B;CAGF,MAAM,UAAU,MAAM,GAAG,SAAS,uBAAuB,OAAO;CAChE,+BAA+B,SAAS,SAAS,qBAAqB;CACtE,OAAO,QAAQ,SAAS,IAAI,IAAI,UAAU,GAAG,QAAQ;AACvD;AAEA,SAAS,+BACP,SACA,SACA,uBACM;CACN,MAAM,WAAW,cAAc,OAAO;CACtC,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,gCAAgC,sBAAsB,IAAI,SAAS,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAAI,GACnH;CAGF,MAAM,UAAU,SAAS,KAAK;CAI9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,UAAU,SAAS,SAC3D,MAAM,IAAI,MACR,GAAG,sBAAsB,iDAAiD,QAAQ,EACpF;AAEJ;AAEA,SAAS,yBAAiC;CACxC,OAAO;AACT;AAEA,SAAS,0BAA0B,SAAyB;CAC1D,OAAO,GAAG,YAAY,OAAO,EAAE;AACjC;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,2BAAmC;CAC1C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,YAAY;CACxC,MAAM,aAAa,oBAAoB,OAAO;CAC9C,IAAI,CAAC,WAAW,OACd,MAAM,IAAI,MAAM,qBAAqB,WAAW,OAAO;CAEzD,OAAO;AACT"}
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { execFileSync } from "node:child_process";
4
- import { existsSync, readFileSync, readdirSync } from "node:fs";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const LOCAL_POSTGRES_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
9
- const LOCAL_POSTGRES_PORT = "5434";
10
- const POSTGRES_SERVICE = "postgres";
11
- const POSTGRES_USER = "postgres";
12
- const ROOT_DIR = path.resolve(
13
- path.dirname(fileURLToPath(import.meta.url)),
14
- "..",
15
- );
16
- const PACKAGES_DIR = path.join(ROOT_DIR, "packages");
17
-
18
- const databases = new Set(["auth"]);
19
-
20
- for (const packageDir of listPackageDirs()) {
21
- const database = readPackageDatabaseName(packageDir);
22
- if (database != null) {
23
- databases.add(database);
24
- }
25
- }
26
-
27
- for (const database of [...databases].sort()) {
28
- ensureDatabase(database);
29
- }
30
-
31
- function listPackageDirs() {
32
- if (!existsSync(PACKAGES_DIR)) return [];
33
-
34
- return readdirSync(PACKAGES_DIR, { withFileTypes: true })
35
- .filter((entry) => entry.isDirectory())
36
- .map((entry) => packageDirFor(entry.name));
37
- }
38
-
39
- function packageDirFor(packageName) {
40
- if (
41
- packageName === "." ||
42
- packageName === ".." ||
43
- packageName.includes("/") ||
44
- packageName.includes("\\")
45
- ) {
46
- throw new Error(`Unexpected package directory name: ${packageName}`);
47
- }
48
-
49
- return path.join(PACKAGES_DIR, packageName);
50
- }
51
-
52
- function readPackageDatabaseName(packageDir) {
53
- const env = readPackageEnvFile(packageDir);
54
- const databaseUrl = env.DATABASE_URL;
55
- if (databaseUrl == null || databaseUrl.length === 0) return null;
56
-
57
- let url;
58
- try {
59
- url = new URL(databaseUrl);
60
- } catch {
61
- throw new Error(
62
- `Invalid DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local`,
63
- );
64
- }
65
-
66
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
67
- throw new Error(
68
- `DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must use postgres or postgresql.`,
69
- );
70
- }
71
-
72
- const port = url.port || "5432";
73
- if (!LOCAL_POSTGRES_HOSTS.has(url.hostname) || port !== LOCAL_POSTGRES_PORT) {
74
- console.log(
75
- `Skipping non-local app database for ${path.relative(ROOT_DIR, packageDir)}.`,
76
- );
77
- return null;
78
- }
79
-
80
- const database = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
81
- if (database.length === 0) {
82
- throw new Error(
83
- `DATABASE_URL in ${path.relative(ROOT_DIR, packageDir)}/.env.local must include a database name.`,
84
- );
85
- }
86
-
87
- return database;
88
- }
89
-
90
- function readPackageEnvFile(packageDir) {
91
- const safePackageDir = assertPackageDir(packageDir);
92
- const envPath = path.join(safePackageDir, ".env.local");
93
- if (!existsSync(envPath)) return {};
94
-
95
- const env = {};
96
- const content = readFileSync(envPath, "utf8");
97
- for (const rawLine of content.split(/\r?\n/)) {
98
- const line = rawLine.trim();
99
- if (!line || line.startsWith("#")) continue;
100
-
101
- const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
102
- const separatorIndex = normalized.indexOf("=");
103
- if (separatorIndex === -1) continue;
104
-
105
- const key = normalized.slice(0, separatorIndex).trim();
106
- const rawValue = normalized.slice(separatorIndex + 1).trim();
107
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
108
-
109
- env[key] = unquote(rawValue);
110
- }
111
-
112
- return env;
113
- }
114
-
115
- function assertPackageDir(packageDir) {
116
- const resolvedPackageDir = path.resolve(packageDir);
117
- const relative = path.relative(PACKAGES_DIR, resolvedPackageDir);
118
-
119
- if (
120
- relative.length === 0 ||
121
- relative.startsWith("..") ||
122
- path.isAbsolute(relative) ||
123
- relative.includes(path.sep)
124
- ) {
125
- throw new Error(`Unexpected package directory: ${packageDir}`);
126
- }
127
-
128
- return resolvedPackageDir;
129
- }
130
-
131
- function unquote(value) {
132
- if (
133
- (value.startsWith('"') && value.endsWith('"')) ||
134
- (value.startsWith("'") && value.endsWith("'"))
135
- ) {
136
- return value.slice(1, -1);
137
- }
138
-
139
- return value;
140
- }
141
-
142
- function ensureDatabase(database) {
143
- const exists = execDockerCompose([
144
- "exec",
145
- "-T",
146
- POSTGRES_SERVICE,
147
- "psql",
148
- "-U",
149
- POSTGRES_USER,
150
- "-d",
151
- "postgres",
152
- "-tAc",
153
- `SELECT 1 FROM pg_database WHERE datname = ${quotePgLiteral(database)}`,
154
- ]).trim();
155
-
156
- if (exists === "1") {
157
- console.log(`Database ${database} already exists.`);
158
- return;
159
- }
160
-
161
- execDockerCompose([
162
- "exec",
163
- "-T",
164
- POSTGRES_SERVICE,
165
- "createdb",
166
- "-U",
167
- POSTGRES_USER,
168
- database,
169
- ]);
170
- console.log(`Database ${database} created.`);
171
- }
172
-
173
- function execDockerCompose(args) {
174
- return execFileSync("docker", ["compose", ...args], {
175
- cwd: ROOT_DIR,
176
- encoding: "utf8",
177
- stdio: ["ignore", "pipe", "inherit"],
178
- });
179
- }
180
-
181
- function quotePgLiteral(value) {
182
- return `'${value.replaceAll("'", "''")}'`;
183
- }