@percepta/create 3.1.3 → 3.1.5
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 +1073 -1067
- package/dist/index.js.map +1 -0
- package/dist/init-OeK4Yk6_.js +52 -0
- package/dist/init-OeK4Yk6_.js.map +1 -0
- package/dist/status-DC8mvHZj.js +48 -0
- package/dist/status-DC8mvHZj.js.map +1 -0
- package/dist/sync-C5Pd32VM.js +101 -0
- package/dist/sync-C5Pd32VM.js.map +1 -0
- package/dist/upstream-F6m8zRBQ.js +85 -0
- package/dist/upstream-F6m8zRBQ.js.map +1 -0
- package/package.json +23 -24
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/README.md +1 -1
- package/templates/webapp/agent-skills/database.md +5 -1
- package/templates/webapp/agent-skills/deploy.md +5 -3
- package/templates/webapp/agent-skills/inngest.md +13 -8
- package/templates/webapp/agent-skills/oneshot.md +1 -1
- package/templates/webapp/deploy/README.md +2 -2
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +3 -3
- package/templates/webapp/package.json.template +2 -2
- package/templates/webapp/scripts/deploy-percepta-test.ts +311 -36
- package/templates/webapp/scripts/generate-migrations.ts +28 -0
- package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +24 -0
- package/templates/webapp/src/drizzle/migrationSql.ts +8 -0
- package/templates/webapp/src/services/inngest/AppWorkflowService.ts +19 -0
- package/templates/webapp/src/services/inngest/__tests__/AppWorkflowService.test.ts +19 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +7 -13
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +1 -3
- 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/dist/index.js
CHANGED
|
@@ -1,1155 +1,1161 @@
|
|
|
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
|
|
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";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
23
6
|
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
7
|
import fs from "fs-extra";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
8
|
+
import ora from "ora";
|
|
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
|
-
__DB_NAME__: "dbName",
|
|
100
|
-
__APP_NAME_UPPER__: "nameUpper",
|
|
101
|
-
__APP_NAME_SNAKE__: "nameSnake",
|
|
102
|
-
__REPO_NAME__: "repoName",
|
|
103
|
-
__REPO_NAME_SNAKE__: "repoNameSnake"
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/utils/detect-monorepo.ts
|
|
82
|
+
const NOT_FOUND = {
|
|
83
|
+
found: false,
|
|
84
|
+
rootDir: null,
|
|
85
|
+
workspacePatterns: [],
|
|
86
|
+
packageDir: null
|
|
104
87
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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"
|
|
356
|
+
};
|
|
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
|
+
".mjs",
|
|
385
|
+
".cjs"
|
|
134
386
|
]);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
387
|
+
const PROCESSABLE_FILENAMES = new Set([
|
|
388
|
+
"Dockerfile",
|
|
389
|
+
".env.example",
|
|
390
|
+
".env.local",
|
|
391
|
+
"terraform.tfvars.example"
|
|
140
392
|
]);
|
|
141
393
|
function shouldProcessFile(filePath) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
394
|
+
const fileName = path.basename(filePath);
|
|
395
|
+
const ext = path.extname(filePath);
|
|
396
|
+
if (SKIP_FILES.has(fileName)) return false;
|
|
397
|
+
if (PROCESSABLE_FILENAMES.has(fileName)) return true;
|
|
398
|
+
return PROCESSABLE_EXTENSIONS.has(ext);
|
|
147
399
|
}
|
|
148
400
|
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;
|
|
401
|
+
let content;
|
|
402
|
+
try {
|
|
403
|
+
content = await fs.readFile(filePath, "utf-8");
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
let modified = false;
|
|
408
|
+
let newContent = content;
|
|
409
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort((a, b) => b[0].length - a[0].length);
|
|
410
|
+
for (const [placeholder, configKey] of sortedEntries) {
|
|
411
|
+
const value = config[configKey];
|
|
412
|
+
if (newContent.includes(placeholder)) {
|
|
413
|
+
newContent = newContent.split(placeholder).join(value);
|
|
414
|
+
modified = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (modified) {
|
|
418
|
+
await fs.writeFile(filePath, newContent);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
172
422
|
}
|
|
173
423
|
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;
|
|
424
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort((a, b) => b[0].length - a[0].length);
|
|
425
|
+
let result = name;
|
|
426
|
+
for (const [placeholder, configKey] of sortedEntries) if (result.includes(placeholder)) result = result.split(placeholder).join(config[configKey]);
|
|
427
|
+
return result;
|
|
186
428
|
}
|
|
187
429
|
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
|
-
}
|
|
430
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
433
|
+
if (entry.isDirectory()) {
|
|
434
|
+
if (!SKIP_DIRS.has(entry.name)) await processDirectory(fullPath, config, stats);
|
|
435
|
+
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
|
436
|
+
stats.processed++;
|
|
437
|
+
if (await replaceInFile(fullPath, config)) stats.modified++;
|
|
438
|
+
const renamed = substituteName(entry.name, config);
|
|
439
|
+
if (renamed !== entry.name) await fs.move(fullPath, path.join(dirPath, renamed), { overwrite: true });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
208
442
|
}
|
|
209
443
|
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
|
-
}
|
|
444
|
+
const stats = {
|
|
445
|
+
processed: 0,
|
|
446
|
+
modified: 0
|
|
447
|
+
};
|
|
448
|
+
await processDirectory(targetDir, config, stats);
|
|
449
|
+
return stats;
|
|
316
450
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"dependencies",
|
|
326
|
-
"devDependencies",
|
|
327
|
-
"optionalDependencies",
|
|
328
|
-
"peerDependencies"
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/utils/resolve-percepta-versions.ts
|
|
453
|
+
const execFileAsync = promisify(execFile);
|
|
454
|
+
const DEPENDENCY_SECTIONS = [
|
|
455
|
+
"dependencies",
|
|
456
|
+
"devDependencies",
|
|
457
|
+
"optionalDependencies",
|
|
458
|
+
"peerDependencies"
|
|
329
459
|
];
|
|
330
460
|
function getPerceptaPackages(pkg) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return [...names].sort();
|
|
461
|
+
const names = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
463
|
+
const deps = pkg[section];
|
|
464
|
+
if (!deps) continue;
|
|
465
|
+
for (const name of Object.keys(deps)) if (name.startsWith("@percepta/")) names.add(name);
|
|
466
|
+
}
|
|
467
|
+
return [...names].sort();
|
|
342
468
|
}
|
|
343
469
|
async function npmViewDistTagLatest(packageName, cwd) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
470
|
+
const { stdout } = await execFileAsync("npm", [
|
|
471
|
+
"view",
|
|
472
|
+
packageName,
|
|
473
|
+
"dist-tags.latest",
|
|
474
|
+
"--silent"
|
|
475
|
+
], {
|
|
476
|
+
cwd,
|
|
477
|
+
encoding: "utf8",
|
|
478
|
+
timeout: 5e3
|
|
479
|
+
});
|
|
480
|
+
const version = stdout.trim();
|
|
481
|
+
return version.length > 0 ? version : null;
|
|
355
482
|
}
|
|
356
483
|
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
|
-
|
|
484
|
+
const cwd = path.dirname(packageJsonPath);
|
|
485
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
486
|
+
const packageNames = getPerceptaPackages(pkg);
|
|
487
|
+
const resolved = {};
|
|
488
|
+
const failed = [];
|
|
489
|
+
const results = await Promise.all(packageNames.map(async (packageName) => {
|
|
490
|
+
try {
|
|
491
|
+
return {
|
|
492
|
+
packageName,
|
|
493
|
+
latest: await lookupLatest(packageName, cwd)
|
|
494
|
+
};
|
|
495
|
+
} catch {
|
|
496
|
+
return {
|
|
497
|
+
packageName,
|
|
498
|
+
latest: null
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}));
|
|
502
|
+
for (const { packageName, latest } of results) {
|
|
503
|
+
if (!latest) {
|
|
504
|
+
failed.push(packageName);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
resolved[packageName] = latest;
|
|
508
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
509
|
+
const deps = pkg[section];
|
|
510
|
+
if (deps?.[packageName]) deps[packageName] = latest;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (Object.keys(resolved).length > 0) {
|
|
514
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
515
|
+
await fs.appendFile(packageJsonPath, "\n");
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
resolved,
|
|
519
|
+
failed
|
|
520
|
+
};
|
|
392
521
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/commands/create.ts
|
|
524
|
+
const PACKAGE_MANAGER = "pnpm";
|
|
525
|
+
/** Paths in copy-paste shell commands (POSIX-style). */
|
|
396
526
|
function shPath(p) {
|
|
397
|
-
|
|
527
|
+
return p.split(path.sep).join("/");
|
|
398
528
|
}
|
|
529
|
+
/** Non-blocking install so ora can animate (execSync would block timers). */
|
|
399
530
|
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
|
-
});
|
|
531
|
+
return new Promise((resolve, reject) => {
|
|
532
|
+
const child = spawn(packageManager, args, {
|
|
533
|
+
cwd,
|
|
534
|
+
stdio: "ignore"
|
|
535
|
+
});
|
|
536
|
+
child.on("error", reject);
|
|
537
|
+
child.on("close", (code) => {
|
|
538
|
+
if (code === 0) resolve();
|
|
539
|
+
else reject(/* @__PURE__ */ new Error(`${packageManager} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
|
|
540
|
+
});
|
|
541
|
+
});
|
|
416
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Runs the `setup` script in the package directory (docker:up + db:setup-and-migrate + db:seed).
|
|
545
|
+
* Uses `pnpm run setup` (not `pnpm setup`) because `pnpm setup` is a pnpm builtin that configures
|
|
546
|
+
* PNPM_HOME in the user's shell rc — it ignores the package.json script of the same name.
|
|
547
|
+
*/
|
|
417
548
|
function runWebappSetup(packageManager, cwd) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
549
|
+
return new Promise((resolve, reject) => {
|
|
550
|
+
const child = spawn(packageManager, ["run", "setup"], {
|
|
551
|
+
cwd,
|
|
552
|
+
stdio: "inherit"
|
|
553
|
+
});
|
|
554
|
+
child.on("error", reject);
|
|
555
|
+
child.on("close", (code) => {
|
|
556
|
+
if (code === 0) resolve();
|
|
557
|
+
else reject(/* @__PURE__ */ new Error(`${packageManager} run setup exited with code ${code ?? "unknown"}`));
|
|
558
|
+
});
|
|
559
|
+
});
|
|
426
560
|
}
|
|
427
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Spawns `pnpm dev` in the package directory and resolves once Next reports
|
|
563
|
+
* "Ready in" (so we know the server is accepting requests). Returns the child
|
|
564
|
+
* process so the caller can await its exit when the user hits Ctrl+C.
|
|
565
|
+
*
|
|
566
|
+
* Captures the actual URL Next picked (Next falls back to 3001+ if 3000 is
|
|
567
|
+
* taken) so the caller can open the right one in the browser.
|
|
568
|
+
*/
|
|
569
|
+
const ANSI_PATTERN = /\u001b\[[0-9;]*[a-zA-Z]/g;
|
|
428
570
|
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
|
-
|
|
571
|
+
const child = spawn(packageManager, ["run", "dev"], {
|
|
572
|
+
cwd,
|
|
573
|
+
stdio: [
|
|
574
|
+
"inherit",
|
|
575
|
+
"pipe",
|
|
576
|
+
"pipe"
|
|
577
|
+
]
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
child,
|
|
581
|
+
ready: new Promise((resolve, reject) => {
|
|
582
|
+
let resolved = false;
|
|
583
|
+
let detectedUrl = "http://localhost:3000";
|
|
584
|
+
let buffer = "";
|
|
585
|
+
const onChunk = (chunk) => {
|
|
586
|
+
const text = chunk.toString();
|
|
587
|
+
process.stdout.write(text);
|
|
588
|
+
buffer = (buffer + text).slice(-4096).replace(ANSI_PATTERN, "");
|
|
589
|
+
const urlMatch = buffer.match(/Local:\s+(https?:\/\/\S+?)(?:\s|$)/i);
|
|
590
|
+
if (urlMatch?.[1]) detectedUrl = urlMatch[1].trim();
|
|
591
|
+
if (!resolved && /Ready in /i.test(buffer)) {
|
|
592
|
+
resolved = true;
|
|
593
|
+
resolve({ url: detectedUrl });
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
child.stdout?.on("data", onChunk);
|
|
597
|
+
child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
598
|
+
child.on("error", reject);
|
|
599
|
+
child.on("close", (code) => {
|
|
600
|
+
if (!resolved) reject(/* @__PURE__ */ new Error(`${packageManager} run dev exited with code ${code ?? "unknown"} before becoming ready`));
|
|
601
|
+
});
|
|
602
|
+
})
|
|
603
|
+
};
|
|
458
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Cross-platform "open this URL in the user's default browser". Returns true
|
|
607
|
+
* if the launcher process spawned, false on synchronous failure. The empty
|
|
608
|
+
* `error` listener swallows the asynchronous error event that fires when the
|
|
609
|
+
* launcher binary is missing (e.g. minimal Linux without `xdg-open`) — without
|
|
610
|
+
* it, that event becomes an uncaught exception that kills the CLI mid-run.
|
|
611
|
+
*/
|
|
459
612
|
function openInBrowser(url) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
613
|
+
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? [
|
|
614
|
+
"cmd",
|
|
615
|
+
"/c",
|
|
616
|
+
"start",
|
|
617
|
+
"",
|
|
618
|
+
url
|
|
619
|
+
] : ["xdg-open", url];
|
|
620
|
+
try {
|
|
621
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
622
|
+
stdio: "ignore",
|
|
623
|
+
detached: true
|
|
624
|
+
});
|
|
625
|
+
child.on("error", () => {});
|
|
626
|
+
child.unref();
|
|
627
|
+
return true;
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
470
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Post-scaffold orchestration for webapps: run setup (docker + db + seed),
|
|
634
|
+
* start the dev server, open the served URL in the user's browser, then hand
|
|
635
|
+
* control to the dev server until the user exits with Ctrl+C.
|
|
636
|
+
*
|
|
637
|
+
* Returns true if the dev server got running (caller should NOT print next
|
|
638
|
+
* steps in that case — the user is already in the running app). Returns
|
|
639
|
+
* false on any failure so the caller can print manual fallback steps.
|
|
640
|
+
*
|
|
641
|
+
* Callers should only invoke this when install actually succeeded —
|
|
642
|
+
* starting setup without node_modules will leave an orphan Docker container.
|
|
643
|
+
*/
|
|
471
644
|
async function autoRunWebapp(packageDir) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
console.log(chalk.dim("Open"), chalk.cyan(url), chalk.dim("in your browser."));
|
|
510
|
-
}
|
|
511
|
-
await closed;
|
|
512
|
-
return true;
|
|
645
|
+
const packageManager = PACKAGE_MANAGER;
|
|
646
|
+
console.log();
|
|
647
|
+
console.log(chalk.bold("Running setup (docker, db, seed)..."));
|
|
648
|
+
console.log();
|
|
649
|
+
try {
|
|
650
|
+
await runWebappSetup(packageManager, packageDir);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.log();
|
|
653
|
+
console.log(chalk.yellow("!"), "Setup failed. You can re-run it manually:", chalk.cyan(`cd ${packageDir} && ${packageManager} run setup`));
|
|
654
|
+
console.log(chalk.dim(error.message));
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
console.log();
|
|
658
|
+
console.log(chalk.bold("Starting dev server..."));
|
|
659
|
+
console.log();
|
|
660
|
+
const { child, ready } = spawnDevServer(packageManager, packageDir);
|
|
661
|
+
const closed = new Promise((resolve) => {
|
|
662
|
+
child.on("close", () => resolve());
|
|
663
|
+
});
|
|
664
|
+
let url = "http://localhost:3000";
|
|
665
|
+
try {
|
|
666
|
+
({url} = await ready);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.log();
|
|
669
|
+
console.log(chalk.yellow("!"), "Dev server failed to become ready.");
|
|
670
|
+
console.log(chalk.dim(error.message));
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (openInBrowser(url)) {
|
|
674
|
+
console.log();
|
|
675
|
+
console.log(chalk.green("✔"), "Opened", chalk.cyan(url));
|
|
676
|
+
} else {
|
|
677
|
+
console.log();
|
|
678
|
+
console.log(chalk.dim("Open"), chalk.cyan(url), chalk.dim("in your browser."));
|
|
679
|
+
}
|
|
680
|
+
await closed;
|
|
681
|
+
return true;
|
|
513
682
|
}
|
|
514
683
|
function readTemplateVersions() {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
} catch {
|
|
523
|
-
return {};
|
|
524
|
-
}
|
|
684
|
+
const versionsPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../template-versions.json");
|
|
685
|
+
try {
|
|
686
|
+
const content = fs.readFileSync(versionsPath, "utf-8");
|
|
687
|
+
return JSON.parse(content);
|
|
688
|
+
} catch {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
525
691
|
}
|
|
526
692
|
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
|
-
);
|
|
693
|
+
await writeManifest(packageDir, {
|
|
694
|
+
templateType: projectType,
|
|
695
|
+
templateVersion: readTemplateVersions()[projectType] || "1.0.0",
|
|
696
|
+
templateCommit: "npm",
|
|
697
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
698
|
+
placeholders: derivePlaceholders(config.name, config.title, config.repoName),
|
|
699
|
+
source: { templatePath: `packages/create-mosaic-module/templates/${projectType}` }
|
|
700
|
+
});
|
|
701
|
+
const notesPath = path.join(packageDir, "mosaic-template-notes.md");
|
|
702
|
+
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
703
|
}
|
|
553
704
|
function buildAppConfig(name, title = toTitleCase(name), repoName = name) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
705
|
+
return {
|
|
706
|
+
name,
|
|
707
|
+
title,
|
|
708
|
+
dbName: `${toSnakeCase(name)}_db`,
|
|
709
|
+
nameUpper: name.toUpperCase(),
|
|
710
|
+
nameSnake: toSnakeCase(name),
|
|
711
|
+
repoName,
|
|
712
|
+
repoNameSnake: toSnakeCase(repoName)
|
|
713
|
+
};
|
|
563
714
|
}
|
|
715
|
+
/** Copy the monorepo template into `targetDir` and replace its placeholders. */
|
|
564
716
|
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
|
-
}
|
|
717
|
+
const monoSpinner = ora("Copying monorepo template...").start();
|
|
718
|
+
try {
|
|
719
|
+
await copyTemplate(targetDir, "monorepo");
|
|
720
|
+
monoSpinner.succeed("Copied monorepo template");
|
|
721
|
+
} catch (error) {
|
|
722
|
+
monoSpinner.fail("Failed to copy monorepo template");
|
|
723
|
+
console.error(error);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
const replaceSpinner = ora("Replacing monorepo placeholders...").start();
|
|
727
|
+
try {
|
|
728
|
+
const stats = await replacePlaceholders(targetDir, config);
|
|
729
|
+
replaceSpinner.succeed(`Replaced placeholders in ${stats.modified} monorepo files`);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
replaceSpinner.fail("Failed to replace monorepo placeholders");
|
|
732
|
+
console.error(error);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
585
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Add a package (webapp or library) to a monorepo: copy the template into
|
|
738
|
+
* `packageDir`, replace placeholders, write the Mosaic manifest, and run the
|
|
739
|
+
* webapp-only post-copy steps (.env.local, workflow relocation) when applicable.
|
|
740
|
+
*/
|
|
586
741
|
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
|
-
}
|
|
742
|
+
const { packageDir, monorepoRoot, projectType, config } = args;
|
|
743
|
+
const copySpinner = ora("Copying package template...").start();
|
|
744
|
+
try {
|
|
745
|
+
await copyTemplate(packageDir, projectType);
|
|
746
|
+
copySpinner.succeed("Copied package template");
|
|
747
|
+
} catch (error) {
|
|
748
|
+
copySpinner.fail("Failed to copy package template");
|
|
749
|
+
console.error(error);
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
const replaceSpinner = ora("Replacing package placeholders...").start();
|
|
753
|
+
try {
|
|
754
|
+
const stats = await replacePlaceholders(packageDir, config);
|
|
755
|
+
replaceSpinner.succeed(`Replaced placeholders in ${stats.modified} package files`);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
replaceSpinner.fail("Failed to replace package placeholders");
|
|
758
|
+
console.error(error);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
await writeMosaicFiles(packageDir, config, projectType);
|
|
762
|
+
if (projectType === "webapp") {
|
|
763
|
+
await resolvePerceptaPackageVersions(packageDir);
|
|
764
|
+
await generateEnvLocal(packageDir);
|
|
765
|
+
await relocateWorkflowsToRoot(packageDir, monorepoRoot, config.name);
|
|
766
|
+
}
|
|
614
767
|
}
|
|
615
768
|
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
|
-
}
|
|
769
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
770
|
+
if (!await fs.pathExists(packageJsonPath)) return;
|
|
771
|
+
const spinner = ora("Resolving latest @percepta/* versions...").start();
|
|
772
|
+
try {
|
|
773
|
+
const result = await resolvePerceptaVersionsInPackageJson(packageJsonPath);
|
|
774
|
+
const count = Object.keys(result.resolved).length;
|
|
775
|
+
if (result.failed.length > 0) spinner.warn(`Resolved ${count} @percepta/* versions; kept existing ranges for ${result.failed.join(", ")}`);
|
|
776
|
+
else spinner.succeed(`Resolved ${count} @percepta/* versions`);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
spinner.warn("Could not resolve latest @percepta/* versions; kept template ranges");
|
|
779
|
+
console.log(chalk.dim(error.message));
|
|
780
|
+
}
|
|
635
781
|
}
|
|
782
|
+
/** Initialize a git repo at `targetDir` with an initial commit. Best-effort. */
|
|
636
783
|
function initGitRepo(targetDir) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
784
|
+
const gitSpinner = ora("Initializing git repository...").start();
|
|
785
|
+
try {
|
|
786
|
+
execSync("git init", {
|
|
787
|
+
cwd: targetDir,
|
|
788
|
+
stdio: "ignore"
|
|
789
|
+
});
|
|
790
|
+
execSync("git add -A", {
|
|
791
|
+
cwd: targetDir,
|
|
792
|
+
stdio: "ignore"
|
|
793
|
+
});
|
|
794
|
+
execSync("git commit -m \"Initial commit from @percepta/create\"", {
|
|
795
|
+
cwd: targetDir,
|
|
796
|
+
stdio: "ignore"
|
|
797
|
+
});
|
|
798
|
+
gitSpinner.succeed("Initialized git repository");
|
|
799
|
+
} catch {
|
|
800
|
+
gitSpinner.warn("Failed to initialize git repository");
|
|
801
|
+
}
|
|
649
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Run `pnpm install` at the monorepo root with a spinner. Returns true if
|
|
805
|
+
* install ran successfully; false if it failed or was skipped.
|
|
806
|
+
*/
|
|
650
807
|
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
|
-
}
|
|
808
|
+
if (!installDeps) return false;
|
|
809
|
+
const spinner = ora(`Installing dependencies with ${PACKAGE_MANAGER}...`).start();
|
|
810
|
+
try {
|
|
811
|
+
await runPackageManagerInstall(PACKAGE_MANAGER, monorepoRoot);
|
|
812
|
+
spinner.succeed("Installed dependencies");
|
|
813
|
+
return true;
|
|
814
|
+
} catch {
|
|
815
|
+
spinner.warn(`Failed to install dependencies. Run '${PACKAGE_MANAGER} install' from monorepo root.`);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
665
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* For webapp scaffolds with a successful install, hand off to autoRunWebapp
|
|
821
|
+
* (setup → dev → open browser). No-op otherwise. Returns true if the dev
|
|
822
|
+
* server actually started — caller skips manual next-steps in that case.
|
|
823
|
+
*
|
|
824
|
+
* Gated on `installSucceeded` because starting setup without node_modules
|
|
825
|
+
* leaves an orphan Docker container.
|
|
826
|
+
*/
|
|
666
827
|
async function maybeAutoRunWebapp(packageDir, projectType, installSucceeded) {
|
|
667
|
-
|
|
668
|
-
|
|
828
|
+
if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
|
|
829
|
+
return autoRunWebapp(packageDir);
|
|
669
830
|
}
|
|
670
831
|
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
|
-
}
|
|
832
|
+
switch (projectType) {
|
|
833
|
+
case "monorepo": return "pnpm monorepo";
|
|
834
|
+
case "webapp": return "Next.js webapp";
|
|
835
|
+
case "library": return "TypeScript library";
|
|
836
|
+
default: throw new Error(`Unknown project type: ${String(projectType)}`);
|
|
837
|
+
}
|
|
683
838
|
}
|
|
684
839
|
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);
|
|
840
|
+
if (projectType !== "webapp" || !installDeps || process.env.NPM_TOKEN) return;
|
|
841
|
+
console.log();
|
|
842
|
+
console.error(chalk.red("Error: NPM_TOKEN environment variable is not set."));
|
|
843
|
+
console.error(chalk.dim(" Required to install private @percepta/* packages."));
|
|
844
|
+
console.error();
|
|
845
|
+
console.error(" 1. Grab the npm token from 1Password:");
|
|
846
|
+
console.error(chalk.cyan(" https://start.1password.com/open/i?a=5TX2B4O3QNE4FNQ2A7ZJZDRRBI&v=j7trpyuqh7gt635dtuj6y4pwjm&i=cmmdi5trji7ctkn3fseakf4mgi&h=aitco.1password.com"));
|
|
847
|
+
console.error(" 2. Add to ~/.zshrc:");
|
|
848
|
+
console.error(chalk.cyan(" export NPM_TOKEN=\"<paste-token>\""));
|
|
849
|
+
console.error(" 3. Open a new terminal (or " + chalk.cyan("source ~/.zshrc") + ") and re-run.");
|
|
850
|
+
console.error();
|
|
851
|
+
console.error(chalk.dim(" Or pass --skip-install to scaffold without running install."));
|
|
852
|
+
process.exit(1);
|
|
712
853
|
}
|
|
713
854
|
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
|
-
}
|
|
855
|
+
const cwd = await resolveCreateCwd(options.cwd);
|
|
856
|
+
if (options.type !== void 0 && !isValidProjectType(options.type)) {
|
|
857
|
+
console.error(chalk.red(`Error: Invalid package type "${options.type}". Valid types are: ${VALID_PROJECT_TYPES.join(", ")}`));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
console.log();
|
|
861
|
+
console.log(chalk.bold("Creating a new Mosaic package..."));
|
|
862
|
+
console.log();
|
|
863
|
+
const monorepoContext = await detectMonorepo(cwd);
|
|
864
|
+
if (options.type === "monorepo" && monorepoContext.found) {
|
|
865
|
+
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.`));
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
if (monorepoContext.found) console.log(chalk.dim(" Detected monorepo at"), chalk.cyan(monorepoContext.rootDir));
|
|
869
|
+
else console.log(chalk.dim(" No monorepo detected. A new monorepo will be created."));
|
|
870
|
+
console.log();
|
|
871
|
+
const projectName = options.name;
|
|
872
|
+
const repoName = options.repoName;
|
|
873
|
+
if (options.yes && !projectName) {
|
|
874
|
+
console.error(chalk.red("Error: --name is required when using --yes flag"));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
if (projectName) {
|
|
878
|
+
const validation = validateProjectName(toKebabCase(projectName));
|
|
879
|
+
if (!validation.valid) {
|
|
880
|
+
console.error(chalk.red(`Invalid project name: ${validation.error}`));
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (repoName) {
|
|
885
|
+
const validation = validateProjectName(toKebabCase(repoName));
|
|
886
|
+
if (!validation.valid) {
|
|
887
|
+
console.error(chalk.red(`Invalid repo name: ${validation.error}`));
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
let answers;
|
|
892
|
+
if (options.yes) {
|
|
893
|
+
const projectType = options.type || "webapp";
|
|
894
|
+
requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
|
|
895
|
+
const kebabName = toKebabCase(projectName);
|
|
896
|
+
const kebabRepoName = repoName ? toKebabCase(repoName) : kebabName;
|
|
897
|
+
answers = {
|
|
898
|
+
projectType,
|
|
899
|
+
directory: monorepoContext.found && monorepoContext.packageDir ? path.join(monorepoContext.packageDir, kebabName) : path.resolve(cwd, kebabRepoName),
|
|
900
|
+
name: kebabName,
|
|
901
|
+
title: toTitleCase(kebabName),
|
|
902
|
+
installDeps: !options.skipInstall,
|
|
903
|
+
monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
|
|
904
|
+
monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
|
|
905
|
+
};
|
|
906
|
+
} else {
|
|
907
|
+
answers = await promptProjectDetails({
|
|
908
|
+
projectType: options.type,
|
|
909
|
+
name: projectName ? toKebabCase(projectName) : void 0,
|
|
910
|
+
repoName: repoName ? toKebabCase(repoName) : void 0,
|
|
911
|
+
skipInstall: options.skipInstall,
|
|
912
|
+
monorepoContext,
|
|
913
|
+
cwd,
|
|
914
|
+
beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
|
|
915
|
+
});
|
|
916
|
+
if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) answers.directory = path.join(monorepoContext.packageDir, answers.name);
|
|
917
|
+
}
|
|
918
|
+
const monorepoName = answers.monorepoName ?? answers.name;
|
|
919
|
+
const monorepoConfig = buildAppConfig(monorepoName, answers.monorepoTitle ?? toTitleCase(monorepoName));
|
|
920
|
+
const configRepoName = monorepoContext.found ? path.basename(monorepoContext.rootDir) : monorepoName;
|
|
921
|
+
const config = buildAppConfig(answers.name, answers.title, configRepoName);
|
|
922
|
+
const typeLabel = getProjectTypeLabel(answers.projectType);
|
|
923
|
+
if (monorepoContext.found) {
|
|
924
|
+
const monorepoRoot = monorepoContext.rootDir;
|
|
925
|
+
const packageDir = monorepoContext.packageDir ? path.join(monorepoContext.packageDir, answers.name) : answers.directory;
|
|
926
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
927
|
+
console.log(chalk.dim(" Target:"), packageDir);
|
|
928
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
929
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
930
|
+
if (answers.projectType === "webapp") console.log(chalk.dim(" Database:"), config.dbName);
|
|
931
|
+
console.log();
|
|
932
|
+
if (await fs.pathExists(packageDir)) {
|
|
933
|
+
if ((await fs.readdir(packageDir)).length > 0) {
|
|
934
|
+
console.error(chalk.red(`Error: Directory ${packageDir} is not empty.`));
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (answers.projectType !== "monorepo") await addPackageToMonorepo({
|
|
939
|
+
packageDir,
|
|
940
|
+
monorepoRoot,
|
|
941
|
+
projectType: answers.projectType,
|
|
942
|
+
config
|
|
943
|
+
});
|
|
944
|
+
await warnIfMissingRootNpmrc(monorepoRoot);
|
|
945
|
+
const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
|
|
946
|
+
console.log();
|
|
947
|
+
console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(path.relative(monorepoRoot, packageDir)));
|
|
948
|
+
console.log();
|
|
949
|
+
if (await maybeAutoRunWebapp(packageDir, answers.projectType, installSucceeded)) return;
|
|
950
|
+
printNextStepsExisting(answers, packageDir);
|
|
951
|
+
} else {
|
|
952
|
+
const isBareMonorepo = answers.projectType === "monorepo";
|
|
953
|
+
const monorepoRoot = answers.directory;
|
|
954
|
+
const packageDir = isBareMonorepo ? null : path.join(monorepoRoot, "packages", answers.name);
|
|
955
|
+
if (isBareMonorepo) {
|
|
956
|
+
console.log(chalk.dim(" Type:"), typeLabel);
|
|
957
|
+
console.log(chalk.dim(" Directory:"), monorepoRoot);
|
|
958
|
+
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
959
|
+
console.log(chalk.dim(" Title:"), monorepoConfig.title);
|
|
960
|
+
} else {
|
|
961
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
962
|
+
console.log(chalk.dim(" Monorepo directory:"), monorepoRoot);
|
|
963
|
+
console.log(chalk.dim(" Repo name:"), monorepoConfig.name);
|
|
964
|
+
console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
|
|
965
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
966
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
967
|
+
if (answers.projectType === "webapp") console.log(chalk.dim(" Database:"), config.dbName);
|
|
968
|
+
}
|
|
969
|
+
console.log();
|
|
970
|
+
if (await fs.pathExists(monorepoRoot)) {
|
|
971
|
+
if ((await fs.readdir(monorepoRoot)).length > 0) {
|
|
972
|
+
console.error(chalk.red(`Error: Directory ${monorepoRoot} is not empty.`));
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
await scaffoldMonorepo(monorepoRoot, monorepoConfig);
|
|
977
|
+
if (packageDir && answers.projectType !== "monorepo") await addPackageToMonorepo({
|
|
978
|
+
packageDir,
|
|
979
|
+
monorepoRoot,
|
|
980
|
+
projectType: answers.projectType,
|
|
981
|
+
config
|
|
982
|
+
});
|
|
983
|
+
initGitRepo(monorepoRoot);
|
|
984
|
+
const installSucceeded = await installAtMonorepoRoot(monorepoRoot, answers.installDeps);
|
|
985
|
+
console.log();
|
|
986
|
+
console.log(chalk.green("✔"), chalk.bold(isBareMonorepo ? `Created ${typeLabel} at` : "Created monorepo at"), chalk.cyan(monorepoRoot));
|
|
987
|
+
if (!isBareMonorepo) console.log(chalk.green("✔"), chalk.bold(`Created ${typeLabel} at`), chalk.cyan(`packages/${answers.name}/`));
|
|
988
|
+
console.log();
|
|
989
|
+
if (await maybeAutoRunWebapp(packageDir, answers.projectType, installSucceeded)) return;
|
|
990
|
+
printNextStepsNew(answers, monorepoRoot);
|
|
991
|
+
}
|
|
919
992
|
}
|
|
920
993
|
async function resolveCreateCwd(cwdOption) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
994
|
+
const cwd = cwdOption ? path.resolve(cwdOption) : process.cwd();
|
|
995
|
+
let stat;
|
|
996
|
+
try {
|
|
997
|
+
stat = await fs.stat(cwd);
|
|
998
|
+
} catch {
|
|
999
|
+
console.error(chalk.red(`Error: --cwd directory does not exist: ${cwd}`));
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
if (!stat.isDirectory()) {
|
|
1003
|
+
console.error(chalk.red(`Error: --cwd is not a directory: ${cwd}`));
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
return cwd;
|
|
934
1007
|
}
|
|
935
1008
|
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
|
-
}
|
|
1009
|
+
const { pm, answers, variant, monorepoRelativePath, packageRelativePath } = params;
|
|
1010
|
+
const repoRel = shPath(monorepoRelativePath) || ".";
|
|
1011
|
+
const pkgFromRoot = `packages/${answers.name}`;
|
|
1012
|
+
const pnpmSteps = ["pnpm run setup", "pnpm dev"];
|
|
1013
|
+
if (variant === "new") {
|
|
1014
|
+
const oneLinerParts = [];
|
|
1015
|
+
if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
|
|
1016
|
+
if (!answers.installDeps) oneLinerParts.push(`${pm} install`);
|
|
1017
|
+
oneLinerParts.push(`cd ${pkgFromRoot}`);
|
|
1018
|
+
oneLinerParts.push(...pnpmSteps);
|
|
1019
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
1020
|
+
console.log();
|
|
1021
|
+
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
1022
|
+
console.log();
|
|
1023
|
+
console.log(chalk.bold("Or step by step:"));
|
|
1024
|
+
console.log();
|
|
1025
|
+
let step = 1;
|
|
1026
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1027
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1028
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
1029
|
+
for (const cmd of pnpmSteps) console.log(chalk.dim(` ${step++}.`), cmd);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const pkgRel = shPath(packageRelativePath ?? ".") || ".";
|
|
1033
|
+
const oneLinerParts = [];
|
|
1034
|
+
if (!answers.installDeps) {
|
|
1035
|
+
if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
|
|
1036
|
+
oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
|
|
1037
|
+
} else if (pkgRel !== ".") oneLinerParts.push(`cd ${pkgRel}`);
|
|
1038
|
+
oneLinerParts.push(...pnpmSteps);
|
|
1039
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
1040
|
+
console.log();
|
|
1041
|
+
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
1042
|
+
console.log();
|
|
1043
|
+
console.log(chalk.bold("Or step by step:"));
|
|
1044
|
+
console.log();
|
|
1045
|
+
let step = 1;
|
|
1046
|
+
if (!answers.installDeps) {
|
|
1047
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1048
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1049
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
1050
|
+
} else if (pkgRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
1051
|
+
for (const cmd of pnpmSteps) console.log(chalk.dim(` ${step++}.`), cmd);
|
|
999
1052
|
}
|
|
1000
1053
|
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();
|
|
1054
|
+
const rootNpmrc = path.join(rootDir, ".npmrc");
|
|
1055
|
+
let contents = "";
|
|
1056
|
+
if (await fs.pathExists(rootNpmrc)) contents = await fs.readFile(rootNpmrc, "utf8");
|
|
1057
|
+
if (contents.includes("@percepta:registry")) return;
|
|
1058
|
+
console.log();
|
|
1059
|
+
console.log(chalk.yellow("!"), chalk.bold("Root .npmrc is missing @percepta registry config."));
|
|
1060
|
+
console.log(chalk.dim(" pnpm reads .npmrc from the workspace root, so add these lines to"));
|
|
1061
|
+
console.log(chalk.dim(` ${path.join(rootDir, ".npmrc")}:`));
|
|
1062
|
+
console.log();
|
|
1063
|
+
console.log(chalk.cyan(" @percepta:registry=https://registry.npmjs.org/"));
|
|
1064
|
+
console.log(chalk.cyan(" //registry.npmjs.org/:_authToken=${NPM_TOKEN}"));
|
|
1065
|
+
console.log();
|
|
1028
1066
|
}
|
|
1029
1067
|
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();
|
|
1068
|
+
const pm = PACKAGE_MANAGER;
|
|
1069
|
+
const relativePath = path.relative(process.cwd(), targetDir) || ".";
|
|
1070
|
+
console.log("Next steps:");
|
|
1071
|
+
console.log();
|
|
1072
|
+
switch (answers.projectType) {
|
|
1073
|
+
case "monorepo": {
|
|
1074
|
+
let step = 1;
|
|
1075
|
+
const repoRel = shPath(relativePath);
|
|
1076
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1077
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1078
|
+
console.log(chalk.dim(` ${step++}.`), `Add a package: ${chalk.cyan(`npx @percepta/create --type webapp <name>`)}`);
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
case "webapp":
|
|
1082
|
+
printWebappNextSteps({
|
|
1083
|
+
pm,
|
|
1084
|
+
answers,
|
|
1085
|
+
variant: "new",
|
|
1086
|
+
monorepoRelativePath: relativePath
|
|
1087
|
+
});
|
|
1088
|
+
break;
|
|
1089
|
+
case "library": {
|
|
1090
|
+
let step = 1;
|
|
1091
|
+
const repoRel = shPath(relativePath);
|
|
1092
|
+
if (repoRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
1093
|
+
if (!answers.installDeps) console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
1094
|
+
console.log(chalk.dim(` ${step++}.`), `cd packages/${answers.name}`);
|
|
1095
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
1096
|
+
console.log(chalk.dim(` ${step++}.`), `Edit src/index.ts to add your library code`);
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
console.log();
|
|
1101
|
+
console.log(chalk.dim("For more information, see the README.md in your project."));
|
|
1102
|
+
console.log();
|
|
1084
1103
|
}
|
|
1085
1104
|
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();
|
|
1105
|
+
const pm = PACKAGE_MANAGER;
|
|
1106
|
+
const packageRelativePath = path.relative(process.cwd(), packageDir) || ".";
|
|
1107
|
+
const monorepoRoot = path.dirname(path.dirname(packageDir));
|
|
1108
|
+
const monorepoRelativePath = path.relative(process.cwd(), monorepoRoot) || ".";
|
|
1109
|
+
console.log("Next steps:");
|
|
1110
|
+
console.log();
|
|
1111
|
+
switch (answers.projectType) {
|
|
1112
|
+
case "webapp":
|
|
1113
|
+
printWebappNextSteps({
|
|
1114
|
+
pm,
|
|
1115
|
+
answers,
|
|
1116
|
+
variant: "existing",
|
|
1117
|
+
monorepoRelativePath,
|
|
1118
|
+
packageRelativePath
|
|
1119
|
+
});
|
|
1120
|
+
break;
|
|
1121
|
+
case "library": {
|
|
1122
|
+
let step = 1;
|
|
1123
|
+
const pkgRel = shPath(packageRelativePath);
|
|
1124
|
+
if (pkgRel !== ".") console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
1125
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
1126
|
+
console.log(chalk.dim(` ${step++}.`), "Edit src/index.ts to add your library code");
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
console.log();
|
|
1131
|
+
console.log(chalk.dim("For more information, see the README.md in your project."));
|
|
1132
|
+
console.log();
|
|
1121
1133
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
};
|
|
1128
|
-
program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
|
|
1134
|
+
//#endregion
|
|
1135
|
+
//#region src/index.ts
|
|
1136
|
+
program.name("create").description("Scaffold and manage Mosaic packages").version({
|
|
1137
|
+
name: "@percepta/create",
|
|
1138
|
+
version: "1.0.0"
|
|
1139
|
+
}.version);
|
|
1129
1140
|
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);
|
|
1141
|
+
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) => {
|
|
1142
|
+
const { statusCommand } = await import("./status-DC8mvHZj.js");
|
|
1143
|
+
await statusCommand(options);
|
|
1136
1144
|
});
|
|
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);
|
|
1145
|
+
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) => {
|
|
1146
|
+
const { syncCommand } = await import("./sync-C5Pd32VM.js");
|
|
1147
|
+
await syncCommand(options);
|
|
1143
1148
|
});
|
|
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);
|
|
1149
|
+
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) => {
|
|
1150
|
+
const { upstreamCommand } = await import("./upstream-F6m8zRBQ.js");
|
|
1151
|
+
await upstreamCommand(options);
|
|
1150
1152
|
});
|
|
1151
1153
|
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
|
-
|
|
1154
|
+
const { initCommand } = await import("./init-OeK4Yk6_.js");
|
|
1155
|
+
await initCommand(options);
|
|
1154
1156
|
});
|
|
1155
1157
|
program.parse();
|
|
1158
|
+
//#endregion
|
|
1159
|
+
export { readManifest as a, manifestExists as i, isValidProjectType as n, resolveMosaicTemplatePath as o, derivePlaceholders as r, writeManifest as s, VALID_PROJECT_TYPES as t };
|
|
1160
|
+
|
|
1161
|
+
//# sourceMappingURL=index.js.map
|