@percepta/create 3.1.2 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +3 -4
  2. package/dist/{chunk-CG7IJSB4.js → chunk-CO3YWUD6.js} +2 -2
  3. package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
  4. package/dist/index.js +21 -53
  5. package/dist/{init-XDWSYHYK.js → init-EQZ2TCSJ.js} +2 -2
  6. package/dist/{status-BTHGN6QH.js → status-QW5TQDYY.js} +1 -1
  7. package/dist/{sync-3Q27L7XZ.js → sync-RLBZDOFB.js} +1 -1
  8. package/dist/{upstream-C5KFAHVR.js → upstream-TQFVPMEG.js} +1 -1
  9. package/package.json +1 -1
  10. package/templates/monorepo/.dockerignore +18 -0
  11. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
  12. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
  13. package/templates/webapp/AGENTS.md +17 -5
  14. package/templates/webapp/Dockerfile +16 -7
  15. package/templates/webapp/README.md +64 -2
  16. package/templates/webapp/agent-skills/deploy.md +48 -65
  17. package/templates/webapp/agent-skills/inngest.md +4 -4
  18. package/templates/webapp/agent-skills/langfuse.md +15 -14
  19. package/templates/webapp/agent-skills/llm.md +59 -0
  20. package/templates/webapp/agent-skills/oneshot.md +14 -1
  21. package/templates/webapp/agent-skills/ryvn.md +1 -1
  22. package/templates/webapp/deploy/README.md +34 -33
  23. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
  24. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
  25. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
  26. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +45 -9
  27. package/templates/webapp/env.example.template +20 -2
  28. package/templates/webapp/eslint.config.mjs +6 -0
  29. package/templates/webapp/next.config.ts +9 -0
  30. package/templates/webapp/package.json.template +6 -2
  31. package/templates/webapp/scripts/deploy-percepta-test.ts +837 -0
  32. package/templates/webapp/scripts/migrate.ts +3 -0
  33. package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +5 -3
  34. package/templates/webapp/scripts/with-local-env.ts +75 -0
  35. package/templates/webapp/src/config/getEnvConfig.ts +14 -0
  36. package/templates/webapp/src/instrumentation.ts +102 -10
  37. package/templates/webapp/src/services/llm/LLMService.ts +88 -0
  38. package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
  39. package/templates/webapp/terraform/schema/main.tf +4 -0
  40. package/templates/webapp/terraform/schema/outputs.tf +9 -0
  41. package/templates/webapp/terraform/schema/variables.tf +19 -0
  42. package/templates/webapp/terraform/schema/versions.tf +38 -0
  43. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
package/README.md CHANGED
@@ -43,10 +43,9 @@ The bare command above is the canonical UX. The flags below exist for tests and
43
43
  When you scaffold a webapp (the default flow), `create` automatically runs:
44
44
 
45
45
  1. `pnpm install` (at the monorepo root)
46
- 2. `pnpm install --ignore-workspace` (inside the webapp package, to create the Docker build lockfile)
47
- 3. `pnpm run setup` — Docker Compose Postgres + Drizzle migrations + seed user
48
- 4. `pnpm dev` Next.js dev server
49
- 5. Opens the served URL in your default browser
46
+ 2. `pnpm run setup` Docker Compose Postgres + Drizzle migrations + seed user
47
+ 3. `pnpm dev` — Next.js dev server
48
+ 4. Opens the served URL in your default browser
50
49
 
51
50
  Sign in as `admin@example.com` / `password` to start building.
52
51
 
