create-projx 1.1.1 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +54 -15
  2. package/dist/index.js +34 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -54,32 +54,49 @@ Interactive prompt lets you pick components. Or specify them directly:
54
54
  npx create-projx my-app --components fastapi,fastify,frontend,mobile,e2e,infra
55
55
  ```
56
56
 
57
- ### Add Components Later
57
+ ### Adopt an Existing Project
58
+
59
+ Already have a project? Initialize projx to get the scaffolding (CI, hooks, docker-compose) without overwriting your code:
60
+
61
+ ```bash
62
+ cd my-existing-app
63
+ npx create-projx init
64
+ ```
58
65
 
59
- Already have a project? Add more components anytime:
66
+ Auto-detects components by scanning for `fastapi` in pyproject.toml, `react`/`fastify` in package.json, `flutter` in pubspec.yaml, and `.tf` files. Confirms each mapping, writes `.projx-component` markers, and generates only missing shared files. Existing files get a diff/overwrite/skip prompt.
67
+
68
+ ### Add Components Later
60
69
 
61
70
  ```bash
62
71
  cd my-app
63
72
  npx create-projx add frontend mobile
64
73
  ```
65
74
 
66
- This copies the new component directories, regenerates shared files (docker-compose, CI, pre-commit hooks) to include them, and installs dependencies.
75
+ Copies the new component directories, regenerates shared files (docker-compose, CI, pre-commit hooks) to include them, and installs dependencies.
67
76
 
68
77
  ### Update Scaffolding
69
78
 
70
- When we improve templates, update your project's scaffolding without touching your code:
79
+ When templates improve, update your project:
71
80
 
72
81
  ```bash
73
82
  cd my-app
74
83
  npx create-projx@latest update
75
84
  ```
76
85
 
77
- This updates template files (base models, middleware, configs, Dockerfiles, CI) tracked in `.projx` manifest. Files you created (new entities, pages, features) are never touched.
86
+ Creates a `projx/update-vX.X.X` branch with the latest template overlay. Your custom files are never deleted only template files are added or replaced. Merge with conflict resolution:
87
+
88
+ ```bash
89
+ git diff main...projx/update-v1.1.2 # review changes
90
+ git checkout main && git merge --no-ff projx/update-v1.1.2 # merge with conflicts
91
+ ```
92
+
93
+ Git shows conflicts on files you customized (controllers, middleware, configs). You keep your code, accept template improvements.
78
94
 
79
95
  ## Options
80
96
 
81
97
  ```
82
98
  npx create-projx <name> [options]
99
+ npx create-projx init
83
100
  npx create-projx add <components...>
84
101
  npx create-projx update
85
102
 
@@ -90,19 +107,34 @@ npx create-projx update
90
107
  -h, --help Show help
91
108
  ```
92
109
 
110
+ ## Rename Component Directories
111
+
112
+ Rename `fastapi/` to `backend/`? Just rename the folder — the `.projx-component` marker file moves with it. The `update` command auto-discovers where each component lives by scanning for these markers. No config changes needed.
113
+
114
+ ```
115
+ backend/.projx-component → { "components": ["fastapi"] }
116
+ web/.projx-component → { "components": ["frontend"] }
117
+ ```
118
+
119
+ CI, setup.sh, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
120
+
93
121
  ## What a Scaffolded Project Looks Like
94
122
 
95
123
  ```
96
124
  my-app/
97
- ├── fastapi/ # Auto-entity CRUD backend
98
- ├── frontend/ # Auto-entity UI from /_meta
99
- ├── e2e/ # Playwright E2E tests
100
- ├── docker-compose.yml # Production (backend + frontend + SSL)
101
- ├── docker-compose.dev.yml # Development (PostgreSQL + hot reload)
102
- ├── .github/workflows/ # CI per component
103
- ├── .githooks/pre-commit # Format + lint on commit
104
- ├── setup.sh # Install all deps
105
- └── .projx # Manifest (tracks template files for updates)
125
+ ├── fastapi/ # Auto-entity CRUD backend
126
+ │ └── .projx-component # Identifies this as the fastapi component
127
+ ├── frontend/ # Auto-entity UI from /_meta
128
+ │ └── .projx-component
129
+ ├── e2e/ # Playwright E2E tests
130
+ │ └── .projx-component
131
+ ├── docker-compose.yml # Production (backend + frontend + SSL)
132
+ ├── docker-compose.dev.yml # Development (PostgreSQL + hot reload)
133
+ ├── .github/workflows/ # CI per component (runs only on changes)
134
+ ├── .githooks/pre-commit # Format + lint on commit
135
+ ├── .vscode/ # Editor settings + recommended extensions
136
+ ├── setup.sh # Install all deps
137
+ └── .projx # Components list + version
106
138
  ```
107
139
 
108
140
  Only the components you selected appear. Shared files (docker-compose, CI, hooks) are generated to match your selection.
@@ -121,9 +153,10 @@ The core idea: define a data model, get everything else for free.
121
153
 
122
154
  - JWT auth with Keycloak (pluggable providers)
123
155
  - Docker Compose for dev and prod
124
- - GitHub Actions CI per component
156
+ - GitHub Actions CI per component (path-filtered — only runs when that component changes)
125
157
  - Pre-commit hooks (format + lint + typecheck)
126
158
  - Secret detection in pre-commit
159
+ - VS Code settings + recommended extensions
127
160
  - 80% test coverage enforced
128
161
  - Auto-entity discovery across all stacks
129
162
 
@@ -139,6 +172,12 @@ cd projx
139
172
 
140
173
  The CLI lives in `cli/`. Templates are the root-level component directories (`fastapi/`, `frontend/`, etc.).
141
174
 
175
+ ```bash
176
+ cd cli
177
+ npm test # run tests
178
+ npm run build # build CLI
179
+ ```
180
+
142
181
  ## License
143
182
 
144
183
  MIT
package/dist/index.js CHANGED
@@ -437,8 +437,7 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
437
437
  const projxConfig = {
438
438
  version: pkg.version,
439
439
  components: opts.components,
440
- createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
441
- paths: vars.paths
440
+ createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
442
441
  };
443
442
  await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2));
444
443
  if (opts.git) {
@@ -680,21 +679,24 @@ async function update(cwd, localRepo) {
680
679
  }
681
680
  execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
682
681
  p3.log.info(`Created branch: ${branchName}`);
682
+ let touchedFiles;
683
683
  try {
684
- await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
684
+ touchedFiles = await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
685
685
  } finally {
686
686
  await cleanupRepo(repoDir, isLocal);
687
687
  }
688
- execSync2("git add -A", { cwd, stdio: "pipe" });
689
- execSync2(`git commit -m "projx update to v${pkg.version}"`, { cwd, stdio: "pipe" });
688
+ for (const f of touchedFiles) {
689
+ execSync2(`git add "${f}"`, { cwd, stdio: "pipe" });
690
+ }
691
+ execSync2(`git commit --no-verify -m "projx update to v${pkg.version}"`, { cwd, stdio: "pipe" });
690
692
  p3.outro(
691
693
  `Updated on branch: ${branchName}
692
694
 
693
695
  Review changes:
694
696
  git diff ${originalBranch}...${branchName}
695
697
 
696
- Switch back and merge:
697
- git checkout ${originalBranch} && git merge ${branchName}`
698
+ Merge (resolve conflicts for files you customized):
699
+ git checkout ${originalBranch} && git merge --no-ff ${branchName}`
698
700
  );
699
701
  } else {
700
702
  const dlSpinner = p3.spinner();
@@ -720,8 +722,15 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
720
722
  const name = detectProjectName(cwd, config.components, componentPaths);
721
723
  const nameSnake = toSnake(name);
722
724
  const vars = { projectName: name, components: config.components, paths: componentPaths };
725
+ const touchedFiles = [];
726
+ const usedPaths = /* @__PURE__ */ new Set();
723
727
  for (const component of config.components) {
724
728
  const targetDir = componentPaths[component];
729
+ if (usedPaths.has(targetDir)) {
730
+ p3.log.warn(`${component} shares directory ${targetDir}/ with another component \u2014 skipping overlay to avoid nesting.`);
731
+ continue;
732
+ }
733
+ usedPaths.add(targetDir);
725
734
  const spinner6 = p3.spinner();
726
735
  spinner6.start(`Updating ${targetDir}/ (${component})`);
727
736
  const componentSrc = join4(repoDir, component);
@@ -739,10 +748,12 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
739
748
  const dir = dest.substring(0, dest.lastIndexOf("/"));
740
749
  await mkdir3(dir, { recursive: true });
741
750
  await cp2(src, dest, { force: true });
751
+ touchedFiles.push(destRel);
742
752
  }
743
753
  await rm2(tmpDest, { recursive: true, force: true });
744
754
  if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
745
755
  await writeComponentMarker(join4(cwd, targetDir), component);
756
+ touchedFiles.push(`${targetDir}/.projx-component`);
746
757
  }
747
758
  spinner6.stop(`${targetDir}/ updated.`);
748
759
  }
@@ -750,29 +761,24 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
750
761
  spinner5.start("Updating shared files");
751
762
  const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
752
763
  if (hasBackend || config.components.includes("frontend")) {
753
- await writeFile3(
754
- join4(cwd, "docker-compose.yml"),
755
- await generateDockerCompose(vars)
756
- );
757
- await writeFile3(
758
- join4(cwd, "docker-compose.dev.yml"),
759
- await generateDockerComposeDev(vars)
760
- );
764
+ await writeFile3(join4(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
765
+ touchedFiles.push("docker-compose.yml");
766
+ await writeFile3(join4(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
767
+ touchedFiles.push("docker-compose.dev.yml");
761
768
  }
762
769
  await mkdir3(join4(cwd, ".githooks"), { recursive: true });
763
- const preCommit = await generatePreCommit(vars);
764
- await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
770
+ await writeFile3(join4(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
765
771
  await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
772
+ touchedFiles.push(".githooks/pre-commit");
766
773
  await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
767
- await writeFile3(
768
- join4(cwd, ".github/workflows/ci.yml"),
769
- await generateCiYml(vars)
770
- );
771
- const setupSh = await generateSetupSh(vars);
772
- await writeFile3(join4(cwd, "setup.sh"), setupSh);
774
+ await writeFile3(join4(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
775
+ touchedFiles.push(".github/workflows/ci.yml");
776
+ await writeFile3(join4(cwd, "setup.sh"), await generateSetupSh(vars));
773
777
  await chmod2(join4(cwd, "setup.sh"), 493);
778
+ touchedFiles.push("setup.sh");
774
779
  await mkdir3(join4(cwd, ".vscode"), { recursive: true });
775
780
  await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
781
+ touchedFiles.push(".vscode/settings.json");
776
782
  spinner5.stop("Shared files updated.");
777
783
  if (config.components.includes("mobile")) {
778
784
  const mobilePath = componentPaths.mobile ?? "mobile";
@@ -786,10 +792,11 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
786
792
  const updatedConfig = {
787
793
  version,
788
794
  components: config.components,
789
- createdAt: config.createdAt,
790
- paths: componentPaths
795
+ createdAt: config.createdAt
791
796
  };
792
797
  await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
798
+ touchedFiles.push(".projx");
799
+ return touchedFiles;
793
800
  }
794
801
  function detectProjectName(cwd, components, componentPaths) {
795
802
  for (const component of components) {
@@ -901,8 +908,7 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
901
908
  const updatedConfig = {
902
909
  version: pkg.version,
903
910
  components: allComponents,
904
- createdAt: config.createdAt,
905
- paths
911
+ createdAt: config.createdAt
906
912
  };
907
913
  await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
908
914
  p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
@@ -1236,8 +1242,7 @@ async function init(cwd, localRepo) {
1236
1242
  const projxConfig = {
1237
1243
  version: pkg.version,
1238
1244
  components,
1239
- createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1240
- paths
1245
+ createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1241
1246
  };
1242
1247
  await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
1243
1248
  p5.log.success(".projx");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Scaffold production-grade projects. Pick your stack (FastAPI, Fastify, React, Flutter), get a fully wired template with auth, database, CI/CD, and E2E tests.",
5
5
  "type": "module",
6
6
  "bin": {