@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 +5 -3
- package/dist/index.js +1 -1
- package/dist/{register-app-CprpQn_h.js → register-app-0iB8cN97.js} +148 -106
- package/dist/register-app-0iB8cN97.js.map +1 -0
- package/package.json +1 -1
- package/templates/infra/os.blueprint.yaml.template +32 -0
- package/templates/monorepo/README.md +3 -2
- package/dist/register-app-CprpQn_h.js.map +0 -1
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
|
|
112
|
-
|
|
113
|
-
|
|
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-
|
|
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,
|
|
117
|
+
changed = addAppInput(document, inputs, renderIngressDomainInput()) || changed;
|
|
116
118
|
changed = addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) || changed;
|
|
117
|
-
changed = addAppInput(document, inputs, renderInngestEventKeyInput(
|
|
118
|
-
changed = addAppInput(document, inputs, renderInngestSigningKeyInput(
|
|
119
|
-
changed = addAppInput(document, inputs, renderLangfusePublicKeyInput(
|
|
120
|
-
changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput(
|
|
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
|
|
171
|
+
function renderIngressDomainInput() {
|
|
157
172
|
return {
|
|
158
|
-
name:
|
|
173
|
+
name: ingressDomainInputName(),
|
|
159
174
|
type: "string",
|
|
160
175
|
group: "Applications",
|
|
161
|
-
displayName:
|
|
162
|
-
description:
|
|
163
|
-
default:
|
|
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(
|
|
196
|
+
function renderInngestEventKeyInput() {
|
|
182
197
|
return {
|
|
183
|
-
name: inngestEventKeyInputName(
|
|
198
|
+
name: inngestEventKeyInputName(),
|
|
184
199
|
type: "string",
|
|
185
200
|
isSecret: true,
|
|
186
201
|
group: "Applications",
|
|
187
|
-
displayName:
|
|
188
|
-
description:
|
|
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(
|
|
206
|
+
function renderInngestSigningKeyInput() {
|
|
193
207
|
return {
|
|
194
|
-
name: inngestSigningKeyInputName(
|
|
208
|
+
name: inngestSigningKeyInputName(),
|
|
195
209
|
type: "string",
|
|
196
210
|
isSecret: true,
|
|
197
211
|
group: "Applications",
|
|
198
|
-
displayName:
|
|
199
|
-
description:
|
|
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(
|
|
216
|
+
function renderLangfusePublicKeyInput() {
|
|
204
217
|
return {
|
|
205
|
-
name: langfusePublicKeyInputName(
|
|
218
|
+
name: langfusePublicKeyInputName(),
|
|
206
219
|
type: "string",
|
|
207
220
|
group: "Applications",
|
|
208
|
-
displayName:
|
|
209
|
-
description:
|
|
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(
|
|
226
|
+
function renderLangfuseSecretKeyInput() {
|
|
214
227
|
return {
|
|
215
|
-
name: langfuseSecretKeyInputName(
|
|
228
|
+
name: langfuseSecretKeyInputName(),
|
|
216
229
|
type: "string",
|
|
217
230
|
isSecret: true,
|
|
218
231
|
group: "Applications",
|
|
219
|
-
displayName:
|
|
220
|
-
description:
|
|
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(
|
|
291
|
+
valueFromInput: { name: inngestEventKeyInputName() }
|
|
235
292
|
},
|
|
236
293
|
{
|
|
237
294
|
key: "INNGEST_SIGNING_KEY",
|
|
238
295
|
isSecret: true,
|
|
239
|
-
valueFromInput: { name: inngestSigningKeyInputName(
|
|
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(
|
|
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
|
|
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: '
|
|
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
|
-
` - '
|
|
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
|
|
360
|
-
return
|
|
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(
|
|
366
|
-
return
|
|
407
|
+
function inngestEventKeyInputName() {
|
|
408
|
+
return "inngest_event_key";
|
|
367
409
|
}
|
|
368
|
-
function inngestSigningKeyInputName(
|
|
369
|
-
return
|
|
410
|
+
function inngestSigningKeyInputName() {
|
|
411
|
+
return "inngest_signing_key";
|
|
370
412
|
}
|
|
371
|
-
function langfusePublicKeyInputName(
|
|
372
|
-
return
|
|
413
|
+
function langfusePublicKeyInputName() {
|
|
414
|
+
return "langfuse_public_key";
|
|
373
415
|
}
|
|
374
|
-
function langfuseSecretKeyInputName(
|
|
375
|
-
return
|
|
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-
|
|
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
|
@@ -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,
|
|
127
|
-
Inngest
|
|
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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"}
|