@@ -85,10 +85,10 @@ async function promptProjectDetails(defaults) {
85
85
  await defaults.beforeNamePrompt?.(projectType);
86
86
  finalName = defaults.name || await promptName("Package name?");
87
87
  } else {
88
+ const repoName = defaults.repoName || (defaults.projectType === "monorepo" ? defaults.name : void 0) || await promptName("Repo name?");
89
+ const repoTitle = toTitleCase(repoName);
88
90
  projectType = defaults.projectType ?? await promptOutsideMonorepoType();
89
91
  await defaults.beforeNamePrompt?.(projectType);
90
- const repoName = defaults.repoName || (projectType === "monorepo" ? defaults.name : void 0) || await promptName("Repo name?");
91
- const repoTitle = toTitleCase(repoName);
92
92
  if (projectType === "monorepo") {
93
93
  finalName = repoName;
94
94
  const finalTitle3 = repoTitle;
@@ -28,14 +28,17 @@ async function writeManifest(dir, manifest) {
28
28
  async function manifestExists(dir) {
29
29
  return fs.pathExists(getManifestPath(dir));
30
30
  }
31
- function derivePlaceholders(appName, appTitle) {
31
+ function derivePlaceholders(appName, appTitle, repoName = appName) {
32
32
  const nameSnake = appName.replace(/-/g, "_");
33
+ const repoNameSnake = repoName.replace(/-/g, "_");
33
34
  return {
34
35
  __APP_NAME__: appName,
35
36
  __APP_TITLE__: appTitle,
36
37
  __DB_NAME__: nameSnake + "_db",
37
38
  __APP_NAME_UPPER__: appName.toUpperCase(),
38
- __APP_NAME_SNAKE__: nameSnake
39
+ __APP_NAME_SNAKE__: nameSnake,
40
+ __REPO_NAME__: repoName,
41
+ __REPO_NAME_SNAKE__: repoNameSnake
39
42
  };
40
43
  }
41
44
  function resolveMosaicTemplatePath(options) {
package/dist/index.js CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  toSnakeCase,
8
8
  toTitleCase,
9
9
  validateProjectName
10
- } from "./chunk-CG7IJSB4.js";
10
+ } from "./chunk-CO3YWUD6.js";
11
11
  import {
12
12
  derivePlaceholders,
13
13
  writeManifest
14
- } from "./chunk-WMJT7CB5.js";
14
+ } from "./chunk-V5EJIUBJ.js";
15
15
 
16
16
  // src/index.ts
17
17
  import { program } from "commander";
@@ -98,7 +98,9 @@ var PLACEHOLDERS = {
98
98
  __APP_TITLE__: "title",
99
99
  __DB_NAME__: "dbName",
100
100
  __APP_NAME_UPPER__: "nameUpper",
101
- __APP_NAME_SNAKE__: "nameSnake"
101
+ __APP_NAME_SNAKE__: "nameSnake",
102
+ __REPO_NAME__: "repoName",
103
+ __REPO_NAME_SNAKE__: "repoNameSnake"
102
104
  };
103
105
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
104
106
  "node_modules",
@@ -280,6 +282,8 @@ async function generateEnvLocal(packageDir) {
280
282
  const deploySecrets = [
281
283
  `BETTER_AUTH_SECRET=${deployAuthSecret}`,
282
284
  `ENCRYPTION_SECRET_KEY=${deployEncKey}`,
285
+ "",
286
+ "# Langfuse and LLM demo credentials are inherited from the demos-commons Ryvn variable group.",
283
287
  ""
284
288
  ].join("\n");
285
289
  await fs4.ensureDir(path4.dirname(ryvnSecretsPath));
@@ -526,7 +530,7 @@ async function writeMosaicFiles(packageDir, config, projectType) {
526
530
  templateVersion: templateVersions[projectType] || "1.0.0",
527
531
  templateCommit: "npm",
528
532
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
529
- placeholders: derivePlaceholders(config.name, config.title),
533
+ placeholders: derivePlaceholders(config.name, config.title, config.repoName),
530
534
  source: {
531
535
  templatePath: `packages/create-mosaic-module/templates/${projectType}`
532
536
  }
@@ -546,13 +550,15 @@ _None yet \u2014 freshly created from template._
546
550
  `
547
551
  );
548
552
  }
549
- function buildAppConfig(name, title = toTitleCase(name)) {
553
+ function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
550
554
  return {
551
555
  name,
552
556
  title,
553
557
  dbName: `${toSnakeCase(name)}_db`,
554
558
  nameUpper: name.toUpperCase(),
555
- nameSnake: toSnakeCase(name)
559
+ nameSnake: toSnakeCase(name),
560
+ repoName,
561
+ repoNameSnake: toSnakeCase(repoName)
556
562
  };
557
563
  }
558
564
  async function scaffoldMonorepo(targetDir, config) {
@@ -657,27 +663,6 @@ async function installAtMonorepoRoot(monorepoRoot, installDeps) {
657
663
  return false;
658
664
  }
659
665
  }
660
- async function installAtWebappPackage(packageDir, projectType, installDeps) {
661
- if (!installDeps || !packageDir || projectType !== "webapp") {
662
- return projectType !== "webapp";
663
- }
664
- const spinner = ora(
665
- `Generating package lockfile with ${PACKAGE_MANAGER}...`
666
- ).start();
667
- try {
668
- await runPackageManagerInstall(PACKAGE_MANAGER, packageDir, [
669
- "install",
670
- "--ignore-workspace"
671
- ]);
672
- spinner.succeed("Generated package lockfile");
673
- return true;
674
- } catch {
675
- spinner.warn(
676
- `Failed to generate package lockfile. Run '${PACKAGE_MANAGER} install --ignore-workspace' from ${packageDir}.`
677
- );
678
- return false;
679
- }
680
- }
681
666
  async function maybeAutoRunWebapp(packageDir, projectType, installSucceeded) {
682
667
  if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
683
668
  return autoRunWebapp(packageDir);
@@ -812,10 +797,11 @@ async function createProject(options) {
812
797
  answers.directory = path7.join(monorepoContext.packageDir, answers.name);
813
798
  }
814
799
  }
815
- const config = buildAppConfig(answers.name, answers.title);
816
800
  const monorepoName = answers.monorepoName ?? answers.name;
817
801
  const monorepoTitle = answers.monorepoTitle ?? toTitleCase(monorepoName);
818
802
  const monorepoConfig = buildAppConfig(monorepoName, monorepoTitle);
803
+ const configRepoName = monorepoContext.found ? path7.basename(monorepoContext.rootDir) : monorepoName;
804
+ const config = buildAppConfig(answers.name, answers.title, configRepoName);
819
805
  const typeLabel = getProjectTypeLabel(answers.projectType);
820
806
  if (monorepoContext.found) {
821
807
  const monorepoRoot = monorepoContext.rootDir;
@@ -846,16 +832,10 @@ async function createProject(options) {
846
832
  });
847
833
  }
848
834
  await warnIfMissingRootNpmrc(monorepoRoot);
849
- const rootInstallSucceeded = await installAtMonorepoRoot(
835
+ const installSucceeded = await installAtMonorepoRoot(
850
836
  monorepoRoot,
851
837
  answers.installDeps
852
838
  );
853
- const packageInstallSucceeded = await installAtWebappPackage(
854
- packageDir,
855
- answers.projectType,
856
- answers.installDeps
857
- );
858
- const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
859
839
  console.log();
860
840
  console.log(
861
841
  chalk.green("\u2714"),
@@ -910,16 +890,10 @@ async function createProject(options) {
910
890
  });
911
891
  }
912
892
  initGitRepo(monorepoRoot);
913
- const rootInstallSucceeded = await installAtMonorepoRoot(
893
+ const installSucceeded = await installAtMonorepoRoot(
914
894
  monorepoRoot,
915
895
  answers.installDeps
916
896
  );
917
- const packageInstallSucceeded = await installAtWebappPackage(
918
- packageDir,
919
- answers.projectType,
920
- answers.installDeps
921
- );
922
- const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
923
897
  console.log();
924
898
  console.log(
925
899
  chalk.green("\u2714"),
@@ -968,14 +942,12 @@ function printWebappNextSteps(params) {
968
942
  } = params;
969
943
  const repoRel = shPath(monorepoRelativePath) || ".";
970
944
  const pkgFromRoot = `packages/${answers.name}`;
971
- const packageInstallStep = `${pm} install --ignore-workspace`;
972
945
  const pnpmSteps = ["pnpm run setup", "pnpm dev"];
973
946
  if (variant === "new") {
974
947
  const oneLinerParts2 = [];
975
948
  if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
976
949
  if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
977
950
  oneLinerParts2.push(`cd ${pkgFromRoot}`);
978
- if (!answers.installDeps) oneLinerParts2.push(packageInstallStep);
979
951
  oneLinerParts2.push(...pnpmSteps);
980
952
  console.log(chalk.bold("Copy-paste (from your current directory):"));
981
953
  console.log();
@@ -991,9 +963,6 @@ function printWebappNextSteps(params) {
991
963
  console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
992
964
  }
993
965
  console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
994
- if (!answers.installDeps) {
995
- console.log(chalk.dim(` ${step2++}.`), packageInstallStep);
996
- }
997
966
  for (const cmd of pnpmSteps) {
998
967
  console.log(chalk.dim(` ${step2++}.`), cmd);
999
968
  }
@@ -1003,7 +972,7 @@ function printWebappNextSteps(params) {
1003
972
  const oneLinerParts = [];
1004
973
  if (!answers.installDeps) {
1005
974
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
1006
- oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`, packageInstallStep);
975
+ oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
1007
976
  } else if (pkgRel !== ".") {
1008
977
  oneLinerParts.push(`cd ${pkgRel}`);
1009
978
  }
@@ -1021,7 +990,6 @@ function printWebappNextSteps(params) {
1021
990
  }
1022
991
  console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1023
992
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
1024
- console.log(chalk.dim(` ${step++}.`), packageInstallStep);
1025
993
  } else if (pkgRel !== ".") {
1026
994
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
1027
995
  }
@@ -1163,25 +1131,25 @@ program.command("status").description("Show template sync status for current app
1163
1131
  "--mosaic-template-path <path>",
1164
1132
  "Path to local mosaic repo checkout"
1165
1133
  ).action(async (options) => {
1166
- const { statusCommand } = await import("./status-BTHGN6QH.js");
1134
+ const { statusCommand } = await import("./status-QW5TQDYY.js");
1167
1135
  await statusCommand(options);
1168
1136
  });
1169
1137
  program.command("sync").description("Generate downstream sync context (template \u2192 app)").option(
1170
1138
  "--mosaic-template-path <path>",
1171
1139
  "Path to local mosaic repo checkout"
1172
1140
  ).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
1173
- const { syncCommand } = await import("./sync-3Q27L7XZ.js");
1141
+ const { syncCommand } = await import("./sync-RLBZDOFB.js");
1174
1142
  await syncCommand(options);
1175
1143
  });
