@percepta/create 3.1.4 → 3.2.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 +8 -8
- package/dist/git-ops-C2CIjuce.js +51 -0
- package/dist/git-ops-C2CIjuce.js.map +1 -0
- package/dist/index.js +1085 -1072
- package/dist/index.js.map +1 -0
- package/dist/init-CtCp7Tv2.js +52 -0
- package/dist/init-CtCp7Tv2.js.map +1 -0
- package/dist/status-CKe4aKso.js +48 -0
- package/dist/status-CKe4aKso.js.map +1 -0
- package/dist/sync-D1vkoofl.js +101 -0
- package/dist/sync-D1vkoofl.js.map +1 -0
- package/dist/upstream-D-LH_1z4.js +85 -0
- package/dist/upstream-D-LH_1z4.js.map +1 -0
- package/package.json +23 -24
- package/template-versions.json +1 -1
- package/templates/monorepo/.github/workflows/access-control.yml +38 -0
- package/templates/monorepo/README.md +41 -2
- package/templates/monorepo/access/README.md +39 -0
- package/templates/monorepo/access/bootstrap-grants.yaml.example +9 -0
- package/templates/monorepo/access/dev-grants.yaml.example +19 -0
- package/templates/monorepo/access/dev-groups.yaml.example +8 -0
- package/templates/monorepo/access/reconcile.yaml.example +11 -0
- package/templates/monorepo/auth/README.md +26 -0
- package/templates/monorepo/auth/drizzle.config.ts +13 -0
- package/templates/monorepo/auth/package.json +32 -0
- package/templates/monorepo/auth/scripts/setup-database.ts +57 -0
- package/templates/monorepo/auth/src/auth.ts +77 -0
- package/templates/monorepo/auth/src/config/database.ts +31 -0
- package/templates/monorepo/auth/src/drizzle/db.ts +9 -0
- package/templates/monorepo/auth/src/drizzle/migrations/0000_shared_auth.sql +89 -0
- package/templates/monorepo/auth/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/accounts.ts +1 -6
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/sessions.ts +1 -5
- package/templates/{webapp → monorepo/auth}/src/drizzle/schema/auth/verifications.ts +0 -4
- package/templates/monorepo/auth/src/drizzle/schema/groups.ts +16 -0
- package/templates/monorepo/auth/src/drizzle/schema/index.ts +5 -0
- package/templates/monorepo/auth/src/drizzle/schema/users.ts +6 -0
- package/templates/monorepo/auth/src/index.ts +1 -0
- package/templates/monorepo/auth/src/scim/README.md +6 -0
- package/templates/monorepo/auth/tsconfig.json +12 -0
- package/templates/monorepo/package.json.template +18 -6
- package/templates/monorepo/pnpm-workspace.yaml +1 -0
- package/templates/webapp/AGENTS.md +13 -6
- package/templates/webapp/README.md +34 -18
- package/templates/webapp/agent-skills/access-control.md +301 -0
- package/templates/webapp/agent-skills/database.md +1 -1
- package/templates/webapp/docker-compose.yml +16 -0
- package/templates/webapp/env.example.template +9 -0
- package/templates/webapp/next.config.ts +1 -0
- package/templates/webapp/package.json.template +8 -4
- package/templates/webapp/scripts/seed.ts +87 -36
- package/templates/webapp/scripts/setup-database.ts +7 -1
- package/templates/webapp/scripts/start.sh +0 -9
- package/templates/webapp/src/access/access.manifest.ts +15 -0
- package/templates/webapp/src/access/schema.zed +7 -0
- package/templates/webapp/src/app/(app)/admin/_lib/PrincipalRoleTable.tsx +113 -0
- package/templates/webapp/src/app/(app)/admin/_lib/accessAdmin.ts +85 -0
- package/templates/webapp/src/app/(app)/admin/groups/page.tsx +117 -0
- package/templates/webapp/src/app/(app)/admin/users/page.tsx +79 -0
- package/templates/webapp/src/app/(app)/layout.tsx +16 -2
- package/templates/webapp/src/app/(app)/page.tsx +1 -12
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +2 -5
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +2 -5
- package/templates/webapp/src/config/getEnvConfig.ts +8 -0
- package/templates/webapp/src/drizzle/db.ts +3 -4
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +1 -57
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +1 -347
- package/templates/webapp/src/drizzle/schema/index.ts +3 -4
- package/templates/webapp/src/lib/auth/index.ts +6 -81
- package/templates/webapp/src/server/api/root.ts +4 -1
- package/templates/webapp/src/server/api/routers/access.ts +13 -0
- package/templates/webapp/src/server/trpc.ts +42 -8
- package/templates/webapp/src/services/DatabaseService.ts +4 -5
- package/templates/webapp/src/services/access/AppAccessControl.ts +39 -0
- package/dist/chunk-CO3YWUD6.js +0 -139
- package/dist/chunk-DCM7JOSC.js +0 -49
- package/dist/chunk-V5EJIUBJ.js +0 -60
- package/dist/index.d.ts +0 -1
- package/dist/init-EQZ2TCSJ.js +0 -96
- package/dist/status-QW5TQDYY.js +0 -76
- package/dist/sync-RLBZDOFB.js +0 -136
- package/dist/upstream-TQFVPMEG.js +0 -144
- package/templates/webapp/scripts/create-user.ts +0 -47
- package/templates/webapp/src/drizzle/schema/auth/users.ts +0 -38
package/dist/index.js
CHANGED
|
@@ -1,1155 +1,1168 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
VALID_PROJECT_TYPES,
|
|
4
|
-
isValidProjectType,
|
|
5
|
-
promptProjectDetails,
|
|
6
|
-
toKebabCase,
|
|
7
|
-
toSnakeCase,
|
|
8
|
-
toTitleCase,
|
|
9
|
-
validateProjectName
|
|
10
|
-
} from "./chunk-CO3YWUD6.js";
|
|
11
|
-
import {
|
|
12
|
-
derivePlaceholders,
|
|
13
|
-
writeManifest
|
|
14
|
-
} from "./chunk-V5EJIUBJ.js";
|
|
15
|
-
|
|
16
|
-
// src/index.ts
|
|
17
2
|
import { program } from "commander";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import path7 from "path";
|
|
21
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
|
-
import fs7 from "fs-extra";
|
|
3
|
+
import { execFile, execSync, spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
23
5
|
import chalk from "chalk";
|
|
24
|
-
import ora from "ora";
|
|
25
|
-
import { execSync, spawn } from "child_process";
|
|
26
|
-
|
|
27
|
-
// src/utils/copy-template.ts
|
|
28
|
-
import path from "path";
|
|
29
|
-
import { fileURLToPath } from "url";
|
|
30
6
|
import fs from "fs-extra";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parse } from "yaml";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import validateNpmPackageName from "validate-npm-package-name";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
//#region src/utils/case-converters.ts
|
|
15
|
+
/** Lowercase, hyphenated, npm-package-name-safe form: "My Cool App" → "my-cool-app". */
|
|
16
|
+
function toKebabCase(str) {
|
|
17
|
+
return str.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
18
|
+
}
|
|
19
|
+
/** Display form derived from a kebab-case name: "my-cool-app" → "My Cool App". */
|
|
20
|
+
function toTitleCase(str) {
|
|
21
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
22
|
+
}
|
|
23
|
+
/** Identifier form for env vars and DB names: "my-cool-app" → "my_cool_app". */
|
|
24
|
+
function toSnakeCase(str) {
|
|
25
|
+
return str.replace(/-/g, "_");
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/utils/copy-template.ts
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
const SKIP_DIRS$1 = new Set([
|
|
32
|
+
"node_modules",
|
|
33
|
+
".git",
|
|
34
|
+
".next",
|
|
35
|
+
"dist",
|
|
36
|
+
".turbo",
|
|
37
|
+
".vercel",
|
|
38
|
+
".cursor"
|
|
41
39
|
]);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const SKIP_FILES$1 = new Set([
|
|
41
|
+
"pnpm-lock.yaml",
|
|
42
|
+
"package-lock.json",
|
|
43
|
+
"yarn.lock",
|
|
44
|
+
".DS_Store"
|
|
47
45
|
]);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const TEMPLATE_FILE_MAPPINGS = {
|
|
47
|
+
"package.json.template": "package.json",
|
|
48
|
+
"gitignore.template": ".gitignore",
|
|
49
|
+
"env.example.template": ".env.example",
|
|
50
|
+
"npmrc.template": ".npmrc"
|
|
53
51
|
};
|
|
54
52
|
function shouldSkip(src) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
const basename = path.basename(src);
|
|
54
|
+
if (SKIP_DIRS$1.has(basename)) return true;
|
|
55
|
+
if (SKIP_FILES$1.has(basename)) return true;
|
|
56
|
+
return false;
|
|
59
57
|
}
|
|
60
58
|
function getTemplateDir(templateType) {
|
|
61
|
-
|
|
59
|
+
return path.resolve(__dirname, "../templates", templateType);
|
|
62
60
|
}
|
|
63
61
|
async function copyTemplate(targetDir, templateType) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (templateType === "webapp") {
|
|
82
|
-
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
83
|
-
const claudePath = path.join(targetDir, "CLAUDE.md");
|
|
84
|
-
if (await fs.pathExists(agentsPath)) {
|
|
85
|
-
if (await fs.pathExists(claudePath)) {
|
|
86
|
-
await fs.remove(claudePath);
|
|
87
|
-
}
|
|
88
|
-
await fs.ensureSymlink("AGENTS.md", claudePath);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
62
|
+
const templateDir = getTemplateDir(templateType);
|
|
63
|
+
if (!await fs.pathExists(templateDir)) throw new Error(`Template directory not found: ${templateDir}`);
|
|
64
|
+
await fs.ensureDir(targetDir);
|
|
65
|
+
await fs.copy(templateDir, targetDir, { filter: (src) => !shouldSkip(src) });
|
|
66
|
+
for (const [templateName, targetName] of Object.entries(TEMPLATE_FILE_MAPPINGS)) {
|
|
67
|
+
const templatePath = path.join(targetDir, templateName);
|
|
68
|
+
const targetPath = path.join(targetDir, targetName);
|
|
69
|
+
if (await fs.pathExists(templatePath)) await fs.move(templatePath, targetPath, { overwrite: true });
|
|
70
|
+
}
|
|
71
|
+
if (templateType === "webapp") {
|
|
72
|
+
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
73
|
+
const claudePath = path.join(targetDir, "CLAUDE.md");
|
|
74
|
+
if (await fs.pathExists(agentsPath)) {
|
|
75
|
+
if (await fs.pathExists(claudePath)) await fs.remove(claudePath);
|
|
76
|
+
await fs.ensureSymlink("AGENTS.md", claudePath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
91
79
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/utils/detect-monorepo.ts
|
|
82
|
+
const NOT_FOUND = {
|
|
83
|
+
found: false,
|
|
84
|
+
rootDir: null,
|
|
85
|
+
workspacePatterns: [],
|
|
86
|
+
packageDir: null
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Walk up from `startDir` looking for pnpm-workspace.yaml.
|
|
90
|
+
* If found, parse it and derive the package directory.
|
|
91
|
+
*/
|
|
92
|
+
async function detectMonorepo(startDir) {
|
|
93
|
+
let current = path.resolve(startDir);
|
|
94
|
+
const root = path.parse(current).root;
|
|
95
|
+
while (current !== root) {
|
|
96
|
+
const workspaceFile = path.join(current, "pnpm-workspace.yaml");
|
|
97
|
+
if (await fs.pathExists(workspaceFile)) try {
|
|
98
|
+
const parsed = parse(await fs.readFile(workspaceFile, "utf-8"));
|
|
99
|
+
if (!parsed?.packages || !Array.isArray(parsed.packages)) return NOT_FOUND;
|
|
100
|
+
const workspacePatterns = parsed.packages;
|
|
101
|
+
const firstPattern = workspacePatterns[0];
|
|
102
|
+
if (!firstPattern) return NOT_FOUND;
|
|
103
|
+
const baseDir = firstPattern.replace(/\/?\*.*$/, "").trim();
|
|
104
|
+
const packageDir = path.join(current, baseDir);
|
|
105
|
+
return {
|
|
106
|
+
found: true,
|
|
107
|
+
rootDir: current,
|
|
108
|
+
workspacePatterns,
|
|
109
|
+
packageDir
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return NOT_FOUND;
|
|
113
|
+
}
|
|
114
|
+
current = path.dirname(current);
|
|
115
|
+
}
|
|
116
|
+
return NOT_FOUND;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/utils/env-local.ts
|
|
120
|
+
/**
|
|
121
|
+
* Writes .env.local for the webapp template with real generated values for
|
|
122
|
+
* BETTER_AUTH_SECRET and ENCRYPTION_SECRET_KEY so the user can run dev/seed
|
|
123
|
+
* immediately. Also writes deploy/ryvn/percepta-test.secrets.env with separate
|
|
124
|
+
* generated values that can be imported into Ryvn for the deployed installation.
|
|
125
|
+
* The .env.example file remains the documentation source.
|
|
126
|
+
*
|
|
127
|
+
* Each generated file is a no-op if it already exists.
|
|
128
|
+
*/
|
|
129
|
+
async function generateEnvLocal(packageDir) {
|
|
130
|
+
const examplePath = path.join(packageDir, ".env.example");
|
|
131
|
+
const localPath = path.join(packageDir, ".env.local");
|
|
132
|
+
if (!await fs.pathExists(examplePath)) return;
|
|
133
|
+
if (!await fs.pathExists(localPath)) {
|
|
134
|
+
const authSecret = randomBytes(32).toString("base64");
|
|
135
|
+
const encKey = randomBytes(16).toString("hex");
|
|
136
|
+
const content = (await fs.readFile(examplePath, "utf-8")).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${authSecret}`).replace(/^ENCRYPTION_SECRET_KEY=.*$/m, `ENCRYPTION_SECRET_KEY=${encKey}`);
|
|
137
|
+
await fs.writeFile(localPath, content);
|
|
138
|
+
}
|
|
139
|
+
const ryvnSecretsPath = path.join(packageDir, "deploy", "ryvn", "percepta-test.secrets.env");
|
|
140
|
+
if (await fs.pathExists(ryvnSecretsPath)) return;
|
|
141
|
+
const deployAuthSecret = randomBytes(32).toString("base64");
|
|
142
|
+
const deployEncKey = randomBytes(16).toString("hex");
|
|
143
|
+
const deploySecrets = [
|
|
144
|
+
`BETTER_AUTH_SECRET=${deployAuthSecret}`,
|
|
145
|
+
`ENCRYPTION_SECRET_KEY=${deployEncKey}`,
|
|
146
|
+
"",
|
|
147
|
+
"# Langfuse and LLM demo credentials are inherited from the demos-commons Ryvn variable group.",
|
|
148
|
+
""
|
|
149
|
+
].join("\n");
|
|
150
|
+
await fs.ensureDir(path.dirname(ryvnSecretsPath));
|
|
151
|
+
await fs.writeFile(ryvnSecretsPath, deploySecrets);
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/utils/manifest.ts
|
|
155
|
+
const MANIFEST_FILENAME = ".mosaic-template.json";
|
|
156
|
+
function getManifestPath(dir) {
|
|
157
|
+
return path.join(dir, MANIFEST_FILENAME);
|
|
158
|
+
}
|
|
159
|
+
async function readManifest(dir) {
|
|
160
|
+
const manifestPath = getManifestPath(dir);
|
|
161
|
+
if (!await fs.pathExists(manifestPath)) throw new Error(`No ${MANIFEST_FILENAME} found in ${dir}. Run 'create init' to create one.`);
|
|
162
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(content);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`Invalid JSON in ${MANIFEST_FILENAME}: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function writeManifest(dir, manifest) {
|
|
170
|
+
const manifestPath = getManifestPath(dir);
|
|
171
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
172
|
+
}
|
|
173
|
+
async function manifestExists(dir) {
|
|
174
|
+
return fs.pathExists(getManifestPath(dir));
|
|
175
|
+
}
|
|
176
|
+
function derivePlaceholders(appName, appTitle, repoName = appName) {
|
|
177
|
+
const nameSnake = appName.replace(/-/g, "_");
|
|
178
|
+
const repoNameSnake = repoName.replace(/-/g, "_");
|
|
179
|
+
return {
|
|
180
|
+
__APP_NAME__: appName,
|
|
181
|
+
__APP_TITLE__: appTitle,
|
|
182
|
+
__DB_NAME__: nameSnake + "_db",
|
|
183
|
+
__APP_NAME_UPPER__: appName.toUpperCase(),
|
|
184
|
+
__APP_NAME_SNAKE__: nameSnake,
|
|
185
|
+
__REPO_NAME__: repoName,
|
|
186
|
+
__REPO_NAME_SNAKE__: repoNameSnake
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function resolveMosaicTemplatePath(options) {
|
|
190
|
+
if (options.mosaicTemplatePath) return path.resolve(options.mosaicTemplatePath);
|
|
191
|
+
if (process.env.MOSAIC_TEMPLATE_PATH) return path.resolve(process.env.MOSAIC_TEMPLATE_PATH);
|
|
192
|
+
throw new Error("Mosaic repo path required. Use --mosaic-template-path or set MOSAIC_TEMPLATE_PATH.");
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/utils/validate.ts
|
|
196
|
+
function validateProjectName(name) {
|
|
197
|
+
const result = validateNpmPackageName(name);
|
|
198
|
+
if (!result.validForNewPackages) return {
|
|
199
|
+
valid: false,
|
|
200
|
+
error: [...result.errors || [], ...result.warnings || []][0] || "Invalid package name"
|
|
201
|
+
};
|
|
202
|
+
return { valid: true };
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/utils/prompts.ts
|
|
206
|
+
const VALID_PROJECT_TYPES = [
|
|
207
|
+
"monorepo",
|
|
208
|
+
"webapp",
|
|
209
|
+
"library"
|
|
210
|
+
];
|
|
211
|
+
function isValidProjectType(value) {
|
|
212
|
+
return typeof value === "string" && VALID_PROJECT_TYPES.includes(value);
|
|
213
|
+
}
|
|
214
|
+
async function promptName(message) {
|
|
215
|
+
const { name } = await inquirer.prompt([{
|
|
216
|
+
type: "input",
|
|
217
|
+
name: "name",
|
|
218
|
+
message,
|
|
219
|
+
filter: toKebabCase,
|
|
220
|
+
validate: (input) => {
|
|
221
|
+
const result = validateProjectName(toKebabCase(input));
|
|
222
|
+
return result.valid || result.error || "Invalid project name";
|
|
223
|
+
}
|
|
224
|
+
}]);
|
|
225
|
+
return name;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Outside a monorepo we collect the repo name first, then ask a single
|
|
229
|
+
* "is it a webapp?" Y/n. Yes (default) → monorepo + webapp, no → bare
|
|
230
|
+
* monorepo. Library at the top level is rare enough that we leave it as a
|
|
231
|
+
* `--type library` flag rather than a third prompt option. The repo name is
|
|
232
|
+
* collected separately from any initial package name so a customer monorepo
|
|
233
|
+
* is not forced to share its first app's name.
|
|
234
|
+
*/
|
|
235
|
+
async function promptOutsideMonorepoType() {
|
|
236
|
+
const { webapp } = await inquirer.prompt([{
|
|
237
|
+
type: "confirm",
|
|
238
|
+
name: "webapp",
|
|
239
|
+
message: "Initialize with a webapp?",
|
|
240
|
+
default: true
|
|
241
|
+
}]);
|
|
242
|
+
return webapp ? "webapp" : "monorepo";
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Inside a monorepo, both webapp and library are common, so present them as
|
|
246
|
+
* a numbered rawlist — user presses 1 or 2 + Enter. Defaults to webapp.
|
|
247
|
+
*/
|
|
248
|
+
async function promptInsideMonorepoType() {
|
|
249
|
+
const { projectType } = await inquirer.prompt([{
|
|
250
|
+
type: "rawlist",
|
|
251
|
+
name: "projectType",
|
|
252
|
+
message: "What kind of package?",
|
|
253
|
+
default: "webapp",
|
|
254
|
+
choices: [{
|
|
255
|
+
name: "Webapp — A Next.js webapp",
|
|
256
|
+
value: "webapp"
|
|
257
|
+
}, {
|
|
258
|
+
name: "Library — A TypeScript library",
|
|
259
|
+
value: "library"
|
|
260
|
+
}]
|
|
261
|
+
}]);
|
|
262
|
+
return projectType;
|
|
263
|
+
}
|
|
264
|
+
async function promptProjectDetails(defaults) {
|
|
265
|
+
const inMonorepo = defaults.monorepoContext?.found ?? false;
|
|
266
|
+
const cwd = defaults.cwd ?? process.cwd();
|
|
267
|
+
let projectType;
|
|
268
|
+
let finalName;
|
|
269
|
+
if (inMonorepo) {
|
|
270
|
+
projectType = defaults.projectType ?? await promptInsideMonorepoType();
|
|
271
|
+
await defaults.beforeNamePrompt?.(projectType);
|
|
272
|
+
finalName = defaults.name || await promptName("Package name?");
|
|
273
|
+
} else {
|
|
274
|
+
const repoName = defaults.repoName || (defaults.projectType === "monorepo" ? defaults.name : void 0) || await promptName("Repo name?");
|
|
275
|
+
const repoTitle = toTitleCase(repoName);
|
|
276
|
+
projectType = defaults.projectType ?? await promptOutsideMonorepoType();
|
|
277
|
+
await defaults.beforeNamePrompt?.(projectType);
|
|
278
|
+
if (projectType === "monorepo") {
|
|
279
|
+
finalName = repoName;
|
|
280
|
+
const finalTitle = repoTitle;
|
|
281
|
+
const finalDirectory = path.resolve(cwd, repoName);
|
|
282
|
+
return {
|
|
283
|
+
projectType,
|
|
284
|
+
directory: finalDirectory,
|
|
285
|
+
name: finalName,
|
|
286
|
+
title: finalTitle,
|
|
287
|
+
installDeps: !defaults.skipInstall,
|
|
288
|
+
monorepoName: repoName,
|
|
289
|
+
monorepoTitle: repoTitle
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const packageNamePrompt = projectType === "webapp" ? "Webapp name?" : "Library name?";
|
|
293
|
+
finalName = defaults.name || await promptName(packageNamePrompt);
|
|
294
|
+
const finalTitle = toTitleCase(finalName);
|
|
295
|
+
const finalDirectory = path.resolve(cwd, repoName);
|
|
296
|
+
return {
|
|
297
|
+
projectType,
|
|
298
|
+
directory: finalDirectory,
|
|
299
|
+
name: finalName,
|
|
300
|
+
title: finalTitle,
|
|
301
|
+
installDeps: !defaults.skipInstall,
|
|
302
|
+
monorepoName: repoName,
|
|
303
|
+
monorepoTitle: repoTitle
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const finalTitle = finalName ? toTitleCase(finalName) : "";
|
|
307
|
+
const finalDirectory = !inMonorepo && finalName ? path.resolve(cwd, finalName) : "";
|
|
308
|
+
return {
|
|
309
|
+
projectType,
|
|
310
|
+
directory: finalDirectory,
|
|
311
|
+
name: finalName,
|
|
312
|
+
title: finalTitle,
|
|
313
|
+
installDeps: !defaults.skipInstall
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/utils/relocate-workflows.ts
|
|
318
|
+
/**
|
|
319
|
+
* Moves per-app GitHub Actions workflows from the package's .github/workflows
|
|
320
|
+
* directory to the monorepo root, where GitHub Actions actually picks them up.
|
|
321
|
+
*
|
|
322
|
+
* Only moves files whose names start with the app name (e.g.
|
|
323
|
+
* `myapp-ryvn-release.yaml`). Generic workflows like `ci.yml` are left in
|
|
324
|
+
* place — those are an unrelated monorepo-vs-package concern.
|
|
325
|
+
*
|
|
326
|
+
* Cleans up an empty `.github/workflows` (and empty parent `.github`) after
|
|
327
|
+
* the move.
|
|
328
|
+
*/
|
|
329
|
+
async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
|
|
330
|
+
const sourceDir = path.join(packageDir, ".github", "workflows");
|
|
331
|
+
if (!await fs.pathExists(sourceDir)) return;
|
|
332
|
+
const targetDir = path.join(monorepoRoot, ".github", "workflows");
|
|
333
|
+
await fs.ensureDir(targetDir);
|
|
334
|
+
const entries = await fs.readdir(sourceDir);
|
|
335
|
+
for (const name of entries) {
|
|
336
|
+
if (!name.startsWith(`${appName}-`)) continue;
|
|
337
|
+
if (!/\.(ya?ml)$/.test(name)) continue;
|
|
338
|
+
await fs.move(path.join(sourceDir, name), path.join(targetDir, name), { overwrite: true });
|
|
339
|
+
}
|
|
340
|
+
if ((await fs.readdir(sourceDir)).length === 0) {
|
|
341
|
+
await fs.rmdir(sourceDir);
|
|
342
|
+
const packageGithub = path.join(packageDir, ".github");
|
|
343
|
+
if ((await fs.readdir(packageGithub)).length === 0) await fs.rmdir(packageGithub);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/utils/replace-placeholders.ts
|
|
348
|
+
const PLACEHOLDERS = {
|
|
349
|
+
__APP_NAME__: "name",
|
|
350
|
+
__APP_TITLE__: "title",
|
|
351
|
+
__DB_NAME__: "dbName",
|
|
352
|
+
__APP_NAME_UPPER__: "nameUpper",
|
|
353
|
+
__APP_NAME_SNAKE__: "nameSnake",
|
|
354
|
+
__REPO_NAME__: "repoName",
|
|
355
|
+
__REPO_NAME_SNAKE__: "repoNameSnake"
|
|
104
356
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
357
|
+
const SKIP_DIRS = new Set([
|
|
358
|
+
"node_modules",
|
|
359
|
+
".git",
|
|
360
|
+
".next",
|
|
361
|
+
"dist",
|
|
362
|
+
".turbo",
|
|
363
|
+
".vercel"
|
|
112
364
|
]);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
365
|
+
const SKIP_FILES = new Set([
|
|
366
|
+
"pnpm-lock.yaml",
|
|
367
|
+
"package-lock.json",
|
|
368
|
+
"yarn.lock"
|
|
117
369
|
]);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
370
|
+
const PROCESSABLE_EXTENSIONS = new Set([
|
|
371
|
+
".ts",
|
|
372
|
+
".tsx",
|
|
373
|
+
".js",
|
|
374
|
+
".jsx",
|
|
375
|
+
".json",
|
|
376
|
+
".yml",
|
|
377
|
+
".yaml",
|
|
378
|
+
".md",
|
|
379
|
+
".env",
|
|
380
|
+
".sql",
|
|
381
|
+
".tf",
|
|
382
|
+
".tfvars",
|
|
383
|
+
".sh",
|
|
384
|
+
".zed",
|
|
385
|
+
".mjs",
|
|
386
|
+
".cjs"
|
|
134
387
|
]);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
388
|
+
const PROCESSABLE_FILENAMES = new Set([
|
|
389
|
+
"Dockerfile",
|
|
390
|
+
".env.example",
|
|
391
|
+
".env.local",
|
|
392
|
+
"terraform.tfvars.example"
|
|
140
393
|
]);
|
|
141
394
|
function shouldProcessFile(filePath) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
395
|
+
const fileName = path.basename(filePath);
|
|
396
|
+
const ext = path.extname(filePath);
|
|
397
|
+
if (SKIP_FILES.has(fileName)) return false;
|
|
398
|
+
if (PROCESSABLE_FILENAMES.has(fileName)) return true;
|
|
399
|
+
return PROCESSABLE_EXTENSIONS.has(ext);
|
|
147
400
|
}
|
|
148
401
|
async function replaceInFile(filePath, config) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
return false;
|
|
402
|
+
let content;
|
|
403
|
+
try {
|
|
404
|
+
content = await fs.readFile(filePath, "utf-8");
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
let modified = false;
|
|
409
|
+
let newContent = content;
|
|
410
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort((a, b) => b[0].length - a[0].length);
|
|
411
|
+
for (const [placeholder, configKey] of sortedEntries) {
|
|
412
|
+
const value = config[configKey];
|
|
413
|
+
if (newContent.includes(placeholder)) {
|
|
414
|
+
newContent = newContent.split(placeholder).join(value);
|
|
415
|
+
modified = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (modified) {
|
|
419
|
+
await fs.writeFile(filePath, newContent);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
172
423
|
}
|
|
173
424
|
function substituteName(name, config) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for (const [placeholder, configKey] of sortedEntries) {
|
|
179
|
-
if (result.includes(placeholder)) {
|
|
180
|
-
result = result.split(placeholder).join(
|
|
181
|
-
config[configKey]
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
425
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort((a, b) => b[0].length - a[0].length);
|
|
426
|
+
let result = name;
|
|
427
|
+
for (const [placeholder, configKey] of sortedEntries) if (result.includes(placeholder)) result = result.split(placeholder).join(config[configKey]);
|
|
428
|
+
return result;
|
|
186
429
|
}
|
|
187
430
|
async function processDirectory(dirPath, config, stats) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const renamed = substituteName(entry.name, config);
|
|
201
|
-
if (renamed !== entry.name) {
|
|
202
|
-
await fs2.move(fullPath, path2.join(dirPath, renamed), {
|
|
203
|
-
overwrite: true
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
431
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
434
|
+
if (entry.isDirectory()) {
|
|
435
|
+
if (!SKIP_DIRS.has(entry.name)) await processDirectory(fullPath, config, stats);
|
|
436
|
+
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
|
437
|
+
stats.processed++;
|
|
438
|
+
if (await replaceInFile(fullPath, config)) stats.modified++;
|
|
439
|
+
const renamed = substituteName(entry.name, config);
|
|
440
|
+
if (renamed !== entry.name) await fs.move(fullPath, path.join(dirPath, renamed), { overwrite: true });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
208
443
|
}
|
|
209
444
|
async function replacePlaceholders(targetDir, config) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
import path3 from "path";
|
|
217
|
-
import fs3 from "fs-extra";
|
|
218
|
-
import { parse } from "yaml";
|
|
219
|
-
var NOT_FOUND = {
|
|
220
|
-
found: false,
|
|
221
|
-
rootDir: null,
|
|
222
|
-
workspacePatterns: [],
|
|
223
|
-
packageDir: null
|
|
224
|
-
};
|
|
225
|
-
async function detectMonorepo(startDir) {
|
|
226
|
-
let current = path3.resolve(startDir);
|
|
227
|
-
const root = path3.parse(current).root;
|
|
228
|
-
while (current !== root) {
|
|
229
|
-
const workspaceFile = path3.join(current, "pnpm-workspace.yaml");
|
|
230
|
-
if (await fs3.pathExists(workspaceFile)) {
|
|
231
|
-
try {
|
|
232
|
-
const content = await fs3.readFile(workspaceFile, "utf-8");
|
|
233
|
-
const parsed = parse(content);
|
|
234
|
-
if (!parsed?.packages || !Array.isArray(parsed.packages)) {
|
|
235
|
-
return NOT_FOUND;
|
|
236
|
-
}
|
|
237
|
-
const workspacePatterns = parsed.packages;
|
|
238
|
-
const firstPattern = workspacePatterns[0];
|
|
239
|
-
if (!firstPattern) {
|
|
240
|
-
return NOT_FOUND;
|
|
241
|
-
}
|
|
242
|
-
const baseDir = firstPattern.replace(/\/?\*.*$/, "").trim();
|
|
243
|
-
const packageDir = path3.join(current, baseDir);
|
|
244
|
-
return {
|
|
245
|
-
found: true,
|
|
246
|
-
rootDir: current,
|
|
247
|
-
workspacePatterns,
|
|
248
|
-
packageDir
|
|
249
|
-
};
|
|
250
|
-
} catch {
|
|
251
|
-
return NOT_FOUND;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
current = path3.dirname(current);
|
|
255
|
-
}
|
|
256
|
-
return NOT_FOUND;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// src/utils/env-local.ts
|
|
260
|
-
import path4 from "path";
|
|
261
|
-
import { randomBytes } from "crypto";
|
|
262
|
-
import fs4 from "fs-extra";
|
|
263
|
-
async function generateEnvLocal(packageDir) {
|
|
264
|
-
const examplePath = path4.join(packageDir, ".env.example");
|
|
265
|
-
const localPath = path4.join(packageDir, ".env.local");
|
|
266
|
-
if (!await fs4.pathExists(examplePath)) return;
|
|
267
|
-
if (!await fs4.pathExists(localPath)) {
|
|
268
|
-
const authSecret = randomBytes(32).toString("base64");
|
|
269
|
-
const encKey = randomBytes(16).toString("hex");
|
|
270
|
-
const content = (await fs4.readFile(examplePath, "utf-8")).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${authSecret}`).replace(/^ENCRYPTION_SECRET_KEY=.*$/m, `ENCRYPTION_SECRET_KEY=${encKey}`);
|
|
271
|
-
await fs4.writeFile(localPath, content);
|
|
272
|
-
}
|
|
273
|
-
const ryvnSecretsPath = path4.join(
|
|
274
|
-
packageDir,
|
|
275
|
-
"deploy",
|
|
276
|
-
"ryvn",
|
|
277
|
-
"percepta-test.secrets.env"
|
|
278
|
-
);
|
|
279
|
-
if (await fs4.pathExists(ryvnSecretsPath)) return;
|
|
280
|
-
const deployAuthSecret = randomBytes(32).toString("base64");
|
|
281
|
-
const deployEncKey = randomBytes(16).toString("hex");
|
|
282
|
-
const deploySecrets = [
|
|
283
|
-
`BETTER_AUTH_SECRET=${deployAuthSecret}`,
|
|
284
|
-
`ENCRYPTION_SECRET_KEY=${deployEncKey}`,
|
|
285
|
-
"",
|
|
286
|
-
"# Langfuse and LLM demo credentials are inherited from the demos-commons Ryvn variable group.",
|
|
287
|
-
""
|
|
288
|
-
].join("\n");
|
|
289
|
-
await fs4.ensureDir(path4.dirname(ryvnSecretsPath));
|
|
290
|
-
await fs4.writeFile(ryvnSecretsPath, deploySecrets);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// src/utils/relocate-workflows.ts
|
|
294
|
-
import path5 from "path";
|
|
295
|
-
import fs5 from "fs-extra";
|
|
296
|
-
async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
|
|
297
|
-
const sourceDir = path5.join(packageDir, ".github", "workflows");
|
|
298
|
-
if (!await fs5.pathExists(sourceDir)) return;
|
|
299
|
-
const targetDir = path5.join(monorepoRoot, ".github", "workflows");
|
|
300
|
-
await fs5.ensureDir(targetDir);
|
|
301
|
-
const entries = await fs5.readdir(sourceDir);
|
|
302
|
-
for (const name of entries) {
|
|
303
|
-
if (!name.startsWith(`${appName}-`)) continue;
|
|
304
|
-
if (!/\.(ya?ml)$/.test(name)) continue;
|
|
305
|
-
await fs5.move(path5.join(sourceDir, name), path5.join(targetDir, name), {
|
|
306
|
-
overwrite: true
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
if ((await fs5.readdir(sourceDir)).length === 0) {
|
|
310
|
-
await fs5.rmdir(sourceDir);
|
|
311
|
-
const packageGithub = path5.join(packageDir, ".github");
|
|
312
|
-
if ((await fs5.readdir(packageGithub)).length === 0) {
|
|
313
|
-
await fs5.rmdir(packageGithub);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
445
|
+
const stats = {
|
|
446
|
+
processed: 0,
|
|
447
|
+
modified: 0
|
|
448
|
+
};
|
|
449
|
+
await processDirectory(targetDir, config, stats);
|
|
450
|
+
return stats;
|
|
316
451
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"dependencies",
|
|
326
|
-
"devDependencies",
|
|
327
|
-
"optionalDependencies",
|
|
328
|
-
"peerDependencies"
|
|
452
|
+
//#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"
|
|
329
460
|
];
|
|
330
461
|
function getPerceptaPackages(pkg) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return [...names].sort();
|
|
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();
|
|
342
469
|
}
|
|
343
470
|
async function npmViewDistTagLatest(packageName, cwd) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
483
|
}
|
|
356
484
|
async function resolvePerceptaVersionsInPackageJson(packageJsonPath, lookupLatest = npmViewDistTagLatest) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
};
|
|
392
522
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/utils/template-versions.ts
|
|
525
|
+
const FALLBACK_TEMPLATE_VERSION = "1.0.0";
|
|
526
|
+
function readTemplateVersions() {
|
|
527
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
528
|
+
const candidates = [path.resolve(currentDir, "../template-versions.json"), path.resolve(currentDir, "../../template-versions.json")];
|
|
529
|
+
for (const versionsPath of candidates) try {
|
|
530
|
+
const content = fs.readFileSync(versionsPath, "utf-8");
|
|
531
|
+
return JSON.parse(content);
|
|
532
|
+
} catch {}
|
|
533
|
+
return {};
|
|
534
|
+
}
|
|
535
|
+
function getTemplateVersion(templateType) {
|
|
536
|
+
return readTemplateVersions()[templateType] ?? FALLBACK_TEMPLATE_VERSION;
|
|
537
|
+
}
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/commands/create.ts
|
|
540
|
+
const PACKAGE_MANAGER = "pnpm";
|
|
541
|
+
/** Paths in copy-paste shell commands (POSIX-style). */
|
|
396
542
|
function shPath(p) {
|
|
397
|
-
|
|
543
|
+
return p.split(path.sep).join("/");
|
|
398
544
|
}
|
|
545
|
+
/** Non-blocking install so ora can animate (execSync would block timers). */
|
|
399
546
|
function runPackageManagerInstall(packageManager, cwd, args = ["install"]) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`
|
|
412
|
-
)
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
});
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
const child = spawn(packageManager, args, {
|
|
549
|
+
cwd,
|
|
550
|
+
stdio: "ignore"
|
|
551
|
+
});
|
|
552
|
+
child.on("error", reject);
|
|
553
|
+
child.on("close", (code) => {
|
|
554
|
+
if (code === 0) resolve();
|
|
555
|
+
else reject(/* @__PURE__ */ new Error(`${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
|
|
556
|
+
});
|
|
557
|
+
});
|
|
416
558
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Runs the monorepo-root `setup` script (docker + access + db + seed).
|
|
561
|
+
* Uses `pnpm run setup` (not `pnpm setup`) because `pnpm setup` is a pnpm builtin that configures
|
|
562
|
+
* PNPM_HOME in the user's shell rc — it ignores the package.json script of the same name.
|
|
563
|
+
*/
|
|
564
|
+
function runWebappSetup(packageManager, monorepoRoot) {
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
const child = spawn(packageManager, ["run", "setup"], {
|
|
567
|
+
cwd: monorepoRoot,
|
|
568
|
+
stdio: "inherit"
|
|
569
|
+
});
|
|
570
|
+
child.on("error", reject);
|
|
571
|
+
child.on("close", (code) => {
|
|
572
|
+
if (code === 0) resolve();
|
|
573
|
+
else reject(/* @__PURE__ */ new Error(`${packageManager} run setup exited with code ${code ?? "unknown"}`));
|
|
574
|
+
});
|
|
575
|
+
});
|
|
426
576
|
}
|
|
427
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Spawns `pnpm dev` in the package directory and resolves once Next reports
|
|
579
|
+
* "Ready in" (so we know the server is accepting requests). Returns the child
|
|
580
|
+
* process so the caller can await its exit when the user hits Ctrl+C.
|
|
581
|
+
*
|
|
582
|
+
* Captures the actual URL Next picked (Next falls back to 3001+ if 3000 is
|
|
583
|
+
* taken) so the caller can open the right one in the browser.
|
|
584
|
+
*/
|
|
585
|
+
const ANSI_PATTERN = /\u001b\[[0-9;]*[a-zA-Z]/g;
|
|
428
586
|
function spawnDevServer(packageManager, cwd) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
587
|
+
const child = spawn(packageManager, ["run", "dev"], {
|
|
588
|
+
cwd,
|
|
589
|
+
stdio: [
|
|
590
|
+
"inherit",
|
|
591
|
+
"pipe",
|
|
592
|
+
"pipe"
|
|
593
|
+
]
|
|
594
|
+
});
|
|
595
|
+
return {
|
|
596
|
+
child,
|
|
597
|
+
ready: new Promise((resolve, reject) => {
|
|
598
|
+
let resolved = false;
|
|
599
|
+
let detectedUrl = "http://localhost:3000";
|
|
600
|
+
let buffer = "";
|
|
601
|
+
const onChunk = (chunk) => {
|
|
602
|
+
const text = chunk.toString();
|
|
603
|
+
process.stdout.write(text);
|
|
604
|
+
buffer = (buffer + text).slice(-4096).replace(ANSI_PATTERN, "");
|
|
605
|
+
const urlMatch = buffer.match(/Local:\s+(https?:\/\/\S+?)(?:\s|$)/i);
|
|
606
|
+
if (urlMatch?.[1]) detectedUrl = urlMatch[1].trim();
|
|
607
|
+
if (!resolved && /Ready in /i.test(buffer)) {
|
|
608
|
+
resolved = true;
|
|
609
|
+
resolve({ url: detectedUrl });
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
child.stdout?.on("data", onChunk);
|
|
613
|
+
child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
614
|
+
child.on("error", reject);
|
|
615
|
+
child.on("close", (code) => {
|
|
616
|
+
if (!resolved) reject(/* @__PURE__ */ new Error(`${packageManager} run dev exited with code ${code ?? "unknown"} before becoming ready`));
|
|
617
|
+
});
|
|
618
|
+
})
|
|
619
|
+
};
|
|
458
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Cross-platform "open this URL in the user's default browser". Returns true
|
|
623
|
+
* if the launcher process spawned, false on synchronous failure. The empty
|
|
624
|
+
* `error` listener swallows the asynchronous error event that fires when the
|
|
625
|
+
* launcher binary is missing (e.g. minimal Linux without `xdg-open`) — without
|
|
626
|
+
* it, that event becomes an uncaught exception that kills the CLI mid-run.
|
|
627
|
+
*/
|
|
459
628
|
function openInBrowser(url) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
} catch (error) {
|
|
479
|
-
console.log();
|
|
480
|
-
console.log(
|
|
481
|
-
chalk.yellow("!"),
|
|
482
|
-
"Setup failed. You can re-run it manually:",
|
|
483
|
-
chalk.cyan(`cd ${packageDir} && ${packageManager} run setup`)
|
|
484
|
-
);
|
|
485
|
-
console.log(chalk.dim(error.message));
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
console.log();
|
|
489
|
-
console.log(chalk.bold("Starting dev server..."));
|
|
490
|
-
console.log();
|
|
491
|
-
const { child, ready } = spawnDevServer(packageManager, packageDir);
|
|
492
|
-
const closed = new Promise((resolve) => {
|
|
493
|
-
child.on("close", () => resolve());
|
|
494
|
-
});
|
|
495
|
-
let url = "http://localhost:3000";
|
|
496
|
-
try {
|
|
497
|
-
({ url } = await ready);
|
|
498
|
-
} catch (error) {
|
|
499
|
-
console.log();
|
|
500
|
-
console.log(chalk.yellow("!"), "Dev server failed to become ready.");
|
|
501
|
-
console.log(chalk.dim(error.message));
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
if (openInBrowser(url)) {
|
|
505
|
-
console.log();
|
|
506
|
-
console.log(chalk.green("\u2714"), "Opened", chalk.cyan(url));
|
|
507
|
-
} else {
|
|
508
|
-
console.log();
|
|
509
|
-
console.log(chalk.dim("Open"), chalk.cyan(url), chalk.dim("in your browser."));
|
|
510
|
-
}
|
|
511
|
-
await closed;
|
|
512
|
-
return true;
|
|
629
|
+
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? [
|
|
630
|
+
"cmd",
|
|
631
|
+
"/c",
|
|
632
|
+
"start",
|
|
633
|
+
"",
|
|
634
|
+
url
|
|
635
|
+
] : ["xdg-open", url];
|
|
636
|
+
try {
|
|
637
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
638
|
+
stdio: "ignore",
|
|
639
|
+
detached: true
|
|
640
|
+
});
|
|
641
|
+
child.on("error", () => {});
|
|
642
|
+
child.unref();
|
|
643
|
+
return true;
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
513
647
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Post-scaffold orchestration for webapps: run root setup (docker + access + db + seed),
|
|
650
|
+
* start the dev server, open the served URL in the user's browser, then hand
|
|
651
|
+
* control to the dev server until the user exits with Ctrl+C.
|
|
652
|
+
*
|
|
653
|
+
* Returns true if the dev server got running (caller should NOT print next
|
|
654
|
+
* steps in that case — the user is already in the running app). Returns
|
|
655
|
+
* false on any failure so the caller can print manual fallback steps.
|
|
656
|
+
*
|
|
657
|
+
* Callers should only invoke this when install actually succeeded —
|
|
658
|
+
* starting setup without node_modules will leave an orphan Docker container.
|
|
659
|
+
*/
|
|
660
|
+
async function autoRunWebapp(packageDir, monorepoRoot) {
|
|
661
|
+
const packageManager = PACKAGE_MANAGER;
|
|
662
|
+
console.log();
|
|
663
|
+
console.log(chalk.bold("Running setup (docker, access, db, seed)..."));
|
|
664
|
+
console.log();
|
|
665
|
+
try {
|
|
666
|
+
await runWebappSetup(packageManager, monorepoRoot);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.log();
|
|
669
|
+
console.log(chalk.yellow("!"), "Setup failed. You can re-run it manually:", chalk.cyan(`cd ${monorepoRoot} && ${packageManager} run setup`));
|
|
670
|
+
console.log(chalk.dim(error.message));
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
console.log();
|
|
674
|
+
console.log(chalk.bold("Starting dev server..."));
|
|
675
|
+
console.log();
|
|
676
|
+
const { child, ready } = spawnDevServer(packageManager, packageDir);
|
|
677
|
+
const closed = new Promise((resolve) => {
|
|
678
|
+
child.on("close", () => resolve());
|
|
679
|
+
});
|
|
680
|
+
let url = "http://localhost:3000";
|
|
681
|
+
try {
|
|
682
|
+
({url} = await ready);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.log();
|
|
685
|
+
console.log(chalk.yellow("!"), "Dev server failed to become ready.");
|
|
686
|
+
console.log(chalk.dim(error.message));
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (openInBrowser(url)) {
|
|
690
|
+
console.log();
|
|
691
|
+
console.log(chalk.green("✔"), "Opened", chalk.cyan(url));
|
|
692
|
+
} else {
|
|
693
|
+
console.log();
|
|
694
|
+
console.log(chalk.dim("Open"), chalk.cyan(url), chalk.dim("in your browser."));
|
|
695
|
+
}
|
|
696
|
+
await closed;
|
|
697
|
+
return true;
|
|
525
698
|
}
|
|
526
699
|
async function writeMosaicFiles(packageDir, config, projectType) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
};
|
|
538
|
-
await writeManifest(packageDir, manifest);
|
|
539
|
-
const notesPath = path7.join(packageDir, "mosaic-template-notes.md");
|
|
540
|
-
await fs7.writeFile(
|
|
541
|
-
notesPath,
|
|
542
|
-
`# Mosaic Divergence Notes
|
|
543
|
-
|
|
544
|
-
Document intentional differences from the ${projectType} template here.
|
|
545
|
-
Claude reads this file during sync to preserve your customizations.
|
|
546
|
-
|
|
547
|
-
## Intentional Divergences
|
|
548
|
-
|
|
549
|
-
_None yet \u2014 freshly created from template._
|
|
550
|
-
`
|
|
551
|
-
);
|
|
700
|
+
await writeManifest(packageDir, {
|
|
701
|
+
templateType: projectType,
|
|
702
|
+
templateVersion: getTemplateVersion(projectType),
|
|
703
|
+
templateCommit: "npm",
|
|
704
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
705
|
+
placeholders: derivePlaceholders(config.name, config.title, config.repoName),
|
|
706
|
+
source: { templatePath: `packages/create-mosaic-module/templates/${projectType}` }
|
|
707
|
+
});
|
|
708
|
+
const notesPath = path.join(packageDir, "mosaic-template-notes.md");
|
|
709
|
+
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`);
|
|
552
710
|
}
|
|
553
711
|
function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
712
|
+
return {
|
|
713
|
+
name,
|
|
714
|
+
title,
|
|
715
|
+
dbName: `${toSnakeCase(name)}_db`,
|
|
716
|
+
nameUpper: name.toUpperCase(),
|
|
717
|
+
nameSnake: toSnakeCase(name),
|
|
718
|
+
repoName,
|
|
719
|
+
repoNameSnake: toSnakeCase(repoName)
|
|
720
|
+
};
|
|
563
721
|
}
|
|
722
|
+
/** Copy the monorepo template into `targetDir` and replace its placeholders. */
|
|
564
723
|
async function scaffoldMonorepo(targetDir, config) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
724
|
+
const monoSpinner = ora("Copying monorepo template...").start();
|
|
725
|
+
try {
|
|
726
|
+
await copyTemplate(targetDir, "monorepo");
|
|
727
|
+
monoSpinner.succeed("Copied monorepo template");
|
|
728
|
+
} catch (error) {
|
|
729
|
+
monoSpinner.fail("Failed to copy monorepo template");
|
|
730
|
+
console.error(error);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
const replaceSpinner = ora("Replacing monorepo placeholders...").start();
|
|
734
|
+
try {
|
|
735
|
+
const stats = await replacePlaceholders(targetDir, config);
|
|
736
|
+
replaceSpinner.succeed(`Replaced placeholders in ${stats.modified} monorepo files`);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
replaceSpinner.fail("Failed to replace monorepo placeholders");
|
|
739
|
+
console.error(error);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
585
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Add a package (webapp or library) to a monorepo: copy the template into
|
|
745
|
+
* `packageDir`, replace placeholders, write the Mosaic manifest, and run the
|
|
746
|
+
* webapp-only post-copy steps (.env.local, workflow relocation) when applicable.
|
|
747
|
+
*/
|
|
586
748
|
async function addPackageToMonorepo(args) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
|
|
613
|
-
}
|
|
749
|
+
const { packageDir, monorepoRoot, projectType, config } = args;
|
|
750
|
+
const copySpinner = ora("Copying package template...").start();
|
|
751
|
+
try {
|
|
752
|
+
await copyTemplate(packageDir, projectType);
|
|
753
|
+
copySpinner.succeed("Copied package template");
|
|
754
|
+
} catch (error) {
|
|
755
|
+
copySpinner.fail("Failed to copy package template");
|
|
756
|
+
console.error(error);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
const replaceSpinner = ora("Replacing package placeholders...").start();
|
|
760
|
+
try {
|
|
761
|
+
const stats = await replacePlaceholders(packageDir, config);
|
|
762
|
+
replaceSpinner.succeed(`Replaced placeholders in ${stats.modified} package files`);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
replaceSpinner.fail("Failed to replace package placeholders");
|
|
765
|
+
console.error(error);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
await writeMosaicFiles(packageDir, config, projectType);
|
|
769
|
+
if (projectType === "webapp") {
|
|
770
|
+
await resolvePerceptaPackageVersions(packageDir);
|
|
771
|
+
await generateEnvLocal(packageDir);
|
|
772
|
+
await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
|
|
773
|
+
}
|
|
614
774
|
}
|
|
615
775
|
async function resolvePerceptaPackageVersions(packageDir) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
} catch (error) {
|
|
630
|
-
spinner.warn(
|
|
631
|
-
"Could not resolve latest @percepta/* versions; kept template ranges"
|
|
632
|
-
);
|
|
633
|
-
console.log(chalk.dim(error.message));
|
|
634
|
-
}
|
|
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
|
+
}
|
|
635
788
|
}
|
|
789
|
+
/** Initialize a git repo at `targetDir` with an initial commit. Best-effort. */
|
|
636
790
|
function initGitRepo(targetDir) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
791
|
+
const gitSpinner = ora("Initializing git repository...").start();
|
|
792
|
+
try {
|
|
793
|
+
execSync("git init", {
|
|
794
|
+
cwd: targetDir,
|
|
795
|
+
stdio: "ignore"
|
|
796
|
+
});
|
|
797
|
+
execSync("git add -A", {
|
|
798
|
+
cwd: targetDir,
|
|
799
|
+
stdio: "ignore"
|
|
800
|
+
});
|
|
801
|
+
execSync("git commit -m \"Initial commit from @percepta/create\"", {
|
|
802
|
+
cwd: targetDir,
|
|
803
|
+
stdio: "ignore"
|
|
804
|
+
});
|
|
805
|
+
gitSpinner.succeed("Initialized git repository");
|
|
806
|
+
} catch {
|
|
807
|
+
gitSpinner.warn("Failed to initialize git repository");
|
|
808
|
+
}
|
|
649
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Run `pnpm install` at the monorepo root with a spinner. Returns true if
|
|
812
|
+
* install ran successfully; false if it failed or was skipped.
|
|
813
|
+
*/
|
|
650
814
|
async function installAtMonorepoRoot(monorepoRoot, installDeps) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
`Failed to install dependencies. Run '${PACKAGE_MANAGER} install' from monorepo root.`
|
|
662
|
-
);
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
815
|
+
if (!installDeps) return false;
|
|
816
|
+
const spinner = ora(`Installing dependencies with ${PACKAGE_MANAGER}...`).start();
|
|
817
|
+
try {
|
|
818
|
+
await runPackageManagerInstall(PACKAGE_MANAGER, monorepoRoot);
|
|
819
|
+
spinner.succeed("Installed dependencies");
|
|
820
|
+
return true;
|
|
821
|
+
} catch {
|
|
822
|
+
spinner.warn(`Failed to install dependencies. Run '${PACKAGE_MANAGER} install' from monorepo root.`);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
665
825
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
826
|
+
/**
|
|
827
|
+
* For webapp scaffolds with a successful install, hand off to autoRunWebapp
|
|
828
|
+
* (setup → dev → open browser). No-op otherwise. Returns true if the dev
|
|
829
|
+
* server actually started — caller skips manual next-steps in that case.
|
|
830
|
+
*
|
|
831
|
+
* Gated on `installSucceeded` because starting setup without node_modules
|
|
832
|
+
* leaves an orphan Docker container.
|
|
833
|
+
*/
|
|
834
|
+
async function maybeAutoRunWebapp(packageDir, monorepoRoot, projectType, installSucceeded) {
|
|
835
|
+
if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
|
|
836
|
+
return autoRunWebapp(packageDir, monorepoRoot);
|
|
669
837
|
}
|
|
670
838
|
function getProjectTypeLabel(projectType) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return "TypeScript library";
|
|
678
|
-
default: {
|
|
679
|
-
const exhaustiveCheck = projectType;
|
|
680
|
-
throw new Error(`Unknown project type: ${exhaustiveCheck}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
839
|
+
switch (projectType) {
|
|
840
|
+
case "monorepo": return "pnpm monorepo";
|
|
841
|
+
case "webapp": return "Next.js webapp";
|
|
842
|
+
case "library": return "TypeScript library";
|
|
843
|
+
default: throw new Error(`Unknown project type: ${String(projectType)}`);
|
|
844
|
+
}
|
|
683
845
|
}
|
|
684
846
|
function requireNpmTokenForWebappInstall(projectType, installDeps) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
)
|
|
699
|
-
);
|
|
700
|
-
console.error(" 2. Add to ~/.zshrc:");
|
|
701
|
-
console.error(chalk.cyan(' export NPM_TOKEN="<paste-token>"'));
|
|
702
|
-
console.error(
|
|
703
|
-
" 3. Open a new terminal (or " + chalk.cyan("source ~/.zshrc") + ") and re-run."
|
|
704
|
-
);
|
|
705
|
-
console.error();
|
|
706
|
-
console.error(
|
|
707
|
-
chalk.dim(
|
|
708
|
-
" Or pass --skip-install to scaffold without running install."
|
|
709
|
-
)
|
|
710
|
-
);
|
|
711
|
-
process.exit(1);
|
|
847
|
+
if (projectType !== "webapp" || !installDeps || process.env.NPM_TOKEN) return;
|
|
848
|
+
console.log();
|
|
849
|
+
console.error(chalk.red("Error: NPM_TOKEN environment variable is not set."));
|
|
850
|
+
console.error(chalk.dim(" Required to install private @percepta/* packages."));
|
|
851
|
+
console.error();
|
|
852
|
+
console.error(" 1. Grab the npm token from 1Password:");
|
|
853
|
+
console.error(chalk.cyan(" https://start.1password.com/open/i?a=5TX2B4O3QNE4FNQ2A7ZJZDRRBI&v=j7trpyuqh7gt635dtuj6y4pwjm&i=cmmdi5trji7ctkn3fseakf4mgi&h=aitco.1password.com"));
|
|
854
|
+
console.error(" 2. Add to ~/.zshrc:");
|
|
855
|
+
console.error(chalk.cyan(" export NPM_TOKEN=\"<paste-token>\""));
|
|
856
|
+
console.error(" 3. Open a new terminal (or " + chalk.cyan("source ~/.zshrc") + ") and re-run.");
|
|
857
|
+
console.error();
|
|
858
|
+
console.error(chalk.dim(" Or pass --skip-install to scaffold without running install."));
|
|
859
|
+
process.exit(1);
|
|
712
860
|
}
|
|
713
861
|
async function createProject(options) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (devStarted) return;
|
|
852
|
-
printNextStepsExisting(answers, packageDir);
|
|
853
|
-
} else {
|
|
854
|
-
const isBareMonorepo = answers.projectType === "monorepo";
|
|
855
|
-
const monorepoRoot = answers.directory;
|
|
856
|
-
const packageDir = isBareMonorepo ? null : path7.join(monorepoRoot, "packages", answers.name);
|
|
857
|
-
if (isBareMonorepo) {
|
|
858
|
-
console.log(chalk.dim(" Type:"), typeLabel);
|
|
859
|
-
console.log(chalk.dim(" Directory:"), monorepoRoot);
|
|
860
|
-
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
861
|
-
console.log(chalk.dim(" Title:"), monorepoConfig.title);
|
|
862
|
-
} else {
|
|
863
|
-
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
864
|
-
console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
|
|
865
|
-
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
866
|
-
console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
|
|
867
|
-
console.log(chalk.dim(" Name:"), config.name);
|
|
868
|
-
console.log(chalk.dim(" Title:"), config.title);
|
|
869
|
-
if (answers.projectType === "webapp") {
|
|
870
|
-
console.log(chalk.dim(" Database:"), config.dbName);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
console.log();
|
|
874
|
-
if (await fs7.pathExists(monorepoRoot)) {
|
|
875
|
-
const files = await fs7.readdir(monorepoRoot);
|
|
876
|
-
if (files.length > 0) {
|
|
877
|
-
console.error(
|
|
878
|
-
chalk.red(`Error: Directory ${monorepoRoot} is not empty.`)
|
|
879
|
-
);
|
|
880
|
-
process.exit(1);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
await scaffoldMonorepo(monorepoRoot, monorepoConfig);
|
|
884
|
-
if (packageDir && answers.projectType !== "monorepo") {
|
|
885
|
-
await addPackageToMonorepo({
|
|
886
|
-
packageDir,
|
|
887
|
-
monorepoRoot,
|
|
888
|
-
projectType: answers.projectType,
|
|
889
|
-
config
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
initGitRepo(monorepoRoot);
|
|
893
|
-
const installSucceeded = await installAtMonorepoRoot(
|
|
894
|
-
monorepoRoot,
|
|
895
|
-
answers.installDeps
|
|
896
|
-
);
|
|
897
|
-
console.log();
|
|
898
|
-
console.log(
|
|
899
|
-
chalk.green("\u2714"),
|
|
900
|
-
chalk.bold(isBareMonorepo ? `Created ${typeLabel} at` : "Created monorepo at"),
|
|
901
|
-
chalk.cyan(monorepoRoot)
|
|
902
|
-
);
|
|
903
|
-
if (!isBareMonorepo) {
|
|
904
|
-
console.log(
|
|
905
|
-
chalk.green("\u2714"),
|
|
906
|
-
chalk.bold(`Created ${typeLabel} at`),
|
|
907
|
-
chalk.cyan(`packages/${answers.name}/`)
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
console.log();
|
|
911
|
-
const devStarted = await maybeAutoRunWebapp(
|
|
912
|
-
packageDir,
|
|
913
|
-
answers.projectType,
|
|
914
|
-
installSucceeded
|
|
915
|
-
);
|
|
916
|
-
if (devStarted) return;
|
|
917
|
-
printNextStepsNew(answers, monorepoRoot);
|
|
918
|
-
}
|
|
862
|
+
const cwd = await resolveCreateCwd(options.cwd);
|
|
863
|
+
if (options.type !== void 0 && !isValidProjectType(options.type)) {
|
|
864
|
+
console.error(chalk.red(`Error: Invalid package type "${options.type}". Valid types are: ${VALID_PROJECT_TYPES.join(", ")}`));
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
console.log();
|
|
868
|
+
console.log(chalk.bold("Creating a new Mosaic package..."));
|
|
869
|
+
console.log();
|
|
870
|
+
const monorepoContext = await detectMonorepo(cwd);
|
|
871
|
+
if (options.type === "monorepo" && monorepoContext.found) {
|
|
872
|
+
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
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
if (monorepoContext.found) console.log(chalk.dim(" Detected monorepo at"), chalk.cyan(monorepoContext.rootDir));
|
|
876
|
+
else console.log(chalk.dim(" No monorepo detected. A new monorepo will be created."));
|
|
877
|
+
console.log();
|
|
878
|
+
const projectName = options.name;
|
|
879
|
+
const repoName = options.repoName;
|
|
880
|
+
if (options.yes && !projectName) {
|
|
881
|
+
console.error(chalk.red("Error: --name is required when using --yes flag"));
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
if (projectName) {
|
|
885
|
+
const validation = validateProjectName(toKebabCase(projectName));
|
|
886
|
+
if (!validation.valid) {
|
|
887
|
+
console.error(chalk.red(`Invalid project name: ${validation.error}`));
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (repoName) {
|
|
892
|
+
const validation = validateProjectName(toKebabCase(repoName));
|
|
893
|
+
if (!validation.valid) {
|
|
894
|
+
console.error(chalk.red(`Invalid repo name: ${validation.error}`));
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
let answers;
|
|
899
|
+
if (options.yes) {
|
|
900
|
+
const projectType = options.type || "webapp";
|
|
901
|
+
requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
|
|
902
|
+
const kebabName = toKebabCase(projectName);
|
|
903
|
+
const kebabRepoName = repoName ? toKebabCase(repoName) : kebabName;
|
|
904
|
+
answers = {
|
|
905
|
+
projectType,
|
|
906
|
+
directory: monorepoContext.found && monorepoContext.packageDir ? path.join(monorepoContext.packageDir, kebabName) : path.resolve(cwd, kebabRepoName),
|
|
907
|
+
name: kebabName,
|
|
908
|
+
title: toTitleCase(kebabName),
|
|
909
|
+
installDeps: !options.skipInstall,
|
|
910
|
+
monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
|
|
911
|
+
monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
|
|
912
|
+
};
|
|
913
|
+
} else {
|
|
914
|
+
answers = await promptProjectDetails({
|
|
915
|
+
projectType: options.type,
|
|
916
|
+
name: projectName ? toKebabCase(projectName) : void 0,
|
|
917
|
+
repoName: repoName ? toKebabCase(repoName) : void 0,
|
|
918
|
+
skipInstall: options.skipInstall,
|
|
919
|
+
monorepoContext,
|
|
920
|
+
cwd,
|
|
921
|
+
beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
|
|
922
|
+
});
|
|
923
|
+
if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) answers.directory = path.join(monorepoContext.packageDir, answers.name);
|
|
924
|
+
}
|
|
925
|
+
const monorepoName = answers.monorepoName ?? answers.name;
|
|
926
|
+
const monorepoConfig = buildAppConfig(monorepoName, answers.monorepoTitle ?? toTitleCase(monorepoName));
|
|
927
|
+
const configRepoName = monorepoContext.found ? path.basename(monorepoContext.rootDir) : monorepoName;
|
|
928
|
+
const config = buildAppConfig(answers.name, answers.title, configRepoName);
|
|
929
|
+
const typeLabel = getProjectTypeLabel(answers.projectType);
|
|
930
|
+
if (monorepoContext.found) {
|
|
931
|
+
const monorepoRoot = monorepoContext.rootDir;
|
|
932
|
+
const packageDir = monorepoContext.packageDir ? path.join(monorepoContext.packageDir, answers.name) : answers.directory;
|
|
933
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
934
|
+
console.log(chalk.dim(" Target:"), packageDir);
|
|
935
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
936
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
937
|
+
if (answers.projectType === "webapp") console.log(chalk.dim(" Database:"), config.dbName);
|
|
938
|
+
console.log();
|
|
939
|
+
if (await fs.pathExists(packageDir)) {
|
|
940
|
+
if ((await fs.readdir(packageDir)).length > 0) {
|
|
941
|
+
console.error(chalk.red(`Error: Directory ${packageDir} is not empty.`));
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (answers.projectType !== "monorepo") await addPackageToMonorepo({
|
|
946
|
+
packageDir,
|
|
947
|
+
monorepoRoot,
|
|
948
|
+
projectType: answers.projectType,
|
|
949
|
+
config
|
|
950
|
+
});
|
|
951
|
+
await warnIfMissingRootNpmrc(monorepoRoot);
|
|
952
|
+
const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
|
|
953
|
+
console.log();
|
|
954
|
+
console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(path.relative(monorepoRoot, packageDir)));
|
|
955
|
+
console.log();
|
|
956
|
+
if (await maybeAutoRunWebapp(packageDir, monorepoRoot, answers.projectType, installSucceeded)) return;
|
|
957
|
+
printNextStepsExisting(answers, packageDir);
|
|
958
|
+
} else {
|
|
959
|
+
const isBareMonorepo = answers.projectType === "monorepo";
|
|
960
|
+
const monorepoRoot = answers.directory;
|
|
961
|
+
const packageDir = isBareMonorepo ? null : path.join(monorepoRoot, "packages", answers.name);
|
|
962
|
+
if (isBareMonorepo) {
|
|
963
|
+
console.log(chalk.dim(" Type:"), typeLabel);
|
|
964
|
+
console.log(chalk.dim(" Directory:"), monorepoRoot);
|
|
965
|
+
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
966
|
+
console.log(chalk.dim(" Title:"), monorepoConfig.title);
|
|
967
|
+
} else {
|
|
968
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
969
|
+
console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
|
|
970
|
+
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
971
|
+
console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
|
|
972
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
973
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
974
|
+
if (answers.projectType === "webapp") console.log(chalk.dim(" Database:"), config.dbName);
|
|
975
|
+
}
|
|
976
|
+
console.log();
|
|
977
|
+
if (await fs.pathExists(monorepoRoot)) {
|
|
978
|
+
if ((await fs.readdir(monorepoRoot)).length > 0) {
|
|
979
|
+
console.error(chalk.red(`Error: Directory ${monorepoRoot} is not empty.`));
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
await scaffoldMonorepo(monorepoRoot, monorepoConfig);
|
|
984
|
+
if (packageDir && answers.projectType !== "monorepo") await addPackageToMonorepo({
|
|
985
|
+
packageDir,
|
|
986
|
+
monorepoRoot,
|
|
987
|
+
projectType: answers.projectType,
|
|
988
|
+
config
|
|
989
|
+
});
|
|
990
|
+
initGitRepo(monorepoRoot);
|
|
991
|
+
const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
|
|
992
|
+
console.log();
|
|
993
|
+
console.log(chalk.green("✔"), chalk.bold(isBareMonorepo ? `Created ${typeLabel} at` : "Created monorepo at"), chalk.cyan(monorepoRoot));
|
|
994
|
+
if (!isBareMonorepo) console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(`packages/${answers.name}/`));
|
|
995
|
+
console.log();
|
|
996
|
+
if (await maybeAutoRunWebapp(packageDir, monorepoRoot, answers.projectType, installSucceeded)) return;
|
|
997
|
+
printNextStepsNew(answers, monorepoRoot);
|
|
998
|
+
}
|
|
919
999
|
}
|
|
920
1000
|
async function resolveCreateCwd(cwdOption) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1001
|
+
const cwd = cwdOption ? path.resolve(cwdOption) : process.cwd();
|
|
1002
|
+
let stat;
|
|
1003
|
+
try {
|
|
1004
|
+
stat = await fs.stat(cwd);
|
|
1005
|
+
} catch {
|
|
1006
|
+
console.error(chalk.red(`Error: --cwd directory does not exist: ${cwd}`));
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
if (!stat.isDirectory()) {
|
|
1010
|
+
console.error(chalk.red(`Error: --cwd is not a directory: ${cwd}`));
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
return cwd;
|
|
934
1014
|
}
|
|
935
1015
|
function printWebappNextSteps(params) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
oneLinerParts.push(...pnpmSteps);
|
|
980
|
-
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
981
|
-
console.log();
|
|
982
|
-
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
983
|
-
console.log();
|
|
984
|
-
console.log(chalk.bold("Or step by step:"));
|
|
985
|
-
console.log();
|
|
986
|
-
let step = 1;
|
|
987
|
-
if (!answers.installDeps) {
|
|
988
|
-
if (repoRel !== ".") {
|
|
989
|
-
console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
990
|
-
}
|
|
991
|
-
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
992
|
-
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
993
|
-
} else if (pkgRel !== ".") {
|
|
994
|
-
console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
995
|
-
}
|
|
996
|
-
for (const cmd of pnpmSteps) {
|
|
997
|
-
console.log(chalk.dim(` ${step++}.`), cmd);
|
|
998
|
-
}
|
|
1016
|
+
const { pm, answers, variant, monorepoRelativePath, packageRelativePathFromRoot } = params;
|
|
1017
|
+
const repoRel = shPath(monorepoRelativePath) || ".";
|
|
1018
|
+
const pkgFromRoot = shPath(packageRelativePathFromRoot ?? `packages/${answers.name}`) || ".";
|
|
1019
|
+
const setupStep = "pnpm run setup";
|
|
1020
|
+
const devStep = "pnpm dev";
|
|
1021
|
+
if (variant === "new") {
|
|
1022
|
+
const oneLinerParts = [];
|
|
1023
|
+
if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
|
|
1024
|
+
if (!answers.installDeps) oneLinerParts.push(`${pm} install`);
|
|
1025
|
+
oneLinerParts.push(setupStep);
|
|
1026
|
+
oneLinerParts.push(`cd ${pkgFromRoot}`);
|
|
1027
|
+
oneLinerParts.push(devStep);
|
|
1028
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
1029
|
+
console.log();
|
|
1030
|
+
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
1031
|
+
console.log();
|
|
1032
|
+
console.log(chalk.bold("Or step by step:"));
|
|
1033
|
+
console.log();
|
|
1034
|
+
let step = 1;
|
|
1035
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1036
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1037
|
+
console.log(chalk.dim(` ${step++}.`), setupStep);
|
|
1038
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
1039
|
+
console.log(chalk.dim(` ${step++}.`), devStep);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const oneLinerParts = [];
|
|
1043
|
+
if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
|
|
1044
|
+
if (!answers.installDeps) oneLinerParts.push(`${pm} install`);
|
|
1045
|
+
oneLinerParts.push(setupStep, `cd ${pkgFromRoot}`, devStep);
|
|
1046
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
1047
|
+
console.log();
|
|
1048
|
+
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
1049
|
+
console.log();
|
|
1050
|
+
console.log(chalk.bold("Or step by step:"));
|
|
1051
|
+
console.log();
|
|
1052
|
+
let step = 1;
|
|
1053
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1054
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1055
|
+
console.log(chalk.dim(` ${step++}.`), setupStep);
|
|
1056
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
1057
|
+
console.log(chalk.dim(` ${step++}.`), devStep);
|
|
999
1058
|
}
|
|
1000
1059
|
async function warnIfMissingRootNpmrc(rootDir) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
);
|
|
1014
|
-
console.log(
|
|
1015
|
-
chalk.dim(
|
|
1016
|
-
" pnpm reads .npmrc from the workspace root, so add these lines to"
|
|
1017
|
-
)
|
|
1018
|
-
);
|
|
1019
|
-
console.log(chalk.dim(` ${path7.join(rootDir, ".npmrc")}:`));
|
|
1020
|
-
console.log();
|
|
1021
|
-
console.log(
|
|
1022
|
-
chalk.cyan(" @percepta:registry=https://registry.npmjs.org/")
|
|
1023
|
-
);
|
|
1024
|
-
console.log(
|
|
1025
|
-
chalk.cyan(" //registry.npmjs.org/:_authToken=${NPM_TOKEN}")
|
|
1026
|
-
);
|
|
1027
|
-
console.log();
|
|
1060
|
+
const rootNpmrc = path.join(rootDir, ".npmrc");
|
|
1061
|
+
let contents = "";
|
|
1062
|
+
if (await fs.pathExists(rootNpmrc)) contents = await fs.readFile(rootNpmrc, "utf8");
|
|
1063
|
+
if (contents.includes("@percepta:registry")) return;
|
|
1064
|
+
console.log();
|
|
1065
|
+
console.log(chalk.yellow("!"), chalk.bold("Root .npmrc is missing @percepta registry config."));
|
|
1066
|
+
console.log(chalk.dim(" pnpm reads .npmrc from the workspace root, so add these lines to"));
|
|
1067
|
+
console.log(chalk.dim(` ${path.join(rootDir, ".npmrc")}:`));
|
|
1068
|
+
console.log();
|
|
1069
|
+
console.log(chalk.cyan(" @percepta:registry=https://registry.npmjs.org/"));
|
|
1070
|
+
console.log(chalk.cyan(" //registry.npmjs.org/:_authToken=${NPM_TOKEN}"));
|
|
1071
|
+
console.log();
|
|
1028
1072
|
}
|
|
1029
1073
|
function printNextStepsNew(answers, targetDir) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1066
|
-
}
|
|
1067
|
-
console.log(
|
|
1068
|
-
chalk.dim(` ${step++}.`),
|
|
1069
|
-
`cd packages/${answers.name}`
|
|
1070
|
-
);
|
|
1071
|
-
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
1072
|
-
console.log(
|
|
1073
|
-
chalk.dim(` ${step++}.`),
|
|
1074
|
-
`Edit src/index.ts to add your library code`
|
|
1075
|
-
);
|
|
1076
|
-
break;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
console.log();
|
|
1080
|
-
console.log(
|
|
1081
|
-
chalk.dim("For more information, see the README.md in your project.")
|
|
1082
|
-
);
|
|
1083
|
-
console.log();
|
|
1074
|
+
const pm = PACKAGE_MANAGER;
|
|
1075
|
+
const relativePath = path.relative(process.cwd(), targetDir) || ".";
|
|
1076
|
+
console.log("Next steps:");
|
|
1077
|
+
console.log();
|
|
1078
|
+
switch (answers.projectType) {
|
|
1079
|
+
case "monorepo": {
|
|
1080
|
+
let step = 1;
|
|
1081
|
+
const repoRel = shPath(relativePath);
|
|
1082
|
+
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>`)}`);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
case "webapp":
|
|
1088
|
+
printWebappNextSteps({
|
|
1089
|
+
pm,
|
|
1090
|
+
answers,
|
|
1091
|
+
variant: "new",
|
|
1092
|
+
monorepoRelativePath: relativePath
|
|
1093
|
+
});
|
|
1094
|
+
break;
|
|
1095
|
+
case "library": {
|
|
1096
|
+
let step = 1;
|
|
1097
|
+
const repoRel = shPath(relativePath);
|
|
1098
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1099
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1100
|
+
console.log(chalk.dim(` ${step++}.`), `cd packages/${answers.name}`);
|
|
1101
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
1102
|
+
console.log(chalk.dim(` ${step++}.`), `Edit src/index.ts to add your library code`);
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
console.log();
|
|
1107
|
+
console.log(chalk.dim("For more information, see the README.md in your project."));
|
|
1108
|
+
console.log();
|
|
1084
1109
|
}
|
|
1085
1110
|
function printNextStepsExisting(answers, packageDir) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
console.log();
|
|
1117
|
-
console.log(
|
|
1118
|
-
chalk.dim("For more information, see the README.md in your project.")
|
|
1119
|
-
);
|
|
1120
|
-
console.log();
|
|
1111
|
+
const pm = PACKAGE_MANAGER;
|
|
1112
|
+
const packageRelativePath = path.relative(process.cwd(), packageDir) || ".";
|
|
1113
|
+
const monorepoRoot = path.dirname(path.dirname(packageDir));
|
|
1114
|
+
const monorepoRelativePath = path.relative(process.cwd(), monorepoRoot) || ".";
|
|
1115
|
+
const packageRelativePathFromRoot = path.relative(monorepoRoot, packageDir) || ".";
|
|
1116
|
+
console.log("Next steps:");
|
|
1117
|
+
console.log();
|
|
1118
|
+
switch (answers.projectType) {
|
|
1119
|
+
case "webapp":
|
|
1120
|
+
printWebappNextSteps({
|
|
1121
|
+
pm,
|
|
1122
|
+
answers,
|
|
1123
|
+
variant: "existing",
|
|
1124
|
+
monorepoRelativePath,
|
|
1125
|
+
packageRelativePathFromRoot
|
|
1126
|
+
});
|
|
1127
|
+
break;
|
|
1128
|
+
case "library": {
|
|
1129
|
+
let step = 1;
|
|
1130
|
+
const pkgRel = shPath(packageRelativePath);
|
|
1131
|
+
if (pkgRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
1132
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
1133
|
+
console.log(chalk.dim(` ${step++}.`), "Edit src/index.ts to add your library code");
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
console.log();
|
|
1138
|
+
console.log(chalk.dim("For more information, see the README.md in your project."));
|
|
1139
|
+
console.log();
|
|
1121
1140
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
};
|
|
1128
|
-
program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#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);
|
|
1129
1147
|
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);
|
|
1130
|
-
program.command("status").description("Show template sync status for current app").option(
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
).action(async (options) => {
|
|
1134
|
-
const { statusCommand } = await import("./status-QW5TQDYY.js");
|
|
1135
|
-
await statusCommand(options);
|
|
1148
|
+
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
|
+
const { statusCommand } = await import("./status-CKe4aKso.js");
|
|
1150
|
+
await statusCommand(options);
|
|
1136
1151
|
});
|
|
1137
|
-
program.command("sync").description("Generate downstream sync context (template
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
|
|
1141
|
-
const { syncCommand } = await import("./sync-RLBZDOFB.js");
|
|
1142
|
-
await syncCommand(options);
|
|
1152
|
+
program.command("sync").description("Generate downstream sync context (template → app)").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").option("--to <version>", "Target template version (default: latest)").action(async (options) => {
|
|
1153
|
+
const { syncCommand } = await import("./sync-D1vkoofl.js");
|
|
1154
|
+
await syncCommand(options);
|
|
1143
1155
|
});
|
|
1144
|
-
program.command("upstream").description("Generate upstream context (app
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
|
|
1148
|
-
const { upstreamCommand } = await import("./upstream-TQFVPMEG.js");
|
|
1149
|
-
await upstreamCommand(options);
|
|
1156
|
+
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");
|
|
1158
|
+
await upstreamCommand(options);
|
|
1150
1159
|
});
|
|
1151
1160
|
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) => {
|
|
1152
|
-
|
|
1153
|
-
|
|
1161
|
+
const { initCommand } = await import("./init-CtCp7Tv2.js");
|
|
1162
|
+
await initCommand(options);
|
|
1154
1163
|
});
|
|
1155
1164
|
program.parse();
|
|
1165
|
+
//#endregion
|
|
1166
|
+
export { manifestExists as a, writeManifest as c, derivePlaceholders as i, VALID_PROJECT_TYPES as n, readManifest as o, isValidProjectType as r, resolveMosaicTemplatePath as s, getTemplateVersion as t };
|
|
1167
|
+
|
|
1168
|
+
//# sourceMappingURL=index.js.map
|