create-projx 1.0.0 → 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/README.md +144 -0
- package/dist/index.js +545 -127
- package/package.json +14 -2
- package/src/templates/README.md.ejs +23 -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 +6 -6
- package/src/templates/Makefile.ejs +0 -286
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
|
}
|
|
@@ -271,9 +319,6 @@ async function generateDockerCompose(vars) {
|
|
|
271
319
|
async function generateDockerComposeDev(vars) {
|
|
272
320
|
return renderShared("docker-compose.dev.yml.ejs", vars);
|
|
273
321
|
}
|
|
274
|
-
async function generateMakefile(vars) {
|
|
275
|
-
return renderShared("Makefile.ejs", vars);
|
|
276
|
-
}
|
|
277
322
|
async function generatePreCommit(vars) {
|
|
278
323
|
return renderShared("pre-commit.ejs", vars);
|
|
279
324
|
}
|
|
@@ -291,7 +336,10 @@ async function generateReadme(vars) {
|
|
|
291
336
|
async function scaffold(opts, dest, localRepo) {
|
|
292
337
|
const name = toKebab(opts.name);
|
|
293
338
|
const nameSnake = toSnake(opts.name);
|
|
294
|
-
const
|
|
339
|
+
const paths = Object.fromEntries(
|
|
340
|
+
opts.components.map((c) => [c, c])
|
|
341
|
+
);
|
|
342
|
+
const vars = { projectName: name, components: opts.components, paths };
|
|
295
343
|
const isLocal = !!localRepo;
|
|
296
344
|
await mkdir2(dest, { recursive: true });
|
|
297
345
|
const dlSpinner = p2.spinner();
|
|
@@ -310,45 +358,28 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
310
358
|
}
|
|
311
359
|
async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
312
360
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
313
|
-
const manifest = [];
|
|
314
361
|
for (const component of opts.components) {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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}/`);
|
|
320
367
|
}
|
|
321
368
|
await substituteNames(dest, opts.components, name, nameSnake);
|
|
322
369
|
const hasBackend = opts.components.includes("fastapi") || opts.components.includes("fastify");
|
|
323
370
|
if (hasBackend || opts.components.includes("frontend")) {
|
|
324
|
-
|
|
325
|
-
await writeFile2(join3(dest, "docker-compose.yml"),
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
await writeFile2(join3(dest, "docker-compose.dev.yml"), dcDev);
|
|
329
|
-
manifest.push("docker-compose.dev.yml");
|
|
330
|
-
}
|
|
331
|
-
const makefile = await generateMakefile(vars);
|
|
332
|
-
await writeFile2(join3(dest, "Makefile"), makefile);
|
|
333
|
-
manifest.push("Makefile");
|
|
334
|
-
const readme = await generateReadme(vars);
|
|
335
|
-
await writeFile2(join3(dest, "README.md"), readme);
|
|
336
|
-
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));
|
|
337
375
|
await mkdir2(join3(dest, ".githooks"), { recursive: true });
|
|
338
|
-
|
|
339
|
-
await writeFile2(join3(dest, ".githooks/pre-commit"), preCommit);
|
|
376
|
+
await writeFile2(join3(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
340
377
|
await chmod(join3(dest, ".githooks/pre-commit"), 493);
|
|
341
|
-
manifest.push(".githooks/pre-commit");
|
|
342
378
|
await mkdir2(join3(dest, ".github/workflows"), { recursive: true });
|
|
343
|
-
|
|
344
|
-
await writeFile2(join3(dest, ".
|
|
345
|
-
manifest.push(".github/workflows/ci.yml");
|
|
346
|
-
const setupSh = await generateSetupSh(vars);
|
|
347
|
-
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));
|
|
348
381
|
await chmod(join3(dest, "setup.sh"), 493);
|
|
349
|
-
|
|
350
|
-
const staticFiles = await copyStaticFiles(repoDir, dest);
|
|
351
|
-
manifest.push(...staticFiles);
|
|
382
|
+
await copyStaticFiles(repoDir, dest);
|
|
352
383
|
const pkg = JSON.parse(
|
|
353
384
|
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
354
385
|
);
|
|
@@ -356,7 +387,7 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
356
387
|
version: pkg.version,
|
|
357
388
|
components: opts.components,
|
|
358
389
|
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
359
|
-
|
|
390
|
+
paths: vars.paths
|
|
360
391
|
};
|
|
361
392
|
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
362
393
|
if (opts.git) {
|
|
@@ -383,7 +414,9 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
383
414
|
p2.outro(`Done! Next steps:
|
|
384
415
|
|
|
385
416
|
cd ${name}
|
|
386
|
-
|
|
417
|
+
./setup.sh
|
|
418
|
+
|
|
419
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
387
420
|
}
|
|
388
421
|
async function substituteNames(dest, components, name, nameSnake) {
|
|
389
422
|
if (components.includes("fastapi")) {
|
|
@@ -430,44 +463,44 @@ async function substituteNames(dest, components, name, nameSnake) {
|
|
|
430
463
|
}
|
|
431
464
|
async function installDeps(dest, components) {
|
|
432
465
|
for (const component of components) {
|
|
433
|
-
const
|
|
466
|
+
const spinner5 = p2.spinner();
|
|
434
467
|
try {
|
|
435
468
|
switch (component) {
|
|
436
469
|
case "fastapi":
|
|
437
470
|
if (hasCommand("uv")) {
|
|
438
|
-
|
|
471
|
+
spinner5.start("Installing FastAPI dependencies (uv sync)");
|
|
439
472
|
exec("uv sync --all-extras", join3(dest, "fastapi"));
|
|
440
|
-
|
|
473
|
+
spinner5.stop("FastAPI dependencies installed.");
|
|
441
474
|
} else {
|
|
442
475
|
p2.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
443
476
|
}
|
|
444
477
|
break;
|
|
445
478
|
case "fastify":
|
|
446
479
|
if (hasCommand("pnpm")) {
|
|
447
|
-
|
|
480
|
+
spinner5.start("Installing Fastify dependencies (pnpm install)");
|
|
448
481
|
exec("pnpm install", join3(dest, "fastify"));
|
|
449
|
-
|
|
482
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
450
483
|
} else {
|
|
451
|
-
|
|
484
|
+
spinner5.start("Installing Fastify dependencies (npm install)");
|
|
452
485
|
exec("npm install", join3(dest, "fastify"));
|
|
453
|
-
|
|
486
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
454
487
|
}
|
|
455
488
|
break;
|
|
456
489
|
case "frontend":
|
|
457
|
-
|
|
490
|
+
spinner5.start("Installing Frontend dependencies (npm install)");
|
|
458
491
|
exec("npm install", join3(dest, "frontend"));
|
|
459
|
-
|
|
492
|
+
spinner5.stop("Frontend dependencies installed.");
|
|
460
493
|
break;
|
|
461
494
|
case "e2e":
|
|
462
|
-
|
|
495
|
+
spinner5.start("Installing E2E dependencies (npm install)");
|
|
463
496
|
exec("npm install", join3(dest, "e2e"));
|
|
464
|
-
|
|
497
|
+
spinner5.stop("E2E dependencies installed.");
|
|
465
498
|
break;
|
|
466
499
|
case "mobile":
|
|
467
500
|
if (hasCommand("flutter")) {
|
|
468
|
-
|
|
501
|
+
spinner5.start("Installing Flutter dependencies");
|
|
469
502
|
exec("flutter pub get", join3(dest, "mobile"));
|
|
470
|
-
|
|
503
|
+
spinner5.stop("Flutter dependencies installed.");
|
|
471
504
|
} else {
|
|
472
505
|
p2.log.warn(
|
|
473
506
|
"Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
|
|
@@ -478,7 +511,7 @@ async function installDeps(dest, components) {
|
|
|
478
511
|
break;
|
|
479
512
|
}
|
|
480
513
|
} catch {
|
|
481
|
-
|
|
514
|
+
spinner5.stop(`Failed to install ${component} dependencies.`);
|
|
482
515
|
}
|
|
483
516
|
}
|
|
484
517
|
}
|
|
@@ -505,7 +538,8 @@ var NEVER_OVERWRITE = [
|
|
|
505
538
|
/\.env$/,
|
|
506
539
|
/\.env\.(dev|staging|prod)$/,
|
|
507
540
|
/prisma\/migrations\//,
|
|
508
|
-
/src\/migrations\/versions
|
|
541
|
+
/src\/migrations\/versions\//,
|
|
542
|
+
/\.projx-component$/
|
|
509
543
|
];
|
|
510
544
|
function isGitRepo(cwd) {
|
|
511
545
|
try {
|
|
@@ -556,11 +590,17 @@ async function update(cwd, localRepo) {
|
|
|
556
590
|
config = {
|
|
557
591
|
version: "0.0.0",
|
|
558
592
|
components: detected,
|
|
559
|
-
createdAt: "unknown"
|
|
560
|
-
files: []
|
|
593
|
+
createdAt: "unknown"
|
|
561
594
|
};
|
|
562
595
|
p3.log.info(`Detected: ${detected.join(", ")}`);
|
|
563
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
|
+
}
|
|
564
604
|
const useGitBranch = isGitRepo(cwd);
|
|
565
605
|
let branchName;
|
|
566
606
|
let originalBranch;
|
|
@@ -590,7 +630,7 @@ async function update(cwd, localRepo) {
|
|
|
590
630
|
execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
|
|
591
631
|
p3.log.info(`Created branch: ${branchName}`);
|
|
592
632
|
try {
|
|
593
|
-
await doUpdate(cwd, config, repoDir, pkg.version);
|
|
633
|
+
await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
594
634
|
} finally {
|
|
595
635
|
await cleanupRepo(repoDir, isLocal);
|
|
596
636
|
}
|
|
@@ -618,76 +658,73 @@ async function update(cwd, localRepo) {
|
|
|
618
658
|
await readFile4(join4(repoDir, "cli/package.json"), "utf-8")
|
|
619
659
|
);
|
|
620
660
|
try {
|
|
621
|
-
await doUpdate(cwd, config, repoDir, pkg.version);
|
|
661
|
+
await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
622
662
|
} finally {
|
|
623
663
|
await cleanupRepo(repoDir, isLocal);
|
|
624
664
|
}
|
|
625
665
|
p3.outro(`Updated to v${pkg.version}. Review changes before committing.`);
|
|
626
666
|
}
|
|
627
667
|
}
|
|
628
|
-
async function doUpdate(cwd, config, repoDir, version) {
|
|
629
|
-
const name = detectProjectName(cwd, config.components);
|
|
668
|
+
async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
669
|
+
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
630
670
|
const nameSnake = toSnake(name);
|
|
631
|
-
const vars = { projectName: name, components: config.components };
|
|
632
|
-
const newManifest = [];
|
|
671
|
+
const vars = { projectName: name, components: config.components, paths: componentPaths };
|
|
633
672
|
for (const component of config.components) {
|
|
634
|
-
const
|
|
635
|
-
|
|
673
|
+
const targetDir = componentPaths[component];
|
|
674
|
+
const spinner6 = p3.spinner();
|
|
675
|
+
spinner6.start(`Updating ${targetDir}/ (${component})`);
|
|
636
676
|
const componentSrc = join4(repoDir, component);
|
|
637
677
|
if (!existsSync3(componentSrc)) {
|
|
638
|
-
|
|
678
|
+
spinner6.stop(`${component} template not found, skipping.`);
|
|
639
679
|
continue;
|
|
640
680
|
}
|
|
641
681
|
const tmpDest = join4(cwd, `.projx-tmp`);
|
|
642
682
|
const files = await copyComponent(repoDir, component, tmpDest);
|
|
643
683
|
for (const file of files) {
|
|
644
|
-
const rel = `${component}/${file}`;
|
|
645
684
|
const src = join4(tmpDest, component, file);
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
if (
|
|
685
|
+
const destRel = `${targetDir}/${file}`;
|
|
686
|
+
const dest = join4(cwd, destRel);
|
|
687
|
+
if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
|
|
649
688
|
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
650
689
|
await mkdir3(dir, { recursive: true });
|
|
651
690
|
await cp2(src, dest, { force: true });
|
|
652
|
-
newManifest.push(rel);
|
|
653
691
|
}
|
|
654
692
|
await rm2(tmpDest, { recursive: true, force: true });
|
|
655
|
-
|
|
693
|
+
if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
|
|
694
|
+
await writeComponentMarker(join4(cwd, targetDir), component);
|
|
695
|
+
}
|
|
696
|
+
spinner6.stop(`${targetDir}/ updated.`);
|
|
656
697
|
}
|
|
657
|
-
const
|
|
658
|
-
|
|
698
|
+
const spinner5 = p3.spinner();
|
|
699
|
+
spinner5.start("Updating shared files");
|
|
659
700
|
const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
|
|
660
701
|
if (hasBackend || config.components.includes("frontend")) {
|
|
661
702
|
await writeFile3(
|
|
662
703
|
join4(cwd, "docker-compose.yml"),
|
|
663
704
|
await generateDockerCompose(vars)
|
|
664
705
|
);
|
|
665
|
-
newManifest.push("docker-compose.yml");
|
|
666
706
|
await writeFile3(
|
|
667
707
|
join4(cwd, "docker-compose.dev.yml"),
|
|
668
708
|
await generateDockerComposeDev(vars)
|
|
669
709
|
);
|
|
670
|
-
newManifest.push("docker-compose.dev.yml");
|
|
671
710
|
}
|
|
672
711
|
await mkdir3(join4(cwd, ".githooks"), { recursive: true });
|
|
673
712
|
const preCommit = await generatePreCommit(vars);
|
|
674
713
|
await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
|
|
675
714
|
await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
|
|
676
|
-
newManifest.push(".githooks/pre-commit");
|
|
677
715
|
await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
|
|
678
716
|
await writeFile3(
|
|
679
717
|
join4(cwd, ".github/workflows/ci.yml"),
|
|
680
718
|
await generateCiYml(vars)
|
|
681
719
|
);
|
|
682
|
-
newManifest.push(".github/workflows/ci.yml");
|
|
683
720
|
const setupSh = await generateSetupSh(vars);
|
|
684
721
|
await writeFile3(join4(cwd, "setup.sh"), setupSh);
|
|
685
722
|
await chmod2(join4(cwd, "setup.sh"), 493);
|
|
686
|
-
|
|
687
|
-
spinner4.stop("Shared files updated.");
|
|
723
|
+
spinner5.stop("Shared files updated.");
|
|
688
724
|
if (config.components.includes("mobile")) {
|
|
725
|
+
const mobilePath = componentPaths.mobile ?? "mobile";
|
|
689
726
|
await replaceInDir(
|
|
690
|
-
join4(cwd,
|
|
727
|
+
join4(cwd, mobilePath),
|
|
691
728
|
"package:projx_mobile/",
|
|
692
729
|
`package:${nameSnake}_mobile/`,
|
|
693
730
|
".dart"
|
|
@@ -697,13 +734,14 @@ async function doUpdate(cwd, config, repoDir, version) {
|
|
|
697
734
|
version,
|
|
698
735
|
components: config.components,
|
|
699
736
|
createdAt: config.createdAt,
|
|
700
|
-
|
|
737
|
+
paths: componentPaths
|
|
701
738
|
};
|
|
702
739
|
await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
703
740
|
}
|
|
704
|
-
function detectProjectName(cwd, components) {
|
|
741
|
+
function detectProjectName(cwd, components, componentPaths) {
|
|
705
742
|
for (const component of components) {
|
|
706
|
-
const
|
|
743
|
+
const dir = componentPaths[component] ?? component;
|
|
744
|
+
const pkgPath = join4(cwd, dir, "package.json");
|
|
707
745
|
if (existsSync3(pkgPath)) {
|
|
708
746
|
try {
|
|
709
747
|
const pkg = JSON.parse(
|
|
@@ -760,45 +798,37 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
760
798
|
}
|
|
761
799
|
}
|
|
762
800
|
async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
763
|
-
const name = detectProjectName2(cwd, config.components);
|
|
764
|
-
const nameSnake = toSnake(name);
|
|
765
801
|
const allComponents = [...config.components, ...toAdd];
|
|
766
|
-
const
|
|
767
|
-
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 };
|
|
768
808
|
for (const component of toAdd) {
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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}/`);
|
|
774
814
|
}
|
|
775
815
|
await substituteNames2(cwd, toAdd, name, nameSnake);
|
|
776
|
-
const
|
|
777
|
-
|
|
816
|
+
const spinner5 = p4.spinner();
|
|
817
|
+
spinner5.start("Regenerating shared files");
|
|
778
818
|
const hasBackend = allComponents.includes("fastapi") || allComponents.includes("fastify");
|
|
779
819
|
if (hasBackend || allComponents.includes("frontend")) {
|
|
780
|
-
await writeFile4(
|
|
781
|
-
|
|
782
|
-
await generateDockerCompose(vars)
|
|
783
|
-
);
|
|
784
|
-
await writeFile4(
|
|
785
|
-
join5(cwd, "docker-compose.dev.yml"),
|
|
786
|
-
await generateDockerComposeDev(vars)
|
|
787
|
-
);
|
|
820
|
+
await writeFile4(join5(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
821
|
+
await writeFile4(join5(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
788
822
|
}
|
|
789
|
-
await writeFile4(join5(cwd, "Makefile"), await generateMakefile(vars));
|
|
790
823
|
await writeFile4(join5(cwd, "README.md"), await generateReadme(vars));
|
|
791
824
|
await mkdir4(join5(cwd, ".githooks"), { recursive: true });
|
|
792
825
|
await writeFile4(join5(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
793
826
|
await chmod3(join5(cwd, ".githooks/pre-commit"), 493);
|
|
794
827
|
await mkdir4(join5(cwd, ".github/workflows"), { recursive: true });
|
|
795
|
-
await writeFile4(
|
|
796
|
-
join5(cwd, ".github/workflows/ci.yml"),
|
|
797
|
-
await generateCiYml(vars)
|
|
798
|
-
);
|
|
828
|
+
await writeFile4(join5(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
799
829
|
await writeFile4(join5(cwd, "setup.sh"), await generateSetupSh(vars));
|
|
800
830
|
await chmod3(join5(cwd, "setup.sh"), 493);
|
|
801
|
-
|
|
831
|
+
spinner5.stop("Shared files regenerated.");
|
|
802
832
|
if (!skipInstall) {
|
|
803
833
|
await installDeps2(cwd, toAdd);
|
|
804
834
|
}
|
|
@@ -819,10 +849,12 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
|
819
849
|
version: pkg.version,
|
|
820
850
|
components: allComponents,
|
|
821
851
|
createdAt: config.createdAt,
|
|
822
|
-
|
|
852
|
+
paths
|
|
823
853
|
};
|
|
824
854
|
await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
825
|
-
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`);
|
|
826
858
|
}
|
|
827
859
|
async function substituteNames2(dest, components, name, nameSnake) {
|
|
828
860
|
if (components.includes("fastapi")) {
|
|
@@ -869,44 +901,44 @@ async function substituteNames2(dest, components, name, nameSnake) {
|
|
|
869
901
|
}
|
|
870
902
|
async function installDeps2(dest, components) {
|
|
871
903
|
for (const component of components) {
|
|
872
|
-
const
|
|
904
|
+
const spinner5 = p4.spinner();
|
|
873
905
|
try {
|
|
874
906
|
switch (component) {
|
|
875
907
|
case "fastapi":
|
|
876
908
|
if (hasCommand("uv")) {
|
|
877
|
-
|
|
909
|
+
spinner5.start("Installing FastAPI dependencies");
|
|
878
910
|
exec("uv sync --all-extras", join5(dest, "fastapi"));
|
|
879
|
-
|
|
911
|
+
spinner5.stop("FastAPI dependencies installed.");
|
|
880
912
|
} else {
|
|
881
913
|
p4.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
882
914
|
}
|
|
883
915
|
break;
|
|
884
916
|
case "fastify":
|
|
885
917
|
if (hasCommand("pnpm")) {
|
|
886
|
-
|
|
918
|
+
spinner5.start("Installing Fastify dependencies");
|
|
887
919
|
exec("pnpm install", join5(dest, "fastify"));
|
|
888
|
-
|
|
920
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
889
921
|
} else {
|
|
890
|
-
|
|
922
|
+
spinner5.start("Installing Fastify dependencies");
|
|
891
923
|
exec("npm install", join5(dest, "fastify"));
|
|
892
|
-
|
|
924
|
+
spinner5.stop("Fastify dependencies installed.");
|
|
893
925
|
}
|
|
894
926
|
break;
|
|
895
927
|
case "frontend":
|
|
896
|
-
|
|
928
|
+
spinner5.start("Installing Frontend dependencies");
|
|
897
929
|
exec("npm install", join5(dest, "frontend"));
|
|
898
|
-
|
|
930
|
+
spinner5.stop("Frontend dependencies installed.");
|
|
899
931
|
break;
|
|
900
932
|
case "e2e":
|
|
901
|
-
|
|
933
|
+
spinner5.start("Installing E2E dependencies");
|
|
902
934
|
exec("npm install", join5(dest, "e2e"));
|
|
903
|
-
|
|
935
|
+
spinner5.stop("E2E dependencies installed.");
|
|
904
936
|
break;
|
|
905
937
|
case "mobile":
|
|
906
938
|
if (hasCommand("flutter")) {
|
|
907
|
-
|
|
939
|
+
spinner5.start("Installing Flutter dependencies");
|
|
908
940
|
exec("flutter pub get", join5(dest, "mobile"));
|
|
909
|
-
|
|
941
|
+
spinner5.stop("Flutter dependencies installed.");
|
|
910
942
|
} else {
|
|
911
943
|
p4.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
912
944
|
}
|
|
@@ -915,13 +947,14 @@ async function installDeps2(dest, components) {
|
|
|
915
947
|
break;
|
|
916
948
|
}
|
|
917
949
|
} catch {
|
|
918
|
-
|
|
950
|
+
spinner5.stop(`Failed to install ${component} dependencies.`);
|
|
919
951
|
}
|
|
920
952
|
}
|
|
921
953
|
}
|
|
922
|
-
function detectProjectName2(cwd, components) {
|
|
954
|
+
function detectProjectName2(cwd, components, paths) {
|
|
923
955
|
for (const component of components) {
|
|
924
|
-
const
|
|
956
|
+
const dir = paths[component] ?? component;
|
|
957
|
+
const pkgPath = join5(cwd, dir, "package.json");
|
|
925
958
|
if (existsSync4(pkgPath)) {
|
|
926
959
|
try {
|
|
927
960
|
const pkg = JSON.parse(
|
|
@@ -938,6 +971,382 @@ function detectProjectName2(cwd, components) {
|
|
|
938
971
|
return toKebab(cwd.split("/").pop());
|
|
939
972
|
}
|
|
940
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
|
+
|
|
941
1350
|
// src/index.ts
|
|
942
1351
|
var args = process.argv.slice(2);
|
|
943
1352
|
function parseArgs() {
|
|
@@ -956,6 +1365,10 @@ function parseArgs() {
|
|
|
956
1365
|
command = "add";
|
|
957
1366
|
continue;
|
|
958
1367
|
}
|
|
1368
|
+
if (arg === "init" && !name) {
|
|
1369
|
+
command = "init";
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
959
1372
|
if (arg === "--components") {
|
|
960
1373
|
const val = args[++i];
|
|
961
1374
|
if (val) {
|
|
@@ -999,6 +1412,7 @@ function printHelp() {
|
|
|
999
1412
|
console.log(`
|
|
1000
1413
|
Usage:
|
|
1001
1414
|
projx <name> [options] Create a new project
|
|
1415
|
+
projx init Adopt existing project into projx
|
|
1002
1416
|
projx add <components...> Add components to existing project
|
|
1003
1417
|
projx update Update scaffolding to latest
|
|
1004
1418
|
|
|
@@ -1020,6 +1434,10 @@ function printHelp() {
|
|
|
1020
1434
|
}
|
|
1021
1435
|
async function main() {
|
|
1022
1436
|
const { command, name, options, localRepo, extraArgs } = parseArgs();
|
|
1437
|
+
if (command === "init") {
|
|
1438
|
+
await init(process.cwd(), localRepo);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1023
1441
|
if (command === "update") {
|
|
1024
1442
|
await update(process.cwd(), localRepo);
|
|
1025
1443
|
return;
|
|
@@ -1053,7 +1471,7 @@ async function main() {
|
|
|
1053
1471
|
opts.install = options.install ?? opts.install;
|
|
1054
1472
|
}
|
|
1055
1473
|
const dest = resolve2(process.cwd(), opts.name);
|
|
1056
|
-
if (
|
|
1474
|
+
if (existsSync7(dest)) {
|
|
1057
1475
|
console.error(`Error: ${dest} already exists.`);
|
|
1058
1476
|
process.exit(1);
|
|
1059
1477
|
}
|