create-projx 1.7.3 → 1.7.5

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/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![CI](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml/badge.svg)](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml)
5
5
  [![GitHub stars](https://img.shields.io/github/stars/ukanhaupa/projx)](https://github.com/ukanhaupa/projx)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![Sponsor](https://img.shields.io/github/sponsors/ukanhaupa?label=Sponsor&logo=GitHub)](https://github.com/sponsors/ukanhaupa)
7
8
 
8
9
  **Go from blank folder to production-ready project in 30 seconds.** Backend-only API, AI/ML app, mobile, full-stack, infra setup — pick what you need and get it wired with auth, database, Docker, CI/CD, hooks, and tests. All optional. All yours.
9
10
 
@@ -85,20 +86,76 @@ If this saves you even one hour, it's already paid for itself. (It's free.)
85
86
 
86
87
  ## What you get
87
88
 
88
- | Component | Stack | What it gives you |
89
- | ---------- | ------------------------------------------------------------- | -------------------------------------------------------------------------- |
90
- | `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
91
- | `fastify` | Node.js, Prisma / Drizzle / Sequelize / TypeORM, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs |
92
- | `express` | Express 5, TypeScript, Prisma / Drizzle / Sequelize / TypeORM | Auto-entity CRUD, JWT auth, validation, security middleware, health checks |
93
- | `frontend` | React 19, TypeScript, Vite | Auth, theming, design tokens, light/dark mode |
94
- | `mobile` | Flutter, Riverpod, GoRouter | Auth, biometric, theming, GoRouter shell |
95
- | `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
96
- | `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
89
+ | Component | Stack | What it gives you |
90
+ | ------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
91
+ | `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
92
+ | `fastify` | Node.js, Prisma / Drizzle / Sequelize / TypeORM, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs |
93
+ | `express` | Express 5, TypeScript, Prisma / Drizzle / Sequelize / TypeORM | Auto-entity CRUD, JWT auth, validation, security middleware, health checks |
94
+ | `frontend` | React 19, TypeScript, Vite | Auth, theming, design tokens, light/dark mode |
95
+ | `mobile` | Flutter, Riverpod, GoRouter | Auth, biometric, theming, GoRouter shell |
96
+ | `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
97
+ | `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
98
+ | `admin-panel` | Go, HTMX (Docker), Postgres | Auth-gated table browser over any Postgres, read-only by default, internal-only behind nginx |
97
99
 
98
100
  Plus, in every project: Docker Compose for dev + prod, GitHub Actions CI per component (path-filtered), pre-commit hooks, secret detection, VS Code settings, and 80% test coverage enforced.
99
101
 
100
102
  All optional. Pick any combination.
101
103
 
104
+ ## Optional features
105
+
106
+ Components are the floor. Features are the opt-in modules that ride on top — same standard, same CI, same skip-list discipline. Today there's one, and it's the one everyone rewrites: **auth**.
107
+
108
+ ```bash
109
+ # Fastify + Prisma (default ORM)
110
+ npx create-projx my-app --components fastify --auth fastify
111
+
112
+ # Express + Drizzle
113
+ npx create-projx my-app --components express --orm drizzle --auth express
114
+
115
+ # FastAPI + SQLAlchemy
116
+ npx create-projx my-app --components fastapi --auth fastapi
117
+
118
+ # Multiple backends, one flag — comma-separable targets
119
+ npx create-projx my-app --components fastify,express --auth fastify,express
120
+
121
+ # Add auth to an existing project
122
+ npx create-projx add frontend --auth fastify
123
+ ```
124
+
125
+ `update` re-applies any feature recorded on a component, so template upgrades never strip your auth wiring.
126
+
127
+ ### What `--auth` ships
128
+
129
+ - Email + password signup — first user auto-promoted to admin
130
+ - Login with JWT access token and refresh-token rotation, with replay detection
131
+ - Account lockout after 5 failed logins (15-minute cooldown)
132
+ - MFA via TOTP authenticator app — enroll via otpauth URL (client renders the QR), verify on challenge
133
+ - MFA recovery codes — generate, single-use consume, regenerate
134
+ - MFA lockout on repeated bad codes with time-based unlock
135
+ - Password reset via emailed single-use token (30-minute TTL)
136
+ - Email verification with resend endpoint (24-hour TTL token)
137
+ - Authenticated password change — revokes all other sessions
138
+ - Active session listing
139
+ - Current-user lookup via `/me`
140
+ - Role-based permissions baked into the JWT payload
141
+ - SMTP mailer for verification and reset emails — falls back to logging the link when SMTP is unset
142
+ - Cron-driven cleanup of expired tokens (toggle via `AUTH_BACKGROUND_JOBS`)
143
+ - Centralized error responses with `request_id` propagation
144
+
145
+ Sixteen endpoints across signup, login, MFA challenge/enroll/disable, recovery-code regen, refresh, logout, change-password, sessions, forgot/reset password, verify/resend email, me. Same surface on every backend — mounted at `/auth/*` on fastify and express, `/api/v1/auth/*` on fastapi.
146
+
147
+ ### Backend × ORM compatibility
148
+
149
+ | Backend | Prisma | Drizzle | Sequelize | TypeORM | SQLAlchemy |
150
+ | --------- | ------ | ------- | --------- | ------- | ---------- |
151
+ | `fastify` | yes | yes | yes | yes | — |
152
+ | `express` | yes | yes | yes | yes | — |
153
+ | `fastapi` | — | — | — | — | yes |
154
+
155
+ Nine combinations, one external contract. ORM-specific bits live under [features/auth/](features/auth/) (per-stack, per-ORM subdirectories); the shared surface per stack lives under each stack's `common/` subdirectory. Env vars the feature reads: `JWT_SECRET`, `FRONTEND_URL`, `AUTH_BACKGROUND_JOBS` (all backends); `JWT_ALGORITHMS`, `MFA_ISSUER`, `AUTH_CLEANUP_INTERVAL_SECONDS` (fastapi).
156
+
157
+ Full feature-template spec — manifests, patches, anchors, idempotency — in [docs/feature-templates.md](docs/feature-templates.md).
158
+
102
159
  ## Built for humans and AI agents
103
160
 
104
161
  Projx is a shared operating system for teams that ship with both:
@@ -462,6 +519,12 @@ Add this to your project's README:
462
519
 
463
520
  ---
464
521
 
522
+ ## Sponsor
523
+
524
+ Projx is free and MIT-licensed. If it saves you time, consider [sponsoring its development](https://github.com/sponsors/ukanhaupa) — it keeps the templates current and the roadmap moving.
525
+
526
+ ---
527
+
465
528
  ## License
466
529
 
467
530
  MIT
@@ -8,8 +8,8 @@ import {
8
8
  matchesSkip,
9
9
  saveBaselineRef,
10
10
  writeTemplateToDir
11
- } from "./chunk-RA6FWWUM.js";
12
- import "./chunk-3NL6OTAP.js";
11
+ } from "./chunk-RFHLWYJ4.js";
12
+ import "./chunk-B7PW6QO7.js";
13
13
  export {
14
14
  BASELINE_REF,
15
15
  applyTemplate,
@@ -23,7 +23,8 @@ var COMPONENTS = [
23
23
  "frontend",
24
24
  "mobile",
25
25
  "e2e",
26
- "infra"
26
+ "infra",
27
+ "admin-panel"
27
28
  ];
28
29
  var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
29
30
  var ORM_PROVIDERS = [
@@ -296,7 +297,10 @@ function parseMarker(raw) {
296
297
  if (!component) return null;
297
298
  return {
298
299
  component,
299
- skip: Array.isArray(data.skip) ? data.skip : []
300
+ skip: Array.isArray(data.skip) ? data.skip : [],
301
+ features: Array.isArray(data.features) ? data.features.filter(
302
+ (f) => typeof f === "string"
303
+ ) : void 0
300
304
  };
301
305
  } catch {
302
306
  return null;
@@ -309,9 +313,25 @@ async function readComponentMarker(dir) {
309
313
  }
310
314
  async function writeComponentMarker(dir, data) {
311
315
  const markerPath = join(dir, COMPONENT_MARKER);
316
+ const existing = await readFileOrNull(markerPath);
317
+ let preservedFeatures;
318
+ if (existing) {
319
+ try {
320
+ const prev = JSON.parse(existing);
321
+ if (Array.isArray(prev.features)) {
322
+ preservedFeatures = prev.features.filter(
323
+ (f) => typeof f === "string"
324
+ );
325
+ }
326
+ } catch {
327
+ preservedFeatures = void 0;
328
+ }
329
+ }
330
+ const features = data.features ?? preservedFeatures;
312
331
  const out = {
313
332
  component: data.component,
314
- skip: Array.isArray(data.skip) ? data.skip : []
333
+ skip: Array.isArray(data.skip) ? data.skip : [],
334
+ ...features && features.length > 0 ? { features } : {}
315
335
  };
316
336
  await writeFile(markerPath, JSON.stringify(out, null, 2) + "\n");
317
337
  }
@@ -13,7 +13,7 @@ import {
13
13
  toSnake,
14
14
  upsertComponentMarker,
15
15
  writeProjxConfig
16
- } from "./chunk-3NL6OTAP.js";
16
+ } from "./chunk-B7PW6QO7.js";
17
17
 
18
18
  // src/baseline.ts
19
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
@@ -44,7 +44,8 @@ var CANONICAL_DISPLAY = {
44
44
  frontend: "Frontend",
45
45
  mobile: "Flutter",
46
46
  e2e: "E2E",
47
- infra: "Terraform"
47
+ infra: "Terraform",
48
+ "admin-panel": "Admin Panel"
48
49
  };
49
50
  function withInstances(vars) {
50
51
  const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({
@@ -66,7 +67,8 @@ function withInstances(vars) {
66
67
  frontendInstances: byType("frontend"),
67
68
  mobileInstances: byType("mobile"),
68
69
  e2eInstances: byType("e2e"),
69
- infraInstances: byType("infra")
70
+ infraInstances: byType("infra"),
71
+ adminPanelInstances: byType("admin-panel")
70
72
  };
71
73
  }
72
74
  async function renderShared(filename, vars) {
@@ -126,7 +128,7 @@ function generateVscodeSettings(vars) {
126
128
  // src/baseline.ts
127
129
  var BASELINE_REF = "refs/projx/baseline";
128
130
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
129
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-W4CWICA7.js");
131
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-X2P47QNN.js");
130
132
  for (const component of components) {
131
133
  const dir = componentPaths[component];
132
134
  const markerDir = join2(cwd, dir);
@@ -423,7 +425,7 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
423
425
  if (!matchesSkip(file, effectiveSkip)) return true;
424
426
  return !existsSync(join2(realCwd, file));
425
427
  };
426
- if (hasBackend || components.includes("frontend")) {
428
+ if (hasBackend || components.includes("frontend") || components.includes("admin-panel")) {
427
429
  if (shouldWrite("docker-compose.yml"))
428
430
  await writeFile(
429
431
  join2(dest, "docker-compose.yml"),
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  matchesSkip,
10
10
  saveBaselineRef,
11
11
  writeTemplateToDir
12
- } from "./chunk-RA6FWWUM.js";
12
+ } from "./chunk-RFHLWYJ4.js";
13
13
  import {
14
14
  COMPONENTS,
15
15
  COMPONENT_MARKER,
@@ -36,11 +36,12 @@ import {
36
36
  toSnake,
37
37
  writeComponentMarker,
38
38
  writeProjxConfig
39
- } from "./chunk-3NL6OTAP.js";
39
+ } from "./chunk-B7PW6QO7.js";
40
40
 
41
41
  // src/index.ts
42
42
  import { existsSync as existsSync11 } from "fs";
43
43
  import { resolve } from "path";
44
+ import { pathToFileURL } from "url";
44
45
 
45
46
  // src/features.ts
46
47
  import { existsSync } from "fs";
@@ -186,16 +187,16 @@ async function readManifest(featureDir, feature) {
186
187
  }
187
188
  return manifest;
188
189
  }
189
- async function applyTarget(args2) {
190
- const stackDir = join(args2.featureDir, args2.target.component);
190
+ async function applyTarget(args) {
191
+ const stackDir = join(args.featureDir, args.target.component);
191
192
  if (!existsSync(stackDir)) return;
192
- const targetPath = join(args2.dest, args2.target.path);
193
+ const targetPath = join(args.dest, args.target.path);
193
194
  if (!existsSync(targetPath)) {
194
195
  throw new Error(
195
- `Target instance path ${args2.target.path} not found in ${args2.dest}.`
196
+ `Target instance path ${args.target.path} not found in ${args.dest}.`
196
197
  );
197
198
  }
198
- const orm = typeof args2.vars.orm === "string" ? args2.vars.orm : void 0;
199
+ const orm = typeof args.vars.orm === "string" ? args.vars.orm : void 0;
199
200
  const ormDir = orm ? join(stackDir, orm) : void 0;
200
201
  const hasOrmDir = ormDir !== void 0 && existsSync(ormDir);
201
202
  const ormPatchNames = /* @__PURE__ */ new Set();
@@ -210,7 +211,7 @@ async function applyTarget(args2) {
210
211
  for (const sub of ["files", join("common", "files")]) {
211
212
  const filesDir = join(stackDir, sub);
212
213
  if (existsSync(filesDir)) {
213
- await renderFilesInto(filesDir, targetPath, args2.vars);
214
+ await renderFilesInto(filesDir, targetPath, args.vars);
214
215
  }
215
216
  }
216
217
  for (const sub of ["patches", join("common", "patches")]) {
@@ -219,7 +220,7 @@ async function applyTarget(args2) {
219
220
  await applyPatches(
220
221
  patchesDir,
221
222
  targetPath,
222
- args2.featureName,
223
+ args.featureName,
223
224
  ormPatchNames,
224
225
  hasOrmDir
225
226
  );
@@ -228,18 +229,18 @@ async function applyTarget(args2) {
228
229
  if (hasOrmDir) {
229
230
  const ormFilesDir = join(ormDir, "files");
230
231
  if (existsSync(ormFilesDir)) {
231
- await renderFilesInto(ormFilesDir, targetPath, args2.vars);
232
+ await renderFilesInto(ormFilesDir, targetPath, args.vars);
232
233
  }
233
234
  const ormPatchesDir = join(ormDir, "patches");
234
235
  if (existsSync(ormPatchesDir)) {
235
- await applyPatches(ormPatchesDir, targetPath, args2.featureName);
236
+ await applyPatches(ormPatchesDir, targetPath, args.featureName);
236
237
  }
237
238
  }
238
- const envKeys = args2.manifest.env?.[args2.target.component] ?? [];
239
+ const envKeys = args.manifest.env?.[args.target.component] ?? [];
239
240
  if (envKeys.length > 0) {
240
- await appendEnvExample(targetPath, args2.featureName, envKeys);
241
+ await appendEnvExample(targetPath, args.featureName, envKeys);
241
242
  }
242
- await recordFeatureInMarker(targetPath, args2.featureName);
243
+ await recordFeatureInMarker(targetPath, args.featureName);
243
244
  }
244
245
  async function renderFilesInto(filesDir, targetPath, vars) {
245
246
  const entries = await collectFiles(filesDir);
@@ -382,7 +383,11 @@ var LABELS = {
382
383
  frontend: { label: "Frontend", hint: "React 19 + Vite + React Router" },
383
384
  mobile: { label: "Mobile", hint: "Flutter + Riverpod + GoRouter" },
384
385
  e2e: { label: "E2E Tests", hint: "Playwright" },
385
- infra: { label: "Infrastructure", hint: "Terraform + AWS" }
386
+ infra: { label: "Infrastructure", hint: "Terraform + AWS" },
387
+ "admin-panel": {
388
+ label: "Admin Panel",
389
+ hint: "Go + HTMX \u2014 auth-gated table browser over any Postgres"
390
+ }
386
391
  };
387
392
  var DEFAULTS = ["fastify", "frontend", "e2e"];
388
393
  async function runPrompts(nameArg) {
@@ -607,6 +612,8 @@ async function installDeps(dest, components, pm) {
607
612
  break;
608
613
  case "infra":
609
614
  break;
615
+ case "admin-panel":
616
+ break;
610
617
  }
611
618
  } catch {
612
619
  spinner6.stop(`Failed to install ${component} dependencies.`);
@@ -632,6 +639,32 @@ import { readFile as readFile3, unlink } from "fs/promises";
632
639
  import { execSync } from "child_process";
633
640
  import { join as join3 } from "path";
634
641
  import * as p3 from "@clack/prompts";
642
+ async function collectRecordedFeatures(cwd, components, componentPaths) {
643
+ const byFeature = /* @__PURE__ */ new Map();
644
+ for (const component of components) {
645
+ const dir = componentPaths[component] ?? component;
646
+ const raw = await readFile3(join3(cwd, dir, COMPONENT_MARKER), "utf-8").catch(
647
+ () => null
648
+ );
649
+ if (!raw) continue;
650
+ let features;
651
+ try {
652
+ features = JSON.parse(raw).features;
653
+ } catch {
654
+ continue;
655
+ }
656
+ if (!Array.isArray(features)) continue;
657
+ for (const name of features) {
658
+ if (typeof name !== "string") continue;
659
+ const list = byFeature.get(name) ?? [];
660
+ list.push(dir);
661
+ byFeature.set(name, list);
662
+ }
663
+ }
664
+ const out = {};
665
+ for (const [feature, targets] of byFeature) out[feature] = targets.join(",");
666
+ return out;
667
+ }
635
668
  async function update(cwd, localRepo) {
636
669
  p3.intro("projx update");
637
670
  const isLocal = !!localRepo;
@@ -766,6 +799,24 @@ async function update(cwd, localRepo) {
766
799
  extraInstances
767
800
  );
768
801
  spinner6.stop("Template applied.");
802
+ const recordedFeatures = await collectRecordedFeatures(
803
+ cwd,
804
+ components,
805
+ componentPaths
806
+ );
807
+ if (Object.keys(recordedFeatures).length > 0) {
808
+ const featSpinner = p3.spinner();
809
+ featSpinner.start("Re-applying recorded features");
810
+ await applyFeatures({
811
+ features: recordedFeatures,
812
+ repoDir,
813
+ components,
814
+ instances,
815
+ dest: cwd,
816
+ vars
817
+ });
818
+ featSpinner.stop("Features re-applied.");
819
+ }
769
820
  const pinnedUpdates = await findPinnedFilesWithUpdates(
770
821
  cwd,
771
822
  repoDir,
@@ -851,7 +902,7 @@ function hasUncommittedChanges(cwd) {
851
902
  async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
852
903
  const { mkdtemp: mkdtemp2, rm: rm2, readFile: readFile8 } = await import("fs/promises");
853
904
  const { tmpdir: tmpdir2 } = await import("os");
854
- const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-HNSDAHQ4.js");
905
+ const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-O25CAKIL.js");
855
906
  const config = await readProjxConfig(cwd);
856
907
  const rootPinned = Array.isArray(config.skip) ? config.skip : [];
857
908
  const componentPinned = [];
@@ -1036,7 +1087,7 @@ import { copyFileSync as copyFileSync2, existsSync as existsSync4 } from "fs";
1036
1087
  import { readFile as readFile4 } from "fs/promises";
1037
1088
  import { join as join4 } from "path";
1038
1089
  import * as p4 from "@clack/prompts";
1039
- async function add(cwd, newComponents, localRepo, skipInstall = false, customName) {
1090
+ async function add(cwd, newComponents, localRepo, skipInstall = false, customName, features) {
1040
1091
  p4.intro("projx add");
1041
1092
  const isLocal = !!localRepo;
1042
1093
  if (!existsSync4(join4(cwd, ".projx"))) {
@@ -1124,6 +1175,19 @@ async function add(cwd, newComponents, localRepo, skipInstall = false, customNam
1124
1175
  { realCwd: cwd }
1125
1176
  );
1126
1177
  spinner6.stop("Components added.");
1178
+ if (features && Object.keys(features).length > 0) {
1179
+ const featSpinner = p4.spinner();
1180
+ featSpinner.start("Applying features");
1181
+ await applyFeatures({
1182
+ features,
1183
+ repoDir,
1184
+ components: allComponents,
1185
+ instances,
1186
+ dest: cwd,
1187
+ vars
1188
+ });
1189
+ featSpinner.stop("Features applied.");
1190
+ }
1127
1191
  if (!skipInstall) {
1128
1192
  await installDeps2(
1129
1193
  cwd,
@@ -1322,6 +1386,8 @@ async function installDeps2(dest, instances, pm) {
1322
1386
  break;
1323
1387
  case "infra":
1324
1388
  break;
1389
+ case "admin-panel":
1390
+ break;
1325
1391
  }
1326
1392
  } catch {
1327
1393
  spinner6.stop(`Failed to install ${type} dependencies (${path}/).`);
@@ -1418,6 +1484,15 @@ async function scanDirectory(dir, relPath) {
1418
1484
  evidence: "Terraform .tf files found"
1419
1485
  });
1420
1486
  }
1487
+ const goMod = await readFileOrNull(join5(dir, "go.mod"));
1488
+ if (goMod && /^module\s+adminpanel\b/m.test(goMod)) {
1489
+ results.push({
1490
+ component: "admin-panel",
1491
+ directory: relPath,
1492
+ confidence: "high",
1493
+ evidence: 'Go module "adminpanel" found'
1494
+ });
1495
+ }
1421
1496
  return results;
1422
1497
  }
1423
1498
  async function readPkg(dir) {
@@ -4086,7 +4161,6 @@ async function runGen(opts) {
4086
4161
  }
4087
4162
 
4088
4163
  // src/index.ts
4089
- var args = process.argv.slice(2);
4090
4164
  function matchFeatureFlag(arg, argv, i) {
4091
4165
  for (const feat of KNOWN_FEATURES) {
4092
4166
  const eq = `--${feat}=`;
@@ -4109,7 +4183,8 @@ function matchFeatureFlag(arg, argv, i) {
4109
4183
  }
4110
4184
  return null;
4111
4185
  }
4112
- function parseArgs() {
4186
+ function parseArgs(argv = process.argv.slice(2)) {
4187
+ const args = argv;
4113
4188
  let command = "create";
4114
4189
  let name;
4115
4190
  let localRepo;
@@ -4249,7 +4324,7 @@ function printHelp() {
4249
4324
  projx gen entity <name> Generate a new entity
4250
4325
 
4251
4326
  Options:
4252
- --components <list> Comma-separated: fastapi,fastify,express,frontend,mobile,e2e,infra
4327
+ --components <list> Comma-separated: fastapi,fastify,express,frontend,mobile,e2e,infra,admin-panel
4253
4328
  --orm <provider> Node backend ORM: prisma (default), drizzle, sequelize, typeorm
4254
4329
  --auth <targets> Add auth feature. Targets: <component>[:<instance>] (comma-separated)
4255
4330
  --no-git Skip git init
@@ -4263,6 +4338,7 @@ function printHelp() {
4263
4338
  npx create-projx my-app --components fastapi,frontend,e2e
4264
4339
  npx create-projx my-app --components express,frontend,e2e --orm drizzle
4265
4340
  npx create-projx my-app --components fastify,frontend,mobile --auth fastify
4341
+ npx create-projx my-app --components fastify,frontend,admin-panel
4266
4342
  npx create-projx my-app -y
4267
4343
  npx create-projx add frontend mobile
4268
4344
  npx create-projx add fastify --name email-ingestor
@@ -4306,7 +4382,8 @@ async function main() {
4306
4382
  components,
4307
4383
  localRepo,
4308
4384
  options.install === false,
4309
- customName
4385
+ customName,
4386
+ options.features
4310
4387
  );
4311
4388
  return;
4312
4389
  }
@@ -4379,7 +4456,14 @@ async function main() {
4379
4456
  }
4380
4457
  await scaffold(opts, dest, localRepo);
4381
4458
  }
4382
- main().catch((err) => {
4383
- console.error(err);
4384
- process.exit(1);
4385
- });
4459
+ var isEntrypoint = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
4460
+ if (isEntrypoint) {
4461
+ main().catch((err) => {
4462
+ console.error(err);
4463
+ process.exit(1);
4464
+ });
4465
+ }
4466
+ export {
4467
+ matchFeatureFlag,
4468
+ parseArgs
4469
+ };
@@ -35,7 +35,7 @@ import {
35
35
  upsertComponentMarker,
36
36
  writeComponentMarker,
37
37
  writeProjxConfig
38
- } from "./chunk-3NL6OTAP.js";
38
+ } from "./chunk-B7PW6QO7.js";
39
39
  export {
40
40
  COMPONENTS,
41
41
  COMPONENT_MARKER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, Express, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,9 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
27
27
  <% if (components.includes('infra')) { %>
28
28
  | **<%= paths.infra %>/** | Terraform, AWS (EKS, RDS, VPC, CodePipeline) |
29
29
  <% } %>
30
+ <% if (components.includes('admin-panel')) { %>
31
+ | **<%= paths['admin-panel'] %>/** | Go + HTMX admin panel — auth-gated table browser over Postgres |
32
+ <% } %>
30
33
  | **Identity** | OIDC / JWT |
31
34
  | **Containers** | Docker, Docker Compose |
32
35
 
@@ -83,6 +86,20 @@ cd <%= paths.frontend %> && cp .env.example .env && <%= pm.install %> && <%= pm.
83
86
  cd <%= paths.mobile %> && cp .env.example .env && flutter pub get && flutter run
84
87
  ```
85
88
  <% } %>
89
+ <% if (components.includes('admin-panel')) { %>
90
+
91
+ ### <%= paths['admin-panel'] %>/
92
+
93
+ ```bash
94
+ cd <%= paths['admin-panel'] %> && cp .env.example .env # set DATABASE_URL, SESSION_SECRET, ADMIN_EMAIL, ADMIN_PASSWORD
95
+ docker compose up --build <%= paths['admin-panel'] %>
96
+ ```
97
+ <% if (components.includes('frontend')) { %>
98
+ Internal-only — reach the admin UI at `https://localhost/admin/` through the frontend proxy.
99
+ <% } else { %>
100
+ Internal-only — publish the port for local admin work: `docker compose run --service-ports --rm <%= paths['admin-panel'] %>`.
101
+ <% } %>
102
+ <% } %>
86
103
 
87
104
  ## Testing
88
105
 
@@ -39,6 +39,9 @@ jobs:
39
39
  <% } %>
40
40
  <% for (const inst of infraInstances) { %>
41
41
  <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
42
+ <% } %>
43
+ <% for (const inst of adminPanelInstances) { %>
44
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
42
45
  <% } %>
43
46
  steps:
44
47
  - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
@@ -77,6 +80,10 @@ jobs:
77
80
  <%= inst.path %>:
78
81
  - '<%= inst.path %>/**'
79
82
  <% } %>
83
+ <% for (const inst of adminPanelInstances) { %>
84
+ <%= inst.path %>:
85
+ - '<%= inst.path %>/**'
86
+ <% } %>
80
87
 
81
88
  secrets:
82
89
  name: Secret scan
@@ -385,3 +392,26 @@ jobs:
385
392
  - run: terraform init -backend=false
386
393
  - run: terraform validate
387
394
  <% } %>
395
+ <% for (const inst of adminPanelInstances) { %>
396
+
397
+ <%= inst.path %>:
398
+ name: <%= inst.display %>
399
+ needs: changes
400
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
401
+ runs-on: ubuntu-latest
402
+ defaults:
403
+ run:
404
+ working-directory: ${{ env.WS }}/<%= inst.path %>
405
+ steps:
406
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
407
+ with:
408
+ path: ${{ env.WS }}
409
+ - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
410
+ with:
411
+ go-version: '1.26'
412
+ cache-dependency-path: ${{ env.WS }}/<%= inst.path %>/go.sum
413
+ - run: test -z "$(gofmt -l .)"
414
+ - run: go vet ./...
415
+ - run: go build ./...
416
+ - run: docker build -t <%= inst.path %>:ci .
417
+ <% } %>
@@ -157,6 +157,23 @@ services:
157
157
  networks:
158
158
  - app-network
159
159
  <% } %>
160
+ <% for (const inst of adminPanelInstances) { %>
161
+ <%= inst.path %>:
162
+ build: ./<%= inst.path %>
163
+ expose:
164
+ - "8055"
165
+ env_file:
166
+ - ./<%= inst.path %>/.env
167
+ restart: unless-stopped
168
+ healthcheck:
169
+ test: ["CMD", "/admin", "healthcheck"]
170
+ interval: 30s
171
+ timeout: 5s
172
+ retries: 3
173
+ start_period: 10s
174
+ networks:
175
+ - app-network
176
+ <% } %>
160
177
  <% if (frontendInstances.length > 0) { %>
161
178
  certbot:
162
179
  image: certbot/certbot:latest
@@ -157,3 +157,17 @@ if [ -n "$<%= inst.upper %>_TF" ]; then
157
157
  fi
158
158
  fi
159
159
  <% } %>
160
+ <% for (const inst of adminPanelInstances) { %>
161
+
162
+ <%= inst.upper %>_GO=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/.*\.go$' || true)
163
+ if [ -n "$<%= inst.upper %>_GO" ]; then
164
+ if command -v go &> /dev/null; then
165
+ echo "Checking <%= inst.path %>..."
166
+ echo "$<%= inst.upper %>_GO" | sed 's|^<%= inst.path %>/||' | (cd <%= inst.path %> && xargs gofmt -w)
167
+ (cd <%= inst.path %> && go vet ./...)
168
+ echo "$<%= inst.upper %>_GO" | xargs git add
169
+ else
170
+ echo "Skipping <%= inst.path %> checks (go not installed)"
171
+ fi
172
+ fi
173
+ <% } %>
@@ -121,6 +121,14 @@ else
121
121
  echo "<%= inst.display %> skipped (SDK not installed)."
122
122
  fi
123
123
  <% } %>
124
+ <% for (const inst of adminPanelInstances) { %>
125
+
126
+ if [ ! -f <%= inst.path %>/.env ] && [ -f <%= inst.path %>/.env.example ]; then
127
+ cp <%= inst.path %>/.env.example <%= inst.path %>/.env
128
+ echo "<%= inst.path %>/.env created from .env.example."
129
+ fi
130
+ echo "<%= inst.display %> configured (Docker — run 'docker compose up <%= inst.path %>')."
131
+ <% } %>
124
132
 
125
133
  echo ""
126
134
  echo "Done. Ensure PostgreSQL is running locally, then run '<%= pm.runDev %>' (or 'uv run main.py') in each service."