create-projx 1.6.3 → 1.6.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 +7 -7
- package/dist/{baseline-RXPDDEDD.js → baseline-PZM4KJJW.js} +2 -2
- package/dist/{chunk-LYPPFXGK.js → chunk-6YRBHJ2V.js} +35 -11
- package/dist/{chunk-OBYYB6PR.js → chunk-XQ7FE4U3.js} +67 -16
- package/dist/index.js +45 -37
- package/dist/{utils-BXHJP6HF.js → utils-AVKSTHIF.js} +1 -1
- package/package.json +1 -1
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/docker-compose.dev.yml.ejs +44 -29
- package/src/templates/docker-compose.yml.ejs +27 -25
- package/src/templates/setup.sh.ejs +20 -5
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Ask an LLM to "scaffold a full-stack app" and you get 50 files of plausible-look
|
|
|
39
39
|
```bash
|
|
40
40
|
npx create-projx my-app # interactive — pick exactly what you need
|
|
41
41
|
cd my-app
|
|
42
|
-
./setup.sh
|
|
42
|
+
./scripts/setup.sh # installs everything you picked
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Pick any combination of components — they're all optional:
|
|
@@ -116,7 +116,7 @@ npx create-projx my-app -y
|
|
|
116
116
|
|
|
117
117
|
## Package Manager Support
|
|
118
118
|
|
|
119
|
-
Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — setup.sh
|
|
119
|
+
Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — `scripts/setup.sh`, Docker, CI, pre-commit hooks, and README.
|
|
120
120
|
|
|
121
121
|
```json
|
|
122
122
|
{ "packageManager": "pnpm" }
|
|
@@ -166,7 +166,7 @@ Need a second backend service alongside an existing one (e.g. an SMTP listener n
|
|
|
166
166
|
npx create-projx add fastify --name email-ingestor
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Creates `email-ingestor/` with the fastify scaffold and a `.projx-component` marker. Each instance gets its own job in `.github/workflows/ci.yml`, its own section in `.githooks/pre-commit`, and its own install step in `setup.sh`. `update` keeps every instance refreshed on every run.
|
|
169
|
+
Creates `email-ingestor/` with the fastify scaffold and a `.projx-component` marker. Each instance gets its own job in `.github/workflows/ci.yml`, its own section in `.githooks/pre-commit`, and its own install step in `scripts/setup.sh`. `update` keeps every instance refreshed on every run.
|
|
170
170
|
|
|
171
171
|
### Update Scaffolding
|
|
172
172
|
|
|
@@ -191,7 +191,7 @@ Common user-owned files are **default-skipped** automatically — template updat
|
|
|
191
191
|
|
|
192
192
|
| Scope | Default skips |
|
|
193
193
|
|-------|---------------|
|
|
194
|
-
| Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `setup.sh` |
|
|
194
|
+
| Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `scripts/setup.sh`, `scripts/setup-docker.sh`, `scripts/setup-ssl.sh` |
|
|
195
195
|
| fastapi | `pyproject.toml` |
|
|
196
196
|
| fastify / frontend / e2e | `package.json` |
|
|
197
197
|
| mobile | `pubspec.yaml` |
|
|
@@ -336,7 +336,7 @@ backend/.projx-component → { "components": ["fastapi"] }
|
|
|
336
336
|
web/.projx-component → { "components": ["frontend"] }
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
CI, setup.sh
|
|
339
|
+
CI, `scripts/setup.sh`, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
|
|
340
340
|
|
|
341
341
|
## What a Scaffolded Project Looks Like
|
|
342
342
|
|
|
@@ -353,7 +353,7 @@ my-app/
|
|
|
353
353
|
├── .github/workflows/ # CI per component (runs only on changes)
|
|
354
354
|
├── .githooks/pre-commit # Format + lint on commit
|
|
355
355
|
├── .vscode/ # Editor settings + recommended extensions
|
|
356
|
-
├── setup.sh
|
|
356
|
+
├── scripts/ # setup.sh, setup-docker.sh, setup-ssl.sh
|
|
357
357
|
└── .projx # Components list + version
|
|
358
358
|
```
|
|
359
359
|
|
|
@@ -378,7 +378,7 @@ Contributing to Projx itself:
|
|
|
378
378
|
```bash
|
|
379
379
|
git clone https://github.com/ukanhaupa/projx.git
|
|
380
380
|
cd projx
|
|
381
|
-
./setup.sh
|
|
381
|
+
./scripts/setup.sh
|
|
382
382
|
```
|
|
383
383
|
|
|
384
384
|
The CLI lives in `cli/`. Templates are the root-level component directories (`fastapi/`, `frontend/`, etc.).
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmod,
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
readdir,
|
|
9
|
+
readFile,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "fs/promises";
|
|
5
13
|
import { join, resolve } from "path";
|
|
6
14
|
import { tmpdir } from "os";
|
|
7
15
|
import { fileURLToPath } from "url";
|
|
@@ -205,10 +213,19 @@ async function copyStaticFiles(repoDir, dest) {
|
|
|
205
213
|
await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
|
|
206
214
|
manifest.push(".vscode/extensions.json");
|
|
207
215
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
const staticScripts = ["setup-docker.sh", "setup-ssl.sh"];
|
|
217
|
+
const scriptsSrc = join(tpl, "scripts");
|
|
218
|
+
if (existsSync(scriptsSrc)) {
|
|
219
|
+
await mkdir(join(dest, "scripts"), { recursive: true });
|
|
220
|
+
for (const file of staticScripts) {
|
|
221
|
+
const src = join(scriptsSrc, file);
|
|
222
|
+
const dst = join(dest, "scripts", file);
|
|
223
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
224
|
+
await cp(src, dst);
|
|
225
|
+
await chmod(dst, 493);
|
|
226
|
+
manifest.push(`scripts/${file}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
212
229
|
}
|
|
213
230
|
return manifest;
|
|
214
231
|
}
|
|
@@ -313,7 +330,9 @@ var DEFAULT_ROOT_SKIP_PATTERNS = [
|
|
|
313
330
|
"README.md",
|
|
314
331
|
".githooks/pre-commit",
|
|
315
332
|
".github/workflows/ci.yml",
|
|
316
|
-
"setup.sh"
|
|
333
|
+
"scripts/setup.sh",
|
|
334
|
+
"scripts/setup-docker.sh",
|
|
335
|
+
"scripts/setup-ssl.sh"
|
|
317
336
|
];
|
|
318
337
|
var DEFAULT_COMPONENT_SKIP_PATTERNS = {
|
|
319
338
|
fastapi: ["pyproject.toml"],
|
|
@@ -446,12 +465,17 @@ function renderLines(lines, vars) {
|
|
|
446
465
|
continue;
|
|
447
466
|
}
|
|
448
467
|
if (stack.length > 0 && stack.some((v) => !v.active)) continue;
|
|
449
|
-
const replaced = line.replace(/<%=\s*(
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
468
|
+
const replaced = line.replace(/<%=\s*(.+?)\s*%>/g, (_, expr) => {
|
|
469
|
+
const trimmed = expr.trim();
|
|
470
|
+
if (/^[\w.]+$/.test(trimmed)) {
|
|
471
|
+
const parts = trimmed.split(".");
|
|
472
|
+
let val2 = vars;
|
|
473
|
+
for (const p of parts) {
|
|
474
|
+
val2 = val2?.[p];
|
|
475
|
+
}
|
|
476
|
+
return String(val2 ?? "");
|
|
454
477
|
}
|
|
478
|
+
const val = evalExpr(trimmed, vars);
|
|
455
479
|
return String(val ?? "");
|
|
456
480
|
});
|
|
457
481
|
output.push(replaced);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
toSnake,
|
|
14
14
|
upsertComponentMarker,
|
|
15
15
|
writeProjxConfig
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-6YRBHJ2V.js";
|
|
17
17
|
|
|
18
18
|
// src/baseline.ts
|
|
19
19
|
import { existsSync, writeFileSync, unlinkSync } from "fs";
|
|
@@ -44,7 +44,10 @@ var CANONICAL_DISPLAY = {
|
|
|
44
44
|
infra: "Terraform"
|
|
45
45
|
};
|
|
46
46
|
function withInstances(vars) {
|
|
47
|
-
const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({
|
|
47
|
+
const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({
|
|
48
|
+
type,
|
|
49
|
+
path: vars.paths[type] ?? type
|
|
50
|
+
}));
|
|
48
51
|
const enriched = base.map((inst) => ({
|
|
49
52
|
...inst,
|
|
50
53
|
upper: shellSafeUpper(inst.path),
|
|
@@ -124,7 +127,7 @@ function generateVscodeSettings(vars) {
|
|
|
124
127
|
// src/baseline.ts
|
|
125
128
|
var BASELINE_REF = "refs/projx/baseline";
|
|
126
129
|
async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
|
|
127
|
-
const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-
|
|
130
|
+
const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-AVKSTHIF.js");
|
|
128
131
|
for (const component of components) {
|
|
129
132
|
const dir = componentPaths[component];
|
|
130
133
|
const markerDir = join2(cwd, dir);
|
|
@@ -392,7 +395,10 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
|
|
|
392
395
|
type,
|
|
393
396
|
path: componentPaths[type]
|
|
394
397
|
}));
|
|
395
|
-
const allInstances = [
|
|
398
|
+
const allInstances = [
|
|
399
|
+
...primaryInstances,
|
|
400
|
+
...extraInstances
|
|
401
|
+
];
|
|
396
402
|
const toScaffold = instancesToScaffold ?? allInstances;
|
|
397
403
|
for (const inst of toScaffold) {
|
|
398
404
|
await writeOneInstance(inst, {
|
|
@@ -444,9 +450,13 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
|
|
|
444
450
|
await generateCiYml(vars)
|
|
445
451
|
);
|
|
446
452
|
}
|
|
447
|
-
if (shouldWrite("setup.sh")) {
|
|
448
|
-
await
|
|
449
|
-
await
|
|
453
|
+
if (shouldWrite("scripts/setup.sh")) {
|
|
454
|
+
await mkdir(join2(dest, "scripts"), { recursive: true });
|
|
455
|
+
await writeFile(
|
|
456
|
+
join2(dest, "scripts/setup.sh"),
|
|
457
|
+
await generateSetupSh(vars)
|
|
458
|
+
);
|
|
459
|
+
await chmod(join2(dest, "scripts/setup.sh"), 493);
|
|
450
460
|
}
|
|
451
461
|
await copyStaticFiles(repoDir, dest);
|
|
452
462
|
if (shouldWrite(".vscode/settings.json")) {
|
|
@@ -459,14 +469,26 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
|
|
|
459
469
|
await writeManagedProjx(dest, version, vars, applyDefaults);
|
|
460
470
|
}
|
|
461
471
|
async function writeOneInstance(inst, opts) {
|
|
462
|
-
const {
|
|
472
|
+
const {
|
|
473
|
+
dest,
|
|
474
|
+
repoDir,
|
|
475
|
+
vars,
|
|
476
|
+
componentPaths,
|
|
477
|
+
realCwd,
|
|
478
|
+
applyDefaults,
|
|
479
|
+
baseSkip,
|
|
480
|
+
projectName,
|
|
481
|
+
nameSnake
|
|
482
|
+
} = opts;
|
|
463
483
|
const { type, path: targetDir } = inst;
|
|
464
484
|
const realMarker = await readComponentMarker(join2(realCwd, targetDir));
|
|
465
485
|
const isNewMarker = !realMarker;
|
|
466
486
|
const shouldApplyComponentDefault = isNewMarker || applyDefaults;
|
|
467
487
|
const markerSkip = realMarker?.skip ?? [];
|
|
468
488
|
const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[type] ?? [] : [];
|
|
469
|
-
const skipPatterns = [
|
|
489
|
+
const skipPatterns = [
|
|
490
|
+
.../* @__PURE__ */ new Set([...baseSkip, ...markerSkip, ...defaultSkip])
|
|
491
|
+
];
|
|
470
492
|
const tmpDir = join2(dest, "__cptmp__");
|
|
471
493
|
await copyComponent(repoDir, type, tmpDir);
|
|
472
494
|
const srcDir = join2(tmpDir, type);
|
|
@@ -480,33 +502,62 @@ async function writeOneInstance(inst, opts) {
|
|
|
480
502
|
await cp(srcDir, outDir, { recursive: true, force: true });
|
|
481
503
|
}
|
|
482
504
|
await rm(tmpDir, { recursive: true, force: true });
|
|
483
|
-
const instancePaths = {
|
|
505
|
+
const instancePaths = {
|
|
506
|
+
...componentPaths,
|
|
507
|
+
[type]: targetDir
|
|
508
|
+
};
|
|
484
509
|
await renderEjsInDir(outDir, { ...vars, paths: instancePaths });
|
|
485
510
|
await upsertComponentMarker(
|
|
486
511
|
join2(dest, targetDir),
|
|
487
512
|
type,
|
|
488
513
|
skipPatterns.length > 0 ? skipPatterns : void 0
|
|
489
514
|
);
|
|
490
|
-
await substituteNamesForInstance(
|
|
515
|
+
await substituteNamesForInstance(
|
|
516
|
+
inst,
|
|
517
|
+
dest,
|
|
518
|
+
projectName,
|
|
519
|
+
nameSnake,
|
|
520
|
+
vars.nameOverrides
|
|
521
|
+
);
|
|
491
522
|
}
|
|
492
523
|
async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides) {
|
|
493
524
|
const { type, path } = inst;
|
|
494
525
|
const isCanonical = path === type;
|
|
495
526
|
if (type === "fastapi") {
|
|
496
527
|
const target = isCanonical ? overrides?.fastapi ?? `${name}-fastapi` : `${name}-${path}`;
|
|
497
|
-
await replaceInFile(
|
|
528
|
+
await replaceInFile(
|
|
529
|
+
join2(dest, `${path}/pyproject.toml`),
|
|
530
|
+
"projx-fastapi",
|
|
531
|
+
target
|
|
532
|
+
);
|
|
498
533
|
} else if (type === "fastify") {
|
|
499
534
|
const target = isCanonical ? overrides?.fastify ?? `${name}-fastify` : `${name}-${path}`;
|
|
500
|
-
await replaceInFile(
|
|
535
|
+
await replaceInFile(
|
|
536
|
+
join2(dest, `${path}/package.json`),
|
|
537
|
+
"projx-fastify",
|
|
538
|
+
target
|
|
539
|
+
);
|
|
501
540
|
} else if (type === "frontend") {
|
|
502
541
|
const target = isCanonical ? overrides?.frontend ?? `${name}-frontend` : `${name}-${path}`;
|
|
503
|
-
await replaceInFile(
|
|
542
|
+
await replaceInFile(
|
|
543
|
+
join2(dest, `${path}/package.json`),
|
|
544
|
+
"projx-frontend",
|
|
545
|
+
target
|
|
546
|
+
);
|
|
504
547
|
} else if (type === "e2e") {
|
|
505
548
|
const target = isCanonical ? overrides?.e2e ?? `${name}-e2e` : `${name}-${path}`;
|
|
506
|
-
await replaceInFile(
|
|
549
|
+
await replaceInFile(
|
|
550
|
+
join2(dest, `${path}/package.json`),
|
|
551
|
+
"projx-e2e",
|
|
552
|
+
target
|
|
553
|
+
);
|
|
507
554
|
} else if (type === "mobile") {
|
|
508
555
|
const target = isCanonical ? overrides?.mobile ?? `${nameSnake}_mobile` : toSnake(`${nameSnake}_${path}`);
|
|
509
|
-
await replaceInFile(
|
|
556
|
+
await replaceInFile(
|
|
557
|
+
join2(dest, `${path}/pubspec.yaml`),
|
|
558
|
+
"projx_mobile",
|
|
559
|
+
target
|
|
560
|
+
);
|
|
510
561
|
await replaceInDir(
|
|
511
562
|
join2(dest, path),
|
|
512
563
|
"package:projx_mobile/",
|
package/dist/index.js
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
matchesSkip,
|
|
10
10
|
saveBaselineRef,
|
|
11
11
|
writeTemplateToDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XQ7FE4U3.js";
|
|
13
13
|
import {
|
|
14
14
|
COMPONENTS,
|
|
15
15
|
COMPONENT_MARKER,
|
|
16
|
+
DEFAULT_ROOT_SKIP_PATTERNS,
|
|
16
17
|
EXCLUDE,
|
|
17
18
|
PACKAGE_MANAGERS,
|
|
18
19
|
cleanupRepo,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
toTitle,
|
|
34
35
|
writeComponentMarker,
|
|
35
36
|
writeProjxConfig
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-6YRBHJ2V.js";
|
|
37
38
|
|
|
38
39
|
// src/index.ts
|
|
39
40
|
import { existsSync as existsSync11 } from "fs";
|
|
@@ -164,7 +165,7 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
164
165
|
`Done! Next steps:
|
|
165
166
|
|
|
166
167
|
cd ${name}
|
|
167
|
-
./setup.sh
|
|
168
|
+
./scripts/setup.sh
|
|
168
169
|
|
|
169
170
|
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
170
171
|
);
|
|
@@ -474,7 +475,7 @@ function hasUncommittedChanges(cwd) {
|
|
|
474
475
|
async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
|
|
475
476
|
const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
|
|
476
477
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
477
|
-
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-
|
|
478
|
+
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-PZM4KJJW.js");
|
|
478
479
|
const config = await readProjxConfig(cwd);
|
|
479
480
|
const rootPinned = Array.isArray(config.skip) ? config.skip : [];
|
|
480
481
|
const componentPinned = [];
|
|
@@ -806,7 +807,9 @@ async function addInstance(cwd, type, customName, config, existing, localRepo, s
|
|
|
806
807
|
const INSTANCE_AWARE_ROOT = /* @__PURE__ */ new Set([
|
|
807
808
|
".github/workflows/ci.yml",
|
|
808
809
|
".githooks/pre-commit",
|
|
809
|
-
"setup.sh"
|
|
810
|
+
"scripts/setup.sh",
|
|
811
|
+
"docker-compose.yml",
|
|
812
|
+
"docker-compose.dev.yml"
|
|
810
813
|
]);
|
|
811
814
|
const rawSkip = Array.isArray(config.skip) ? config.skip : [];
|
|
812
815
|
const rootSkip = rawSkip.filter((p11) => !INSTANCE_AWARE_ROOT.has(p11));
|
|
@@ -1056,12 +1059,14 @@ async function init(cwd, localRepo) {
|
|
|
1056
1059
|
spinner7.stop(
|
|
1057
1060
|
detected.length > 0 ? `Found ${detected.length} component(s).` : "No components detected."
|
|
1058
1061
|
);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1062
|
+
if (detected.length === 0) {
|
|
1063
|
+
await writeBareProjx(cwd, localRepo, isLocal, detectPackageManager(cwd));
|
|
1064
|
+
p5.outro(
|
|
1065
|
+
"Initialized empty .projx. Add components with 'npx create-projx add <component>'."
|
|
1066
|
+
);
|
|
1067
|
+
return;
|
|
1064
1068
|
}
|
|
1069
|
+
const confirmed = await confirmDetections(detected);
|
|
1065
1070
|
if (confirmed.length === 0) {
|
|
1066
1071
|
p5.log.warn("No components selected. Nothing to do.");
|
|
1067
1072
|
process.exit(0);
|
|
@@ -1161,6 +1166,36 @@ async function init(cwd, localRepo) {
|
|
|
1161
1166
|
await cleanupRepo(repoDir, isLocal);
|
|
1162
1167
|
}
|
|
1163
1168
|
}
|
|
1169
|
+
async function writeBareProjx(cwd, localRepo, isLocal, pm) {
|
|
1170
|
+
const dlSpinner = p5.spinner();
|
|
1171
|
+
dlSpinner.start(
|
|
1172
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1173
|
+
);
|
|
1174
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1175
|
+
dlSpinner.stop("Failed.");
|
|
1176
|
+
p5.log.error(String(err));
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
});
|
|
1179
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1180
|
+
try {
|
|
1181
|
+
const pkg = JSON.parse(
|
|
1182
|
+
await readFile4(join5(repoDir, "cli/package.json"), "utf-8")
|
|
1183
|
+
);
|
|
1184
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1185
|
+
const config = {
|
|
1186
|
+
version: pkg.version,
|
|
1187
|
+
createdAt: today,
|
|
1188
|
+
updatedAt: today,
|
|
1189
|
+
skip: [...DEFAULT_ROOT_SKIP_PATTERNS],
|
|
1190
|
+
defaultsApplied: true
|
|
1191
|
+
};
|
|
1192
|
+
if (pm) config.packageManager = pm;
|
|
1193
|
+
await writeProjxConfig(cwd, config);
|
|
1194
|
+
saveBaselineRef(cwd);
|
|
1195
|
+
} finally {
|
|
1196
|
+
await cleanupRepo(repoDir, isLocal);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1164
1199
|
async function confirmDetections(detected) {
|
|
1165
1200
|
const confirmed = [];
|
|
1166
1201
|
for (const d of detected) {
|
|
@@ -1175,33 +1210,6 @@ async function confirmDetections(detected) {
|
|
|
1175
1210
|
}
|
|
1176
1211
|
return confirmed;
|
|
1177
1212
|
}
|
|
1178
|
-
async function manualSelect(cwd) {
|
|
1179
|
-
const selected = await p5.multiselect({
|
|
1180
|
-
message: "No components detected. Select manually:",
|
|
1181
|
-
options: COMPONENTS.map((c) => ({
|
|
1182
|
-
value: c,
|
|
1183
|
-
label: LABELS[c].label,
|
|
1184
|
-
hint: LABELS[c].hint
|
|
1185
|
-
})),
|
|
1186
|
-
required: false
|
|
1187
|
-
});
|
|
1188
|
-
if (p5.isCancel(selected)) process.exit(0);
|
|
1189
|
-
const result = [];
|
|
1190
|
-
for (const component of selected) {
|
|
1191
|
-
const dir = await p5.text({
|
|
1192
|
-
message: `Directory for ${LABELS[component].label}?`,
|
|
1193
|
-
placeholder: component,
|
|
1194
|
-
defaultValue: component
|
|
1195
|
-
});
|
|
1196
|
-
if (p5.isCancel(dir)) process.exit(0);
|
|
1197
|
-
if (!existsSync5(join5(cwd, dir))) {
|
|
1198
|
-
p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
result.push({ component, directory: dir });
|
|
1202
|
-
}
|
|
1203
|
-
return result;
|
|
1204
|
-
}
|
|
1205
1213
|
function isGitRepo2(cwd) {
|
|
1206
1214
|
try {
|
|
1207
1215
|
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
4
4
|
"description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
|
|
|
30
30
|
## Getting Started
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
./setup.sh
|
|
33
|
+
./scripts/setup.sh # Install all dependencies
|
|
34
34
|
docker compose -f docker-compose.dev.yml up # Start with Docker (dev mode)
|
|
35
35
|
```
|
|
36
36
|
<% if (components.includes('fastapi')) { %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
services:
|
|
2
|
-
<% if (
|
|
2
|
+
<% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
|
|
3
3
|
db:
|
|
4
4
|
image: postgres:16-alpine
|
|
5
5
|
environment:
|
|
@@ -23,9 +23,9 @@ services:
|
|
|
23
23
|
networks:
|
|
24
24
|
- app-network
|
|
25
25
|
<% } %>
|
|
26
|
-
<%
|
|
27
|
-
<%=
|
|
28
|
-
build: ./<%=
|
|
26
|
+
<% for (const inst of fastapiInstances) { %>
|
|
27
|
+
<%= inst.path %>-migrate:
|
|
28
|
+
build: ./<%= inst.path %>
|
|
29
29
|
command: ['uv', 'run', 'migrate.py']
|
|
30
30
|
environment:
|
|
31
31
|
- SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
|
|
@@ -39,16 +39,21 @@ services:
|
|
|
39
39
|
cpus: '0.5'
|
|
40
40
|
networks:
|
|
41
41
|
- app-network
|
|
42
|
-
<%=
|
|
43
|
-
build: ./<%=
|
|
42
|
+
<%= inst.path %>:
|
|
43
|
+
build: ./<%= inst.path %>
|
|
44
44
|
command:
|
|
45
45
|
[
|
|
46
46
|
'uv', 'run', 'uvicorn', 'src.app:app',
|
|
47
47
|
'--host', '0.0.0.0', '--port', '7860',
|
|
48
48
|
'--workers', '2', '--timeout-keep-alive', '120', '--reload',
|
|
49
49
|
]
|
|
50
|
+
<% if (inst.path === inst.type) { %>
|
|
50
51
|
ports:
|
|
51
52
|
- '7860:7860'
|
|
53
|
+
<% } else { %>
|
|
54
|
+
expose:
|
|
55
|
+
- '7860'
|
|
56
|
+
<% } %>
|
|
52
57
|
environment:
|
|
53
58
|
- SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
|
|
54
59
|
- CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost
|
|
@@ -56,11 +61,11 @@ services:
|
|
|
56
61
|
- JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
|
|
57
62
|
- JWT_ALGORITHMS=HS256
|
|
58
63
|
volumes:
|
|
59
|
-
- ./<%=
|
|
60
|
-
- ./<%=
|
|
61
|
-
- ./<%=
|
|
64
|
+
- ./<%= inst.path %>/src:/app/src
|
|
65
|
+
- ./<%= inst.path %>/alembic.ini:/app/alembic.ini
|
|
66
|
+
- ./<%= inst.path %>/migrate.py:/app/migrate.py
|
|
62
67
|
depends_on:
|
|
63
|
-
<%=
|
|
68
|
+
<%= inst.path %>-migrate:
|
|
64
69
|
condition: service_completed_successfully
|
|
65
70
|
restart: unless-stopped
|
|
66
71
|
healthcheck:
|
|
@@ -81,9 +86,9 @@ services:
|
|
|
81
86
|
networks:
|
|
82
87
|
- app-network
|
|
83
88
|
<% } %>
|
|
84
|
-
<%
|
|
85
|
-
<%=
|
|
86
|
-
build: ./<%=
|
|
89
|
+
<% for (const inst of fastifyInstances) { %>
|
|
90
|
+
<%= inst.path %>-migrate:
|
|
91
|
+
build: ./<%= inst.path %>
|
|
87
92
|
command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
|
|
88
93
|
environment:
|
|
89
94
|
- DATABASE_URL=postgresql://dev:dev@db:5432/app
|
|
@@ -97,20 +102,25 @@ services:
|
|
|
97
102
|
cpus: '0.5'
|
|
98
103
|
networks:
|
|
99
104
|
- app-network
|
|
100
|
-
<%=
|
|
101
|
-
build: ./<%=
|
|
105
|
+
<%= inst.path %>:
|
|
106
|
+
build: ./<%= inst.path %>
|
|
102
107
|
command: ["sh", "-c", "<%= pm.runDev %>"]
|
|
108
|
+
<% if (inst.path === inst.type) { %>
|
|
103
109
|
ports:
|
|
104
110
|
- '3000:3000'
|
|
111
|
+
<% } else { %>
|
|
112
|
+
expose:
|
|
113
|
+
- '3000'
|
|
114
|
+
<% } %>
|
|
105
115
|
environment:
|
|
106
116
|
- DATABASE_URL=postgresql://dev:dev@db:5432/app
|
|
107
117
|
- CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost
|
|
108
118
|
- JWT_PROVIDER=shared_secret
|
|
109
119
|
- JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
|
|
110
120
|
volumes:
|
|
111
|
-
- ./<%=
|
|
121
|
+
- ./<%= inst.path %>/src:/app/src
|
|
112
122
|
depends_on:
|
|
113
|
-
<%=
|
|
123
|
+
<%= inst.path %>-migrate:
|
|
114
124
|
condition: service_completed_successfully
|
|
115
125
|
restart: unless-stopped
|
|
116
126
|
healthcheck:
|
|
@@ -127,25 +137,30 @@ services:
|
|
|
127
137
|
networks:
|
|
128
138
|
- app-network
|
|
129
139
|
<% } %>
|
|
130
|
-
<%
|
|
131
|
-
|
|
140
|
+
<% for (const inst of frontendInstances) { %>
|
|
141
|
+
<%= inst.path %>:
|
|
132
142
|
image: node:20-alpine
|
|
133
143
|
working_dir: /app
|
|
134
144
|
command: sh -c "<%= pm.install %> && <%= pm.run %> dev -- --host 0.0.0.0"
|
|
145
|
+
<% if (inst.path === inst.type) { %>
|
|
135
146
|
ports:
|
|
136
147
|
- '5173:5173'
|
|
148
|
+
<% } else { %>
|
|
149
|
+
expose:
|
|
150
|
+
- '5173'
|
|
151
|
+
<% } %>
|
|
137
152
|
env_file:
|
|
138
|
-
- ./<%=
|
|
153
|
+
- ./<%= inst.path %>/.env
|
|
139
154
|
volumes:
|
|
140
|
-
- ./<%=
|
|
141
|
-
-
|
|
142
|
-
<% if (
|
|
155
|
+
- ./<%= inst.path %>:/app
|
|
156
|
+
- <%= inst.upper.toLowerCase() %>_node_modules:/app/node_modules
|
|
157
|
+
<% if (fastifyInstances.length > 0) { %>
|
|
143
158
|
depends_on:
|
|
144
|
-
<%=
|
|
159
|
+
<%= fastifyInstances[0].path %>:
|
|
145
160
|
condition: service_healthy
|
|
146
|
-
<% } else if (
|
|
161
|
+
<% } else if (fastapiInstances.length > 0) { %>
|
|
147
162
|
depends_on:
|
|
148
|
-
<%=
|
|
163
|
+
<%= fastapiInstances[0].path %>:
|
|
149
164
|
condition: service_healthy
|
|
150
165
|
<% } %>
|
|
151
166
|
healthcheck:
|
|
@@ -163,11 +178,11 @@ services:
|
|
|
163
178
|
- app-network
|
|
164
179
|
<% } %>
|
|
165
180
|
volumes:
|
|
166
|
-
<% if (
|
|
181
|
+
<% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
|
|
167
182
|
pgdata:
|
|
168
183
|
<% } %>
|
|
169
|
-
<%
|
|
170
|
-
|
|
184
|
+
<% for (const inst of frontendInstances) { %>
|
|
185
|
+
<%= inst.upper.toLowerCase() %>_node_modules:
|
|
171
186
|
<% } %>
|
|
172
187
|
networks:
|
|
173
188
|
app-network:
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
services:
|
|
2
|
-
<%
|
|
3
|
-
<%=
|
|
4
|
-
build: ./<%=
|
|
2
|
+
<% for (const inst of fastapiInstances) { %>
|
|
3
|
+
<%= inst.path %>-migrate:
|
|
4
|
+
build: ./<%= inst.path %>
|
|
5
5
|
command: ["uv", "run", "migrate.py"]
|
|
6
6
|
env_file:
|
|
7
|
-
- ./<%=
|
|
7
|
+
- ./<%= inst.path %>/.env
|
|
8
8
|
networks:
|
|
9
9
|
- app-network
|
|
10
|
-
<%=
|
|
11
|
-
build: ./<%=
|
|
10
|
+
<%= inst.path %>:
|
|
11
|
+
build: ./<%= inst.path %>
|
|
12
12
|
expose:
|
|
13
13
|
- "7860"
|
|
14
14
|
env_file:
|
|
15
|
-
- ./<%=
|
|
15
|
+
- ./<%= inst.path %>/.env
|
|
16
16
|
restart: unless-stopped
|
|
17
17
|
depends_on:
|
|
18
|
-
<%=
|
|
18
|
+
<%= inst.path %>-migrate:
|
|
19
19
|
condition: service_completed_successfully
|
|
20
20
|
healthcheck:
|
|
21
21
|
test:
|
|
@@ -32,23 +32,23 @@ services:
|
|
|
32
32
|
networks:
|
|
33
33
|
- app-network
|
|
34
34
|
<% } %>
|
|
35
|
-
<%
|
|
36
|
-
<%=
|
|
37
|
-
build: ./<%=
|
|
35
|
+
<% for (const inst of fastifyInstances) { %>
|
|
36
|
+
<%= inst.path %>-migrate:
|
|
37
|
+
build: ./<%= inst.path %>
|
|
38
38
|
command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
|
|
39
39
|
env_file:
|
|
40
|
-
- ./<%=
|
|
40
|
+
- ./<%= inst.path %>/.env
|
|
41
41
|
networks:
|
|
42
42
|
- app-network
|
|
43
|
-
<%=
|
|
44
|
-
build: ./<%=
|
|
43
|
+
<%= inst.path %>:
|
|
44
|
+
build: ./<%= inst.path %>
|
|
45
45
|
expose:
|
|
46
46
|
- "3000"
|
|
47
47
|
env_file:
|
|
48
|
-
- ./<%=
|
|
48
|
+
- ./<%= inst.path %>/.env
|
|
49
49
|
restart: unless-stopped
|
|
50
50
|
depends_on:
|
|
51
|
-
<%=
|
|
51
|
+
<%= inst.path %>-migrate:
|
|
52
52
|
condition: service_completed_successfully
|
|
53
53
|
healthcheck:
|
|
54
54
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
|
@@ -59,10 +59,10 @@ services:
|
|
|
59
59
|
networks:
|
|
60
60
|
- app-network
|
|
61
61
|
<% } %>
|
|
62
|
-
<%
|
|
63
|
-
|
|
62
|
+
<% for (const inst of frontendInstances) { %>
|
|
63
|
+
<%= inst.path %>:
|
|
64
64
|
build:
|
|
65
|
-
context: ./<%=
|
|
65
|
+
context: ./<%= inst.path %>
|
|
66
66
|
args:
|
|
67
67
|
VITE_API_URL: ""
|
|
68
68
|
ports:
|
|
@@ -71,13 +71,13 @@ services:
|
|
|
71
71
|
volumes:
|
|
72
72
|
- letsencrypt:/etc/letsencrypt
|
|
73
73
|
- certbot-www:/var/www/certbot
|
|
74
|
-
<% if (
|
|
74
|
+
<% if (fastifyInstances.length > 0) { %>
|
|
75
75
|
depends_on:
|
|
76
|
-
<%=
|
|
76
|
+
<%= fastifyInstances[0].path %>:
|
|
77
77
|
condition: service_healthy
|
|
78
|
-
<% } else if (
|
|
78
|
+
<% } else if (fastapiInstances.length > 0) { %>
|
|
79
79
|
depends_on:
|
|
80
|
-
<%=
|
|
80
|
+
<%= fastapiInstances[0].path %>:
|
|
81
81
|
condition: service_healthy
|
|
82
82
|
<% } %>
|
|
83
83
|
restart: unless-stopped
|
|
@@ -89,6 +89,8 @@ services:
|
|
|
89
89
|
start_period: 10s
|
|
90
90
|
networks:
|
|
91
91
|
- app-network
|
|
92
|
+
<% } %>
|
|
93
|
+
<% if (frontendInstances.length > 0) { %>
|
|
92
94
|
certbot:
|
|
93
95
|
image: certbot/certbot:latest
|
|
94
96
|
volumes:
|
|
@@ -97,14 +99,14 @@ services:
|
|
|
97
99
|
entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done"
|
|
98
100
|
restart: unless-stopped
|
|
99
101
|
depends_on:
|
|
100
|
-
|
|
102
|
+
<%= frontendInstances[0].path %>:
|
|
101
103
|
condition: service_healthy
|
|
102
104
|
profiles:
|
|
103
105
|
- ssl
|
|
104
106
|
networks:
|
|
105
107
|
- app-network
|
|
106
108
|
<% } %>
|
|
107
|
-
<% if (
|
|
109
|
+
<% if (frontendInstances.length > 0) { %>
|
|
108
110
|
volumes:
|
|
109
111
|
letsencrypt:
|
|
110
112
|
certbot-www:
|
|
@@ -5,28 +5,43 @@ git config core.hooksPath .githooks
|
|
|
5
5
|
echo "Git hooks configured."
|
|
6
6
|
<% for (const inst of fastapiInstances) { %>
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
(
|
|
9
|
+
cd <%= inst.path %>
|
|
10
|
+
uv sync --all-extras
|
|
11
|
+
)
|
|
9
12
|
echo "<%= inst.display %> dependencies installed."
|
|
10
13
|
<% } %>
|
|
11
14
|
<% for (const inst of fastifyInstances) { %>
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
(
|
|
17
|
+
cd <%= inst.path %>
|
|
18
|
+
<%= pm.install %>
|
|
19
|
+
)
|
|
14
20
|
echo "<%= inst.display %> dependencies installed."
|
|
15
21
|
<% } %>
|
|
16
22
|
<% for (const inst of frontendInstances) { %>
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
(
|
|
25
|
+
cd <%= inst.path %>
|
|
26
|
+
<%= pm.install %>
|
|
27
|
+
)
|
|
19
28
|
echo "<%= inst.display %> dependencies installed."
|
|
20
29
|
<% } %>
|
|
21
30
|
<% for (const inst of e2eInstances) { %>
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
(
|
|
33
|
+
cd <%= inst.path %>
|
|
34
|
+
<%= pm.install %>
|
|
35
|
+
)
|
|
24
36
|
echo "<%= inst.display %> dependencies installed."
|
|
25
37
|
<% } %>
|
|
26
38
|
<% for (const inst of mobileInstances) { %>
|
|
27
39
|
|
|
28
40
|
if command -v flutter &>/dev/null; then
|
|
29
|
-
|
|
41
|
+
(
|
|
42
|
+
cd <%= inst.path %>
|
|
43
|
+
flutter pub get
|
|
44
|
+
)
|
|
30
45
|
echo "<%= inst.display %> dependencies installed."
|
|
31
46
|
else
|
|
32
47
|
echo "<%= inst.display %> skipped (SDK not installed)."
|