@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.
Files changed (39) hide show
  1. package/README.md +8 -8
  2. package/dist/git-ops-C2CIjuce.js +51 -0
  3. package/dist/git-ops-C2CIjuce.js.map +1 -0
  4. package/dist/index.js +1073 -1067
  5. package/dist/index.js.map +1 -0
  6. package/dist/init-OeK4Yk6_.js +52 -0
  7. package/dist/init-OeK4Yk6_.js.map +1 -0
  8. package/dist/status-DC8mvHZj.js +48 -0
  9. package/dist/status-DC8mvHZj.js.map +1 -0
  10. package/dist/sync-C5Pd32VM.js +101 -0
  11. package/dist/sync-C5Pd32VM.js.map +1 -0
  12. package/dist/upstream-F6m8zRBQ.js +85 -0
  13. package/dist/upstream-F6m8zRBQ.js.map +1 -0
  14. package/package.json +23 -24
  15. package/templates/webapp/AGENTS.md +1 -1
  16. package/templates/webapp/README.md +1 -1
  17. package/templates/webapp/agent-skills/database.md +5 -1
  18. package/templates/webapp/agent-skills/deploy.md +5 -3
  19. package/templates/webapp/agent-skills/inngest.md +13 -8
  20. package/templates/webapp/agent-skills/oneshot.md +1 -1
  21. package/templates/webapp/deploy/README.md +2 -2
  22. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +3 -3
  23. package/templates/webapp/package.json.template +2 -2
  24. package/templates/webapp/scripts/deploy-percepta-test.ts +311 -36
  25. package/templates/webapp/scripts/generate-migrations.ts +28 -0
  26. package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +24 -0
  27. package/templates/webapp/src/drizzle/migrationSql.ts +8 -0
  28. package/templates/webapp/src/services/inngest/AppWorkflowService.ts +19 -0
  29. package/templates/webapp/src/services/inngest/__tests__/AppWorkflowService.test.ts +19 -0
  30. package/templates/webapp/src/services/inngest/events/AppEvents.ts +7 -13
  31. package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +1 -3
  32. package/dist/chunk-CO3YWUD6.js +0 -139
  33. package/dist/chunk-DCM7JOSC.js +0 -49
  34. package/dist/chunk-V5EJIUBJ.js +0 -60
  35. package/dist/index.d.ts +0 -1
  36. package/dist/init-EQZ2TCSJ.js +0 -96
  37. package/dist/status-QW5TQDYY.js +0 -76
  38. package/dist/sync-RLBZDOFB.js +0 -136
  39. 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