1176
1144
  program.command("upstream").description("Generate upstream context (app \u2192 template)").option(
1177
1145
  "--mosaic-template-path <path>",
1178
1146
  "Path to local mosaic repo checkout"
1179
1147
  ).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
1180
- const { upstreamCommand } = await import("./upstream-C5KFAHVR.js");
1148
+ const { upstreamCommand } = await import("./upstream-TQFVPMEG.js");
1181
1149
  await upstreamCommand(options);
1182
1150
  });
1183
1151
  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) => {
1184
- const { initCommand } = await import("./init-XDWSYHYK.js");
1152
+ const { initCommand } = await import("./init-EQZ2TCSJ.js");
1185
1153
  await initCommand(options);
1186
1154
  });
1187
1155
  program.parse();
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  VALID_PROJECT_TYPES,
3
3
  isValidProjectType
4
- } from "./chunk-CG7IJSB4.js";
4
+ } from "./chunk-CO3YWUD6.js";
5
5
  import {
6
6
  derivePlaceholders,
7
7
  manifestExists,
8
8
  writeManifest
9
- } from "./chunk-WMJT7CB5.js";
9
+ } from "./chunk-V5EJIUBJ.js";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import path from "path";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-DCM7JOSC.js";
5
5
  import {
6
6
  readManifest
7
- } from "./chunk-WMJT7CB5.js";
7
+ } from "./chunk-V5EJIUBJ.js";
8
8
 
