@percepta/create 3.0.1 → 3.1.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 (32) hide show
  1. package/README.md +6 -5
  2. package/dist/{chunk-GEVZERMP.js → chunk-7NPWSTCY.js} +3 -1
  3. package/dist/{chunk-R4FWPE4A.js → chunk-DCM7JOSC.js} +2 -2
  4. package/dist/index.js +207 -60
  5. package/dist/{init-Z4VGBHAK.js → init-NP6GRXLL.js} +1 -1
  6. package/dist/{status-MITGDLTT.js → status-BTHGN6QH.js} +1 -1
  7. package/dist/{sync-J4SFZHDX.js → sync-3Q27L7XZ.js} +1 -1
  8. package/dist/{upstream-AQI7P4EU.js → upstream-C5KFAHVR.js} +1 -1
  9. package/package.json +1 -1
  10. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +3 -2
  11. package/templates/webapp/AGENTS.md +8 -2
  12. package/templates/webapp/Dockerfile +0 -1
  13. package/templates/webapp/README.md +1 -0
  14. package/templates/webapp/agent-skills/database.md +1 -0
  15. package/templates/webapp/agent-skills/deploy.md +24 -27
  16. package/templates/webapp/agent-skills/oneshot.md +3 -3
  17. package/templates/webapp/deploy/README.md +8 -6
  18. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +0 -2
  19. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +11 -27
  20. package/templates/webapp/drizzle.config.ts +15 -6
  21. package/templates/webapp/env.example.template +1 -0
  22. package/templates/webapp/eslint.config.mjs +7 -0
  23. package/templates/webapp/package.json.template +6 -6
  24. package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +377 -0
  25. package/templates/webapp/scripts/seed.ts +1 -1
  26. package/templates/webapp/scripts/setup-database.ts +14 -0
  27. package/templates/webapp/src/app/global-error.tsx +2 -0
  28. package/templates/webapp/src/config/getEnvConfig.ts +1 -0
  29. package/templates/webapp/src/drizzle/db.ts +3 -0
  30. package/templates/webapp/src/drizzle/searchPath.test.ts +21 -0
  31. package/templates/webapp/src/drizzle/searchPath.ts +16 -0
  32. package/templates/webapp/src/styles/globals.css +0 -7
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 project name and (depending on context) whether it's a webapp. Defaults yield a running app — sign in as `admin@example.com` / `password`.
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`.
12
12
 
13
13
  ## Options (mostly for automation)
14
14
 
@@ -33,7 +33,7 @@ The bare command above is the canonical UX. The flags below exist for tests and
33
33
 
34
34
  `create` auto-detects whether you're inside an existing pnpm monorepo (by walking up for `pnpm-workspace.yaml`) and changes its prompts accordingly:
35
35
 
36
- - **Outside a monorepo** — pick `Monorepo` (default) or `Library`. If `Monorepo`, you're asked "Initialize with a webapp?" (Y/n, default Y). Picking the webapp option scaffolds a monorepo with a webapp inside `packages/<name>/`. Declining gives you an empty monorepo.
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.
37
37
  - **Inside a monorepo** — pick `Webapp` (default) or `Library` to add a new package under the workspace pattern.
38
38
 
39
39
  ## Happy-path: zero-friction webapp
@@ -41,9 +41,10 @@ The bare command above is the canonical UX. The flags below exist for tests and
41
41
  When you scaffold a webapp (the default flow), `create` automatically runs:
42
42
 
43
43
  1. `pnpm install` (at the monorepo root)
44
- 2. `pnpm run setup` Docker Compose Postgres + Drizzle migrations + seed user
45
- 3. `pnpm dev` — Next.js dev server
46
- 4. Opens the served URL in your default browser
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
47
48
 
48
49
  Sign in as `admin@example.com` / `password` to start building.
49
50
 
@@ -81,10 +81,12 @@ async function promptProjectDetails(defaults) {
81
81
  let finalName;
82
82
  if (inMonorepo) {
83
83
  projectType = defaults.projectType ?? await promptInsideMonorepoType();
84
+ await defaults.beforeNamePrompt?.(projectType);
84
85
  finalName = defaults.name || await promptName("Package name?");
85
86
  } else {
86
- finalName = defaults.name || await promptName("Project name?");
87
87
  projectType = defaults.projectType ?? await promptOutsideMonorepoType();
88
+ await defaults.beforeNamePrompt?.(projectType);
89
+ finalName = defaults.name || await promptName("Project name?");
88
90
  }
89
91
  const finalTitle = finalName ? toTitleCase(finalName) : "";
90
92
  const finalDirectory = !inMonorepo && finalName ? path.resolve(process.cwd(), finalName) : "";
@@ -11,14 +11,14 @@ function getLatestTemplateTag(type, repoPath) {
11
11
  { cwd: repoPath, encoding: "utf-8" }
12
12
  ).trim();
13
13
  if (!tags) return null;
14
- return tags.split("\n")[0];
14
+ return tags.split("\n")[0] ?? null;
15
15
  } catch {
16
16
  return null;
17
17
  }
18
18
  }
19
19
  function getTemplateVersionFromTag(tag) {
20
20
  const parts = tag.split("/");
21
- return parts[parts.length - 1];
21
+ return parts[parts.length - 1] ?? "";
22
22
  }
23
23
  function getTemplateDiff(repoPath, templatePath, fromTag, toTag) {
24
24
  return execFileSync(
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  toSnakeCase,
8
8
  toTitleCase,
9
9
  validateProjectName
10
- } from "./chunk-GEVZERMP.js";
10
+ } from "./chunk-7NPWSTCY.js";
11
11
  import {
12
12
  derivePlaceholders,
13
13
  writeManifest
@@ -17,9 +17,9 @@ import {
17
17
  import { program } from "commander";
18
18
 
19
19
  // src/commands/create.ts
20
- import path6 from "path";
20
+ import path7 from "path";
21
21
  import { fileURLToPath as fileURLToPath2 } from "url";
22
- import fs6 from "fs-extra";
22
+ import fs7 from "fs-extra";
23
23
  import chalk from "chalk";
24
24
  import ora from "ora";
25
25
  import { execSync, spawn } from "child_process";
@@ -294,14 +294,90 @@ async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
294
294
  }
295
295
  }
296
296
 
297
+ // src/utils/resolve-percepta-versions.ts
298
+ import path6 from "path";
299
+ import { execFile } from "child_process";
300
+ import { promisify } from "util";
301
+ import fs6 from "fs-extra";
302
+ var execFileAsync = promisify(execFile);
303
+ var DEPENDENCY_SECTIONS = [
304
+ "dependencies",
305
+ "devDependencies",
306
+ "optionalDependencies",
307
+ "peerDependencies"
308
+ ];
309
+ function getPerceptaPackages(pkg) {
310
+ const names = /* @__PURE__ */ new Set();
311
+ for (const section of DEPENDENCY_SECTIONS) {
312
+ const deps = pkg[section];
313
+ if (!deps) continue;
314
+ for (const name of Object.keys(deps)) {
315
+ if (name.startsWith("@percepta/")) {
316
+ names.add(name);
317
+ }
318
+ }
319
+ }
320
+ return [...names].sort();
321
+ }
322
+ async function npmViewDistTagLatest(packageName, cwd) {
323
+ const { stdout } = await execFileAsync(
324
+ "npm",
325
+ ["view", packageName, "dist-tags.latest", "--silent"],
326
+ {
327
+ cwd,
328
+ encoding: "utf8",
329
+ timeout: 5e3
330
+ }
331
+ );
332
+ const version = stdout.trim();
333
+ return version.length > 0 ? version : null;
334
+ }
335
+ async function resolvePerceptaVersionsInPackageJson(packageJsonPath, lookupLatest = npmViewDistTagLatest) {
336
+ const cwd = path6.dirname(packageJsonPath);
337
+ const pkg = await fs6.readJson(packageJsonPath);
338
+ const packageNames = getPerceptaPackages(pkg);
339
+ const resolved = {};
340
+ const failed = [];
341
+ const results = await Promise.all(
342
+ packageNames.map(async (packageName) => {
343
+ try {
344
+ return {
345
+ packageName,
346
+ latest: await lookupLatest(packageName, cwd)
347
+ };
348
+ } catch {
349
+ return { packageName, latest: null };
350
+ }
351
+ })
352
+ );
353
+ for (const { packageName, latest } of results) {
354
+ if (!latest) {
355
+ failed.push(packageName);
356
+ continue;
357
+ }
358
+ resolved[packageName] = latest;
359
+ for (const section of DEPENDENCY_SECTIONS) {
360
+ const deps = pkg[section];
361
+ if (deps?.[packageName]) {
362
+ deps[packageName] = latest;
363
+ }
364
+ }
365
+ }
366
+ if (Object.keys(resolved).length > 0) {
367
+ await fs6.writeJson(packageJsonPath, pkg, { spaces: 2 });
368
+ await fs6.appendFile(packageJsonPath, "\n");
369
+ }
370
+ return { resolved, failed };
371
+ }
372
+
297
373
  // src/commands/create.ts
298
374
  var PACKAGE_MANAGER = "pnpm";
299
375
  function shPath(p) {
300
- return p.split(path6.sep).join("/");
376
+ return p.split(path7.sep).join("/");
301
377
  }
302
- function runPackageManagerInstall(packageManager, cwd) {
378
+ function runPackageManagerInstall(packageManager, cwd, args = ["install"]) {
303
379
  return new Promise((resolve, reject) => {
304
- const child = spawn(packageManager, ["install"], {
380
+ const child = spawn(packageManager, args, {
305
381
  cwd,
306
382
  stdio: "ignore"
307
383
  });
@@ -311,7 +387,7 @@ function runPackageManagerInstall(packageManager, cwd) {
311
387
  else
312
388
  reject(
313
389
  new Error(
314
- `${packageManager} install exited with code ${code ?? "unknown"}`
390
+ `${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`
315
391
  )
316
392
  );
317
393
  });
@@ -415,12 +491,12 @@ async function autoRunWebapp(packageDir) {
415
491
  return true;
416
492
  }
417
493
  function readTemplateVersions() {
418
- const versionsPath = path6.resolve(
419
- path6.dirname(fileURLToPath2(import.meta.url)),
494
+ const versionsPath = path7.resolve(
495
+ path7.dirname(fileURLToPath2(import.meta.url)),
420
496
  "../template-versions.json"
421
497
  );
422
498
  try {
423
- const content = fs6.readFileSync(versionsPath, "utf-8");
499
+ const content = fs7.readFileSync(versionsPath, "utf-8");
424
500
  return JSON.parse(content);
425
501
  } catch {
426
502
  return {};
@@ -439,8 +515,8 @@ async function writeMosaicFiles(packageDir, config, projectType) {
439
515
  }
440
516
  };
441
517
  await writeManifest(packageDir, manifest);
442
- const notesPath = path6.join(packageDir, "mosaic-template-notes.md");
443
- await fs6.writeFile(
518
+ const notesPath = path7.join(packageDir, "mosaic-template-notes.md");
519
+ await fs7.writeFile(
444
520
  notesPath,
445
521
  `# Mosaic Divergence Notes
446
522
 
@@ -499,10 +575,32 @@ async function addPackageToMonorepo(args) {
499
575
  }
500
576
  await writeMosaicFiles(packageDir, config, projectType);
501
577
  if (projectType === "webapp") {
578
+ await resolvePerceptaPackageVersions(packageDir);
502
579
  await generateEnvLocal(packageDir);
503
580
  await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
504
581
  }
505
582
  }
