@percepta/create 4.0.1 → 4.1.1

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
@@ -31,7 +31,7 @@ The bare command above is the canonical UX. The flags below exist for tests and
31
31
  - `create` (default) — scaffold a new Mosaic package
32
32
  - `add` — add a webapp or library to the current monorepo
33
33
  - `infra register-os-blueprint` — open or update an infra PR for this customer monorepo's OS blueprint
34
- - `infra register-app <app>` — open or update an infra PR for a webapp database
34
+ - `infra register-app <app>` — open or update an infra PR for a webapp service, blueprint installation, and database
35
35
  - `status` — show template sync status for the current app
36
36
  - `sync` — generate downstream sync context (template → app)
37
37
  - `upstream` — generate upstream context (app → template)
@@ -99,15 +99,18 @@ updates an infra PR that writes
99
99
  not create environment installations. It authenticates with `GITHUB_TOKEN`,
100
100
  `GH_TOKEN`, or the GitHub CLI's `gh auth token`.
101
101
 
102
- After adding a webapp, register its app database in that customer OS blueprint:
102
+ After adding a webapp, register it in that customer OS blueprint:
103
103
 
104
104
  ```bash
105
105
  pnpm mosaic infra register-app my-app
106
106
  ```
107
107
 
108
- This opens or updates an infra PR that adds `my-app: {}` to the customer OS
109
- blueprint's `app_databases` default. Merge the OS blueprint PR before
110
- registering apps.
108
+ This opens or updates an infra PR that registers the app service, adds the app
109
+ to the customer OS blueprint's `app_databases` default, and adds the app's
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
114
 
112
115
  ## Development
113
116
 
@@ -5,18 +5,34 @@ const INFRA_REPOSITORY = "Percepta-Core/infra";
5
5
  const INFRA_REPOSITORY_OWNER = "Percepta-Core";
6
6
  const INFRA_BASE_BRANCH = "main";
