create-projx 1.0.1 → 1.1.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.
- package/dist/index.js +545 -120
- package/package.json +14 -2
- package/src/templates/README.md.ejs +13 -9
- package/src/templates/ci.yml.ejs +9 -9
- package/src/templates/docker-compose.dev.yml.ejs +8 -8
- package/src/templates/docker-compose.yml.ejs +7 -7
- package/src/templates/pre-commit.ejs +31 -31
- package/src/templates/setup.sh.ejs +5 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync7 } from "fs";
|
|
5
5
|
import { resolve as resolve2 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/utils.ts
|
|
@@ -176,6 +176,47 @@ async function replaceInDir(dir, find, replace, ext) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
var COMPONENT_MARKER = ".projx-component";
|
|
180
|
+
async function readFileOrNull(path) {
|
|
181
|
+
try {
|
|
182
|
+
return await readFile(path, "utf-8");
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function writeComponentMarker(dir, component) {
|
|
188
|
+
await writeFile(
|
|
189
|
+
join(dir, COMPONENT_MARKER),
|
|
190
|
+
JSON.stringify({ component }, null, 2) + "\n"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
async function discoverComponentPaths(cwd, components) {
|
|
194
|
+
const paths = {};
|
|
195
|
+
const scan = async (dir) => {
|
|
196
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (!entry.isDirectory()) continue;
|
|
199
|
+
if (EXCLUDE.has(entry.name)) continue;
|
|
200
|
+
if (entry.name.startsWith(".")) continue;
|
|
201
|
+
const full = join(dir, entry.name);
|
|
202
|
+
const marker = join(full, COMPONENT_MARKER);
|
|
203
|
+
if (existsSync(marker)) {
|
|
204
|
+
try {
|
|
205
|
+
const data = JSON.parse(await readFile(marker, "utf-8"));
|
|
206
|
+
if (components.includes(data.component)) {
|
|
207
|
+
paths[data.component] = entry.name;
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
await scan(cwd);
|
|
215
|
+
for (const c of components) {
|
|
216
|
+
if (!paths[c]) paths[c] = c;
|
|
217
|
+
}
|
|
218
|
+
return paths;
|
|
219
|
+
}
|
|
179
220
|
function render(template, vars) {
|
|
180
221
|
const components = vars.components;
|
|
181
222
|
const projectName = vars.projectName;
|
|
@@ -201,8 +242,15 @@ function render(template, vars) {
|
|
|
201
242
|
}
|
|
202
243
|
if (stack.length > 0 && stack.some((v) => !v)) continue;
|
|
203
244
|
const replaced = line.replace(
|
|
204
|
-
/<%=\s*(\w+)\s*%>/g,
|
|
205
|
-
(_,
|
|
245
|
+
/<%=\s*([\w.]+)\s*%>/g,
|
|
246
|
+
(_, expr) => {
|
|
247
|
+
const parts = expr.split(".");
|
|
248
|
+
let val = vars;
|
|
249
|
+
for (const p6 of parts) {
|
|
250
|
+
val = val?.[p6];
|
|
251
|
+
}
|
|
252
|
+
return String(val ?? "");
|
|
253
|
+
}
|
|
206
254
|
);
|
|
207
255
|
output.push(replaced);
|
|
208
256
|
}
|
|
@@ -288,7 +336,10 @@ async function generateReadme(vars) {
|
|
|
288
336
|
async function scaffold(opts, dest, localRepo) {
|
|
289
337
|
const name = toKebab(opts.name);
|
|
290
338
|
const nameSnake = toSnake(opts.name);
|
|
291
|
-
const
|
|
339
|
+
const paths = Object.fromEntries(
|
|
340
|
+
opts.components.map((c) => [c, c])
|
|
341
|
+
);
|
|
342
|
+
const vars = { projectName: name, components: opts.components, paths };
|
|
292
343
|
const isLocal = !!localRepo;
|
|
293
344
|
await mkdir2(dest, { recursive: true });
|
|
294
345
|
const dlSpinner = p2.spinner();
|
|
@@ -307,42 +358,28 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
307
358
|
}
|
|
308
359
|
async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
309
360
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
310
|
-
const manifest = [];
|
|
311
361
|
for (const component of opts.components) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
362
|
+
const spinner5 = p2.spinner();
|
|
363
|
+
spinner5.start(`Copying ${component}/`);
|
|
364
|
+
await copyComponent(repoDir, component, dest);
|
|
365
|
+
await writeComponentMarker(join3(dest, component), component);
|
|
366
|
+
spinner5.stop(`${component}/`);
|
|
317
367
|
}
|
|
318
368
|
await substituteNames(dest, opts.components, name, nameSnake);
|
|
319
369
|
const hasBackend = opts.components.includes("fastapi") || opts.components.includes("fastify");
|
|
320
370
|
if (hasBackend || opts.components.includes("frontend")) {
|
|
321
|
-
|
|
322
|
-
await writeFile2(join3(dest, "docker-compose.yml"),
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
await writeFile2(join3(dest, "docker-compose.dev.yml"), dcDev);
|
|
326
|
-
manifest.push("docker-compose.dev.yml");
|
|
327
|
-
}
|
|
328
|
-
const readme = await generateReadme(vars);
|
|
329
|
-
await writeFile2(join3(dest, "README.md"), readme);
|
|
330
|
-
manifest.push("README.md");
|
|
371
|
+
await writeFile2(join3(dest, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
372
|
+
await writeFile2(join3(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
373
|
+
}
|
|
374
|
+
await writeFile2(join3(dest, "README.md"), await generateReadme(vars));
|
|
331
375
|
await mkdir2(join3(dest, ".githooks"), { recursive: true });
|
|
332
|
-
|
|
333
|
-
await writeFile2(join3(dest, ".githooks/pre-commit"), preCommit);
|
|
376
|
+
await writeFile2(join3(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
334
377
|
await chmod(join3(dest, ".githooks/pre-commit"), 493);
|
|
335
|
-
manifest.push(".githooks/pre-commit");
|
|
336
378
|
await mkdir2(join3(dest, ".github/workflows"), { recursive: true });
|
|
337
|
-
|
|
338
|
-
await writeFile2(join3(dest, ".
|
|
339
|
-
manifest.push(".github/workflows/ci.yml");
|
|
340
|
-
const setupSh = await generateSetupSh(vars);
|
|
341
|
-
await writeFile2(join3(dest, "setup.sh"), setupSh);
|
|
379
|
+
await writeFile2(join3(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
380
|
+
await writeFile2(join3(dest, "setup.sh"), await generateSetupSh(vars));
|
|
342
381
|
await chmod(join3(dest, "setup.sh"), 493);
|
|
343
|
-
|
|
344
|
-
const staticFiles = await copyStaticFiles(repoDir, dest);
|
|
345
|
-
manifest.push(...staticFiles);
|
|
382
|
+
await copyStaticFiles(repoDir, dest);
|
|
346
383
|
const pkg = JSON.parse(
|
|
347
384
|
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
348
385
|
);
|
|
@@ -350,7 +387,7 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
350
387
|
version: pkg.version,
|
|
351
388
|
components: opts.components,
|
|
352
389
|
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
353
|
-
|
|
390
|
+
paths: vars.paths
|
|
354
391
|
};
|
|
355
392
|
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
356
393
|
if (opts.git) {
|
|
@@ -377,7 +414,9 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
377
414
|
p2.outro(`Done! Next steps:
|
|
378
415
|
|
|
379
416
|
cd ${name}
|
|
380
|
-
./setup.sh
|
|
417
|
+
./setup.sh
|
|
418
|
+
|
|
419
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
381
420
|
}
|
|
382
421
|
async function substituteNames(dest, components, name, nameSnake) {
|
|
383
422
|
if (components.includes("fastapi")) {
|
|
@@ -424,44 +463,44 @@ async function substituteNames(dest, components, name, nameSnake) {
|
|
|
424
463
|
}
|
|
425
464
|
async function installDeps(dest, components) {
|
|
426
465
|
for (const component of components) {
|
|
427
|
-
const
|
|
466
|
+
const spinner5 = p2.spinner();
|
|
428
467
|
try {
|
|
429
468
|
switch (component) {
|
|
430
469
|
case "fastapi":
|
|
431
470
|
if (hasCommand("uv")) {
|
|
432
|
-
|
|
471
|
+
spinner5.start("Installing FastAPI dependencies (uv sync)");
|
|
433
472
|
exec("uv sync --all-extras", join3(dest, "fastapi"));
|
|
434
|
-
|
|
473
|
+
spinner5.stop("FastAPI dependencies installed.");
|
|
435
474
|
} else {
|
|
436
475
|
p2.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
437
476
|
}
|
|
438
477
|
break;
|
|
439
478
|
case "fastify":
|
|
440
479
|
if (hasCommand("pnpm")) {
|
|
441
|
-
|
|
480
|
+
spinner5.start("Installing Fastify dependencies (pnpm install)");
|
|
442
481
|
exec("pnpm install", join3(dest, "fastify"));
|
|
443
|
-
|
|
482
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
444
483
|
} else {
|
|
445
|
-
|
|
484
|
+
spinner5.start("Installing Fastify dependencies (npm install)");
|
|
446
485
|
exec("npm install", join3(dest, "fastify"));
|
|
447
|
-
|
|
486
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
448
487
|
}
|
|
449
488
|
break;
|
|
450
489
|
case "frontend":
|
|
451
|
-
|
|
490
|
+
spinner5.start("Installing Frontend dependencies (npm install)");
|
|
452
491
|
exec("npm install", join3(dest, "frontend"));
|
|
453
|
-
|
|
492
|
+
spinner5.stop("Frontend dependencies installed.");
|
|
454
493
|
break;
|
|
455
494
|
case "e2e":
|
|
456
|
-
|
|
495
|
+
spinner5.start("Installing E2E dependencies (npm install)");
|
|
457
496
|
exec("npm install", join3(dest, "e2e"));
|
|
458
|
-
|
|
497
|
+
spinner5.stop("E2E dependencies installed.");
|
|
459
498
|
break;
|
|
460
499
|
case "mobile":
|
|
461
500
|
if (hasCommand("flutter")) {
|
|
462
|
-
|
|
501
|
+
spinner5.start("Installing Flutter dependencies");
|
|
463
502
|
exec("flutter pub get", join3(dest, "mobile"));
|
|
464
|
-
|
|
503
|
+
spinner5.stop("Flutter dependencies installed.");
|
|
465
504
|
} else {
|
|
466
505
|
p2.log.warn(
|
|
467
506
|
"Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
|
|
@@ -472,7 +511,7 @@ async function installDeps(dest, components) {
|
|
|
472
511
|
break;
|
|
473
512
|
}
|
|
474
513
|
} catch {
|
|
475
|
-
|
|
514
|
+
spinner5.stop(`Failed to install ${component} dependencies.`);
|
|
476
515
|
}
|
|
477
516
|
}
|
|
478
517
|
}
|
|
@@ -499,7 +538,8 @@ var NEVER_OVERWRITE = [
|
|
|
499
538
|
/\.env$/,
|
|
500
539
|
/\.env\.(dev|staging|prod)$/,
|
|
501
540
|
/prisma\/migrations\//,
|
|
502
|
-
/src\/migrations\/versions
|
|
541
|
+
/src\/migrations\/versions\//,
|
|
542
|
+
/\.projx-component$/
|
|
503
543
|
];
|
|
504
544
|
function isGitRepo(cwd) {
|
|
505
545
|
try {
|
|
@@ -550,11 +590,17 @@ async function update(cwd, localRepo) {
|
|
|
550
590
|
config = {
|
|
551
591
|
version: "0.0.0",
|
|
552
592
|
components: detected,
|
|
553
|
-
createdAt: "unknown"
|
|
554
|
-
files: []
|
|
593
|
+
createdAt: "unknown"
|
|
555
594
|
};
|
|
556
595
|
p3.log.info(`Detected: ${detected.join(", ")}`);
|
|
557
596
|
}
|
|
597
|
+
const componentPaths = await discoverComponentPaths(cwd, config.components);
|
|
598
|
+
const remapped = config.components.filter((c) => componentPaths[c] !== c);
|
|
599
|
+
if (remapped.length > 0) {
|
|
600
|
+
for (const c of remapped) {
|
|
601
|
+
p3.log.info(`${c} \u2192 ${componentPaths[c]}/`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
558
604
|
const useGitBranch = isGitRepo(cwd);
|
|
559
605
|
let branchName;
|
|
560
606
|
let originalBranch;
|
|
@@ -584,7 +630,7 @@ async function update(cwd, localRepo) {
|
|
|
584
630
|
execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
|
|
585
631
|
p3.log.info(`Created branch: ${branchName}`);
|
|
586
632
|
try {
|
|
587
|
-
await doUpdate(cwd, config, repoDir, pkg.version);
|
|
633
|
+
await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
588
634
|
} finally {
|
|
589
635
|
await cleanupRepo(repoDir, isLocal);
|
|
590
636
|
}
|
|
@@ -612,76 +658,73 @@ async function update(cwd, localRepo) {
|
|
|
612
658
|
await readFile4(join4(repoDir, "cli/package.json"), "utf-8")
|
|
613
659
|
);
|
|
614
660
|
try {
|
|
615
|
-
await doUpdate(cwd, config, repoDir, pkg.version);
|
|
661
|
+
await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
616
662
|
} finally {
|
|
617
663
|
await cleanupRepo(repoDir, isLocal);
|
|
618
664
|
}
|
|
619
665
|
p3.outro(`Updated to v${pkg.version}. Review changes before committing.`);
|
|
620
666
|
}
|
|
621
667
|
}
|
|
622
|
-
async function doUpdate(cwd, config, repoDir, version) {
|
|
623
|
-
const name = detectProjectName(cwd, config.components);
|
|
668
|
+
async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
669
|
+
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
624
670
|
const nameSnake = toSnake(name);
|
|
625
|
-
const vars = { projectName: name, components: config.components };
|
|
626
|
-
const newManifest = [];
|
|
671
|
+
const vars = { projectName: name, components: config.components, paths: componentPaths };
|
|
627
672
|
for (const component of config.components) {
|
|
628
|
-
const
|
|
629
|
-
|
|
673
|
+
const targetDir = componentPaths[component];
|
|
674
|
+
const spinner6 = p3.spinner();
|
|
675
|
+
spinner6.start(`Updating ${targetDir}/ (${component})`);
|
|
630
676
|
const componentSrc = join4(repoDir, component);
|
|
631
677
|
if (!existsSync3(componentSrc)) {
|
|
632
|
-
|
|
678
|
+
spinner6.stop(`${component} template not found, skipping.`);
|
|
633
679
|
continue;
|
|
634
680
|
}
|
|
635
681
|
const tmpDest = join4(cwd, `.projx-tmp`);
|
|
636
682
|
const files = await copyComponent(repoDir, component, tmpDest);
|
|
637
683
|
for (const file of files) {
|
|
638
|
-
const rel = `${component}/${file}`;
|
|
639
684
|
const src = join4(tmpDest, component, file);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
if (
|
|
685
|
+
const destRel = `${targetDir}/${file}`;
|
|
686
|
+
const dest = join4(cwd, destRel);
|
|
687
|
+
if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
|
|
643
688
|
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
644
689
|
await mkdir3(dir, { recursive: true });
|
|
645
690
|
await cp2(src, dest, { force: true });
|
|
646
|
-
newManifest.push(rel);
|
|
647
691
|
}
|
|
648
692
|
await rm2(tmpDest, { recursive: true, force: true });
|
|
649
|
-
|
|
693
|
+
if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
|
|
694
|
+
await writeComponentMarker(join4(cwd, targetDir), component);
|
|
695
|
+
}
|
|
696
|
+
spinner6.stop(`${targetDir}/ updated.`);
|
|
650
697
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
698
|
+
const spinner5 = p3.spinner();
|
|
699
|
+
spinner5.start("Updating shared files");
|
|
653
700
|
const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
|
|
654
701
|
if (hasBackend || config.components.includes("frontend")) {
|
|
655
702
|
await writeFile3(
|
|
656
703
|
join4(cwd, "docker-compose.yml"),
|
|
657
704
|
await generateDockerCompose(vars)
|
|
658
705
|
);
|
|
659
|
-
newManifest.push("docker-compose.yml");
|
|
660
706
|
await writeFile3(
|
|
661
707
|
join4(cwd, "docker-compose.dev.yml"),
|
|
662
708
|
await generateDockerComposeDev(vars)
|
|
663
709
|
);
|
|
664
|
-
newManifest.push("docker-compose.dev.yml");
|
|
665
710
|
}
|
|
666
711
|
await mkdir3(join4(cwd, ".githooks"), { recursive: true });
|
|
667
712
|
const preCommit = await generatePreCommit(vars);
|
|
668
713
|
await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
|
|
669
714
|
await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
|
|
670
|
-
newManifest.push(".githooks/pre-commit");
|
|
671
715
|
await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
|
|
672
716
|
await writeFile3(
|
|
673
717
|
join4(cwd, ".github/workflows/ci.yml"),
|
|
674
718
|
await generateCiYml(vars)
|
|
675
719
|
);
|
|
676
|
-
newManifest.push(".github/workflows/ci.yml");
|
|
677
720
|
const setupSh = await generateSetupSh(vars);
|
|
678
721
|
await writeFile3(join4(cwd, "setup.sh"), setupSh);
|
|
679
722
|
await chmod2(join4(cwd, "setup.sh"), 493);
|
|
680
|
-
|
|
681
|
-
spinner4.stop("Shared files updated.");
|
|
723
|
+
spinner5.stop("Shared files updated.");
|
|
682
724
|
if (config.components.includes("mobile")) {
|
|
725
|
+
const mobilePath = componentPaths.mobile ?? "mobile";
|
|
683
726
|
await replaceInDir(
|
|
684
|
-
join4(cwd,
|
|
727
|
+
join4(cwd, mobilePath),
|
|
685
728
|
"package:projx_mobile/",
|
|
686
729
|
`package:${nameSnake}_mobile/`,
|
|
687
730
|
".dart"
|
|
@@ -691,13 +734,14 @@ async function doUpdate(cwd, config, repoDir, version) {
|
|
|
691
734
|
version,
|
|
692
735
|
components: config.components,
|
|
693
736
|
createdAt: config.createdAt,
|
|
694
|
-
|
|
737
|
+
paths: componentPaths
|
|
695
738
|
};
|
|
696
739
|
await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
697
740
|
}
|
|
698
|
-
function detectProjectName(cwd, components) {
|
|
741
|
+
function detectProjectName(cwd, components, componentPaths) {
|
|
699
742
|
for (const component of components) {
|
|
700
|
-
const
|
|
743
|
+
const dir = componentPaths[component] ?? component;
|
|
744
|
+
const pkgPath = join4(cwd, dir, "package.json");
|
|
701
745
|
if (existsSync3(pkgPath)) {
|
|
702
746
|
try {
|
|
703
747
|
const pkg = JSON.parse(
|
|
@@ -754,44 +798,37 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
754
798
|
}
|
|
755
799
|
}
|
|
756
800
|
async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
757
|
-
const name = detectProjectName2(cwd, config.components);
|
|
758
|
-
const nameSnake = toSnake(name);
|
|
759
801
|
const allComponents = [...config.components, ...toAdd];
|
|
760
|
-
const
|
|
761
|
-
const
|
|
802
|
+
const existingPaths = await discoverComponentPaths(cwd, config.components);
|
|
803
|
+
const paths = { ...existingPaths };
|
|
804
|
+
for (const c of toAdd) paths[c] = c;
|
|
805
|
+
const name = detectProjectName2(cwd, config.components, paths);
|
|
806
|
+
const nameSnake = toSnake(name);
|
|
807
|
+
const vars = { projectName: name, components: allComponents, paths };
|
|
762
808
|
for (const component of toAdd) {
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
809
|
+
const spinner6 = p4.spinner();
|
|
810
|
+
spinner6.start(`Adding ${component}/`);
|
|
811
|
+
await copyComponent(repoDir, component, cwd);
|
|
812
|
+
await writeComponentMarker(join5(cwd, component), component);
|
|
813
|
+
spinner6.stop(`${component}/`);
|
|
768
814
|
}
|
|
769
815
|
await substituteNames2(cwd, toAdd, name, nameSnake);
|
|
770
|
-
const
|
|
771
|
-
|
|
816
|
+
const spinner5 = p4.spinner();
|
|
817
|
+
spinner5.start("Regenerating shared files");
|
|
772
818
|
const hasBackend = allComponents.includes("fastapi") || allComponents.includes("fastify");
|
|
773
819
|
if (hasBackend || allComponents.includes("frontend")) {
|
|
774
|
-
await writeFile4(
|
|
775
|
-
|
|
776
|
-
await generateDockerCompose(vars)
|
|
777
|
-
);
|
|
778
|
-
await writeFile4(
|
|
779
|
-
join5(cwd, "docker-compose.dev.yml"),
|
|
780
|
-
await generateDockerComposeDev(vars)
|
|
781
|
-
);
|
|
820
|
+
await writeFile4(join5(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
821
|
+
await writeFile4(join5(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
782
822
|
}
|
|
783
823
|
await writeFile4(join5(cwd, "README.md"), await generateReadme(vars));
|
|
784
824
|
await mkdir4(join5(cwd, ".githooks"), { recursive: true });
|
|
785
825
|
await writeFile4(join5(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
786
826
|
await chmod3(join5(cwd, ".githooks/pre-commit"), 493);
|
|
787
827
|
await mkdir4(join5(cwd, ".github/workflows"), { recursive: true });
|
|
788
|
-
await writeFile4(
|
|
789
|
-
join5(cwd, ".github/workflows/ci.yml"),
|
|
790
|
-
await generateCiYml(vars)
|
|
791
|
-
);
|
|
828
|
+
await writeFile4(join5(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
792
829
|
await writeFile4(join5(cwd, "setup.sh"), await generateSetupSh(vars));
|
|
793
830
|
await chmod3(join5(cwd, "setup.sh"), 493);
|
|
794
|
-
|
|
831
|
+
spinner5.stop("Shared files regenerated.");
|
|
795
832
|
if (!skipInstall) {
|
|
796
833
|
await installDeps2(cwd, toAdd);
|
|
797
834
|
}
|
|
@@ -812,10 +849,12 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
|
812
849
|
version: pkg.version,
|
|
813
850
|
components: allComponents,
|
|
814
851
|
createdAt: config.createdAt,
|
|
815
|
-
|
|
852
|
+
paths
|
|
816
853
|
};
|
|
817
854
|
await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
818
|
-
p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components
|
|
855
|
+
p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
|
|
856
|
+
|
|
857
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
819
858
|
}
|
|
820
859
|
async function substituteNames2(dest, components, name, nameSnake) {
|
|
821
860
|
if (components.includes("fastapi")) {
|
|
@@ -862,44 +901,44 @@ async function substituteNames2(dest, components, name, nameSnake) {
|
|
|
862
901
|
}
|
|
863
902
|
async function installDeps2(dest, components) {
|
|
864
903
|
for (const component of components) {
|
|
865
|
-
const
|
|
904
|
+
const spinner5 = p4.spinner();
|
|
866
905
|
try {
|
|
867
906
|
switch (component) {
|
|
868
907
|
case "fastapi":
|
|
869
908
|
if (hasCommand("uv")) {
|
|
870
|
-
|
|
909
|
+
spinner5.start("Installing FastAPI dependencies");
|
|
871
910
|
exec("uv sync --all-extras", join5(dest, "fastapi"));
|
|
872
|
-
|
|
911
|
+
spinner5.stop("FastAPI dependencies installed.");
|
|
873
912
|
} else {
|
|
874
913
|
p4.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
875
914
|
}
|
|
876
915
|
break;
|
|
877
916
|
case "fastify":
|
|
878
917
|
if (hasCommand("pnpm")) {
|
|
879
|
-
|
|
918
|
+
spinner5.start("Installing Fastify dependencies");
|
|
880
919
|
exec("pnpm install", join5(dest, "fastify"));
|
|
881
|
-
|
|
920
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
882
921
|
} else {
|
|
883
|
-
|
|
922
|
+
spinner5.start("Installing Fastify dependencies");
|
|
884
923
|
exec("npm install", join5(dest, "fastify"));
|
|
885
|
-
|
|
924
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
886
925
|
}
|
|
887
926
|
break;
|
|
888
927
|
case "frontend":
|
|
889
|
-
|
|
928
|
+
spinner5.start("Installing Frontend dependencies");
|
|
890
929
|
exec("npm install", join5(dest, "frontend"));
|
|
891
|
-
|
|
930
|
+
spinner5.stop("Frontend dependencies installed.");
|
|
892
931
|
break;
|
|
893
932
|
case "e2e":
|
|
894
|
-
|
|
933
|
+
spinner5.start("Installing E2E dependencies");
|
|
895
934
|
exec("npm install", join5(dest, "e2e"));
|
|
896
|
-
|
|
935
|
+
spinner5.stop("E2E dependencies installed.");
|
|
897
936
|
break;
|
|
898
937
|
case "mobile":
|
|
899
938
|
if (hasCommand("flutter")) {
|
|
900
|
-
|
|
939
|
+
spinner5.start("Installing Flutter dependencies");
|
|
901
940
|
exec("flutter pub get", join5(dest, "mobile"));
|
|
902
|
-
|
|
941
|
+
spinner5.stop("Flutter dependencies installed.");
|
|
903
942
|
} else {
|
|
904
943
|
p4.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
905
944
|
}
|
|
@@ -908,13 +947,14 @@ async function installDeps2(dest, components) {
|
|
|
908
947
|
break;
|
|
909
948
|
}
|
|
910
949
|
} catch {
|
|
911
|
-
|
|
950
|
+
spinner5.stop(`Failed to install ${component} dependencies.`);
|
|
912
951
|
}
|
|
913
952
|
}
|
|
914
953
|
}
|
|
915
|
-
function detectProjectName2(cwd, components) {
|
|
954
|
+
function detectProjectName2(cwd, components, paths) {
|
|
916
955
|
for (const component of components) {
|
|
917
|
-
const
|
|
956
|
+
const dir = paths[component] ?? component;
|
|
957
|
+
const pkgPath = join5(cwd, dir, "package.json");
|
|
918
958
|
if (existsSync4(pkgPath)) {
|
|
919
959
|
try {
|
|
920
960
|
const pkg = JSON.parse(
|
|
@@ -931,6 +971,382 @@ function detectProjectName2(cwd, components) {
|
|
|
931
971
|
return toKebab(cwd.split("/").pop());
|
|
932
972
|
}
|
|
933
973
|
|
|
974
|
+
// src/init.ts
|
|
975
|
+
import { existsSync as existsSync6 } from "fs";
|
|
976
|
+
import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5, chmod as chmod4, cp as cp3 } from "fs/promises";
|
|
977
|
+
import { execSync as execSync3 } from "child_process";
|
|
978
|
+
import { join as join7 } from "path";
|
|
979
|
+
import * as p5 from "@clack/prompts";
|
|
980
|
+
|
|
981
|
+
// src/detect.ts
|
|
982
|
+
import { existsSync as existsSync5 } from "fs";
|
|
983
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
984
|
+
import { join as join6 } from "path";
|
|
985
|
+
async function detectComponents(cwd) {
|
|
986
|
+
const results = [];
|
|
987
|
+
const entries = await readdir2(cwd, { withFileTypes: true });
|
|
988
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)).map((e) => e.name);
|
|
989
|
+
for (const dir of dirs) {
|
|
990
|
+
const full = join6(cwd, dir);
|
|
991
|
+
const detections = await scanDirectory(full, dir);
|
|
992
|
+
results.push(...detections);
|
|
993
|
+
}
|
|
994
|
+
return results;
|
|
995
|
+
}
|
|
996
|
+
async function scanDirectory(dir, relPath) {
|
|
997
|
+
const results = [];
|
|
998
|
+
const pyproject = await readFileOrNull(join6(dir, "pyproject.toml"));
|
|
999
|
+
if (pyproject && /fastapi/i.test(pyproject)) {
|
|
1000
|
+
results.push({
|
|
1001
|
+
component: "fastapi",
|
|
1002
|
+
directory: relPath,
|
|
1003
|
+
confidence: "high",
|
|
1004
|
+
evidence: "pyproject.toml has fastapi dependency"
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const pkg = await readPkg(dir);
|
|
1008
|
+
if (pkg) {
|
|
1009
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1010
|
+
if (allDeps.fastify) {
|
|
1011
|
+
results.push({
|
|
1012
|
+
component: "fastify",
|
|
1013
|
+
directory: relPath,
|
|
1014
|
+
confidence: "high",
|
|
1015
|
+
evidence: "package.json has fastify dependency"
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
if (allDeps.react || allDeps["react-dom"]) {
|
|
1019
|
+
results.push({
|
|
1020
|
+
component: "frontend",
|
|
1021
|
+
directory: relPath,
|
|
1022
|
+
confidence: "high",
|
|
1023
|
+
evidence: "package.json has react dependency"
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
if (allDeps["@playwright/test"] || allDeps.playwright) {
|
|
1027
|
+
results.push({
|
|
1028
|
+
component: "e2e",
|
|
1029
|
+
directory: relPath,
|
|
1030
|
+
confidence: "high",
|
|
1031
|
+
evidence: "package.json has playwright dependency"
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const pubspec = await readFileOrNull(join6(dir, "pubspec.yaml"));
|
|
1036
|
+
if (pubspec && /flutter:/i.test(pubspec)) {
|
|
1037
|
+
results.push({
|
|
1038
|
+
component: "mobile",
|
|
1039
|
+
directory: relPath,
|
|
1040
|
+
confidence: "high",
|
|
1041
|
+
evidence: "pubspec.yaml has flutter dependency"
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const hasTf = existsSync5(join6(dir, "main.tf")) || existsSync5(join6(dir, "variables.tf")) || existsSync5(join6(dir, "stack/main.tf")) || existsSync5(join6(dir, "versions.tf"));
|
|
1045
|
+
if (hasTf) {
|
|
1046
|
+
results.push({
|
|
1047
|
+
component: "infra",
|
|
1048
|
+
directory: relPath,
|
|
1049
|
+
confidence: "high",
|
|
1050
|
+
evidence: "Terraform .tf files found"
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return results;
|
|
1054
|
+
}
|
|
1055
|
+
async function readPkg(dir) {
|
|
1056
|
+
const content = await readFileOrNull(join6(dir, "package.json"));
|
|
1057
|
+
if (!content) return null;
|
|
1058
|
+
try {
|
|
1059
|
+
return JSON.parse(content);
|
|
1060
|
+
} catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/diff.ts
|
|
1066
|
+
function unifiedDiff(existing, template, label) {
|
|
1067
|
+
const a = existing.split("\n");
|
|
1068
|
+
const b = template.split("\n");
|
|
1069
|
+
const lines = [`--- existing ${label}`, `+++ template ${label}`];
|
|
1070
|
+
const lcs = computeLCS(a, b);
|
|
1071
|
+
let ai = 0;
|
|
1072
|
+
let bi = 0;
|
|
1073
|
+
for (const match of lcs) {
|
|
1074
|
+
while (ai < match.ai) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
|
|
1075
|
+
while (bi < match.bi) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
|
|
1076
|
+
lines.push(` ${a[ai]}`);
|
|
1077
|
+
ai++;
|
|
1078
|
+
bi++;
|
|
1079
|
+
}
|
|
1080
|
+
while (ai < a.length) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
|
|
1081
|
+
while (bi < b.length) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
|
|
1082
|
+
if (lines.length > 80) {
|
|
1083
|
+
return lines.slice(0, 80).join("\n") + `
|
|
1084
|
+
... (${lines.length - 80} more lines)`;
|
|
1085
|
+
}
|
|
1086
|
+
return lines.join("\n");
|
|
1087
|
+
}
|
|
1088
|
+
function computeLCS(a, b) {
|
|
1089
|
+
const m = a.length;
|
|
1090
|
+
const n = b.length;
|
|
1091
|
+
if (m * n > 1e5) {
|
|
1092
|
+
return simpleLCS(a, b);
|
|
1093
|
+
}
|
|
1094
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
1095
|
+
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
1096
|
+
for (let j2 = n - 1; j2 >= 0; j2--) {
|
|
1097
|
+
if (a[i2] === b[j2]) {
|
|
1098
|
+
dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
|
|
1099
|
+
} else {
|
|
1100
|
+
dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const matches = [];
|
|
1105
|
+
let i = 0;
|
|
1106
|
+
let j = 0;
|
|
1107
|
+
while (i < m && j < n) {
|
|
1108
|
+
if (a[i] === b[j]) {
|
|
1109
|
+
matches.push({ ai: i, bi: j });
|
|
1110
|
+
i++;
|
|
1111
|
+
j++;
|
|
1112
|
+
} else if (dp[i + 1]?.[j] ?? 0 >= (dp[i]?.[j + 1] ?? 0)) {
|
|
1113
|
+
i++;
|
|
1114
|
+
} else {
|
|
1115
|
+
j++;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return matches;
|
|
1119
|
+
}
|
|
1120
|
+
function simpleLCS(a, b) {
|
|
1121
|
+
const matches = [];
|
|
1122
|
+
let bi = 0;
|
|
1123
|
+
for (let ai = 0; ai < a.length && bi < b.length; ai++) {
|
|
1124
|
+
const idx = b.indexOf(a[ai], bi);
|
|
1125
|
+
if (idx !== -1) {
|
|
1126
|
+
matches.push({ ai, bi: idx });
|
|
1127
|
+
bi = idx + 1;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return matches;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/init.ts
|
|
1134
|
+
async function init(cwd, localRepo) {
|
|
1135
|
+
p5.intro("projx init");
|
|
1136
|
+
const isLocal = !!localRepo;
|
|
1137
|
+
if (existsSync6(join7(cwd, ".projx"))) {
|
|
1138
|
+
p5.log.error("This project is already initialized. Use 'projx update' or 'projx add' instead.");
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
1141
|
+
const spinner5 = p5.spinner();
|
|
1142
|
+
spinner5.start("Scanning for components");
|
|
1143
|
+
const detected = await detectComponents(cwd);
|
|
1144
|
+
spinner5.stop(
|
|
1145
|
+
detected.length > 0 ? `Found ${detected.length} component(s).` : "No components detected."
|
|
1146
|
+
);
|
|
1147
|
+
let confirmed;
|
|
1148
|
+
if (detected.length > 0) {
|
|
1149
|
+
confirmed = await confirmDetections(detected);
|
|
1150
|
+
} else {
|
|
1151
|
+
confirmed = await manualSelect(cwd);
|
|
1152
|
+
}
|
|
1153
|
+
if (confirmed.length === 0) {
|
|
1154
|
+
p5.log.warn("No components selected. Nothing to do.");
|
|
1155
|
+
process.exit(0);
|
|
1156
|
+
}
|
|
1157
|
+
const components = confirmed.map((c) => c.component);
|
|
1158
|
+
const paths = Object.fromEntries(
|
|
1159
|
+
confirmed.map((c) => [c.component, c.directory])
|
|
1160
|
+
);
|
|
1161
|
+
const projectName = toKebab(cwd.split("/").pop());
|
|
1162
|
+
const vars = { projectName, components, paths };
|
|
1163
|
+
const dlSpinner = p5.spinner();
|
|
1164
|
+
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
1165
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1166
|
+
dlSpinner.stop("Failed.");
|
|
1167
|
+
p5.log.error(String(err));
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
});
|
|
1170
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1171
|
+
try {
|
|
1172
|
+
for (const { component, directory } of confirmed) {
|
|
1173
|
+
const dir = join7(cwd, directory);
|
|
1174
|
+
if (existsSync6(dir)) {
|
|
1175
|
+
await writeComponentMarker(dir, component);
|
|
1176
|
+
p5.log.success(`${directory}/.projx-component`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
await generateSharedFiles(cwd, repoDir, vars);
|
|
1180
|
+
const pkg = JSON.parse(
|
|
1181
|
+
await readFile6(join7(repoDir, "cli/package.json"), "utf-8")
|
|
1182
|
+
);
|
|
1183
|
+
const projxConfig = {
|
|
1184
|
+
version: pkg.version,
|
|
1185
|
+
components,
|
|
1186
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
1187
|
+
paths
|
|
1188
|
+
};
|
|
1189
|
+
await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
1190
|
+
p5.log.success(".projx");
|
|
1191
|
+
if (isGitRepo2(cwd)) {
|
|
1192
|
+
try {
|
|
1193
|
+
execSync3("git config core.hooksPath .githooks", { cwd, stdio: "pipe" });
|
|
1194
|
+
p5.log.success("Git hooks configured.");
|
|
1195
|
+
} catch {
|
|
1196
|
+
p5.log.warn("Failed to configure git hooks.");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} finally {
|
|
1200
|
+
await cleanupRepo(repoDir, isLocal);
|
|
1201
|
+
}
|
|
1202
|
+
p5.outro("Project initialized. Run './setup.sh' to install dependencies.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx");
|
|
1203
|
+
}
|
|
1204
|
+
async function confirmDetections(detected) {
|
|
1205
|
+
const confirmed = [];
|
|
1206
|
+
for (const d of detected) {
|
|
1207
|
+
const yes = await p5.confirm({
|
|
1208
|
+
message: `Found ${LABELS[d.component].label} in ${d.directory}/ \u2014 register as "${d.component}"?`,
|
|
1209
|
+
initialValue: true
|
|
1210
|
+
});
|
|
1211
|
+
if (p5.isCancel(yes)) process.exit(0);
|
|
1212
|
+
if (yes) {
|
|
1213
|
+
confirmed.push({ component: d.component, directory: d.directory });
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return confirmed;
|
|
1217
|
+
}
|
|
1218
|
+
async function manualSelect(cwd) {
|
|
1219
|
+
const selected = await p5.multiselect({
|
|
1220
|
+
message: "No components detected. Select manually:",
|
|
1221
|
+
options: COMPONENTS.map((c) => ({
|
|
1222
|
+
value: c,
|
|
1223
|
+
label: LABELS[c].label,
|
|
1224
|
+
hint: LABELS[c].hint
|
|
1225
|
+
})),
|
|
1226
|
+
required: false
|
|
1227
|
+
});
|
|
1228
|
+
if (p5.isCancel(selected)) process.exit(0);
|
|
1229
|
+
const result = [];
|
|
1230
|
+
for (const component of selected) {
|
|
1231
|
+
const dir = await p5.text({
|
|
1232
|
+
message: `Directory for ${LABELS[component].label}?`,
|
|
1233
|
+
placeholder: component,
|
|
1234
|
+
defaultValue: component
|
|
1235
|
+
});
|
|
1236
|
+
if (p5.isCancel(dir)) process.exit(0);
|
|
1237
|
+
if (!existsSync6(join7(cwd, dir))) {
|
|
1238
|
+
p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
result.push({ component, directory: dir });
|
|
1242
|
+
}
|
|
1243
|
+
return result;
|
|
1244
|
+
}
|
|
1245
|
+
async function generateSharedFiles(cwd, repoDir, vars) {
|
|
1246
|
+
const files = [];
|
|
1247
|
+
const hasBackend = vars.components.includes("fastapi") || vars.components.includes("fastify");
|
|
1248
|
+
if (hasBackend || vars.components.includes("frontend")) {
|
|
1249
|
+
files.push(
|
|
1250
|
+
{ path: "docker-compose.yml", content: await generateDockerCompose(vars) },
|
|
1251
|
+
{ path: "docker-compose.dev.yml", content: await generateDockerComposeDev(vars) }
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
files.push(
|
|
1255
|
+
{ path: "README.md", content: await generateReadme(vars) },
|
|
1256
|
+
{ path: ".githooks/pre-commit", content: await generatePreCommit(vars), mode: 493 },
|
|
1257
|
+
{ path: ".github/workflows/ci.yml", content: await generateCiYml(vars) },
|
|
1258
|
+
{ path: "setup.sh", content: await generateSetupSh(vars), mode: 493 }
|
|
1259
|
+
);
|
|
1260
|
+
for (const file of files) {
|
|
1261
|
+
const dest = join7(cwd, file.path);
|
|
1262
|
+
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
1263
|
+
if (dir !== cwd) await mkdir5(dir, { recursive: true });
|
|
1264
|
+
const existing = await readFileOrNull(dest);
|
|
1265
|
+
if (existing === null) {
|
|
1266
|
+
await writeFile5(dest, file.content);
|
|
1267
|
+
if (file.mode) await chmod4(dest, file.mode);
|
|
1268
|
+
p5.log.success(file.path);
|
|
1269
|
+
} else if (existing === file.content) {
|
|
1270
|
+
p5.log.info(`${file.path} \u2014 identical, skipped.`);
|
|
1271
|
+
} else {
|
|
1272
|
+
const action = await resolveConflict(file.path, existing, file.content);
|
|
1273
|
+
if (action === "overwrite") {
|
|
1274
|
+
await writeFile5(dest, file.content);
|
|
1275
|
+
if (file.mode) await chmod4(dest, file.mode);
|
|
1276
|
+
p5.log.success(`${file.path} \u2014 overwritten.`);
|
|
1277
|
+
} else {
|
|
1278
|
+
p5.log.info(`${file.path} \u2014 kept existing.`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const statics = [".editorconfig", "LICENSE"];
|
|
1283
|
+
for (const file of statics) {
|
|
1284
|
+
const src = join7(repoDir, file);
|
|
1285
|
+
const dest = join7(cwd, file);
|
|
1286
|
+
if (!existsSync6(src)) continue;
|
|
1287
|
+
if (!existsSync6(dest)) {
|
|
1288
|
+
await cp3(src, dest);
|
|
1289
|
+
p5.log.success(file);
|
|
1290
|
+
} else {
|
|
1291
|
+
const existing = await readFileOrNull(dest);
|
|
1292
|
+
const template = await readFileOrNull(src);
|
|
1293
|
+
if (existing === template) {
|
|
1294
|
+
p5.log.info(`${file} \u2014 identical, skipped.`);
|
|
1295
|
+
} else {
|
|
1296
|
+
const action = await resolveConflict(file, existing ?? "", template ?? "");
|
|
1297
|
+
if (action === "overwrite") {
|
|
1298
|
+
await cp3(src, dest, { force: true });
|
|
1299
|
+
p5.log.success(`${file} \u2014 overwritten.`);
|
|
1300
|
+
} else {
|
|
1301
|
+
p5.log.info(`${file} \u2014 kept existing.`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const vscode = join7(repoDir, ".vscode");
|
|
1307
|
+
if (existsSync6(vscode)) {
|
|
1308
|
+
const vscodeDest = join7(cwd, ".vscode");
|
|
1309
|
+
if (!existsSync6(vscodeDest)) {
|
|
1310
|
+
await cp3(vscode, vscodeDest, { recursive: true });
|
|
1311
|
+
p5.log.success(".vscode/");
|
|
1312
|
+
} else {
|
|
1313
|
+
p5.log.info(".vscode/ \u2014 already exists, skipped.");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function resolveConflict(filePath, existing, template) {
|
|
1318
|
+
let action = await p5.select({
|
|
1319
|
+
message: `${filePath} differs from projx template`,
|
|
1320
|
+
options: [
|
|
1321
|
+
{ value: "diff", label: "View diff" },
|
|
1322
|
+
{ value: "overwrite", label: "Overwrite with template" },
|
|
1323
|
+
{ value: "skip", label: "Skip (keep existing)" }
|
|
1324
|
+
]
|
|
1325
|
+
});
|
|
1326
|
+
if (p5.isCancel(action)) process.exit(0);
|
|
1327
|
+
if (action === "diff") {
|
|
1328
|
+
const diff = unifiedDiff(existing, template, filePath);
|
|
1329
|
+
p5.log.message(diff);
|
|
1330
|
+
action = await p5.select({
|
|
1331
|
+
message: `${filePath}`,
|
|
1332
|
+
options: [
|
|
1333
|
+
{ value: "overwrite", label: "Overwrite with template" },
|
|
1334
|
+
{ value: "skip", label: "Skip (keep existing)" }
|
|
1335
|
+
]
|
|
1336
|
+
});
|
|
1337
|
+
if (p5.isCancel(action)) process.exit(0);
|
|
1338
|
+
}
|
|
1339
|
+
return action;
|
|
1340
|
+
}
|
|
1341
|
+
function isGitRepo2(cwd) {
|
|
1342
|
+
try {
|
|
1343
|
+
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1344
|
+
return true;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
934
1350
|
// src/index.ts
|
|
935
1351
|
var args = process.argv.slice(2);
|
|
936
1352
|
function parseArgs() {
|
|
@@ -949,6 +1365,10 @@ function parseArgs() {
|
|
|
949
1365
|
command = "add";
|
|
950
1366
|
continue;
|
|
951
1367
|
}
|
|
1368
|
+
if (arg === "init" && !name) {
|
|
1369
|
+
command = "init";
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
952
1372
|
if (arg === "--components") {
|
|
953
1373
|
const val = args[++i];
|
|
954
1374
|
if (val) {
|
|
@@ -992,6 +1412,7 @@ function printHelp() {
|
|
|
992
1412
|
console.log(`
|
|
993
1413
|
Usage:
|
|
994
1414
|
projx <name> [options] Create a new project
|
|
1415
|
+
projx init Adopt existing project into projx
|
|
995
1416
|
projx add <components...> Add components to existing project
|
|
996
1417
|
projx update Update scaffolding to latest
|
|
997
1418
|
|
|
@@ -1013,6 +1434,10 @@ function printHelp() {
|
|
|
1013
1434
|
}
|
|
1014
1435
|
async function main() {
|
|
1015
1436
|
const { command, name, options, localRepo, extraArgs } = parseArgs();
|
|
1437
|
+
if (command === "init") {
|
|
1438
|
+
await init(process.cwd(), localRepo);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1016
1441
|
if (command === "update") {
|
|
1017
1442
|
await update(process.cwd(), localRepo);
|
|
1018
1443
|
return;
|
|
@@ -1046,7 +1471,7 @@ async function main() {
|
|
|
1046
1471
|
opts.install = options.install ?? opts.install;
|
|
1047
1472
|
}
|
|
1048
1473
|
const dest = resolve2(process.cwd(), opts.name);
|
|
1049
|
-
if (
|
|
1474
|
+
if (existsSync7(dest)) {
|
|
1050
1475
|
console.error(`Error: ${dest} already exists.`);
|
|
1051
1476
|
process.exit(1);
|
|
1052
1477
|
}
|