@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.
@@ -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. 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)
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
- .option('-n, --name <name>', 'Project name')
77
- .option('-s, --slug <slug>', 'Project slug')
78
- .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>)')
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 initialing a git repository in the generated project directory')
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(options.name);
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
- console.error('Project name is required!');
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
- console.error('Project slug is required!');
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
- console.error(`Schema file not found: ${resolvedPath}`);
205
+ throw new Error(`Schema file not found: ${resolvedPath}`);
177
206
  }
178
- else {
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
- console.error(`Schema file is not valid JSON: ${resolvedPath}`);
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
- process.exit(1);
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
- * If projectPath is not provided:
209
- * - Prompt user for path if it should be standalone (in ../{slug}) or workspace (in ./projects/{slug}). Default is standalone.
210
- * With projectPath:
211
- * - if it is local within projects, throw error as we do not support workspace project anymore.
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
- console.error(`Cannot access target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
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
- console.error(`Target path "${targetPath}" already exists and is not a directory! Please choose a different path.`);
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
- console.error(`Target path "${targetPath}" already exists and is not empty${hint}! Please choose a different path.`);
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
- console.error(`Cannot read target path "${targetPath}": ${error instanceof Error ? error.message : error}`);
261
- 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}`);
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
- * 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).
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.5.0",
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
  },