@postxl/cli 0.0.9

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/LICENSE ADDED
@@ -0,0 +1,50 @@
1
+ PostXL Non-Commercial License
2
+
3
+ Copyright (c) 2025 PostXL GmbH
4
+
5
+ NON-COMMERCIAL USE
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to use,
9
+ copy, modify, and distribute the Software for non-commercial purposes only,
10
+ subject to the following conditions:
11
+
12
+ 1. The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ 2. The Software may not be used for commercial purposes without obtaining a
16
+ commercial license from PostXL GmbH.
17
+
18
+ DEFINITION OF NON-COMMERCIAL USE
19
+
20
+ "Non-commercial use" means personal, educational, research, or evaluation use
21
+ where the primary purpose is not to generate revenue or commercial advantage.
22
+
23
+ Non-commercial use includes:
24
+
25
+ - Personal projects and learning
26
+ - Academic research and education
27
+ - Open source projects that are not commercially monetized
28
+ - Evaluation and testing of the Software
29
+
30
+ COMMERCIAL USE
31
+
32
+ For commercial use of this Software, including but not limited to:
33
+
34
+ - Use in products or services sold or licensed to third parties
35
+ - Use in internal business operations that generate revenue
36
+ - Use by for-profit organizations in production environments
37
+
38
+ You must obtain a commercial license from PostXL GmbH.
39
+
40
+ Contact: licensing@postxl.com
41
+
42
+ DISCLAIMER
43
+
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
50
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # PXL CLI
2
+
3
+ Small command line tool that helps with the development of PXL projects.
4
+
5
+ Currently, the CLI provides the following commands:
6
+
7
+ - `pxl generate`: Generate code for a PXL project
8
+ - `pxl create-project` (alias: `pxl new`): Create a new PostXL project
9
+
10
+ ## `pxl generate`
11
+
12
+ The `pxl generate` command generates code for a PXL project.
13
+
14
+ ### Usage
15
+
16
+ In the project root, ensure that a `project-schema.json` and a `generate.ts` file exist. See the [template project](../../projects/template) for an example.
17
+
18
+ ```bash
19
+ cd /projects/my-project
20
+ pxl generate <project-name>
21
+ ```
22
+
23
+ See `pxl generate --help` for more information.
24
+
25
+ ## `pxl create-project` / `pxl new`
26
+
27
+ The `pxl create-project` command creates a new PostXL project with a selected schema template. It scaffolds the project structure, installs dependencies, runs the initial code generation, and generates the Prisma client.
28
+
29
+ ### Usage
30
+
31
+ Run the command from the PostXL repository root:
32
+
33
+ ```bash
34
+ pxl create-project
35
+ # or
36
+ pxl new
37
+ ```
38
+
39
+ The CLI will interactively prompt you for:
40
+
41
+ 1. **Project name**: The display name for your project
42
+ 2. **Project slug**: A URL-friendly identifier (auto-suggested from the name)
43
+ 3. **Schema template**: Choose from available starter schemas (la, mca, ring, simple, subex, uspmsm)
44
+ 4. **Project location**: Either a standalone project (outside the workspace) or a workspace project (in the `projects/` folder)
45
+
46
+ ### Options
47
+
48
+ | Option | Description |
49
+ | --------------------------- | ------------------------------------------------------------- |
50
+ | `-n, --name <name>` | Project name (skip interactive prompt) |
51
+ | `-s, --slug <slug>` | Project slug (skip interactive prompt) |
52
+ | `--schema <schema>` | Schema template to use (la, mca, ring, simple, subex, uspmsm) |
53
+ | `-p, --project-path <path>` | Custom path for the generated project |
54
+ | `--skip-git` | Skip initializing a git repository |
55
+ | `--skip-generate` | Skip running the initial project generation |
56
+
57
+ ### Examples
58
+
59
+ ```bash
60
+ # Interactive mode
61
+ pxl new
62
+
63
+ # Non-interactive with all options
64
+ pxl new --name "My App" --slug my-app --schema simple
65
+
66
+ # Create at a specific path
67
+ pxl new --name "My App" -p /path/to/my-app
68
+ ```
69
+
70
+ See `pxl create-project --help` for more information.
@@ -0,0 +1,17 @@
1
+ import { ProjectSchemaName, ProjectSlug } from '@postxl/schema';
2
+ import { ProjectPath } from '../create-project.command';
3
+ export type CreateProjectOptions = {
4
+ name: ProjectSchemaName;
5
+ slug: ProjectSlug;
6
+ schema: object & {
7
+ projectType?: {
8
+ kind: string;
9
+ };
10
+ };
11
+ projectPath: ProjectPath;
12
+ };
13
+ /**
14
+ * Creates a new PostXL project with the specified options.
15
+ * This function is used by the create-project CLI command.
16
+ */
17
+ export declare function createProject(options: CreateProjectOptions): Promise<void>;
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createProject = createProject;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const path = __importStar(require("node:path"));
39
+ const Generator = __importStar(require("@postxl/generator"));
40
+ const generator_1 = require("@postxl/generator");
41
+ const baseDevDependencies = [
42
+ { packageName: Generator.toPostXlPackageName('@postxl/generator') },
43
+ { packageName: Generator.toPostXlPackageName('@postxl/generators') },
44
+ { packageName: Generator.toPostXlPackageName('@postxl/schema') },
45
+ { packageName: '@typescript-eslint/eslint-plugin', version: '8.46.3' },
46
+ { packageName: '@typescript-eslint/parser', version: '8.46.3' },
47
+ { packageName: 'commander', version: '12.1.0' },
48
+ ];
49
+ const workspaceDevDependencies = [
50
+ { packageName: '@prisma/client', version: '6.19.0' },
51
+ { packageName: 'chokidar-cli', version: '3.0.0' },
52
+ { packageName: 'eslint', version: '9.39.1' },
53
+ { packageName: 'eslint-plugin-simple-import-sort', version: '12.1.1' },
54
+ { packageName: 'jest', version: '29.7.0' },
55
+ { packageName: 'prettier', version: '3.4.2' },
56
+ { packageName: 'prisma', version: '6.19.0' },
57
+ { packageName: 'rimraf', version: '6.1.0' },
58
+ ];
59
+ const baseScripts = [
60
+ { name: 'prettier:check', command: 'prettier --check "**/*.{ts,tsx}" --config ../../prettier.config.js' },
61
+ { name: 'pxl', command: 'node ../../packages/cli/dist/index.js' },
62
+ ];
63
+ const getWorkspaceScripts = (slug) => [
64
+ { name: 'docker', command: './scripts/docker.sh' },
65
+ { name: 'generate', command: 'pnpm run generate:project && pnpm run generate:prisma' },
66
+ { name: 'generate:prisma', command: 'prisma generate' },
67
+ { name: 'generate:project', command: 'pnpm run generate:project:pxl && pnpm run generate:project:tsr' },
68
+ { name: 'generate:project:pxl', command: 'node ../../packages/cli/dist/index.js generate' },
69
+ { name: 'generate:project:tsr', command: `pnpm --filter @postxl/${slug}-frontend exec tsr generate` },
70
+ {
71
+ name: 'generate:watch',
72
+ command: "chokidar '../../packages/**/dist/**' '../../generators/**/dist/**' '../../generators/**/template/**' './generate.ts' './postxl-schema.json' -c 'pnpm run generate:project:pxl -f -i && pnpm run generate:project:tsr'",
73
+ },
74
+ { name: 'setup', command: './scripts/setup.sh' },
75
+ ];
76
+ // TODO: Adjust this for standalone projects once full standalone generation is implemented!
77
+ const standaloneScripts = [
78
+ { name: 'generate', command: 'node ../../packages/cli/dist/index.js generate' },
79
+ ];
80
+ const tsConfig = {
81
+ compilerOptions: {
82
+ forceConsistentCasingInFileNames: true,
83
+ esModuleInterop: true,
84
+ incremental: true,
85
+ isolatedModules: true,
86
+ resolveJsonModule: true,
87
+ skipLibCheck: true,
88
+ strict: true,
89
+ },
90
+ exclude: ['node_modules'],
91
+ };
92
+ const generators = [
93
+ 'backendActionsGenerator',
94
+ 'backendAuthenticationGenerator',
95
+ 'backendDataManagementGenerator',
96
+ 'backendE2eGenerator',
97
+ 'backendGenerator',
98
+ 'backendImportGenerator',
99
+ 'backendModuleXlPortGenerator',
100
+ 'backendRepositoriesGenerator',
101
+ 'backendRestApiGenerator',
102
+ 'backendS3Generator',
103
+ 'backendSeedGenerator',
104
+ 'backendTrpcRouterGenerator',
105
+ 'backendUpdateGenerator',
106
+ 'backendViewGenerator',
107
+ 'baseGenerator',
108
+ {
109
+ imports: 'configureDevopsGenerator',
110
+ code: 'configureDevopsGenerator({ useDatabase: true, keycloakComponents: [], useBitbucket: true, useS3: true })',
111
+ },
112
+ 'databasePrismaGenerator',
113
+ 'decodersGenerator',
114
+ 'e2eGenerator',
115
+ 'frontendAdminGenerator',
116
+ { imports: 'Frontend', code: 'Frontend.configureFrontendGenerator(Frontend.all)' },
117
+ 'frontendFormsGenerator',
118
+ 'frontendTablesGenerator',
119
+ 'frontendTrpcClientGenerator',
120
+ 'mockDataGenerator',
121
+ 'seedDataGenerator',
122
+ 'typesGenerator',
123
+ ];
124
+ /**
125
+ * Creates a new PostXL project with the specified options.
126
+ * This function is used by the create-project CLI command.
127
+ */
128
+ async function createProject(options) {
129
+ const { projectPath, name, slug, schema } = options;
130
+ const targetPath = projectPath.kind === 'standalone' ? projectPath.workspaceProjectPath : projectPath.projectGenerationPath;
131
+ // Create target directory if it doesn't exist
132
+ await fs.mkdir(targetPath, { recursive: true });
133
+ // Update schema with new name and slug
134
+ // Use the source field which contains the original JSON input
135
+ const updatedSchema = {
136
+ ...schema,
137
+ name,
138
+ slug,
139
+ projectType: {
140
+ kind: projectPath.kind,
141
+ projectDirectory: projectPath.projectGenerationPath,
142
+ },
143
+ };
144
+ // Write source files directly to disk (not tracked as generated files)
145
+ // These are the files that define the project, not generated output
146
+ await fs.writeFile(path.join(targetPath, 'postxl-schema.json'), JSON.stringify(updatedSchema, null, 2) + '\n');
147
+ await fs.writeFile(path.join(targetPath, 'generate.ts'), await (0, generator_1.format)({ path: 'generate.ts', content: generateGenerateTs() }));
148
+ await fs.writeFile(path.join(targetPath, 'tsconfig.json'), await (0, generator_1.format)({ path: 'tsconfig.json', content: generateTsConfig() }));
149
+ // Generate package.json using the helper
150
+ const packageJsonConfig = {
151
+ name: Generator.toPostXlPackageName(`@postxl/${slug}`),
152
+ description: `A PostXL project for ${name}.`,
153
+ dependencies: [],
154
+ devDependencies: { ...baseDevDependencies, ...(projectPath.kind === 'workspace' ? workspaceDevDependencies : {}) },
155
+ scripts: [...baseScripts, ...(projectPath.kind === 'workspace' ? getWorkspaceScripts(slug) : standaloneScripts)],
156
+ };
157
+ await fs.writeFile(path.join(targetPath, 'package.json'), await (0, generator_1.format)({ path: 'package.json', content: Generator.generatePackageJson(packageJsonConfig) }));
158
+ // Generate .env and .env.example with database connection and target generation path
159
+ const relativeGenerationPath = path.relative(targetPath, projectPath.projectGenerationPath) || '.';
160
+ const envContent = `DATABASE_CONNECTION=postgresql://postgres:postgres@localhost:5432/${slug}\n` +
161
+ `PXL_PROJECT_PATH=${relativeGenerationPath}\n`;
162
+ await fs.writeFile(path.join(targetPath, '.env.example'), envContent);
163
+ await fs.writeFile(path.join(targetPath, '.env'), envContent);
164
+ }
165
+ function generateTsConfig() {
166
+ return JSON.stringify(tsConfig, null, 2) + '\n';
167
+ }
168
+ function generateGenerateTs() {
169
+ return `
170
+ import * as Generator from '@postxl/generator'
171
+ import {
172
+ ${generators.map((g) => (typeof g === 'string' ? g : `${g.imports}`)).join(',\n ')}
173
+ } from '@postxl/generators'
174
+
175
+ export default function generate(): Generator.GeneratorInterface[] {
176
+ return [
177
+ ${generators.map((g) => (typeof g === 'string' ? g : g.code)).join(',\n ')}
178
+ ]
179
+ }`;
180
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ export declare function register(program: Command): void;
3
+ export type ProjectPath = ProjectPath_Standalone | ProjectPath_Workspace;
4
+ type ProjectPath_Standalone = {
5
+ kind: 'standalone';
6
+ workspaceProjectPath: string;
7
+ projectGenerationPath: string;
8
+ };
9
+ type ProjectPath_Workspace = {
10
+ kind: 'workspace';
11
+ projectGenerationPath: string;
12
+ };
13
+ export {};
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.register = register;
37
+ const node_child_process_1 = require("node:child_process");
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const path = __importStar(require("node:path"));
40
+ const readline = __importStar(require("node:readline"));
41
+ const node_util_1 = require("node:util");
42
+ const schema_1 = require("@postxl/schema");
43
+ const utils_1 = require("@postxl/utils");
44
+ const create_project_generator_1 = require("./create-project/create-project.generator");
45
+ const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
46
+ function register(program) {
47
+ program
48
+ .command('create-project')
49
+ .alias('new')
50
+ .summary('Creates a new PostXL project.')
51
+ .description('Creates a new PostXL project with a selected schema template.')
52
+ .option('-n, --name <name>', 'Project name')
53
+ .option('-s, --slug <slug>', 'Project slug')
54
+ .option('--schema <schema>', 'Schema to use (la, mca, ring, simple, subex, uspmsm)')
55
+ .option('-p, --project-path <path>', 'Path where the generated project should be written. If not specified, a workspace project will be created in the projects folder')
56
+ .option('--skip-git', 'Skip initialing a git repository in the generated project directory')
57
+ .option('--skip-generate', 'Skip running the initial project generation')
58
+ .action(async (options) => {
59
+ try {
60
+ await checkNodeVersion();
61
+ const name = await getProjectName(options.name);
62
+ const slug = await getProjectSlug(options.slug, name);
63
+ const schema = await getProjectSchema(options.schema);
64
+ const projectPath = await resolveProjectPath({ slug, projectPath: options.projectPath });
65
+ await createProjectStructure({
66
+ name,
67
+ slug,
68
+ schema,
69
+ projectPath,
70
+ });
71
+ // In case it is a workspace project, we install dependencies in the workspace part - else in the project
72
+ await installDependencies(projectPath.kind === 'workspace' ? projectPath.projectGenerationPath : projectPath.workspaceProjectPath);
73
+ if (!options.skipGenerate) {
74
+ await runGenerate(projectPath);
75
+ await installDependencies(projectPath.projectGenerationPath);
76
+ await generatePrismaClient(projectPath.projectGenerationPath);
77
+ if (!options.skipInitGit && projectPath.kind === 'standalone') {
78
+ await initializeGitRepository(projectPath.projectGenerationPath);
79
+ }
80
+ }
81
+ console.log(`\n✓ Project "${name}" created successfully at ${projectPath.projectGenerationPath}`);
82
+ console.log('\nNext steps:');
83
+ console.log(` cd ${path.relative(process.cwd(), projectPath.projectGenerationPath)}`);
84
+ console.log(' pnpm run generate # generates the project to your configured path');
85
+ console.log(' pnpm run dev # in backend/');
86
+ console.log(' pnpm run dev # in frontend/');
87
+ }
88
+ catch (error) {
89
+ console.error('Error creating project:', error);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ }
94
+ async function getProjectName(providedName) {
95
+ if (providedName) {
96
+ return providedName;
97
+ }
98
+ const projectName = await prompt('Project name');
99
+ if (!projectName) {
100
+ console.error('Project name is required!');
101
+ process.exit(1);
102
+ }
103
+ return projectName;
104
+ }
105
+ async function getProjectSlug(providedSlug, projectName) {
106
+ if (providedSlug) {
107
+ return providedSlug;
108
+ }
109
+ const suggestedSlug = (0, utils_1.slugify)(projectName);
110
+ const projectSlug = await prompt('Project slug', suggestedSlug);
111
+ if (!projectSlug) {
112
+ console.error('Project slug is required!');
113
+ process.exit(1);
114
+ }
115
+ return projectSlug;
116
+ }
117
+ async function getProjectSchema(providedSchema) {
118
+ const schemaChoices = Object.keys(schema_1.sampleSchemas);
119
+ let selectedSchemaKey = providedSchema;
120
+ if (!selectedSchemaKey || !schemaChoices.includes(selectedSchemaKey)) {
121
+ const selectedSchemaIndex = await select('Which schema would you like to use as a starter?', schemaChoices, 3);
122
+ selectedSchemaKey = schemaChoices[selectedSchemaIndex];
123
+ }
124
+ const schema = schema_1.sampleSchemas[selectedSchemaKey];
125
+ if (!schema) {
126
+ console.error(`Schema "${selectedSchemaKey}" not found!`);
127
+ process.exit(1);
128
+ }
129
+ return schema;
130
+ }
131
+ /**
132
+ * If projectPath is provided:
133
+ * - if it is local within projects, check if it exists, if yes, throw error. if not, create it and return workspace project
134
+ * - if it is outside of the cwd (regardless of local or absolute), check if exists. if so, throw error, if not create and return standalone project
135
+ * If projectPath is not provided:
136
+ * - Ask user if it should be standalone (in ../{slug}) or workspace (in ./projects/{slug}). Default is standalone.
137
+ * - Create the folder and return the appropriate project path
138
+ */
139
+ async function resolveProjectPath({ slug, projectPath, }) {
140
+ const repositoryRoot = process.cwd();
141
+ const projectsFolder = path.join(repositoryRoot, 'projects');
142
+ const defaultStandalonePath = path.resolve(repositoryRoot, '..', slug);
143
+ const workspaceProjectPath = path.join(projectsFolder, slug);
144
+ if (projectPath) {
145
+ const projectGenerationPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(process.cwd(), projectPath);
146
+ await createFolderOrThrow(projectGenerationPath);
147
+ const isInProjectsFolder = projectGenerationPath.startsWith(projectsFolder + path.sep);
148
+ if (isInProjectsFolder) {
149
+ return { kind: 'workspace', projectGenerationPath };
150
+ }
151
+ else {
152
+ await createFolderOrThrow(workspaceProjectPath);
153
+ return {
154
+ kind: 'standalone',
155
+ workspaceProjectPath,
156
+ projectGenerationPath,
157
+ };
158
+ }
159
+ }
160
+ // No projectPath provided, ask user
161
+ const selectedOption = await select('Where should the project be created?', [`Standalone project at ${defaultStandalonePath}`, `Workspace project at ${workspaceProjectPath}`], 0);
162
+ const useWorkspace = selectedOption === 1;
163
+ if (useWorkspace) {
164
+ await createFolderOrThrow(workspaceProjectPath);
165
+ return {
166
+ kind: 'workspace',
167
+ projectGenerationPath: workspaceProjectPath,
168
+ };
169
+ }
170
+ else {
171
+ await createFolderOrThrow(workspaceProjectPath);
172
+ await createFolderOrThrow(defaultStandalonePath);
173
+ return {
174
+ kind: 'standalone',
175
+ workspaceProjectPath,
176
+ projectGenerationPath: defaultStandalonePath,
177
+ };
178
+ }
179
+ }
180
+ async function createFolderOrThrow(targetPath) {
181
+ try {
182
+ await fs.access(targetPath);
183
+ console.error(`Target path "${targetPath}" already exists! Please choose a different path.`);
184
+ process.exit(1);
185
+ }
186
+ catch {
187
+ // Path does not exist, create it
188
+ await fs.mkdir(targetPath, { recursive: true });
189
+ }
190
+ }
191
+ async function createProjectStructure({ name, slug, schema, projectPath, }) {
192
+ console.log(`\nCreating project "${name}" at ${projectPath.projectGenerationPath}...`);
193
+ await (0, create_project_generator_1.createProject)({
194
+ name: (0, schema_1.toProjectSchemaName)(name),
195
+ slug: (0, schema_1.toProjectSlug)(slug),
196
+ schema: schema,
197
+ projectPath,
198
+ });
199
+ console.log('Project files created successfully!');
200
+ }
201
+ async function installDependencies(path) {
202
+ console.log('\nInstalling dependencies...');
203
+ try {
204
+ const { stdout, stderr } = await execAsync('pnpm install', { cwd: path });
205
+ if (stdout) {
206
+ console.log(stdout);
207
+ }
208
+ if (stderr) {
209
+ console.error(stderr);
210
+ }
211
+ console.log('Dependencies installed successfully!');
212
+ }
213
+ catch (error) {
214
+ console.error('Failed to install dependencies:', error);
215
+ console.log('You can manually run "pnpm install" in the project directory.');
216
+ }
217
+ }
218
+ async function runGenerate(projectPath) {
219
+ console.log('\nGenerating project code...');
220
+ try {
221
+ const path = projectPath.kind === 'standalone' ? projectPath.workspaceProjectPath : projectPath.projectGenerationPath;
222
+ const { stdout, stderr } = await execAsync('pnpm run generate:project -f', {
223
+ cwd: path,
224
+ env: { ...process.env, PXL_PROJECT_PATH: projectPath.projectGenerationPath },
225
+ });
226
+ if (stdout) {
227
+ console.log(stdout);
228
+ }
229
+ if (stderr) {
230
+ console.error(stderr);
231
+ }
232
+ console.log('Project code generated successfully!');
233
+ }
234
+ catch (error) {
235
+ console.error('Failed to generate project code:', error);
236
+ console.log('You can manually run "pnpm run generate" in the project directory.');
237
+ }
238
+ }
239
+ async function generatePrismaClient(path) {
240
+ // 9. Generate Prisma
241
+ console.log('\nGenerating Prisma client...');
242
+ try {
243
+ const { stdout, stderr } = await execAsync('pnpm run generate:prisma', { cwd: path });
244
+ if (stdout) {
245
+ console.log(stdout);
246
+ }
247
+ if (stderr) {
248
+ console.error(stderr);
249
+ }
250
+ console.log('Prisma client generated successfully!');
251
+ }
252
+ catch (error) {
253
+ console.error('Failed to generate Prisma client:', error);
254
+ console.log('You can manually run "pnpm run generate:prisma" in the project directory.');
255
+ }
256
+ }
257
+ async function initializeGitRepository(path) {
258
+ console.log('\nInitializing git repository...');
259
+ try {
260
+ await execAsync('git init -b main', { cwd: path });
261
+ console.log('Git repository initialized successfully!');
262
+ }
263
+ catch (error) {
264
+ console.error('Failed to initialize git repository:', error);
265
+ }
266
+ }
267
+ /**
268
+ * Throws if is less than what .nvmrc requires
269
+ */
270
+ async function checkNodeVersion() {
271
+ try {
272
+ const repositoryRoot = process.cwd();
273
+ const nvmrcPath = path.join(repositoryRoot, '.nvmrc');
274
+ // Read the required version from .nvmrc
275
+ const nvmrcContent = await fs.readFile(nvmrcPath, 'utf-8');
276
+ const requiredVersion = nvmrcContent.trim().replace(/^v/, '');
277
+ // Get current Node.js version
278
+ const currentVersion = process.version.replace(/^v/, '');
279
+ // Compare major versions
280
+ const [requiredMajor] = requiredVersion.split('.').map(Number);
281
+ const [currentMajor] = currentVersion.split('.').map(Number);
282
+ if (currentMajor < requiredMajor) {
283
+ throw new Error(`Node.js version ${requiredVersion} or higher is required. Current version: ${currentVersion}`);
284
+ }
285
+ }
286
+ catch (error) {
287
+ if (error.code === 'ENOENT') {
288
+ // .nvmrc file not found, skip check
289
+ return;
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ // Helper to prompt user for input
295
+ async function prompt(question, defaultValue) {
296
+ const rl = readline.createInterface({
297
+ input: process.stdin,
298
+ output: process.stdout,
299
+ });
300
+ return new Promise((resolve) => {
301
+ const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
302
+ rl.question(promptText, (answer) => {
303
+ rl.close();
304
+ resolve(answer.trim() || defaultValue || '');
305
+ });
306
+ });
307
+ }
308
+ // Helper to select from a list
309
+ async function select(question, choices, defaultIndex = 0) {
310
+ if (choices.length === 0) {
311
+ throw new Error('No choices available');
312
+ }
313
+ if (defaultIndex < 0 || defaultIndex >= choices.length) {
314
+ defaultIndex = 0;
315
+ }
316
+ console.log(`\n${question}`);
317
+ for (const [index, choice] of choices.entries()) {
318
+ const num = index + 1;
319
+ const marker = index === defaultIndex ? ' (default)' : '';
320
+ console.log(` ${num}. ${choice}${marker}`);
321
+ }
322
+ const rl = readline.createInterface({
323
+ input: process.stdin,
324
+ output: process.stdout,
325
+ });
326
+ return new Promise((resolve) => {
327
+ const promptText = `\nEnter your choice (number) [default ${defaultIndex + 1}]: `;
328
+ rl.question(promptText, (answer) => {
329
+ rl.close();
330
+ const trimmed = answer.trim();
331
+ if (!trimmed) {
332
+ resolve(defaultIndex);
333
+ return;
334
+ }
335
+ const index = Number.parseInt(trimmed, 10) - 1;
336
+ if (index >= 0 && index < choices.length) {
337
+ resolve(index);
338
+ }
339
+ else {
340
+ console.log('Invalid choice, using default option.');
341
+ resolve(defaultIndex);
342
+ }
343
+ });
344
+ });
345
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function register(program: Command): void;
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.register = register;
40
+ const child_process_1 = require("child_process");
41
+ const fs = __importStar(require("fs/promises"));
42
+ const util_1 = require("util");
43
+ const dotenv_1 = __importDefault(require("dotenv"));
44
+ const path = __importStar(require("node:path"));
45
+ const zod_validation_error_1 = require("zod-validation-error");
46
+ const generator_1 = require("@postxl/generator");
47
+ const schema_1 = require("@postxl/schema");
48
+ const utils_1 = require("@postxl/utils");
49
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
50
+ function register(program) {
51
+ program
52
+ .command('generate', { isDefault: true })
53
+ .argument('[path]', 'The path to the project to generate. Default is current directory', process.cwd())
54
+ .argument('[target]', 'The target path to generate the project to. Default is [path]')
55
+ .summary('Generates the project.')
56
+ .description(`Generates the project.
57
+
58
+ This reads the schema from ${(0, utils_1.yellow)('schema.json')} and runs the generators specified in ${(0, utils_1.yellow)('generate.ts')}.`)
59
+ .option('-d, --diff', 'shows diff between ejected and generated files')
60
+ .option('-e, --ejected', 'lists ejected files')
61
+ .option('-f, --force', 'overwrites ejected files')
62
+ .option('-i, --ignore-errors', 'ignores errors during generation')
63
+ .option('-n, --no-fix', 'does not attempt to fix linting errors')
64
+ .option('-p, --pattern <glob>', 'filters files by glob pattern (e.g., "**/*.ts" or "backend/libs/**")')
65
+ .option('-q, --quiet', 'does not print errors')
66
+ .option('-s, --skip-lint', 'skips linting')
67
+ .option('-v, --verbose', 'shows verbose linting messages')
68
+ .action(async (projectPath, targetPath, options) => {
69
+ if (projectPath.startsWith('.')) {
70
+ projectPath = path.join(process.cwd(), projectPath);
71
+ }
72
+ try {
73
+ await fs.access(projectPath);
74
+ }
75
+ catch {
76
+ program.error(`Cannot find project path ${projectPath}!`);
77
+ }
78
+ const envConfig = dotenv_1.default.config({ path: path.join(projectPath, '.env') });
79
+ const localGenerators = await getLocalGenerators({ program, projectPath });
80
+ const schema = await getSchema({ program, projectPath });
81
+ targetPath = resolveTargetPath({
82
+ projectPath,
83
+ targetPath,
84
+ envTargetPath: process.env.PXL_PROJECT_PATH ?? envConfig.parsed?.PXL_PROJECT_PATH,
85
+ schemaSlug: schema.slug,
86
+ projectType: schema.projectType,
87
+ });
88
+ await fs.mkdir(targetPath, { recursive: true });
89
+ console.log(`Generating project from ${projectPath} to ${targetPath}`);
90
+ if (options.force) {
91
+ console.log((0, utils_1.yellow)('Warning: Forcing generation of ejected files!'));
92
+ }
93
+ if (options.pattern) {
94
+ console.log(`Filtering files by pattern: ${options.pattern}`);
95
+ }
96
+ const manager = new generator_1.GeneratorManager(localGenerators);
97
+ const baseContext = (0, generator_1.prepareBaseContext)(schema, {
98
+ ...options,
99
+ ...(options.pattern ? { filePattern: options.pattern } : {}),
100
+ });
101
+ const generatedContext = await manager.generate(baseContext);
102
+ return (new generator_1.Generator(generatedContext)
103
+ // Not sure why we need this - but without the next line fails 💥
104
+ .generate((g) => g)
105
+ //
106
+ .then(async (g) => {
107
+ const lintResult = await (0, generator_1.lint)(g.context.vfs, options);
108
+ if (!options.quiet) {
109
+ (0, generator_1.logLintResults)(lintResult, options);
110
+ }
111
+ await g.context.vfs.transform(generator_1.format, {
112
+ onError: ({ path, error }) => {
113
+ if (options.ignoreErrors) {
114
+ console.error(`Error formatting ${path}:`, error);
115
+ }
116
+ else {
117
+ throw error;
118
+ }
119
+ },
120
+ });
121
+ const result = await (0, generator_1.sync)({
122
+ vfs: g.context.vfs,
123
+ lockFilePath: path.join(targetPath, 'postxl-lock.json'),
124
+ diskFilePath: targetPath,
125
+ force: options.force ?? false,
126
+ });
127
+ (0, generator_1.logSyncResult)(result, {
128
+ showEjectedStats: options.ejected ?? false,
129
+ showEjectedDiff: options.diff ?? false,
130
+ });
131
+ return g;
132
+ })
133
+ .then((g) => {
134
+ if (g.context.options.errors.length > 0 && !options.quiet) {
135
+ console.error('Generation completed with errors:\n', g.context.options.errors.join('\n'));
136
+ }
137
+ }));
138
+ });
139
+ }
140
+ /**
141
+ * This function compiles the generate.ts file into a JS file and then imports and returns it
142
+ * via dynamic import.
143
+ */
144
+ async function getLocalGenerators(params) {
145
+ const { projectPath } = params;
146
+ const generateTsPath = path.join(projectPath, 'generate.ts');
147
+ const tsconfigPath = path.join(projectPath, 'tsconfig.generate.json');
148
+ const outDir = path.join(projectPath, 'dist');
149
+ const generateJsPath = path.join(outDir, 'generate.js');
150
+ // Check if `generate.ts` exists
151
+ try {
152
+ await fs.access(generateTsPath);
153
+ }
154
+ catch {
155
+ params.program.error(`Cannot find file "generate.ts" found at ${generateTsPath}`);
156
+ }
157
+ //write a tsconfig.json file that extends the base tsconfig.json but limits the file to generate.ts
158
+ const tsconfigJson = `{
159
+ "extends": "./tsconfig.json",
160
+ "compilerOptions": {
161
+ "noEmit": false,
162
+ "outDir": "./dist"
163
+ },
164
+ "include": ["generate.ts"]
165
+ }`;
166
+ await fs.writeFile(tsconfigPath, tsconfigJson);
167
+ // Run tsc to compile `generate.ts` into `dist/generate.js`
168
+ try {
169
+ await execAsync(`tsc -b tsconfig.generate.json`);
170
+ }
171
+ catch (err) {
172
+ if (err.stdout) {
173
+ console.log(err.stdout);
174
+ }
175
+ else if (err.stderr) {
176
+ console.log(err.stderr);
177
+ }
178
+ else {
179
+ console.log(JSON.stringify(err, null, 2));
180
+ }
181
+ params.program.error(`Failed to compile ${generateTsPath}:`, err);
182
+ }
183
+ // Check if `dist/generate.js` exists
184
+ try {
185
+ await fs.access(generateJsPath);
186
+ }
187
+ catch {
188
+ params.program.error(`No dist/generate.js found at ${generateJsPath}, skipping local generation.`);
189
+ }
190
+ // Load the transpiled `generate.js` from the `dist` folder
191
+ let localGenerate;
192
+ try {
193
+ const localGenerateFile = await import(`file://${generateJsPath}`);
194
+ if (localGenerateFile.default?.default) {
195
+ localGenerate = localGenerateFile.default.default;
196
+ }
197
+ else if (localGenerateFile.default?.generate) {
198
+ localGenerate = localGenerateFile.default.generate;
199
+ }
200
+ else if (typeof localGenerateFile.default === 'function') {
201
+ localGenerate = localGenerateFile.default;
202
+ }
203
+ else if (typeof localGenerateFile.generate === 'function') {
204
+ localGenerate = localGenerateFile.generate;
205
+ }
206
+ else {
207
+ params.program.error(`${generateTsPath} does not export a default or generate function!`);
208
+ }
209
+ }
210
+ catch (e) {
211
+ console.error(e);
212
+ params.program.error(`Error importing local generate.js at ${generateJsPath}`, e);
213
+ }
214
+ // Delete the tsconfig.generate.json file
215
+ try {
216
+ await fs.rm(tsconfigPath, { force: true });
217
+ }
218
+ catch {
219
+ /* empty */
220
+ }
221
+ return localGenerate();
222
+ }
223
+ async function getSchema(params) {
224
+ const { projectPath } = params;
225
+ const schemaPath = path.join(projectPath, 'postxl-schema.json');
226
+ try {
227
+ await fs.access(schemaPath);
228
+ }
229
+ catch {
230
+ params.program.error(`Cannot find file "postxl-schema.json" found at ${schemaPath}`);
231
+ }
232
+ const jsonSchema = JSON.parse((await fs.readFile(schemaPath)).toString());
233
+ const schema = schema_1.zProjectSchema.safeParse(jsonSchema);
234
+ if (!schema.success) {
235
+ params.program.error(`Error parsing postxl-schema.json: \n${(0, zod_validation_error_1.fromZodError)(schema.error)
236
+ .details.map((e) => e.message)
237
+ .join('\n')}`);
238
+ }
239
+ return schema.data;
240
+ }
241
+ function resolveTargetPath({ projectPath, targetPath, envTargetPath, schemaSlug, projectType, }) {
242
+ let resolved = targetPath ?? envTargetPath;
243
+ if (!resolved) {
244
+ if (projectType.kind === 'workspace') {
245
+ resolved = projectPath;
246
+ }
247
+ else {
248
+ const repositoryRoot = path.join(projectPath, '..', '..');
249
+ resolved = path.resolve(repositoryRoot, '..', schemaSlug);
250
+ }
251
+ }
252
+ if (resolved.startsWith('.') || !path.isAbsolute(resolved)) {
253
+ resolved = path.resolve(projectPath, resolved);
254
+ }
255
+ return resolved;
256
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const create_project_command_1 = require("./create-project.command");
6
+ const generate_command_1 = require("./generate.command");
7
+ const program = new commander_1.Command();
8
+ program
9
+ //
10
+ .name('pxl')
11
+ .description('CLI for PostXL')
12
+ .version('0.1.0');
13
+ // Adding all the commands
14
+ (0, generate_command_1.register)(program);
15
+ (0, create_project_command_1.register)(program);
16
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@postxl/cli",
3
+ "version": "0.0.9",
4
+ "description": "Command-line interface for PostXL code generation framework",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "pxl": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "license": "SEE LICENSE IN LICENSE",
19
+ "author": "PostXL GmbH",
20
+ "keywords": [
21
+ "postxl",
22
+ "pxl",
23
+ "cli",
24
+ "code-generation",
25
+ "typescript",
26
+ "nestjs",
27
+ "react"
28
+ ],
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc -b tsconfig.build.json",
40
+ "lint": "eslint .",
41
+ "prettier:check": "prettier --check \"**/*.{ts,tsx}\" --config ../../prettier.config.js --ignore-path ../../.prettierignore",
42
+ "test:types": "tsc --noEmit"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/postxl/pxl",
47
+ "directory": "packages/cli"
48
+ },
49
+ "dependencies": {
50
+ "@postxl/generator": "workspace:*",
51
+ "@postxl/generators": "workspace:*",
52
+ "@postxl/schema": "workspace:*",
53
+ "commander": "12.1.0",
54
+ "dotenv": "16.4.7",
55
+ "zod-validation-error": "3.4.0"
56
+ },
57
+ "devDependencies": {},
58
+ "wallaby": {
59
+ "env": {
60
+ "type": "node",
61
+ "params": {
62
+ "runner": "--experimental-vm-modules"
63
+ }
64
+ }
65
+ }
66
+ }