@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.
- package/README.md +13 -0
- package/dist/index.js +196 -123
- package/dist/index.js.map +1 -1
- package/dist/{init-CtCp7Tv2.js → init-sI9aIrkU.js} +2 -2
- package/dist/init-sI9aIrkU.js.map +1 -0
- package/dist/{upstream-D-LH_1z4.js → upstream-gUHLWSR1.js} +2 -2
- package/dist/upstream-gUHLWSR1.js.map +1 -0
- package/package.json +1 -1
- package/template-versions.json +2 -1
- package/templates/monorepo/README.md +8 -5
- package/templates/monorepo/auth/package.json +1 -1
- package/templates/monorepo/package.json.template +1 -0
- package/templates/monorepo/pnpm-workspace.yaml +13 -0
- package/templates/webapp/.claude/commands/upstream.md +1 -1
- package/templates/webapp/agent-skills/access-control.md +24 -1
- package/templates/webapp/drizzle.config.ts +7 -3
- package/templates/webapp/e2e/rbac.spec.ts +8 -4
- package/templates/webapp/package.json.template +6 -6
- package/templates/webapp/playwright.config.ts +5 -2
- package/templates/webapp/scripts/seed.ts +6 -2
- package/templates/webapp/src/drizzle/db.ts +1 -2
- package/templates/webapp/vitest.setup.ts +6 -2
- package/dist/init-CtCp7Tv2.js.map +0 -1
- package/dist/upstream-D-LH_1z4.js.map +0 -1
- package/templates/webapp/scripts/generate-migrations.ts +0 -28
- package/templates/webapp/scripts/migrate.ts +0 -21
- package/templates/webapp/scripts/setup-database.ts +0 -78
- package/templates/webapp/scripts/setup-readonly-user.ts +0 -193
- package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +0 -24
- package/templates/webapp/src/drizzle/migrationSql.ts +0 -8
- package/templates/webapp/src/drizzle/searchPath.test.ts +0 -21
- package/templates/webapp/src/drizzle/searchPath.ts +0 -16
- 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 {
|
|
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:
|
|
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(
|
|
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
|
|
703
|
-
templateCommit
|
|
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/
|
|
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) =>
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1084
|
-
console.log(chalk.dim(` ${step++}.`), `Add a package: ${chalk.cyan(`
|
|
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 (
|
|
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
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
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-
|
|
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-
|
|
1234
|
+
const { initCommand } = await import("./init-sI9aIrkU.js");
|
|
1162
1235
|
await initCommand(options);
|
|
1163
1236
|
});
|
|
1164
1237
|
program.parse();
|