@percepta/create 3.6.2 → 4.0.0

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 (117) 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 +68 -125
  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/library/README.md +5 -2
  30. package/templates/library/gitignore.template +1 -0
  31. package/templates/library/package.json.template +17 -13
  32. package/templates/library/src/index.test.ts +8 -0
  33. package/templates/library/tsconfig.json +1 -17
  34. package/templates/library/tsdown.config.ts +3 -0
  35. package/templates/library/vitest.config.ts +3 -0
  36. package/templates/monorepo/.dockerignore +1 -0
  37. package/templates/monorepo/.github/CODEOWNERS +67 -0
  38. package/templates/monorepo/.github/actions/ci/action.yml +56 -0
  39. package/templates/monorepo/.github/workflows/build-and-publish.yml +22 -0
  40. package/templates/monorepo/.github/workflows/pr-build.yml +21 -0
  41. package/templates/monorepo/.node-version +1 -0
  42. package/templates/monorepo/README.md +41 -3
  43. package/templates/monorepo/auth/README.md +6 -3
  44. package/templates/monorepo/auth/package.json +5 -7
  45. package/templates/monorepo/auth/src/auth.ts +0 -1
  46. package/templates/monorepo/auth/src/config/database.ts +1 -1
  47. package/templates/monorepo/auth/tsconfig.json +1 -10
  48. package/templates/{webapp → monorepo}/docker-compose.yml +2 -2
  49. package/templates/monorepo/gitignore.template +1 -0
  50. package/templates/monorepo/oxfmt.config.ts.template +3 -0
  51. package/templates/monorepo/oxlint.config.ts.template +3 -0
  52. package/templates/monorepo/package.json.template +22 -11
  53. package/templates/monorepo/scripts/setup-local-databases.mjs +183 -0
  54. package/templates/monorepo/turbo.json +20 -0
  55. package/templates/webapp/.node-version +0 -1
  56. package/templates/webapp/AGENTS.md +33 -35
  57. package/templates/webapp/README.md +34 -38
  58. package/templates/webapp/agent-skills/database.md +21 -21
  59. package/templates/webapp/agent-skills/langfuse.md +7 -7
  60. package/templates/webapp/agent-skills/llm.md +4 -2
  61. package/templates/webapp/agent-skills/oneshot.md +7 -6
  62. package/templates/webapp/agent-skills/ryvn.md +12 -16
  63. package/templates/webapp/deploy/README.md +10 -51
  64. package/templates/webapp/drizzle.config.ts +2 -23
  65. package/templates/webapp/env.example.template +8 -14
  66. package/templates/webapp/globals.d.ts +1 -0
  67. package/templates/webapp/oxfmt.config.ts.template +5 -0
  68. package/templates/webapp/package.json.template +18 -33
  69. package/templates/webapp/scripts/seed.ts +1 -1
  70. package/templates/webapp/scripts/start.sh +12 -16
  71. package/templates/webapp/src/app/global-error.tsx +1 -1
  72. package/templates/webapp/src/config/getEnvConfig.ts +4 -10
  73. package/templates/webapp/src/config/isDev.ts +0 -2
  74. package/templates/webapp/src/drizzle/db.ts +6 -21
  75. package/templates/webapp/src/lib/auth-client.ts +6 -3
  76. package/templates/webapp/src/startup-checks.ts +28 -7
  77. package/templates/webapp/tsconfig.json +1 -12
  78. package/templates/webapp/vitest.config.ts +3 -7
  79. package/templates/library/eslint.config.js +0 -10
  80. package/templates/monorepo/auth/scripts/setup-database.ts +0 -11
  81. package/templates/monorepo/eslint.config.js +0 -10
  82. package/templates/monorepo/tsconfig.json +0 -16
  83. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +0 -92
  84. package/templates/webapp/.github/workflows/ci.yml +0 -149
  85. package/templates/webapp/.prettierrc.mjs +0 -5
  86. package/templates/webapp/agent-skills/deploy.md +0 -92
  87. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +0 -10
  88. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +0 -11
  89. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +0 -154
  90. package/templates/webapp/eslint.config.mjs +0 -100
  91. package/templates/webapp/npmrc.template +0 -4
  92. package/templates/webapp/terraform/README.md +0 -147
  93. package/templates/webapp/terraform/deploy.sh +0 -97
  94. package/templates/webapp/terraform/main.tf +0 -101
  95. package/templates/webapp/terraform/modules/cloudtrail/main.tf +0 -27
  96. package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +0 -10
  97. package/templates/webapp/terraform/modules/cloudtrail/variables.tf +0 -15
  98. package/templates/webapp/terraform/modules/networking/main.tf +0 -118
  99. package/templates/webapp/terraform/modules/networking/outputs.tf +0 -38
  100. package/templates/webapp/terraform/modules/networking/variables.tf +0 -24
  101. package/templates/webapp/terraform/modules/rds/main.tf +0 -227
  102. package/templates/webapp/terraform/modules/rds/outputs.tf +0 -73
  103. package/templates/webapp/terraform/modules/rds/variables.tf +0 -61
  104. package/templates/webapp/terraform/modules/s3-logging/main.tf +0 -148
  105. package/templates/webapp/terraform/modules/s3-logging/outputs.tf +0 -10
  106. package/templates/webapp/terraform/modules/s3-logging/variables.tf +0 -16
  107. package/templates/webapp/terraform/modules/secrets/main.tf +0 -39
  108. package/templates/webapp/terraform/modules/secrets/outputs.tf +0 -9
  109. package/templates/webapp/terraform/modules/secrets/variables.tf +0 -51
  110. package/templates/webapp/terraform/outputs.tf +0 -102
  111. package/templates/webapp/terraform/providers.tf +0 -32
  112. package/templates/webapp/terraform/schema/main.tf +0 -4
  113. package/templates/webapp/terraform/schema/outputs.tf +0 -9
  114. package/templates/webapp/terraform/schema/variables.tf +0 -19
  115. package/templates/webapp/terraform/schema/versions.tf +0 -38
  116. package/templates/webapp/terraform/terraform.tfvars.example +0 -65
  117. 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);
