@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.
@@ -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. Prompts for project name, slug, path, and schema (if not provided via options)
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
- 3. Runs pnpm install to fetch initial dependencies
68
- 4. Runs first code generation without formatting (as prettier/eslint not yet installed)
69
- 5. Runs pnpm install again to fetch devDependencies added by the generated package.json
70
- 6. Swaps in the custom schema (if provided) before the final generation
71
- 7. Runs second code generation with full formatting (prettier, eslint)
72
- 8. Generates TanStack Router route tree
73
- 9. Generates Prisma client
74
- 10. Initializes a git repository (unless --skip-git)
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
- .option('-n, --name <name>', 'Project name')
78
- .option('-s, --slug <slug>', 'Project slug')
79
- .option('-p, --project-path <path>', 'Path where the generated project should be written. If not specified, user will be prompted')
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 initialing a git repository in the generated project directory')
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(options.name);
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
- console.error('Project name is required!');
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
- console.error('Project slug is required!');
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
- console.error(`Schema file not found: ${resolvedPath}`);
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
- process.exit(1);
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
- console.error(`Schema file is not valid JSON: ${resolvedPath}`);
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
- process.exit(1);
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
- * If projectPath is not provided:
211
- * - Prompt user for path if it should be standalone (in ../{slug}) or workspace (in ./projects/{slug}). Default is standalone.
212
- * With projectPath:
213
- * - if it is local within projects, throw error as we do not support workspace project anymore.
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
- console.error(`Cannot access target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
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
- console.error(`Target path "${targetPath}" already exists and is not a directory! Please choose a different path.`);
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
- console.error(`Target path "${targetPath}" already exists and is not empty${hint}! Please choose a different path.`);
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
- console.error(`Cannot read target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
263
- process.exit(1);
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 run generate:tsr', { cwd: targetPath });
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 run generate:tsr" in the project directory.');
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
- * Throws if Node.js version is less than what package.json devEngines.runtime requires
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.5.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.0",
48
+ "@postxl/generators": "^1.23.1",
49
49
  "@postxl/schema": "^1.8.2",
50
50
  "@postxl/utils": "^1.4.0"
51
51
  },