- // src/commands/create.ts
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";
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
- var __filename = fileURLToPath(import.meta.url);
32
- var __dirname = path.dirname(__filename);
33
- var SKIP_DIRS = /* @__PURE__ */ new Set([
34
- "node_modules",
35
- ".git",
36
- ".next",
37
- "dist",
38
- ".turbo",
39
- ".vercel",
40
- ".cursor"
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
- var SKIP_FILES = /* @__PURE__ */ new Set([
43
- "pnpm-lock.yaml",
44
- "package-lock.json",
45
- "yarn.lock",
46
- ".DS_Store"
40
+ const SKIP_FILES$1 = new Set([
41
+ "pnpm-lock.yaml",
42
+ "package-lock.json",
43
+ "yarn.lock",
44
+ ".DS_Store"
47
45
  ]);
48
- var TEMPLATE_FILE_MAPPINGS = {
49
- "package.json.template": "package.json",
50
- "gitignore.template": ".gitignore",
51
- "env.example.template": ".env.example",
52
- "npmrc.template": ".npmrc"
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
- const basename = path.basename(src);
56
- if (SKIP_DIRS.has(basename)) return true;
57
- if (SKIP_FILES.has(basename)) return true;
58
- return false;
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
- return path.resolve(__dirname, "../templates", templateType);
59
+ return path.resolve(__dirname, "../templates", templateType);
62
60
  }
63
61
  async function copyTemplate(targetDir, templateType) {
64
- const templateDir = getTemplateDir(templateType);
65
- if (!await fs.pathExists(templateDir)) {
66
- throw new Error(`Template directory not found: ${templateDir}`);
67
- }
68
- await fs.ensureDir(targetDir);
69
- await fs.copy(templateDir, targetDir, {
70
- filter: (src) => !shouldSkip(src)
71
- });
72
- for (const [templateName, targetName] of Object.entries(
73
- TEMPLATE_FILE_MAPPINGS
74
- )) {
75
- const templatePath = path.join(targetDir, templateName);
76
- const targetPath = path.join(targetDir, targetName);
77
- if (await fs.pathExists(templatePath)) {
78
- await fs.move(templatePath, targetPath, { overwrite: true });
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
- // src/utils/replace-placeholders.ts
94
- import path2 from "path";
95
- import fs2 from "fs-extra";
96
- var PLACEHOLDERS = {
97
- __APP_NAME__: "name",
98
- __APP_TITLE__: "title",
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
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
106
- "node_modules",
107
- ".git",
108
- ".next",
109
- "dist",
110
- ".turbo",
111
- ".vercel"
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
- var SKIP_FILES2 = /* @__PURE__ */ new Set([
114
- "pnpm-lock.yaml",
115
- "package-lock.json",
116
- "yarn.lock"
365
+ const SKIP_FILES = new Set([
366
+ "pnpm-lock.yaml",
367
+ "package-lock.json",
368
+ "yarn.lock"
117
369
  ]);
118
- var PROCESSABLE_EXTENSIONS = /* @__PURE__ */ new Set([
119
- ".ts",
120
- ".tsx",
121
- ".js",
122
- ".jsx",
123
- ".json",
124
- ".yml",
125
- ".yaml",
126
- ".md",
127
- ".env",
128
- ".sql",
129
- ".tf",
130
- ".tfvars",
131
- ".sh",
132
- ".mjs",
133
- ".cjs"
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
- var PROCESSABLE_FILENAMES = /* @__PURE__ */ new Set([
136
- "Dockerfile",
137
- ".env.example",
138
- ".env.local",
139
- "terraform.tfvars.example"
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
- const fileName = path2.basename(filePath);
143
- const ext = path2.extname(filePath);
144
- if (SKIP_FILES2.has(fileName)) return false;
145
- if (PROCESSABLE_FILENAMES.has(fileName)) return true;
146
- return PROCESSABLE_EXTENSIONS.has(ext);
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
- let content;
150
- try {
151
- content = await fs2.readFile(filePath, "utf-8");
152
- } catch {
153
- return false;
154
- }
155
- let modified = false;
156
- let newContent = content;
157
- const sortedEntries = Object.entries(PLACEHOLDERS).sort(
158
- (a, b) => b[0].length - a[0].length
159
- );
160
- for (const [placeholder, configKey] of sortedEntries) {
161
- const value = config[configKey];
162
- if (newContent.includes(placeholder)) {
163
- newContent = newContent.split(placeholder).join(value);
164
- modified = true;
165
- }
166
- }
167
- if (modified) {
168
- await fs2.writeFile(filePath, newContent);
169
- return true;
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
- const sortedEntries = Object.entries(PLACEHOLDERS).sort(
175
- (a, b) => b[0].length - a[0].length
176
- );
177
- let result = name;
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
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
189
- for (const entry of entries) {
190
- const fullPath = path2.join(dirPath, entry.name);
191
- if (entry.isDirectory()) {
192
- if (!SKIP_DIRS2.has(entry.name)) {
193
- await processDirectory(fullPath, config, stats);
194
- }
195
- } else if (entry.isFile() && shouldProcessFile(fullPath)) {
196
- stats.processed++;
197
- if (await replaceInFile(fullPath, config)) {
198
- stats.modified++;
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
- const stats = { processed: 0, modified: 0 };
211
- await processDirectory(targetDir, config, stats);
212
- return stats;
213
- }
214
-
215
- // src/utils/detect-monorepo.ts
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
- // src/utils/resolve-percepta-versions.ts
319
- import path6 from "path";
320
- import { execFile } from "child_process";
321
- import { promisify } from "util";
322
- import fs6 from "fs-extra";
323
- var execFileAsync = promisify(execFile);
324
- var DEPENDENCY_SECTIONS = [
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
- const names = /* @__PURE__ */ new Set();
332
- for (const section of DEPENDENCY_SECTIONS) {
333
- const deps = pkg[section];
334
- if (!deps) continue;
335
- for (const name of Object.keys(deps)) {
336
- if (name.startsWith("@percepta/")) {
337
- names.add(name);
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
- const { stdout } = await execFileAsync(
345
- "npm",
346
- ["view", packageName, "dist-tags.latest", "--silent"],
347
- {
348
- cwd,
349
- encoding: "utf8",
350
- timeout: 5e3
351
- }
352
- );
353
- const version = stdout.trim();
354
- return version.length > 0 ? version : null;
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
- const cwd = path6.dirname(packageJsonPath);
358
- const pkg = await fs6.readJson(packageJsonPath);
359
- const packageNames = getPerceptaPackages(pkg);
360
- const resolved = {};
361
- const failed = [];
362
- const results = await Promise.all(
363
- packageNames.map(async (packageName) => {
364
- try {
365
- return {
366
- packageName,
367
- latest: await lookupLatest(packageName, cwd)
368
- };
369
- } catch {
370
- return { packageName, latest: null };
371
- }
372
- })
373
- );
374
- for (const { packageName, latest } of results) {
375
- if (!latest) {
376
- failed.push(packageName);
377
- continue;
378
- }
379
- resolved[packageName] = latest;
380
- for (const section of DEPENDENCY_SECTIONS) {
381
- const deps = pkg[section];
382
- if (deps?.[packageName]) {
383
- deps[packageName] = latest;
384
- }
385
- }
386
- }
387
- if (Object.keys(resolved).length > 0) {
388
- await fs6.writeJson(packageJsonPath, pkg, { spaces: 2 });
389
- await fs6.appendFile(packageJsonPath, "\n");
390
- }
391
- return { resolved, failed };
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
- // src/commands/create.ts
395
- var PACKAGE_MANAGER = "pnpm";
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
- return p.split(path7.sep).join("/");
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
- return new Promise((resolve, reject) => {
401
- const child = spawn(packageManager, args, {
402
- cwd,
403
- stdio: "ignore"
404
- });
405
- child.on("error", reject);
406
- child.on("close", (code) => {
407
- if (code === 0) resolve();
408
- else
409
- reject(
410
- new Error(
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
- return new Promise((resolve, reject) => {
419
- const child = spawn(packageManager, ["run", "setup"], { cwd, stdio: "inherit" });
420
- child.on("error", reject);
421
- child.on("close", (code) => {
422
- if (code === 0) resolve();
423
- else reject(new Error(`${packageManager} run setup exited with code ${code ?? "unknown"}`));
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
- var ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g;
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
- const child = spawn(packageManager, ["run", "dev"], {
430
- cwd,
431
- stdio: ["inherit", "pipe", "pipe"]
432
- });
433
- const ready = new Promise((resolve, reject) => {
434
- let resolved = false;
435
- let detectedUrl = "http://localhost:3000";
436
- let buffer = "";
437
- const onChunk = (chunk) => {
438
- const text = chunk.toString();
439
- process.stdout.write(text);
440
- buffer = (buffer + text).slice(-4096).replace(ANSI_PATTERN, "");
441
- const urlMatch = buffer.match(/Local:\s+(https?:\/\/\S+?)(?:\s|$)/i);
442
- if (urlMatch?.[1]) detectedUrl = urlMatch[1].trim();
443
- if (!resolved && /Ready in /i.test(buffer)) {
444
- resolved = true;
445
- resolve({ url: detectedUrl });
446
- }
447
- };
448
- child.stdout?.on("data", onChunk);
449
- child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
450
- child.on("error", reject);
451
- child.on("close", (code) => {
452
- if (!resolved) {
453
- reject(new Error(`${packageManager} run dev exited with code ${code ?? "unknown"} before becoming ready`));
454
- }
455
- });
456
- });
457
- return { child, ready };
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
- const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
461
- try {
462
- const child = spawn(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true });
463
- child.on("error", () => {
464
- });
465
- child.unref();
466
- return true;
467
- } catch {
468
- return false;
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
- const packageManager = PACKAGE_MANAGER;
473
- console.log();
474
- console.log(chalk.bold("Running setup (docker, db, seed)..."));
475
- console.log();
476
- try {
477
- await runWebappSetup(packageManager, packageDir);
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;
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
- const versionsPath = path7.resolve(
516
- path7.dirname(fileURLToPath2(import.meta.url)),
517
- "../template-versions.json"
518
- );
519
- try {
520
- const content = fs7.readFileSync(versionsPath, "utf-8");
521
- return JSON.parse(content);
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
- const templateVersions = readTemplateVersions();
528
- const manifest = {
529
- templateType: projectType,
530
- templateVersion: templateVersions[projectType] || "1.0.0",
531
- templateCommit: "npm",
532
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
533
- placeholders: derivePlaceholders(config.name, config.title, config.repoName),
534
- source: {
535
- templatePath: `packages/create-mosaic-module/templates/${projectType}`
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
- return {
555
- name,
556
- title,
557
- dbName: `${toSnakeCase(name)}_db`,
558
- nameUpper: name.toUpperCase(),
559
- nameSnake: toSnakeCase(name),
560
- repoName,
561
- repoNameSnake: toSnakeCase(repoName)
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
- const monoSpinner = ora("Copying monorepo template...").start();
566
- try {
567
- await copyTemplate(targetDir, "monorepo");
568
- monoSpinner.succeed("Copied monorepo template");
569
- } catch (error) {
570
- monoSpinner.fail("Failed to copy monorepo template");
571
- console.error(error);
572
- process.exit(1);
573
- }
574
- const replaceSpinner = ora("Replacing monorepo placeholders...").start();
575
- try {
576
- const stats = await replacePlaceholders(targetDir, config);
577
- replaceSpinner.succeed(
578
- `Replaced placeholders in ${stats.modified} monorepo files`
579
- );
580
- } catch (error) {
581
- replaceSpinner.fail("Failed to replace monorepo placeholders");
582
- console.error(error);
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
- const { packageDir, monorepoRoot, projectType, config } = args;
588
- const copySpinner = ora("Copying package template...").start();
589
- try {
590
- await copyTemplate(packageDir, projectType);
591
- copySpinner.succeed("Copied package template");
592
- } catch (error) {
593
- copySpinner.fail("Failed to copy package template");
594
- console.error(error);
595
- process.exit(1);
596
- }
597
- const replaceSpinner = ora("Replacing package placeholders...").start();
598
- try {
599
- const stats = await replacePlaceholders(packageDir, config);
600
- replaceSpinner.succeed(
601
- `Replaced placeholders in ${stats.modified} package files`
602
- );
603
- } catch (error) {
604
- replaceSpinner.fail("Failed to replace package placeholders");
605
- console.error(error);
606
- process.exit(1);
607
- }
608
- await writeMosaicFiles(packageDir, config, projectType);
609
- if (projectType === "webapp") {
610
- await resolvePerceptaPackageVersions(packageDir);
611
- await generateEnvLocal(packageDir);
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
- const packageJsonPath = path7.join(packageDir, "package.json");
617
- if (!await fs7.pathExists(packageJsonPath)) return;
618
- const spinner = ora("Resolving latest @percepta/* versions...").start();
619
- try {
620
- const result = await resolvePerceptaVersionsInPackageJson(packageJsonPath);
621
- const count = Object.keys(result.resolved).length;
622
- if (result.failed.length > 0) {
623
- spinner.warn(
624
- `Resolved ${count} @percepta/* versions; kept existing ranges for ${result.failed.join(", ")}`
625
- );
626
- } else {
627
- spinner.succeed(`Resolved ${count} @percepta/* versions`);
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
- const gitSpinner = ora("Initializing git repository...").start();
638
- try {
639
- execSync("git init", { cwd: targetDir, stdio: "ignore" });
640
- execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
641
- execSync('git commit -m "Initial commit from @percepta/create"', {
642
- cwd: targetDir,
643
- stdio: "ignore"
644
- });
645
- gitSpinner.succeed("Initialized git repository");
646
- } catch {
647
- gitSpinner.warn("Failed to initialize git repository");
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
- if (!installDeps) return false;
652
- const spinner = ora(
653
- `Installing dependencies with ${PACKAGE_MANAGER}...`
654
- ).start();
655
- try {
656
- await runPackageManagerInstall(PACKAGE_MANAGER, monorepoRoot);
657
- spinner.succeed("Installed dependencies");
658
- return true;
659
- } catch {
660
- spinner.warn(
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
- if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
668
- return autoRunWebapp(packageDir);
828
+ if (!packageDir || projectType !== "webapp" || !installSucceeded) return false;
829
+ return autoRunWebapp(packageDir);
669
830
  }
670
831
  function getProjectTypeLabel(projectType) {
671
- switch (projectType) {
672
- case "monorepo":
673
- return "pnpm monorepo";
674
- case "webapp":
675
- return "Next.js webapp";
676
- case "library":
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
- if (projectType !== "webapp" || !installDeps || process.env.NPM_TOKEN) {
686
- return;
687
- }
688
- console.log();
689
- console.error(chalk.red("Error: NPM_TOKEN environment variable is not set."));
690
- console.error(
691
- chalk.dim(" Required to install private @percepta/* packages.")
692
- );
693
- console.error();
694
- console.error(" 1. Grab the npm token from 1Password:");
695
- console.error(
696
- chalk.cyan(
697
- " https://start.1password.com/open/i?a=5TX2B4O3QNE4FNQ2A7ZJZDRRBI&v=j7trpyuqh7gt635dtuj6y4pwjm&i=cmmdi5trji7ctkn3fseakf4mgi&h=aitco.1password.com"
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
- const cwd = await resolveCreateCwd(options.cwd);
715
- if (options.type !== void 0 && !isValidProjectType(options.type)) {
716
- console.error(
717
- chalk.red(
718
- `Error: Invalid package type "${options.type}". Valid types are: ${VALID_PROJECT_TYPES.join(", ")}`
719
- )
720
- );
721
- process.exit(1);
722
- }
723
- console.log();
724
- console.log(chalk.bold("Creating a new Mosaic package..."));
725
- console.log();
726
- const monorepoContext = await detectMonorepo(cwd);
727
- if (options.type === "monorepo" && monorepoContext.found) {
728
- console.error(
729
- chalk.red(
730
- `Error: Already inside a monorepo at ${monorepoContext.rootDir}. Choose 'webapp' or 'library' to add a package, or run from outside the monorepo.`
731
- )
732
- );
733
- process.exit(1);
734
- }
735
- if (monorepoContext.found) {
736
- console.log(
737
- chalk.dim(" Detected monorepo at"),
738
- chalk.cyan(monorepoContext.rootDir)
739
- );
740
- } else {
741
- console.log(
742
- chalk.dim(
743
- " No monorepo detected. A new monorepo will be created."
744
- )
745
- );
746
- }
747
- console.log();
748
- const projectName = options.name;
749
- const repoName = options.repoName;
750
- if (options.yes && !projectName) {
751
- console.error(
752
- chalk.red("Error: --name is required when using --yes flag")
753
- );
754
- process.exit(1);
755
- }
756
- if (projectName) {
757
- const validation = validateProjectName(toKebabCase(projectName));
758
- if (!validation.valid) {
759
- console.error(chalk.red(`Invalid project name: ${validation.error}`));
760
- process.exit(1);
761
- }
762
- }
763
- if (repoName) {
764
- const validation = validateProjectName(toKebabCase(repoName));
765
- if (!validation.valid) {
766
- console.error(chalk.red(`Invalid repo name: ${validation.error}`));
767
- process.exit(1);
768
- }
769
- }
770
- let answers;
771
- if (options.yes) {
772
- const projectType = options.type || "webapp";
773
- requireNpmTokenForWebappInstall(projectType, !options.skipInstall);
774
- const kebabName = toKebabCase(projectName);
775
- const kebabRepoName = repoName ? toKebabCase(repoName) : kebabName;
776
- const directory = monorepoContext.found && monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, kebabName) : path7.resolve(cwd, kebabRepoName);
777
- answers = {
778
- projectType,
779
- directory,
780
- name: kebabName,
781
- title: toTitleCase(kebabName),
782
- installDeps: !options.skipInstall,
783
- monorepoName: monorepoContext.found ? void 0 : kebabRepoName,
784
- monorepoTitle: monorepoContext.found ? void 0 : toTitleCase(kebabRepoName)
785
- };
786
- } else {
787
- answers = await promptProjectDetails({
788
- projectType: options.type,
789
- name: projectName ? toKebabCase(projectName) : void 0,
790
- repoName: repoName ? toKebabCase(repoName) : void 0,
791
- skipInstall: options.skipInstall,
792
- monorepoContext,
793
- cwd,
794
- beforeNamePrompt: (projectType) => requireNpmTokenForWebappInstall(projectType, !options.skipInstall)
795
- });
796
- if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) {
797
- answers.directory = path7.join(monorepoContext.packageDir, answers.name);
798
- }
799
- }
800
- const monorepoName = answers.monorepoName ?? answers.name;
801
- const monorepoTitle = answers.monorepoTitle ?? toTitleCase(monorepoName);
802
- const monorepoConfig = buildAppConfig(monorepoName, monorepoTitle);
803
- const configRepoName = monorepoContext.found ? path7.basename(monorepoContext.rootDir) : monorepoName;
804
- const config = buildAppConfig(answers.name, answers.title, configRepoName);
805
- const typeLabel = getProjectTypeLabel(answers.projectType);
806
- if (monorepoContext.found) {
807
- const monorepoRoot = monorepoContext.rootDir;
808
- const packageDir = monorepoContext.packageDir ? path7.join(monorepoContext.packageDir, answers.name) : answers.directory;
809
- console.log(chalk.dim(" Package type:"), typeLabel);
810
- console.log(chalk.dim(" Target:"), packageDir);
811
- console.log(chalk.dim(" Name:"), config.name);
812
- console.log(chalk.dim(" Title:"), config.title);
813
- if (answers.projectType === "webapp") {
814
- console.log(chalk.dim(" Database:"), config.dbName);
815
- }
816
- console.log();
817
- if (await fs7.pathExists(packageDir)) {
818
- const files = await fs7.readdir(packageDir);
819
- if (files.length > 0) {
820
- console.error(
821
- chalk.red(`Error: Directory ${packageDir} is not empty.`)
822
- );
823
- process.exit(1);
824
- }
825
- }
826
- if (answers.projectType !== "monorepo") {
827
- await addPackageToMonorepo({
828
- packageDir,
829
- monorepoRoot,
830
- projectType: answers.projectType,
831
- config
832
- });
833
- }
834
- await warnIfMissingRootNpmrc(monorepoRoot);
835
- const installSucceeded = await installAtMonorepoRoot(
836
- monorepoRoot,
837
- answers.installDeps
838
- );
839
- console.log();
840
- console.log(
841
- chalk.green("\u2714"),
842
- chalk.bold(`Created ${typeLabel} at`),
843
- chalk.cyan(path7.relative(monorepoRoot, packageDir))
844
- );
845
- console.log();
846
- const devStarted = await maybeAutoRunWebapp(
847
- packageDir,
848
- answers.projectType,
849
- installSucceeded
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
- const cwd = cwdOption ? path7.resolve(cwdOption) : process.cwd();
922
- let stat;
923
- try {
924
- stat = await fs7.stat(cwd);
925
- } catch {
926
- console.error(chalk.red(`Error: --cwd directory does not exist: ${cwd}`));
927
- process.exit(1);
928
- }
929
- if (!stat.isDirectory()) {
930
- console.error(chalk.red(`Error: --cwd is not a directory: ${cwd}`));
931
- process.exit(1);
932
- }
933
- return cwd;
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
- const {
937
- pm,
938
- answers,
939
- variant,
940
- monorepoRelativePath,
941
- packageRelativePath
942
- } = params;
943
- const repoRel = shPath(monorepoRelativePath) || ".";
944
- const pkgFromRoot = `packages/${answers.name}`;
945
- const pnpmSteps = ["pnpm run setup", "pnpm dev"];
946
- if (variant === "new") {
947
- const oneLinerParts2 = [];
948
- if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
949
- if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
950
- oneLinerParts2.push(`cd ${pkgFromRoot}`);
951
- oneLinerParts2.push(...pnpmSteps);
952
- console.log(chalk.bold("Copy-paste (from your current directory):"));
953
- console.log();
954
- console.log(chalk.cyan(` ${oneLinerParts2.join(" && ")}`));
955
- console.log();
956
- console.log(chalk.bold("Or step by step:"));
957
- console.log();
958
- let step2 = 1;
959
- if (repoRel !== ".") {
960
- console.log(chalk.dim(` ${step2++}.`), `cd ${repoRel}`);
961
- }
962
- if (!answers.installDeps) {
963
- console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
964
- }
965
- console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
966
- for (const cmd of pnpmSteps) {
967
- console.log(chalk.dim(` ${step2++}.`), cmd);
968
- }
969
- return;
970
- }
971
- const pkgRel = shPath(packageRelativePath ?? ".") || ".";
972
- const oneLinerParts = [];
973
- if (!answers.installDeps) {
974
- if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
975
- oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
976
- } else if (pkgRel !== ".") {
977
- oneLinerParts.push(`cd ${pkgRel}`);
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
- const rootNpmrc = path7.join(rootDir, ".npmrc");
1002
- let contents = "";
1003
- if (await fs7.pathExists(rootNpmrc)) {
1004
- contents = await fs7.readFile(rootNpmrc, "utf8");
1005
- }
1006
- if (contents.includes("@percepta:registry")) {
1007
- return;
1008
- }
1009
- console.log();
1010
- console.log(
1011
- chalk.yellow("!"),
1012
- chalk.bold("Root .npmrc is missing @percepta registry config.")
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
- const pm = PACKAGE_MANAGER;
1031
- const relativePath = path7.relative(process.cwd(), targetDir) || ".";
1032
- console.log("Next steps:");
1033
- console.log();
1034
- switch (answers.projectType) {
1035
- case "monorepo": {
1036
- let step = 1;
1037
- const repoRel = shPath(relativePath);
1038
- if (repoRel !== ".") {
1039
- console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1040
- }
1041
- if (!answers.installDeps) {
1042
- console.log(chalk.dim(` ${step++}.`), `${pm} install`);
1043
- }
1044
- console.log(
1045
- chalk.dim(` ${step++}.`),
1046
- `Add a package: ${chalk.cyan(`npx @percepta/create --type webapp <name>`)}`
1047
- );
1048
- break;
1049
- }
1050
- case "webapp":
1051
- printWebappNextSteps({
1052
- pm,
1053
- answers,
1054
- variant: "new",
1055
- monorepoRelativePath: relativePath
1056
- });
1057
- break;
1058
- case "library": {
1059
- let step = 1;
1060
- const repoRel = shPath(relativePath);
1061
- if (repoRel !== ".") {
1062
- console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
1063
- }
1064
- if (!answers.installDeps) {
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
- const pm = PACKAGE_MANAGER;
1087
- const packageRelativePath = path7.relative(process.cwd(), packageDir) || ".";
1088
- const monorepoRoot = path7.dirname(path7.dirname(packageDir));
1089
- const monorepoRelativePath = path7.relative(process.cwd(), monorepoRoot) || ".";
1090
- console.log("Next steps:");
1091
- console.log();
1092
- switch (answers.projectType) {
1093
- case "webapp":
1094
- printWebappNextSteps({
1095
- pm,
1096
- answers,
1097
- variant: "existing",
1098
- monorepoRelativePath,
1099
- packageRelativePath
1100
- });
1101
- break;
1102
- case "library": {
1103
- let step = 1;
1104
- const pkgRel = shPath(packageRelativePath);
1105
- if (pkgRel !== ".") {
1106
- console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
1107
- }
1108
- console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
1109
- console.log(
1110
- chalk.dim(` ${step++}.`),
1111
- "Edit src/index.ts to add your library code"
1112
- );
1113
- break;
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
- // src/index.ts
1124
- var packageJson = {
1125
- name: "@percepta/create",
1126
- version: "1.0.0"
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
- "--mosaic-template-path <path>",
1132
- "Path to local mosaic repo checkout"
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 \u2192 app)").option(
1138
- "--mosaic-template-path <path>",
1139
- "Path to local mosaic repo checkout"
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 \u2192 template)").option(
1145
- "--mosaic-template-path <path>",
1146
- "Path to local mosaic repo checkout"
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
- const { initCommand } = await import("./init-EQZ2TCSJ.js");
1153
- await initCommand(options);
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