@percepta/create 4.1.1 → 4.1.3

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/README.md CHANGED
@@ -108,9 +108,11 @@ pnpm mosaic infra register-app my-app
108
108
  This opens or updates an infra PR that registers the app service, adds the app
109
109
  to the customer OS blueprint's `app_databases` default, and adds the app's
110
110
  server installation to the customer OS blueprint. The generated installation
111
- wires database/auth secrets, consumes Mosaic runtime outputs for Inngest,
112
- Langfuse, OpenTelemetry, and SpiceDB, plus app-specific credential inputs for
113
- Inngest and Langfuse. Merge the OS blueprint PR before registering apps.
111
+ wires runtime environment variables at the Ryvn installation level, consumes
112
+ the customer OS Postgres outputs with Ryvn `valueFromOutput`, references
113
+ Mosaic blueprint outputs from supported Ryvn templates, and keeps the Helm
114
+ config focused on chart knobs such as replicas, service, probes, resources,
115
+ and ingress. Merge the OS blueprint PR before registering apps.
114
116
 
115
117
  ## Development
116
118
 
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-CprpQn_h.js");
1211
+ const { registerAppCommand } = await import("./register-app-0iB8cN97.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) => {
@@ -7,6 +7,8 @@ import chalk from "chalk";
7
7
  import fs from "fs-extra";
8
8
  import { isMap, isSeq, parseDocument } from "yaml";
9
9
  //#region src/commands/infra/register-app.ts
10
+ const OS_POSTGRESQL_TERRAFORM_ALIAS = "os-postgresql-terraform";
11
+ const OS_POSTGRESQL_TERRAFORM_SERVICES = new Set(["os-postgresql-terraform-aws", "os-postgresql-terraform-azure"]);
10
12
  async function registerApp(appNameInput, args = {}) {
11
13
  const appName = normalizeAppName(appNameInput);
12
14
  const monorepoContext = await detectMonorepo(args.cwd ?? process.cwd());
@@ -112,21 +114,34 @@ function updateBlueprint(blueprintContent, appName, options) {
112
114
  const inputs = spec.get("inputs", true);
113
115
  if (!isSeq(inputs)) throw new Error("OS blueprint spec.inputs must be a sequence.");
114
116
  if (options.appInputs) {
115
- changed = addAppInput(document, inputs, renderHostInput(appName)) || changed;
117
+ changed = addAppInput(document, inputs, renderIngressDomainInput()) || changed;
116
118
  changed = addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) || changed;
117
- changed = addAppInput(document, inputs, renderInngestEventKeyInput(appName)) || changed;
118
- changed = addAppInput(document, inputs, renderInngestSigningKeyInput(appName)) || changed;
119
- changed = addAppInput(document, inputs, renderLangfusePublicKeyInput(appName)) || changed;
120
- changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput(appName)) || changed;
119
+ changed = addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;
120
+ changed = addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;
121
+ changed = addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;
122
+ changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;
121
123
  }
122
124
  if (options.appDatabase) changed = addAppDatabase(document, inputs, appName) || changed;
123
125
  if (options.appInstallation) {
124
126
  const installations = spec.get("installations", true);
125
127
  if (!isSeq(installations)) throw new Error("OS blueprint spec.installations must be a sequence.");
128
+ changed = ensureOsPostgresqlInstallationAlias(installations) || changed;
126
129
  changed = addAppInstallation(document, installations, appName) || changed;
127
130
  }
128
131
  return changed ? document.toString() : blueprintContent;
129
132
  }
133
+ function ensureOsPostgresqlInstallationAlias(installations) {
134
+ let changed = false;
135
+ for (const installation of installations.items) {
136
+ if (!isMap(installation)) continue;
137
+ const service = installation.get("service");
138
+ if (typeof service !== "string" || !OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)) continue;
139
+ if (installation.get("name") === OS_POSTGRESQL_TERRAFORM_ALIAS) continue;
140
+ installation.set("name", OS_POSTGRESQL_TERRAFORM_ALIAS);
141
+ changed = true;
142
+ }
143
+ return changed;
144
+ }
130
145
  function addAppInput(document, inputs, input) {
131
146
  if (inputs.items.some((item) => isMap(item) && item.get("name") === input.name)) return false;
132
147
  inputs.add(document.createNode(input));
@@ -153,14 +168,14 @@ function addAppInstallation(document, installations, appName) {
153
168
  }));
