@percepta/create 3.0.0 → 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 +371 -210
  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/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,10 +17,9 @@ import {
17
17
  import { program } from "commander";
18
18
 
19
19
  // src/commands/create.ts
20
- import path4 from "path";
20
+ import path7 from "path";
21
21
  import { fileURLToPath as fileURLToPath2 } from "url";
22
- import { randomBytes } from "crypto";
23
- import fs4 from "fs-extra";
22
+ import fs7 from "fs-extra";
24
23
  import chalk from "chalk";
25
24
  import ora from "ora";
26
25
  import { execSync, spawn } from "child_process";
@@ -255,14 +254,130 @@ async function detectMonorepo(startDir) {
255
254
  return NOT_FOUND;
256
255
  }
257
256
 
257
+ // src/utils/env-local.ts
258
+ import path4 from "path";
259
+ import { randomBytes } from "crypto";
260
+ import fs4 from "fs-extra";
261
+ async function generateEnvLocal(packageDir) {
262
+ const examplePath = path4.join(packageDir, ".env.example");
263
+ const localPath = path4.join(packageDir, ".env.local");
264
+ 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);
270
+ }
271
+
272
+ // src/utils/relocate-workflows.ts
273
+ import path5 from "path";
274
+ import fs5 from "fs-extra";
275
+ async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
276
+ const sourceDir = path5.join(packageDir, ".github", "workflows");
277
+ if (!await fs5.pathExists(sourceDir)) return;
278
+ const targetDir = path5.join(monorepoRoot, ".github", "workflows");
279
+ await fs5.ensureDir(targetDir);
280
+ const entries = await fs5.readdir(sourceDir);
281
+ for (const name of entries) {
282
+ if (!name.startsWith(`${appName}-`)) continue;
283
+ if (!/\.(ya?ml)$/.test(name)) continue;
284
+ await fs5.move(path5.join(sourceDir, name), path5.join(targetDir, name), {
285
+ overwrite: true
286
+ });
287
+ }
288
+ if ((await fs5.readdir(sourceDir)).length === 0) {
289
+ await fs5.rmdir(sourceDir);
290
+ const packageGithub = path5.join(packageDir, ".github");
291
+ if ((await fs5.readdir(packageGithub)).length === 0) {
292
+ await fs5.rmdir(packageGithub);
293
+ }
294
+ }
295
+ }
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
+
258
373
  // src/commands/create.ts
259
374
  var PACKAGE_MANAGER = "pnpm";
260
375
  function shPath(p) {
261
- return p.split(path4.sep).join("/");
376
+ return p.split(path7.sep).join("/");
262
377
  }
