@percepta/create 3.4.3 → 3.5.1

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 (33) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +196 -123
  3. package/dist/index.js.map +1 -1
  4. package/dist/{init-CtCp7Tv2.js → init-sI9aIrkU.js} +2 -2
  5. package/dist/init-sI9aIrkU.js.map +1 -0
  6. package/dist/{upstream-D-LH_1z4.js → upstream-gUHLWSR1.js} +2 -2
  7. package/dist/upstream-gUHLWSR1.js.map +1 -0
  8. package/package.json +1 -1
  9. package/template-versions.json +2 -1
  10. package/templates/monorepo/README.md +8 -5
  11. package/templates/monorepo/auth/package.json +1 -1
  12. package/templates/monorepo/package.json.template +1 -0
  13. package/templates/monorepo/pnpm-workspace.yaml +13 -0
  14. package/templates/webapp/.claude/commands/upstream.md +1 -1
  15. package/templates/webapp/agent-skills/access-control.md +24 -1
  16. package/templates/webapp/drizzle.config.ts +7 -3
  17. package/templates/webapp/e2e/rbac.spec.ts +8 -4
  18. package/templates/webapp/package.json.template +6 -6
  19. package/templates/webapp/playwright.config.ts +5 -2
  20. package/templates/webapp/scripts/seed.ts +6 -2
  21. package/templates/webapp/src/drizzle/db.ts +1 -2
  22. package/templates/webapp/vitest.setup.ts +6 -2
  23. package/dist/init-CtCp7Tv2.js.map +0 -1
  24. package/dist/upstream-D-LH_1z4.js.map +0 -1
  25. package/templates/webapp/scripts/generate-migrations.ts +0 -28
  26. package/templates/webapp/scripts/migrate.ts +0 -21
  27. package/templates/webapp/scripts/setup-database.ts +0 -78
  28. package/templates/webapp/scripts/setup-readonly-user.ts +0 -193
  29. package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +0 -24
  30. package/templates/webapp/src/drizzle/migrationSql.ts +0 -8
  31. package/templates/webapp/src/drizzle/searchPath.test.ts +0 -21
  32. package/templates/webapp/src/drizzle/searchPath.ts +0 -16
  33. package/templates/webapp/src/drizzle/ssl.ts +0 -5
package/README.md CHANGED
@@ -26,6 +26,7 @@ The bare command above is the canonical UX. The flags below exist for tests and
26
26
  ## Subcommands
27
27
 
28
28
  - `create` (default) — scaffold a new Mosaic package
29
+ - `add` — add a webapp or library to the current monorepo
29
30
  - `status` — show template sync status for the current app
30
31
  - `sync` — generate downstream sync context (template → app)
31
32
  - `upstream` — generate upstream context (app → template)
@@ -38,6 +39,18 @@ The bare command above is the canonical UX. The flags below exist for tests and
38
39
  - **Outside a monorepo** — you're asked "Initialize with a webapp?" (Y/n, default Y), then for the repo name. Picking the webapp option also asks for the webapp name and scaffolds it inside `packages/<webapp-name>/`. Declining gives you an empty monorepo.
39
40
  - **Inside a monorepo** — pick `Webapp` (default) or `Library` to add a new package under the workspace pattern.
40
41
 
42
+ Generated monorepos include a root `.mosaic-workspace.json` and a pinned
43
+ `pnpm mosaic` script. Prefer the workspace-owned command when adding packages:
44
+
45
+ ```bash
46
+ pnpm mosaic add webapp my-app
47
+ pnpm mosaic add library my-lib
48
+ ```
49
+
50
+ That command uses the create package version and template compatibility versions
51
+ the monorepo was created with, so a newly added app does not silently drift to a
52
+ newer scaffold.
53
+
41
54
  ## Happy-path: zero-friction webapp
42
55
 
43
56
  When you scaffold a webapp (the default flow), `create` automatically runs:
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from "commander";
3
- import { execFile, execSync, spawn } from "node:child_process";
3
+ import { execSync, spawn } from "node:child_process";
4
4
  import path from "node:path";
5
5
  import chalk from "chalk";
6
6
  import fs from "fs-extra";
@@ -10,7 +10,6 @@ import { parse } from "yaml";
10
10
  import { randomBytes } from "node:crypto";
11
11
  import inquirer from "inquirer";
12
12
  import validateNpmPackageName from "validate-npm-package-name";