@@ -46,7 +34,9 @@ const TEMPLATE_FILE_MAPPINGS = {
46
34
  "package.json.template": "package.json",
47
35
  "gitignore.template": ".gitignore",
48
36
  "env.example.template": ".env.example",
49
- "npmrc.template": ".npmrc"
37
+ "npmrc.template": ".npmrc",
38
+ "oxlint.config.ts.template": "oxlint.config.ts",
39
+ "oxfmt.config.ts.template": "oxfmt.config.ts"
50
40
  };
51
41
  function shouldSkip(src) {
52
42
  const basename = path.basename(src);
@@ -77,17 +67,6 @@ async function copyTemplate(targetDir, templateType) {
77
67
  }
78
68
  }
79
69
  //#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
70
  //#region src/utils/detect-monorepo.ts
92
71
  const NOT_FOUND = {
93
72
  found: false,
@@ -130,11 +109,9 @@ async function detectMonorepo(startDir) {
130
109
  /**
131
110
  * Writes .env.local for the webapp template with real generated values for
132
111
  * 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.
112
+ * immediately. The .env.example file remains the documentation source.
136
113
  *
137
- * Each generated file is a no-op if it already exists.
114
+ * This is a no-op if .env.local already exists.
138
115
  */
139
116
  async function generateEnvLocal(packageDir) {
140
117
  const examplePath = path.join(packageDir, ".env.example");
@@ -146,61 +123,6 @@ async function generateEnvLocal(packageDir) {
146
123
  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
124
  await fs.writeFile(localPath, content);
148
125
  }
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
126
  }
205
127
  //#endregion
206
128
  //#region src/utils/package-metadata.ts
@@ -221,16 +143,6 @@ function readCreatePackageMetadata() {
221
143
  return FALLBACK_METADATA;
222
144
  }
223
145
  //#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
146
  //#region src/utils/prompts.ts
235
147
  const VALID_PROJECT_TYPES = [
236
148
  "monorepo",
@@ -253,6 +165,25 @@ async function promptName(message) {
253
165
  }]);
254
166
  return name;
255
167
  }