9
9
  // src/commands/status.ts
10
10
  import path from "path";
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  readManifest,
8
8
  resolveMosaicTemplatePath
9
- } from "./chunk-WMJT7CB5.js";
9
+ } from "./chunk-V5EJIUBJ.js";
10
10
 
11
11
  // src/commands/sync.ts
12
12
  import path from "path";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  readManifest,
6
6
  resolveMosaicTemplatePath
7
- } from "./chunk-WMJT7CB5.js";
7
+ } from "./chunk-V5EJIUBJ.js";
8
8
 
9
9
  // src/commands/upstream.ts
10
10
  import path from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,18 @@
1
+ .git
2
+ .next
3
+ .turbo
4
+ .vercel
5
+ .pnpm-store
6
+ coverage
7
+ dist
8
+ build
9
+ node_modules
10
+ **/.next
11
+ **/node_modules
12
+
13
+ .env
14
+ .env.*
15
+ **/.env
16
+ **/.env.*
17
+ !**/.env.example
18
+ **/deploy/ryvn/*.secrets.env
@@ -9,7 +9,9 @@ on:
9
9
  - "packages/__APP_NAME__/scripts/**"
10
10
  - "packages/__APP_NAME__/Dockerfile"
11
11
  - "packages/__APP_NAME__/package.json"
12
- - "packages/__APP_NAME__/pnpm-lock.yaml"
12
+ - "package.json"
13
+ - "pnpm-lock.yaml"
14
+ - "pnpm-workspace.yaml"
13
15
  - ".github/workflows/__APP_NAME__-ryvn-release.yaml"
14
16
  pull_request:
15
17
  branches:
@@ -19,7 +21,9 @@ on:
19
21
  - "packages/__APP_NAME__/scripts/**"
20
22
  - "packages/__APP_NAME__/Dockerfile"
21
23
  - "packages/__APP_NAME__/package.json"
22
- - "packages/__APP_NAME__/pnpm-lock.yaml"
24
+ - "package.json"
25
+ - "pnpm-lock.yaml"
26
+ - "pnpm-workspace.yaml"
23
27
  workflow_dispatch:
24
28
 
25
29
  env:
@@ -0,0 +1,98 @@
1
+ name: Build & Release __APP_NAME__-terraform
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "main"
7
+ paths:
8
+ - "packages/__APP_NAME__/terraform/schema/**"
9
+ - ".github/workflows/__APP_NAME__-terraform-ryvn-release.yaml"
10
+ pull_request:
11
+ branches:
12
+ - "main"
13
+ paths:
14
+ - "packages/__APP_NAME__/terraform/schema/**"
15
+ - ".github/workflows/__APP_NAME__-terraform-ryvn-release.yaml"
16
+ workflow_dispatch:
17
+
18
+ env:
19
+ SERVICE_NAME: __APP_NAME__-terraform
20
+
21
+ jobs:
22
+ build-and-release:
23
+ name: Build and Release
24
+ runs-on: ubuntu-latest
25
+ permissions:
26
+ contents: write
27
+ id-token: write
28
+
29
+ steps:
30
+ - name: Checkout code
31
+ uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
34
+
35
+ - name: Install Ryvn CLI
36
+ uses: ryvn-technologies/install-ryvn-cli@v1.0.0
37
+
38
+ - name: Generate Release Tag
39
+ id: generate-tag
40
+ env:
41
+ RYVN_CLIENT_ID: ${{ secrets.RYVN_CLIENT_ID }}
42
+ RYVN_CLIENT_SECRET: ${{ secrets.RYVN_CLIENT_SECRET }}
43
+ run: |
44
+ tag_info=$(ryvn generate-release-tag "$SERVICE_NAME" --prefix="${SERVICE_NAME}@" -o json --default-bump-minor)
45
+
46
+ version=$(echo "$tag_info" | jq -r '.version')
47
+ new_tag=$(echo "$tag_info" | jq -r '.tag')
48
+ channel=$(echo "$tag_info" | jq -r '.channel')
49
+ isPreview=$(echo "$tag_info" | jq -r '.isPreview')
50
+
51
+ echo "version=$version" >> $GITHUB_OUTPUT
52
+ echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
53
+ echo "channel=$channel" >> $GITHUB_OUTPUT
54
+ echo "isPreview=$isPreview" >> $GITHUB_OUTPUT
55
+
56
+ - name: Create Ryvn Release
57
+ if: |
58
+ !contains(github.event.head_commit.message, '[skip-release]') &&
59
+ !contains(github.event.pull_request.title, '[skip-release]') &&
60
+ (steps.generate-tag.outputs.isPreview == 'true' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
61
+ env:
62
+ RYVN_CLIENT_ID: ${{ secrets.RYVN_CLIENT_ID }}
63
+ RYVN_CLIENT_SECRET: ${{ secrets.RYVN_CLIENT_SECRET }}
64
+ run: |
65
+ version="${{ steps.generate-tag.outputs.new_tag }}"
66
+ version="${version#"${SERVICE_NAME}@"}"
67
+ version="${version#@}"
68
+ channel="${{ steps.generate-tag.outputs.channel }}"
69
+
70
+ if [ -n "$channel" ] && [ "$channel" != "null" ]; then
71
+ ryvn create release "$SERVICE_NAME" "$version" --channel "$channel"
72
+ else
73
+ ryvn create release "$SERVICE_NAME" "$version"
74
+ fi
75
+
76
+ - name: Create GitHub Tag
77
+ if: |
78
+ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
79
+ !contains(github.event.head_commit.message, '[skip-release]') &&
80
+ !contains(github.event.pull_request.title, '[skip-release]')
81
+ run: |
82
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
83
+ git config --global user.name "github-actions[bot]"
84
+ git tag "${{ steps.generate-tag.outputs.new_tag }}"
85
+ git push origin "${{ steps.generate-tag.outputs.new_tag }}"
86
+
87
+ - name: Create GitHub Release
88
+ if: |
89
+ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
90
+ !contains(github.event.head_commit.message, '[skip-release]') &&
91
+ !contains(github.event.pull_request.title, '[skip-release]')
92
+ uses: softprops/action-gh-release@v1
93
+ with:
94
+ tag_name: ${{ steps.generate-tag.outputs.new_tag }}
95
+ name: ${{ steps.generate-tag.outputs.new_tag }}
96
+ generate_release_notes: true
97
+ draft: false
98
+ prerelease: false
@@ -9,6 +9,7 @@ Next.js 15 full-stack application scaffolded from the Mosaic webapp template via
9
9
  - `pnpm lint` — run ESLint
10
10
  - `pnpm test` — run Vitest tests
11
11
  - `pnpm docker:up` / `pnpm docker:down` — start/stop PostgreSQL
12
+ - `pnpm inngest:dev` — start local Inngest dev server when working on background jobs
12
13
  - `pnpm db:generate` — generate Drizzle migrations
13
14
  - `pnpm db:migrate` — apply migrations
14
15
  - `pnpm db:setup-and-migrate` — create DB + migrate
@@ -17,6 +18,8 @@ Next.js 15 full-stack application scaffolded from the Mosaic webapp template via
17
18
 
18
19
  **Package manager**: Always use `pnpm`, never `npm` or `yarn`.
19
20
 
21
+ Local development only requires Postgres by default. Run Inngest locally when a workflow needs it. Do not run a local LGTM/Langfuse stack unless you are specifically debugging telemetry; those are wired by the Ryvn environment for deploys. If local LLM calls are needed, `pnpm dev` loads shared provider keys from `~/.config/percepta/create.env` when it exists.
22
+
20
23
  ## Code Style
21
24
 
22
25
  - Double quotes for strings
@@ -44,14 +47,18 @@ src/ # Application source
44
47
  ├── services/
45
48
  │ ├── inngest/ # Background job definitions
46
49
  │ ├── langfuse/ # LLM observability
50
+ │ ├── llm/ # LLM provider selection and call helpers
47
51
  │ ├── logger/ # App logger setup (wraps @percepta/logger)
48
52
  │ └── observability/ # OpenTelemetry setup
49
53
  └── utils/ # Helpers (cn, pathEncryption, etc.)
50
54
 
51
55
  deploy/ # Infrastructure-as-code for Ryvn deployments
52
56
  └── ryvn/
53
- ├── __APP_NAME__.service.yaml # Ryvn Service definition
54
- └── environments/<env>/installations/__APP_NAME__.env.<env>.serviceinstallation.yaml
57
+ ├── __APP_NAME__.service.yaml # Ryvn web service
58
+ ├── __APP_NAME__-terraform.service.yaml # Ryvn schema service
59
+ └── environments/<env>/installations/
60
+ ├── __APP_NAME__.env.<env>.serviceinstallation.yaml
61
+ └── __APP_NAME__-terraform.env.<env>.serviceinstallation.yaml
55
62
  ```
56
63
 
57
64
  ## @percepta Packages
@@ -184,6 +191,7 @@ Detailed how-to guides for each major stack component. Read the relevant guide w
184
191
  | Guide | File | When to read |
185
192
  |-------|------|-------------|
186
193
  | Background Jobs (Inngest) | [agent-skills/inngest.md](agent-skills/inngest.md) | Adding async tasks, scheduled jobs, or agent workflows |
194
+ | LLM Calls | [agent-skills/llm.md](agent-skills/llm.md) | Adding backend model calls, streaming, or structured generation |
187
195
  | LLM Observability (Langfuse) | [agent-skills/langfuse.md](agent-skills/langfuse.md) | App uses LLMs and needs trace/eval monitoring |
188
196
  | Database (Drizzle) | [agent-skills/database.md](agent-skills/database.md) | Adding tables, writing migrations, querying data |
189
197
  | Deployment (Ryvn) | [agent-skills/ryvn.md](agent-skills/ryvn.md) | Ryvn overview and Percepta environment context |
@@ -220,19 +228,23 @@ Better Auth configured in `src/lib/auth/`. Email/password credentials enabled by
220
228
 
221
229
  Inngest for async task processing. Define functions in `src/services/inngest/`. Configure via `INNGEST_*` env vars.
222
230
 
231
+ ### LLM Calls
232
+
233
+ Use `LLMService` from `src/services/llm/LLMService.ts` for backend model calls. It chooses the configured provider, enables AI SDK telemetry, and attaches provider/model metadata for Langfuse.
234
+
223
235
  ### Database
224
236
 
225
237
  PostgreSQL with Drizzle ORM. Schema in `src/drizzle/schema/`, migrations in `src/drizzle/migrations/`. Connection managed by `src/services/DatabaseService.ts`.
226
238
 
227
239
  ### Observability
228
240
 
229
- OpenTelemetry initialized in `src/instrumentation.ts`. Langfuse for LLM tracking in `src/services/langfuse/`. Faro for frontend monitoring via `@percepta/next-utils/faro`.
241
+ OpenTelemetry initialized in `src/instrumentation.ts`. Server traces and metrics export to the configured OTEL collector; platform logs are collected from stdout. Langfuse for LLM tracking in `src/services/langfuse/` activates when `LANGFUSE_*` env vars are configured, but only AI SDK spans are forwarded to Langfuse by default. Faro for frontend monitoring via `@percepta/next-utils/faro`.
230
242
 
231
243
  ## Deployment
232
244
 
233
- To deploy this app to percepta-test, follow [agent-skills/deploy.md](agent-skills/deploy.md). The Ryvn service definition and percepta-test installation YAML are already scaffolded at `deploy/ryvn/` with all values filled in deploy is mostly "copy these two files into the infra repo, open a PR, set three secrets in the Ryvn UI."
245
+ To deploy this app to percepta-test, follow [agent-skills/deploy.md](agent-skills/deploy.md). The direct deploy helper treats percepta-test as an existing platform environment: it preflights the shared Postgres, Inngest, OTEL collector, and LGTM installations, then creates/updates this app's Ryvn services and installations.
234
246
 
235
- The release CI/CD workflow is already included at `.github/workflows/ryvn-release.yaml`.
247
+ The release CI/CD workflows are already included under `.github/workflows/`.
236
248
 
237
249
  For Ryvn CLI operations, use the `/use-ryvn` skill.
238
250
 
@@ -6,6 +6,8 @@ RUN npm install -g pnpm
6
6
 
7
7
  # Build stage:
8
8
  FROM base AS builder
9
+ WORKDIR /repo
10
+ ENV CI=true
9
11
 
10
12
  COPY . .
11
13
 
@@ -20,7 +22,7 @@ ENV BASE_PATH=${BASE_PATH}
20
22
 
21
23
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
22
24
 
23
- RUN NODE_ENV=production NODE_OPTIONS="--max-old-space-size=4096" pnpm build
25
+ RUN NODE_ENV=production NODE_OPTIONS="--max-old-space-size=4096" pnpm --filter ./packages/__APP_NAME__ build
24
26
 
25
27
  # Remove .npmrc for security (contains auth token):
26
28
  RUN rm -f .npmrc
@@ -43,21 +45,28 @@ ENV NEXT_TELEMETRY_DISABLED=1
43
45
  ENV BASE_PATH=${BASE_PATH}
44
46
 
45
47
  # Copy built app from builder stage
46
- COPY --from=builder /app/.next/standalone ./
47
- COPY --from=builder /app/.next/static ./.next/static
48
+ COPY --from=builder /repo/packages/__APP_NAME__/.next/standalone ./
49
+ COPY --from=builder /repo/packages/__APP_NAME__/.next/static ./packages/__APP_NAME__/.next/static
48
50
 
49
51
  # Copy scripts and source files needed for start.sh and runtime
50
- COPY --from=builder /app/scripts ./scripts
51
- COPY --from=builder /app/src ./src
52
- COPY --from=builder /app/node_modules ./node_modules
52
+ COPY --from=builder /repo/package.json ./package.json
53
+ COPY --from=builder /repo/pnpm-lock.yaml ./pnpm-lock.yaml
54
+ COPY --from=builder /repo/pnpm-workspace.yaml ./pnpm-workspace.yaml
55
+ COPY --from=builder /repo/node_modules ./node_modules
56
+ COPY --from=builder /repo/packages/__APP_NAME__/package.json ./packages/__APP_NAME__/package.json
57
+ COPY --from=builder /repo/packages/__APP_NAME__/tsconfig.json ./packages/__APP_NAME__/tsconfig.json
58
+ COPY --from=builder /repo/packages/__APP_NAME__/node_modules ./packages/__APP_NAME__/node_modules
59
+ COPY --from=builder /repo/packages/__APP_NAME__/scripts ./packages/__APP_NAME__/scripts
60
+ COPY --from=builder /repo/packages/__APP_NAME__/src ./packages/__APP_NAME__/src
53
61
 
54
62
 
55
63
  # Expose the port:
56
64
  EXPOSE 3000
57
65
 
58
66
  # Set correct permissions and user:
59
- RUN chown -R 1000:1000 /app && chmod +x /app/scripts/start.sh
67
+ RUN chown -R 1000:1000 /app && chmod +x /app/packages/__APP_NAME__/scripts/start.sh
60
68
  USER 1000
69
+ WORKDIR /app/packages/__APP_NAME__
61
70
 
62
71
  # Start the application:
63
72
  CMD ["./scripts/start.sh"]