583
+ async function resolvePerceptaPackageVersions(packageDir) {
584
+ const packageJsonPath = path7.join(packageDir, "package.json");
585
+ if (!await fs7.pathExists(packageJsonPath)) return;
586
+ const spinner = ora("Resolving latest @percepta/* versions...").start();
587
+ try {
588
+ const result = await resolvePerceptaVersionsInPackageJson(packageJsonPath);
589
+ const count = Object.keys(result.resolved).length;
590
+ if (result.failed.length > 0) {
591
+ spinner.warn(
592
+ `Resolved ${count} @percepta/* versions; kept existing ranges for ${result.failed.join(", ")}`
593
+ );
594
+ } else {
595
+ spinner.succeed(`Resolved ${count} @percepta/* versions`);
596
+ }
597
+ } catch (error) {
598
+ spinner.warn(
599
+ "Could not resolve latest @percepta/* versions; kept template ranges"
600
+ );
601
+ console.log(chalk.dim(error.message));
602
+ }
603
+ }
506
604
  function initGitRepo(targetDir) {
507
605
  const gitSpinner = ora("Initializing git repository...").start();
508
606
  try {
@@ -533,6 +631,27 @@ async function installAtMonorepoRoot(monorepoRoot, installDeps) {
533
631
  return false;
534
632
  }
535
633
  }
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
+ }
536
655
  async function maybeAutoRunWebapp(packageDir, projectType, installSucceeded) {
537
656
  if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
538
657
  return autoRunWebapp(packageDir);
@@ -551,6 +670,35 @@ function getProjectTypeLabel(projectType) {
551
670
  }
552
671
  }