263
- function runPackageManagerInstall(packageManager, cwd) {
378
+ function runPackageManagerInstall(packageManager, cwd, args = ["install"]) {
264
379
  return new Promise((resolve, reject) => {
265
- const child = spawn(packageManager, ["install"], {
380
+ const child = spawn(packageManager, args, {
266
381
  cwd,
267
382
  stdio: "ignore"
268
383
  });
@@ -272,7 +387,7 @@ function runPackageManagerInstall(packageManager, cwd) {
272
387
  else
273
388
  reject(
274
389
  new Error(
275
- `${packageManager} install exited with code ${code ?? "unknown"}`
390
+ `${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`
276
391
  )
277
392
  );
278
393
  });
@@ -376,48 +491,17 @@ async function autoRunWebapp(packageDir) {
376
491
  return true;
377
492
  }
378
493
  function readTemplateVersions() {
379
- const versionsPath = path4.resolve(
380
- path4.dirname(fileURLToPath2(import.meta.url)),
494
+ const versionsPath = path7.resolve(
495
+ path7.dirname(fileURLToPath2(import.meta.url)),
381
496
  "../template-versions.json"
382
497
  );
383
498
  try {
384
- const content = fs4.readFileSync(versionsPath, "utf-8");
499
+ const content = fs7.readFileSync(versionsPath, "utf-8");
385
500
  return JSON.parse(content);
386
501
  } catch {
387
502
  return {};
388
503
  }
389
504
  }
390
- async function generateEnvLocal(packageDir) {
391
- const examplePath = path4.join(packageDir, ".env.example");
392
- const localPath = path4.join(packageDir, ".env.local");
393
- if (!await fs4.pathExists(examplePath)) return;
394
- if (await fs4.pathExists(localPath)) return;
395
- const authSecret = randomBytes(32).toString("base64");
396
- const encKey = randomBytes(16).toString("hex");
397
- 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}`);
398
- await fs4.writeFile(localPath, content);
399
- }
400
- async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
401
- const sourceDir = path4.join(packageDir, ".github", "workflows");
402
- if (!await fs4.pathExists(sourceDir)) return;
403
- const targetDir = path4.join(monorepoRoot, ".github", "workflows");
404
- await fs4.ensureDir(targetDir);
405
- const entries = await fs4.readdir(sourceDir);
406
- for (const name of entries) {
407
- if (!name.startsWith(`${appName}-`)) continue;
408
- if (!/\.(ya?ml)$/.test(name)) continue;
409
- await fs4.move(path4.join(sourceDir, name), path4.join(targetDir, name), {
410
- overwrite: true
411
- });
412
- }
413
- if ((await fs4.readdir(sourceDir)).length === 0) {
414
- await fs4.rmdir(sourceDir);
415
- const packageGithub = path4.join(packageDir, ".github");
416
- if ((await fs4.readdir(packageGithub)).length === 0) {
417
- await fs4.rmdir(packageGithub);
418
- }
419
- }
420
- }
421
505
  async function writeMosaicFiles(packageDir, config, projectType) {
422
506
  const templateVersions = readTemplateVersions();
423
507
  const manifest = {
@@ -431,8 +515,8 @@ async function writeMosaicFiles(packageDir, config, projectType) {
431
515
  }
432
516
  };
433
517
  await writeManifest(packageDir, manifest);
434
- const notesPath = path4.join(packageDir, "mosaic-template-notes.md");
435
- await fs4.writeFile(
518
+ const notesPath = path7.join(packageDir, "mosaic-template-notes.md");
519
+ await fs7.writeFile(
436
520
  notesPath,
437
521
  `# Mosaic Divergence Notes
438
522
 
@@ -445,6 +529,133 @@ _None yet \u2014 freshly created from template._
445
529
  `
446
530
  );
447
531
  }
532
+ async function scaffoldMonorepo(targetDir, config) {
533
+ const monoSpinner = ora("Copying monorepo template...").start();
534
+ try {
535
+ await copyTemplate(targetDir, "monorepo");
536
+ monoSpinner.succeed("Copied monorepo template");
537
+ } catch (error) {
538
+ monoSpinner.fail("Failed to copy monorepo template");
539
+ console.error(error);
540
+ process.exit(1);
541
+ }
542
+ const replaceSpinner = ora("Replacing monorepo placeholders...").start();
543
+ try {
544
+ const stats = await replacePlaceholders(targetDir, config);
545
+ replaceSpinner.succeed(
546
+ `Replaced placeholders in ${stats.modified} monorepo files`
547
+ );
548
+ } catch (error) {
549
+ replaceSpinner.fail("Failed to replace monorepo placeholders");
550
+ console.error(error);
551
+ process.exit(1);
552
+ }
553
+ }
554
+ async function addPackageToMonorepo(args) {
555
+ const { packageDir, monorepoRoot, projectType, config } = args;
556
+ const copySpinner = ora("Copying package template...").start();
557
+ try {
558
+ await copyTemplate(packageDir, projectType);
559
+ copySpinner.succeed("Copied package template");
560
+ } catch (error) {
561
+ copySpinner.fail("Failed to copy package template");
562
+ console.error(error);
563
+ process.exit(1);
564
+ }
565
+ const replaceSpinner = ora("Replacing package placeholders...").start();
566
+ try {
567
+ const stats = await replacePlaceholders(packageDir, config);
568
+ replaceSpinner.succeed(
569
+ `Replaced placeholders in ${stats.modified} package files`
570
+ );
571
+ } catch (error) {
572
+ replaceSpinner.fail("Failed to replace package placeholders");
573
+ console.error(error);
574
+ process.exit(1);
575
+ }
576
+ await writeMosaicFiles(packageDir, config, projectType);
577
+ if (projectType === "webapp") {
578
+ await resolvePerceptaPackageVersions(packageDir);
579
+ await generateEnvLocal(packageDir);
580
+ await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
581
+ }
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
+ }
604
+ function initGitRepo(targetDir) {
605
+ const gitSpinner = ora("Initializing git repository...").start();
606
+ try {
607
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
608
+ execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
609
+ execSync('git commit -m "Initial commit from @percepta/create"', {
610
+ cwd: targetDir,
611
+ stdio: "ignore"
612
+ });
613
+ gitSpinner.succeed("Initialized git repository");
614
+ } catch {
615
+ gitSpinner.warn("Failed to initialize git repository");
616
+ }
617
+ }
618
+ async function installAtMonorepoRoot(monorepoRoot, installDeps) {
619
+ if (!installDeps) return false;
620
+ const spinner = ora(
621
+ `Installing dependencies with ${PACKAGE_MANAGER}...`
622
+ ).start();
623
+ try {
624
+ await runPackageManagerInstall(PACKAGE_MANAGER, monorepoRoot);
625
+ spinner.succeed("Installed dependencies");
626
+ return true;
627
+ } catch {
628
+ spinner.warn(
629
+ `Failed to install dependencies. Run '${PACKAGE_MANAGER} install' from monorepo root.`
630
+ );
631
+ return false;
632
+ }
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
+ }
655
+ async function maybeAutoRunWebapp(packageDir, projectType, installSucceeded) {
656
+ if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
657
+ return autoRunWebapp(packageDir);
658
+ }
448
659
  function getProjectTypeLabel(projectType) {
449
660
  switch (projectType) {
450
661
  case "monorepo":
@@ -459,6 +670,35 @@ function getProjectTypeLabel(projectType) {
459
670
  }
460
671
  }
461
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
+ }
462
702
  async function createProject(options) {
463
703
  if (options.type !== void 0 && !isValidProjectType(options.type)) {
464
704
  console.error(
@@ -511,8 +751,9 @@ async function createProject(options) {
511
751
  let answers;
512
752
  if (options.yes) {
513
753
  const projectType = options.type || "webapp";
754
+ requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
514
755
  const kebabName = toKebabCase(projectName);
515
- const directory = monorepoContext.found && monorepoContext.packageDir ? path4.join(monorepoContext.packageDir, kebabName) : path4.resolve(cwd, kebabName);
756
+ const directory = monorepoContext.found && monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, kebabName) : path7.resolve(cwd, kebabName);
516
757
  answers = {
517
758
  projectType,
518
759
  directory,
@@ -525,10 +766,11 @@ async function createProject(options) {
525
766
  projectType: options.type,
526
767
  name: projectName ? toKebabCase(projectName) : void 0,
527
768
  skipInstall: options.skipInstall,
528
- monorepoContext
769
+ monorepoContext,
770
+ beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
529
771
  });
530
772
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) {
531
- answers.directory = path4.join(monorepoContext.packageDir, answers.name);
773
+ answers.directory = path7.join(monorepoContext.packageDir, answers.name);
532
774
  }
533
775
  }
534
776
  const config = {
@@ -540,7 +782,8 @@ async function createProject(options) {
540
782
  };
541
783
  const typeLabel = getProjectTypeLabel(answers.projectType);
542
784
  if (monorepoContext.found) {
543
- const packageDir = monorepoContext.packageDir ? path4.join(monorepoContext.packageDir, answers.name) : answers.directory;
785
+ const monorepoRoot = monorepoContext.rootDir;
786
+ const packageDir = monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, answers.name) : answers.directory;
544
787
  console.log(chalk.dim(" Package type:"), typeLabel);
545
788
  console.log(chalk.dim(" Target:"), packageDir);
546
789
  console.log(chalk.dim(" Name:"), config.name);
@@ -549,8 +792,8 @@ async function createProject(options) {
549
792
  console.log(chalk.dim(" Database:"), config.dbName);
550
793
  }
551
794
  console.log();
552
- if (await fs4.pathExists(packageDir)) {
553
- const files = await fs4.readdir(packageDir);
795
+ if (await fs7.pathExists(packageDir)) {
796
+ const files = await fs7.readdir(packageDir);
554
797
  if (files.length > 0) {
555
798
  console.error(
556
799
  chalk.red(`Error: Directory ${packageDir} is not empty.`)
@@ -558,81 +801,51 @@ async function createProject(options) {
558
801
  process.exit(1);
559
802
  }
560
803
  }
561
- const copySpinner = ora("Copying template files...").start();
562
- try {
563
- await copyTemplate(packageDir, answers.projectType);
564
- copySpinner.succeed("Copied template files");
565
- } catch (error) {
566
- copySpinner.fail("Failed to copy template files");
567
- console.error(error);
568
- process.exit(1);
569
- }
570
- const replaceSpinner = ora("Replacing placeholders...").start();
571
- try {
572
- const stats = await replacePlaceholders(packageDir, config);
573
- replaceSpinner.succeed(
574
- `Replaced placeholders in ${stats.modified} files`
575
- );
576
- } catch (error) {
577
- replaceSpinner.fail("Failed to replace placeholders");
578
- console.error(error);
579
- process.exit(1);
580
- }
581
- await writeMosaicFiles(packageDir, config, answers.projectType);
582
- if (answers.projectType === "webapp") {
583
- await generateEnvLocal(packageDir);
584
- }
585
- if (answers.projectType === "webapp") {
586
- await relocateWorkflowsToRoot(
804
+ if (answers.projectType !== "monorepo") {
805
+ await addPackageToMonorepo({
587
806
  packageDir,
588
- monorepoContext.rootDir,
589
- config.name
590
- );
591
- }
592
- await warnIfMissingRootNpmrc(monorepoContext.rootDir);
593
- let installSucceeded = false;
594
- if (answers.installDeps) {
595
- const packageManager = PACKAGE_MANAGER;
596
- const installSpinner = ora(
597
- `Installing dependencies with ${packageManager}...`
598
- ).start();
599
- try {
600
- await runPackageManagerInstall(
601
- packageManager,
602
- monorepoContext.rootDir
603
- );
604
- installSpinner.succeed("Installed dependencies");
605
- installSucceeded = true;
606
- } catch {
607
- installSpinner.warn(
608
- `Failed to install dependencies. Run '${packageManager} install' from monorepo root.`
609
- );
610
- }
807
+ monorepoRoot,
808
+ projectType: answers.projectType,
809
+ config
810
+ });
611
811
  }
812
+ await warnIfMissingRootNpmrc(monorepoRoot);
813
+ const rootInstallSucceeded = await installAtMonorepoRoot(
814
+ monorepoRoot,
815
+ answers.installDeps
816
+ );
817
+ const packageInstallSucceeded = await installAtWebappPackage(
818
+ packageDir,
819
+ answers.projectType,
820
+ answers.installDeps
821
+ );
822
+ const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
612
823
  console.log();
613
824
  console.log(
614
825
  chalk.green("\u2714"),
615
826
  chalk.bold(`Created ${typeLabel} at`),
616
- chalk.cyan(path4.relative(monorepoContext.rootDir, packageDir))
827
+ chalk.cyan(path7.relative(monorepoRoot, packageDir))
617
828
  );
618
829
  console.log();
619
- if (answers.projectType === "webapp" && installSucceeded) {
620
- const devStarted = await autoRunWebapp(packageDir);
621
- if (devStarted) return;
622
- }
830
+ const devStarted = await maybeAutoRunWebapp(
831
+ packageDir,
832
+ answers.projectType,
833
+ installSucceeded
834
+ );
835
+ if (devStarted) return;
623
836
  printNextStepsExisting(answers, options, packageDir);
624
837
  } else {
625
838
  const isBareMonorepo = answers.projectType === "monorepo";
626
- const targetDir = answers.directory;
627
- const packageDir = isBareMonorepo ? null : path4.join(targetDir, "packages", answers.name);
839
+ const monorepoRoot = answers.directory;
840
+ const packageDir = isBareMonorepo ? null : path7.join(monorepoRoot, "packages", answers.name);
628
841
  if (isBareMonorepo) {
629
842
  console.log(chalk.dim(" Type:"), typeLabel);
630
- console.log(chalk.dim(" Directory:"), targetDir);
843
+ console.log(chalk.dim(" Directory:"), monorepoRoot);
631
844
  console.log(chalk.dim(" Name:"), config.name);
632
845
  console.log(chalk.dim(" Title:"), config.title);
633
846
  } else {
634
847
  console.log(chalk.dim(" Package type:"), typeLabel);
635
- console.log(chalk.dim(" Monorepo directory:"), targetDir);
848
+ console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
636
849
  console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
637
850
  console.log(chalk.dim(" Name:"), config.name);
638
851
  console.log(chalk.dim(" Title:"), config.title);
@@ -641,101 +854,40 @@ async function createProject(options) {
641
854
  }
642
855
  }
643
856
  console.log();
644
- if (await fs4.pathExists(targetDir)) {
645
- const files = await fs4.readdir(targetDir);
857
+ if (await fs7.pathExists(monorepoRoot)) {
858
+ const files = await fs7.readdir(monorepoRoot);
646
859
  if (files.length > 0) {
647
860
  console.error(
648
- chalk.red(`Error: Directory ${targetDir} is not empty.`)
861
+ chalk.red(`Error: Directory ${monorepoRoot} is not empty.`)
649
862
  );
650
863
  process.exit(1);
651
864
  }
652
865
  }
653
- const monoSpinner = ora("Copying monorepo template...").start();
654
- try {
655
- await copyTemplate(targetDir, "monorepo");
656
- monoSpinner.succeed("Copied monorepo template");
657
- } catch (error) {
658
- monoSpinner.fail("Failed to copy monorepo template");
659
- console.error(error);
660
- process.exit(1);
661
- }
662
- const monoReplaceSpinner = ora(
663
- "Replacing monorepo placeholders..."
664
- ).start();
665
- try {
666
- const stats = await replacePlaceholders(targetDir, config);
667
- monoReplaceSpinner.succeed(
668
- `Replaced placeholders in ${stats.modified} monorepo files`
669
- );
670
- } catch (error) {
671
- monoReplaceSpinner.fail("Failed to replace monorepo placeholders");
672
- console.error(error);
673
- process.exit(1);
674
- }
675
- if (packageDir) {
676
- const pkgSpinner = ora("Copying package template...").start();
677
- try {
678
- await copyTemplate(packageDir, answers.projectType);
679
- pkgSpinner.succeed("Copied package template");
680
- } catch (error) {
681
- pkgSpinner.fail("Failed to copy package template");
682
- console.error(error);
683
- process.exit(1);
684
- }
685
- const pkgReplaceSpinner = ora(
686
- "Replacing package placeholders..."
687
- ).start();
688
- try {
689
- const stats = await replacePlaceholders(packageDir, config);
690
- pkgReplaceSpinner.succeed(
691
- `Replaced placeholders in ${stats.modified} package files`
692
- );
693
- } catch (error) {
694
- pkgReplaceSpinner.fail("Failed to replace package placeholders");
695
- console.error(error);
696
- process.exit(1);
697
- }
698
- await writeMosaicFiles(packageDir, config, answers.projectType);
699
- if (answers.projectType === "webapp") {
700
- await generateEnvLocal(packageDir);
701
- }
702
- if (answers.projectType === "webapp") {
703
- await relocateWorkflowsToRoot(packageDir, targetDir, config.name);
704
- }
705
- }
706
- const gitSpinner = ora("Initializing git repository...").start();
707
- try {
708
- execSync("git init", { cwd: targetDir, stdio: "ignore" });
709
- execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
710
- execSync('git commit -m "Initial commit from @percepta/create"', {
711
- cwd: targetDir,
712
- stdio: "ignore"
866
+ await scaffoldMonorepo(monorepoRoot, config);
867
+ if (packageDir && answers.projectType !== "monorepo") {
868
+ await addPackageToMonorepo({
869
+ packageDir,
870
+ monorepoRoot,
871
+ projectType: answers.projectType,
872
+ config
713
873
  });
714
- gitSpinner.succeed("Initialized git repository");
715
- } catch {
716
- gitSpinner.warn("Failed to initialize git repository");
717
- }
718
- let installSucceeded = false;
719
- if (answers.installDeps) {
720
- const packageManager = PACKAGE_MANAGER;
721
- const installSpinner = ora(
722
- `Installing dependencies with ${packageManager}...`
723
- ).start();
724
- try {
725
- await runPackageManagerInstall(packageManager, targetDir);
726
- installSpinner.succeed("Installed dependencies");
727
- installSucceeded = true;
728
- } catch {
729
- installSpinner.warn(
730
- `Failed to install dependencies. Run '${packageManager} install' manually.`
731
- );
732
- }
733
874
  }
875
+ initGitRepo(monorepoRoot);
876
+ const rootInstallSucceeded = await installAtMonorepoRoot(
877
+ monorepoRoot,
878
+ answers.installDeps
879
+ );
880
+ const packageInstallSucceeded = await installAtWebappPackage(
881
+ packageDir,
882
+ answers.projectType,
883
+ answers.installDeps
884
+ );
885
+ const installSucceeded = answers.projectType === "webapp" ? rootInstallSucceeded && packageInstallSucceeded : rootInstallSucceeded;
734
886
  console.log();
735
887
  console.log(
736
888
  chalk.green("\u2714"),
737
889
  chalk.bold(isBareMonorepo ? `Created ${typeLabel} at` : "Created monorepo at"),
738
- chalk.cyan(targetDir)
890
+ chalk.cyan(monorepoRoot)
739
891
  );
740
892
  if (!isBareMonorepo) {
741
893
  console.log(
@@ -745,11 +897,13 @@ async function createProject(options) {
745
897
  );
746
898
  }
747
899
  console.log();
748
- if (packageDir && answers.projectType === "webapp" && installSucceeded) {
749
- const devStarted = await autoRunWebapp(packageDir);
750
- if (devStarted) return;
751
- }
752
- printNextStepsNew(answers, options, targetDir);
900
+ const devStarted = await maybeAutoRunWebapp(
901
+ packageDir,
902
+ answers.projectType,
903
+ installSucceeded
904
+ );
905
+ if (devStarted) return;
906
+ printNextStepsNew(answers, options, monorepoRoot);
753
907
  }
754
908
  }
755
909
  function printWebappNextSteps(params) {
@@ -762,12 +916,15 @@ function printWebappNextSteps(params) {
762
916
  } = params;
763
917
  const repoRel = shPath(monorepoRelativePath) || ".";
764
918
  const pkgFromRoot = `packages/${answers.name}`;
919
+ const packageInstallStep = `${pm} install --ignore-workspace`;
765
920
  const pnpmSteps = ["pnpm run setup", "pnpm dev"];
766
921
  if (variant === "new") {
767
922
  const oneLinerParts2 = [];
768
923
  if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
769
924
  if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
770
- oneLinerParts2.push(`cd ${pkgFromRoot}`, ...pnpmSteps);
925
+ oneLinerParts2.push(`cd ${pkgFromRoot}`);
926
+ if (!answers.installDeps) oneLinerParts2.push(packageInstallStep);
927
+ oneLinerParts2.push(...pnpmSteps);
771
928
  console.log(chalk.bold("Copy-paste (from your current directory):"));
772
929
  console.log();
773
930
  console.log(chalk.cyan(` ${oneLinerParts2.join(" && ")}`));
@@ -782,6 +939,9 @@ function printWebappNextSteps(params) {
782
939
  console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
783
940
  }
784
941
  console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
942
+ if (!answers.installDeps) {
943
+ console.log(chalk.dim(` ${step2++}.`), packageInstallStep);
944
+ }
785
945
  for (const cmd of pnpmSteps) {
786
946
  console.log(chalk.dim(` ${step2++}.`), cmd);
787
947
  }
@@ -791,7 +951,7 @@ function printWebappNextSteps(params) {
791
951
  const oneLinerParts = [];
792
952
  if (!answers.installDeps) {
793
953
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
794
- oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
954
+ oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`, packageInstallStep);
795
955
  } else if (pkgRel !== ".") {
796
956
  oneLinerParts.push(`cd ${pkgRel}`);
797
957
  }
@@ -809,6 +969,7 @@ function printWebappNextSteps(params) {
809
969
  }
810
970
  console.log(chalk.dim(` ${step++}.`), `${pm} install`);
811
971
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
972
+ console.log(chalk.dim(` ${step++}.`), packageInstallStep);
812
973
  } else if (pkgRel !== ".") {
813
974
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
814
975
  }
@@ -817,10 +978,10 @@ function printWebappNextSteps(params) {
817
978
  }
818
979
  }
819
980
  async function warnIfMissingRootNpmrc(rootDir) {
820
- const rootNpmrc = path4.join(rootDir, ".npmrc");
981
+ const rootNpmrc = path7.join(rootDir, ".npmrc");
821
982
  let contents = "";
822
- if (await fs4.pathExists(rootNpmrc)) {
823
- contents = await fs4.readFile(rootNpmrc, "utf8");
983
+ if (await fs7.pathExists(rootNpmrc)) {
984
+ contents = await fs7.readFile(rootNpmrc, "utf8");
824
985
  }
825
986
  if (contents.includes("@percepta:registry")) {
826
987
  return;
@@ -835,7 +996,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
835
996
  " pnpm reads .npmrc from the workspace root, so add these lines to"
836
997
  )
837
998
  );
838
- console.log(chalk.dim(` ${path4.join(rootDir, ".npmrc")}:`));
999
+ console.log(chalk.dim(` ${path7.join(rootDir, ".npmrc")}:`));
839
1000
  console.log();
840
1001
  console.log(
841
1002
  chalk.cyan(" @percepta:registry=https://registry.npmjs.org/")
@@ -847,7 +1008,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
847
1008
  }
848
1009
  function printNextStepsNew(answers, options, targetDir) {
849
1010
  const pm = PACKAGE_MANAGER;
850
- const relativePath = path4.relative(process.cwd(), targetDir) || ".";
1011
+ const relativePath = path7.relative(process.cwd(), targetDir) || ".";
851
1012
  console.log("Next steps:");
852
1013
  console.log();
853
1014
  switch (answers.projectType) {
@@ -903,9 +1064,9 @@ function printNextStepsNew(answers, options, targetDir) {
903
1064
  }
904
1065
  function printNextStepsExisting(answers, options, packageDir) {
905
1066
  const pm = PACKAGE_MANAGER;
906
- const packageRelativePath = path4.relative(process.cwd(), packageDir) || ".";
907
- const monorepoRoot = path4.dirname(path4.dirname(packageDir));
908
- const monorepoRelativePath = path4.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) || ".";
909
1070
  console.log("Next steps:");
910
1071
  console.log();
911
1072
  switch (answers.projectType) {
@@ -950,25 +1111,25 @@ program.command("status").description("Show template sync status for current app
950
1111
  "--mosaic-template-path <path>",
951
1112
  "Path to local mosaic repo checkout"
952
1113
  ).action(async (options) => {
953
- const { statusCommand } = await import("./status-MITGDLTT.js");
1114
+ const { statusCommand } = await import("./status-BTHGN6QH.js");
954
1115
  await statusCommand(options);
955
1116
  });
956
1117
  program.command("sync").description("Generate downstream sync context (template \u2192 app)").option(
957
1118
  "--mosaic-template-path <path>",
958
1119
  "Path to local mosaic repo checkout"
959
1120
  ).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
960
- const { syncCommand } = await import("./sync-J4SFZHDX.js");
1121
+ const { syncCommand } = await import("./sync-3Q27L7XZ.js");
961
1122
  await syncCommand(options);
962
1123
  });
963
1124
  program.command("upstream").description("Generate upstream context (app \u2192 template)").option(
964
1125
  "--mosaic-template-path <path>",
965
1126
  "Path to local mosaic repo checkout"
966
1127
  ).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
967
- const { upstreamCommand } = await import("./upstream-AQI7P4EU.js");
1128
+ const { upstreamCommand } = await import("./upstream-C5KFAHVR.js");
968
1129
  await upstreamCommand(options);
969
1130
  });
970
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) => {
971
- const { initCommand } = await import("./init-Z4VGBHAK.js");
1132
+ const { initCommand } = await import("./init-NP6GRXLL.js");
972
1133
  await initCommand(options);
973
1134
  });
974
1135
  program.parse();