@percepta/create 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/chunk-GEVZERMP.js +108 -0
- package/dist/chunk-R4FWPE4A.js +49 -0
- package/dist/chunk-WMJT7CB5.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +974 -0
- package/dist/init-Z4VGBHAK.js +96 -0
- package/dist/status-MITGDLTT.js +76 -0
- package/dist/sync-J4SFZHDX.js +136 -0
- package/dist/upstream-AQI7P4EU.js +144 -0
- package/package.json +58 -0
- package/template-versions.json +4 -0
- package/templates/library/README.md +30 -0
- package/templates/library/eslint.config.js +10 -0
- package/templates/library/gitignore.template +18 -0
- package/templates/library/package.json.template +29 -0
- package/templates/library/src/index.ts +9 -0
- package/templates/library/tsconfig.json +19 -0
- package/templates/monorepo/README.md +41 -0
- package/templates/monorepo/eslint.config.js +10 -0
- package/templates/monorepo/gitignore.template +31 -0
- package/templates/monorepo/npmrc.template +4 -0
- package/templates/monorepo/package.json.template +25 -0
- package/templates/monorepo/packages/.gitkeep +0 -0
- package/templates/monorepo/pnpm-workspace.yaml +2 -0
- package/templates/monorepo/tsconfig.json +16 -0
- package/templates/webapp/.claude/commands/sync.md +19 -0
- package/templates/webapp/.claude/commands/upstream.md +17 -0
- package/templates/webapp/.dockerignore +59 -0
- package/templates/webapp/.gitattributes +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
- package/templates/webapp/.github/workflows/ci.yml +149 -0
- package/templates/webapp/.node-version +2 -0
- package/templates/webapp/.prettierrc.mjs +5 -0
- package/templates/webapp/AGENTS.md +240 -0
- package/templates/webapp/Dockerfile +64 -0
- package/templates/webapp/README.md +200 -0
- package/templates/webapp/agent-skills/database.md +140 -0
- package/templates/webapp/agent-skills/deploy.md +94 -0
- package/templates/webapp/agent-skills/inngest.md +147 -0
- package/templates/webapp/agent-skills/langfuse.md +117 -0
- package/templates/webapp/agent-skills/oneshot.md +216 -0
- package/templates/webapp/agent-skills/ryvn.md +25 -0
- package/templates/webapp/deploy/README.md +39 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
- package/templates/webapp/docker-compose.yml +19 -0
- package/templates/webapp/drizzle.config.ts +30 -0
- package/templates/webapp/env.example.template +44 -0
- package/templates/webapp/eslint.config.mjs +52 -0
- package/templates/webapp/gitignore.template +53 -0
- package/templates/webapp/next.config.ts +8 -0
- package/templates/webapp/npmrc.template +4 -0
- package/templates/webapp/package.json.template +122 -0
- package/templates/webapp/postcss.config.mjs +5 -0
- package/templates/webapp/scripts/create-user.ts +47 -0
- package/templates/webapp/scripts/migrate.ts +18 -0
- package/templates/webapp/scripts/seed.ts +62 -0
- package/templates/webapp/scripts/setup-database.ts +57 -0
- package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
- package/templates/webapp/scripts/start.sh +52 -0
- package/templates/webapp/src/app/(app)/layout.tsx +21 -0
- package/templates/webapp/src/app/(app)/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
- package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/webapp/src/app/api/healthz/route.ts +10 -0
- package/templates/webapp/src/app/api/inngest/route.ts +31 -0
- package/templates/webapp/src/app/api/readyz/route.ts +31 -0
- package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
- package/templates/webapp/src/app/favicon.ico +0 -0
- package/templates/webapp/src/app/global-error.tsx +27 -0
- package/templates/webapp/src/app/layout.tsx +18 -0
- package/templates/webapp/src/components/FaroProvider.tsx +37 -0
- package/templates/webapp/src/components/Header.tsx +70 -0
- package/templates/webapp/src/components/Providers.tsx +45 -0
- package/templates/webapp/src/components/form/FormItem.tsx +82 -0
- package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
- package/templates/webapp/src/config/getEnvConfig.ts +62 -0
- package/templates/webapp/src/config/isDev.ts +7 -0
- package/templates/webapp/src/drizzle/db.ts +28 -0
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
- package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
- package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
- package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
- package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
- package/templates/webapp/src/drizzle/schema/index.ts +4 -0
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
- package/templates/webapp/src/instrumentation.ts +35 -0
- package/templates/webapp/src/lib/auth/index.ts +85 -0
- package/templates/webapp/src/lib/auth-client.ts +6 -0
- package/templates/webapp/src/lib/trpc.ts +15 -0
- package/templates/webapp/src/server/api/root.ts +5 -0
- package/templates/webapp/src/server/trpc.ts +61 -0
- package/templates/webapp/src/services/AuthContextService.ts +63 -0
- package/templates/webapp/src/services/DatabaseService.ts +54 -0
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
- package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
- package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
- package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
- package/templates/webapp/src/services/observability/initFaro.ts +22 -0
- package/templates/webapp/src/startup-checks.ts +32 -0
- package/templates/webapp/src/styles/globals.css +27 -0
- package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
- package/templates/webapp/src/utils/cn.ts +6 -0
- package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
- package/templates/webapp/terraform/README.md +147 -0
- package/templates/webapp/terraform/deploy.sh +97 -0
- package/templates/webapp/terraform/main.tf +101 -0
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
- package/templates/webapp/terraform/modules/networking/main.tf +118 -0
- package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
- package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
- package/templates/webapp/terraform/modules/rds/main.tf +227 -0
- package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
- package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
- package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
- package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
- package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
- package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
- package/templates/webapp/terraform/outputs.tf +102 -0
- package/templates/webapp/terraform/providers.tf +32 -0
- package/templates/webapp/terraform/terraform.tfvars.example +65 -0
- package/templates/webapp/terraform/variables.tf +129 -0
- package/templates/webapp/tsconfig.json +14 -0
- package/templates/webapp/vitest.config.ts +9 -0
- package/templates/webapp/vitest.setup.ts +5 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
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-GEVZERMP.js";
|
|
11
|
+
import {
|
|
12
|
+
derivePlaceholders,
|
|
13
|
+
writeManifest
|
|
14
|
+
} from "./chunk-WMJT7CB5.js";
|
|
15
|
+
|
|
16
|
+
// src/index.ts
|
|
17
|
+
import { program } from "commander";
|
|
18
|
+
|
|
19
|
+
// src/commands/create.ts
|
|
20
|
+
import path4 from "path";
|
|
21
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
|
+
import { randomBytes } from "crypto";
|
|
23
|
+
import fs4 from "fs-extra";
|
|
24
|
+
import chalk from "chalk";
|
|
25
|
+
import ora from "ora";
|
|
26
|
+
import { execSync, spawn } from "child_process";
|
|
27
|
+
|
|
28
|
+
// src/utils/copy-template.ts
|
|
29
|
+
import path from "path";
|
|
30
|
+
import { fileURLToPath } from "url";
|
|
31
|
+
import fs from "fs-extra";
|
|
32
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
var __dirname = path.dirname(__filename);
|
|
34
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
35
|
+
"node_modules",
|
|
36
|
+
".git",
|
|
37
|
+
".next",
|
|
38
|
+
"dist",
|
|
39
|
+
".turbo",
|
|
40
|
+
".vercel",
|
|
41
|
+
".cursor"
|
|
42
|
+
]);
|
|
43
|
+
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
44
|
+
"pnpm-lock.yaml",
|
|
45
|
+
"package-lock.json",
|
|
46
|
+
"yarn.lock",
|
|
47
|
+
".DS_Store"
|
|
48
|
+
]);
|
|
49
|
+
var TEMPLATE_FILE_MAPPINGS = {
|
|
50
|
+
"package.json.template": "package.json",
|
|
51
|
+
"gitignore.template": ".gitignore",
|
|
52
|
+
"env.example.template": ".env.example",
|
|
53
|
+
"npmrc.template": ".npmrc"
|
|
54
|
+
};
|
|
55
|
+
function shouldSkip(src) {
|
|
56
|
+
const basename = path.basename(src);
|
|
57
|
+
if (SKIP_DIRS.has(basename)) return true;
|
|
58
|
+
if (SKIP_FILES.has(basename)) return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
function getTemplateDir(templateType) {
|
|
62
|
+
return path.resolve(__dirname, "../templates", templateType);
|
|
63
|
+
}
|
|
64
|
+
async function copyTemplate(targetDir, templateType) {
|
|
65
|
+
const templateDir = getTemplateDir(templateType);
|
|
66
|
+
if (!await fs.pathExists(templateDir)) {
|
|
67
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
68
|
+
}
|
|
69
|
+
await fs.ensureDir(targetDir);
|
|
70
|
+
await fs.copy(templateDir, targetDir, {
|
|
71
|
+
filter: (src) => !shouldSkip(src)
|
|
72
|
+
});
|
|
73
|
+
for (const [templateName, targetName] of Object.entries(
|
|
74
|
+
TEMPLATE_FILE_MAPPINGS
|
|
75
|
+
)) {
|
|
76
|
+
const templatePath = path.join(targetDir, templateName);
|
|
77
|
+
const targetPath = path.join(targetDir, targetName);
|
|
78
|
+
if (await fs.pathExists(templatePath)) {
|
|
79
|
+
await fs.move(templatePath, targetPath, { overwrite: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (templateType === "webapp") {
|
|
83
|
+
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
84
|
+
const claudePath = path.join(targetDir, "CLAUDE.md");
|
|
85
|
+
if (await fs.pathExists(agentsPath)) {
|
|
86
|
+
if (await fs.pathExists(claudePath)) {
|
|
87
|
+
await fs.remove(claudePath);
|
|
88
|
+
}
|
|
89
|
+
await fs.ensureSymlink("AGENTS.md", claudePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/utils/replace-placeholders.ts
|
|
95
|
+
import path2 from "path";
|
|
96
|
+
import fs2 from "fs-extra";
|
|
97
|
+
var PLACEHOLDERS = {
|
|
98
|
+
__APP_NAME__: "name",
|
|
99
|
+
__APP_TITLE__: "title",
|
|
100
|
+
__DB_NAME__: "dbName",
|
|
101
|
+
__APP_NAME_UPPER__: "nameUpper",
|
|
102
|
+
__APP_NAME_SNAKE__: "nameSnake"
|
|
103
|
+
};
|
|
104
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
105
|
+
"node_modules",
|
|
106
|
+
".git",
|
|
107
|
+
".next",
|
|
108
|
+
"dist",
|
|
109
|
+
".turbo",
|
|
110
|
+
".vercel"
|
|
111
|
+
]);
|
|
112
|
+
var SKIP_FILES2 = /* @__PURE__ */ new Set([
|
|
113
|
+
"pnpm-lock.yaml",
|
|
114
|
+
"package-lock.json",
|
|
115
|
+
"yarn.lock"
|
|
116
|
+
]);
|
|
117
|
+
var PROCESSABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
118
|
+
".ts",
|
|
119
|
+
".tsx",
|
|
120
|
+
".js",
|
|
121
|
+
".jsx",
|
|
122
|
+
".json",
|
|
123
|
+
".yml",
|
|
124
|
+
".yaml",
|
|
125
|
+
".md",
|
|
126
|
+
".env",
|
|
127
|
+
".sql",
|
|
128
|
+
".tf",
|
|
129
|
+
".tfvars",
|
|
130
|
+
".sh",
|
|
131
|
+
".mjs",
|
|
132
|
+
".cjs"
|
|
133
|
+
]);
|
|
134
|
+
var PROCESSABLE_FILENAMES = /* @__PURE__ */ new Set([
|
|
135
|
+
"Dockerfile",
|
|
136
|
+
".env.example",
|
|
137
|
+
".env.local",
|
|
138
|
+
"terraform.tfvars.example"
|
|
139
|
+
]);
|
|
140
|
+
function shouldProcessFile(filePath) {
|
|
141
|
+
const fileName = path2.basename(filePath);
|
|
142
|
+
const ext = path2.extname(filePath);
|
|
143
|
+
if (SKIP_FILES2.has(fileName)) return false;
|
|
144
|
+
if (PROCESSABLE_FILENAMES.has(fileName)) return true;
|
|
145
|
+
return PROCESSABLE_EXTENSIONS.has(ext);
|
|
146
|
+
}
|
|
147
|
+
async function replaceInFile(filePath, config) {
|
|
148
|
+
let content;
|
|
149
|
+
try {
|
|
150
|
+
content = await fs2.readFile(filePath, "utf-8");
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
let modified = false;
|
|
155
|
+
let newContent = content;
|
|
156
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort(
|
|
157
|
+
(a, b) => b[0].length - a[0].length
|
|
158
|
+
);
|
|
159
|
+
for (const [placeholder, configKey] of sortedEntries) {
|
|
160
|
+
const value = config[configKey];
|
|
161
|
+
if (newContent.includes(placeholder)) {
|
|
162
|
+
newContent = newContent.split(placeholder).join(value);
|
|
163
|
+
modified = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (modified) {
|
|
167
|
+
await fs2.writeFile(filePath, newContent);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function substituteName(name, config) {
|
|
173
|
+
const sortedEntries = Object.entries(PLACEHOLDERS).sort(
|
|
174
|
+
(a, b) => b[0].length - a[0].length
|
|
175
|
+
);
|
|
176
|
+
let result = name;
|
|
177
|
+
for (const [placeholder, configKey] of sortedEntries) {
|
|
178
|
+
if (result.includes(placeholder)) {
|
|
179
|
+
result = result.split(placeholder).join(
|
|
180
|
+
config[configKey]
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
async function processDirectory(dirPath, config, stats) {
|
|
187
|
+
const entries = await fs2.readdir(dirPath, { withFileTypes: true });
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
const fullPath = path2.join(dirPath, entry.name);
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
192
|
+
await processDirectory(fullPath, config, stats);
|
|
193
|
+
}
|
|
194
|
+
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
|
195
|
+
stats.processed++;
|
|
196
|
+
if (await replaceInFile(fullPath, config)) {
|
|
197
|
+
stats.modified++;
|
|
198
|
+
}
|
|
199
|
+
const renamed = substituteName(entry.name, config);
|
|
200
|
+
if (renamed !== entry.name) {
|
|
201
|
+
await fs2.move(fullPath, path2.join(dirPath, renamed), {
|
|
202
|
+
overwrite: true
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function replacePlaceholders(targetDir, config) {
|
|
209
|
+
const stats = { processed: 0, modified: 0 };
|
|
210
|
+
await processDirectory(targetDir, config, stats);
|
|
211
|
+
return stats;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/utils/detect-monorepo.ts
|
|
215
|
+
import path3 from "path";
|
|
216
|
+
import fs3 from "fs-extra";
|
|
217
|
+
import { parse } from "yaml";
|
|
218
|
+
var NOT_FOUND = {
|
|
219
|
+
found: false,
|
|
220
|
+
rootDir: null,
|
|
221
|
+
workspacePatterns: [],
|
|
222
|
+
packageDir: null
|
|
223
|
+
};
|
|
224
|
+
async function detectMonorepo(startDir) {
|
|
225
|
+
let current = path3.resolve(startDir);
|
|
226
|
+
const root = path3.parse(current).root;
|
|
227
|
+
while (current !== root) {
|
|
228
|
+
const workspaceFile = path3.join(current, "pnpm-workspace.yaml");
|
|
229
|
+
if (await fs3.pathExists(workspaceFile)) {
|
|
230
|
+
try {
|
|
231
|
+
const content = await fs3.readFile(workspaceFile, "utf-8");
|
|
232
|
+
const parsed = parse(content);
|
|
233
|
+
if (!parsed?.packages || !Array.isArray(parsed.packages)) {
|
|
234
|
+
return NOT_FOUND;
|
|
235
|
+
}
|
|
236
|
+
const workspacePatterns = parsed.packages;
|
|
237
|
+
const firstPattern = workspacePatterns[0];
|
|
238
|
+
if (!firstPattern) {
|
|
239
|
+
return NOT_FOUND;
|
|
240
|
+
}
|
|
241
|
+
const baseDir = firstPattern.replace(/\/?\*.*$/, "").trim();
|
|
242
|
+
const packageDir = path3.join(current, baseDir);
|
|
243
|
+
return {
|
|
244
|
+
found: true,
|
|
245
|
+
rootDir: current,
|
|
246
|
+
workspacePatterns,
|
|
247
|
+
packageDir
|
|
248
|
+
};
|
|
249
|
+
} catch {
|
|
250
|
+
return NOT_FOUND;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
current = path3.dirname(current);
|
|
254
|
+
}
|
|
255
|
+
return NOT_FOUND;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/commands/create.ts
|
|
259
|
+
var PACKAGE_MANAGER = "pnpm";
|
|
260
|
+
function shPath(p) {
|
|
261
|
+
return p.split(path4.sep).join("/");
|
|
262
|
+
}
|
|
263
|
+
function runPackageManagerInstall(packageManager, cwd) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const child = spawn(packageManager, ["install"], {
|
|
266
|
+
cwd,
|
|
267
|
+
stdio: "ignore"
|
|
268
|
+
});
|
|
269
|
+
child.on("error", reject);
|
|
270
|
+
child.on("close", (code) => {
|
|
271
|
+
if (code === 0) resolve();
|
|
272
|
+
else
|
|
273
|
+
reject(
|
|
274
|
+
new Error(
|
|
275
|
+
`${packageManager} install exited with code ${code ?? "unknown"}`
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function runWebappSetup(packageManager, cwd) {
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
const child = spawn(packageManager, ["run", "setup"], { cwd, stdio: "inherit" });
|
|
284
|
+
child.on("error", reject);
|
|
285
|
+
child.on("close", (code) => {
|
|
286
|
+
if (code === 0) resolve();
|
|
287
|
+
else reject(new Error(`${packageManager} run setup exited with code ${code ?? "unknown"}`));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
var ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
292
|
+
function spawnDevServer(packageManager, cwd) {
|
|
293
|
+
const child = spawn(packageManager, ["run", "dev"], {
|
|
294
|
+
cwd,
|
|
295
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
296
|
+
});
|
|
297
|
+
const ready = new Promise((resolve, reject) => {
|
|
298
|
+
let resolved = false;
|
|
299
|
+
let detectedUrl = "http://localhost:3000";
|
|
300
|
+
let buffer = "";
|
|
301
|
+
const onChunk = (chunk) => {
|
|
302
|
+
const text = chunk.toString();
|
|
303
|
+
process.stdout.write(text);
|
|
304
|
+
buffer = (buffer + text).slice(-4096).replace(ANSI_PATTERN, "");
|
|
305
|
+
const urlMatch = buffer.match(/Local:\s+(https?:\/\/\S+?)(?:\s|$)/i);
|
|
306
|
+
if (urlMatch?.[1]) detectedUrl = urlMatch[1].trim();
|
|
307
|
+
if (!resolved && /Ready in /i.test(buffer)) {
|
|
308
|
+
resolved = true;
|
|
309
|
+
resolve({ url: detectedUrl });
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
child.stdout?.on("data", onChunk);
|
|
313
|
+
child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
314
|
+
child.on("error", reject);
|
|
315
|
+
child.on("close", (code) => {
|
|
316
|
+
if (!resolved) {
|
|
317
|
+
reject(new Error(`${packageManager} run dev exited with code ${code ?? "unknown"} before becoming ready`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
return { child, ready };
|
|
322
|
+
}
|
|
323
|
+
function openInBrowser(url) {
|
|
324
|
+
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
325
|
+
try {
|
|
326
|
+
const child = spawn(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true });
|
|
327
|
+
child.on("error", () => {
|
|
328
|
+
});
|
|
329
|
+
child.unref();
|
|
330
|
+
return true;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function autoRunWebapp(packageDir) {
|
|
336
|
+
const packageManager = PACKAGE_MANAGER;
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(chalk.bold("Running setup (docker, db, seed)..."));
|
|
339
|
+
console.log();
|
|
340
|
+
try {
|
|
341
|
+
await runWebappSetup(packageManager, packageDir);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.log();
|
|
344
|
+
console.log(
|
|
345
|
+
chalk.yellow("!"),
|
|
346
|
+
"Setup failed. You can re-run it manually:",
|
|
347
|
+
chalk.cyan(`cd ${packageDir} && ${packageManager} run setup`)
|
|
348
|
+
);
|
|
349
|
+
console.log(chalk.dim(error.message));
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk.bold("Starting dev server..."));
|
|
354
|
+
console.log();
|
|
355
|
+
const { child, ready } = spawnDevServer(packageManager, packageDir);
|
|
356
|
+
const closed = new Promise((resolve) => {
|
|
357
|
+
child.on("close", () => resolve());
|
|
358
|
+
});
|
|
359
|
+
let url = "http://localhost:3000";
|
|
360
|
+
try {
|
|
361
|
+
({ url } = await ready);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(chalk.yellow("!"), "Dev server failed to become ready.");
|
|
365
|
+
console.log(chalk.dim(error.message));
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (openInBrowser(url)) {
|
|
369
|
+
console.log();
|
|
370
|
+
console.log(chalk.green("\u2714"), "Opened", chalk.cyan(url));
|
|
371
|
+
} else {
|
|
372
|
+
console.log();
|
|
373
|
+
console.log(chalk.dim("Open"), chalk.cyan(url), chalk.dim("in your browser."));
|
|
374
|
+
}
|
|
375
|
+
await closed;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
function readTemplateVersions() {
|
|
379
|
+
const versionsPath = path4.resolve(
|
|
380
|
+
path4.dirname(fileURLToPath2(import.meta.url)),
|
|
381
|
+
"../template-versions.json"
|
|
382
|
+
);
|
|
383
|
+
try {
|
|
384
|
+
const content = fs4.readFileSync(versionsPath, "utf-8");
|
|
385
|
+
return JSON.parse(content);
|
|
386
|
+
} catch {
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function generateEnvLocal(packageDir) {
|
|
391
|
+
const examplePath = path4.join(packageDir, ".env.example");
|
|
392
|
+
const localPath = path4.join(packageDir, ".env.local");
|
|
393
|
+
if (!await fs4.pathExists(examplePath)) return;
|
|
394
|
+
if (await fs4.pathExists(localPath)) return;
|
|
395
|
+
const authSecret = randomBytes(32).toString("base64");
|
|
396
|
+
const encKey = randomBytes(16).toString("hex");
|
|
397
|
+
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}`);
|
|
398
|
+
await fs4.writeFile(localPath, content);
|
|
399
|
+
}
|
|
400
|
+
async function relocateWorkflowsToRoot(packageDir, monorepoRoot, appName) {
|
|
401
|
+
const sourceDir = path4.join(packageDir, ".github", "workflows");
|
|
402
|
+
if (!await fs4.pathExists(sourceDir)) return;
|
|
403
|
+
const targetDir = path4.join(monorepoRoot, ".github", "workflows");
|
|
404
|
+
await fs4.ensureDir(targetDir);
|
|
405
|
+
const entries = await fs4.readdir(sourceDir);
|
|
406
|
+
for (const name of entries) {
|
|
407
|
+
if (!name.startsWith(`${appName}-`)) continue;
|
|
408
|
+
if (!/\.(ya?ml)$/.test(name)) continue;
|
|
409
|
+
await fs4.move(path4.join(sourceDir, name), path4.join(targetDir, name), {
|
|
410
|
+
overwrite: true
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if ((await fs4.readdir(sourceDir)).length === 0) {
|
|
414
|
+
await fs4.rmdir(sourceDir);
|
|
415
|
+
const packageGithub = path4.join(packageDir, ".github");
|
|
416
|
+
if ((await fs4.readdir(packageGithub)).length === 0) {
|
|
417
|
+
await fs4.rmdir(packageGithub);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function writeMosaicFiles(packageDir, config, projectType) {
|
|
422
|
+
const templateVersions = readTemplateVersions();
|
|
423
|
+
const manifest = {
|
|
424
|
+
templateType: projectType,
|
|
425
|
+
templateVersion: templateVersions[projectType] || "1.0.0",
|
|
426
|
+
templateCommit: "npm",
|
|
427
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
428
|
+
placeholders: derivePlaceholders(config.name, config.title),
|
|
429
|
+
source: {
|
|
430
|
+
templatePath: `packages/create-mosaic-module/templates/${projectType}`
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
await writeManifest(packageDir, manifest);
|
|
434
|
+
const notesPath = path4.join(packageDir, "mosaic-template-notes.md");
|
|
435
|
+
await fs4.writeFile(
|
|
436
|
+
notesPath,
|
|
437
|
+
`# Mosaic Divergence Notes
|
|
438
|
+
|
|
439
|
+
Document intentional differences from the ${projectType} template here.
|
|
440
|
+
Claude reads this file during sync to preserve your customizations.
|
|
441
|
+
|
|
442
|
+
## Intentional Divergences
|
|
443
|
+
|
|
444
|
+
_None yet \u2014 freshly created from template._
|
|
445
|
+
`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
function getProjectTypeLabel(projectType) {
|
|
449
|
+
switch (projectType) {
|
|
450
|
+
case "monorepo":
|
|
451
|
+
return "pnpm monorepo";
|
|
452
|
+
case "webapp":
|
|
453
|
+
return "Next.js webapp";
|
|
454
|
+
case "library":
|
|
455
|
+
return "TypeScript library";
|
|
456
|
+
default: {
|
|
457
|
+
const exhaustiveCheck = projectType;
|
|
458
|
+
throw new Error(`Unknown project type: ${exhaustiveCheck}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function createProject(options) {
|
|
463
|
+
if (options.type !== void 0 && !isValidProjectType(options.type)) {
|
|
464
|
+
console.error(
|
|
465
|
+
chalk.red(
|
|
466
|
+
`Error: Invalid package type "${options.type}". Valid types are: ${VALID_PROJECT_TYPES.join(", ")}`
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
const cwd = process.cwd();
|
|
472
|
+
console.log();
|
|
473
|
+
console.log(chalk.bold("Creating a new Mosaic package..."));
|
|
474
|
+
console.log();
|
|
475
|
+
const monorepoContext = await detectMonorepo(cwd);
|
|
476
|
+
if (options.type === "monorepo" && monorepoContext.found) {
|
|
477
|
+
console.error(
|
|
478
|
+
chalk.red(
|
|
479
|
+
`Error: Already inside a monorepo at ${monorepoContext.rootDir}. Choose 'webapp' or 'library' to add a package, or run from outside the monorepo.`
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
if (monorepoContext.found) {
|
|
485
|
+
console.log(
|
|
486
|
+
chalk.dim(" Detected monorepo at"),
|
|
487
|
+
chalk.cyan(monorepoContext.rootDir)
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
console.log(
|
|
491
|
+
chalk.dim(
|
|
492
|
+
" No monorepo detected. A new monorepo will be created."
|
|
493
|
+
)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
console.log();
|
|
497
|
+
const projectName = options.name;
|
|
498
|
+
if (options.yes && !projectName) {
|
|
499
|
+
console.error(
|
|
500
|
+
chalk.red("Error: --name is required when using --yes flag")
|
|
501
|
+
);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
if (projectName) {
|
|
505
|
+
const validation = validateProjectName(toKebabCase(projectName));
|
|
506
|
+
if (!validation.valid) {
|
|
507
|
+
console.error(chalk.red(`Invalid project name: ${validation.error}`));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
let answers;
|
|
512
|
+
if (options.yes) {
|
|
513
|
+
const projectType = options.type || "webapp";
|
|
514
|
+
const kebabName = toKebabCase(projectName);
|
|
515
|
+
const directory = monorepoContext.found && monorepoContext.packageDir ? path4.join(monorepoContext.packageDir, kebabName) : path4.resolve(cwd, kebabName);
|
|
516
|
+
answers = {
|
|
517
|
+
projectType,
|
|
518
|
+
directory,
|
|
519
|
+
name: kebabName,
|
|
520
|
+
title: toTitleCase(kebabName),
|
|
521
|
+
installDeps: !options.skipInstall
|
|
522
|
+
};
|
|
523
|
+
} else {
|
|
524
|
+
answers = await promptProjectDetails({
|
|
525
|
+
projectType: options.type,
|
|
526
|
+
name: projectName ? toKebabCase(projectName) : void 0,
|
|
527
|
+
skipInstall: options.skipInstall,
|
|
528
|
+
monorepoContext
|
|
529
|
+
});
|
|
530
|
+
if (monorepoContext.found && monorepoContext.packageDir && !answers.directory) {
|
|
531
|
+
answers.directory = path4.join(monorepoContext.packageDir, answers.name);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const config = {
|
|
535
|
+
name: answers.name,
|
|
536
|
+
title: answers.title,
|
|
537
|
+
dbName: toSnakeCase(answers.name) + "_db",
|
|
538
|
+
nameUpper: answers.name.toUpperCase(),
|
|
539
|
+
nameSnake: toSnakeCase(answers.name)
|
|
540
|
+
};
|
|
541
|
+
const typeLabel = getProjectTypeLabel(answers.projectType);
|
|
542
|
+
if (monorepoContext.found) {
|
|
543
|
+
const packageDir = monorepoContext.packageDir ? path4.join(monorepoContext.packageDir, answers.name) : answers.directory;
|
|
544
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
545
|
+
console.log(chalk.dim(" Target:"), packageDir);
|
|
546
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
547
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
548
|
+
if (answers.projectType === "webapp") {
|
|
549
|
+
console.log(chalk.dim(" Database:"), config.dbName);
|
|
550
|
+
}
|
|
551
|
+
console.log();
|
|
552
|
+
if (await fs4.pathExists(packageDir)) {
|
|
553
|
+
const files = await fs4.readdir(packageDir);
|
|
554
|
+
if (files.length > 0) {
|
|
555
|
+
console.error(
|
|
556
|
+
chalk.red(`Error: Directory ${packageDir} is not empty.`)
|
|
557
|
+
);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const copySpinner = ora("Copying template files...").start();
|
|
562
|
+
try {
|
|
563
|
+
await copyTemplate(packageDir, answers.projectType);
|
|
564
|
+
copySpinner.succeed("Copied template files");
|
|
565
|
+
} catch (error) {
|
|
566
|
+
copySpinner.fail("Failed to copy template files");
|
|
567
|
+
console.error(error);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
const replaceSpinner = ora("Replacing placeholders...").start();
|
|
571
|
+
try {
|
|
572
|
+
const stats = await replacePlaceholders(packageDir, config);
|
|
573
|
+
replaceSpinner.succeed(
|
|
574
|
+
`Replaced placeholders in ${stats.modified} files`
|
|
575
|
+
);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
replaceSpinner.fail("Failed to replace placeholders");
|
|
578
|
+
console.error(error);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
await writeMosaicFiles(packageDir, config, answers.projectType);
|
|
582
|
+
if (answers.projectType === "webapp") {
|
|
583
|
+
await generateEnvLocal(packageDir);
|
|
584
|
+
}
|
|
585
|
+
if (answers.projectType === "webapp") {
|
|
586
|
+
await relocateWorkflowsToRoot(
|
|
587
|
+
packageDir,
|
|
588
|
+
monorepoContext.rootDir,
|
|
589
|
+
config.name
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
await warnIfMissingRootNpmrc(monorepoContext.rootDir);
|
|
593
|
+
let installSucceeded = false;
|
|
594
|
+
if (answers.installDeps) {
|
|
595
|
+
const packageManager = PACKAGE_MANAGER;
|
|
596
|
+
const installSpinner = ora(
|
|
597
|
+
`Installing dependencies with ${packageManager}...`
|
|
598
|
+
).start();
|
|
599
|
+
try {
|
|
600
|
+
await runPackageManagerInstall(
|
|
601
|
+
packageManager,
|
|
602
|
+
monorepoContext.rootDir
|
|
603
|
+
);
|
|
604
|
+
installSpinner.succeed("Installed dependencies");
|
|
605
|
+
installSucceeded = true;
|
|
606
|
+
} catch {
|
|
607
|
+
installSpinner.warn(
|
|
608
|
+
`Failed to install dependencies. Run '${packageManager} install' from monorepo root.`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
console.log(
|
|
614
|
+
chalk.green("\u2714"),
|
|
615
|
+
chalk.bold(`Created ${typeLabel} at`),
|
|
616
|
+
chalk.cyan(path4.relative(monorepoContext.rootDir, packageDir))
|
|
617
|
+
);
|
|
618
|
+
console.log();
|
|
619
|
+
if (answers.projectType === "webapp" && installSucceeded) {
|
|
620
|
+
const devStarted = await autoRunWebapp(packageDir);
|
|
621
|
+
if (devStarted) return;
|
|
622
|
+
}
|
|
623
|
+
printNextStepsExisting(answers, options, packageDir);
|
|
624
|
+
} else {
|
|
625
|
+
const isBareMonorepo = answers.projectType === "monorepo";
|
|
626
|
+
const targetDir = answers.directory;
|
|
627
|
+
const packageDir = isBareMonorepo ? null : path4.join(targetDir, "packages", answers.name);
|
|
628
|
+
if (isBareMonorepo) {
|
|
629
|
+
console.log(chalk.dim(" Type:"), typeLabel);
|
|
630
|
+
console.log(chalk.dim(" Directory:"), targetDir);
|
|
631
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
632
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
633
|
+
} else {
|
|
634
|
+
console.log(chalk.dim(" Package type:"), typeLabel);
|
|
635
|
+
console.log(chalk.dim(" Monorepo directory:"), targetDir);
|
|
636
|
+
console.log(chalk.dim(" Package:"), `packages/${answers.name}/`);
|
|
637
|
+
console.log(chalk.dim(" Name:"), config.name);
|
|
638
|
+
console.log(chalk.dim(" Title:"), config.title);
|
|
639
|
+
if (answers.projectType === "webapp") {
|
|
640
|
+
console.log(chalk.dim(" Database:"), config.dbName);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
console.log();
|
|
644
|
+
if (await fs4.pathExists(targetDir)) {
|
|
645
|
+
const files = await fs4.readdir(targetDir);
|
|
646
|
+
if (files.length > 0) {
|
|
647
|
+
console.error(
|
|
648
|
+
chalk.red(`Error: Directory ${targetDir} is not empty.`)
|
|
649
|
+
);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const monoSpinner = ora("Copying monorepo template...").start();
|
|
654
|
+
try {
|
|
655
|
+
await copyTemplate(targetDir, "monorepo");
|
|
656
|
+
monoSpinner.succeed("Copied monorepo template");
|
|
657
|
+
} catch (error) {
|
|
658
|
+
monoSpinner.fail("Failed to copy monorepo template");
|
|
659
|
+
console.error(error);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
const monoReplaceSpinner = ora(
|
|
663
|
+
"Replacing monorepo placeholders..."
|
|
664
|
+
).start();
|
|
665
|
+
try {
|
|
666
|
+
const stats = await replacePlaceholders(targetDir, config);
|
|
667
|
+
monoReplaceSpinner.succeed(
|
|
668
|
+
`Replaced placeholders in ${stats.modified} monorepo files`
|
|
669
|
+
);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
monoReplaceSpinner.fail("Failed to replace monorepo placeholders");
|
|
672
|
+
console.error(error);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
if (packageDir) {
|
|
676
|
+
const pkgSpinner = ora("Copying package template...").start();
|
|
677
|
+
try {
|
|
678
|
+
await copyTemplate(packageDir, answers.projectType);
|
|
679
|
+
pkgSpinner.succeed("Copied package template");
|
|
680
|
+
} catch (error) {
|
|
681
|
+
pkgSpinner.fail("Failed to copy package template");
|
|
682
|
+
console.error(error);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
const pkgReplaceSpinner = ora(
|
|
686
|
+
"Replacing package placeholders..."
|
|
687
|
+
).start();
|
|
688
|
+
try {
|
|
689
|
+
const stats = await replacePlaceholders(packageDir, config);
|
|
690
|
+
pkgReplaceSpinner.succeed(
|
|
691
|
+
`Replaced placeholders in ${stats.modified} package files`
|
|
692
|
+
);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
pkgReplaceSpinner.fail("Failed to replace package placeholders");
|
|
695
|
+
console.error(error);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
await writeMosaicFiles(packageDir, config, answers.projectType);
|
|
699
|
+
if (answers.projectType === "webapp") {
|
|
700
|
+
await generateEnvLocal(packageDir);
|
|
701
|
+
}
|
|
702
|
+
if (answers.projectType === "webapp") {
|
|
703
|
+
await relocateWorkflowsToRoot(packageDir, targetDir, config.name);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const gitSpinner = ora("Initializing git repository...").start();
|
|
707
|
+
try {
|
|
708
|
+
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
709
|
+
execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
710
|
+
execSync('git commit -m "Initial commit from @percepta/create"', {
|
|
711
|
+
cwd: targetDir,
|
|
712
|
+
stdio: "ignore"
|
|
713
|
+
});
|
|
714
|
+
gitSpinner.succeed("Initialized git repository");
|
|
715
|
+
} catch {
|
|
716
|
+
gitSpinner.warn("Failed to initialize git repository");
|
|
717
|
+
}
|
|
718
|
+
let installSucceeded = false;
|
|
719
|
+
if (answers.installDeps) {
|
|
720
|
+
const packageManager = PACKAGE_MANAGER;
|
|
721
|
+
const installSpinner = ora(
|
|
722
|
+
`Installing dependencies with ${packageManager}...`
|
|
723
|
+
).start();
|
|
724
|
+
try {
|
|
725
|
+
await runPackageManagerInstall(packageManager, targetDir);
|
|
726
|
+
installSpinner.succeed("Installed dependencies");
|
|
727
|
+
installSucceeded = true;
|
|
728
|
+
} catch {
|
|
729
|
+
installSpinner.warn(
|
|
730
|
+
`Failed to install dependencies. Run '${packageManager} install' manually.`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
console.log();
|
|
735
|
+
console.log(
|
|
736
|
+
chalk.green("\u2714"),
|
|
737
|
+
chalk.bold(isBareMonorepo ? `Created ${typeLabel} at` : "Created monorepo at"),
|
|
738
|
+
chalk.cyan(targetDir)
|
|
739
|
+
);
|
|
740
|
+
if (!isBareMonorepo) {
|
|
741
|
+
console.log(
|
|
742
|
+
chalk.green("\u2714"),
|
|
743
|
+
chalk.bold(`Created ${typeLabel} at`),
|
|
744
|
+
chalk.cyan(`packages/${answers.name}/`)
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
console.log();
|
|
748
|
+
if (packageDir && answers.projectType === "webapp" && installSucceeded) {
|
|
749
|
+
const devStarted = await autoRunWebapp(packageDir);
|
|
750
|
+
if (devStarted) return;
|
|
751
|
+
}
|
|
752
|
+
printNextStepsNew(answers, options, targetDir);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function printWebappNextSteps(params) {
|
|
756
|
+
const {
|
|
757
|
+
pm,
|
|
758
|
+
answers,
|
|
759
|
+
variant,
|
|
760
|
+
monorepoRelativePath,
|
|
761
|
+
packageRelativePath
|
|
762
|
+
} = params;
|
|
763
|
+
const repoRel = shPath(monorepoRelativePath) || ".";
|
|
764
|
+
const pkgFromRoot = `packages/${answers.name}`;
|
|
765
|
+
const pnpmSteps = ["pnpm run setup", "pnpm dev"];
|
|
766
|
+
if (variant === "new") {
|
|
767
|
+
const oneLinerParts2 = [];
|
|
768
|
+
if (repoRel !== ".") oneLinerParts2.push(`cd ${repoRel}`);
|
|
769
|
+
if (!answers.installDeps) oneLinerParts2.push(`${pm} install`);
|
|
770
|
+
oneLinerParts2.push(`cd ${pkgFromRoot}`, ...pnpmSteps);
|
|
771
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
772
|
+
console.log();
|
|
773
|
+
console.log(chalk.cyan(` ${oneLinerParts2.join(" && ")}`));
|
|
774
|
+
console.log();
|
|
775
|
+
console.log(chalk.bold("Or step by step:"));
|
|
776
|
+
console.log();
|
|
777
|
+
let step2 = 1;
|
|
778
|
+
if (repoRel !== ".") {
|
|
779
|
+
console.log(chalk.dim(` ${step2++}.`), `cd ${repoRel}`);
|
|
780
|
+
}
|
|
781
|
+
if (!answers.installDeps) {
|
|
782
|
+
console.log(chalk.dim(` ${step2++}.`), `${pm} install`);
|
|
783
|
+
}
|
|
784
|
+
console.log(chalk.dim(` ${step2++}.`), `cd ${pkgFromRoot}`);
|
|
785
|
+
for (const cmd of pnpmSteps) {
|
|
786
|
+
console.log(chalk.dim(` ${step2++}.`), cmd);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const pkgRel = shPath(packageRelativePath ?? ".") || ".";
|
|
791
|
+
const oneLinerParts = [];
|
|
792
|
+
if (!answers.installDeps) {
|
|
793
|
+
if (repoRel !== ".") oneLinerParts.push(`cd ${repoRel}`);
|
|
794
|
+
oneLinerParts.push(`${pm} install`, `cd ${pkgFromRoot}`);
|
|
795
|
+
} else if (pkgRel !== ".") {
|
|
796
|
+
oneLinerParts.push(`cd ${pkgRel}`);
|
|
797
|
+
}
|
|
798
|
+
oneLinerParts.push(...pnpmSteps);
|
|
799
|
+
console.log(chalk.bold("Copy-paste (from your current directory):"));
|
|
800
|
+
console.log();
|
|
801
|
+
console.log(chalk.cyan(` ${oneLinerParts.join(" && ")}`));
|
|
802
|
+
console.log();
|
|
803
|
+
console.log(chalk.bold("Or step by step:"));
|
|
804
|
+
console.log();
|
|
805
|
+
let step = 1;
|
|
806
|
+
if (!answers.installDeps) {
|
|
807
|
+
if (repoRel !== ".") {
|
|
808
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
809
|
+
}
|
|
810
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
811
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgFromRoot}`);
|
|
812
|
+
} else if (pkgRel !== ".") {
|
|
813
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
814
|
+
}
|
|
815
|
+
for (const cmd of pnpmSteps) {
|
|
816
|
+
console.log(chalk.dim(` ${step++}.`), cmd);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function warnIfMissingRootNpmrc(rootDir) {
|
|
820
|
+
const rootNpmrc = path4.join(rootDir, ".npmrc");
|
|
821
|
+
let contents = "";
|
|
822
|
+
if (await fs4.pathExists(rootNpmrc)) {
|
|
823
|
+
contents = await fs4.readFile(rootNpmrc, "utf8");
|
|
824
|
+
}
|
|
825
|
+
if (contents.includes("@percepta:registry")) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log();
|
|
829
|
+
console.log(
|
|
830
|
+
chalk.yellow("!"),
|
|
831
|
+
chalk.bold("Root .npmrc is missing @percepta registry config.")
|
|
832
|
+
);
|
|
833
|
+
console.log(
|
|
834
|
+
chalk.dim(
|
|
835
|
+
" pnpm reads .npmrc from the workspace root, so add these lines to"
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
console.log(chalk.dim(` ${path4.join(rootDir, ".npmrc")}:`));
|
|
839
|
+
console.log();
|
|
840
|
+
console.log(
|
|
841
|
+
chalk.cyan(" @percepta:registry=https://registry.npmjs.org/")
|
|
842
|
+
);
|
|
843
|
+
console.log(
|
|
844
|
+
chalk.cyan(" //registry.npmjs.org/:_authToken=${NPM_TOKEN}")
|
|
845
|
+
);
|
|
846
|
+
console.log();
|
|
847
|
+
}
|
|
848
|
+
function printNextStepsNew(answers, options, targetDir) {
|
|
849
|
+
const pm = PACKAGE_MANAGER;
|
|
850
|
+
const relativePath = path4.relative(process.cwd(), targetDir) || ".";
|
|
851
|
+
console.log("Next steps:");
|
|
852
|
+
console.log();
|
|
853
|
+
switch (answers.projectType) {
|
|
854
|
+
case "monorepo": {
|
|
855
|
+
let step = 1;
|
|
856
|
+
const repoRel = shPath(relativePath);
|
|
857
|
+
if (repoRel !== ".") {
|
|
858
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
859
|
+
}
|
|
860
|
+
if (!answers.installDeps) {
|
|
861
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
862
|
+
}
|
|
863
|
+
console.log(
|
|
864
|
+
chalk.dim(` ${step++}.`),
|
|
865
|
+
`Add a package: ${chalk.cyan(`npx @percepta/create --type webapp <name>`)}`
|
|
866
|
+
);
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
case "webapp":
|
|
870
|
+
printWebappNextSteps({
|
|
871
|
+
pm,
|
|
872
|
+
answers,
|
|
873
|
+
variant: "new",
|
|
874
|
+
monorepoRelativePath: relativePath
|
|
875
|
+
});
|
|
876
|
+
break;
|
|
877
|
+
case "library": {
|
|
878
|
+
let step = 1;
|
|
879
|
+
const repoRel = shPath(relativePath);
|
|
880
|
+
if (repoRel !== ".") {
|
|
881
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${repoRel}`);
|
|
882
|
+
}
|
|
883
|
+
if (!answers.installDeps) {
|
|
884
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} install`);
|
|
885
|
+
}
|
|
886
|
+
console.log(
|
|
887
|
+
chalk.dim(` ${step++}.`),
|
|
888
|
+
`cd packages/${answers.name}`
|
|
889
|
+
);
|
|
890
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
891
|
+
console.log(
|
|
892
|
+
chalk.dim(` ${step++}.`),
|
|
893
|
+
`Edit src/index.ts to add your library code`
|
|
894
|
+
);
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
console.log();
|
|
899
|
+
console.log(
|
|
900
|
+
chalk.dim("For more information, see the README.md in your project.")
|
|
901
|
+
);
|
|
902
|
+
console.log();
|
|
903
|
+
}
|
|
904
|
+
function printNextStepsExisting(answers, options, packageDir) {
|
|
905
|
+
const pm = PACKAGE_MANAGER;
|
|
906
|
+
const packageRelativePath = path4.relative(process.cwd(), packageDir) || ".";
|
|
907
|
+
const monorepoRoot = path4.dirname(path4.dirname(packageDir));
|
|
908
|
+
const monorepoRelativePath = path4.relative(process.cwd(), monorepoRoot) || ".";
|
|
909
|
+
console.log("Next steps:");
|
|
910
|
+
console.log();
|
|
911
|
+
switch (answers.projectType) {
|
|
912
|
+
case "webapp":
|
|
913
|
+
printWebappNextSteps({
|
|
914
|
+
pm,
|
|
915
|
+
answers,
|
|
916
|
+
variant: "existing",
|
|
917
|
+
monorepoRelativePath,
|
|
918
|
+
packageRelativePath
|
|
919
|
+
});
|
|
920
|
+
break;
|
|
921
|
+
case "library": {
|
|
922
|
+
let step = 1;
|
|
923
|
+
const pkgRel = shPath(packageRelativePath);
|
|
924
|
+
if (pkgRel !== ".") {
|
|
925
|
+
console.log(chalk.dim(` ${step++}.`), `cd ${pkgRel}`);
|
|
926
|
+
}
|
|
927
|
+
console.log(chalk.dim(` ${step++}.`), `${pm} dev`);
|
|
928
|
+
console.log(
|
|
929
|
+
chalk.dim(` ${step++}.`),
|
|
930
|
+
"Edit src/index.ts to add your library code"
|
|
931
|
+
);
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
console.log();
|
|
936
|
+
console.log(
|
|
937
|
+
chalk.dim("For more information, see the README.md in your project.")
|
|
938
|
+
);
|
|
939
|
+
console.log();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/index.ts
|
|
943
|
+
var packageJson = {
|
|
944
|
+
name: "@percepta/create",
|
|
945
|
+
version: "1.0.0"
|
|
946
|
+
};
|
|
947
|
+
program.name("create").description("Scaffold and manage Mosaic packages").version(packageJson.version);
|
|
948
|
+
program.command("create", { isDefault: true }).description("Scaffold a new Mosaic package").option("-t, --type <type>", "Package type: monorepo, webapp, or library").option("--name <name>", "Project name").option("--skip-install", "Skip dependency installation (also skips the auto-run setup + dev + browser)", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(createProject);
|
|
949
|
+
program.command("status").description("Show template sync status for current app").option(
|
|
950
|
+
"--mosaic-template-path <path>",
|
|
951
|
+
"Path to local mosaic repo checkout"
|
|
952
|
+
).action(async (options) => {
|
|
953
|
+
const { statusCommand } = await import("./status-MITGDLTT.js");
|
|
954
|
+
await statusCommand(options);
|
|
955
|
+
});
|
|
956
|
+
program.command("sync").description("Generate downstream sync context (template \u2192 app)").option(
|
|
957
|
+
"--mosaic-template-path <path>",
|
|
958
|
+
"Path to local mosaic repo checkout"
|
|
959
|
+
).option("--to <version>", "Target template version (default: latest)").action(async (options) => {
|
|
960
|
+
const { syncCommand } = await import("./sync-J4SFZHDX.js");
|
|
961
|
+
await syncCommand(options);
|
|
962
|
+
});
|
|
963
|
+
program.command("upstream").description("Generate upstream context (app \u2192 template)").option(
|
|
964
|
+
"--mosaic-template-path <path>",
|
|
965
|
+
"Path to local mosaic repo checkout"
|
|
966
|
+
).option("--files <patterns...>", "Specific files to propose upstream").action(async (options) => {
|
|
967
|
+
const { upstreamCommand } = await import("./upstream-AQI7P4EU.js");
|
|
968
|
+
await upstreamCommand(options);
|
|
969
|
+
});
|
|
970
|
+
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) => {
|
|
971
|
+
const { initCommand } = await import("./init-Z4VGBHAK.js");
|
|
972
|
+
await initCommand(options);
|
|
973
|
+
});
|
|
974
|
+
program.parse();
|