7
7
  async function createOrUpdateInfraPullRequest(args) {
8
+ return createOrUpdateInfraPullRequestFiles({
9
+ body: args.body,
10
+ branchName: args.branchName,
11
+ files: [{
12
+ baseFileSha: args.baseFileSha,
13
+ content: args.content,
14
+ message: args.message,
15
+ path: args.targetPath
16
+ }],
17
+ github: args.github,
18
+ title: args.title
19
+ });
20
+ }
21
+ async function createOrUpdateInfraPullRequestFiles(args) {
8
22
  const mainSha = await args.github.getBranchSha(INFRA_BASE_BRANCH);
9
23
  if (!mainSha) throw new Error(`Could not find ${INFRA_REPOSITORY}@${INFRA_BASE_BRANCH}.`);
10
24
  if (!await args.github.getBranchSha(args.branchName)) await args.github.createBranch(args.branchName, mainSha);
11
- const branchFile = await args.github.getFile(args.targetPath, args.branchName);
12
25
  const existingPullRequest = await args.github.findOpenPullRequest(args.branchName);
13
- if (branchFile?.content !== args.content) await args.github.putFile({
14
- path: args.targetPath,
15
- branch: args.branchName,
16
- content: args.content,
17
- message: args.message,
18
- sha: branchFile?.sha ?? args.baseFileSha
19
- });
26
+ for (const file of args.files) {
27
+ const branchFile = await args.github.getFile(file.path, args.branchName);
28
+ if (branchFile?.content !== file.content) await args.github.putFile({
29
+ path: file.path,
30
+ branch: args.branchName,
31
+ content: file.content,
32
+ message: file.message,
33
+ sha: branchFile?.sha ?? file.baseFileSha
34
+ });
35
+ }
20
36
  return {
21
37
  pullRequestUrl: (existingPullRequest ?? await args.github.createPullRequest({
22
38
  branch: args.branchName,
@@ -126,6 +142,6 @@ function encodePathSegments(filePath) {
126
142
  return filePath.split("/").map(encodeURIComponent).join("/");
127
143
  }
128
144
  //#endregion
129
- export { resolveGitHubToken as a, createOrUpdateInfraPullRequest as i, INFRA_REPOSITORY as n, createInfraGitHubApi as r, INFRA_BASE_BRANCH as t };
145
+ export { createOrUpdateInfraPullRequestFiles as a, createOrUpdateInfraPullRequest as i, INFRA_REPOSITORY as n, resolveGitHubToken as o, createInfraGitHubApi as r, INFRA_BASE_BRANCH as t };
130
146
 
131
- //# sourceMappingURL=github-RCIMUq70.js.map
147
+ //# sourceMappingURL=github-D3YOEl91.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-D3YOEl91.js","names":[],"sources":["../src/commands/infra/github.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\nconst GITHUB_API_BASE_URL = \"https://api.github.com\";\n\nexport const INFRA_REPOSITORY = \"Percepta-Core/infra\";\nexport const INFRA_REPOSITORY_OWNER = \"Percepta-Core\";\nexport const INFRA_BASE_BRANCH = \"main\";\n\nexport interface GitHubFile {\n sha: string;\n content: string;\n}\n\nexport interface GitHubPullRequest {\n number: number;\n url: string;\n}\n\nexport interface InfraGitHubApi {\n getFile(path: string, ref: string): Promise<GitHubFile | null>;\n getBranchSha(branch: string): Promise<string | null>;\n createBranch(branch: string, sha: string): Promise<void>;\n putFile(args: {\n path: string;\n branch: string;\n content: string;\n message: string;\n sha?: string;\n }): Promise<void>;\n findOpenPullRequest(branch: string): Promise<GitHubPullRequest | null>;\n createPullRequest(args: {\n branch: string;\n title: string;\n body: string;\n }): Promise<GitHubPullRequest>;\n}\n\nexport interface InfraPullRequestResult {\n pullRequestUrl: string;\n status: \"created_pr\" | \"updated_pr\";\n}\n\nexport interface InfraPullRequestFile {\n baseFileSha?: string;\n content: string;\n message: string;\n path: string;\n}\n\nexport async function createOrUpdateInfraPullRequest(args: {\n baseFileSha?: string;\n body: string;\n branchName: string;\n content: string;\n github: InfraGitHubApi;\n message: string;\n targetPath: string;\n title: string;\n}): Promise<InfraPullRequestResult> {\n return createOrUpdateInfraPullRequestFiles({\n body: args.body,\n branchName: args.branchName,\n files: [\n {\n baseFileSha: args.baseFileSha,\n content: args.content,\n message: args.message,\n path: args.targetPath,\n },\n ],\n github: args.github,\n title: args.title,\n });\n}\n\nexport async function createOrUpdateInfraPullRequestFiles(args: {\n body: string;\n branchName: string;\n files: InfraPullRequestFile[];\n github: InfraGitHubApi;\n title: string;\n}): Promise<InfraPullRequestResult> {\n const mainSha = await args.github.getBranchSha(INFRA_BASE_BRANCH);\n if (!mainSha) {\n throw new Error(`Could not find ${INFRA_REPOSITORY}@${INFRA_BASE_BRANCH}.`);\n }\n\n if (!(await args.github.getBranchSha(args.branchName))) {\n await args.github.createBranch(args.branchName, mainSha);\n }\n\n const existingPullRequest = await args.github.findOpenPullRequest(\n args.branchName,\n );\n\n for (const file of args.files) {\n const branchFile = await args.github.getFile(file.path, args.branchName);\n if (branchFile?.content !== file.content) {\n await args.github.putFile({\n path: file.path,\n branch: args.branchName,\n content: file.content,\n message: file.message,\n sha: branchFile?.sha ?? file.baseFileSha,\n });\n }\n }\n\n const pullRequest =\n existingPullRequest ??\n (await args.github.createPullRequest({\n branch: args.branchName,\n title: args.title,\n body: args.body,\n }));\n\n return {\n pullRequestUrl: pullRequest.url,\n status: existingPullRequest ? \"updated_pr\" : \"created_pr\",\n };\n}\n\nexport function createInfraGitHubApi(token: string): InfraGitHubApi {\n return {\n async getFile(filePath, ref) {\n const encodedPath = encodePathSegments(filePath);\n const response = await requestGitHub<{\n content?: unknown;\n sha?: unknown;\n type?: unknown;\n }>(\n token,\n \"GET\",\n `/repos/${INFRA_REPOSITORY}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`,\n );\n if (response == null) return null;\n if (\n typeof response.sha !== \"string\" ||\n typeof response.content !== \"string\" ||\n response.type !== \"file\"\n ) {\n throw new Error(\n `Unexpected GitHub content response for ${filePath} in ${INFRA_REPOSITORY}.`,\n );\n }\n\n return {\n sha: response.sha,\n content: Buffer.from(\n response.content.replace(/\\s/g, \"\"),\n \"base64\",\n ).toString(\"utf-8\"),\n };\n },\n\n async getBranchSha(branch) {\n const response = await requestGitHub<{\n object?: { sha?: unknown };\n }>(token, \"GET\", `/repos/${INFRA_REPOSITORY}/git/ref/heads/${branch}`);\n if (response == null) return null;\n const sha = response.object?.sha;\n if (typeof sha !== \"string\") {\n throw new Error(`Unexpected GitHub ref response for ${branch}.`);\n }\n return sha;\n },\n\n async createBranch(branch, sha) {\n await requestGitHub(\n token,\n \"POST\",\n `/repos/${INFRA_REPOSITORY}/git/refs`,\n {\n ref: `refs/heads/${branch}`,\n sha,\n },\n );\n },\n\n async putFile({ path: filePath, branch, content, message, sha }) {\n const encodedPath = encodePathSegments(filePath);\n await requestGitHub(\n token,\n \"PUT\",\n `/repos/${INFRA_REPOSITORY}/contents/${encodedPath}`,\n {\n branch,\n content: Buffer.from(content, \"utf-8\").toString(\"base64\"),\n message,\n sha,\n },\n );\n },\n\n async findOpenPullRequest(branch) {\n const params = new URLSearchParams({\n base: INFRA_BASE_BRANCH,\n head: `${INFRA_REPOSITORY_OWNER}:${branch}`,\n state: \"open\",\n });\n const response = await requestGitHub<\n Array<{ html_url?: unknown; number?: unknown }>\n >(token, \"GET\", `/repos/${INFRA_REPOSITORY}/pulls?${params}`);\n const pullRequest = response?.[0];\n if (!pullRequest) return null;\n if (\n typeof pullRequest.html_url !== \"string\" ||\n typeof pullRequest.number !== \"number\"\n ) {\n throw new Error(`Unexpected GitHub pull request response.`);\n }\n return {\n number: pullRequest.number,\n url: pullRequest.html_url,\n };\n },\n\n async createPullRequest({ branch, title, body }) {\n const response = await requestGitHub<{\n html_url?: unknown;\n number?: unknown;\n }>(token, \"POST\", `/repos/${INFRA_REPOSITORY}/pulls`, {\n base: INFRA_BASE_BRANCH,\n body,\n head: branch,\n maintainer_can_modify: true,\n title,\n });\n if (\n !response ||\n typeof response.html_url !== \"string\" ||\n typeof response.number !== \"number\"\n ) {\n throw new Error(`Unexpected GitHub pull request response.`);\n }\n return {\n number: response.number,\n url: response.html_url,\n };\n },\n };\n}\n\nexport function resolveGitHubToken(): string {\n const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;\n if (envToken) return envToken;\n\n try {\n const token = execFileSync(\"gh\", [\"auth\", \"token\"], {\n encoding: \"utf-8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n if (token) return token;\n } catch {\n // Fall through to the actionable error below.\n }\n\n throw new Error(\n \"GitHub auth is required. Set GITHUB_TOKEN or GH_TOKEN, or run `gh auth login`.\",\n );\n}\n\nasync function requestGitHub<T>(\n token: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T | null> {\n const response = await fetch(`${GITHUB_API_BASE_URL}${path}`, {\n method,\n headers: {\n Accept: \"application/vnd.github+json\",\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (response.status === 404) return null;\n\n if (!response.ok) {\n const responseText = await response.text();\n throw new Error(\n `GitHub ${method} ${path} failed with ${response.status}: ${responseText}`,\n );\n }\n\n if (response.status === 204) return null;\n return (await response.json()) as T;\n}\n\nfunction encodePathSegments(filePath: string): string {\n return filePath.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1204,11 +1204,11 @@ program.command("add").description("Add a Mosaic package to the current monorepo
1204
1204
  });
1205
1205
  const infra = program.command("infra").description("Manage Mosaic infra glue");
1206
1206
  infra.command("register-os-blueprint").description("Register this customer monorepo's OS blueprint in infra").action(async () => {
1207
- const { registerOsBlueprintCommand } = await import("./register-os-blueprint-DGjBUZYa.js");
1207
+ const { registerOsBlueprintCommand } = await import("./register-os-blueprint-Cgq1rXzQ.js");
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-C7ZBpAaZ.js");
1211
+ const { registerAppCommand } = await import("./register-app-CprpQn_h.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) => {
@@ -0,0 +1,386 @@
1
+ import { i as toTitleCase, n as toKebabCase, r as toSnakeCase } from "./template-versions-CEIP9vhl.js";
2
+ import { t as validateProjectName } from "./validate-dssldJAj.js";
3
+ import { i as detectMonorepo, t as readWorkspaceManifest } from "./index.js";
4
+ import { a as createOrUpdateInfraPullRequestFiles, n as INFRA_REPOSITORY, o as resolveGitHubToken, r as createInfraGitHubApi, t as INFRA_BASE_BRANCH } from "./github-D3YOEl91.js";
5
+ import path from "node:path";
6
+ import chalk from "chalk";
7
+ import fs from "fs-extra";
8
+ import { isMap, isSeq, parseDocument } from "yaml";
9
+ //#region src/commands/infra/register-app.ts
10
+ async function registerApp(appNameInput, args = {}) {
11
+ const appName = normalizeAppName(appNameInput);
12
+ const monorepoContext = await detectMonorepo(args.cwd ?? process.cwd());
13
+ if (!monorepoContext.found || !monorepoContext.rootDir) throw new Error("Run this command from a Mosaic customer monorepo with a .mosaic-workspace.json file.");
14
+ const customerSlug = (await readWorkspaceManifest(monorepoContext.rootDir))?.customerSlug;
15
+ if (!customerSlug) throw new Error(".mosaic-workspace.json is missing customerSlug. Recreate the monorepo with a current @percepta/create.");
16
+ const github = args.github ?? createInfraGitHubApi(resolveGitHubToken());
17
+ const blueprintName = `${customerSlug}-os`;
18
+ const branchName = `blueberry/register-${customerSlug}-${appName}`;
19
+ const blueprintPath = [
20
+ "ryvn",
21
+ "definitions",
22
+ customerSlug,
23
+ "blueprints",
24
+ `${blueprintName}.blueprint.yaml`
25
+ ].join("/");
26
+ const servicePath = [
27
+ "ryvn",
28
+ "definitions",
29
+ customerSlug,
30
+ "services",
31
+ `${appName}.service.yaml`
32
+ ].join("/");
33
+ const mainBlueprintFile = await github.getFile(blueprintPath, INFRA_BASE_BRANCH);
34
+ if (!mainBlueprintFile) throw new Error(`${blueprintPath} does not exist in ${INFRA_REPOSITORY}. Run \`pnpm mosaic infra register-os-blueprint\` and merge that infra PR first.`);
35
+ const serviceContent = await github.getFile(servicePath, "main") == null ? await readLocalServiceDefinition(monorepoContext.rootDir, appName) : null;
36
+ const blueprintContent = registerAppInBlueprint(mainBlueprintFile.content, appName);
37
+ const files = [];
38
+ if (blueprintContent !== mainBlueprintFile.content) files.push({
39
+ baseFileSha: mainBlueprintFile.sha,
40
+ content: blueprintContent,
41
+ message: `Register ${appName} in ${blueprintName}`,
42
+ path: blueprintPath
43
+ });
44
+ if (serviceContent != null) files.push({
45
+ content: serviceContent,
46
+ message: `Register ${appName} service`,
47
+ path: servicePath
48
+ });
49
+ if (files.length === 0) return {
50
+ appName,
51
+ blueprintName,
52
+ blueprintPath,
53
+ branchName,
54
+ customerSlug,
55
+ pullRequestUrl: null,
56
+ repository: INFRA_REPOSITORY,
57
+ status: "already_registered",
58
+ servicePath,
59
+ targetPath: blueprintPath
60
+ };
61
+ const pullRequest = await createOrUpdateInfraPullRequestFiles({
62
+ branchName,
63
+ github,
64
+ files,
65
+ title: `Register ${appName} app`,
66
+ body: [
67
+ `Registers the ${appName} service and deployment in ${blueprintName}.`,
68
+ "",
69
+ "Generated by `mosaic infra register-app`."
70
+ ].join("\n")
71
+ });
72
+ return {
73
+ appName,
74
+ blueprintName,
75
+ blueprintPath,
76
+ branchName,
77
+ customerSlug,
78
+ pullRequestUrl: pullRequest.pullRequestUrl,
79
+ repository: INFRA_REPOSITORY,
80
+ status: pullRequest.status,
81
+ servicePath,
82
+ targetPath: blueprintPath
83
+ };
84
+ }
85
+ async function registerAppCommand(appName) {
86
+ try {
87
+ const result = await registerApp(appName);
88
+ if (result.status === "already_registered") {
89
+ console.log(chalk.green("✔"), `${result.appName} is already registered in ${result.repository} at`, chalk.cyan(result.targetPath));
90
+ return;
91
+ }
92
+ const verb = result.status === "created_pr" ? "Created" : "Updated existing";
93
+ console.log(chalk.green("✔"), `${verb} infra PR for ${result.appName}:`, chalk.cyan(result.pullRequestUrl));
94
+ } catch (error) {
95
+ console.error(chalk.red("Error:"), error.message);
96
+ process.exit(1);
97
+ }
98
+ }
99
+ function registerAppInBlueprint(blueprintContent, appName) {
100
+ return updateBlueprint(blueprintContent, appName, {
101
+ appDatabase: true,
102
+ appInstallation: true,
103
+ appInputs: true
104
+ });
105
+ }
106
+ function updateBlueprint(blueprintContent, appName, options) {
107
+ const document = parseDocument(blueprintContent);
108
+ if (document.errors.length > 0) throw new Error(`Invalid OS blueprint YAML: ${document.errors.map((error) => error.message).join("; ")}`);
109
+ const spec = document.get("spec", true);
110
+ if (!isMap(spec)) throw new Error("OS blueprint must include a spec map.");
111
+ let changed = false;
112
+ const inputs = spec.get("inputs", true);
113
+ if (!isSeq(inputs)) throw new Error("OS blueprint spec.inputs must be a sequence.");
114
+ if (options.appInputs) {
115
+ changed = addAppInput(document, inputs, renderHostInput(appName)) || changed;
116
+ 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;
121
+ }
122
+ if (options.appDatabase) changed = addAppDatabase(document, inputs, appName) || changed;
123
+ if (options.appInstallation) {
124
+ const installations = spec.get("installations", true);
125
+ if (!isSeq(installations)) throw new Error("OS blueprint spec.installations must be a sequence.");
126
+ changed = addAppInstallation(document, installations, appName) || changed;
127
+ }
128
+ return changed ? document.toString() : blueprintContent;
129
+ }
130
+ function addAppInput(document, inputs, input) {
131
+ if (inputs.items.some((item) => isMap(item) && item.get("name") === input.name)) return false;
132
+ inputs.add(document.createNode(input));
133
+ return true;
134
+ }
135
+ function addAppDatabase(document, inputs, appName) {
136
+ const appDatabasesInput = inputs.items.find((item) => isMap(item) && item.get("name") === "app_databases");
137
+ if (!isMap(appDatabasesInput)) throw new Error("OS blueprint must include an app_databases input.");
138
+ const defaultValue = appDatabasesInput.get("default", true);
139
+ if (!isMap(defaultValue)) throw new Error("OS blueprint app_databases default must be a map.");
140
+ if (defaultValue.has(appName)) return false;
141
+ defaultValue.flow = false;
142
+ const appDatabaseValue = document.createNode({});
143
+ if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;
144
+ defaultValue.set(appName, appDatabaseValue);
145
+ return true;
146
+ }
147
+ function addAppInstallation(document, installations, appName) {
148
+ if (installations.items.some((item) => isMap(item) && item.get("service") === appName)) return false;
149
+ installations.add(document.createNode({
150
+ service: appName,
151
+ env: renderAppInstallationEnv(appName),
152
+ config: renderAppInstallationConfig(appName)
153
+ }));
154
+ return true;
155
+ }
156
+ function renderHostInput(appName) {
157
+ return {
158
+ name: hostInputName(appName),
159
+ type: "string",
160
+ 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 }}`
164
+ };
165
+ }
166
+ function renderBetterAuthSecretInput(appName) {
167
+ return {
168
+ name: betterAuthSecretInputName(appName),
169
+ type: "string",
170
+ isSecret: true,
171
+ group: "Applications",
172
+ displayName: `${toTitleCase(appName)} Better Auth Secret`,
173
+ description: `Generated Better Auth signing secret for ${appName}.`,
174
+ hidden: true,
175
+ generated: {
176
+ type: "random-bytes",
177
+ length: 32
178
+ }
179
+ };
180
+ }
181
+ function renderInngestEventKeyInput(appName) {
182
+ return {
183
+ name: inngestEventKeyInputName(appName),
184
+ type: "string",
185
+ isSecret: true,
186
+ 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: ""
190
+ };
191
+ }
192
+ function renderInngestSigningKeyInput(appName) {
193
+ return {
194
+ name: inngestSigningKeyInputName(appName),
195
+ type: "string",
196
+ isSecret: true,
197
+ 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: ""
201
+ };
202
+ }
203
+ function renderLangfusePublicKeyInput(appName) {
204
+ return {
205
+ name: langfusePublicKeyInputName(appName),
206
+ type: "string",
207
+ group: "Applications",
208
+ displayName: `${toTitleCase(appName)} Langfuse Public Key`,
209
+ description: `Langfuse public key for ${appName}. Leave empty to disable Langfuse export.`,
210
+ default: ""
211
+ };
212
+ }
213
+ function renderLangfuseSecretKeyInput(appName) {
214
+ return {
215
+ name: langfuseSecretKeyInputName(appName),
216
+ type: "string",
217
+ isSecret: true,
218
+ group: "Applications",
219
+ displayName: `${toTitleCase(appName)} Langfuse Secret Key`,
220
+ description: `Langfuse secret key for ${appName}. Leave empty to disable Langfuse export.`,
221
+ default: ""
222
+ };
223
+ }
224
+ function renderAppInstallationEnv(appName) {
225
+ return [
226
+ {
227
+ key: "BETTER_AUTH_SECRET",
228
+ isSecret: true,
229
+ valueFromInput: { name: betterAuthSecretInputName(appName) }
230
+ },
231
+ {
232
+ key: "INNGEST_EVENT_KEY",
233
+ isSecret: true,
234
+ valueFromInput: { name: inngestEventKeyInputName(appName) }
235
+ },
236
+ {
237
+ key: "INNGEST_SIGNING_KEY",
238
+ isSecret: true,
239
+ valueFromInput: { name: inngestSigningKeyInputName(appName) }
240
+ },
241
+ {
242
+ key: "LANGFUSE_SECRET_KEY",
243
+ isSecret: true,
244
+ valueFromInput: { name: langfuseSecretKeyInputName(appName) }
245
+ }
246
+ ];
247
+ }
248
+ function renderAppInstallationConfig(appName) {
249
+ const hostInput = hostInputName(appName);
250
+ const langfusePublicKeyInput = langfusePublicKeyInputName(appName);
251
+ return [
252
+ "replicaCount: 1",
253
+ "",
254
+ "service:",
255
+ " port: 3000",
256
+ "",
257
+ "livenessEnabled: true",
258
+ "readinessEnabled: true",
259
+ "startupEnabled: true",
260
+ "",
261
+ "resources:",
262
+ " requests:",
263
+ " cpu: \"100m\"",
264
+ " memory: 256Mi",
265
+ " limits:",
266
+ " cpu: \"500m\"",
267
+ " memory: 512Mi",
268
+ "",
269
+ "ingress:",
270
+ " enabled: true",
271
+ " className: external-nginx",
272
+ " annotations:",
273
+ " cert-manager.io/cluster-issuer: external-issuer",
274
+ " nginx.ingress.kubernetes.io/ssl-redirect: \"true\"",
275
+ " hosts:",
276
+ ` - host: '{{ input "${hostInput}" }}'`,
277
+ " paths:",
278
+ " - path: /",
279
+ " pathType: Prefix",
280
+ " tls:",
281
+ ` - secretName: ${appName}-tls`,
282
+ " 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 }}'",
343
+ ""
344
+ ].join("\n");
345
+ }
346
+ async function readLocalServiceDefinition(monorepoRoot, appName) {
347
+ const serviceDefinitionPath = path.join(monorepoRoot, "packages", appName, "deploy", "ryvn", `${appName}.service.yaml`);
348
+ if (!await fs.pathExists(serviceDefinitionPath)) throw new Error(`${serviceDefinitionPath} does not exist. Add the app's Ryvn service definition before registering it in infra.`);
349
+ const content = await fs.readFile(serviceDefinitionPath, "utf-8");
350
+ validateLocalServiceDefinition(content, appName, serviceDefinitionPath);
351
+ return content.endsWith("\n") ? content : `${content}\n`;
352
+ }
353
+ function validateLocalServiceDefinition(content, appName, serviceDefinitionPath) {
354
+ const document = parseDocument(content);
355
+ if (document.errors.length > 0) throw new Error(`Invalid Ryvn service YAML at ${serviceDefinitionPath}: ${document.errors.map((error) => error.message).join("; ")}`);
356
+ const service = document.toJS();
357
+ if (service.kind !== "Service" || service.metadata?.name !== appName) throw new Error(`${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`);
358
+ }
359
+ function hostInputName(appName) {
360
+ return `${toSnakeCase(appName)}_host`;
361
+ }
362
+ function betterAuthSecretInputName(appName) {
363
+ return `${toSnakeCase(appName)}_better_auth_secret`;
364
+ }
365
+ function inngestEventKeyInputName(appName) {
366
+ return `${toSnakeCase(appName)}_inngest_event_key`;
367
+ }
368
+ function inngestSigningKeyInputName(appName) {
369
+ return `${toSnakeCase(appName)}_inngest_signing_key`;
370
+ }
371
+ function langfusePublicKeyInputName(appName) {
372
+ return `${toSnakeCase(appName)}_langfuse_public_key`;
373
+ }
374
+ function langfuseSecretKeyInputName(appName) {
375
+ return `${toSnakeCase(appName)}_langfuse_secret_key`;
376
+ }
377
+ function normalizeAppName(appNameInput) {
378
+ const appName = toKebabCase(appNameInput);
379
+ const validation = validateProjectName(appName);
380
+ if (!validation.valid) throw new Error(`Invalid app name: ${validation.error}`);
381
+ return appName;
382
+ }
383
+ //#endregion
384
+ export { registerAppCommand };
385
+
386
+ //# sourceMappingURL=register-app-CprpQn_h.js.map
@@ -0,0 +1 @@
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":";;;;;;;;;AAmCA,eAAsB,YACpB,cACA,OAGI,EAAE,EACsB;CAC5B,MAAM,UAAU,iBAAiB,aAAa;CAE9C,MAAM,kBAAkB,MAAM,eADlB,KAAK,OAAO,QAAQ,KAAK,CACY;AACjD,KAAI,CAAC,gBAAgB,SAAS,CAAC,gBAAgB,QAC7C,OAAM,IAAI,MACR,uFACD;CAMH,MAAM,gBAAe,MAHW,sBAC9B,gBAAgB,QACjB,GACuC;AACxC,KAAI,CAAC,aACH,OAAM,IAAI,MACR,yGACD;CAGH,MAAM,SAAS,KAAK,UAAU,qBAAqB,oBAAoB,CAAC;CACxE,MAAM,gBAAgB,GAAG,aAAa;CACtC,MAAM,aAAa,sBAAsB,aAAa,GAAG;CACzD,MAAM,gBAAgB;EACpB;EACA;EACA;EACA;EACA,GAAG,cAAc;EAClB,CAAC,KAAK,IAAI;CACX,MAAM,cAAc;EAClB;EACA;EACA;EACA;EACA,GAAG,QAAQ;EACZ,CAAC,KAAK,IAAI;CAEX,MAAM,oBAAoB,MAAM,OAAO,QACrC,eACA,kBACD;AACD,KAAI,CAAC,kBACH,OAAM,IAAI,MACR,GAAG,cAAc,qBAAqB,iBAAiB,kFACxD;CAIH,MAAM,iBACJ,MAF4B,OAAO,QAAQ,aAAA,OAA+B,IAEvD,OACf,MAAM,2BAA2B,gBAAgB,SAAS,QAAQ,GAClE;CACN,MAAM,mBAAmB,uBACvB,kBAAkB,SAClB,QACD;CAED,MAAM,QAAgC,EAAE;AACxC,KAAI,qBAAqB,kBAAkB,QACzC,OAAM,KAAK;EACT,aAAa,kBAAkB;EAC/B,SAAS;EACT,SAAS,YAAY,QAAQ,MAAM;EACnC,MAAM;EACP,CAAC;AAEJ,KAAI,kBAAkB,KACpB,OAAM,KAAK;EACT,SAAS;EACT,SAAS,YAAY,QAAQ;EAC7B,MAAM;EACP,CAAC;AAGJ,KAAI,MAAM,WAAW,EACnB,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB,YAAY;EACZ,QAAQ;EACR;EACA,YAAY;EACb;CAGH,MAAM,cAAc,MAAM,oCAAoC;EAC5D;EACA;EACA;EACA,OAAO,YAAY,QAAQ;EAC3B,MAAM;GACJ,iBAAiB,QAAQ,6BAA6B,cAAc;GACpE;GACA;GACD,CAAC,KAAK,KAAK;EACb,CAAC;AAEF,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,gBAAgB,YAAY;EAC5B,YAAY;EACZ,QAAQ,YAAY;EACpB;EACA,YAAY;EACb;;AAGH,eAAsB,mBAAmB,SAAgC;AACvE,KAAI;EACF,MAAM,SAAS,MAAM,YAAY,QAAQ;AAEzC,MAAI,OAAO,WAAW,sBAAsB;AAC1C,WAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,OAAO,QAAQ,4BAA4B,OAAO,WAAW,MAChE,MAAM,KAAK,OAAO,WAAW,CAC9B;AACD;;EAGF,MAAM,OACJ,OAAO,WAAW,eAAe,YAAY;AAC/C,UAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,KAAK,gBAAgB,OAAO,QAAQ,IACvC,MAAM,KAAK,OAAO,eAAe,CAClC;UACM,OAAO;AACd,UAAQ,MAAM,MAAM,IAAI,SAAS,EAAG,MAAgB,QAAQ;AAC5D,UAAQ,KAAK,EAAE;;;AAenB,SAAgB,uBACd,kBACA,SACQ;AACR,QAAO,gBAAgB,kBAAkB,SAAS;EAChD,aAAa;EACb,iBAAiB;EACjB,WAAW;EACZ,CAAC;;AAGJ,SAAS,gBACP,kBACA,SACA,SAKQ;CACR,MAAM,WAAW,cAAc,iBAAiB;AAChD,KAAI,SAAS,OAAO,SAAS,EAC3B,OAAM,IAAI,MACR,8BAA8B,SAAS,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,KAAK,KAAK,GACvF;CAGH,MAAM,OAAO,SAAS,IAAI,QAAQ,KAAK;AACvC,KAAI,CAAC,MAAM,KAAK,CACd,OAAM,IAAI,MAAM,wCAAwC;CAG1D,IAAI,UAAU;CACd,MAAM,SAAS,KAAK,IAAI,UAAU,KAAK;AACvC,KAAI,CAAC,MAAM,OAAO,CAChB,OAAM,IAAI,MAAM,+CAA+C;AAGjE,KAAI,QAAQ,WAAW;AACrB,YACE,YAAY,UAAU,QAAQ,gBAAgB,QAAQ,CAAC,IAAI;AAC7D,YACE,YAAY,UAAU,QAAQ,4BAA4B,QAAQ,CAAC,IACnE;AACF,YACE,YAAY,UAAU,QAAQ,2BAA2B,QAAQ,CAAC,IAClE;AACF,YACE,YAAY,UAAU,QAAQ,6BAA6B,QAAQ,CAAC,IACpE;AACF,YACE,YAAY,UAAU,QAAQ,6BAA6B,QAAQ,CAAC,IACpE;AACF,YACE,YAAY,UAAU,QAAQ,6BAA6B,QAAQ,CAAC,IACpE;;AAGJ,KAAI,QAAQ,YACV,WAAU,eAAe,UAAU,QAAQ,QAAQ,IAAI;AAGzD,KAAI,QAAQ,iBAAiB;EAC3B,MAAM,gBAAgB,KAAK,IAAI,iBAAiB,KAAK;AACrD,MAAI,CAAC,MAAM,cAAc,CACvB,OAAM,IAAI,MAAM,sDAAsD;AAExE,YAAU,mBAAmB,UAAU,eAAe,QAAQ,IAAI;;AAGpE,QAAO,UAAU,SAAS,UAAU,GAAG;;AAGzC,SAAS,YACP,UACA,QACA,OACS;AACT,KACE,OAAO,MAAM,MAAM,SAAS,MAAM,KAAK,IAAI,KAAK,IAAI,OAAO,KAAK,MAAM,KAAK,CAE3E,QAAO;AAGT,QAAO,IAAI,SAAS,WAAW,MAAM,CAAC;AACtC,QAAO;;AAGT,SAAS,eACP,UACA,QACA,SACS;CACT,MAAM,oBAAoB,OAAO,MAAM,MACpC,SAAS,MAAM,KAAK,IAAI,KAAK,IAAI,OAAO,KAAK,gBAC/C;AACD,KAAI,CAAC,MAAM,kBAAkB,CAC3B,OAAM,IAAI,MAAM,oDAAoD;CAGtE,MAAM,eAAe,kBAAkB,IAAI,WAAW,KAAK;AAC3D,KAAI,CAAC,MAAM,aAAa,CACtB,OAAM,IAAI,MAAM,oDAAoD;AAGtE,KAAI,aAAa,IAAI,QAAQ,CAAE,QAAO;AAEtC,cAAa,OAAO;CACpB,MAAM,mBAAmB,SAAS,WAAW,EAAE,CAAC;AAChD,KAAI,MAAM,iBAAiB,CAAE,kBAAiB,OAAO;AACrD,cAAa,IAAI,SAAS,iBAAiB;AAC3C,QAAO;;AAGT,SAAS,mBACP,UACA,eACA,SACS;AACT,KACE,cAAc,MAAM,MACjB,SAAS,MAAM,KAAK,IAAI,KAAK,IAAI,UAAU,KAAK,QAClD,CAED,QAAO;AAGT,eAAc,IACZ,SAAS,WAAW;EAClB,SAAS;EACT,KAAK,yBAAyB,QAAQ;EACtC,QAAQ,4BAA4B,QAAQ;EAC7C,CAAC,CACH;AACD,QAAO;;AAGT,SAAS,gBAAgB,SAEvB;AACA,QAAO;EACL,MAAM,cAAc,QAAQ;EAC5B,MAAM;EACN,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,oBAAoB,QAAQ;EACzC,SAAS,GAAG,QAAQ;EACrB;;AAGH,SAAS,4BACP,SAC4C;AAC5C,QAAO;EACL,MAAM,0BAA0B,QAAQ;EACxC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,4CAA4C,QAAQ;EACjE,QAAQ;EACR,WAAW;GACT,MAAM;GACN,QAAQ;GACT;EACF;;AAGH,SAAS,2BACP,SAC4C;AAC5C,QAAO;EACL,MAAM,yBAAyB,QAAQ;EACvC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,yBAAyB,QAAQ;EAC9C,SAAS;EACV;;AAGH,SAAS,6BACP,SAC4C;AAC5C,QAAO;EACL,MAAM,2BAA2B,QAAQ;EACzC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,2BAA2B,QAAQ;EAChD,SAAS;EACV;;AAGH,SAAS,6BACP,SAC4C;AAC5C,QAAO;EACL,MAAM,2BAA2B,QAAQ;EACzC,MAAM;EACN,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,2BAA2B,QAAQ;EAChD,SAAS;EACV;;AAGH,SAAS,6BACP,SAC4C;AAC5C,QAAO;EACL,MAAM,2BAA2B,QAAQ;EACzC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa,GAAG,YAAY,QAAQ,CAAC;EACrC,aAAa,2BAA2B,QAAQ;EAChD,SAAS;EACV;;AAGH,SAAS,yBACP,SACgC;AAChC,QAAO;EACL;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,QAAQ,EACzC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,yBAAyB,QAAQ,EACxC;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,QAAQ,EAC1C;GACF;EACD;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,QAAQ,EAC1C;GACF;EACF;;AAGH,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,YAAY,cAAc,QAAQ;CACxC,MAAM,yBAAyB,2BAA2B,QAAQ;AAElE,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,0BAA0B,UAAU;EACpC;EACA;EACA;EACA;EACA,qBAAqB,QAAQ;EAC7B;EACA,wBAAwB,UAAU;EAClC;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kHAAkH,QAAQ;EAC1H;EACA,oHAAoH,QAAQ;EAC5H;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iCAAiC,UAAU;EAC3C;EACA,iCAAiC,UAAU;EAC3C;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,sBAAsB,QAAQ;EAC9B;EACA;EACA;EACA,yBAAyB,uBAAuB;EAChD;EACA,cAAc;EACd;EACA,yEAAyE,QAAQ;EACjF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;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,cAAc,SAAyB;AAC9C,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,0BAA0B,SAAyB;AAC1D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,yBAAyB,SAAyB;AACzD,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,2BAA2B,SAAyB;AAC3D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,2BAA2B,SAAyB;AAC3D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,2BAA2B,SAAyB;AAC3D,QAAO,GAAG,YAAY,QAAQ,CAAC;;AAGjC,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,aAAa;CACzC,MAAM,aAAa,oBAAoB,QAAQ;AAC/C,KAAI,CAAC,WAAW,MACd,OAAM,IAAI,MAAM,qBAAqB,WAAW,QAAQ;AAE1D,QAAO"}
@@ -1,6 +1,6 @@
1
1
  import { i as toTitleCase } from "./template-versions-CEIP9vhl.js";
2
2
  import { i as detectMonorepo, t as readWorkspaceManifest } from "./index.js";
3
- import { a as resolveGitHubToken, i as createOrUpdateInfraPullRequest, n as INFRA_REPOSITORY, r as createInfraGitHubApi, t as INFRA_BASE_BRANCH } from "./github-RCIMUq70.js";
3
+ import { i as createOrUpdateInfraPullRequest, n as INFRA_REPOSITORY, o as resolveGitHubToken, r as createInfraGitHubApi, t as INFRA_BASE_BRANCH } from "./github-D3YOEl91.js";
4
4
  import path from "node:path";
5
5
  import chalk from "chalk";
6
6
  import fs from "fs-extra";
@@ -87,4 +87,4 @@ function getOsBlueprintTemplatePath() {
87
87
  //#endregion
88
88
  export { registerOsBlueprintCommand };
89
89
 
90
- //# sourceMappingURL=register-os-blueprint-DGjBUZYa.js.map
90
+ //# sourceMappingURL=register-os-blueprint-Cgq1rXzQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-os-blueprint-DGjBUZYa.js","names":[],"sources":["../src/commands/infra/register-os-blueprint.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { toTitleCase } from \"../../utils/case-converters.js\";\nimport { detectMonorepo } from \"../../utils/detect-monorepo.js\";\nimport { readWorkspaceManifest } from \"../../utils/workspace-manifest.js\";\nimport {\n createOrUpdateInfraPullRequest,\n createInfraGitHubApi,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n resolveGitHubToken,\n} from \"./github.js\";\n\nconst OS_BLUEPRINT_TEMPLATE = \"os.blueprint.yaml.template\";\n\nexport interface RegisterOsBlueprintResult {\n blueprintName: 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 targetPath: string;\n}\n\nexport async function registerOsBlueprint(\n args: {\n cwd?: string;\n github?: InfraGitHubApi;\n } = {},\n): Promise<RegisterOsBlueprintResult> {\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-${blueprintName}-blueprint`;\n const targetPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n const content = await renderOsBlueprint(customerSlug);\n\n const mainFile = await github.getFile(targetPath, INFRA_BASE_BRANCH);\n if (mainFile) {\n if (mainFile.content === content) {\n return {\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n targetPath,\n };\n }\n\n throw new Error(\n `${targetPath} already exists in ${INFRA_REPOSITORY}. Not overwriting an existing customer OS blueprint.`,\n );\n }\n\n const pullRequest = await createOrUpdateInfraPullRequest({\n branchName,\n content,\n github,\n message: `Register ${blueprintName} blueprint`,\n targetPath,\n title: `Register ${blueprintName} blueprint`,\n body: [\n `Registers the ${blueprintName} OS blueprint for ${customerSlug}.`,\n \"\",\n \"Generated by `mosaic infra register-os-blueprint`.\",\n ].join(\"\\n\"),\n });\n\n return {\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n targetPath,\n };\n}\n\nexport async function registerOsBlueprintCommand(): Promise<void> {\n try {\n const result = await registerOsBlueprint();\n\n if (result.status === \"already_registered\") {\n console.log(\n chalk.green(\"✔\"),\n `${result.blueprintName} 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.blueprintName}:`,\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\nasync function renderOsBlueprint(customerSlug: string): Promise<string> {\n const template = await fs.readFile(getOsBlueprintTemplatePath(), \"utf-8\");\n const customerTitle = toTitleCase(customerSlug);\n\n return template\n .replaceAll(\"__CUSTOMER_SLUG__\", customerSlug)\n .replaceAll(\"__CUSTOMER_TITLE__\", customerTitle);\n}\n\nfunction getOsBlueprintTemplatePath(): string {\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const candidates = [\n path.resolve(currentDir, \"../templates/infra\", OS_BLUEPRINT_TEMPLATE),\n path.resolve(currentDir, \"../../../templates/infra\", OS_BLUEPRINT_TEMPLATE),\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n throw new Error(\n `OS blueprint template not found. Checked: ${candidates.join(\", \")}`,\n );\n}\n"],"mappings":";;;;;;;;AAgBA,MAAM,wBAAwB;AAY9B,eAAsB,oBACpB,OAGI,EAAE,EAC8B;CAEpC,MAAM,kBAAkB,MAAM,eADlB,KAAK,OAAO,QAAQ,KAAK,CACY;AACjD,KAAI,CAAC,gBAAgB,SAAS,CAAC,gBAAgB,QAC7C,OAAM,IAAI,MACR,uFACD;CAMH,MAAM,gBAAe,MAHW,sBAC9B,gBAAgB,QACjB,GACuC;AACxC,KAAI,CAAC,aACH,OAAM,IAAI,MACR,yGACD;CAGH,MAAM,SAAS,KAAK,UAAU,qBAAqB,oBAAoB,CAAC;CACxE,MAAM,gBAAgB,GAAG,aAAa;CACtC,MAAM,aAAa,sBAAsB,cAAc;CACvD,MAAM,aAAa;EACjB;EACA;EACA;EACA;EACA,GAAG,cAAc;EAClB,CAAC,KAAK,IAAI;CACX,MAAM,UAAU,MAAM,kBAAkB,aAAa;CAErD,MAAM,WAAW,MAAM,OAAO,QAAQ,YAAY,kBAAkB;AACpE,KAAI,UAAU;AACZ,MAAI,SAAS,YAAY,QACvB,QAAO;GACL;GACA;GACA;GACA,gBAAgB;GAChB,YAAY;GACZ,QAAQ;GACR;GACD;AAGH,QAAM,IAAI,MACR,GAAG,WAAW,qBAAqB,iBAAiB,sDACrD;;CAGH,MAAM,cAAc,MAAM,+BAA+B;EACvD;EACA;EACA;EACA,SAAS,YAAY,cAAc;EACnC;EACA,OAAO,YAAY,cAAc;EACjC,MAAM;GACJ,iBAAiB,cAAc,oBAAoB,aAAa;GAChE;GACA;GACD,CAAC,KAAK,KAAK;EACb,CAAC;AAEF,QAAO;EACL;EACA;EACA;EACA,gBAAgB,YAAY;EAC5B,YAAY;EACZ,QAAQ,YAAY;EACpB;EACD;;AAGH,eAAsB,6BAA4C;AAChE,KAAI;EACF,MAAM,SAAS,MAAM,qBAAqB;AAE1C,MAAI,OAAO,WAAW,sBAAsB;AAC1C,WAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,OAAO,cAAc,4BAA4B,OAAO,WAAW,MACtE,MAAM,KAAK,OAAO,WAAW,CAC9B;AACD;;EAGF,MAAM,OACJ,OAAO,WAAW,eAAe,YAAY;AAC/C,UAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,KAAK,gBAAgB,OAAO,cAAc,IAC7C,MAAM,KAAK,OAAO,eAAe,CAClC;UACM,OAAO;AACd,UAAQ,MAAM,MAAM,IAAI,SAAS,EAAG,MAAgB,QAAQ;AAC5D,UAAQ,KAAK,EAAE;;;AAInB,eAAe,kBAAkB,cAAuC;CACtE,MAAM,WAAW,MAAM,GAAG,SAAS,4BAA4B,EAAE,QAAQ;CACzE,MAAM,gBAAgB,YAAY,aAAa;AAE/C,QAAO,SACJ,WAAW,qBAAqB,aAAa,CAC7C,WAAW,sBAAsB,cAAc;;AAGpD,SAAS,6BAAqC;CAC5C,MAAM,aAAa,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC/D,MAAM,aAAa,CACjB,KAAK,QAAQ,YAAY,sBAAsB,sBAAsB,EACrE,KAAK,QAAQ,YAAY,4BAA4B,sBAAsB,CAC5E;AAED,MAAK,MAAM,aAAa,WACtB,KAAI,GAAG,WAAW,UAAU,CAAE,QAAO;AAGvC,OAAM,IAAI,MACR,6CAA6C,WAAW,KAAK,KAAK,GACnE"}
1
+ {"version":3,"file":"register-os-blueprint-Cgq1rXzQ.js","names":[],"sources":["../src/commands/infra/register-os-blueprint.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { toTitleCase } from \"../../utils/case-converters.js\";\nimport { detectMonorepo } from \"../../utils/detect-monorepo.js\";\nimport { readWorkspaceManifest } from \"../../utils/workspace-manifest.js\";\nimport {\n createOrUpdateInfraPullRequest,\n createInfraGitHubApi,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n resolveGitHubToken,\n} from \"./github.js\";\n\nconst OS_BLUEPRINT_TEMPLATE = \"os.blueprint.yaml.template\";\n\nexport interface RegisterOsBlueprintResult {\n blueprintName: 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 targetPath: string;\n}\n\nexport async function registerOsBlueprint(\n args: {\n cwd?: string;\n github?: InfraGitHubApi;\n } = {},\n): Promise<RegisterOsBlueprintResult> {\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-${blueprintName}-blueprint`;\n const targetPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n const content = await renderOsBlueprint(customerSlug);\n\n const mainFile = await github.getFile(targetPath, INFRA_BASE_BRANCH);\n if (mainFile) {\n if (mainFile.content === content) {\n return {\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n targetPath,\n };\n }\n\n throw new Error(\n `${targetPath} already exists in ${INFRA_REPOSITORY}. Not overwriting an existing customer OS blueprint.`,\n );\n }\n\n const pullRequest = await createOrUpdateInfraPullRequest({\n branchName,\n content,\n github,\n message: `Register ${blueprintName} blueprint`,\n targetPath,\n title: `Register ${blueprintName} blueprint`,\n body: [\n `Registers the ${blueprintName} OS blueprint for ${customerSlug}.`,\n \"\",\n \"Generated by `mosaic infra register-os-blueprint`.\",\n ].join(\"\\n\"),\n });\n\n return {\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n targetPath,\n };\n}\n\nexport async function registerOsBlueprintCommand(): Promise<void> {\n try {\n const result = await registerOsBlueprint();\n\n if (result.status === \"already_registered\") {\n console.log(\n chalk.green(\"✔\"),\n `${result.blueprintName} 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.blueprintName}:`,\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\nasync function renderOsBlueprint(customerSlug: string): Promise<string> {\n const template = await fs.readFile(getOsBlueprintTemplatePath(), \"utf-8\");\n const customerTitle = toTitleCase(customerSlug);\n\n return template\n .replaceAll(\"__CUSTOMER_SLUG__\", customerSlug)\n .replaceAll(\"__CUSTOMER_TITLE__\", customerTitle);\n}\n\nfunction getOsBlueprintTemplatePath(): string {\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const candidates = [\n path.resolve(currentDir, \"../templates/infra\", OS_BLUEPRINT_TEMPLATE),\n path.resolve(currentDir, \"../../../templates/infra\", OS_BLUEPRINT_TEMPLATE),\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n throw new Error(\n `OS blueprint template not found. Checked: ${candidates.join(\", \")}`,\n );\n}\n"],"mappings":";;;;;;;;AAgBA,MAAM,wBAAwB;AAY9B,eAAsB,oBACpB,OAGI,EAAE,EAC8B;CAEpC,MAAM,kBAAkB,MAAM,eADlB,KAAK,OAAO,QAAQ,KAAK,CACY;AACjD,KAAI,CAAC,gBAAgB,SAAS,CAAC,gBAAgB,QAC7C,OAAM,IAAI,MACR,uFACD;CAMH,MAAM,gBAAe,MAHW,sBAC9B,gBAAgB,QACjB,GACuC;AACxC,KAAI,CAAC,aACH,OAAM,IAAI,MACR,yGACD;CAGH,MAAM,SAAS,KAAK,UAAU,qBAAqB,oBAAoB,CAAC;CACxE,MAAM,gBAAgB,GAAG,aAAa;CACtC,MAAM,aAAa,sBAAsB,cAAc;CACvD,MAAM,aAAa;EACjB;EACA;EACA;EACA;EACA,GAAG,cAAc;EAClB,CAAC,KAAK,IAAI;CACX,MAAM,UAAU,MAAM,kBAAkB,aAAa;CAErD,MAAM,WAAW,MAAM,OAAO,QAAQ,YAAY,kBAAkB;AACpE,KAAI,UAAU;AACZ,MAAI,SAAS,YAAY,QACvB,QAAO;GACL;GACA;GACA;GACA,gBAAgB;GAChB,YAAY;GACZ,QAAQ;GACR;GACD;AAGH,QAAM,IAAI,MACR,GAAG,WAAW,qBAAqB,iBAAiB,sDACrD;;CAGH,MAAM,cAAc,MAAM,+BAA+B;EACvD;EACA;EACA;EACA,SAAS,YAAY,cAAc;EACnC;EACA,OAAO,YAAY,cAAc;EACjC,MAAM;GACJ,iBAAiB,cAAc,oBAAoB,aAAa;GAChE;GACA;GACD,CAAC,KAAK,KAAK;EACb,CAAC;AAEF,QAAO;EACL;EACA;EACA;EACA,gBAAgB,YAAY;EAC5B,YAAY;EACZ,QAAQ,YAAY;EACpB;EACD;;AAGH,eAAsB,6BAA4C;AAChE,KAAI;EACF,MAAM,SAAS,MAAM,qBAAqB;AAE1C,MAAI,OAAO,WAAW,sBAAsB;AAC1C,WAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,OAAO,cAAc,4BAA4B,OAAO,WAAW,MACtE,MAAM,KAAK,OAAO,WAAW,CAC9B;AACD;;EAGF,MAAM,OACJ,OAAO,WAAW,eAAe,YAAY;AAC/C,UAAQ,IACN,MAAM,MAAM,IAAI,EAChB,GAAG,KAAK,gBAAgB,OAAO,cAAc,IAC7C,MAAM,KAAK,OAAO,eAAe,CAClC;UACM,OAAO;AACd,UAAQ,MAAM,MAAM,IAAI,SAAS,EAAG,MAAgB,QAAQ;AAC5D,UAAQ,KAAK,EAAE;;;AAInB,eAAe,kBAAkB,cAAuC;CACtE,MAAM,WAAW,MAAM,GAAG,SAAS,4BAA4B,EAAE,QAAQ;CACzE,MAAM,gBAAgB,YAAY,aAAa;AAE/C,QAAO,SACJ,WAAW,qBAAqB,aAAa,CAC7C,WAAW,sBAAsB,cAAc;;AAGpD,SAAS,6BAAqC;CAC5C,MAAM,aAAa,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC/D,MAAM,aAAa,CACjB,KAAK,QAAQ,YAAY,sBAAsB,sBAAsB,EACrE,KAAK,QAAQ,YAAY,4BAA4B,sBAAsB,CAC5E;AAED,MAAK,MAAM,aAAa,WACtB,KAAI,GAAG,WAAW,UAAU,CAAE,QAAO;AAGvC,OAAM,IAAI,MACR,6CAA6C,WAAW,KAAK,KAAK,GACnE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "4.0.1",
3
+ "version": "4.1.1",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -113,12 +113,15 @@ updates an infra PR that writes
113
113
  not create environment installations. It authenticates with `GITHUB_TOKEN`,
114
114
  `GH_TOKEN`, or the GitHub CLI's `gh auth token`.
115
115
 
116
- After adding a webapp, register its app database in the customer OS blueprint:
116
+ After adding a webapp, register it in the customer OS blueprint:
117
117
 
118
118
  ```bash
119
119
  pnpm mosaic infra register-app my-app
120
120
  ```
121
121
 
122
- This opens or updates an infra PR that adds `my-app: {}` to the customer OS
123
- blueprint's `app_databases` default. Merge the OS blueprint PR before
124
- registering apps.
122
+ This opens or updates an infra PR that registers the app service, adds the app
123
+ to the customer OS blueprint's `app_databases` default, and adds the app's
124
+ server installation to the customer OS blueprint. The generated installation
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.
@@ -1 +0,0 @@
1
- {"version":3,"file":"github-RCIMUq70.js","names":[],"sources":["../src/commands/infra/github.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\nconst GITHUB_API_BASE_URL = \"https://api.github.com\";\n\nexport const INFRA_REPOSITORY = \"Percepta-Core/infra\";\nexport const INFRA_REPOSITORY_OWNER = \"Percepta-Core\";\nexport const INFRA_BASE_BRANCH = \"main\";\n\nexport interface GitHubFile {\n sha: string;\n content: string;\n}\n\nexport interface GitHubPullRequest {\n number: number;\n url: string;\n}\n\nexport interface InfraGitHubApi {\n getFile(path: string, ref: string): Promise<GitHubFile | null>;\n getBranchSha(branch: string): Promise<string | null>;\n createBranch(branch: string, sha: string): Promise<void>;\n putFile(args: {\n path: string;\n branch: string;\n content: string;\n message: string;\n sha?: string;\n }): Promise<void>;\n findOpenPullRequest(branch: string): Promise<GitHubPullRequest | null>;\n createPullRequest(args: {\n branch: string;\n title: string;\n body: string;\n }): Promise<GitHubPullRequest>;\n}\n\nexport interface InfraPullRequestResult {\n pullRequestUrl: string;\n status: \"created_pr\" | \"updated_pr\";\n}\n\nexport async function createOrUpdateInfraPullRequest(args: {\n baseFileSha?: string;\n body: string;\n branchName: string;\n content: string;\n github: InfraGitHubApi;\n message: string;\n targetPath: string;\n title: string;\n}): Promise<InfraPullRequestResult> {\n const mainSha = await args.github.getBranchSha(INFRA_BASE_BRANCH);\n if (!mainSha) {\n throw new Error(`Could not find ${INFRA_REPOSITORY}@${INFRA_BASE_BRANCH}.`);\n }\n\n if (!(await args.github.getBranchSha(args.branchName))) {\n await args.github.createBranch(args.branchName, mainSha);\n }\n\n const branchFile = await args.github.getFile(\n args.targetPath,\n args.branchName,\n );\n const existingPullRequest = await args.github.findOpenPullRequest(\n args.branchName,\n );\n if (branchFile?.content !== args.content) {\n await args.github.putFile({\n path: args.targetPath,\n branch: args.branchName,\n content: args.content,\n message: args.message,\n sha: branchFile?.sha ?? args.baseFileSha,\n });\n }\n\n const pullRequest =\n existingPullRequest ??\n (await args.github.createPullRequest({\n branch: args.branchName,\n title: args.title,\n body: args.body,\n }));\n\n return {\n pullRequestUrl: pullRequest.url,\n status: existingPullRequest ? \"updated_pr\" : \"created_pr\",\n };\n}\n\nexport function createInfraGitHubApi(token: string): InfraGitHubApi {\n return {\n async getFile(filePath, ref) {\n const encodedPath = encodePathSegments(filePath);\n const response = await requestGitHub<{\n content?: unknown;\n sha?: unknown;\n type?: unknown;\n }>(\n token,\n \"GET\",\n `/repos/${INFRA_REPOSITORY}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`,\n );\n if (response == null) return null;\n if (\n typeof response.sha !== \"string\" ||\n typeof response.content !== \"string\" ||\n response.type !== \"file\"\n ) {\n throw new Error(\n `Unexpected GitHub content response for ${filePath} in ${INFRA_REPOSITORY}.`,\n );\n }\n\n return {\n sha: response.sha,\n content: Buffer.from(\n response.content.replace(/\\s/g, \"\"),\n \"base64\",\n ).toString(\"utf-8\"),\n };\n },\n\n async getBranchSha(branch) {\n const response = await requestGitHub<{\n object?: { sha?: unknown };\n }>(token, \"GET\", `/repos/${INFRA_REPOSITORY}/git/ref/heads/${branch}`);\n if (response == null) return null;\n const sha = response.object?.sha;\n if (typeof sha !== \"string\") {\n throw new Error(`Unexpected GitHub ref response for ${branch}.`);\n }\n return sha;\n },\n\n async createBranch(branch, sha) {\n await requestGitHub(\n token,\n \"POST\",\n `/repos/${INFRA_REPOSITORY}/git/refs`,\n {\n ref: `refs/heads/${branch}`,\n sha,\n },\n );\n },\n\n async putFile({ path: filePath, branch, content, message, sha }) {\n const encodedPath = encodePathSegments(filePath);\n await requestGitHub(\n token,\n \"PUT\",\n `/repos/${INFRA_REPOSITORY}/contents/${encodedPath}`,\n {\n branch,\n content: Buffer.from(content, \"utf-8\").toString(\"base64\"),\n message,\n sha,\n },\n );\n },\n\n async findOpenPullRequest(branch) {\n const params = new URLSearchParams({\n base: INFRA_BASE_BRANCH,\n head: `${INFRA_REPOSITORY_OWNER}:${branch}`,\n state: \"open\",\n });\n const response = await requestGitHub<\n Array<{ html_url?: unknown; number?: unknown }>\n >(token, \"GET\", `/repos/${INFRA_REPOSITORY}/pulls?${params}`);\n const pullRequest = response?.[0];\n if (!pullRequest) return null;\n if (\n typeof pullRequest.html_url !== \"string\" ||\n typeof pullRequest.number !== \"number\"\n ) {\n throw new Error(`Unexpected GitHub pull request response.`);\n }\n return {\n number: pullRequest.number,\n url: pullRequest.html_url,\n };\n },\n\n async createPullRequest({ branch, title, body }) {\n const response = await requestGitHub<{\n html_url?: unknown;\n number?: unknown;\n }>(token, \"POST\", `/repos/${INFRA_REPOSITORY}/pulls`, {\n base: INFRA_BASE_BRANCH,\n body,\n head: branch,\n maintainer_can_modify: true,\n title,\n });\n if (\n !response ||\n typeof response.html_url !== \"string\" ||\n typeof response.number !== \"number\"\n ) {\n throw new Error(`Unexpected GitHub pull request response.`);\n }\n return {\n number: response.number,\n url: response.html_url,\n };\n },\n };\n}\n\nexport function resolveGitHubToken(): string {\n const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;\n if (envToken) return envToken;\n\n try {\n const token = execFileSync(\"gh\", [\"auth\", \"token\"], {\n encoding: \"utf-8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n if (token) return token;\n } catch {\n // Fall through to the actionable error below.\n }\n\n throw new Error(\n \"GitHub auth is required. Set GITHUB_TOKEN or GH_TOKEN, or run `gh auth login`.\",\n );\n}\n\nasync function requestGitHub<T>(\n token: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T | null> {\n const response = await fetch(`${GITHUB_API_BASE_URL}${path}`, {\n method,\n headers: {\n Accept: \"application/vnd.github+json\",\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (response.status === 404) return null;\n\n if (!response.ok) {\n const responseText = await response.text();\n throw new Error(\n `GitHub ${method} ${path} failed with ${response.status}: ${responseText}`,\n );\n }\n\n if (response.status === 204) return null;\n return (await response.json()) as T;\n}\n\nfunction encodePathSegments(filePath: string): string {\n return filePath.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n"],"mappings":"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"}
@@ -1,103 +0,0 @@
1
- import { n as toKebabCase } from "./template-versions-CEIP9vhl.js";
2
- import { t as validateProjectName } from "./validate-dssldJAj.js";
3
- import { i as detectMonorepo, t as readWorkspaceManifest } from "./index.js";
4
- import { a as resolveGitHubToken, i as createOrUpdateInfraPullRequest, n as INFRA_REPOSITORY, r as createInfraGitHubApi, t as INFRA_BASE_BRANCH } from "./github-RCIMUq70.js";
5
- import chalk from "chalk";
6
- import { isMap, isSeq, parseDocument } from "yaml";
7
- //#region src/commands/infra/register-app.ts
8
- async function registerApp(appNameInput, args = {}) {
9
- const appName = normalizeAppName(appNameInput);
10
- const monorepoContext = await detectMonorepo(args.cwd ?? process.cwd());
11
- if (!monorepoContext.found || !monorepoContext.rootDir) throw new Error("Run this command from a Mosaic customer monorepo with a .mosaic-workspace.json file.");
12
- const customerSlug = (await readWorkspaceManifest(monorepoContext.rootDir))?.customerSlug;
13
- if (!customerSlug) throw new Error(".mosaic-workspace.json is missing customerSlug. Recreate the monorepo with a current @percepta/create.");
14
- const github = args.github ?? createInfraGitHubApi(resolveGitHubToken());
15
- const blueprintName = `${customerSlug}-os`;
16
- const branchName = `blueberry/register-${customerSlug}-${appName}-database`;
17
- const targetPath = [
18
- "ryvn",
19
- "definitions",
20
- customerSlug,
21
- "blueprints",
22
- `${blueprintName}.blueprint.yaml`
23
- ].join("/");
24
- const mainFile = await github.getFile(targetPath, INFRA_BASE_BRANCH);
25
- if (!mainFile) throw new Error(`${targetPath} does not exist in ${INFRA_REPOSITORY}. Run \`pnpm mosaic infra register-os-blueprint\` and merge that infra PR first.`);
26
- const content = addAppDatabaseToBlueprint(mainFile.content, appName);
27
- if (content === mainFile.content) return {
28
- appName,
29
- blueprintName,
30
- branchName,
31
- customerSlug,
32
- pullRequestUrl: null,
33
- repository: INFRA_REPOSITORY,
34
- status: "already_registered",
35
- targetPath
36
- };
37
- const pullRequest = await createOrUpdateInfraPullRequest({
38
- baseFileSha: mainFile.sha,
39
- branchName,
40
- content,
41
- github,
42
- message: `Register ${appName} app database`,
43
- targetPath,
44
- title: `Register ${appName} app database`,
45
- body: [
46
- `Registers the ${appName} app database in ${blueprintName}.`,
47
- "",
48
- "Generated by `mosaic infra register-app`."
49
- ].join("\n")
50
- });
51
- return {
52
- appName,
53
- blueprintName,
54
- branchName,
55
- customerSlug,
56
- pullRequestUrl: pullRequest.pullRequestUrl,
57
- repository: INFRA_REPOSITORY,
58
- status: pullRequest.status,
59
- targetPath
60
- };
61
- }
62
- async function registerAppCommand(appName) {
63
- try {
64
- const result = await registerApp(appName);
65
- if (result.status === "already_registered") {
66
- console.log(chalk.green("✔"), `${result.appName} is already registered in ${result.repository} at`, chalk.cyan(result.targetPath));
67
- return;
68
- }
69
- const verb = result.status === "created_pr" ? "Created" : "Updated existing";
70
- console.log(chalk.green("✔"), `${verb} infra PR for ${result.appName}:`, chalk.cyan(result.pullRequestUrl));
71
- } catch (error) {
72
- console.error(chalk.red("Error:"), error.message);
73
- process.exit(1);
74
- }
75
- }
76
- function addAppDatabaseToBlueprint(blueprintContent, appName) {
77
- const document = parseDocument(blueprintContent);
78
- if (document.errors.length > 0) throw new Error(`Invalid OS blueprint YAML: ${document.errors.map((error) => error.message).join("; ")}`);
79
- const spec = document.get("spec", true);
80
- if (!isMap(spec)) throw new Error("OS blueprint must include a spec map.");
81
- const inputs = spec.get("inputs", true);
82
- if (!isSeq(inputs)) throw new Error("OS blueprint spec.inputs must be a sequence.");
83
- const appDatabasesInput = inputs.items.find((item) => isMap(item) && item.get("name") === "app_databases");
84
- if (!isMap(appDatabasesInput)) throw new Error("OS blueprint must include an app_databases input.");
85
- const defaultValue = appDatabasesInput.get("default", true);
86
- if (!isMap(defaultValue)) throw new Error("OS blueprint app_databases default must be a map.");
87
- if (defaultValue.has(appName)) return blueprintContent;
88
- defaultValue.flow = false;
89
- const appDatabaseValue = document.createNode({});
90
- if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;
91
- defaultValue.set(appName, appDatabaseValue);
92
- return document.toString();
93
- }
94
- function normalizeAppName(appNameInput) {
95
- const appName = toKebabCase(appNameInput);
96
- const validation = validateProjectName(appName);
97
- if (!validation.valid) throw new Error(`Invalid app name: ${validation.error}`);
98
- return appName;
99
- }
100
- //#endregion
101
- export { registerAppCommand };
102
-
103
- //# sourceMappingURL=register-app-C7ZBpAaZ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"register-app-C7ZBpAaZ.js","names":[],"sources":["../src/commands/infra/register-app.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport { isMap, isSeq, parseDocument } from \"yaml\";\nimport { toKebabCase } 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 createOrUpdateInfraPullRequest,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n resolveGitHubToken,\n} from \"./github.js\";\n\nexport interface RegisterAppResult {\n appName: string;\n blueprintName: 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 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}-database`;\n const targetPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n\n const mainFile = await github.getFile(targetPath, INFRA_BASE_BRANCH);\n if (!mainFile) {\n throw new Error(\n `${targetPath} does not exist in ${INFRA_REPOSITORY}. Run \\`pnpm mosaic infra register-os-blueprint\\` and merge that infra PR first.`,\n );\n }\n\n const content = addAppDatabaseToBlueprint(mainFile.content, appName);\n if (content === mainFile.content) {\n return {\n appName,\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n targetPath,\n };\n }\n\n const pullRequest = await createOrUpdateInfraPullRequest({\n baseFileSha: mainFile.sha,\n branchName,\n content,\n github,\n message: `Register ${appName} app database`,\n targetPath,\n title: `Register ${appName} app database`,\n body: [\n `Registers the ${appName} app database in ${blueprintName}.`,\n \"\",\n \"Generated by `mosaic infra register-app`.\",\n ].join(\"\\n\"),\n });\n\n return {\n appName,\n blueprintName,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n targetPath,\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 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 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 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 blueprintContent;\n\n defaultValue.flow = false;\n const appDatabaseValue = document.createNode({});\n if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;\n defaultValue.set(appName, appDatabaseValue);\n\n return document.toString();\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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"}