@percepta/create 3.6.2 → 3.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +37 -6
  2. package/dist/{git-ops-C2CIjuce.js → git-ops-BD7JNnal.js} +1 -1
  3. package/dist/{git-ops-C2CIjuce.js.map → git-ops-BD7JNnal.js.map} +1 -1
  4. package/dist/github-RCIMUq70.js +131 -0
  5. package/dist/github-RCIMUq70.js.map +1 -0
  6. package/dist/index.js +63 -122
  7. package/dist/index.js.map +1 -1
  8. package/dist/{init-sI9aIrkU.js → init-COp0nGdk.js} +4 -2
  9. package/dist/{init-sI9aIrkU.js.map → init-COp0nGdk.js.map} +1 -1
  10. package/dist/manifest-CqIDnbgs.js +58 -0
  11. package/dist/manifest-CqIDnbgs.js.map +1 -0
  12. package/dist/register-app-C7ZBpAaZ.js +103 -0
  13. package/dist/register-app-C7ZBpAaZ.js.map +1 -0
  14. package/dist/register-os-blueprint-DGjBUZYa.js +90 -0
  15. package/dist/register-os-blueprint-DGjBUZYa.js.map +1 -0
  16. package/dist/{status-CKe4aKso.js → status-BXYaQ4a2.js} +3 -3
  17. package/dist/{status-CKe4aKso.js.map → status-BXYaQ4a2.js.map} +1 -1
  18. package/dist/{sync-D1vkoofl.js → sync-BayU4w1j.js} +3 -3
  19. package/dist/{sync-D1vkoofl.js.map → sync-BayU4w1j.js.map} +1 -1
  20. package/dist/template-versions-CEIP9vhl.js +35 -0
  21. package/dist/template-versions-CEIP9vhl.js.map +1 -0
  22. package/dist/{upstream-gUHLWSR1.js → upstream-CZEzLrS4.js} +3 -3
  23. package/dist/{upstream-gUHLWSR1.js.map → upstream-CZEzLrS4.js.map} +1 -1
  24. package/dist/validate-dssldJAj.js +14 -0
  25. package/dist/validate-dssldJAj.js.map +1 -0
  26. package/package.json +1 -1
  27. package/template-versions.json +2 -2
  28. package/templates/infra/os.blueprint.yaml.template +138 -0
  29. package/templates/monorepo/README.md +41 -3
  30. package/templates/monorepo/auth/README.md +6 -3
  31. package/templates/monorepo/auth/package.json +2 -4
  32. package/templates/monorepo/auth/src/config/database.ts +1 -1
  33. package/templates/{webapp → monorepo}/docker-compose.yml +2 -2
  34. package/templates/monorepo/package.json.template +5 -2
  35. package/templates/monorepo/scripts/setup-local-databases.mjs +183 -0
  36. package/templates/webapp/AGENTS.md +13 -20
  37. package/templates/webapp/README.md +32 -36
  38. package/templates/webapp/agent-skills/database.md +21 -21
  39. package/templates/webapp/agent-skills/langfuse.md +7 -7
  40. package/templates/webapp/agent-skills/llm.md +4 -2
  41. package/templates/webapp/agent-skills/oneshot.md +7 -6
  42. package/templates/webapp/agent-skills/ryvn.md +12 -16
  43. package/templates/webapp/deploy/README.md +10 -51
  44. package/templates/webapp/drizzle.config.ts +2 -23
  45. package/templates/webapp/env.example.template +8 -14
  46. package/templates/webapp/package.json.template +5 -12
  47. package/templates/webapp/scripts/start.sh +12 -16
  48. package/templates/webapp/src/config/getEnvConfig.ts +4 -10
  49. package/templates/webapp/src/drizzle/db.ts +6 -21
  50. package/templates/webapp/src/startup-checks.ts +28 -7
  51. package/templates/monorepo/auth/scripts/setup-database.ts +0 -11
  52. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +0 -92
  53. package/templates/webapp/agent-skills/deploy.md +0 -92
  54. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +0 -10
  55. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +0 -11
  56. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +0 -154
  57. package/templates/webapp/terraform/README.md +0 -147
  58. package/templates/webapp/terraform/deploy.sh +0 -97
  59. package/templates/webapp/terraform/main.tf +0 -101
  60. package/templates/webapp/terraform/modules/cloudtrail/main.tf +0 -27
  61. package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +0 -10
  62. package/templates/webapp/terraform/modules/cloudtrail/variables.tf +0 -15
  63. package/templates/webapp/terraform/modules/networking/main.tf +0 -118
  64. package/templates/webapp/terraform/modules/networking/outputs.tf +0 -38
  65. package/templates/webapp/terraform/modules/networking/variables.tf +0 -24
  66. package/templates/webapp/terraform/modules/rds/main.tf +0 -227
  67. package/templates/webapp/terraform/modules/rds/outputs.tf +0 -73
  68. package/templates/webapp/terraform/modules/rds/variables.tf +0 -61
  69. package/templates/webapp/terraform/modules/s3-logging/main.tf +0 -148
  70. package/templates/webapp/terraform/modules/s3-logging/outputs.tf +0 -10
  71. package/templates/webapp/terraform/modules/s3-logging/variables.tf +0 -16
  72. package/templates/webapp/terraform/modules/secrets/main.tf +0 -39
  73. package/templates/webapp/terraform/modules/secrets/outputs.tf +0 -9
  74. package/templates/webapp/terraform/modules/secrets/variables.tf +0 -51
  75. package/templates/webapp/terraform/outputs.tf +0 -102
  76. package/templates/webapp/terraform/providers.tf +0 -32
  77. package/templates/webapp/terraform/schema/main.tf +0 -4
  78. package/templates/webapp/terraform/schema/outputs.tf +0 -9
  79. package/templates/webapp/terraform/schema/variables.tf +0 -19
  80. package/templates/webapp/terraform/schema/versions.tf +0 -38
  81. package/templates/webapp/terraform/terraform.tfvars.example +0 -65
  82. package/templates/webapp/terraform/variables.tf +0 -129