13
- import { promisify } from "node:util";
14
13
  //#region src/utils/case-converters.ts
15
14
  /** Lowercase, hyphenated, npm-package-name-safe form: "My Cool App" → "my-cool-app". */
16
15
  function toKebabCase(str) {
@@ -192,6 +191,24 @@ function resolveMosaicTemplatePath(options) {
192
191
  throw new Error("Mosaic repo path required. Use --mosaic-template-path or set MOSAIC_TEMPLATE_PATH.");
193
192
  }
194
193
  //#endregion
194
+ //#region src/utils/package-metadata.ts
195
+ const FALLBACK_METADATA = {
196
+ name: "@percepta/create",
197
+ version: "0.0.0"
198
+ };
199
+ function readCreatePackageMetadata() {
200
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
201
+ const candidates = [path.resolve(currentDir, "../package.json"), path.resolve(currentDir, "../../package.json")];
202
+ for (const packageJsonPath of candidates) try {
203
+ const pkg = fs.readJsonSync(packageJsonPath);
204
+ if (typeof pkg.name === "string" && typeof pkg.version === "string") return {
205
+ name: pkg.name,
206
+ version: pkg.version
207
+ };
208
+ } catch {}
209
+ return FALLBACK_METADATA;
210
+ }
211
+ //#endregion
195
212
  //#region src/utils/validate.ts
196
213
  function validateProjectName(name) {
197
214
  const result = validateNpmPackageName(name);
@@ -352,7 +369,9 @@ const PLACEHOLDERS = {
352
369
  __APP_NAME_UPPER__: "nameUpper",
353
370
  __APP_NAME_SNAKE__: "nameSnake",
354
371
  __REPO_NAME__: "repoName",
355
- __REPO_NAME_SNAKE__: "repoNameSnake"
372
+ __REPO_NAME_SNAKE__: "repoNameSnake",
373
+ __CREATE_PACKAGE__: "createPackage",
374
+ __CREATE_VERSION__: "createVersion"
356
375
  };
357
376
  const SKIP_DIRS = new Set([
358
377
  "node_modules",
@@ -450,77 +469,6 @@ async function replacePlaceholders(targetDir, config) {
450
469
  return stats;
451
470
  }
452
471
  //#endregion
453
- //#region src/utils/resolve-percepta-versions.ts
454
- const execFileAsync = promisify(execFile);
455
- const DEPENDENCY_SECTIONS = [
456
- "dependencies",
457
- "devDependencies",
458
- "optionalDependencies",
459
- "peerDependencies"
460
- ];
461
- function getPerceptaPackages(pkg) {
462
- const names = /* @__PURE__ */ new Set();
463
- for (const section of DEPENDENCY_SECTIONS) {
464
- const deps = pkg[section];
465
- if (!deps) continue;
466
- for (const name of Object.keys(deps)) if (name.startsWith("@percepta/")) names.add(name);
467
- }
468
- return [...names].sort();
469
- }
470
- async function npmViewDistTagLatest(packageName, cwd) {
471
- const { stdout } = await execFileAsync("npm", [
472
- "view",
473
- packageName,
474
- "dist-tags.latest",
475
- "--silent"
476
- ], {
477
- cwd,
478
- encoding: "utf8",
479
- timeout: 5e3
480
- });
481
- const version = stdout.trim();
482
- return version.length > 0 ? version : null;
483
- }
484
- async function resolvePerceptaVersionsInPackageJson(packageJsonPath, lookupLatest = npmViewDistTagLatest) {
485
- const cwd = path.dirname(packageJsonPath);
486
- const pkg = await fs.readJson(packageJsonPath);
487
- const packageNames = getPerceptaPackages(pkg);
488
- const resolved = {};
489
- const failed = [];
490
- const results = await Promise.all(packageNames.map(async (packageName) => {
491
- try {
492
- return {
493
- packageName,
494
- latest: await lookupLatest(packageName, cwd)
495
- };
496
- } catch {
497
- return {
498
- packageName,
499
- latest: null
500
- };
501
- }
502
- }));
503
- for (const { packageName, latest } of results) {
504
- if (!latest) {
505
- failed.push(packageName);
506
- continue;
507
- }
508
- resolved[packageName] = latest;
509
- for (const section of DEPENDENCY_SECTIONS) {
510
- const deps = pkg[section];
511
- if (deps?.[packageName]) deps[packageName] = latest;
512
- }
513
- }
514
- if (Object.keys(resolved).length > 0) {
515
- await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
516
- await fs.appendFile(packageJsonPath, "\n");
517
- }
518
- return {
519
- resolved,
520
- failed
521
- };
522
- }
523
- //#endregion
524
472
  //#region src/utils/template-versions.ts
525
473
  const FALLBACK_TEMPLATE_VERSION = "1.0.0";
526
474
  function readTemplateVersions() {
@@ -536,8 +484,55 @@ function getTemplateVersion(templateType) {
536
484
  return readTemplateVersions()[templateType] ?? FALLBACK_TEMPLATE_VERSION;
537
485
  }
538
486
  //#endregion
487
+ //#region src/utils/workspace-manifest.ts
488
+ const WORKSPACE_MANIFEST_FILENAME = ".mosaic-workspace.json";
489
+ function getWorkspaceManifestPath(rootDir) {
490
+ return path.join(rootDir, WORKSPACE_MANIFEST_FILENAME);
491
+ }
492
+ function createWorkspaceManifest(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
493
+ const createPackage = readCreatePackageMetadata();
494
+ return {
495
+ schemaVersion: 1,
496
+ createPackage: createPackage.name,
497
+ createVersion: createPackage.version,
498
+ monorepoTemplateVersion: getTemplateVersion("monorepo"),
499
+ compatibleTemplates: {
500
+ webapp: getTemplateVersion("webapp"),
501
+ library: getTemplateVersion("library")
502
+ },
503
+ createdAt
504
+ };
505
+ }
506
+ async function readWorkspaceManifest(rootDir) {
507
+ const manifestPath = getWorkspaceManifestPath(rootDir);
508
+ if (!await fs.pathExists(manifestPath)) return null;
509
+ const content = await fs.readFile(manifestPath, "utf-8");
510
+ try {
511
+ return JSON.parse(content);
512
+ } catch (error) {
513
+ throw new Error(`Invalid JSON in ${WORKSPACE_MANIFEST_FILENAME}: ${error.message}`);
514
+ }
515
+ }
516
+ async function writeWorkspaceManifest(rootDir, manifest) {
517
+ const manifestPath = getWorkspaceManifestPath(rootDir);
518
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
519
+ await fs.appendFile(manifestPath, "\n");
520
+ }
521
+ function getCompatibleTemplateVersion(manifest, templateType) {
522
+ return manifest?.compatibleTemplates[templateType] ?? getTemplateVersion(templateType);
523
+ }
524
+ //#endregion
539
525
  //#region src/commands/create.ts
540
526
  const PACKAGE_MANAGER = "pnpm";
527
+ const MAX_INSTALL_OUTPUT_CHARS = 64e3;
528
+ const MAX_INSTALL_OUTPUT_LINES = 80;
529
+ var PackageManagerCommandError = class extends Error {
530
+ constructor(message, output) {
531
+ super(message);
532
+ this.output = output;
533
+ this.name = "PackageManagerCommandError";
534
+ }
535
+ };
541
536
  /** Paths in copy-paste shell commands (POSIX-style). */
542
537
  function shPath(p) {
543
538
  return p.split(path.sep).join("/");
@@ -545,17 +540,42 @@ function shPath(p) {
545
540
  /** Non-blocking install so ora can animate (execSync would block timers). */
546
541
  function runPackageManagerInstall(packageManager, cwd, args = ["install"]) {
547
542
  return new Promise((resolve, reject) => {
543
+ let output = "";
544
+ const appendOutput = (chunk) => {
545
+ output += chunk.toString();
546
+ if (output.length > MAX_INSTALL_OUTPUT_CHARS) output = output.slice(-MAX_INSTALL_OUTPUT_CHARS);
547
+ };
548
548
  const child = spawn(packageManager, args, {
549
549
  cwd,
550
- stdio: "ignore"
550
+ stdio: [
551
+ "ignore",
552
+ "pipe",
553
+ "pipe"
554
+ ]
555
+ });
556
+ child.stdout?.on("data", appendOutput);
557
+ child.stderr?.on("data", appendOutput);
558
+ child.on("error", (error) => {
559
+ reject(new PackageManagerCommandError(`${packageManager} ${args.join(" ")} failed: ${error.message}`, output));
551
560
  });
552
- child.on("error", reject);
553
561
  child.on("close", (code) => {
554
562
  if (code === 0) resolve();
555
- else reject(/* @__PURE__ */ new Error(`${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
563
+ else reject(new PackageManagerCommandError(`${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`, output));
556
564
  });
557
565
  });
558
566
  }
567
+ function printInstallFailureOutput(error) {
568
+ if (!(error instanceof PackageManagerCommandError)) return;
569
+ const output = error.output.trim();
570
+ if (!output) return;
571
+ const lines = output.split(/\r?\n/);
572
+ const omitted = Math.max(0, lines.length - MAX_INSTALL_OUTPUT_LINES);
573
+ const visibleLines = lines.slice(-MAX_INSTALL_OUTPUT_LINES);
574
+ console.log();
575
+ console.log(chalk.bold(`Last ${visibleLines.length} lines from pnpm install:`));
576
+ if (omitted > 0) console.log(chalk.dim(`... omitted ${omitted} earlier lines ...`));
577
+ console.log(visibleLines.join("\n"));
578
+ }
559
579
  /**
560
580
  * Runs the monorepo-root `setup` script (docker + access + db + seed).
561
581
  * Uses `pnpm run setup` (not `pnpm setup`) because `pnpm setup` is a pnpm builtin that configures
@@ -696,19 +716,20 @@ async function autoRunWebapp(packageDir, monorepoRoot) {
696
716
  await closed;
697
717
  return true;
698
718
  }
699
- async function writeMosaicFiles(packageDir, config, projectType) {
719
+ async function writeMosaicFiles(packageDir, config, projectType, templateVersion = getTemplateVersion(projectType), templateCommit = "npm") {
700
720
  await writeManifest(packageDir, {
701
721
  templateType: projectType,
702
- templateVersion: getTemplateVersion(projectType),
703
- templateCommit: "npm",
722
+ templateVersion,
723
+ templateCommit,
704
724
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
705
725
  placeholders: derivePlaceholders(config.name, config.title, config.repoName),
706
- source: { templatePath: `packages/create-mosaic-module/templates/${projectType}` }
726
+ source: { templatePath: `packages/blueberry/templates/${projectType}` }
707
727
  });
708
728
  const notesPath = path.join(packageDir, "mosaic-template-notes.md");
709
729
  await fs.writeFile(notesPath, `# Mosaic Divergence Notes\n\nDocument intentional differences from the ${projectType} template here.\nClaude reads this file during sync to preserve your customizations.\n\n## Intentional Divergences\n\n_None yet — freshly created from template._\n`);
710
730
  }
711
731
  function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
732
+ const createPackage = readCreatePackageMetadata();
712
733
  return {
713
734
  name,
714
735
  title,
@@ -716,7 +737,9 @@ function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
716
737
  nameUpper: name.toUpperCase(),
717
738
  nameSnake: toSnakeCase(name),
718
739
  repoName,
719
- repoNameSnake: toSnakeCase(repoName)
740
+ repoNameSnake: toSnakeCase(repoName),
741
+ createPackage: createPackage.name,
742
+ createVersion: createPackage.version
720
743
  };
721
744
  }
722
745
  /** Copy the monorepo template into `targetDir` and replace its placeholders. */
@@ -746,7 +769,7 @@ async function scaffoldMonorepo(targetDir, config) {
746
769
  * webapp-only post-copy steps (.env.local, workflow relocation) when applicable.
747
770
  */
748
771
  async function addPackageToMonorepo(args) {
749
- const { packageDir, monorepoRoot, projectType, config } = args;
772
+ const { packageDir, monorepoRoot, projectType, config, templateVersion, templateCommit } = args;
750
773
  const copySpinner = ora("Copying package template...").start();
751
774
  try {
752
775
  await copyTemplate(packageDir, projectType);
@@ -765,27 +788,12 @@ async function addPackageToMonorepo(args) {
765
788
  console.error(error);
766
789
  process.exit(1);
767
790
  }
768
- await writeMosaicFiles(packageDir, config, projectType);
791
+ await writeMosaicFiles(packageDir, config, projectType, templateVersion, templateCommit);
769
792
  if (projectType === "webapp") {
770
- await resolvePerceptaPackageVersions(packageDir);
771
793
  await generateEnvLocal(packageDir);
772
794
  await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
773
795
  }
774
796
  }
775
- async function resolvePerceptaPackageVersions(packageDir) {
776
- const packageJsonPath = path.join(packageDir, "package.json");
777
- if (!await fs.pathExists(packageJsonPath)) return;
778
- const spinner = ora("Resolving latest @percepta/* versions...").start();
779
- try {
780
- const result = await resolvePerceptaVersionsInPackageJson(packageJsonPath);
781
- const count = Object.keys(result.resolved).length;
782
- if (result.failed.length > 0) spinner.warn(`Resolved ${count} @percepta/* versions; kept existing ranges for ${result.failed.join(", ")}`);
783
- else spinner.succeed(`Resolved ${count} @percepta/* versions`);
784
- } catch (error) {
785
- spinner.warn("Could not resolve latest @percepta/* versions; kept template ranges");
786
- console.log(chalk.dim(error.message));
787
- }
788
- }
789
797
  /** Initialize a git repo at `targetDir` with an initial commit. Best-effort. */
790
798
  function initGitRepo(targetDir) {
791
799
  const gitSpinner = ora("Initializing git repository...").start();
@@ -818,8 +826,9 @@ async function installAtMonorepoRoot(monorepoRoot, installDeps) {
818
826
  await runPackageManagerInstall(PACKAGE_MANAGER, monorepoRoot);
819
827
  spinner.succeed("Installed dependencies");
820
828
  return true;
821
- } catch {
829
+ } catch (error) {
822
830
  spinner.warn(`Failed to install dependencies. Run '${PACKAGE_MANAGER} install' from monorepo root.`);
831
+ printInstallFailureOutput(error);
823
832
  return false;
824
833
  }
825
834
  }
@@ -858,6 +867,23 @@ function requireNpmTokenForWebappInstall(projectType, installDeps) {
858
867
  console.error(chalk.dim(" Or pass --skip-install to scaffold without running install."));
859
868
  process.exit(1);
860
869
  }
870
+ function ensureWorkspaceCreateVersionCompatible(workspaceManifest, projectType) {
871
+ if (!workspaceManifest || projectType === "monorepo") return;
872
+ const createPackage = readCreatePackageMetadata();
873
+ if (workspaceManifest.createPackage === createPackage.name && workspaceManifest.createVersion === createPackage.version) return;
874
+ console.log();
875
+ console.error(chalk.red(`Error: This workspace is pinned to ${workspaceManifest.createPackage}@${workspaceManifest.createVersion}, but you are running ${createPackage.name}@${createPackage.version}.`));
876
+ console.error();
877
+ console.error(chalk.dim(" Run the workspace-owned command so new packages match this monorepo:"));
878
+ console.error(chalk.cyan(` pnpm mosaic add ${projectType} <name>`));
879
+ console.error();
880
+ console.error(chalk.dim(" To adopt newer templates, upgrade the workspace manifest first."));
881
+ process.exit(1);
882
+ }
883
+ function getTemplateCommitSource(workspaceManifest) {
884
+ if (!workspaceManifest) return "npm";
885
+ return `${workspaceManifest.createPackage}@${workspaceManifest.createVersion}`;
886
+ }
861
887
  async function createProject(options) {
862
888
  const cwd = await resolveCreateCwd(options.cwd);
863
889
  if (options.type !== void 0 && !isValidProjectType(options.type)) {
@@ -868,6 +894,10 @@ async function createProject(options) {
868
894
  console.log(chalk.bold("Creating a new Mosaic package..."));
869
895
  console.log();
870
896
  const monorepoContext = await detectMonorepo(cwd);
897
+ if (options.addOnly && !monorepoContext.found) {
898
+ console.error(chalk.red("Error: 'create add' must be run inside an existing pnpm monorepo."));
899
+ process.exit(1);
900
+ }
871
901
  if (options.type === "monorepo" && monorepoContext.found) {
872
902
  console.error(chalk.red(`Error: Already inside a monorepo at ${monorepoContext.rootDir}. Choose 'webapp' or 'library' to add a package, or run from outside the monorepo.`));
873
903
  process.exit(1);
@@ -875,6 +905,14 @@ async function createProject(options) {
875
905
  if (monorepoContext.found) console.log(chalk.dim(" Detected monorepo at"), chalk.cyan(monorepoContext.rootDir));
876
906
  else console.log(chalk.dim(" No monorepo detected. A new monorepo will be created."));
877
907
  console.log();
908
+ const workspaceManifest = monorepoContext.found ? await readWorkspaceManifest(monorepoContext.rootDir) : null;
909
+ if (workspaceManifest) {
910
+ console.log(chalk.dim(" Workspace create version:"), chalk.cyan(`${workspaceManifest.createPackage}@${workspaceManifest.createVersion}`));
911
+ console.log();
912
+ } else if (monorepoContext.found) {
913
+ console.log(chalk.yellow("!"), "No .mosaic-workspace.json found; using this CLI's bundled templates.");
914
+ console.log();
915
+ }
878
916
  const projectName = options.name;
879
917
  const repoName = options.repoName;
880
918
  if (options.yes && !projectName) {
@@ -898,6 +936,7 @@ async function createProject(options) {
898
936
  let answers;
899
937
  if (options.yes) {
900
938
  const projectType = options.type || "webapp";
939
+ ensureWorkspaceCreateVersionCompatible(workspaceManifest, projectType);
901
940
  requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
902
941
  const kebabName = toKebabCase(projectName);
903
942
  const kebabRepoName = repoName ? toKebabCase(repoName) : kebabName;
@@ -918,7 +957,10 @@ async function createProject(options) {
918
957
  skipInstall: options.skipInstall,
919
958
  monorepoContext,
920
959
  cwd,
921
- beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
960
+ beforeNamePrompt: (projectType) => {
961
+ ensureWorkspaceCreateVersionCompatible(workspaceManifest, projectType);
962
+ requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
963
+ }
922
964
  });
923
965
  if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) answers.directory = path.join(monorepoContext.packageDir, answers.name);
924
966
  }
@@ -946,7 +988,9 @@ async function createProject(options) {
946
988
  packageDir,
947
989
  monorepoRoot,
948
990
  projectType: answers.projectType,
949
- config
991
+ config,
992
+ templateVersion: getCompatibleTemplateVersion(workspaceManifest, answers.projectType),
993
+ templateCommit: getTemplateCommitSource(workspaceManifest)
950
994
  });
951
995
  await warnIfMissingRootNpmrc(monorepoRoot);
952
996
  const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
@@ -954,7 +998,7 @@ async function createProject(options) {
954
998
  console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(path.relative(monorepoRoot, packageDir)));
955
999
  console.log();
956
1000
  if (await maybeAutoRunWebapp(packageDir, monorepoRoot, answers.projectType, installSucceeded)) return;
957
- printNextStepsExisting(answers, packageDir);
1001
+ printNextStepsExisting(answers, packageDir, !installSucceeded);
958
1002
  } else {
959
1003
  const isBareMonorepo = answers.projectType === "monorepo";
960
1004
  const monorepoRoot = answers.directory;
@@ -981,11 +1025,15 @@ async function createProject(options) {
981
1025
  }
982
1026
  }
983
1027
  await scaffoldMonorepo(monorepoRoot, monorepoConfig);
1028
+ const newWorkspaceManifest = createWorkspaceManifest();
1029
+ await writeWorkspaceManifest(monorepoRoot, newWorkspaceManifest);
984
1030
  if (packageDir && answers.projectType !== "monorepo") await addPackageToMonorepo({
985
1031
  packageDir,
986
1032
  monorepoRoot,
987
1033
  projectType: answers.projectType,
988
- config
1034
+ config,
1035
+ templateVersion: getCompatibleTemplateVersion(newWorkspaceManifest, answers.projectType),
1036
+ templateCommit: getTemplateCommitSource(newWorkspaceManifest)
989
1037
  });
990
1038
  initGitRepo(monorepoRoot);
991
1039
  const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
@@ -994,7 +1042,7 @@ async function createProject(options) {
994
1042
  if (!isBareMonorepo) console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(`packages/${answers.name}/`));
995
1043
  console.log();
996
1044
  if (await maybeAutoRunWebapp(packageDir, monorepoRoot, answers.projectType, installSucceeded)) return;
997
- printNextStepsNew(answers, monorepoRoot);
1045
+ printNextStepsNew(answers, monorepoRoot, !installSucceeded);
998
1046
  }
999
1047
  }
1000
1048
  async function resolveCreateCwd(cwdOption) {
@@ -1013,7 +1061,7 @@ async function resolveCreateCwd(cwdOption) {
1013
1061
  return cwd;
1014
1062
  }
1015
1063
  function printWebappNextSteps(params) {
1016
- const { pm, answers, variant, monorepoRelativePath, packageRelativePathFromRoot } = params;
1064
+ const { pm, answers, variant, installNeeded, monorepoRelativePath, packageRelativePathFromRoot } = params;
1017
1065
  const repoRel = shPath(monorepoRelativePath) || ".";
1018
1066
  const pkgFromRoot = shPath(packageRelativePathFromRoot ?? `packages/${answers.name}`) || ".";
1019
1067
  const setupStep = "pnpm run setup";
@@ -1021,7 +1069,7 @@ function printWebappNextSteps(params) {
1021
1069
  if (variant === "new") {
1022
1070
  const oneLinerParts = [];
1023
1071
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
1024
- if (!answers.installDeps) oneLinerParts.push(`${pm} install`);
1072
+ if (installNeeded) oneLinerParts.push(`${pm} install`);
1025
1073
  oneLinerParts.push(setupStep);
1026
1074
  oneLinerParts.push(`cd ${pkgFromRoot}`);
1027
1075
  oneLinerParts.push(devStep);
@@ -1033,7 +1081,7 @@ function printWebappNextSteps(params) {
1033
1081
  console.log();
1034
1082
  let step = 1;
1035
1083
  if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1036
- if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1084
+ if (installNeeded) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1037
1085
  console.log(chalk.dim(` ${step++}.`), setupStep);
1038
1086
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
1039
1087
  console.log(chalk.dim(` ${step++}.`), devStep);
@@ -1041,7 +1089,7 @@ function printWebappNextSteps(params) {
1041
1089
  }
1042
1090
  const oneLinerParts = [];
1043
1091
  if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
1044
- if (!answers.installDeps) oneLinerParts.push(`${pm} install`);
1092
+ if (installNeeded) oneLinerParts.push(`${pm} install`);
1045
1093
  oneLinerParts.push(setupStep, `cd ${pkgFromRoot}`, devStep);
1046
1094
  console.log(chalk.bold("Copy-paste (from your current directory):"));
1047
1095
  console.log();
@@ -1051,7 +1099,7 @@ function printWebappNextSteps(params) {
1051
1099
  console.log();
1052
1100
  let step = 1;
1053
1101
  if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1054
- if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1102
+ if (installNeeded) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1055
1103
  console.log(chalk.dim(` ${step++}.`), setupStep);
1056
1104
  console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
1057
1105
  console.log(chalk.dim(` ${step++}.`), devStep);
@@ -1070,7 +1118,7 @@ async function warnIfMissingRootNpmrc(rootDir) {
1070
1118
  console.log(chalk.cyan(" //registry.npmjs.org/:_authToken=${NPM_TOKEN}"));
1071
1119
  console.log();
1072
1120
  }
1073
- function printNextStepsNew(answers, targetDir) {
1121
+ function printNextStepsNew(answers, targetDir, installNeeded) {
1074
1122
  const pm = PACKAGE_MANAGER;
1075
1123
  const relativePath = path.relative(process.cwd(), targetDir) || ".";
1076
1124
  console.log("Next steps:");
@@ -1080,8 +1128,8 @@ function printNextStepsNew(answers, targetDir) {
1080
1128
  let step = 1;
1081
1129
  const repoRel = shPath(relativePath);
1082
1130
  if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1083
- if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1084
- console.log(chalk.dim(` ${step++}.`), `Add a package: ${chalk.cyan(`npx @percepta/create --type webapp <name>`)}`);
1131
+ if (installNeeded) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1132
+ console.log(chalk.dim(` ${step++}.`), `Add a package: ${chalk.cyan(`pnpm mosaic add webapp <name>`)}`);
1085
1133
  break;
1086
1134
  }
1087
1135
  case "webapp":
@@ -1089,6 +1137,7 @@ function printNextStepsNew(answers, targetDir) {
1089
1137
  pm,
1090
1138
  answers,
1091
1139
  variant: "new",
1140
+ installNeeded,
1092
1141
  monorepoRelativePath: relativePath
1093
1142
  });
1094
1143
  break;
@@ -1096,7 +1145,7 @@ function printNextStepsNew(answers, targetDir) {
1096
1145
  let step = 1;
1097
1146
  const repoRel = shPath(relativePath);
1098
1147
  if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1099
- if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1148
+ if (installNeeded) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1100
1149
  console.log(chalk.dim(` ${step++}.`), `cd packages/${answers.name}`);
1101
1150
  console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
1102
1151
  console.log(chalk.dim(` ${step++}.`), `Edit src/index.ts to add your library code`);
@@ -1107,7 +1156,7 @@ function printNextStepsNew(answers, targetDir) {
1107
1156
  console.log(chalk.dim("For more information, see the README.md in your project."));
1108
1157
  console.log();
1109
1158
  }
1110
- function printNextStepsExisting(answers, packageDir) {
1159
+ function printNextStepsExisting(answers, packageDir, installNeeded) {
1111
1160
  const pm = PACKAGE_MANAGER;
1112
1161
  const packageRelativePath = path.relative(process.cwd(), packageDir) || ".";
1113
1162
  const monorepoRoot = path.dirname(path.dirname(packageDir));
@@ -1121,14 +1170,22 @@ function printNextStepsExisting(answers, packageDir) {
1121
1170
  pm,
1122
1171
  answers,
1123
1172
  variant: "existing",
1173
+ installNeeded,
1124
1174
  monorepoRelativePath,
1125
1175
  packageRelativePathFromRoot
1126
1176
  });
1127
1177
  break;
1128
1178
  case "library": {
1129
1179
  let step = 1;
1130
- const pkgRel = shPath(packageRelativePath);
1131
- if (pkgRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
1180
+ if (installNeeded) {
1181
+ const repoRel = shPath(monorepoRelativePath);
1182
+ if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1183
+ console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1184
+ console.log(chalk.dim(` ${step++}.`), `cd ${shPath(packageRelativePathFromRoot)}`);
1185
+ } else {
1186
+ const pkgRel = shPath(packageRelativePath);
1187
+ if (pkgRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
1188
+ }
1132
1189
  console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
1133
1190
  console.log(chalk.dim(` ${step++}.`), "Edit src/index.ts to add your library code");
1134
1191
  break;
@@ -1140,11 +1197,27 @@ function printNextStepsExisting(answers, packageDir) {
1140
1197
  }
1141
1198
  //#endregion
1142
1199
  //#region src/index.ts
1143
- program.name("create").description("Scaffold and manage Mosaic packages").version({
1144
- name: "@percepta/create",
1145
- version: "1.0.0"
1146
- }.version);
1200
+ const packageJson = readCreatePackageMetadata();
1201
+ program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
1147
1202
  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);
1203
+ program.command("add").description("Add a Mosaic package to the current monorepo").argument("[typeOrName]", "Package type (webapp/library) or package name").argument("[name]", "Package/app name").option("-t, --type <type>", "Package type: webapp or library").option("--name <name>", "Package/app name").option("--skip-install", "Skip dependency installation (also skips the auto-run setup + dev + browser)", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(async (typeOrName, name, options) => {
1204
+ let projectType = options.type;
1205
+ let projectName = options.name;
1206
+ if (typeOrName === "webapp" || typeOrName === "library") {
1207
+ projectType = projectType ?? typeOrName;
1208
+ projectName = projectName ?? name;
1209
+ } else if (typeOrName === "monorepo") program.error("error: 'add' only supports webapp or library packages");
1210
+ else if (typeOrName) {
1211
+ if (name) program.error(`error: unknown package type "${typeOrName}". Expected webapp or library.`);
1212
+ projectName = projectName ?? typeOrName;
1213
+ }
1214
+ await createProject({
1215
+ ...options,
1216
+ type: projectType,
1217
+ name: projectName,
1218
+ addOnly: true
1219
+ });
1220
+ });
1148
1221
  program.command("status").description("Show template sync status for current app").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").action(async (options) => {
1149
1222
  const { statusCommand } = await import("./status-CKe4aKso.js");
1150
1223
  await statusCommand(options);
@@ -1154,11 +1227,11 @@ program.command("sync").description("Generate downstream sync context (template
1154
1227
  await syncCommand(options);
1155
1228
  });
1156
1229
  program.command("upstream").description("Generate upstream context (app → template)").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
1157
- const { upstreamCommand } = await import("./upstream-D-LH_1z4.js");
1230
+ const { upstreamCommand } = await import("./upstream-gUHLWSR1.js");
1158
1231
  await upstreamCommand(options);
1159
1232
  });
1160
1233
  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) => {
1161
- const { initCommand } = await import("./init-CtCp7Tv2.js");
1234
+ const { initCommand } = await import("./init-sI9aIrkU.js");
1162
1235
  await initCommand(options);
1163
1236
  });
1164
1237
  program.parse();