553
672
  }
673
+ function requireNpmTokenForWebappInstall(projectType, installDeps) {
674
+ if (projectType !== "webapp" || !installDeps || process.env.NPM_TOKEN) {
675
+ return;
676
+ }
677
+ console.log();
678
+ console.error(chalk.red("Error: NPM_TOKEN environment variable is not set."));
679
+ console.error(
680
+ chalk.dim(" Required to install private @percepta/* packages.")
681
+ );
682
+ console.error();
683
+ console.error(" 1. Grab the npm token from 1Password:");
684
+ console.error(
685
+ chalk.cyan(
686
+ " https://start.1password.com/open/i?a=5TX2B4O3QNE4FNQ2A7ZJZDRRBI&v=j7trpyuqh7gt635dtuj6y4pwjm&i=cmmdi5trji7ctkn3fseakf4mgi&h=aitco.1password.com"
687
+ )
688
+ );
689
+ console.error(" 2. Add to ~/.zshrc:");
690
+ console.error(chalk.cyan(' export NPM_TOKEN="<paste-token>"'));
691
+ console.error(
692
+ " 3. Open a new terminal (or " + chalk.cyan("source ~/.zshrc") + ") and re-run."
693
+ );
694
+ console.error();
695
+ console.error(
696
+ chalk.dim(
697
+ " Or pass --skip-install to scaffold without running install."
698
+ )
699
+ );
700
+ process.exit(1);
701
+ }
554
702
  async function createProject(options) {
555
703
  if (options.type !== void 0 && !isValidProjectType(options.type)) {
556
704
  console.error(
@@ -603,8 +751,9 @@ async function createProject(options) {
603
751
  let answers;
604
752
  if (options.yes) {
605
753
  const projectType = options.type || "webapp";
754
+ requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
606
755
  const kebabName = toKebabCase(projectName);
607
- const directory = monorepoContext.found && monorepoContext.packageDir ? path6.join(monorepoContext.packageDir, kebabName) : path6.resolve(cwd, kebabName);
756
+ const directory = monorepoContext.found && monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, kebabName) : path7.resolve(cwd, kebabName);
608
757
  answers = {
609
758
  projectType,
610
759
  directory,
@@ -617,34 +766,13 @@ async function createProject(options) {
617
766
  projectType: options.type,
618
767
  name: projectName ? toKebabCase(projectName) : void 0,
619
768
  skipInstall: options.skipInstall,
620
- monorepoContext
769
+ monorepoContext,
770
+ beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
621
771
  });
622
772
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) {
623
- answers.directory = path6.join(monorepoContext.packageDir, answers.name);
773
+ answers.directory = path7.join(monorepoContext.packageDir, answers.name);
624
774
  }
625
775
  }
626
- if (answers.projectType === "webapp" && answers.installDeps && !process.env.NPM_TOKEN) {
627
- console.log();
628
- console.error(chalk.red("Error: NPM_TOKEN environment variable is not set."));
629
- console.error(chalk.dim(" Required to install private @percepta/* packages."));
630
- console.error();
631
- console.error(" 1. Grab the npm token from 1Password:");
632
- console.error(
633
- chalk.cyan(
634
- " https://start.1password.com/open/i?a=5TX2B4O3QNE4FNQ2A7ZJZDRRBI&v=j7trpyuqh7gt635dtuj6y4pwjm&i=cmmdi5trji7ctkn3fseakf4mgi&h=aitco.1password.com"
635
- )
636
- );
637
- console.error(" 2. Add to ~/.zshrc:");
638
- console.error(chalk.cyan(' export NPM_TOKEN="<paste-token>"'));
639
- console.error(" 3. Open a new terminal (or " + chalk.cyan("source ~/.zshrc") + ") and re-run.");
640
- console.error();
641
- console.error(
642
- chalk.dim(
643
- " Or pass --skip-install to scaffold without running install."
644
- )
645
- );
646
- process.exit(1);
647
- }
648
776
  const config = {
649
777
  name: answers.name,
650
778
  title: answers.title,
@@ -655,7 +783,7 @@ async function createProject(options) {
655
783
  const typeLabel = getProjectTypeLabel(answers.projectType);
656
784
  if (monorepoContext.found) {
657
785
  const monorepoRoot = monorepoContext.rootDir;
658
- const packageDir = monorepoContext.packageDir ? path6.join(monorepoContext.packageDir, answers.name) : answers.directory;
786
+ const packageDir = monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, answers.name) : answers.directory;
659
787
  console.log(chalk.dim(" Package type:"), typeLabel);
660
788
  console.log(chalk.dim(" Target:"), packageDir);
661
789
  console.log(chalk.dim(" Name:"), config.name);
@@ -664,8 +792,8 @@ async function createProject(options) {
664
792
  console.log(chalk.dim(" Database:"), config.dbName);
665
793
  }
666
794
  console.log();
667
- if (await fs6.pathExists(packageDir)) {
668
- const files = await fs6.readdir(packageDir);
795
+ if (await fs7.pathExists(packageDir)) {
796
+ const files = await fs7.readdir(packageDir);
669
797
  if (files.length > 0) {
670
798
  console.error(
671
799
  chalk.red(`Error: Directory ${packageDir} is not empty.`)
@@ -682,15 +810,21 @@ async function createProject(options) {
682
810
  });
683
811
  }
684
812
  await warnIfMissingRootNpmrc(monorepoRoot);
685
- const installSucceeded = await installAtMonorepoRoot(
813
+ const rootInstallSucceeded = await installAtMonorepoRoot(
686
814
  monorepoRoot,
687
815
  answers.installDeps
688
816
  );
817
+ const packageInstallSucceeded = await installAtWebappPackage(
818
+ packageDir,
819
+ answers.projectType,
820
+ answers.installDeps
821
+ );
822
+ const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
689
823
  console.log();
690
824
  console.log(
691
825
  chalk.green("\u2714"),
692
826
  chalk.bold(`Created ${typeLabel} at`),
693
- chalk.cyan(path6.relative(monorepoRoot, packageDir))
827
+ chalk.cyan(path7.relative(monorepoRoot, packageDir))
694
828
  );
695
829
  console.log();
696
830
  const devStarted = await maybeAutoRunWebapp(
@@ -703,7 +837,7 @@ async function createProject(options) {
703
837
  } else {
704
838
  const isBareMonorepo = answers.projectType === "monorepo";
705
839
  const monorepoRoot = answers.directory;
706
- const packageDir = isBareMonorepo ? null : path6.join(monorepoRoot, "packages", answers.name);
840
+ const packageDir = isBareMonorepo ? null : path7.join(monorepoRoot, "packages", answers.name);
707
841
  if (isBareMonorepo) {
708
842
  console.log(chalk.dim(" Type:"), typeLabel);
709
843
  console.log(chalk.dim(" Directory:"), monorepoRoot);
@@ -720,8 +854,8 @@ async function createProject(options) {
720
854
  }
721
855
  }
722
856
  console.log();
723
- if (await fs6.pathExists(monorepoRoot)) {
724
- const files = await fs6.readdir(monorepoRoot);
857
+ if (await fs7.pathExists(monorepoRoot)) {
858
+ const files = await fs7.readdir(monorepoRoot);
725
859
  if (files.length > 0) {
726
860
  console.error(
727
861
  chalk.red(`Error: Directory ${monorepoRoot} is not empty.`)
@@ -739,10 +873,16 @@ async function createProject(options) {
739
873
  });
740
874
  }
741
875
  initGitRepo(monorepoRoot);
742
- const installSucceeded = await installAtMonorepoRoot(
876
+ const rootInstallSucceeded = await installAtMonorepoRoot(
743
877
  monorepoRoot,
744
878
  answers.installDeps
745
879
  );
880
+ const packageInstallSucceeded = await installAtWebappPackage(
881
+ packageDir,
882
+ answers.projectType,
883
+ answers.installDeps
884
+ );
885
+ const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
746
886
  console.log();
747
887
  console.log(
748
888
  chalk.green("\u2714"),
@@ -776,12 +916,15 @@ function printWebappNextSteps(params) {
776
916
  } = params;
777
917
  const repoRel = shPath(monorepoRelativePath) || ".";
778
918
  const pkgFromRoot = `packages/${answers.name}`;
919
+ const packageInstallStep = `${pm} install --ignore-workspace`;
779
920
  const pnpmSteps = ["pnpm run setup", "pnpm dev"];
780
921
  if (variant === "new") {
781
922
  const oneLinerParts2 = [];
782
923
  if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
783
924
  if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
784
- oneLinerParts2.push(`cd ${pkgFromRoot}`, ...pnpmSteps);
925
+ oneLinerParts2.push(`cd ${pkgFromRoot}`);
926
+ if (!answers.installDeps) oneLinerParts2.push(packageInstallStep);
927
+ oneLinerParts2.push(...pnpmSteps);
785
928
  console.log(chalk.bold("Copy-paste (from your current directory):"));
786
929
  console.log();
787
930
  console.log(chalk.cyan(` ${oneLinerParts2.join(" && ")}`));
@@ -796,6 +939,9 @@ function printWebappNextSteps(params) {
796
939
  console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
797
940
  }
798
941
  console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
942
+ if (!answers.installDeps) {
943
+ console.log(chalk.dim(` ${step2++}.`), packageInstallStep);
944
+ }
799
945
  for (const cmd of pnpmSteps) {
800
946
  console.log(chalk.dim(` ${step2++}.`), cmd);
801
947
  }
@@ -805,7 +951,7 @@ function printWebappNextSteps(params) {
805
951
  const oneLinerParts = [];
806
952
  if (!answers.installDeps) {
807
953
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
808
- oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
954
+ oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`, packageInstallStep);
809
955
  } else if (pkgRel !== ".") {
810
956
  oneLinerParts.push(`cd ${pkgRel}`);
811
957
  }
@@ -823,6 +969,7 @@ function printWebappNextSteps(params) {
823
969
  }
824
970
  console.log(chalk.dim(` ${step++}.`), `${pm} install`);
825
971
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
972
+ console.log(chalk.dim(` ${step++}.`), packageInstallStep);
826
973
  } else if (pkgRel !== ".") {
827
974
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
828
975
  }
@@ -831,10 +978,10 @@ function printWebappNextSteps(params) {
831
978
  }
832
979
  }
833
980
  async function warnIfMissingRootNpmrc(rootDir) {
834
- const rootNpmrc = path6.join(rootDir, ".npmrc");
981
+ const rootNpmrc = path7.join(rootDir, ".npmrc");
835
982
  let contents = "";
836
- if (await fs6.pathExists(rootNpmrc)) {
837
- contents = await fs6.readFile(rootNpmrc, "utf8");
983
+ if (await fs7.pathExists(rootNpmrc)) {
984
+ contents = await fs7.readFile(rootNpmrc, "utf8");
838
985
  }
839
986
  if (contents.includes("@percepta:registry")) {
840
987
  return;
@@ -849,7 +996,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
849
996
  " pnpm reads .npmrc from the workspace root, so add these lines to"
850
997
  )
851
998
  );
852
- console.log(chalk.dim(` ${path6.join(rootDir, ".npmrc")}:`));
999
+ console.log(chalk.dim(` ${path7.join(rootDir, ".npmrc")}:`));
853
1000
  console.log();
854
1001
  console.log(
855
1002
  chalk.cyan(" @percepta:registry=https://registry.npmjs.org/")
@@ -861,7 +1008,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
861
1008
  }
862
1009
  function printNextStepsNew(answers, options, targetDir) {
863
1010
  const pm = PACKAGE_MANAGER;
864
- const relativePath = path6.relative(process.cwd(), targetDir) || ".";
1011
+ const relativePath = path7.relative(process.cwd(), targetDir) || ".";
865
1012
  console.log("Next steps:");
866
1013
  console.log();
867
1014
  switch (answers.projectType) {
@@ -917,9 +1064,9 @@ function printNextStepsNew(answers, options, targetDir) {
917
1064
  }
918
1065
  function printNextStepsExisting(answers, options, packageDir) {
919
1066
  const pm = PACKAGE_MANAGER;
920
- const packageRelativePath = path6.relative(process.cwd(), packageDir) || ".";
921
- const monorepoRoot = path6.dirname(path6.dirname(packageDir));
922
- const monorepoRelativePath = path6.relative(process.cwd(), monorepoRoot) || ".";
1067
+ const packageRelativePath = path7.relative(process.cwd(), packageDir) || ".";
1068
+ const monorepoRoot = path7.dirname(path7.dirname(packageDir));
1069
+ const monorepoRelativePath = path7.relative(process.cwd(), monorepoRoot) || ".";
923
1070
  console.log("Next steps:");
924
1071
  console.log();
925
1072
  switch (answers.projectType) {
@@ -964,25 +1111,25 @@ program.command("status").description("Show template sync status for current app
964
1111
  "--mosaic-template-path <path>",
965
1112
  "Path to local mosaic repo checkout"
966
1113
  ).action(async (options) => {
967
- const { statusCommand } = await import("./status-MITGDLTT.js");
1114
+ const { statusCommand } = await import("./status-BTHGN6QH.js");
968
1115
  await statusCommand(options);
969
1116
  });
970
1117
  program.command("sync").description("Generate downstream sync context (template \u2192 app)").option(
971
1118
  "--mosaic-template-path <path>",
972
1119
  "Path to local mosaic repo checkout"
973
1120
  ).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
974
- const { syncCommand } = await import("./sync-J4SFZHDX.js");
1121
+ const { syncCommand } = await import("./sync-3Q27L7XZ.js");
975
1122
  await syncCommand(options);
976
1123
  });
977
1124
  program.command("upstream").description("Generate upstream context (app \u2192 template)").option(
978
1125
  "--mosaic-template-path <path>",
979
1126
  "Path to local mosaic repo checkout"
980
1127
  ).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
981
- const { upstreamCommand } = await import("./upstream-AQI7P4EU.js");
1128
+ const { upstreamCommand } = await import("./upstream-C5KFAHVR.js");
982
1129
  await upstreamCommand(options);
983
1130
  });
984
1131
  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) => {
985
- const { initCommand } = await import("./init-Z4VGBHAK.js");
1132
+ const { initCommand } = await import("./init-NP6GRXLL.js");
986
1133
  await initCommand(options);
987
1134
  });
988
1135
  program.parse();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  VALID_PROJECT_TYPES,
3
3
  isValidProjectType
4
- } from "./chunk-GEVZERMP.js";
4
+ } from "./chunk-7NPWSTCY.js";
5
5
  import {
6
6
  derivePlaceholders,
7
7
  manifestExists,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLatestTemplateTag,
3
3
  getTemplateVersionFromTag
4
- } from "./chunk-R4FWPE4A.js";
4
+ } from "./chunk-DCM7JOSC.js";
5
5
  import {
6
6
  readManifest
7
7
  } from "./chunk-WMJT7CB5.js";
@@ -2,7 +2,7 @@ import {
2
2
  getLatestTemplateTag,
3
3
  getTemplateDiff,
4
4
  getTemplateVersionFromTag
5
- } from "./chunk-R4FWPE4A.js";
5
+ } from "./chunk-DCM7JOSC.js";
6
6
  import {
7
7
  readManifest,
8
8
  resolveMosaicTemplatePath
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getFileAtTag
3
- } from "./chunk-R4FWPE4A.js";
3
+ } from "./chunk-DCM7JOSC.js";
4
4
  import {
5
5
  readManifest,
6
6
  resolveMosaicTemplatePath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@ on:
9
9
  - "packages/__APP_NAME__/scripts/**"
10
10
  - "packages/__APP_NAME__/Dockerfile"
11
11
  - "packages/__APP_NAME__/package.json"
12
- - "pnpm-lock.yaml"
12
+ - "packages/__APP_NAME__/pnpm-lock.yaml"
13
13
  - ".github/workflows/__APP_NAME__-ryvn-release.yaml"
14
14
  pull_request:
15
15
  branches:
@@ -19,7 +19,7 @@ on:
19
19
  - "packages/__APP_NAME__/scripts/**"
20
20
  - "packages/__APP_NAME__/Dockerfile"
21
21
  - "packages/__APP_NAME__/package.json"
22
- - "pnpm-lock.yaml"
22
+ - "packages/__APP_NAME__/pnpm-lock.yaml"
23
23
  workflow_dispatch:
24
24
 
25
25
  env:
@@ -66,6 +66,7 @@ jobs:
66
66
  service_name: ${{ env.SERVICE_NAME }}
67
67
  version: ${{ steps.generate-tag.outputs.version }}
68
68
  build_only: ${{ !(github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || steps.generate-tag.outputs.isPreview == 'true') }}
69
+ build_args: NPM_TOKEN=${{ secrets.NPM_TOKEN }}
69
70
  ryvn_client_id: ${{ secrets.RYVN_CLIENT_ID }}
70
71
  ryvn_client_secret: ${{ secrets.RYVN_CLIENT_SECRET }}
71
72
 
@@ -24,7 +24,7 @@ Next.js 15 full-stack application scaffolded from the Mosaic webapp template via
24
24
  - Logger messages must be plain string literals, not variables or templates
25
25
  - `no-process-env` is enforced — use `getEnvConfig()` from `src/config/`
26
26
  - Use `@percepta/design` components before writing custom UI
27
- - Use `AsyncContent` from `@percepta/components` for loading/error states
27
+ - For loading/error states, use local UI or add `@percepta/components` if you want `AsyncContent`
28
28
  - Tailwind CSS for all styling; icons from `lucide-react`
29
29
 
30
30
  ## Project Structure
@@ -114,10 +114,16 @@ export default [
114
114
 
115
115
  Vitest config is also available via `@percepta/build/vitest`.
116
116
 
117
- ### @percepta/components — React Utilities
117
+ ### @percepta/components — Optional React Utilities
118
118
 
119
119
  Async data handling and hooks for React Query:
120
120
 
121
+ Install this package first if you want these helpers:
122
+
123
+ ```bash
124
+ pnpm add @percepta/components
125
+ ```
126
+
121
127
  - **`AsyncContent<T>`** — renders loading spinner, error state, or children based on a React Query result. Use this for all data-fetching UI.
122
128
  - **`AsyncArrayContent<T[]>`** — same but for multiple parallel queries
123
129
  - **`ErrorContainer`** — consistent error display
@@ -45,7 +45,6 @@ ENV BASE_PATH=${BASE_PATH}
45
45
  # Copy built app from builder stage
46
46
  COPY --from=builder /app/.next/standalone ./
47
47
  COPY --from=builder /app/.next/static ./.next/static
48
- COPY --from=builder /app/public ./public
49
48
 
50
49
  # Copy scripts and source files needed for start.sh and runtime
51
50
  COPY --from=builder /app/scripts ./scripts
@@ -134,6 +134,7 @@ pnpm db:seed
134
134
  | `DATABASE_USERNAME` | Database user | `postgres` |
135
135
  | `DATABASE_PASSWORD` | Database password | `postgres` |
136
136
  | `DATABASE_NAME` | Database name | `__DB_NAME__` |
137
+ | `DATABASE_SCHEMA` | Optional Postgres schema/search path | - |
137
138
  | `DATABASE_USE_SSL` | Enable SSL | `false` |
138
139
 
139
140
  ### Security