package/README.md CHANGED
@@ -8,7 +8,9 @@ Scaffold and manage Mosaic packages.
8
8
  npx @percepta/create
9
9
  ```
10
10
 
11
- That's it. The CLI prompts you for the package type, repo name, and package name as needed. Defaults yield a running app — sign in as `app-admin@example.com` / `password`.
11
+ That's it. The CLI prompts you for the package type, repo name, customer slug,
12
+ and package name as needed. Defaults yield a running app — sign in as
13
+ `app-admin@example.com` / `password`.
12
14
 
13
15
  ## Options (mostly for automation)
14
16
 
@@ -18,6 +20,7 @@ The bare command above is the canonical UX. The flags below exist for tests and
18
20
  | -------------------- | --------------------------------------------------------------------------------------------------------------------- |
19
21
  | `-t, --type <type>` | Package type: `monorepo`, `webapp`, or `library` (skips the type prompt) |
20
22
  | `--name <name>` | Package/app name (skips the package name prompt) |
23
+ | `--customer <slug>` | Customer slug stored in the generated monorepo workspace manifest |
21
24
  | `--repo-name <name>` | Repo name when creating a new monorepo (skips the repo name prompt) |
22
25
  | `--cwd <dir>` | Run as if the CLI was started from `<dir>` |
23
26
  | `--skip-install` | Skip dependency installation, which also skips the auto-run setup + dev + browser, leaving you with manual next-steps |
@@ -27,6 +30,8 @@ The bare command above is the canonical UX. The flags below exist for tests and
27
30
 
28
31
  - `create` (default) — scaffold a new Mosaic package
29
32
  - `add` — add a webapp or library to the current monorepo
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
30
35
  - `status` — show template sync status for the current app
31
36
  - `sync` — generate downstream sync context (template → app)
32
37
  - `upstream` — generate upstream context (app → template)
@@ -36,11 +41,12 @@ The bare command above is the canonical UX. The flags below exist for tests and
36
41
 
37
42
  `create` auto-detects whether you're inside an existing pnpm monorepo (by walking up for `pnpm-workspace.yaml`) and changes its prompts accordingly:
38
43
 
39
- - **Outside a monorepo** — you're asked "Initialize with a webapp?" (Y/n, default Y), then for the repo name. Picking the webapp option also asks for the webapp name and scaffolds it inside `packages/<webapp-name>/`. Declining gives you an empty monorepo.
44
+ - **Outside a monorepo** — you're asked for the repo name, whether to initialize with a webapp (Y/n, default Y), and the customer slug stored in `.mosaic-workspace.json`. Picking the webapp option also asks for the webapp name and scaffolds it inside `packages/<webapp-name>/`. Declining gives you an empty monorepo.
40
45
  - **Inside a monorepo** — pick `Webapp` (default) or `Library` to add a new package under the workspace pattern.
41
46
 
42
- Generated monorepos include a root `.mosaic-workspace.json` and a pinned
43
- `pnpm mosaic` script. Prefer the workspace-owned command when adding packages:
47
+ Generated monorepos include a root `.mosaic-workspace.json` with the customer
48
+ slug plus template compatibility metadata, and a pinned `pnpm mosaic` script.
49
+ Prefer the workspace-owned command when adding packages:
44
50
 
45
51
  ```bash
46
52
  pnpm mosaic add webapp my-app
@@ -56,7 +62,7 @@ newer scaffold.
56
62
  When you scaffold a webapp (the default flow), `create` automatically runs:
57
63
 
58
64
  1. `pnpm install` (at the monorepo root)
59
- 2. `pnpm run setup` — Docker Compose Postgres + Drizzle migrations + seed users
65
+ 2. `pnpm run setup` — root Docker Compose services + local database creation + Drizzle migrations + seed users
60
66
  3. `pnpm dev` — Next.js dev server
61
67
  4. Opens the served URL in your default browser
62
68
 
@@ -76,7 +82,32 @@ The `webapp` template ships with:
76
82
 
77
83
  The `library` template ships with TypeScript, ESLint, and a minimal `src/index.ts`.
78
84
 
79
- The `monorepo` template ships with `pnpm-workspace.yaml`, root scripts (`dev`, `build`, `lint`, `test`), strict `tsconfig.json`, flat-config ESLint, and an empty `packages/` directory.
85
+ The `monorepo` template ships with `pnpm-workspace.yaml`, root local Postgres/SpiceDB Docker Compose, root scripts (`setup`, `dev`, `build`, `lint`, `test`), strict `tsconfig.json`, flat-config ESLint, and an empty `packages/` directory.
86
+
87
+ ## Customer OS blueprint
88
+
89
+ After creating a customer monorepo, register the customer OS blueprint in
90
+ `Percepta-Core/infra`:
91
+
92
+ ```bash
93
+ pnpm mosaic infra register-os-blueprint
94
+ ```
95
+
96
+ The command reads `customerSlug` from `.mosaic-workspace.json`, opens or
97
+ updates an infra PR that writes
98
+ `ryvn/definitions/<customer>/blueprints/<customer>-os.blueprint.yaml`, and does
99
+ not create environment installations. It authenticates with `GITHUB_TOKEN`,
100
+ `GH_TOKEN`, or the GitHub CLI's `gh auth token`.
101
+
102
+ After adding a webapp, register its app database in that customer OS blueprint:
103
+
104
+ ```bash
105
+ pnpm mosaic infra register-app my-app
106
+ ```
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.
80
111
 
