@postxl/cli 1.5.0 → 1.6.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/dist/create-project.command.js +86 -62
- package/package.json +2 -2
|
@@ -60,31 +60,36 @@ function register(program) {
|
|
|
60
60
|
.summary('Creates a new PostXL project.')
|
|
61
61
|
.description(`Creates a new PostXL project.
|
|
62
62
|
|
|
63
|
+
Usage: pxl create-project [name] [options]
|
|
64
|
+
|
|
65
|
+
When run interactively (TTY), prompts for any missing values.
|
|
66
|
+
When piped, all required values must be provided via arguments/options.
|
|
67
|
+
|
|
63
68
|
Installation flow:
|
|
64
|
-
1.
|
|
65
|
-
2. Writes initial project files (schema, generate.ts, tsconfig, package.json, .env)
|
|
69
|
+
1. Writes initial project files (schema, generate.ts, tsconfig, package.json, .env)
|
|
66
70
|
Uses the simple schema for the first pass even if a custom schema is provided
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
2. Runs pnpm install to fetch initial dependencies
|
|
72
|
+
3. Runs first code generation without formatting (as prettier/eslint not yet installed)
|
|
73
|
+
4. Runs pnpm install again to fetch devDependencies added by the generated package.json
|
|
74
|
+
5. Swaps in the custom schema (if provided) before the final generation
|
|
75
|
+
6. Runs second code generation with full formatting (prettier, eslint)
|
|
76
|
+
7. Generates TanStack Router route tree
|
|
72
77
|
8. Generates Prisma client
|
|
73
78
|
9. Initializes a git repository (unless --skip-git)
|
|
74
79
|
|
|
75
80
|
If --skip-generate is used with a custom schema, the custom schema is written directly.`)
|
|
76
|
-
.
|
|
77
|
-
.option('-s, --slug <slug>', 'Project slug')
|
|
78
|
-
.option('-p, --project-path <path>', 'Path where the generated project should be written
|
|
81
|
+
.argument('[name]', 'Project name')
|
|
82
|
+
.option('-s, --slug <slug>', 'Project slug (defaults to slugified project name)')
|
|
83
|
+
.option('-p, --project-path <path>', 'Path where the generated project should be written (defaults to ../<slug>)')
|
|
79
84
|
.option('-S, --schema <path>', 'Path to an existing postxl-schema.json to use instead of the default simple schema')
|
|
80
|
-
.option('--skip-git', 'Skip
|
|
85
|
+
.option('--skip-git', 'Skip initializing a git repository in the generated project directory')
|
|
81
86
|
.option('--skip-generate', 'Skip running the initial project generation')
|
|
82
87
|
.option('-l, --link-postxl', 'Link project to local PostXL monorepo packages (for development purposes)')
|
|
83
|
-
.action(async (options) => {
|
|
88
|
+
.action(async (nameArg, options) => {
|
|
84
89
|
try {
|
|
85
90
|
await checkNodeVersion();
|
|
86
91
|
const link = options.linkPostxl === true;
|
|
87
|
-
const name = await getProjectName(
|
|
92
|
+
const name = await getProjectName(nameArg);
|
|
88
93
|
const slug = await getProjectSlug(options.slug, name);
|
|
89
94
|
const customSchema = await loadCustomSchema(options.schema);
|
|
90
95
|
const schema = customSchema ?? getProjectSchema();
|
|
@@ -98,7 +103,6 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
98
103
|
schema: initialSchema,
|
|
99
104
|
projectPath,
|
|
100
105
|
});
|
|
101
|
-
// In case it is a workspace project, we install dependencies in the workspace part - else in the project
|
|
102
106
|
await installDependencies({ targetPath: projectPath, link });
|
|
103
107
|
if (!options.skipGenerate) {
|
|
104
108
|
// We initially only install the PostXL generator - without prettier, eslint and eslint rules. Hence, we run the first generation
|
|
@@ -113,16 +117,21 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
113
117
|
}
|
|
114
118
|
// The second generation runs with transformations - setting up prettier, eslint, etc.
|
|
115
119
|
await runGenerate({ projectPath, transform: true });
|
|
120
|
+
await generateTanStackRouter(projectPath);
|
|
116
121
|
await generatePrismaClient(projectPath);
|
|
117
|
-
if (!options.skipInitGit) {
|
|
118
|
-
await initializeGitRepository(projectPath);
|
|
119
|
-
}
|
|
120
122
|
}
|
|
121
123
|
else if (customSchema) {
|
|
122
124
|
// With --skip-generate, createProjectStructure already wrote the custom schema,
|
|
123
125
|
// but we still need to apply name/slug/projectType overrides.
|
|
124
126
|
await writeSchema({ name, slug, schema: customSchema, projectPath });
|
|
125
127
|
}
|
|
128
|
+
if (!options.skipGit) {
|
|
129
|
+
await initializeGitRepository(projectPath);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Safety net: remove .git if a subprocess created it despite --skip-git
|
|
133
|
+
await removeGitDirectory(projectPath);
|
|
134
|
+
}
|
|
126
135
|
console.log(`\n✓ Project "${name}" created successfully at ${projectPath}`);
|
|
127
136
|
console.log('\nNext steps:');
|
|
128
137
|
console.log(` cd ${path.relative(process.cwd(), projectPath)}`);
|
|
@@ -136,14 +145,32 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
136
145
|
}
|
|
137
146
|
});
|
|
138
147
|
}
|
|
148
|
+
function isInteractive() {
|
|
149
|
+
return process.stdin.isTTY === true;
|
|
150
|
+
}
|
|
151
|
+
async function prompt(question, defaultValue) {
|
|
152
|
+
const rl = readline.createInterface({
|
|
153
|
+
input: process.stdin,
|
|
154
|
+
output: process.stdout,
|
|
155
|
+
});
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
158
|
+
rl.question(promptText, (answer) => {
|
|
159
|
+
rl.close();
|
|
160
|
+
resolve(answer.trim() || defaultValue || '');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
139
164
|
async function getProjectName(providedName) {
|
|
140
165
|
if (providedName) {
|
|
141
166
|
return providedName;
|
|
142
167
|
}
|
|
168
|
+
if (!isInteractive()) {
|
|
169
|
+
throw new Error('Project name is required. Usage: pxl create-project <name>');
|
|
170
|
+
}
|
|
143
171
|
const projectName = await prompt('Project name');
|
|
144
172
|
if (!projectName) {
|
|
145
|
-
|
|
146
|
-
process.exit(1);
|
|
173
|
+
throw new Error('Project name is required!');
|
|
147
174
|
}
|
|
148
175
|
return projectName;
|
|
149
176
|
}
|
|
@@ -152,10 +179,12 @@ async function getProjectSlug(providedSlug, projectName) {
|
|
|
152
179
|
return providedSlug;
|
|
153
180
|
}
|
|
154
181
|
const suggestedSlug = (0, utils_1.slugify)(projectName);
|
|
182
|
+
if (!isInteractive()) {
|
|
183
|
+
return suggestedSlug;
|
|
184
|
+
}
|
|
155
185
|
const projectSlug = await prompt('Project slug', suggestedSlug);
|
|
156
186
|
if (!projectSlug) {
|
|
157
|
-
|
|
158
|
-
process.exit(1);
|
|
187
|
+
throw new Error('Project slug is required!');
|
|
159
188
|
}
|
|
160
189
|
return projectSlug;
|
|
161
190
|
}
|
|
@@ -173,25 +202,21 @@ async function loadCustomSchema(providedPath) {
|
|
|
173
202
|
}
|
|
174
203
|
catch (error) {
|
|
175
204
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
176
|
-
|
|
205
|
+
throw new Error(`Schema file not found: ${resolvedPath}`);
|
|
177
206
|
}
|
|
178
|
-
|
|
179
|
-
console.error(`Failed to read schema file: ${error instanceof Error ? error.message : error}`);
|
|
180
|
-
}
|
|
181
|
-
process.exit(1);
|
|
207
|
+
throw new Error(`Failed to read schema file: ${error instanceof Error ? error.message : error}`);
|
|
182
208
|
}
|
|
183
209
|
let json;
|
|
184
210
|
try {
|
|
185
211
|
json = JSON.parse(content);
|
|
186
212
|
}
|
|
187
213
|
catch {
|
|
188
|
-
|
|
189
|
-
process.exit(1);
|
|
214
|
+
throw new Error(`Schema file is not valid JSON: ${resolvedPath}`);
|
|
190
215
|
}
|
|
191
216
|
const result = schema_1.zProjectSchema.safeParse(json);
|
|
192
217
|
if (!result.success) {
|
|
193
218
|
(0, log_schema_error_1.logSchemaValidationError)(result.error);
|
|
194
|
-
|
|
219
|
+
throw new Error('Schema validation failed');
|
|
195
220
|
}
|
|
196
221
|
return json;
|
|
197
222
|
}
|
|
@@ -205,21 +230,16 @@ async function writeSchema({ name, slug, schema, projectPath, }) {
|
|
|
205
230
|
await fs.writeFile(path.join(projectPath, 'postxl-schema.json'), JSON.stringify(updatedSchema, null, 2) + '\n');
|
|
206
231
|
}
|
|
207
232
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* - if it is outside of the cwd (regardless of local or absolute), check if exists. if so, throw error, if not create and return-
|
|
233
|
+
* Resolves the project output path. Defaults to `../{slug}` (standalone).
|
|
234
|
+
* Prompts interactively if no path is given and stdin is a TTY.
|
|
235
|
+
* Rejects paths inside `./projects/` (workspace projects are no longer supported).
|
|
236
|
+
* Creates the directory if it doesn't exist; throws if it exists and is non-empty.
|
|
213
237
|
*/
|
|
214
238
|
async function resolveProjectPath({ slug, projectPath, }) {
|
|
215
239
|
const repositoryRoot = process.cwd();
|
|
216
240
|
const defaultStandalonePath = path.resolve(repositoryRoot, '..', slug);
|
|
217
241
|
if (!projectPath) {
|
|
218
|
-
projectPath = await prompt('Project path', defaultStandalonePath);
|
|
219
|
-
}
|
|
220
|
-
if (!projectPath) {
|
|
221
|
-
console.error('Project path is required!');
|
|
222
|
-
process.exit(1);
|
|
242
|
+
projectPath = isInteractive() ? await prompt('Project path', defaultStandalonePath) : defaultStandalonePath;
|
|
223
243
|
}
|
|
224
244
|
const projectGenerationPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(repositoryRoot, projectPath);
|
|
225
245
|
const projectsFolder = path.join(repositoryRoot, 'projects');
|
|
@@ -240,25 +260,24 @@ async function createFolderOrThrow(targetPath) {
|
|
|
240
260
|
await fs.mkdir(targetPath, { recursive: true });
|
|
241
261
|
return;
|
|
242
262
|
}
|
|
243
|
-
|
|
244
|
-
process.exit(1);
|
|
263
|
+
throw new Error(`Cannot access target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
|
|
245
264
|
}
|
|
246
265
|
if (!stat.isDirectory()) {
|
|
247
|
-
|
|
248
|
-
process.exit(1);
|
|
266
|
+
throw new Error(`Target path "${targetPath}" already exists and is not a directory! Please choose a different path.`);
|
|
249
267
|
}
|
|
250
268
|
try {
|
|
251
269
|
const entries = await fs.readdir(targetPath);
|
|
252
270
|
if (entries.length > 0) {
|
|
253
271
|
const hasOnlyHidden = entries.every((entry) => entry.startsWith('.'));
|
|
254
272
|
const hint = hasOnlyHidden ? ' (it contains hidden files/folders)' : '';
|
|
255
|
-
|
|
256
|
-
process.exit(1);
|
|
273
|
+
throw new Error(`Target path "${targetPath}" already exists and is not empty${hint}! Please choose a different path.`);
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
276
|
catch (error) {
|
|
260
|
-
|
|
261
|
-
|
|
277
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`Cannot read target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
|
|
262
281
|
}
|
|
263
282
|
}
|
|
264
283
|
async function createProjectStructure({ name, slug, schema, projectPath, }) {
|
|
@@ -380,6 +399,18 @@ async function runGenerate({ projectPath, transform, }) {
|
|
|
380
399
|
console.log('You can manually run "pnpm run generate" in the project directory.');
|
|
381
400
|
}
|
|
382
401
|
}
|
|
402
|
+
async function generateTanStackRouter(targetPath) {
|
|
403
|
+
const frontendPath = path.join(targetPath, 'frontend');
|
|
404
|
+
console.log('\nGenerating TanStack Router route tree...');
|
|
405
|
+
try {
|
|
406
|
+
await run('pnpm exec tsr generate', { cwd: frontendPath });
|
|
407
|
+
console.log('TanStack Router route tree generated successfully!');
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
console.error('Failed to generate TanStack Router route tree:', error);
|
|
411
|
+
console.log('You can manually run "pnpm exec tsr generate" in the frontend directory.');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
383
414
|
async function generatePrismaClient(targetPath) {
|
|
384
415
|
console.log('\nGenerating Prisma client...');
|
|
385
416
|
try {
|
|
@@ -402,8 +433,15 @@ async function initializeGitRepository(targetPath) {
|
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
435
|
/**
|
|
405
|
-
*
|
|
436
|
+
* Removes a .git directory if it exists. This handles the case where
|
|
437
|
+
* a subprocess (e.g., pnpm install, code generation) inadvertently
|
|
438
|
+
* created a .git directory even though --skip-git was requested.
|
|
439
|
+
* fs.rm with force:true already ignores ENOENT (missing path).
|
|
406
440
|
*/
|
|
441
|
+
async function removeGitDirectory(targetPath) {
|
|
442
|
+
const gitDir = path.join(targetPath, '.git');
|
|
443
|
+
await fs.rm(gitDir, { recursive: true, force: true });
|
|
444
|
+
}
|
|
407
445
|
async function checkNodeVersion() {
|
|
408
446
|
try {
|
|
409
447
|
const repositoryRoot = process.cwd();
|
|
@@ -433,17 +471,3 @@ async function checkNodeVersion() {
|
|
|
433
471
|
throw error;
|
|
434
472
|
}
|
|
435
473
|
}
|
|
436
|
-
// Helper to prompt user for input
|
|
437
|
-
async function prompt(question, defaultValue) {
|
|
438
|
-
const rl = readline.createInterface({
|
|
439
|
-
input: process.stdin,
|
|
440
|
-
output: process.stdout,
|
|
441
|
-
});
|
|
442
|
-
return new Promise((resolve) => {
|
|
443
|
-
const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
444
|
-
rl.question(promptText, (answer) => {
|
|
445
|
-
rl.close();
|
|
446
|
-
resolve(answer.trim() || defaultValue || '');
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postxl/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Command-line interface for PostXL code generation framework",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dotenv": "17.3.1",
|
|
46
46
|
"zod-validation-error": "5.0.0",
|
|
47
47
|
"@postxl/generator": "^1.3.7",
|
|
48
|
-
"@postxl/generators": "^1.23.
|
|
48
|
+
"@postxl/generators": "^1.23.1",
|
|
49
49
|
"@postxl/schema": "^1.8.2",
|
|
50
50
|
"@postxl/utils": "^1.4.0"
|
|
51
51
|
},
|