@postxl/cli 1.5.1 → 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 +78 -67
- package/package.json +2 -2
|
@@ -60,32 +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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
77
|
+
8. Generates Prisma client
|
|
78
|
+
9. Initializes a git repository (unless --skip-git)
|
|
75
79
|
|
|
76
80
|
If --skip-generate is used with a custom schema, the custom schema is written directly.`)
|
|
77
|
-
.
|
|
78
|
-
.option('-s, --slug <slug>', 'Project slug')
|
|
79
|
-
.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>)')
|
|
80
84
|
.option('-S, --schema <path>', 'Path to an existing postxl-schema.json to use instead of the default simple schema')
|
|
81
|
-
.option('--skip-git', 'Skip
|
|
85
|
+
.option('--skip-git', 'Skip initializing a git repository in the generated project directory')
|
|
82
86
|
.option('--skip-generate', 'Skip running the initial project generation')
|
|
83
87
|
.option('-l, --link-postxl', 'Link project to local PostXL monorepo packages (for development purposes)')
|
|
84
|
-
.action(async (options) => {
|
|
88
|
+
.action(async (nameArg, options) => {
|
|
85
89
|
try {
|
|
86
90
|
await checkNodeVersion();
|
|
87
91
|
const link = options.linkPostxl === true;
|
|
88
|
-
const name = await getProjectName(
|
|
92
|
+
const name = await getProjectName(nameArg);
|
|
89
93
|
const slug = await getProjectSlug(options.slug, name);
|
|
90
94
|
const customSchema = await loadCustomSchema(options.schema);
|
|
91
95
|
const schema = customSchema ?? getProjectSchema();
|
|
@@ -99,7 +103,6 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
99
103
|
schema: initialSchema,
|
|
100
104
|
projectPath,
|
|
101
105
|
});
|
|
102
|
-
// In case it is a workspace project, we install dependencies in the workspace part - else in the project
|
|
103
106
|
await installDependencies({ targetPath: projectPath, link });
|
|
104
107
|
if (!options.skipGenerate) {
|
|
105
108
|
// We initially only install the PostXL generator - without prettier, eslint and eslint rules. Hence, we run the first generation
|
|
@@ -116,15 +119,19 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
116
119
|
await runGenerate({ projectPath, transform: true });
|
|
117
120
|
await generateTanStackRouter(projectPath);
|
|
118
121
|
await generatePrismaClient(projectPath);
|
|
119
|
-
if (!options.skipGit) {
|
|
120
|
-
await initializeGitRepository(projectPath);
|
|
121
|
-
}
|
|
122
122
|
}
|
|
123
123
|
else if (customSchema) {
|
|
124
124
|
// With --skip-generate, createProjectStructure already wrote the custom schema,
|
|
125
125
|
// but we still need to apply name/slug/projectType overrides.
|
|
126
126
|
await writeSchema({ name, slug, schema: customSchema, projectPath });
|
|
127
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
|
+
}
|
|
128
135
|
console.log(`\n✓ Project "${name}" created successfully at ${projectPath}`);
|
|
129
136
|
console.log('\nNext steps:');
|
|
130
137
|
console.log(` cd ${path.relative(process.cwd(), projectPath)}`);
|
|
@@ -138,14 +145,32 @@ If --skip-generate is used with a custom schema, the custom schema is written di
|
|
|
138
145
|
}
|
|
139
146
|
});
|
|
140
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
|
+
}
|
|
141
164
|
async function getProjectName(providedName) {
|
|
142
165
|
if (providedName) {
|
|
143
166
|
return providedName;
|
|
144
167
|
}
|
|
168
|
+
if (!isInteractive()) {
|
|
169
|
+
throw new Error('Project name is required. Usage: pxl create-project <name>');
|
|
170
|
+
}
|
|
145
171
|
const projectName = await prompt('Project name');
|
|
146
172
|
if (!projectName) {
|
|
147
|
-
|
|
148
|
-
process.exit(1);
|
|
173
|
+
throw new Error('Project name is required!');
|
|
149
174
|
}
|
|
150
175
|
return projectName;
|
|
151
176
|
}
|
|
@@ -154,10 +179,12 @@ async function getProjectSlug(providedSlug, projectName) {
|
|
|
154
179
|
return providedSlug;
|
|
155
180
|
}
|
|
156
181
|
const suggestedSlug = (0, utils_1.slugify)(projectName);
|
|
182
|
+
if (!isInteractive()) {
|
|
183
|
+
return suggestedSlug;
|
|
184
|
+
}
|
|
157
185
|
const projectSlug = await prompt('Project slug', suggestedSlug);
|
|
158
186
|
if (!projectSlug) {
|
|
159
|
-
|
|
160
|
-
process.exit(1);
|
|
187
|
+
throw new Error('Project slug is required!');
|
|
161
188
|
}
|
|
162
189
|
return projectSlug;
|
|
163
190
|
}
|
|
@@ -175,25 +202,21 @@ async function loadCustomSchema(providedPath) {
|
|
|
175
202
|
}
|
|
176
203
|
catch (error) {
|
|
177
204
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
console.error(`Failed to read schema file: ${error instanceof Error ? error.message : error}`);
|
|
205
|
+
throw new Error(`Schema file not found: ${resolvedPath}`);
|
|
182
206
|
}
|
|
183
|
-
|
|
207
|
+
throw new Error(`Failed to read schema file: ${error instanceof Error ? error.message : error}`);
|
|
184
208
|
}
|
|
185
209
|
let json;
|
|
186
210
|
try {
|
|
187
211
|
json = JSON.parse(content);
|
|
188
212
|
}
|
|
189
213
|
catch {
|
|
190
|
-
|
|
191
|
-
process.exit(1);
|
|
214
|
+
throw new Error(`Schema file is not valid JSON: ${resolvedPath}`);
|
|
192
215
|
}
|
|
193
216
|
const result = schema_1.zProjectSchema.safeParse(json);
|
|
194
217
|
if (!result.success) {
|
|
195
218
|
(0, log_schema_error_1.logSchemaValidationError)(result.error);
|
|
196
|
-
|
|
219
|
+
throw new Error('Schema validation failed');
|
|
197
220
|
}
|
|
198
221
|
return json;
|
|
199
222
|
}
|
|
@@ -207,21 +230,16 @@ async function writeSchema({ name, slug, schema, projectPath, }) {
|
|
|
207
230
|
await fs.writeFile(path.join(projectPath, 'postxl-schema.json'), JSON.stringify(updatedSchema, null, 2) + '\n');
|
|
208
231
|
}
|
|
209
232
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* - 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.
|
|
215
237
|
*/
|
|
216
238
|
async function resolveProjectPath({ slug, projectPath, }) {
|
|
217
239
|
const repositoryRoot = process.cwd();
|
|
218
240
|
const defaultStandalonePath = path.resolve(repositoryRoot, '..', slug);
|
|
219
241
|
if (!projectPath) {
|
|
220
|
-
projectPath = await prompt('Project path', defaultStandalonePath);
|
|
221
|
-
}
|
|
222
|
-
if (!projectPath) {
|
|
223
|
-
console.error('Project path is required!');
|
|
224
|
-
process.exit(1);
|
|
242
|
+
projectPath = isInteractive() ? await prompt('Project path', defaultStandalonePath) : defaultStandalonePath;
|
|
225
243
|
}
|
|
226
244
|
const projectGenerationPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(repositoryRoot, projectPath);
|
|
227
245
|
const projectsFolder = path.join(repositoryRoot, 'projects');
|
|
@@ -242,25 +260,24 @@ async function createFolderOrThrow(targetPath) {
|
|
|
242
260
|
await fs.mkdir(targetPath, { recursive: true });
|
|
243
261
|
return;
|
|
244
262
|
}
|
|
245
|
-
|
|
246
|
-
process.exit(1);
|
|
263
|
+
throw new Error(`Cannot access target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
|
|
247
264
|
}
|
|
248
265
|
if (!stat.isDirectory()) {
|
|
249
|
-
|
|
250
|
-
process.exit(1);
|
|
266
|
+
throw new Error(`Target path "${targetPath}" already exists and is not a directory! Please choose a different path.`);
|
|
251
267
|
}
|
|
252
268
|
try {
|
|
253
269
|
const entries = await fs.readdir(targetPath);
|
|
254
270
|
if (entries.length > 0) {
|
|
255
271
|
const hasOnlyHidden = entries.every((entry) => entry.startsWith('.'));
|
|
256
272
|
const hint = hasOnlyHidden ? ' (it contains hidden files/folders)' : '';
|
|
257
|
-
|
|
258
|
-
process.exit(1);
|
|
273
|
+
throw new Error(`Target path "${targetPath}" already exists and is not empty${hint}! Please choose a different path.`);
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
276
|
catch (error) {
|
|
262
|
-
|
|
263
|
-
|
|
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}`);
|
|
264
281
|
}
|
|
265
282
|
}
|
|
266
283
|
async function createProjectStructure({ name, slug, schema, projectPath, }) {
|
|
@@ -383,14 +400,15 @@ async function runGenerate({ projectPath, transform, }) {
|
|
|
383
400
|
}
|
|
384
401
|
}
|
|
385
402
|
async function generateTanStackRouter(targetPath) {
|
|
403
|
+
const frontendPath = path.join(targetPath, 'frontend');
|
|
386
404
|
console.log('\nGenerating TanStack Router route tree...');
|
|
387
405
|
try {
|
|
388
|
-
await run('pnpm
|
|
406
|
+
await run('pnpm exec tsr generate', { cwd: frontendPath });
|
|
389
407
|
console.log('TanStack Router route tree generated successfully!');
|
|
390
408
|
}
|
|
391
409
|
catch (error) {
|
|
392
410
|
console.error('Failed to generate TanStack Router route tree:', error);
|
|
393
|
-
console.log('You can manually run "pnpm
|
|
411
|
+
console.log('You can manually run "pnpm exec tsr generate" in the frontend directory.');
|
|
394
412
|
}
|
|
395
413
|
}
|
|
396
414
|
async function generatePrismaClient(targetPath) {
|
|
@@ -415,8 +433,15 @@ async function initializeGitRepository(targetPath) {
|
|
|
415
433
|
}
|
|
416
434
|
}
|
|
417
435
|
/**
|
|
418
|
-
*
|
|
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).
|
|
419
440
|
*/
|
|
441
|
+
async function removeGitDirectory(targetPath) {
|
|
442
|
+
const gitDir = path.join(targetPath, '.git');
|
|
443
|
+
await fs.rm(gitDir, { recursive: true, force: true });
|
|
444
|
+
}
|
|
420
445
|
async function checkNodeVersion() {
|
|
421
446
|
try {
|
|
422
447
|
const repositoryRoot = process.cwd();
|
|
@@ -446,17 +471,3 @@ async function checkNodeVersion() {
|
|
|
446
471
|
throw error;
|
|
447
472
|
}
|
|
448
473
|
}
|
|
449
|
-
// Helper to prompt user for input
|
|
450
|
-
async function prompt(question, defaultValue) {
|
|
451
|
-
const rl = readline.createInterface({
|
|
452
|
-
input: process.stdin,
|
|
453
|
-
output: process.stdout,
|
|
454
|
-
});
|
|
455
|
-
return new Promise((resolve) => {
|
|
456
|
-
const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
457
|
-
rl.question(promptText, (answer) => {
|
|
458
|
-
rl.close();
|
|
459
|
-
resolve(answer.trim() || defaultValue || '');
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
}
|
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
|
},
|