81
112
  ## Development
82
113
 
@@ -48,4 +48,4 @@ function getFileAtTag(repoPath, tag, filePath) {
48
48
  //#endregion
49
49
  export { getTemplateVersionFromTag as i, getLatestTemplateTag as n, getTemplateDiff as r, getFileAtTag as t };
50
50
 
51
- //# sourceMappingURL=git-ops-C2CIjuce.js.map
51
+ //# sourceMappingURL=git-ops-BD7JNnal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-ops-C2CIjuce.js","names":[],"sources":["../src/utils/git-ops.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\n// Git requires forward slashes for in-repo paths regardless of OS\nfunction toGitPath(p: string): string {\n return p.replace(/\\\\/g, \"/\");\n}\n\nexport function getLatestTemplateTag(\n type: string,\n repoPath: string,\n): string | null {\n try {\n const tags = execFileSync(\n \"git\",\n [\"tag\", \"-l\", `template/${type}/*`, \"--sort=-v:refname\"],\n { cwd: repoPath, encoding: \"utf-8\" },\n ).trim();\n if (!tags) return null;\n return tags.split(\"\\n\")[0] ?? null;\n } catch {\n return null;\n }\n}\n\nexport function getTemplateVersionFromTag(tag: string): string {\n const parts = tag.split(\"/\");\n return parts[parts.length - 1] ?? \"\";\n}\n\nexport function getTemplateDiff(\n repoPath: string,\n templatePath: string,\n fromTag: string,\n toTag: string,\n): string {\n return execFileSync(\n \"git\",\n [\"diff\", `${fromTag}..${toTag}`, \"--\", toGitPath(templatePath)],\n {\n cwd: repoPath,\n encoding: \"utf-8\",\n },\n );\n}\n\nexport function getFileAtTag(\n repoPath: string,\n tag: string,\n filePath: string,\n): string | null {\n try {\n return execFileSync(\"git\", [\"show\", `${tag}:${toGitPath(filePath)}`], {\n cwd: repoPath,\n encoding: \"utf-8\",\n });\n } catch {\n return null;\n }\n}\n"],"mappings":";;AAGA,SAAS,UAAU,GAAmB;AACpC,QAAO,EAAE,QAAQ,OAAO,IAAI;;AAG9B,SAAgB,qBACd,MACA,UACe;AACf,KAAI;EACF,MAAM,OAAO,aACX,OACA;GAAC;GAAO;GAAM,YAAY,KAAK;GAAK;GAAoB,EACxD;GAAE,KAAK;GAAU,UAAU;GAAS,CACrC,CAAC,MAAM;AACR,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,MAAM,KAAK,CAAC,MAAM;SACxB;AACN,SAAO;;;AAIX,SAAgB,0BAA0B,KAAqB;CAC7D,MAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,QAAO,MAAM,MAAM,SAAS,MAAM;;AAGpC,SAAgB,gBACd,UACA,cACA,SACA,OACQ;AACR,QAAO,aACL,OACA;EAAC;EAAQ,GAAG,QAAQ,IAAI;EAAS;EAAM,UAAU,aAAa;EAAC,EAC/D;EACE,KAAK;EACL,UAAU;EACX,CACF;;AAGH,SAAgB,aACd,UACA,KACA,UACe;AACf,KAAI;AACF,SAAO,aAAa,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,UAAU,SAAS,GAAG,EAAE;GACpE,KAAK;GACL,UAAU;GACX,CAAC;SACI;AACN,SAAO"}
1
+ {"version":3,"file":"git-ops-BD7JNnal.js","names":[],"sources":["../src/utils/git-ops.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\n// Git requires forward slashes for in-repo paths regardless of OS\nfunction toGitPath(p: string): string {\n return p.replace(/\\\\/g, \"/\");\n}\n\nexport function getLatestTemplateTag(\n type: string,\n repoPath: string,\n): string | null {\n try {\n const tags = execFileSync(\n \"git\",\n [\"tag\", \"-l\", `template/${type}/*`, \"--sort=-v:refname\"],\n { cwd: repoPath, encoding: \"utf-8\" },\n ).trim();\n if (!tags) return null;\n return tags.split(\"\\n\")[0] ?? null;\n } catch {\n return null;\n }\n}\n\nexport function getTemplateVersionFromTag(tag: string): string {\n const parts = tag.split(\"/\");\n return parts[parts.length - 1] ?? \"\";\n}\n\nexport function getTemplateDiff(\n repoPath: string,\n templatePath: string,\n fromTag: string,\n toTag: string,\n): string {\n return execFileSync(\n \"git\",\n [\"diff\", `${fromTag}..${toTag}`, \"--\", toGitPath(templatePath)],\n {\n cwd: repoPath,\n encoding: \"utf-8\",\n },\n );\n}\n\nexport function getFileAtTag(\n repoPath: string,\n tag: string,\n filePath: string,\n): string | null {\n try {\n return execFileSync(\"git\", [\"show\", `${tag}:${toGitPath(filePath)}`], {\n cwd: repoPath,\n encoding: \"utf-8\",\n });\n } catch {\n return null;\n }\n}\n"],"mappings":";;AAGA,SAAS,UAAU,GAAmB;AACpC,QAAO,EAAE,QAAQ,OAAO,IAAI;;AAG9B,SAAgB,qBACd,MACA,UACe;AACf,KAAI;EACF,MAAM,OAAO,aACX,OACA;GAAC;GAAO;GAAM,YAAY,KAAK;GAAK;GAAoB,EACxD;GAAE,KAAK;GAAU,UAAU;GAAS,CACrC,CAAC,MAAM;AACR,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,MAAM,KAAK,CAAC,MAAM;SACxB;AACN,SAAO;;;AAIX,SAAgB,0BAA0B,KAAqB;CAC7D,MAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,QAAO,MAAM,MAAM,SAAS,MAAM;;AAGpC,SAAgB,gBACd,UACA,cACA,SACA,OACQ;AACR,QAAO,aACL,OACA;EAAC;EAAQ,GAAG,QAAQ,IAAI;EAAS;EAAM,UAAU,aAAa;EAAC,EAC/D;EACE,KAAK;EACL,UAAU;EACX,CACF;;AAGH,SAAgB,aACd,UACA,KACA,UACe;AACf,KAAI;AACF,SAAO,aAAa,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,UAAU,SAAS,GAAG,EAAE;GACpE,KAAK;GACL,UAAU;GACX,CAAC;SACI;AACN,SAAO"}
@@ -0,0 +1,131 @@
1
+ import { execFileSync } from "node:child_process";
2
+ //#region src/commands/infra/github.ts
3
+ const GITHUB_API_BASE_URL = "https://api.github.com";
4
+ const INFRA_REPOSITORY = "Percepta-Core/infra";
5
+ const INFRA_REPOSITORY_OWNER = "Percepta-Core";
6
+ const INFRA_BASE_BRANCH = "main";
7
+ async function createOrUpdateInfraPullRequest(args) {
8
+ const mainSha = await args.github.getBranchSha(INFRA_BASE_BRANCH);
9
+ if (!mainSha) throw new Error(`Could not find ${INFRA_REPOSITORY}@${INFRA_BASE_BRANCH}.`);
10
+ 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
+ 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
+ });
20
+ return {
21
+ pullRequestUrl: (existingPullRequest ?? await args.github.createPullRequest({
22
+ branch: args.branchName,
23
+ title: args.title,
24
+ body: args.body
25
+ })).url,
26
+ status: existingPullRequest ? "updated_pr" : "created_pr"
27
+ };
28
+ }
29
+ function createInfraGitHubApi(token) {
30
+ return {
31
+ async getFile(filePath, ref) {
32
+ const response = await requestGitHub(token, "GET", `/repos/${INFRA_REPOSITORY}/contents/${encodePathSegments(filePath)}?ref=${encodeURIComponent(ref)}`);
33
+ if (response == null) return null;
34
+ if (typeof response.sha !== "string" || typeof response.content !== "string" || response.type !== "file") throw new Error(`Unexpected GitHub content response for ${filePath} in ${INFRA_REPOSITORY}.`);
35
+ return {
36
+ sha: response.sha,
37
+ content: Buffer.from(response.content.replace(/\s/g, ""), "base64").toString("utf-8")
38
+ };
39
+ },
40
+ async getBranchSha(branch) {
41
+ const response = await requestGitHub(token, "GET", `/repos/${INFRA_REPOSITORY}/git/ref/heads/${branch}`);
42
+ if (response == null) return null;
43
+ const sha = response.object?.sha;
44
+ if (typeof sha !== "string") throw new Error(`Unexpected GitHub ref response for ${branch}.`);
45
+ return sha;
46
+ },
47
+ async createBranch(branch, sha) {
48
+ await requestGitHub(token, "POST", `/repos/${INFRA_REPOSITORY}/git/refs`, {
49
+ ref: `refs/heads/${branch}`,
50
+ sha
51
+ });
52
+ },
53
+ async putFile({ path: filePath, branch, content, message, sha }) {
54
+ await requestGitHub(token, "PUT", `/repos/${INFRA_REPOSITORY}/contents/${encodePathSegments(filePath)}`, {
55
+ branch,
56
+ content: Buffer.from(content, "utf-8").toString("base64"),
57
+ message,
58
+ sha
59
+ });
60
+ },
61
+ async findOpenPullRequest(branch) {
62
+ const pullRequest = (await requestGitHub(token, "GET", `/repos/${INFRA_REPOSITORY}/pulls?${new URLSearchParams({
63
+ base: INFRA_BASE_BRANCH,
64
+ head: `${INFRA_REPOSITORY_OWNER}:${branch}`,
65
+ state: "open"
66
+ })}`))?.[0];
67
+ if (!pullRequest) return null;
68
+ if (typeof pullRequest.html_url !== "string" || typeof pullRequest.number !== "number") throw new Error(`Unexpected GitHub pull request response.`);
69
+ return {
70
+ number: pullRequest.number,
71
+ url: pullRequest.html_url
72
+ };
73
+ },
74
+ async createPullRequest({ branch, title, body }) {
75
+ const response = await requestGitHub(token, "POST", `/repos/${INFRA_REPOSITORY}/pulls`, {
76
+ base: INFRA_BASE_BRANCH,
77
+ body,
78
+ head: branch,
79
+ maintainer_can_modify: true,
80
+ title
81
+ });
82
+ if (!response || typeof response.html_url !== "string" || typeof response.number !== "number") throw new Error(`Unexpected GitHub pull request response.`);
83
+ return {
84
+ number: response.number,
85
+ url: response.html_url
86
+ };
87
+ }
88
+ };
89
+ }
90
+ function resolveGitHubToken() {
91
+ const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
92
+ if (envToken) return envToken;
93
+ try {
94
+ const token = execFileSync("gh", ["auth", "token"], {
95
+ encoding: "utf-8",
96
+ stdio: [
97
+ "ignore",
98
+ "pipe",
99
+ "ignore"
100
+ ]
101
+ }).trim();
102
+ if (token) return token;
103
+ } catch {}
104
+ throw new Error("GitHub auth is required. Set GITHUB_TOKEN or GH_TOKEN, or run `gh auth login`.");
105
+ }
106
+ async function requestGitHub(token, method, path, body) {
107
+ const response = await fetch(`${GITHUB_API_BASE_URL}${path}`, {
108
+ method,
109
+ headers: {
110
+ Accept: "application/vnd.github+json",
111
+ Authorization: `Bearer ${token}`,
112
+ "Content-Type": "application/json",
113
+ "X-GitHub-Api-Version": "2022-11-28"
114
+ },
115
+ body: body === void 0 ? void 0 : JSON.stringify(body)
116
+ });
117
+ if (response.status === 404) return null;
118
+ if (!response.ok) {
119
+ const responseText = await response.text();
120
+ throw new Error(`GitHub ${method} ${path} failed with ${response.status}: ${responseText}`);
121
+ }
122
+ if (response.status === 204) return null;
123
+ return await response.json();
124
+ }
125
+ function encodePathSegments(filePath) {
126
+ return filePath.split("/").map(encodeURIComponent).join("/");
127
+ }
128
+ //#endregion
129
+ export { resolveGitHubToken as a, createOrUpdateInfraPullRequest as i, INFRA_REPOSITORY as n, createInfraGitHubApi as r, INFRA_BASE_BRANCH as t };
130
+
131
+ //# sourceMappingURL=github-RCIMUq70.js.map
@@ -0,0 +1 @@
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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"}
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { i as toTitleCase, n as toKebabCase, r as toSnakeCase, t as getTemplateVersion } from "./template-versions-CEIP9vhl.js";
3
+ import { a as writeManifest, c as isValidMosaicDesignTheme, o as DEFAULT_MOSAIC_DESIGN_THEME, s as VALID_MOSAIC_DESIGN_THEMES, t as derivePlaceholders } from "./manifest-CqIDnbgs.js";
4
+ import { t as validateProjectName } from "./validate-dssldJAj.js";
2
5
  import { program } from "commander";
