@percepta/create 3.1.0 → 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 (62) hide show
  1. package/README.md +15 -10
  2. package/dist/{chunk-7NPWSTCY.js → chunk-CO3YWUD6.js} +31 -2
  3. package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
  4. package/dist/index.js +93 -73
  5. package/dist/{init-NP6GRXLL.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 +3 -2
  10. package/templates/monorepo/.dockerignore +18 -0
  11. package/templates/monorepo/gitignore.template +1 -0
  12. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
  13. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
  14. package/templates/webapp/AGENTS.md +17 -5
  15. package/templates/webapp/Dockerfile +16 -7
  16. package/templates/webapp/README.md +64 -2
  17. package/templates/webapp/agent-skills/deploy.md +50 -51
  18. package/templates/webapp/agent-skills/inngest.md +4 -4
  19. package/templates/webapp/agent-skills/langfuse.md +15 -14
  20. package/templates/webapp/agent-skills/llm.md +59 -0
  21. package/templates/webapp/agent-skills/oneshot.md +14 -1
  22. package/templates/webapp/agent-skills/ryvn.md +1 -1
  23. package/templates/webapp/deploy/README.md +41 -16
  24. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
  25. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
  26. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
  27. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +60 -11
  28. package/templates/webapp/env.example.template +20 -2
  29. package/templates/webapp/eslint.config.mjs +7 -0
  30. package/templates/webapp/gitignore.template +1 -0
  31. package/templates/webapp/next.config.ts +9 -0
  32. package/templates/webapp/package.json.template +6 -2
  33. package/templates/webapp/scripts/deploy-percepta-test.ts +837 -0
  34. package/templates/webapp/scripts/migrate.ts +3 -0
  35. package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +152 -32
  36. package/templates/webapp/scripts/seed.ts +1 -1
  37. package/templates/webapp/scripts/setup-database.ts +2 -1
  38. package/templates/webapp/scripts/start.sh +3 -2
  39. package/templates/webapp/scripts/with-local-env.ts +75 -0
  40. package/templates/webapp/src/app/(app)/layout.tsx +1 -5
  41. package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +11 -1
  42. package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +113 -0
  43. package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +30 -0
  44. package/templates/webapp/src/app/global-error.tsx +1 -1
  45. package/templates/webapp/src/components/FaroProvider.tsx +2 -4
  46. package/templates/webapp/src/components/form/FormItem.tsx +2 -2
  47. package/templates/webapp/src/config/getEnvConfig.ts +14 -0
  48. package/templates/webapp/src/drizzle/db.ts +2 -1
  49. package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +3 -3
  50. package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +7 -19
  51. package/templates/webapp/src/drizzle/ssl.ts +5 -0
  52. package/templates/webapp/src/instrumentation.ts +102 -10
  53. package/templates/webapp/src/lib/auth/index.ts +1 -1
  54. package/templates/webapp/src/lib/auth-client.ts +1 -1
  55. package/templates/webapp/src/services/llm/LLMService.ts +88 -0
  56. package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
  57. package/templates/webapp/src/services/observability/initFaro.ts +1 -1
  58. package/templates/webapp/terraform/schema/main.tf +4 -0
  59. package/templates/webapp/terraform/schema/outputs.tf +9 -0
  60. package/templates/webapp/terraform/schema/variables.tf +19 -0
  61. package/templates/webapp/terraform/schema/versions.tf +38 -0
  62. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
package/README.md CHANGED
@@ -8,7 +8,7 @@ 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 and project name. Defaults yield a running app — sign in as `admin@example.com` / `password`.
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 `admin@example.com` / `password`.
12
12
 
13
13
  ## Options (mostly for automation)
14
14
 
@@ -17,7 +17,9 @@ The bare command above is the canonical UX. The flags below exist for tests and
17
17
  | Option | Description |
18
18
  |--------|-------------|
19
19
  | `-t, --type <type>` | Package type: `monorepo`, `webapp`, or `library` (skips the type prompt) |
20
- | `--name <name>` | Project name (skips the name prompt) |
20
+ | `--name <name>` | Package/app name (skips the package name prompt) |
21
+ | `--repo-name <name>` | Repo name when creating a new monorepo (skips the repo name prompt) |
22
+ | `--cwd <dir>` | Run as if the CLI was started from `<dir>` |
21
23
  | `--skip-install` | Skip dependency installation, which also skips the auto-run setup + dev + browser, leaving you with manual next-steps |
22
24
  | `-y, --yes` | Skip all prompts; requires `--name` |
23
25
 
@@ -33,7 +35,7 @@ The bare command above is the canonical UX. The flags below exist for tests and
33
35
 
34
36
  `create` auto-detects whether you're inside an existing pnpm monorepo (by walking up for `pnpm-workspace.yaml`) and changes its prompts accordingly:
35
37
 
36
- - **Outside a monorepo** — you're asked "Initialize with a webapp?" (Y/n, default Y) before the project name. Picking the webapp option scaffolds a monorepo with a webapp inside `packages/<name>/`. Declining gives you an empty monorepo.
38
+ - **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.
37
39
  - **Inside a monorepo** — pick `Webapp` (default) or `Library` to add a new package under the workspace pattern.
38
40
 
39
41
  ## Happy-path: zero-friction webapp
@@ -41,10 +43,9 @@ The bare command above is the canonical UX. The flags below exist for tests and
41
43
  When you scaffold a webapp (the default flow), `create` automatically runs:
42
44
 
43
45
  1. `pnpm install` (at the monorepo root)
44
- 2. `pnpm install --ignore-workspace` (inside the webapp package, to create the Docker build lockfile)
45
- 3. `pnpm run setup` — Docker Compose Postgres + Drizzle migrations + seed user
46
- 4. `pnpm dev` Next.js dev server
47
- 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
48
49
 
49
50
  Sign in as `admin@example.com` / `password` to start building.
50
51
 
@@ -75,9 +76,13 @@ pnpm build
75
76
  ### Testing locally
76
77
 
77
78
  ```bash
78
- pnpm build
79
- npm link
80
- create test-app
79
+ pnpm create:local --cwd /tmp --name test-app --yes --skip-install
80
+ ```
81
+
82
+ From the repo root, the same script can be run with a filter:
83
+
84
+ ```bash
85
+ pnpm --filter @percepta/create create:local --cwd /tmp --name test-app --yes --skip-install
81
86
  ```
82
87
 
83
88
  ### Syncing template files
@@ -77,6 +77,7 @@ async function promptInsideMonorepoType() {
77
77
  }
78
78
  async function promptProjectDetails(defaults) {
79
79
  const inMonorepo = defaults.monorepoContext?.found ?? false;
80
+ const cwd = defaults.cwd ?? process.cwd();
80
81
  let projectType;
81
82
  let finalName;
82
83
  if (inMonorepo) {
@@ -84,12 +85,40 @@ async function promptProjectDetails(defaults) {
84
85
  await defaults.beforeNamePrompt?.(projectType);
85
86
  finalName = defaults.name || await promptName("Package name?");
86
87
  } else {
88
+ const repoName = defaults.repoName || (defaults.projectType === "monorepo" ? defaults.name : void 0) || await promptName("Repo name?");
89
+ const repoTitle = toTitleCase(repoName);
87
90
  projectType = defaults.projectType ?? await promptOutsideMonorepoType();
88
91
  await defaults.beforeNamePrompt?.(projectType);
89
- finalName = defaults.name || await promptName("Project name?");
92
+ if (projectType === "monorepo") {
93
+ finalName = repoName;
94
+ const finalTitle3 = repoTitle;
95
+ const finalDirectory3 = path.resolve(cwd, repoName);
96
+ return {
97
+ projectType,
98
+ directory: finalDirectory3,
99
+ name: finalName,
100
+ title: finalTitle3,
101
+ installDeps: !defaults.skipInstall,
102
+ monorepoName: repoName,
103
+ monorepoTitle: repoTitle
104
+ };
105
+ }
106
+ const packageNamePrompt = projectType === "webapp" ? "Webapp name?" : "Library name?";
107
+ finalName = defaults.name || await promptName(packageNamePrompt);
108
+ const finalTitle2 = toTitleCase(finalName);
109
+ const finalDirectory2 = path.resolve(cwd, repoName);
110
+ return {
111
+ projectType,
112
+ directory: finalDirectory2,
113
+ name: finalName,
114
+ title: finalTitle2,
115
+ installDeps: !defaults.skipInstall,
116
+ monorepoName: repoName,
117
+ monorepoTitle: repoTitle
118
+ };
90
119
  }
91
120
  const finalTitle = finalName ? toTitleCase(finalName) : "";
92
- const finalDirectory = !inMonorepo && finalName ? path.resolve(process.cwd(), finalName) : "";
121
+ const finalDirectory = !inMonorepo && finalName ? path.resolve(cwd, finalName) : "";
93
122
  return {
94
123
  projectType,
95
124
  directory: finalDirectory,
@@ -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-7NPWSTCY.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",
@@ -262,11 +264,30 @@ async function generateEnvLocal(packageDir) {
262
264
  const examplePath = path4.join(packageDir, ".env.example");
263
265
  const localPath = path4.join(packageDir, ".env.local");
264
266
  if (!await fs4.pathExists(examplePath)) return;
265
- if (await fs4.pathExists(localPath)) return;
266
- const authSecret = randomBytes(32).toString("base64");
267
- const encKey = randomBytes(16).toString("hex");
268
- const content = (await fs4.readFile(examplePath, "utf-8")).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${authSecret}`).replace(/^ENCRYPTION_SECRET_KEY=.*$/m, `ENCRYPTION_SECRET_KEY=${encKey}`);
269
- await fs4.writeFile(localPath, content);
267
+ if (!await fs4.pathExists(localPath)) {
268
+ const authSecret = randomBytes(32).toString("base64");
269
+ const encKey = randomBytes(16).toString("hex");
270
+ const content = (await fs4.readFile(examplePath, "utf-8")).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${authSecret}`).replace(/^ENCRYPTION_SECRET_KEY=.*$/m, `ENCRYPTION_SECRET_KEY=${encKey}`);
271
+ await fs4.writeFile(localPath, content);
272
+ }
273
+ const ryvnSecretsPath = path4.join(
274
+ packageDir,
275
+ "deploy",
276
+ "ryvn",
277
+ "percepta-test.secrets.env"
278
+ );
279
+ if (await fs4.pathExists(ryvnSecretsPath)) return;
280
+ const deployAuthSecret = randomBytes(32).toString("base64");
281
+ const deployEncKey = randomBytes(16).toString("hex");
282
+ const deploySecrets = [
283
+ `BETTER_AUTH_SECRET=${deployAuthSecret}`,
284
+ `ENCRYPTION_SECRET_KEY=${deployEncKey}`,
285
+ "",
286
+ "# Langfuse and LLM demo credentials are inherited from the demos-commons Ryvn variable group.",
287
+ ""
288
+ ].join("\n");
289
+ await fs4.ensureDir(path4.dirname(ryvnSecretsPath));
290
+ await fs4.writeFile(ryvnSecretsPath, deploySecrets);
270
291
  }
271
292
 
272
293
  // src/utils/relocate-workflows.ts
@@ -509,7 +530,7 @@ async function writeMosaicFiles(packageDir, config, projectType) {
509
530
  templateVersion: templateVersions[projectType] || "1.0.0",
510
531
  templateCommit: "npm",
511
532
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
512
- placeholders: derivePlaceholders(config.name, config.title),
533
+ placeholders: derivePlaceholders(config.name, config.title, config.repoName),
513
534
  source: {
514
535
  templatePath: `packages/create-mosaic-module/templates/${projectType}`
515
536
  }
@@ -529,6 +550,17 @@ _None yet \u2014 freshly created from template._
529
550
  `
530
551
  );
531
552
  }
553
+ function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
554
+ return {
555
+ name,
556
+ title,
557
+ dbName: `${toSnakeCase(name)}_db`,
558
+ nameUpper: name.toUpperCase(),
559
+ nameSnake: toSnakeCase(name),
560
+ repoName,
561
+ repoNameSnake: toSnakeCase(repoName)
562
+ };
563
+ }
532
564
  async function scaffoldMonorepo(targetDir, config) {
533
565
  const monoSpinner = ora("Copying monorepo template...").start();
534
566
  try {
@@ -631,27 +663,6 @@ async function installAtMonorepoRoot(monorepoRoot, installDeps) {
631
663
  return false;
632
664
  }
633
665
  }
634
- async function installAtWebappPackage(packageDir, projectType, installDeps) {
635
- if (!installDeps || !packageDir || projectType !== "webapp") {
636
- return projectType !== "webapp";
637
- }
638
- const spinner = ora(
639
- `Generating package lockfile with ${PACKAGE_MANAGER}...`
640
- ).start();
641
- try {
642
- await runPackageManagerInstall(PACKAGE_MANAGER, packageDir, [
643
- "install",
644
- "--ignore-workspace"
645
- ]);
646
- spinner.succeed("Generated package lockfile");
647
- return true;
648
- } catch {
649
- spinner.warn(
650
- `Failed to generate package lockfile. Run '${PACKAGE_MANAGER} install --ignore-workspace' from ${packageDir}.`
651
- );
652
- return false;
653
- }
654
- }
655
666
  async function maybeAutoRunWebapp(packageDir, projectType, installSucceeded) {
656
667
  if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
657
668
  return autoRunWebapp(packageDir);
@@ -700,6 +711,7 @@ function requireNpmTokenForWebappInstall(projectType, installDeps) {
700
711
  process.exit(1);
701
712
  }
702
713
  async function createProject(options) {
714
+ const cwd = await resolveCreateCwd(options.cwd);
703
715
  if (options.type !== void 0 && !isValidProjectType(options.type)) {
704
716
  console.error(
705
717
  chalk.red(
@@ -708,7 +720,6 @@ async function createProject(options) {
708
720
  );
709
721
  process.exit(1);
710
722
  }
711
- const cwd = process.cwd();
712
723
  console.log();
713
724
  console.log(chalk.bold("Creating a new Mosaic package..."));
714
725
  console.log();
@@ -735,6 +746,7 @@ async function createProject(options) {
735
746
  }
736
747
  console.log();
737
748
  const projectName = options.name;
749
+ const repoName = options.repoName;
738
750
  if (options.yes && !projectName) {
739
751
  console.error(
740
752
  chalk.red("Error: --name is required when using --yes flag")
@@ -748,38 +760,48 @@ async function createProject(options) {
748
760
  process.exit(1);
749
761
  }
750
762
  }
763
+ if (repoName) {
764
+ const validation = validateProjectName(toKebabCase(repoName));
765
+ if (!validation.valid) {
766
+ console.error(chalk.red(`Invalid repo name: ${validation.error}`));
767
+ process.exit(1);
768
+ }
769
+ }
751
770
  let answers;
752
771
  if (options.yes) {
753
772
  const projectType = options.type || "webapp";
754
773
  requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
755
774
  const kebabName = toKebabCase(projectName);
756
- const directory = monorepoContext.found && monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, kebabName) : path7.resolve(cwd, kebabName);
775
+ const kebabRepoName = repoName ? toKebabCase(repoName) : kebabName;
776
+ const directory = monorepoContext.found && monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, kebabName) : path7.resolve(cwd, kebabRepoName);
757
777
  answers = {
758
778
  projectType,
759
779
  directory,
760
780
  name: kebabName,
761
781
  title: toTitleCase(kebabName),
762
- installDeps: !options.skipInstall
782
+ installDeps: !options.skipInstall,
783
+ monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
784
+ monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
763
785
  };
764
786
  } else {
765
787
  answers = await promptProjectDetails({
766
788
  projectType: options.type,
767
789
  name: projectName ? toKebabCase(projectName) : void 0,
790
+ repoName: repoName ? toKebabCase(repoName) : void 0,
768
791
  skipInstall: options.skipInstall,
769
792
  monorepoContext,
793
+ cwd,
770
794
  beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
771
795
  });
772
796
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) {
773
797
  answers.directory = path7.join(monorepoContext.packageDir, answers.name);
774
798
  }
775
799
  }
776
- const config = {
777
- name: answers.name,
778
- title: answers.title,
779
- dbName: toSnakeCase(answers.name) + "_db",
780
- nameUpper: answers.name.toUpperCase(),
781
- nameSnake: toSnakeCase(answers.name)
782
- };
800
+ const monorepoName = answers.monorepoName ?? answers.name;
801
+ const monorepoTitle = answers.monorepoTitle ?? toTitleCase(monorepoName);
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);
783
805
  const typeLabel = getProjectTypeLabel(answers.projectType);
784
806
  if (monorepoContext.found) {
785
807
  const monorepoRoot = monorepoContext.rootDir;
@@ -810,16 +832,10 @@ async function createProject(options) {
810
832
  });
811
833
  }
812
834
  await warnIfMissingRootNpmrc(monorepoRoot);
813
- const rootInstallSucceeded = await installAtMonorepoRoot(
835
+ const installSucceeded = await installAtMonorepoRoot(
814
836
  monorepoRoot,
815
837
  answers.installDeps
816
838
  );
817
- const packageInstallSucceeded = await installAtWebappPackage(
818
- packageDir,
819
- answers.projectType,
820
- answers.installDeps
821
- );
822
- const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
823
839
  console.log();
824
840
  console.log(
825
841
  chalk.green("\u2714"),
@@ -833,7 +849,7 @@ async function createProject(options) {
833
849
  installSucceeded
834
850
  );
835
851
  if (devStarted) return;
836
- printNextStepsExisting(answers, options, packageDir);
852
+ printNextStepsExisting(answers, packageDir);
837
853
  } else {
838
854
  const isBareMonorepo = answers.projectType === "monorepo";
839
855
  const monorepoRoot = answers.directory;
@@ -841,11 +857,12 @@ async function createProject(options) {
841
857
  if (isBareMonorepo) {
842
858
  console.log(chalk.dim(" Type:"), typeLabel);
843
859
  console.log(chalk.dim(" Directory:"), monorepoRoot);
844
- console.log(chalk.dim(" Name:"), config.name);
845
- console.log(chalk.dim(" Title:"), config.title);
860
+ console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
861
+ console.log(chalk.dim(" Title:"), monorepoConfig.title);
846
862
  } else {
847
863
  console.log(chalk.dim(" Package type:"), typeLabel);
848
864
  console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
865
+ console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
849
866
  console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
850
867
  console.log(chalk.dim(" Name:"), config.name);
851
868
  console.log(chalk.dim(" Title:"), config.title);
@@ -863,7 +880,7 @@ async function createProject(options) {
863
880
  process.exit(1);
864
881
  }
865
882
  }
866
- await scaffoldMonorepo(monorepoRoot, config);
883
+ await scaffoldMonorepo(monorepoRoot, monorepoConfig);
867
884
  if (packageDir && answers.projectType !== "monorepo") {
868
885
  await addPackageToMonorepo({
869
886
  packageDir,
@@ -873,16 +890,10 @@ async function createProject(options) {
873
890
  });
874
891
  }
875
892
  initGitRepo(monorepoRoot);
876
- const rootInstallSucceeded = await installAtMonorepoRoot(
893
+ const installSucceeded = await installAtMonorepoRoot(
877
894
  monorepoRoot,
878
895
  answers.installDeps
879
896
  );
880
- const packageInstallSucceeded = await installAtWebappPackage(
881
- packageDir,
882
- answers.projectType,
883
- answers.installDeps
884
- );
885
- const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
886
897
  console.log();
887
898
  console.log(
888
899
  chalk.green("\u2714"),
@@ -903,8 +914,23 @@ async function createProject(options) {
903
914
  installSucceeded
904
915
  );
905
916
  if (devStarted) return;
906
- printNextStepsNew(answers, options, monorepoRoot);
917
+ printNextStepsNew(answers, monorepoRoot);
918
+ }
919
+ }
920
+ async function resolveCreateCwd(cwdOption) {
921
+ const cwd = cwdOption ? path7.resolve(cwdOption) : process.cwd();
922
+ let stat;
923
+ try {
924
+ stat = await fs7.stat(cwd);
925
+ } catch {
926
+ console.error(chalk.red(`Error: --cwd directory does not exist: ${cwd}`));
927
+ process.exit(1);
928
+ }
929
+ if (!stat.isDirectory()) {
930
+ console.error(chalk.red(`Error: --cwd is not a directory: ${cwd}`));
931
+ process.exit(1);
907
932
  }
933
+ return cwd;
908
934
  }
909
935
  function printWebappNextSteps(params) {
910
936
  const {
@@ -916,14 +942,12 @@ function printWebappNextSteps(params) {
916
942
  } = params;
917
943
  const repoRel = shPath(monorepoRelativePath) || ".";
918
944
  const pkgFromRoot = `packages/${answers.name}`;
919
- const packageInstallStep = `${pm} install --ignore-workspace`;
920
945
  const pnpmSteps = ["pnpm run setup", "pnpm dev"];
921
946
  if (variant === "new") {
922
947
  const oneLinerParts2 = [];
923
948
  if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
924
949
  if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
925
950
  oneLinerParts2.push(`cd ${pkgFromRoot}`);
926
- if (!answers.installDeps) oneLinerParts2.push(packageInstallStep);
927
951
  oneLinerParts2.push(...pnpmSteps);
928
952
  console.log(chalk.bold("Copy-paste (from your current directory):"));
929
953
  console.log();
@@ -939,9 +963,6 @@ function printWebappNextSteps(params) {
939
963
  console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
940
964
  }
941
965
  console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
942
- if (!answers.installDeps) {
943
- console.log(chalk.dim(` ${step2++}.`), packageInstallStep);
944
- }
945
966
  for (const cmd of pnpmSteps) {
946
967
  console.log(chalk.dim(` ${step2++}.`), cmd);
947
968
  }
@@ -951,7 +972,7 @@ function printWebappNextSteps(params) {
951
972
  const oneLinerParts = [];
952
973
  if (!answers.installDeps) {
953
974
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
954
- oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`, packageInstallStep);
975
+ oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
955
976
  } else if (pkgRel !== ".") {
956
977
  oneLinerParts.push(`cd ${pkgRel}`);
957
978
  }
@@ -969,7 +990,6 @@ function printWebappNextSteps(params) {
969
990
  }
970
991
  console.log(chalk.dim(` ${step++}.`), `${pm} install`);
971
992
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
972
- console.log(chalk.dim(` ${step++}.`), packageInstallStep);
973
993
  } else if (pkgRel !== ".") {
974
994
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
975
995
  }
