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