@percepta/create 4.1.3 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1208,7 +1208,7 @@ infra.command("register-os-blueprint").description("Register this customer monor
1208
1208
  await registerOsBlueprintCommand();
1209
1209
  });
1210
1210
  infra.command("register-app").description("Register a webapp database in this customer OS blueprint").argument("<app>", "Webapp package name").action(async (appName) => {
1211
- const { registerAppCommand } = await import("./register-app-0iB8cN97.js");
1211
+ const { registerAppCommand } = await import("./register-app-Ctv1Grnr.js");
1212
1212
  await registerAppCommand(appName);
1213
1213
  });
1214
1214
  program.command("status").description("Show template sync status for current app").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").action(async (options) => {
@@ -257,34 +257,18 @@ function renderAppInstallationEnv(appName) {
257
257
  value: `https://${appHost}`
258
258
  },
259
259
  {
260
- key: "BETTER_AUTH_URL",
261
- value: `https://${appHost}`
260
+ key: "DEPLOYMENT_ENVIRONMENT",
261
+ value: "{{ EnvironmentName }}"
262
262
  },
263
263
  {
264
264
  key: "BETTER_AUTH_SECRET",
265
265
  isSecret: true,
266
266
  valueFromInput: { name: betterAuthSecretInputName(appName) }
267
267
  },
268
- {
269
- key: "NODE_ENV",
270
- value: "production"
271
- },
272
- {
273
- key: "PORT",
274
- value: "3000"
275
- },
276
- {
277
- key: "AUTH_TRUST_HOST",
278
- value: "true"
279
- },
280
268
  {
281
269
  key: "INNGEST_BASE_URL",
282
270
  value: "{{ (blueprintInstallation \"mosaic\").outputs.inngest_base_url }}"
283
271
  },
284
- {
285
- key: "INNGEST_APP_URL",
286
- value: `http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest`
287
- },
288
272
  {
289
273
  key: "INNGEST_EVENT_KEY",
290
274
  isSecret: true,
@@ -308,26 +292,6 @@ function renderAppInstallationEnv(appName) {
308
292
  isSecret: true,
309
293
  valueFromInput: { name: langfuseSecretKeyInputName() }
310
294
  },
311
- {
312
- key: "OTEL_SERVICE_NAME",
313
- value: appName
314
- },
315
- {
316
- key: "OTEL_RESOURCE_ATTRIBUTES",
317
- value: `deployment.environment={{ EnvironmentName }},service.name=${appName}`
318
- },
319
- {
320
- key: "OTEL_TRACES_EXPORTER",
321
- value: "otlp"
322
- },
323
- {
324
- key: "OTEL_METRICS_EXPORTER",
325
- value: "otlp"
326
- },
327
- {
328
- key: "OTEL_EXPORTER_OTLP_PROTOCOL",
329
- value: "http/protobuf"
330
- },
331
295
  {
332
296
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
333
297
  value: "{{ (blueprintInstallation \"mosaic\").outputs.otel_exporter_otlp_endpoint }}"
@@ -425,4 +389,4 @@ function normalizeAppName(appNameInput) {
425
389
  //#endregion
426
390
  export { registerAppCommand };
427
391
 
428
- //# sourceMappingURL=register-app-0iB8cN97.js.map
392
+ //# sourceMappingURL=register-app-Ctv1Grnr.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-app-0iB8cN97.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_ALIAS = \"os-postgresql-terraform\";\nconst OS_POSTGRESQL_TERRAFORM_SERVICES = new Set([\n \"os-postgresql-terraform-aws\",\n \"os-postgresql-terraform-azure\",\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 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, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || 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 changed = ensureOsPostgresqlInstallationAlias(installations) || changed;\n changed = addAppInstallation(document, installations, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\n}\n\nfunction ensureOsPostgresqlInstallationAlias(installations: {\n items: unknown[];\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\") === OS_POSTGRESQL_TERRAFORM_ALIAS) continue;\n\n installation.set(\"name\", OS_POSTGRESQL_TERRAFORM_ALIAS);\n changed = true;\n }\n\n return changed;\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 if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;\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): 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),\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 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:\n \"Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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:\n \"Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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 };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n const appHost = `${appName}.{{ input \"${ingressDomainInputName()}\" }}`;\n\n return [\n {\n key: \"DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: \"os-postgresql-terraform\",\n name: `app_database_urls.${appName}`,\n },\n },\n {\n key: \"AUTH_DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: \"os-postgresql-terraform\",\n name: \"auth_database_url\",\n },\n },\n {\n key: \"APP_BASE_URL\",\n value: `https://${appHost}`,\n },\n {\n key: \"BETTER_AUTH_URL\",\n value: `https://${appHost}`,\n },\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"NODE_ENV\",\n value: \"production\",\n },\n {\n key: \"PORT\",\n value: \"3000\",\n },\n {\n key: \"AUTH_TRUST_HOST\",\n value: \"true\",\n },\n {\n key: \"INNGEST_BASE_URL\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.inngest_base_url }}',\n },\n {\n key: \"INNGEST_APP_URL\",\n value: `http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest`,\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: \"LANGFUSE_BASE_URL\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.langfuse_base_url }}',\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_SERVICE_NAME\",\n value: appName,\n },\n {\n key: \"OTEL_RESOURCE_ATTRIBUTES\",\n value: `deployment.environment={{ EnvironmentName }},service.name=${appName}`,\n },\n {\n key: \"OTEL_TRACES_EXPORTER\",\n value: \"otlp\",\n },\n {\n key: \"OTEL_METRICS_EXPORTER\",\n value: \"otlp\",\n },\n {\n key: \"OTEL_EXPORTER_OTLP_PROTOCOL\",\n value: \"http/protobuf\",\n },\n {\n key: \"OTEL_EXPORTER_OTLP_ENDPOINT\",\n value:\n '{{ (blueprintInstallation \"mosaic\").outputs.otel_exporter_otlp_endpoint }}',\n },\n {\n key: \"SPICEDB_ENDPOINT\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_endpoint }}',\n },\n {\n key: \"SPICEDB_PRESHARED_KEY\",\n isSecret: true,\n value:\n '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_preshared_key }}',\n },\n {\n key: \"SPICEDB_INSECURE\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_insecure }}',\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 inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_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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ACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM;IACP;GACF;EACD;GACE,KAAK;GACL,OAAO,WAAW;GACnB;EACD;GACE,KAAK;GACL,OAAO,WAAW;GACnB;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,QAAQ,EACzC;GACF;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO,UAAU,QAAQ;GAC1B;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,EACjC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO,6DAA6D;GACrE;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,OACE;GACH;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,UAAU;GACV,OACE;GACH;EACD;GACE,KAAK;GACL,OAAO;GACR;EACF;;AAGH,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,UAAU,GAAG,QAAQ,aAAa,wBAAwB,CAAC;AAEjE,QAAO;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;EACD,CAAC,KAAK,KAAK;;AAGd,eAAe,2BACb,cACA,SACiB;CACjB,MAAM,wBAAwB,KAAK,KACjC,cACA,YACA,SACA,UACA,QACA,GAAG,QAAQ,eACZ;AACD,KAAI,CAAE,MAAM,GAAG,WAAW,sBAAsB,CAC9C,OAAM,IAAI,MACR,GAAG,sBAAsB,wFAC1B;CAGH,MAAM,UAAU,MAAM,GAAG,SAAS,uBAAuB,QAAQ;AACjE,gCAA+B,SAAS,SAAS,sBAAsB;AACvE,QAAO,QAAQ,SAAS,KAAK,GAAG,UAAU,GAAG,QAAQ;;AAGvD,SAAS,+BACP,SACA,SACA,uBACM;CACN,MAAM,WAAW,cAAc,QAAQ;AACvC,KAAI,SAAS,OAAO,SAAS,EAC3B,OAAM,IAAI,MACR,gCAAgC,sBAAsB,IAAI,SAAS,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,KAAK,KAAK,GACnH;CAGH,MAAM,UAAU,SAAS,MAAM;AAI/B,KAAI,QAAQ,SAAS,aAAa,QAAQ,UAAU,SAAS,QAC3D,OAAM,IAAI,MACR,GAAG,sBAAsB,iDAAiD,QAAQ,GACnF;;AAIL,SAAS,yBAAiC;AACxC,QAAO;;AAGT,SAAS,0BAA0B,SAAyB;AAC1D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,2BAAmC;AAC1C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,aAAa;CACzC,MAAM,aAAa,oBAAoB,QAAQ;AAC/C,KAAI,CAAC,WAAW,MACd,OAAM,IAAI,MAAM,qBAAqB,WAAW,QAAQ;AAE1D,QAAO"}
1
+ {"version":3,"file":"register-app-Ctv1Grnr.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_ALIAS = \"os-postgresql-terraform\";\nconst OS_POSTGRESQL_TERRAFORM_SERVICES = new Set([\n \"os-postgresql-terraform-aws\",\n \"os-postgresql-terraform-azure\",\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 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, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || 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 changed = ensureOsPostgresqlInstallationAlias(installations) || changed;\n changed = addAppInstallation(document, installations, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\n}\n\nfunction ensureOsPostgresqlInstallationAlias(installations: {\n items: unknown[];\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\") === OS_POSTGRESQL_TERRAFORM_ALIAS) continue;\n\n installation.set(\"name\", OS_POSTGRESQL_TERRAFORM_ALIAS);\n changed = true;\n }\n\n return changed;\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 if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;\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): 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),\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 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:\n \"Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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:\n \"Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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 };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n const appHost = `${appName}.{{ input \"${ingressDomainInputName()}\" }}`;\n\n return [\n {\n key: \"DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: \"os-postgresql-terraform\",\n name: `app_database_urls.${appName}`,\n },\n },\n {\n key: \"AUTH_DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: \"os-postgresql-terraform\",\n name: \"auth_database_url\",\n },\n },\n {\n key: \"APP_BASE_URL\",\n value: `https://${appHost}`,\n },\n {\n key: \"DEPLOYMENT_ENVIRONMENT\",\n value: \"{{ EnvironmentName }}\",\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 value: '{{ (blueprintInstallation \"mosaic\").outputs.inngest_base_url }}',\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: \"LANGFUSE_BASE_URL\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.langfuse_base_url }}',\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 value:\n '{{ (blueprintInstallation \"mosaic\").outputs.otel_exporter_otlp_endpoint }}',\n },\n {\n key: \"SPICEDB_ENDPOINT\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_endpoint }}',\n },\n {\n key: \"SPICEDB_PRESHARED_KEY\",\n isSecret: true,\n value:\n '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_preshared_key }}',\n },\n {\n key: \"SPICEDB_INSECURE\",\n value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_insecure }}',\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 inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_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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ACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM;IACP;GACF;EACD;GACE,KAAK;GACL,OAAO,WAAW;GACnB;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,QAAQ,EACzC;GACF;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,EACjC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,4BAA4B,EACnC;GACF;EACD;GACE,KAAK;GACL,OACE;GACH;EACD;GACE,KAAK;GACL,OAAO;GACR;EACD;GACE,KAAK;GACL,UAAU;GACV,OACE;GACH;EACD;GACE,KAAK;GACL,OAAO;GACR;EACF;;AAGH,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,UAAU,GAAG,QAAQ,aAAa,wBAAwB,CAAC;AAEjE,QAAO;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;EACD,CAAC,KAAK,KAAK;;AAGd,eAAe,2BACb,cACA,SACiB;CACjB,MAAM,wBAAwB,KAAK,KACjC,cACA,YACA,SACA,UACA,QACA,GAAG,QAAQ,eACZ;AACD,KAAI,CAAE,MAAM,GAAG,WAAW,sBAAsB,CAC9C,OAAM,IAAI,MACR,GAAG,sBAAsB,wFAC1B;CAGH,MAAM,UAAU,MAAM,GAAG,SAAS,uBAAuB,QAAQ;AACjE,gCAA+B,SAAS,SAAS,sBAAsB;AACvE,QAAO,QAAQ,SAAS,KAAK,GAAG,UAAU,GAAG,QAAQ;;AAGvD,SAAS,+BACP,SACA,SACA,uBACM;CACN,MAAM,WAAW,cAAc,QAAQ;AACvC,KAAI,SAAS,OAAO,SAAS,EAC3B,OAAM,IAAI,MACR,gCAAgC,sBAAsB,IAAI,SAAS,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,KAAK,KAAK,GACnH;CAGH,MAAM,UAAU,SAAS,MAAM;AAI/B,KAAI,QAAQ,SAAS,aAAa,QAAQ,UAAU,SAAS,QAC3D,OAAM,IAAI,MACR,GAAG,sBAAsB,iDAAiD,QAAQ,GACnF;;AAIL,SAAS,yBAAiC;AACxC,QAAO;;AAGT,SAAS,0BAA0B,SAAyB;AAC1D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,2BAAmC;AAC1C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,6BAAqC;AAC5C,QAAO;;AAGT,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,aAAa;CACzC,MAAM,aAAa,oBAAoB,QAAQ;AAC/C,KAAI,CAAC,WAAW,MACd,OAAM,IAAI,MAAM,qBAAqB,WAAW,QAAQ;AAE1D,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -23,9 +23,17 @@ function getSecret(): string {
23
23
  return requiredEnv("BETTER_AUTH_SECRET");
24
24
  }
25
25
 
26
+ function getBaseUrl(): string {
27
+ return (
28
+ process.env.BETTER_AUTH_URL ??
29
+ process.env.APP_BASE_URL ??
30
+ "http://localhost:3000"
31
+ );
32
+ }
33
+
26
34
  function createAuth() {
27
35
  return createPerceptaAuth({
28
- baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
36
+ baseURL: getBaseUrl(),
29
37
  database: db,
30
38
  schema: {
31
39
  user: users,
@@ -226,7 +226,7 @@ Better Auth is configured in the customer monorepo's shared `@__REPO_NAME__/auth
226
226
  - **Sign in**: `authClient.signIn.email({ email, password })` — client-side
227
227
  - **Sign out**: `authClient.signOut()` — client-side
228
228
  - **API route**: `src/app/api/auth/[...all]/route.ts` — Better Auth handler
229
- - **Env vars**: `BETTER_AUTH_SECRET` (required), `BETTER_AUTH_URL` (defaults to `http://localhost:3000`), `AUTH_DATABASE_URL` for deployed shared auth DB wiring
229
+ - **Env vars**: `BETTER_AUTH_SECRET` (required), optional `BETTER_AUTH_URL` override, `AUTH_DATABASE_URL` for deployed shared auth DB wiring
230
230
 
231
231
  ### Background Jobs
232
232
 
@@ -242,7 +242,7 @@ PostgreSQL with Drizzle ORM. Schema in `src/drizzle/schema/`, migrations in `src
242
242
 
243
243
  ### Observability
244
244
 
245
- OpenTelemetry initialized in `src/instrumentation.ts`. Server traces and metrics export to the configured OTEL collector; platform logs are collected from stdout. Langfuse for LLM tracking in `src/services/langfuse/` activates when `LANGFUSE_*` env vars are configured, but only AI SDK spans are forwarded to Langfuse by default. Faro for frontend monitoring via `@percepta/next-utils/faro`.
245
+ OpenTelemetry initialized in `src/instrumentation.ts`. Server traces export to the configured OTEL collector; platform logs are collected from stdout. Langfuse for LLM tracking in `src/services/langfuse/` activates when `LANGFUSE_*` env vars are configured, but only AI SDK spans are forwarded to Langfuse by default. Faro for frontend monitoring via `@percepta/next-utils/faro`.
246
246
 
247
247
  ## Deployment
248
248
 
@@ -155,9 +155,11 @@ Required auth environment variables:
155
155
 
156
156
  ```bash
157
157
  BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
158
- BETTER_AUTH_URL=http://localhost:3000
159
158
  ```
160
159
 
160
+ Auth uses `BETTER_AUTH_URL`, `APP_BASE_URL`, or `http://localhost:3000`, in
161
+ that order, for its base URL.
162
+
161
163
  Remote deployments should also set `AUTH_DATABASE_URL` from the shared auth
162
164
  database Secret. Local development can omit it and use the root-created local
163
165
  `auth` database.
@@ -183,6 +185,7 @@ App permissions are authored in `src/access/schema.zed`; `src/access/access.mani
183
185
  |----------|-------------|---------|
184
186
  | `NODE_ENV` | Environment mode | `development` |
185
187
  | `APP_BASE_URL` | Base URL for the app | - |
188
+ | `DEPLOYMENT_ENVIRONMENT` | Deployment environment label for telemetry | `NODE_ENV` |
186
189
 
187
190
  ### App Database
188
191
 
@@ -260,17 +263,14 @@ For local development, `pnpm dev` also loads `~/.config/percepta/create.env` whe
260
263
 
261
264
  | Variable | Description |
262
265
  |----------|-------------|
263
- | `OTEL_SERVICE_NAME` | Service name attached to traces and metrics |
264
- | `OTEL_RESOURCE_ATTRIBUTES` | Extra resource labels such as deployment environment |
265
- | `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol, usually `http/protobuf` |
266
266
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP HTTP collector endpoint |
267
267
  | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Optional trace-specific OTLP endpoint |
268
- | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Optional metric-specific OTLP endpoint |
269
- | `OTEL_TRACES_EXPORTER` | Set to `otlp` to export server traces |
270
- | `OTEL_METRICS_EXPORTER` | Set to `otlp` to export server metrics |
271
- | `OTEL_METRIC_EXPORT_INTERVAL` | Metrics export interval in milliseconds |
268
+ | `OTEL_TRACES_EXPORTER` | Set to `none` to disable server trace export |
272
269
 
273
- Configure these values through the target deployment platform. Application logs are written to stdout so the platform collector can collect them.
270
+ The generated app sets its OpenTelemetry service name and deployment
271
+ environment resource label internally. Configure the collector endpoint through
272
+ the target deployment platform. Application logs are written to stdout so the
273
+ platform collector can collect them.
274
274
 
275
275
  ## Local AWS Development
276
276
 
@@ -1,13 +1,13 @@
1
1
  # Application
2
2
  NODE_ENV=development
3
3
  APP_BASE_URL=http://localhost:3000
4
+ # DEPLOYMENT_ENVIRONMENT=development
4
5
 
5
6
  # App Database
6
7
  DATABASE_URL=postgresql://postgres:postgres@localhost:5434/__DB_NAME__
7
8
 
8
9
  # Authentication (Better Auth)
9
10
  BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
10
- BETTER_AUTH_URL=http://localhost:3000
11
11
 
12
12
  # Shared Auth Database
13
13
  # Deployed apps should set this from the customer monorepo auth database Secret.
@@ -48,14 +48,10 @@ NEXT_PUBLIC_FARO_APP_ENVIRONMENT=development
48
48
  # LLM_PROVIDER=anthropic
49
49
  # LLM_MODEL=claude-sonnet-4-5-20250929
50
50
 
51
- # OpenTelemetry (server-side traces and metrics)
52
- # OTEL_SERVICE_NAME=__APP_NAME__
53
- # OTEL_TRACES_EXPORTER=otlp
54
- # OTEL_METRICS_EXPORTER=otlp
55
- # OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
51
+ # OpenTelemetry (server-side traces)
56
52
  # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
57
53
  # OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
58
- # OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metrics
54
+ # OTEL_TRACES_EXPORTER=none
59
55
 
60
56
  # AWS (uses default credential chain in development)
61
57
  # AWS_REGION=us-east-1
@@ -9,6 +9,7 @@ export const { getEnvConfig, schema: ENV_CONFIG_SCHEMA } = createEnvConfig(
9
9
  z.object({
10
10
  // Application:
11
11
  APP_BASE_URL: z.string().optional(),
12
+ DEPLOYMENT_ENVIRONMENT: z.string().optional(),
12
13
 
13
14
  // App database:
14
15
  DATABASE_URL: z
@@ -20,7 +21,6 @@ export const { getEnvConfig, schema: ENV_CONFIG_SCHEMA } = createEnvConfig(
20
21
 
21
22
  // Authentication (Better Auth):
22
23
  BETTER_AUTH_SECRET: z.string().optional(),
23
- BETTER_AUTH_URL: z.string().default("http://localhost:3000"),
24
24
  AUTH_DATABASE_URL: z.string().optional(),
25
25
 
26
26
  // Inngest:
@@ -39,15 +39,9 @@ export const { getEnvConfig, schema: ENV_CONFIG_SCHEMA } = createEnvConfig(
39
39
  LANGFUSE_SECRET_KEY: z.string().optional(),
40
40
 
41
41
  // OpenTelemetry:
42
- OTEL_SERVICE_NAME: z.string().optional(),
43
- OTEL_RESOURCE_ATTRIBUTES: z.string().optional(),
44
42
  OTEL_TRACES_EXPORTER: z.string().optional(),
45
- OTEL_METRICS_EXPORTER: z.string().optional(),
46
- OTEL_EXPORTER_OTLP_PROTOCOL: z.string().optional(),
47
43
  OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(),
48
44
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: z.string().optional(),
49
- OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: z.string().optional(),
50
- OTEL_METRIC_EXPORT_INTERVAL: z.string().optional(),
51
45
 
52
46
  // Security:
53
47
  ENCRYPTION_SECRET_KEY: z.string().optional(),
@@ -8,6 +8,17 @@ import { getLogger } from "./services/logger/AppLogger";
8
8
 
9
9
  type SpanProcessor = tracing.SpanProcessor;
10
10
 
11
+ function setDefaultOpenTelemetryEnv(): void {
12
+ const {
13
+ DEPLOYMENT_ENVIRONMENT: deploymentEnvironment,
14
+ NODE_ENV: nodeEnv,
15
+ } = getEnvConfig();
16
+
17
+ process.env.OTEL_SERVICE_NAME ??= "__APP_NAME__";
18
+ process.env.OTEL_RESOURCE_ATTRIBUTES ??=
19
+ `deployment.environment=${deploymentEnvironment ?? nodeEnv}`;
20
+ }
21
+
11
22
  function getOtlpTracesEndpoint(): string | undefined {
12
23
  const {
13
24
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: tracesEndpoint,
@@ -53,6 +64,8 @@ function getLangfuseSpanProcessor(): SpanProcessor | undefined {
53
64
  return createLangfuseSpanProcessor(getEnvConfig(), getLogger());
54
65
  }
55
66
 
67
+ setDefaultOpenTelemetryEnv();
68
+
56
69
  const spanProcessors: tracing.SpanProcessor[] = compact([
57
70
  getOtlpSpanProcessor(),
58
71
  getLangfuseSpanProcessor(),