@@ -1006,7 +1026,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
1006
1026
  );
1007
1027
  console.log();
1008
1028
  }
1009
- function printNextStepsNew(answers, options, targetDir) {
1029
+ function printNextStepsNew(answers, targetDir) {
1010
1030
  const pm = PACKAGE_MANAGER;
1011
1031
  const relativePath = path7.relative(process.cwd(), targetDir) || ".";
1012
1032
  console.log("Next steps:");
@@ -1062,7 +1082,7 @@ function printNextStepsNew(answers, options, targetDir) {
1062
1082
  );
1063
1083
  console.log();
1064
1084
  }
1065
- function printNextStepsExisting(answers, options, packageDir) {
1085
+ function printNextStepsExisting(answers, packageDir) {
1066
1086
  const pm = PACKAGE_MANAGER;
1067
1087
  const packageRelativePath = path7.relative(process.cwd(), packageDir) || ".";
1068
1088
  const monorepoRoot = path7.dirname(path7.dirname(packageDir));
@@ -1106,30 +1126,30 @@ var packageJson = {
1106
1126
  version: "1.0.0"
1107
1127
  };
1108
1128
  program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
1109
- program.command("create", { isDefault: true }).description("Scaffold a new Mosaic package").option("-t, --type <type>", "Package type: monorepo, webapp, or library").option("--name <name>", "Project name").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);
1129
+ 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("--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);
1110
1130
  program.command("status").description("Show template sync status for current app").option(
1111
1131
  "--mosaic-template-path <path>",
1112
1132
  "Path to local mosaic repo checkout"
1113
1133
  ).action(async (options) => {
1114
- const { statusCommand } = await import("./status-BTHGN6QH.js");
1134
+ const { statusCommand } = await import("./status-QW5TQDYY.js");
1115
1135
  await statusCommand(options);
1116
1136
  });
1117
1137
  program.command("sync").description("Generate downstream sync context (template \u2192 app)").option(
1118
1138
  "--mosaic-template-path <path>",
1119
1139
  "Path to local mosaic repo checkout"
1120
1140
  ).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
1121
- const { syncCommand } = await import("./sync-3Q27L7XZ.js");
1141
+ const { syncCommand } = await import("./sync-RLBZDOFB.js");
1122
1142
  await syncCommand(options);
1123
1143
  });
1124
1144
  program.command("upstream").description("Generate upstream context (app \u2192 template)").option(
1125
1145
  "--mosaic-template-path <path>",
1126
1146
  "Path to local mosaic repo checkout"
1127
1147
  ).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
1128
- const { upstreamCommand } = await import("./upstream-C5KFAHVR.js");
1148
+ const { upstreamCommand } = await import("./upstream-TQFVPMEG.js");
1129
1149
  await upstreamCommand(options);
1130
1150
  });