168
+ async function promptCustomerSlug(defaultCustomerSlug) {
169
+ const { customerSlug } = await inquirer.prompt([{
170
+ type: "input",
171
+ name: "customerSlug",
172
+ message: "Customer slug?",
173
+ default: defaultCustomerSlug,
174
+ filter: toKebabCase,
175
+ validate: (input) => {
176
+ const result = validateProjectName(toKebabCase(input));
177
+ return result.valid || result.error || "Invalid customer slug";
178
+ }
179
+ }]);
180
+ return customerSlug;
181
+ }
182
+ async function resolveCustomerSlug(repoName, defaults) {
183
+ if (defaults.customerSlug) return defaults.customerSlug;
184
+ if (defaults.projectType && defaults.name) return repoName;
185
+ return promptCustomerSlug(repoName);
186
+ }
256
187
  async function promptDesignTheme() {
257
188
  const { designTheme } = await inquirer.prompt([{
258
189
  type: "rawlist",
@@ -323,6 +254,7 @@ async function promptProjectDetails(defaults) {
323
254
  const repoTitle = toTitleCase(repoName);
324
255
  projectType = defaults.projectType ?? await promptOutsideMonorepoType();
325
256
  await defaults.beforeNamePrompt?.(projectType);
257
+ const customerSlug = await resolveCustomerSlug(repoName, defaults);
326
258
  if (projectType === "monorepo") {
327
259
  finalName = repoName;
328
260
  const finalTitle = repoTitle;
@@ -333,6 +265,7 @@ async function promptProjectDetails(defaults) {
333
265
  name: finalName,
334
266
  title: finalTitle,
335
267
  installDeps: !defaults.skipInstall,
268
+ customerSlug,
336
269
  monorepoName: repoName,
337
270
  monorepoTitle: repoTitle
338
271
  };
@@ -348,6 +281,7 @@ async function promptProjectDetails(defaults) {
348
281
  name: finalName,
349
282
  title: finalTitle,
350
283
  installDeps: !defaults.skipInstall,
284
+ customerSlug,
351
285
  designTheme,
352
286
  monorepoName: repoName,
353
287
  monorepoTitle: repoTitle
@@ -372,8 +306,8 @@ async function promptProjectDetails(defaults) {
372
306
  * directory to the monorepo root, where GitHub Actions actually picks them up.
373
307
  *
374
308
  * Only moves files whose names start with the app name (e.g.
375
- * `myapp-ryvn-release.yaml`). Generic workflows like `ci.yml` are left in
376
- * place — those are an unrelated monorepo-vs-package concern.
309
+ * `myapp-ryvn-release.yaml`). Any other (generic) workflow a package happens
310
+ * to ship is left in place — CI lives at the monorepo root, not per-package.
377
311
  *
378
312
  * Cleans up an empty `.github/workflows` (and empty parent `.github`) after
379
313
  * the move.
@@ -505,30 +439,16 @@ async function replacePlaceholders(targetDir, config) {
505
439
  return stats;
506
440
  }
507
441
  //#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
442
  //#region src/utils/workspace-manifest.ts
524
443
  const WORKSPACE_MANIFEST_FILENAME = ".mosaic-workspace.json";
525
444
  function getWorkspaceManifestPath(rootDir) {
526
445
  return path.join(rootDir, WORKSPACE_MANIFEST_FILENAME);
527
446
  }
528
- function createWorkspaceManifest(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
447
+ function createWorkspaceManifest({ customerSlug, createdAt = (/* @__PURE__ */ new Date()).toISOString() }) {
529
448
  const createPackage = readCreatePackageMetadata();
530
449
  return {
531
450
  schemaVersion: 1,
451
+ customerSlug,
532
452
  createPackage: createPackage.name,
533
453
  createVersion: createPackage.version,
534
454
  monorepoTemplateVersion: getTemplateVersion("monorepo"),
@@ -613,7 +533,7 @@ function printInstallFailureOutput(error) {
613
533
  console.log(visibleLines.join("\n"));
614
534
  }
615
535
  /**
616
- * Runs the monorepo-root `setup` script (docker + access + db + seed).
536
+ * Runs the monorepo-root `setup` script (local services + dbs + access + seed).
617
537
  * Uses `pnpm run setup` (not `pnpm setup`) because `pnpm setup` is a pnpm builtin that configures
618
538
  * PNPM_HOME in the user's shell rc — it ignores the package.json script of the same name.
619
539
  */
@@ -702,7 +622,7 @@ function openInBrowser(url) {
702
622
  }
703
623
  }
704
624
  /**
705
- * Post-scaffold orchestration for webapps: run root setup (docker + access + db + seed),
625
+ * Post-scaffold orchestration for webapps: run root setup (local services + dbs + access + seed),
706
626
  * start the dev server, open the served URL in the user's browser, then hand
707
627
  * control to the dev server until the user exits with Ctrl+C.
708
628
  *
@@ -716,7 +636,7 @@ function openInBrowser(url) {
716
636
  async function autoRunWebapp(packageDir, monorepoRoot) {
717
637
  const packageManager = PACKAGE_MANAGER;
718
638
  console.log();
719
- console.log(chalk.bold("Running setup (docker, access, db, seed)..."));
639
+ console.log(chalk.bold("Running setup (local services, dbs, access, seed)..."));
720
640
  console.log();
721
641
  try {
722
642
  await runWebappSetup(packageManager, monorepoRoot);
@@ -974,6 +894,14 @@ async function createProject(options) {
974
894
  process.exit(1);
975
895
  }
976
896
  }
897
+ const customerSlug = options.customer ? toKebabCase(options.customer) : void 0;
898
+ if (customerSlug) {
899
+ const validation = validateProjectName(customerSlug);
900
+ if (!validation.valid) {
901
+ console.error(chalk.red(`Invalid customer slug: ${validation.error}`));
902
+ process.exit(1);
903
+ }
904
+ }
977
905
  let answers;
978
906
  if (options.yes) {
979
907
  const projectType = options.type || "webapp";
@@ -987,6 +915,7 @@ async function createProject(options) {
987
915
  name: kebabName,
988
916
  title: toTitleCase(kebabName),
989
917
  installDeps: !options.skipInstall,
918
+ customerSlug: monorepoContext.found ? void 0 : customerSlug ?? kebabRepoName,
990
919
  designTheme: projectType === "webapp" ? options.theme ?? "modern" : void 0,
991
920
  monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
992
921
  monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
@@ -995,6 +924,7 @@ async function createProject(options) {
995
924
  answers = await promptProjectDetails({
996
925
  projectType: options.type,
997
926
  name: projectName ? toKebabCase(projectName) : void 0,
927
+ customerSlug,
998
928
  designTheme: options.theme,
999
929
  repoName: repoName ? toKebabCase(repoName) : void 0,
1000
930
  skipInstall: options.skipInstall,
@@ -1008,7 +938,9 @@ async function createProject(options) {
1008
938
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) answers.directory = path.join(monorepoContext.packageDir, answers.name);
1009
939
  }
1010
940
  const monorepoName = answers.monorepoName ?? answers.name;
1011
- const monorepoConfig = buildAppConfig(monorepoName, answers.monorepoTitle ?? toTitleCase(monorepoName));
941
+ const monorepoTitle = answers.monorepoTitle ?? toTitleCase(monorepoName);
942
+ const newWorkspaceCustomerSlug = answers.customerSlug ?? monorepoName;
943
+ const monorepoConfig = buildAppConfig(monorepoName, monorepoTitle, monorepoName, DEFAULT_MOSAIC_DESIGN_THEME);
1012
944
  const configRepoName = monorepoContext.found ? path.basename(monorepoContext.rootDir) : monorepoName;
1013
945
  const config = buildAppConfig(answers.name, answers.title, configRepoName, answers.designTheme);
1014
946
  const typeLabel = getProjectTypeLabel(answers.projectType);
@@ -1052,11 +984,13 @@ async function createProject(options) {
1052
984
  if (isBareMonorepo) {
1053
985
  console.log(chalk.dim(" Type:"), typeLabel);
1054
986
  console.log(chalk.dim(" Directory:"), monorepoRoot);
987
+ console.log(chalk.dim(" Customer:"), newWorkspaceCustomerSlug);
1055
988
  console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
1056
989
  console.log(chalk.dim(" Title:"), monorepoConfig.title);
1057
990
  } else {
1058
991
  console.log(chalk.dim(" Package type:"), typeLabel);
1059
992
  console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
993
+ console.log(chalk.dim(" Customer:"), newWorkspaceCustomerSlug);
1060
994
  console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
1061
995
  console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
1062
996
  console.log(chalk.dim(" Name:"), config.name);
@@ -1074,7 +1008,7 @@ async function createProject(options) {
1074
1008
  }
1075
1009
  }
1076
1010
  await scaffoldMonorepo(monorepoRoot, monorepoConfig);
1077
- const newWorkspaceManifest = createWorkspaceManifest();
1011
+ const newWorkspaceManifest = createWorkspaceManifest({ customerSlug: newWorkspaceCustomerSlug });
1078
1012
  await writeWorkspaceManifest(monorepoRoot, newWorkspaceManifest);
1079
1013
  if (packageDir && answers.projectType !== "monorepo") await addPackageToMonorepo({
1080
1014
  packageDir,
@@ -1248,7 +1182,7 @@ function printNextStepsExisting(answers, packageDir, installNeeded) {
1248
1182
  //#region src/index.ts
1249
1183
  const packageJson = readCreatePackageMetadata();
1250
1184
  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);
1185
+ 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
1186
  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
1187
  let projectType = options.type;
1254
1188
  let projectName = options.name;
@@ -1267,24 +1201,33 @@ program.command("add").description("Add a Mosaic package to the current monorepo
1267
1201
  addOnly: true
1268
1202
  });
1269
1203
  });
1204
+ const infra = program.command("infra").description("Manage Mosaic infra glue");
1205
+ infra.command("register-os-blueprint").description("Register this customer monorepo's OS blueprint in infra").action(async () => {
1206
+ const { registerOsBlueprintCommand } = await import("./register-os-blueprint-DGjBUZYa.js");
1207
+ await registerOsBlueprintCommand();
1208
+ });
1209
+ infra.command("register-app").description("Register a webapp database in this customer OS blueprint").argument("<app>", "Webapp package name").action(async (appName) => {
1210
+ const { registerAppCommand } = await import("./register-app-C7ZBpAaZ.js");
1211
+ await registerAppCommand(appName);
1212
+ });
1270
1213
  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");
1214
+ const { statusCommand } = await import("./status-BXYaQ4a2.js");
1272
1215
  await statusCommand(options);
1273
1216
  });
1274
1217
  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");
1218
+ const { syncCommand } = await import("./sync-BayU4w1j.js");
1276
1219
  await syncCommand(options);
1277
1220
  });
1278
1221
  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");
1222
+ const { upstreamCommand } = await import("./upstream-CZEzLrS4.js");
1280
1223
  await upstreamCommand(options);
1281
1224
  });
1282
1225
  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");
1226
+ const { initCommand } = await import("./init-COp0nGdk.js");
1284
1227
  await initCommand(options);
1285
1228
  });
1286
1229
  program.parse();
1287
1230
  //#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 };
1231
+ export { detectMonorepo as i, VALID_PROJECT_TYPES as n, isValidProjectType as r, readWorkspaceManifest as t };
1289
1232
 
1290
1233
  //# sourceMappingURL=index.js.map