3
6
  import { execSync, spawn } from "node:child_process";
4
7
  import path from "node:path";
@@ -9,21 +12,6 @@ import { fileURLToPath } from "node:url";
9
12
  import { parse } from "yaml";
10
13
  import { randomBytes } from "node:crypto";
11
14
  import inquirer from "inquirer";
12
- import validateNpmPackageName from "validate-npm-package-name";
13
- //#region src/utils/case-converters.ts
14
- /** Lowercase, hyphenated, npm-package-name-safe form: "My Cool App" → "my-cool-app". */
15
- function toKebabCase(str) {
16
- return str.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
17
- }
18
- /** Display form derived from a kebab-case name: "my-cool-app" → "My Cool App". */
19
- function toTitleCase(str) {
20
- return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
21
- }
22
- /** Identifier form for env vars and DB names: "my-cool-app" → "my_cool_app". */
23
- function toSnakeCase(str) {
24
- return str.replace(/-/g, "_");
25
- }
26
- //#endregion
27
15
  //#region src/utils/copy-template.ts
28
16
  const __filename = fileURLToPath(import.meta.url);
29
17
  const __dirname = path.dirname(__filename);
@@ -77,17 +65,6 @@ async function copyTemplate(targetDir, templateType) {
77
65
  }
78
66
  }