154
169
  return true;
155
170
  }
156
- function renderHostInput(appName) {
171
+ function renderIngressDomainInput() {
157
172
  return {
158
- name: hostInputName(appName),
173
+ name: ingressDomainInputName(),
159
174
  type: "string",
160
175
  group: "Applications",
161
- displayName: `${toTitleCase(appName)} Host`,
162
- description: `Ingress host for ${appName}.`,
163
- default: `${appName}.{{ default "example.local" .ryvn.env.state.public_domain.name }}`
176
+ displayName: "Ingress Domain",
177
+ description: "Shared ingress domain for generated OS webapps.",
178
+ default: "{{ default \"example.local\" .ryvn.env.state.public_domain.name }}"
164
179
  };
165
180
  }
166
181
  function renderBetterAuthSecretInput(appName) {
@@ -178,76 +193,162 @@ function renderBetterAuthSecretInput(appName) {
178
193
  }
179
194
  };
180
195
  }
181
- function renderInngestEventKeyInput(appName) {
196
+ function renderInngestEventKeyInput() {
182
197
  return {
183
- name: inngestEventKeyInputName(appName),
198
+ name: inngestEventKeyInputName(),
184
199
  type: "string",
185
200
  isSecret: true,
186
201
  group: "Applications",
187
- displayName: `${toTitleCase(appName)} Inngest Event Key`,
188
- description: `Inngest event key for ${appName}. Leave empty when the target Inngest installation does not require one.`,
189
- default: ""
202
+ displayName: "Inngest Event Key",
203
+ description: "Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
190
204
  };
191
205
  }
192
- function renderInngestSigningKeyInput(appName) {
206
+ function renderInngestSigningKeyInput() {
193
207
  return {
194
- name: inngestSigningKeyInputName(appName),
208
+ name: inngestSigningKeyInputName(),
195
209
  type: "string",
196
210
  isSecret: true,
197
211
  group: "Applications",
198
- displayName: `${toTitleCase(appName)} Inngest Signing Key`,
199
- description: `Inngest signing key for ${appName}. Leave empty when the target Inngest installation does not require one.`,
200
- default: ""
212
+ displayName: "Inngest Signing Key",
213
+ description: "Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
201
214
  };
202
215
  }
203
- function renderLangfusePublicKeyInput(appName) {
216
+ function renderLangfusePublicKeyInput() {
204
217
  return {
205
- name: langfusePublicKeyInputName(appName),
218
+ name: langfusePublicKeyInputName(),
206
219
  type: "string",
207
220
  group: "Applications",
208
- displayName: `${toTitleCase(appName)} Langfuse Public Key`,
209
- description: `Langfuse public key for ${appName}. Leave empty to disable Langfuse export.`,
221
+ displayName: "Langfuse Public Key",
222
+ description: "Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export.",
210
223
  default: ""
211
224
  };
212
225
  }
213
- function renderLangfuseSecretKeyInput(appName) {
226
+ function renderLangfuseSecretKeyInput() {
214
227
  return {
215
- name: langfuseSecretKeyInputName(appName),
228
+ name: langfuseSecretKeyInputName(),
216
229
  type: "string",
217
230
  isSecret: true,
218
231
  group: "Applications",
219
- displayName: `${toTitleCase(appName)} Langfuse Secret Key`,
220
- description: `Langfuse secret key for ${appName}. Leave empty to disable Langfuse export.`,
221
- default: ""
232
+ displayName: "Langfuse Secret Key",
233
+ description: "Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export."
222
234
  };
223
235
  }
224
236
  function renderAppInstallationEnv(appName) {
237
+ const appHost = `${appName}.{{ input "${ingressDomainInputName()}" }}`;
225
238
  return [
239
+ {
240
+ key: "DATABASE_URL",
241
+ isSecret: true,
242
+ valueFromOutput: {
243
+ serviceInstallation: "os-postgresql-terraform",
244
+ name: `app_database_urls.${appName}`
245
+ }
246
+ },
247
+ {
248
+ key: "AUTH_DATABASE_URL",
249
+ isSecret: true,
250
+ valueFromOutput: {
251
+ serviceInstallation: "os-postgresql-terraform",
252
+ name: "auth_database_url"
253
+ }
254
+ },
255
+ {
256
+ key: "APP_BASE_URL",
257
+ value: `https://${appHost}`
258
+ },
259
+ {
260
+ key: "BETTER_AUTH_URL",
261
+ value: `https://${appHost}`
262
+ },
226
263
  {
227
264
  key: "BETTER_AUTH_SECRET",
228
265
  isSecret: true,
229
266
  valueFromInput: { name: betterAuthSecretInputName(appName) }
230
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
+ {
281
+ key: "INNGEST_BASE_URL",
282
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.inngest_base_url }}"
283
+ },
284
+ {
285
+ key: "INNGEST_APP_URL",
286
+ value: `http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest`
287
+ },
231
288
  {
232
289
  key: "INNGEST_EVENT_KEY",
233
290
  isSecret: true,
234
- valueFromInput: { name: inngestEventKeyInputName(appName) }
291
+ valueFromInput: { name: inngestEventKeyInputName() }
235
292
  },
236
293
  {
237
294
  key: "INNGEST_SIGNING_KEY",
238
295
  isSecret: true,
239
- valueFromInput: { name: inngestSigningKeyInputName(appName) }
296
+ valueFromInput: { name: inngestSigningKeyInputName() }
297
+ },
298
+ {
299
+ key: "LANGFUSE_BASE_URL",
300
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.langfuse_base_url }}"
301
+ },
302
+ {
303
+ key: "LANGFUSE_PUBLIC_KEY",
304
+ valueFromInput: { name: langfusePublicKeyInputName() }
240
305
  },
241
306
  {
242
307
  key: "LANGFUSE_SECRET_KEY",
243
308
  isSecret: true,
244
- valueFromInput: { name: langfuseSecretKeyInputName(appName) }
309
+ valueFromInput: { name: langfuseSecretKeyInputName() }
310
+ },
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
+ {
332
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
333
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.otel_exporter_otlp_endpoint }}"
334
+ },
335
+ {
336
+ key: "SPICEDB_ENDPOINT",
337
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.spicedb_endpoint }}"
338
+ },
339
+ {
340
+ key: "SPICEDB_PRESHARED_KEY",
341
+ isSecret: true,
342
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.spicedb_preshared_key }}"
343
+ },
344
+ {
345
+ key: "SPICEDB_INSECURE",
346
+ value: "{{ (blueprintInstallation \"mosaic\").outputs.spicedb_insecure }}"
245
347
  }