1131
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) => {
1132
- const { initCommand } = await import("./init-NP6GRXLL.js");
1152
+ const { initCommand } = await import("./init-EQZ2TCSJ.js");
1133
1153
  await initCommand(options);
1134
1154
  });
1135
1155
  program.parse();
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  VALID_PROJECT_TYPES,
3
3
  isValidProjectType
4
- } from "./chunk-7NPWSTCY.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.0",
3
+ "version": "3.1.3",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "tsup": "^8.4.0",
28
28
  "typescript": "^5.7.3",
29
29
  "vitest": "^4.0.0",
30
- "@percepta/build": "0.4.0"
30
+ "@percepta/build": "0.4.1"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=18.0.0"
@@ -47,6 +47,7 @@
47
47
  "license": "MIT",
48
48
  "scripts": {
49
49
  "build": "tsup src/index.ts --format esm --dts --clean",
50
+ "create:local": "pnpm build && node dist/index.js",
50
51
  "dev": "tsup src/index.ts --format esm --watch",
51
52
  "typecheck": "tsc --noEmit",
52
53
  "sync-template": "tsx scripts/sync-template.ts",
@@ -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
@@ -21,6 +21,7 @@ Thumbs.db
21
21
  .env
22
22
  .env.local
23
23
  .env.*.local
24
+ **/deploy/ryvn/*.secrets.env
24
25
 
25
26
  # Logs
26
27
  *.log
@@ -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: