create-projx 1.1.2 → 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 +8 -81
  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) {
@@ -592,10 +591,6 @@ var NEVER_OVERWRITE = [
592
591
  /src\/migrations\/versions\//,
593
592
  /\.projx-component$/
594
593
  ];
595
- var MERGE_DEPS = [
596
- /^[^/]+\/package\.json$/,
597
- /^[^/]+\/pyproject\.toml$/
598
- ];
599
594
  function isGitRepo(cwd) {
600
595
  try {
601
596
  execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
@@ -700,8 +695,8 @@ async function update(cwd, localRepo) {
700
695
  Review changes:
701
696
  git diff ${originalBranch}...${branchName}
702
697
 
703
- Switch back and merge:
704
- git checkout ${originalBranch} && git merge ${branchName}`
698
+ Merge (resolve conflicts for files you customized):
699
+ git checkout ${originalBranch} && git merge --no-ff ${branchName}`
705
700
  );
706
701
  } else {
707
702
  const dlSpinner = p3.spinner();
@@ -752,16 +747,8 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
752
747
  if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
753
748
  const dir = dest.substring(0, dest.lastIndexOf("/"));
754
749
  await mkdir3(dir, { recursive: true });
755
- if (MERGE_DEPS.some((re) => re.test(destRel)) && existsSync3(dest)) {
756
- const merged = await mergeDeps(dest, src);
757
- if (merged) {
758
- await writeFile3(dest, merged);
759
- touchedFiles.push(destRel);
760
- }
761
- } else {
762
- await cp2(src, dest, { force: true });
763
- touchedFiles.push(destRel);
764
- }
750
+ await cp2(src, dest, { force: true });
751
+ touchedFiles.push(destRel);
765
752
  }
766
753
  await rm2(tmpDest, { recursive: true, force: true });
767
754
  if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
@@ -805,8 +792,7 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
805
792
  const updatedConfig = {
806
793
  version,
807
794
  components: config.components,
808
- createdAt: config.createdAt,
809
- paths: componentPaths
795
+ createdAt: config.createdAt
810
796
  };
811
797
  await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
812
798
  touchedFiles.push(".projx");
@@ -831,63 +817,6 @@ function detectProjectName(cwd, components, componentPaths) {
831
817
  }
832
818
  return toKebab(cwd.split("/").pop());
833
819
  }
834
- async function mergeDeps(existingPath, templatePath) {
835
- if (existingPath.endsWith("package.json")) {
836
- return mergePackageJson(existingPath, templatePath);
837
- }
838
- if (existingPath.endsWith("pyproject.toml")) {
839
- return mergePyprojectToml(existingPath, templatePath);
840
- }
841
- return null;
842
- }
843
- async function mergePackageJson(existingPath, templatePath) {
844
- const existingRaw = await readFileOrNull(existingPath);
845
- const templateRaw = await readFileOrNull(templatePath);
846
- if (!existingRaw || !templateRaw) return null;
847
- try {
848
- const existing = JSON.parse(existingRaw);
849
- const template = JSON.parse(templateRaw);
850
- if (template.dependencies) {
851
- existing.dependencies = { ...template.dependencies, ...existing.dependencies };
852
- }
853
- if (template.devDependencies) {
854
- existing.devDependencies = { ...template.devDependencies, ...existing.devDependencies };
855
- }
856
- if (template.scripts) {
857
- existing.scripts = { ...template.scripts, ...existing.scripts };
858
- }
859
- return JSON.stringify(existing, null, 2) + "\n";
860
- } catch {
861
- return null;
862
- }
863
- }
864
- async function mergePyprojectToml(existingPath, templatePath) {
865
- const existingRaw = await readFileOrNull(existingPath);
866
- const templateRaw = await readFileOrNull(templatePath);
867
- if (!existingRaw || !templateRaw) return null;
868
- const templateDeps = extractTomlDeps(templateRaw);
869
- if (templateDeps.length === 0) return null;
870
- const existingDeps = extractTomlDeps(existingRaw);
871
- const existingNames = new Set(existingDeps.map((d) => d.replace(/[><=!~[].*/, "").trim().toLowerCase()));
872
- const newDeps = templateDeps.filter((d) => {
873
- const name = d.replace(/[><=!~[].*/, "").trim().toLowerCase();
874
- return !existingNames.has(name);
875
- });
876
- if (newDeps.length === 0) return null;
877
- const depsMatch = existingRaw.match(/^dependencies\s*=\s*\[([^\]]*)\]/m);
878
- if (!depsMatch) return null;
879
- const closingBracket = existingRaw.indexOf("]", depsMatch.index);
880
- const before = existingRaw.slice(0, closingBracket);
881
- const after = existingRaw.slice(closingBracket);
882
- const indent = " ";
883
- const newLines = newDeps.map((d) => `${indent}"${d}",`).join("\n");
884
- return before.trimEnd() + "\n" + newLines + "\n" + after;
885
- }
886
- function extractTomlDeps(toml) {
887
- const match = toml.match(/^dependencies\s*=\s*\[([\s\S]*?)\]/m);
888
- if (!match) return [];
889
- return match[1].split("\n").map((l) => l.trim()).filter((l) => l.startsWith('"') || l.startsWith("'")).map((l) => l.replace(/^["']|["'],?$/g, "").trim()).filter(Boolean);
890
- }
891
820
 
892
821
  // src/add.ts
893
822
  import { copyFileSync as copyFileSync2, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
@@ -979,8 +908,7 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
979
908
  const updatedConfig = {
980
909
  version: pkg.version,
981
910
  components: allComponents,
982
- createdAt: config.createdAt,
983
- paths
911
+ createdAt: config.createdAt
984
912
  };
985
913
  await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
986
914
  p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
@@ -1314,8 +1242,7 @@ async function init(cwd, localRepo) {
1314
1242
  const projxConfig = {
1315
1243
  version: pkg.version,
1316
1244
  components,
1317
- createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1318
- paths
1245
+ createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1319
1246
  };
1320
1247
  await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
1321
1248
  p5.log.success(".projx");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.1.2",
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": {