246
348
  ];
247
349
  }
248
350
  function renderAppInstallationConfig(appName) {
249
- const hostInput = hostInputName(appName);
250
- const langfusePublicKeyInput = langfusePublicKeyInputName(appName);
351
+ const appHost = `${appName}.{{ input "${ingressDomainInputName()}" }}`;
251
352
  return [
252
353
  "replicaCount: 1",
253
354
  "",
@@ -273,73 +374,14 @@ function renderAppInstallationConfig(appName) {
273
374
  " cert-manager.io/cluster-issuer: external-issuer",
274
375
  " nginx.ingress.kubernetes.io/ssl-redirect: \"true\"",
275
376
  " hosts:",
276
- ` - host: '{{ input "${hostInput}" }}'`,
377
+ ` - host: '${appHost}'`,
277
378
  " paths:",
278
379
  " - path: /",
279
380
  " pathType: Prefix",
280
381
  " tls:",
281
382
  ` - secretName: ${appName}-tls`,
282
383
  " hosts:",
283
- ` - '{{ input "${hostInput}" }}'`,
284
- "",
285
- "env:",
286
- " - name: DATABASE_URL",
287
- " valueFrom:",
288
- " secretKeyRef:",
289
- " key: database_url",
290
- " {{ if eq EnvironmentProviderType \"aws\" }}",
291
- ` name: '{{ index (serviceInstallation "os-postgresql-terraform-aws").outputs.app_database_secret_names "${appName}" }}'`,
292
- " {{ else if eq EnvironmentProviderType \"azure\" }}",
293
- ` name: '{{ index (serviceInstallation "os-postgresql-terraform-azure").outputs.app_database_secret_names "${appName}" }}'`,
294
- " {{ end }}",
295
- " - name: AUTH_DATABASE_URL",
296
- " valueFrom:",
297
- " secretKeyRef:",
298
- " key: database_url",
299
- " {{ if eq EnvironmentProviderType \"aws\" }}",
300
- " name: '{{ (serviceInstallation \"os-postgresql-terraform-aws\").outputs.auth_secret_name }}'",
301
- " {{ else if eq EnvironmentProviderType \"azure\" }}",
302
- " name: '{{ (serviceInstallation \"os-postgresql-terraform-azure\").outputs.auth_secret_name }}'",
303
- " {{ end }}",
304
- " - name: APP_BASE_URL",
305
- ` value: 'https://{{ input "${hostInput}" }}'`,
306
- " - name: BETTER_AUTH_URL",
307
- ` value: 'https://{{ input "${hostInput}" }}'`,
308
- " - name: NODE_ENV",
309
- " value: production",
310
- " - name: PORT",
311
- " value: \"3000\"",
312
- " - name: AUTH_TRUST_HOST",
313
- " value: \"true\"",
314
- " - name: INNGEST_BASE_URL",
315
- " value: '{{ (blueprintInstallation \"mosaic\").outputs.inngest_base_url }}'",
316
- " - name: INNGEST_APP_URL",
317
- ` value: 'http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest'`,
318
- " - name: LANGFUSE_BASE_URL",
319
- " value: '{{ (blueprintInstallation \"mosaic\").outputs.langfuse_base_url }}'",
320
- " - name: LANGFUSE_PUBLIC_KEY",
321
- ` value: '{{ input "${langfusePublicKeyInput}" }}'`,
322
- " - name: OTEL_SERVICE_NAME",
323
- ` value: ${appName}`,
324
- " - name: OTEL_RESOURCE_ATTRIBUTES",
325
- ` value: 'deployment.environment={{ EnvironmentName }},service.name=${appName}'`,
326
- " - name: OTEL_TRACES_EXPORTER",
327
- " value: otlp",
328
- " - name: OTEL_METRICS_EXPORTER",
329
- " value: otlp",
330
- " - name: OTEL_EXPORTER_OTLP_PROTOCOL",
331
- " value: http/protobuf",
332
- " - name: OTEL_EXPORTER_OTLP_ENDPOINT",
333
- " value: '{{ (blueprintInstallation \"mosaic\").outputs.otel_exporter_otlp_endpoint }}'",
334
- " - name: SPICEDB_ENDPOINT",
335
- " value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_endpoint }}'",
336
- " - name: SPICEDB_PRESHARED_KEY",
337
- " valueFrom:",
338
- " secretKeyRef:",
339
- " key: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_preshared_key_secret_key }}'",
340
- " name: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_config_secret_name }}'",
341
- " - name: SPICEDB_INSECURE",
342
- " value: '{{ (blueprintInstallation \"mosaic\").outputs.spicedb_insecure }}'",
384
+ ` - '${appHost}'`,
343
385
  ""
344
386
  ].join("\n");
345
387
  }
@@ -356,23 +398,23 @@ function validateLocalServiceDefinition(content, appName, serviceDefinitionPath)
356
398
  const service = document.toJS();
357
399
  if (service.kind !== "Service" || service.metadata?.name !== appName) throw new Error(`${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`);
358
400
  }
