@percepta/create 4.1.0 → 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 +8 -5
- package/dist/{github-RCIMUq70.js → github-D3YOEl91.js} +26 -10
- package/dist/github-D3YOEl91.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/register-app-CprpQn_h.js +386 -0
- package/dist/register-app-CprpQn_h.js.map +1 -0
- package/dist/{register-os-blueprint-DGjBUZYa.js → register-os-blueprint-Cgq1rXzQ.js} +2 -2
- package/dist/{register-os-blueprint-DGjBUZYa.js.map → register-os-blueprint-Cgq1rXzQ.js.map} +1 -1
- package/package.json +1 -1
- package/templates/monorepo/README.md +7 -4
- package/dist/github-RCIMUq70.js.map +0 -1
- package/dist/register-app-C7ZBpAaZ.js +0 -103
- package/dist/register-app-C7ZBpAaZ.js.map +0 -1
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
|
|
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
|
|
109
|
-
blueprint's `app_databases` default
|
|
110
|
-
|
|
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
|
-
|
|
14
|
-
path
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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":"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"}
|
|
@@ -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 {
|
|
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-
|
|
90
|
+
//# sourceMappingURL=register-os-blueprint-Cgq1rXzQ.js.map
|
package/dist/{register-os-blueprint-DGjBUZYa.js.map → register-os-blueprint-Cgq1rXzQ.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-os-blueprint-
|
|
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
|
@@ -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
|
|
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
|
|
123
|
-
blueprint's `app_databases` default
|
|
124
|
-
|
|
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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"}
|