79
67
  //#endregion
80
- //#region src/utils/design-theme.ts
81
- const VALID_MOSAIC_DESIGN_THEMES = [
82
- "paper",
83
- "modern",
84
- "dense"
85
- ];
86
- const DEFAULT_MOSAIC_DESIGN_THEME = "modern";
87
- function isValidMosaicDesignTheme(value) {
88
- return typeof value === "string" && VALID_MOSAIC_DESIGN_THEMES.includes(value);
89
- }
90
- //#endregion
91
68
  //#region src/utils/detect-monorepo.ts
92
69
  const NOT_FOUND = {
93
70
  found: false,
@@ -130,11 +107,9 @@ async function detectMonorepo(startDir) {
130
107
  /**
131
108
  * Writes .env.local for the webapp template with real generated values for
132
109
  * BETTER_AUTH_SECRET and ENCRYPTION_SECRET_KEY so the user can run dev/seed
133
- * immediately. Also writes deploy/ryvn/percepta-test.secrets.env with separate
134
- * generated values that can be imported into Ryvn for the deployed installation.
135
- * The .env.example file remains the documentation source.
110
+ * immediately. The .env.example file remains the documentation source.
136
111
  *
137
- * Each generated file is a no-op if it already exists.
112
+ * This is a no-op if .env.local already exists.
138
113
  */
139
114
  async function generateEnvLocal(packageDir) {
140
115
  const examplePath = path.join(packageDir, ".env.example");
@@ -146,61 +121,6 @@ async function generateEnvLocal(packageDir) {
146
121
  const content = (await fs.readFile(examplePath, "utf-8")).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${authSecret}`).replace(/^ENCRYPTION_SECRET_KEY=.*$/m, `ENCRYPTION_SECRET_KEY=${encKey}`);
147
122
  await fs.writeFile(localPath, content);
148
123
  }
149
- const ryvnSecretsPath = path.join(packageDir, "deploy", "ryvn", "percepta-test.secrets.env");
150
- if (await fs.pathExists(ryvnSecretsPath)) return;
151
- const deployAuthSecret = randomBytes(32).toString("base64");
152
- const deployEncKey = randomBytes(16).toString("hex");
153
- const deploySecrets = [
154
- `BETTER_AUTH_SECRET=${deployAuthSecret}`,
155
- `ENCRYPTION_SECRET_KEY=${deployEncKey}`,
156
- "",
157
- "# Langfuse and LLM demo credentials are inherited from the demos-commons Ryvn variable group.",
158
- ""
159
- ].join("\n");
160
- await fs.ensureDir(path.dirname(ryvnSecretsPath));
161
- await fs.writeFile(ryvnSecretsPath, deploySecrets);
162
- }
163
- //#endregion
164
- //#region src/utils/manifest.ts
165
- const MANIFEST_FILENAME = ".mosaic-template.json";
166
- function getManifestPath(dir) {
167
- return path.join(dir, MANIFEST_FILENAME);
168
- }
169
- async function readManifest(dir) {
170
- const manifestPath = getManifestPath(dir);
171
- if (!await fs.pathExists(manifestPath)) throw new Error(`No ${MANIFEST_FILENAME} found in ${dir}. Run 'create init' to create one.`);
172
- const content = await fs.readFile(manifestPath, "utf-8");
173
- try {
174
- return JSON.parse(content);
175
- } catch (error) {
176
- throw new Error(`Invalid JSON in ${MANIFEST_FILENAME}: ${error.message}`);
177
- }
178
- }
179
- async function writeManifest(dir, manifest) {
180
- const manifestPath = getManifestPath(dir);
181
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
182
- }
183
- async function manifestExists(dir) {
184
- return fs.pathExists(getManifestPath(dir));
185
- }
186
- function derivePlaceholders(appName, appTitle, repoName = appName, designTheme = DEFAULT_MOSAIC_DESIGN_THEME) {
187
- const nameSnake = appName.replace(/-/g, "_");
188
- const repoNameSnake = repoName.replace(/-/g, "_");
189
- return {
190
- __APP_NAME__: appName,
191
- __APP_TITLE__: appTitle,
192
- __DB_NAME__: nameSnake + "_db",
193
- __APP_NAME_UPPER__: appName.toUpperCase(),
194
- __APP_NAME_SNAKE__: nameSnake,
195
- __REPO_NAME__: repoName,
196
- __REPO_NAME_SNAKE__: repoNameSnake,
197
- __MOSAIC_DESIGN_THEME__: designTheme
198
- };
199
- }
200
- function resolveMosaicTemplatePath(options) {
201
- if (options.mosaicTemplatePath) return path.resolve(options.mosaicTemplatePath);
202
- if (process.env.MOSAIC_TEMPLATE_PATH) return path.resolve(process.env.MOSAIC_TEMPLATE_PATH);
203
- throw new Error("Mosaic repo path required. Use --mosaic-template-path or set MOSAIC_TEMPLATE_PATH.");
204
124
  }
205
125
  //#endregion
206
126
  //#region src/utils/package-metadata.ts
@@ -221,16 +141,6 @@ function readCreatePackageMetadata() {
221
141
  return FALLBACK_METADATA;
222
142
  }
223
143
  //#endregion
224
- //#region src/utils/validate.ts
225
- function validateProjectName(name) {
226
- const result = validateNpmPackageName(name);
227
- if (!result.validForNewPackages) return {
228
- valid: false,
229
- error: [...result.errors || [], ...result.warnings || []][0] || "Invalid package name"
230
- };
231
- return { valid: true };
232
- }
233
- //#endregion
234
144
  //#region src/utils/prompts.ts
235
145
  const VALID_PROJECT_TYPES = [
236
146
  "monorepo",
@@ -253,6 +163,25 @@ async function promptName(message) {
253
163
  }]);
254
164
  return name;
255
165
  }
166
+ async function promptCustomerSlug(defaultCustomerSlug) {
167
+ const { customerSlug } = await inquirer.prompt([{
168
+ type: "input",
169
+ name: "customerSlug",
170
+ message: "Customer slug?",
171
+ default: defaultCustomerSlug,
172
+ filter: toKebabCase,
173
+ validate: (input) => {
174
+ const result = validateProjectName(toKebabCase(input));
175
+ return result.valid || result.error || "Invalid customer slug";
176
+ }
177
+ }]);
178
+ return customerSlug;
179
+ }
180
+ async function resolveCustomerSlug(repoName, defaults) {
181
+ if (defaults.customerSlug) return defaults.customerSlug;
182
+ if (defaults.projectType && defaults.name) return repoName;
183
+ return promptCustomerSlug(repoName);
184
+ }
256
185
  async function promptDesignTheme() {
257
186
  const { designTheme } = await inquirer.prompt([{
258
187
  type: "rawlist",
@@ -323,6 +252,7 @@ async function promptProjectDetails(defaults) {
323
252
  const repoTitle = toTitleCase(repoName);
324
253
  projectType = defaults.projectType ?? await promptOutsideMonorepoType();
325
254
  await defaults.beforeNamePrompt?.(projectType);
255
+ const customerSlug = await resolveCustomerSlug(repoName, defaults);
326
256
  if (projectType === "monorepo") {
327
257
  finalName = repoName;
328
258
  const finalTitle = repoTitle;
@@ -333,6 +263,7 @@ async function promptProjectDetails(defaults) {
333
263
  name: finalName,
334
264
  title: finalTitle,
335
265
  installDeps: !defaults.skipInstall,
266
+ customerSlug,
336
267
  monorepoName: repoName,
337
268
  monorepoTitle: repoTitle
338
269
  };
@@ -348,6 +279,7 @@ async function promptProjectDetails(defaults) {
348
279
  name: finalName,
349
280
  title: finalTitle,
350
281
  installDeps: !defaults.skipInstall,
282
+ customerSlug,
351
283
  designTheme,
352
284
  monorepoName: repoName,
353
285
  monorepoTitle: repoTitle
@@ -505,30 +437,16 @@ async function replacePlaceholders(targetDir, config) {
505
437
  return stats;
506
438
  }
507
439
  //#endregion
508
- //#region src/utils/template-versions.ts
509
- const FALLBACK_TEMPLATE_VERSION = "1.0.0";
510
- function readTemplateVersions() {
511
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
512
- const candidates = [path.resolve(currentDir, "../template-versions.json"), path.resolve(currentDir, "../../template-versions.json")];
513
- for (const versionsPath of candidates) try {
514
- const content = fs.readFileSync(versionsPath, "utf-8");
515
- return JSON.parse(content);
516
- } catch {}
517
- return {};
518
- }
519
- function getTemplateVersion(templateType) {
520
- return readTemplateVersions()[templateType] ?? FALLBACK_TEMPLATE_VERSION;
521
- }
522
- //#endregion
523
440
  //#region src/utils/workspace-manifest.ts
524
441
  const WORKSPACE_MANIFEST_FILENAME = ".mosaic-workspace.json";
525
442
  function getWorkspaceManifestPath(rootDir) {
526
443
  return path.join(rootDir, WORKSPACE_MANIFEST_FILENAME);
527
444
  }
528
- function createWorkspaceManifest(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
445
+ function createWorkspaceManifest({ customerSlug, createdAt = (/* @__PURE__ */ new Date()).toISOString() }) {
529
446
  const createPackage = readCreatePackageMetadata();
530
447
  return {
531
448
  schemaVersion: 1,
449
+ customerSlug,
532
450
  createPackage: createPackage.name,
533
451
  createVersion: createPackage.version,
534
452
  monorepoTemplateVersion: getTemplateVersion("monorepo"),
@@ -613,7 +531,7 @@ function printInstallFailureOutput(error) {
613
531
  console.log(visibleLines.join("\n"));
614
532
  }
615
533
  /**
616
- * Runs the monorepo-root `setup` script (docker + access + db + seed).
534
+ * Runs the monorepo-root `setup` script (local services + dbs + access + seed).
617
535
  * Uses `pnpm run setup` (not `pnpm setup`) because `pnpm setup` is a pnpm builtin that configures
618
536
  * PNPM_HOME in the user's shell rc — it ignores the package.json script of the same name.
619
537
  */
@@ -702,7 +620,7 @@ function openInBrowser(url) {
702
620
  }
703
621
  }
704
622
  /**
705
- * Post-scaffold orchestration for webapps: run root setup (docker + access + db + seed),
623
+ * Post-scaffold orchestration for webapps: run root setup (local services + dbs + access + seed),
706
624
  * start the dev server, open the served URL in the user's browser, then hand
707
625
  * control to the dev server until the user exits with Ctrl+C.
708
626
  *
@@ -716,7 +634,7 @@ function openInBrowser(url) {
716
634
  async function autoRunWebapp(packageDir, monorepoRoot) {
717
635
  const packageManager = PACKAGE_MANAGER;
718
636
  console.log();
719
- console.log(chalk.bold("Running setup (docker, access, db, seed)..."));
637
+ console.log(chalk.bold("Running setup (local services, dbs, access, seed)..."));
720
638
  console.log();
721
639
  try {
722
640
  await runWebappSetup(packageManager, monorepoRoot);
@@ -974,6 +892,14 @@ async function createProject(options) {
974
892
  process.exit(1);
975
893
  }
976
894
  }
895
+ const customerSlug = options.customer ? toKebabCase(options.customer) : void 0;
896
+ if (customerSlug) {
897
+ const validation = validateProjectName(customerSlug);
898
+ if (!validation.valid) {
899
+ console.error(chalk.red(`Invalid customer slug: ${validation.error}`));
900
+ process.exit(1);
901
+ }
902
+ }
977
903
  let answers;
978
904
  if (options.yes) {
979
905
  const projectType = options.type || "webapp";
@@ -987,6 +913,7 @@ async function createProject(options) {
987
913
  name: kebabName,
988
914
  title: toTitleCase(kebabName),
989
915
  installDeps: !options.skipInstall,
916
+ customerSlug: monorepoContext.found ? void 0 : customerSlug ?? kebabRepoName,
990
917
  designTheme: projectType === "webapp" ? options.theme ?? "modern" : void 0,
991
918
  monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
992
919
  monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
@@ -995,6 +922,7 @@ async function createProject(options) {
995
922
  answers = await promptProjectDetails({
996
923
  projectType: options.type,
997
924
  name: projectName ? toKebabCase(projectName) : void 0,
925
+ customerSlug,
998
926
  designTheme: options.theme,
999
927
  repoName: repoName ? toKebabCase(repoName) : void 0,
1000
928
  skipInstall: options.skipInstall,
@@ -1008,7 +936,9 @@ async function createProject(options) {
1008
936
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) answers.directory = path.join(monorepoContext.packageDir, answers.name);
1009
937
  }
1010
938
  const monorepoName = answers.monorepoName ?? answers.name;
1011
- const monorepoConfig = buildAppConfig(monorepoName, answers.monorepoTitle ?? toTitleCase(monorepoName));
939
+ const monorepoTitle = answers.monorepoTitle ?? toTitleCase(monorepoName);
940
+ const newWorkspaceCustomerSlug = answers.customerSlug ?? monorepoName;
941
+ const monorepoConfig = buildAppConfig(monorepoName, monorepoTitle, monorepoName, DEFAULT_MOSAIC_DESIGN_THEME);
1012
942
  const configRepoName = monorepoContext.found ? path.basename(monorepoContext.rootDir) : monorepoName;
1013
943
  const config = buildAppConfig(answers.name, answers.title, configRepoName, answers.designTheme);
1014
944
  const typeLabel = getProjectTypeLabel(answers.projectType);
@@ -1052,11 +982,13 @@ async function createProject(options) {
1052
982
  if (isBareMonorepo) {
1053
983
  console.log(chalk.dim(" Type:"), typeLabel);
1054
984
  console.log(chalk.dim(" Directory:"), monorepoRoot);
985
+ console.log(chalk.dim(" Customer:"), newWorkspaceCustomerSlug);
1055
986
  console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
1056
987
  console.log(chalk.dim(" Title:"), monorepoConfig.title);
1057
988
  } else {
1058
989
  console.log(chalk.dim(" Package type:"), typeLabel);
1059
990
  console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
991
+ console.log(chalk.dim(" Customer:"), newWorkspaceCustomerSlug);
1060
992
  console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
1061
993
  console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
1062
994
  console.log(chalk.dim(" Name:"), config.name);
@@ -1074,7 +1006,7 @@ async function createProject(options) {
1074
1006
  }
1075
1007
  }
1076
1008
  await scaffoldMonorepo(monorepoRoot, monorepoConfig);
1077
- const newWorkspaceManifest = createWorkspaceManifest();
1009
+ const newWorkspaceManifest = createWorkspaceManifest({ customerSlug: newWorkspaceCustomerSlug });
1078
1010
  await writeWorkspaceManifest(monorepoRoot, newWorkspaceManifest);
1079
1011
  if (packageDir && answers.projectType !== "monorepo") await addPackageToMonorepo({
1080
1012
  packageDir,
@@ -1248,7 +1180,7 @@ function printNextStepsExisting(answers, packageDir, installNeeded) {
1248
1180
  //#region src/index.ts
1249
1181
  const packageJson = readCreatePackageMetadata();
1250
1182
  program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
1251
- program.command("create", { isDefault: true }).description("Scaffold a new Mosaic package").option("-t, --type <type>", "Package type: monorepo, webapp, or library").option("--name <name>", "Package/app name").option("--theme <theme>", "Webapp design theme: modern, paper, or dense").option("--repo-name <name>", "Repository name when creating a new monorepo").option("--cwd <dir>", "Run create as if started from this directory").option("--skip-install", "Skip dependency installation (also skips the auto-run setup + dev + browser)", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(createProject);
1183
+ program.command("create", { isDefault: true }).description("Scaffold a new Mosaic package").option("-t, --type <type>", "Package type: monorepo, webapp, or library").option("--name <name>", "Package/app name").option("--customer <slug>", "Customer slug for the new monorepo").option("--theme <theme>", "Webapp design theme: modern, paper, or dense").option("--repo-name <name>", "Repository name when creating a new monorepo").option("--cwd <dir>", "Run create as if started from this directory").option("--skip-install", "Skip dependency installation (also skips the auto-run setup + dev + browser)", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(createProject);
1252
1184
  program.command("add").description("Add a Mosaic package to the current monorepo").argument("[typeOrName]", "Package type (webapp/library) or package name").argument("[name]", "Package/app name").option("-t, --type <type>", "Package type: webapp or library").option("--name <name>", "Package/app name").option("--theme <theme>", "Webapp design theme: modern, paper, or dense").option("--skip-install", "Skip dependency installation (also skips the auto-run setup + dev + browser)", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(async (typeOrName, name, options) => {
1253
1185
  let projectType = options.type;
1254
1186
  let projectName = options.name;
@@ -1267,24 +1199,33 @@ program.command("add").description("Add a Mosaic package to the current monorepo
1267
1199
  addOnly: true
1268
1200
  });
1269
1201
  });
1202
+ const infra = program.command("infra").description("Manage Mosaic infra glue");
1203
+ infra.command("register-os-blueprint").description("Register this customer monorepo's OS blueprint in infra").action(async () => {
1204
+ const { registerOsBlueprintCommand } = await import("./register-os-blueprint-DGjBUZYa.js");
1205
+ await registerOsBlueprintCommand();
1206
+ });
1207
+ infra.command("register-app").description("Register a webapp database in this customer OS blueprint").argument("<app>", "Webapp package name").action(async (appName) => {
1208
+ const { registerAppCommand } = await import("./register-app-C7ZBpAaZ.js");
1209
+ await registerAppCommand(appName);
1210
+ });
1270
1211
  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) => {
1271
- const { statusCommand } = await import("./status-CKe4aKso.js");
1212
+ const { statusCommand } = await import("./status-BXYaQ4a2.js");
1272
1213
  await statusCommand(options);
1273
1214
  });
1274
1215
  program.command("sync").description("Generate downstream sync context (template → app)").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").option("--to <version>", "Target template version (default: latest)").action(async (options) => {
1275
- const { syncCommand } = await import("./sync-D1vkoofl.js");
1216
+ const { syncCommand } = await import("./sync-BayU4w1j.js");
1276
1217
  await syncCommand(options);
1277
1218
  });
1278
1219
  program.command("upstream").description("Generate upstream context (app → template)").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
1279
- const { upstreamCommand } = await import("./upstream-gUHLWSR1.js");
1220
+ const { upstreamCommand } = await import("./upstream-CZEzLrS4.js");
1280
1221
  await upstreamCommand(options);
1281
1222
  });
1282
1223
  program.command("init").description("Add .mosaic-template.json to an existing app").option("-t, --type <type>", "Template type (e.g., webapp, library)").option("--template-version <version>", "Template version to set").action(async (options) => {
1283
- const { initCommand } = await import("./init-sI9aIrkU.js");
1224
+ const { initCommand } = await import("./init-COp0nGdk.js");
1284
1225
  await initCommand(options);
1285
1226
  });
1286
1227
  program.parse();
1287
1228
  //#endregion
1288
- export { manifestExists as a, writeManifest as c, derivePlaceholders as i, VALID_PROJECT_TYPES as n, readManifest as o, isValidProjectType as r, resolveMosaicTemplatePath as s, getTemplateVersion as t };
1229
+ export { detectMonorepo as i, VALID_PROJECT_TYPES as n, isValidProjectType as r, readWorkspaceManifest as t };
1289
1230
 
1290
1231
  //# sourceMappingURL=index.js.map