359
- function hostInputName(appName) {
360
- return `${toSnakeCase(appName)}_host`;
401
+ function ingressDomainInputName() {
402
+ return "ingress_domain";
361
403
  }
362
404
  function betterAuthSecretInputName(appName) {
363
405
  return `${toSnakeCase(appName)}_better_auth_secret`;
364
406
  }
365
- function inngestEventKeyInputName(appName) {
366
- return `${toSnakeCase(appName)}_inngest_event_key`;
407
+ function inngestEventKeyInputName() {
408
+ return "inngest_event_key";
367
409
  }
368
- function inngestSigningKeyInputName(appName) {
369
- return `${toSnakeCase(appName)}_inngest_signing_key`;
410
+ function inngestSigningKeyInputName() {
411
+ return "inngest_signing_key";
370
412
  }
371
- function langfusePublicKeyInputName(appName) {
372
- return `${toSnakeCase(appName)}_langfuse_public_key`;
413
+ function langfusePublicKeyInputName() {
414
+ return "langfuse_public_key";
373
415
  }
374
- function langfuseSecretKeyInputName(appName) {
375
- return `${toSnakeCase(appName)}_langfuse_secret_key`;
416
+ function langfuseSecretKeyInputName() {
417
+ return "langfuse_secret_key";
376
418
  }
377
419
  function normalizeAppName(appNameInput) {
378
420
  const appName = toKebabCase(appNameInput);
@@ -383,4 +425,4 @@ function normalizeAppName(appNameInput) {
383
425
  //#endregion
384
426
  export { registerAppCommand };
385
427
 
386
- //# sourceMappingURL=register-app-CprpQn_h.js.map
428
+ //# sourceMappingURL=register-app-0iB8cN97.js.map
@@ -0,0 +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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -13,6 +13,36 @@ spec:
13
13
  displayName: "App Databases"
14
14
  description: "Application database declarations keyed by app name. This default is the __CUSTOMER_TITLE__ OS app registry; BlueprintInstallation inputs should only override it for environment-specific drift. Each value may set database_name, schema_name, username, and secret_name."
15
15
  default: {}
16
+ - name: ingress_domain
17
+ type: string
18
+ group: Applications
19
+ displayName: "Ingress Domain"
20
+ description: "Shared ingress domain for generated OS webapps."
21
+ default: '{{ default "example.local" .ryvn.env.state.public_domain.name }}'
22
+ - name: inngest_event_key
23
+ type: string
24
+ isSecret: true
25
+ group: Applications
26
+ displayName: "Inngest Event Key"
27
+ description: "Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
28
+ - name: inngest_signing_key
29
+ type: string
30
+ isSecret: true
31
+ group: Applications
32
+ displayName: "Inngest Signing Key"
33
+ description: "Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
34
+ - name: langfuse_public_key
35
+ type: string
36
+ group: Applications
37
+ displayName: "Langfuse Public Key"
38
+ description: "Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export."
39
+ default: ""
40
+ - name: langfuse_secret_key
41
+ type: string
42
+ isSecret: true
43
+ group: Applications
44
+ displayName: "Langfuse Secret Key"
45
+ description: "Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export."
16
46
  - name: auth_secret_name
17
47
  type: string
18
48
  group: General
@@ -77,6 +107,7 @@ spec:
77
107
 
78
108
  installations:
79
109
  - service: os-postgresql-terraform-aws
110
+ name: os-postgresql-terraform
80
111
  condition: '{{ eq EnvironmentProviderType "aws" }}'
81
112
  config: |
82
113
  name: {{ EnvironmentName }}
@@ -102,6 +133,7 @@ spec:
102
133
  {{ end }}
103
134
 
104
135
  - service: os-postgresql-terraform-azure
136
+ name: os-postgresql-terraform
105
137
  condition: '{{ eq EnvironmentProviderType "azure" }}'
106
138
  config: |
107
139
  name: {{ EnvironmentName }}
@@ -123,5 +123,6 @@ This opens or updates an infra PR that registers the app service, adds the app
123
123
  to the customer OS blueprint's `app_databases` default, and adds the app's
124
124
  server installation to the customer OS blueprint. The generated installation
125
125
  wires database/auth secrets, consumes Mosaic runtime outputs for Inngest,
126
- Langfuse, OpenTelemetry, and SpiceDB, plus app-specific credential inputs for
127
- Inngest and Langfuse. Merge the OS blueprint PR before registering apps.
126
+ Langfuse, OpenTelemetry, and SpiceDB, and reuses shared customer OS inputs for
127
+ Inngest/Langfuse credentials. Merge the OS blueprint PR before registering
128
+ apps.
@@ -1 +0,0 @@
1
- {"version":3,"file":"register-app-CprpQn_h.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\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, renderHostInput(appName)) || changed;\n changed =\n addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderInngestEventKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput(appName)) ||\n 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 = addAppInstallation(document, installations, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\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 renderHostInput(appName: string): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: hostInputName(appName),\n type: \"string\",\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Host`,\n description: `Ingress host for ${appName}.`,\n default: `${appName}.{{ 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(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: inngestEventKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Inngest Event Key`,\n description: `Inngest event key for ${appName}. Leave empty when the target Inngest installation does not require one.`,\n default: \"\",\n };\n}\n\nfunction renderInngestSigningKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: inngestSigningKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Inngest Signing Key`,\n description: `Inngest signing key for ${appName}. Leave empty when the target Inngest installation does not require one.`,\n default: \"\",\n };\n}\n\nfunction renderLangfusePublicKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: langfusePublicKeyInputName(appName),\n type: \"string\",\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Langfuse Public Key`,\n description: `Langfuse public key for ${appName}. Leave empty to disable Langfuse export.`,\n default: \"\",\n };\n}\n\nfunction renderLangfuseSecretKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: langfuseSecretKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Langfuse Secret Key`,\n description: `Langfuse secret key for ${appName}. Leave empty to disable Langfuse export.`,\n default: \"\",\n };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n return [\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"INNGEST_EVENT_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestEventKeyInputName(appName),\n },\n },\n {\n key: \"INNGEST_SIGNING_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestSigningKeyInputName(appName),\n },\n },\n {\n key: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(appName),\n },\n },\n ];\n}\n\nfunction renderAppInstallationConfig(appName: string): string {\n const hostInput = hostInputName(appName);\n const langfusePublicKeyInput = langfusePublicKeyInputName(appName);\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: '{{ input \"${hostInput}\" }}'`,\n \" paths:\",\n \" - path: /\",\n \" pathType: Prefix\",\n \" tls:\",\n ` - secretName: ${appName}-tls`,\n \" hosts:\",\n ` - '{{ input \"${hostInput}\" }}'`,\n \"\",\n \"env:\",\n \" - name: DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-aws\").outputs.app_database_secret_names \"${appName}\" }}'`,\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-azure\").outputs.app_database_secret_names \"${appName}\" }}'`,\n \" {{ end }}\",\n \" - name: AUTH_DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-aws\\\").outputs.auth_secret_name }}'\",\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-azure\\\").outputs.auth_secret_name }}'\",\n \" {{ end }}\",\n \" - name: APP_BASE_URL\",\n ` value: 'https://{{ input \"${hostInput}\" }}'`,\n \" - name: BETTER_AUTH_URL\",\n ` value: 'https://{{ input \"${hostInput}\" }}'`,\n \" - name: NODE_ENV\",\n \" value: production\",\n \" - name: PORT\",\n ' value: \"3000\"',\n \" - name: AUTH_TRUST_HOST\",\n ' value: \"true\"',\n \" - name: INNGEST_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.inngest_base_url }}'\",\n \" - name: INNGEST_APP_URL\",\n ` value: 'http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest'`,\n \" - name: LANGFUSE_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.langfuse_base_url }}'\",\n \" - name: LANGFUSE_PUBLIC_KEY\",\n ` value: '{{ input \"${langfusePublicKeyInput}\" }}'`,\n \" - name: OTEL_SERVICE_NAME\",\n ` value: ${appName}`,\n \" - name: OTEL_RESOURCE_ATTRIBUTES\",\n ` value: 'deployment.environment={{ EnvironmentName }},service.name=${appName}'`,\n \" - name: OTEL_TRACES_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_METRICS_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_EXPORTER_OTLP_PROTOCOL\",\n \" value: http/protobuf\",\n \" - name: OTEL_EXPORTER_OTLP_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.otel_exporter_otlp_endpoint }}'\",\n \" - name: SPICEDB_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_endpoint }}'\",\n \" - name: SPICEDB_PRESHARED_KEY\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_preshared_key_secret_key }}'\",\n \" name: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_config_secret_name }}'\",\n \" - name: SPICEDB_INSECURE\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_insecure }}'\",\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 hostInputName(appName: string): string {\n return `${toSnakeCase(appName)}_host`;\n}\n\nfunction betterAuthSecretInputName(appName: string): string {\n return `${toSnakeCase(appName)}_better_auth_secret`;\n}\n\nfunction inngestEventKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_inngest_event_key`;\n}\n\nfunction inngestSigningKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_inngest_signing_key`;\n}\n\nfunction langfusePublicKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_langfuse_public_key`;\n}\n\nfunction langfuseSecretKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_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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"}