create-projx 1.2.0 → 1.3.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 +26 -5
- package/dist/index.js +474 -668
- package/package.json +13 -4
- package/src/templates/README.md.ejs +1 -1
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 existsSync8 } from "fs";
|
|
5
5
|
import { resolve as resolve2 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/utils.ts
|
|
@@ -185,14 +185,16 @@ async function readFileOrNull(path) {
|
|
|
185
185
|
return null;
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
async function writeComponentMarker(dir, component) {
|
|
188
|
+
async function writeComponentMarker(dir, component, origin = "scaffold") {
|
|
189
189
|
const markerPath = join(dir, COMPONENT_MARKER);
|
|
190
190
|
let components = [component];
|
|
191
|
+
let existingOrigin = origin;
|
|
191
192
|
const existing = await readFileOrNull(markerPath);
|
|
192
193
|
if (existing) {
|
|
193
194
|
try {
|
|
194
195
|
const data = JSON.parse(existing);
|
|
195
196
|
const prev = data.components ?? (data.component ? [data.component] : []);
|
|
197
|
+
existingOrigin = data.origin ?? origin;
|
|
196
198
|
if (!prev.includes(component)) {
|
|
197
199
|
components = [...prev, component];
|
|
198
200
|
} else {
|
|
@@ -203,7 +205,7 @@ async function writeComponentMarker(dir, component) {
|
|
|
203
205
|
}
|
|
204
206
|
await writeFile(
|
|
205
207
|
markerPath,
|
|
206
|
-
JSON.stringify({ components }, null, 2) + "\n"
|
|
208
|
+
JSON.stringify({ components, origin: existingOrigin }, null, 2) + "\n"
|
|
207
209
|
);
|
|
208
210
|
}
|
|
209
211
|
async function discoverComponentPaths(cwd, components) {
|
|
@@ -265,8 +267,8 @@ function render(template, vars) {
|
|
|
265
267
|
(_, expr) => {
|
|
266
268
|
const parts = expr.split(".");
|
|
267
269
|
let val = vars;
|
|
268
|
-
for (const
|
|
269
|
-
val = val?.[
|
|
270
|
+
for (const p7 of parts) {
|
|
271
|
+
val = val?.[p7];
|
|
270
272
|
}
|
|
271
273
|
return String(val ?? "");
|
|
272
274
|
}
|
|
@@ -317,9 +319,17 @@ async function runPrompts(nameArg) {
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
// src/scaffold.ts
|
|
320
|
-
import { copyFileSync, existsSync as
|
|
321
|
-
import {
|
|
322
|
+
import { copyFileSync, existsSync as existsSync3 } from "fs";
|
|
323
|
+
import { mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
|
|
324
|
+
import { join as join4 } from "path";
|
|
325
|
+
import * as p3 from "@clack/prompts";
|
|
326
|
+
|
|
327
|
+
// src/baseline.ts
|
|
328
|
+
import { existsSync as existsSync2 } from "fs";
|
|
329
|
+
import { chmod, mkdir as mkdir2, writeFile as writeFile2, rm as rm2 } from "fs/promises";
|
|
330
|
+
import { execSync as execSync2 } from "child_process";
|
|
322
331
|
import { join as join3 } from "path";
|
|
332
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
323
333
|
import * as p2 from "@clack/prompts";
|
|
324
334
|
|
|
325
335
|
// src/generators/index.ts
|
|
@@ -381,42 +391,70 @@ function generateVscodeSettings(vars) {
|
|
|
381
391
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
382
392
|
}
|
|
383
393
|
|
|
384
|
-
// src/
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const nameSnake = toSnake(opts.name);
|
|
388
|
-
const paths = Object.fromEntries(
|
|
389
|
-
opts.components.map((c) => [c, c])
|
|
390
|
-
);
|
|
391
|
-
const vars = { projectName: name, components: opts.components, paths };
|
|
392
|
-
const isLocal = !!localRepo;
|
|
393
|
-
await mkdir2(dest, { recursive: true });
|
|
394
|
-
const dlSpinner = p2.spinner();
|
|
395
|
-
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
396
|
-
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
397
|
-
dlSpinner.stop("Failed.");
|
|
398
|
-
p2.log.error(String(err));
|
|
399
|
-
process.exit(1);
|
|
400
|
-
});
|
|
401
|
-
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
394
|
+
// src/baseline.ts
|
|
395
|
+
var BASELINE_BRANCH = "projx/baseline";
|
|
396
|
+
function hasBaseline(cwd) {
|
|
402
397
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
398
|
+
execSync2(`git show-ref --verify --quiet refs/heads/${BASELINE_BRANCH}`, {
|
|
399
|
+
cwd,
|
|
400
|
+
stdio: "pipe"
|
|
401
|
+
});
|
|
402
|
+
return true;
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
406
405
|
}
|
|
407
406
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
407
|
+
function createWorktree(cwd, branch, orphan) {
|
|
408
|
+
const worktree = join3(tmpdir2(), `projx-baseline-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
409
|
+
if (orphan) {
|
|
410
|
+
execSync2(`git worktree add --orphan -b ${branch} "${worktree}"`, {
|
|
411
|
+
cwd,
|
|
412
|
+
stdio: "pipe"
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
execSync2(`git worktree add "${worktree}" ${branch}`, {
|
|
416
|
+
cwd,
|
|
417
|
+
stdio: "pipe"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return worktree;
|
|
421
|
+
}
|
|
422
|
+
function removeWorktree(cwd, worktree) {
|
|
423
|
+
try {
|
|
424
|
+
execSync2(`git worktree remove "${worktree}" --force`, {
|
|
425
|
+
cwd,
|
|
426
|
+
stdio: "pipe"
|
|
427
|
+
});
|
|
428
|
+
} catch {
|
|
429
|
+
try {
|
|
430
|
+
rm2(worktree, { recursive: true, force: true });
|
|
431
|
+
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, origin) {
|
|
437
|
+
const name = vars.projectName;
|
|
438
|
+
const nameSnake = toSnake(name);
|
|
439
|
+
for (const component of components) {
|
|
440
|
+
const targetDir = componentPaths[component];
|
|
441
|
+
if (targetDir === component) {
|
|
442
|
+
await copyComponent(repoDir, component, dest);
|
|
443
|
+
} else {
|
|
444
|
+
await copyComponent(repoDir, component, join3(dest, "__tmp__"));
|
|
445
|
+
const { cp: cp2 } = await import("fs/promises");
|
|
446
|
+
const srcDir = join3(dest, "__tmp__", component);
|
|
447
|
+
const outDir = join3(dest, targetDir);
|
|
448
|
+
if (existsSync2(srcDir)) {
|
|
449
|
+
await cp2(srcDir, outDir, { recursive: true, force: true });
|
|
450
|
+
}
|
|
451
|
+
await rm2(join3(dest, "__tmp__"), { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
await writeComponentMarker(join3(dest, targetDir), component, origin);
|
|
454
|
+
}
|
|
455
|
+
await substituteNames(dest, components, componentPaths, name, nameSnake);
|
|
456
|
+
const hasBackend = components.includes("fastapi") || components.includes("fastify");
|
|
457
|
+
if (hasBackend || components.includes("frontend")) {
|
|
420
458
|
await writeFile2(join3(dest, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
421
459
|
await writeFile2(join3(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
422
460
|
}
|
|
@@ -431,130 +469,225 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
431
469
|
await copyStaticFiles(repoDir, dest);
|
|
432
470
|
await mkdir2(join3(dest, ".vscode"), { recursive: true });
|
|
433
471
|
await writeFile2(join3(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
|
|
434
|
-
const pkg = JSON.parse(
|
|
435
|
-
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
436
|
-
);
|
|
437
472
|
const projxConfig = {
|
|
438
|
-
version
|
|
439
|
-
components
|
|
440
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
exec("git init", dest);
|
|
446
|
-
exec("git config core.hooksPath .githooks", dest);
|
|
447
|
-
p2.log.success("Git initialized with hooks.");
|
|
448
|
-
} catch {
|
|
449
|
-
p2.log.warn("Failed to initialize git.");
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (opts.install) {
|
|
453
|
-
await installDeps(dest, opts.components);
|
|
454
|
-
}
|
|
455
|
-
copyEnvExamples(dest, opts.components);
|
|
456
|
-
if (opts.git) {
|
|
457
|
-
try {
|
|
458
|
-
exec("git add -A", dest);
|
|
459
|
-
exec('git commit -m "Initial scaffold from projx"', dest);
|
|
460
|
-
p2.log.success("Initial commit created.");
|
|
461
|
-
} catch {
|
|
473
|
+
version,
|
|
474
|
+
components,
|
|
475
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
476
|
+
baseline: {
|
|
477
|
+
branch: BASELINE_BRANCH,
|
|
478
|
+
templateVersion: version
|
|
462
479
|
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
cd ${name}
|
|
467
|
-
./setup.sh
|
|
468
|
-
|
|
469
|
-
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
480
|
+
};
|
|
481
|
+
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
|
|
470
482
|
}
|
|
471
|
-
async function substituteNames(dest, components, name, nameSnake) {
|
|
483
|
+
async function substituteNames(dest, components, paths, name, nameSnake) {
|
|
472
484
|
if (components.includes("fastapi")) {
|
|
473
|
-
await replaceInFile(
|
|
474
|
-
join3(dest, "fastapi/pyproject.toml"),
|
|
475
|
-
"projx-fastapi",
|
|
476
|
-
`${name}-fastapi`
|
|
477
|
-
);
|
|
485
|
+
await replaceInFile(join3(dest, `${paths.fastapi}/pyproject.toml`), "projx-fastapi", `${name}-fastapi`);
|
|
478
486
|
}
|
|
479
487
|
if (components.includes("fastify")) {
|
|
480
|
-
await replaceInFile(
|
|
481
|
-
join3(dest, "fastify/package.json"),
|
|
482
|
-
"projx-fastify",
|
|
483
|
-
`${name}-fastify`
|
|
484
|
-
);
|
|
488
|
+
await replaceInFile(join3(dest, `${paths.fastify}/package.json`), "projx-fastify", `${name}-fastify`);
|
|
485
489
|
}
|
|
486
490
|
if (components.includes("frontend")) {
|
|
487
|
-
await replaceInFile(
|
|
488
|
-
join3(dest, "frontend/package.json"),
|
|
489
|
-
"projx-frontend",
|
|
490
|
-
`${name}-frontend`
|
|
491
|
-
);
|
|
491
|
+
await replaceInFile(join3(dest, `${paths.frontend}/package.json`), "projx-frontend", `${name}-frontend`);
|
|
492
492
|
}
|
|
493
493
|
if (components.includes("e2e")) {
|
|
494
|
-
await replaceInFile(
|
|
495
|
-
join3(dest, "e2e/package.json"),
|
|
496
|
-
"projx-e2e",
|
|
497
|
-
`${name}-e2e`
|
|
498
|
-
);
|
|
494
|
+
await replaceInFile(join3(dest, `${paths.e2e}/package.json`), "projx-e2e", `${name}-e2e`);
|
|
499
495
|
}
|
|
500
496
|
if (components.includes("mobile")) {
|
|
501
|
-
await replaceInFile(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
497
|
+
await replaceInFile(join3(dest, `${paths.mobile}/pubspec.yaml`), "projx_mobile", `${nameSnake}_mobile`);
|
|
498
|
+
await replaceInDir(join3(dest, `${paths.mobile}`), "package:projx_mobile/", `package:${nameSnake}_mobile/`, ".dart");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function createBaseline(cwd, repoDir, components, componentPaths, vars, version, origin = "scaffold") {
|
|
502
|
+
const worktree = createWorktree(cwd, BASELINE_BRANCH, true);
|
|
503
|
+
try {
|
|
504
|
+
await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, origin);
|
|
505
|
+
execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
|
|
506
|
+
execSync2(
|
|
507
|
+
`git commit --no-verify -m "projx: baseline template v${version} [${components.join(", ")}]"`,
|
|
508
|
+
{ cwd: worktree, stdio: "pipe" }
|
|
509
|
+
);
|
|
510
|
+
} finally {
|
|
511
|
+
removeWorktree(cwd, worktree);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function updateBaseline(cwd, repoDir, components, componentPaths, vars, version) {
|
|
515
|
+
const worktree = createWorktree(cwd, BASELINE_BRANCH, false);
|
|
516
|
+
try {
|
|
517
|
+
execSync2("git rm -rf .", { cwd: worktree, stdio: "pipe" });
|
|
518
|
+
await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, "scaffold");
|
|
519
|
+
execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
|
|
520
|
+
const diff = execSync2("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
|
|
521
|
+
if (!diff) {
|
|
522
|
+
return { changed: false };
|
|
523
|
+
}
|
|
524
|
+
execSync2(
|
|
525
|
+
`git commit --no-verify -m "projx: update baseline to template v${version}"`,
|
|
526
|
+
{ cwd: worktree, stdio: "pipe" }
|
|
505
527
|
);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
528
|
+
return { changed: true };
|
|
529
|
+
} finally {
|
|
530
|
+
removeWorktree(cwd, worktree);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function addToBaseline(cwd, repoDir, newComponents, allComponents, componentPaths, vars, version) {
|
|
534
|
+
const worktree = createWorktree(cwd, BASELINE_BRANCH, false);
|
|
535
|
+
try {
|
|
536
|
+
await writeTemplateToDir(worktree, repoDir, allComponents, componentPaths, vars, version, "scaffold");
|
|
537
|
+
execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
|
|
538
|
+
execSync2(
|
|
539
|
+
`git commit --no-verify -m "projx: add ${newComponents.join(", ")} template v${version}"`,
|
|
540
|
+
{ cwd: worktree, stdio: "pipe" }
|
|
511
541
|
);
|
|
542
|
+
} finally {
|
|
543
|
+
removeWorktree(cwd, worktree);
|
|
512
544
|
}
|
|
513
545
|
}
|
|
546
|
+
function mergeBaseline(cwd, message, allowUnrelated = false, oursOnConflict = false) {
|
|
547
|
+
const args2 = [`git merge ${BASELINE_BRANCH}`];
|
|
548
|
+
args2.push(`-m "${message}"`);
|
|
549
|
+
if (allowUnrelated) args2.push("--allow-unrelated-histories");
|
|
550
|
+
if (oursOnConflict) {
|
|
551
|
+
try {
|
|
552
|
+
execSync2(`${args2.join(" ")} --no-commit`, { cwd, stdio: "pipe" });
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
execSync2("git checkout --ours .", { cwd, stdio: "pipe" });
|
|
556
|
+
execSync2("git add -A", { cwd, stdio: "pipe" });
|
|
557
|
+
execSync2(`git commit --no-verify --no-edit -m "${message}"`, { cwd, stdio: "pipe" });
|
|
558
|
+
return { status: "clean" };
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
execSync2(args2.join(" "), { cwd, stdio: "pipe" });
|
|
562
|
+
return { status: "clean" };
|
|
563
|
+
} catch {
|
|
564
|
+
const conflicted = execSync2("git diff --name-only --diff-filter=U", { cwd, stdio: "pipe" }).toString().trim();
|
|
565
|
+
if (!conflicted) {
|
|
566
|
+
return { status: "clean" };
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
status: "conflicts",
|
|
570
|
+
conflictedFiles: conflicted.split("\n").filter(Boolean)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function reconstructBaseline(cwd, repoDir, components, componentPaths, vars, version) {
|
|
575
|
+
p2.log.warn("projx/baseline branch not found. Reconstructing...");
|
|
576
|
+
await createBaseline(cwd, repoDir, components, componentPaths, vars, version);
|
|
577
|
+
mergeBaseline(
|
|
578
|
+
cwd,
|
|
579
|
+
`projx: reconstructed baseline for template v${version}`,
|
|
580
|
+
true,
|
|
581
|
+
true
|
|
582
|
+
);
|
|
583
|
+
p2.log.success("Baseline reconstructed.");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/scaffold.ts
|
|
587
|
+
async function scaffold(opts, dest, localRepo) {
|
|
588
|
+
const name = toKebab(opts.name);
|
|
589
|
+
const paths = Object.fromEntries(
|
|
590
|
+
opts.components.map((c) => [c, c])
|
|
591
|
+
);
|
|
592
|
+
const vars = { projectName: name, components: opts.components, paths };
|
|
593
|
+
const isLocal = !!localRepo;
|
|
594
|
+
await mkdir3(dest, { recursive: true });
|
|
595
|
+
const dlSpinner = p3.spinner();
|
|
596
|
+
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
597
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
598
|
+
dlSpinner.stop("Failed.");
|
|
599
|
+
p3.log.error(String(err));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
});
|
|
602
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
603
|
+
try {
|
|
604
|
+
const pkg = JSON.parse(await readFile3(join4(repoDir, "cli/package.json"), "utf-8"));
|
|
605
|
+
const version = pkg.version;
|
|
606
|
+
p3.log.info(`Scaffolding project in ${dest}`);
|
|
607
|
+
if (opts.git) {
|
|
608
|
+
exec("git init", dest);
|
|
609
|
+
exec("git config core.hooksPath .githooks", dest);
|
|
610
|
+
const spinner5 = p3.spinner();
|
|
611
|
+
spinner5.start("Creating baseline and scaffold");
|
|
612
|
+
await createBaseline(dest, repoDir, opts.components, paths, vars, version);
|
|
613
|
+
const result = mergeBaseline(
|
|
614
|
+
dest,
|
|
615
|
+
`projx: initial scaffold from template v${version}`,
|
|
616
|
+
true
|
|
617
|
+
);
|
|
618
|
+
spinner5.stop("Scaffold complete.");
|
|
619
|
+
if (result.status === "conflicts") {
|
|
620
|
+
p3.log.warn("Unexpected conflicts during scaffold \u2014 this shouldn't happen.");
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
const spinner5 = p3.spinner();
|
|
624
|
+
spinner5.start("Copying template files");
|
|
625
|
+
await createBaseline(dest, repoDir, opts.components, paths, vars, version);
|
|
626
|
+
spinner5.stop("Template files copied.");
|
|
627
|
+
}
|
|
628
|
+
if (opts.install) {
|
|
629
|
+
await installDeps(dest, opts.components);
|
|
630
|
+
}
|
|
631
|
+
copyEnvExamples(dest, opts.components);
|
|
632
|
+
if (opts.git) {
|
|
633
|
+
try {
|
|
634
|
+
exec("git add -A", dest);
|
|
635
|
+
exec('git commit --no-verify -m "Initial scaffold from projx"', dest);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} finally {
|
|
640
|
+
await cleanupRepo(repoDir, isLocal);
|
|
641
|
+
}
|
|
642
|
+
p3.outro(`Done! Next steps:
|
|
643
|
+
|
|
644
|
+
cd ${name}
|
|
645
|
+
./setup.sh
|
|
646
|
+
|
|
647
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
648
|
+
}
|
|
514
649
|
async function installDeps(dest, components) {
|
|
515
650
|
for (const component of components) {
|
|
516
|
-
const spinner5 =
|
|
651
|
+
const spinner5 = p3.spinner();
|
|
517
652
|
try {
|
|
518
653
|
switch (component) {
|
|
519
654
|
case "fastapi":
|
|
520
655
|
if (hasCommand("uv")) {
|
|
521
656
|
spinner5.start("Installing FastAPI dependencies (uv sync)");
|
|
522
|
-
exec("uv sync --all-extras",
|
|
657
|
+
exec("uv sync --all-extras", join4(dest, "fastapi"));
|
|
523
658
|
spinner5.stop("FastAPI dependencies installed.");
|
|
524
659
|
} else {
|
|
525
|
-
|
|
660
|
+
p3.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
526
661
|
}
|
|
527
662
|
break;
|
|
528
663
|
case "fastify":
|
|
529
664
|
if (hasCommand("pnpm")) {
|
|
530
665
|
spinner5.start("Installing Fastify dependencies (pnpm install)");
|
|
531
|
-
exec("pnpm install",
|
|
666
|
+
exec("pnpm install", join4(dest, "fastify"));
|
|
532
667
|
spinner5.stop("Fastify dependencies installed.");
|
|
533
668
|
} else {
|
|
534
669
|
spinner5.start("Installing Fastify dependencies (npm install)");
|
|
535
|
-
exec("npm install",
|
|
670
|
+
exec("npm install", join4(dest, "fastify"));
|
|
536
671
|
spinner5.stop("Fastify dependencies installed.");
|
|
537
672
|
}
|
|
538
673
|
break;
|
|
539
674
|
case "frontend":
|
|
540
675
|
spinner5.start("Installing Frontend dependencies (npm install)");
|
|
541
|
-
exec("npm install",
|
|
676
|
+
exec("npm install", join4(dest, "frontend"));
|
|
542
677
|
spinner5.stop("Frontend dependencies installed.");
|
|
543
678
|
break;
|
|
544
679
|
case "e2e":
|
|
545
680
|
spinner5.start("Installing E2E dependencies (npm install)");
|
|
546
|
-
exec("npm install",
|
|
681
|
+
exec("npm install", join4(dest, "e2e"));
|
|
547
682
|
spinner5.stop("E2E dependencies installed.");
|
|
548
683
|
break;
|
|
549
684
|
case "mobile":
|
|
550
685
|
if (hasCommand("flutter")) {
|
|
551
686
|
spinner5.start("Installing Flutter dependencies");
|
|
552
|
-
exec("flutter pub get",
|
|
687
|
+
exec("flutter pub get", join4(dest, "mobile"));
|
|
553
688
|
spinner5.stop("Flutter dependencies installed.");
|
|
554
689
|
} else {
|
|
555
|
-
|
|
556
|
-
"Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
|
|
557
|
-
);
|
|
690
|
+
p3.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
558
691
|
}
|
|
559
692
|
break;
|
|
560
693
|
case "infra":
|
|
@@ -567,9 +700,9 @@ async function installDeps(dest, components) {
|
|
|
567
700
|
}
|
|
568
701
|
function copyEnvExamples(dest, components) {
|
|
569
702
|
for (const component of components) {
|
|
570
|
-
const example =
|
|
571
|
-
const env =
|
|
572
|
-
if (
|
|
703
|
+
const example = join4(dest, component, ".env.example");
|
|
704
|
+
const env = join4(dest, component, ".env");
|
|
705
|
+
if (existsSync3(example) && !existsSync3(env)) {
|
|
573
706
|
try {
|
|
574
707
|
copyFileSync(example, env);
|
|
575
708
|
} catch {
|
|
@@ -579,62 +712,32 @@ function copyEnvExamples(dest, components) {
|
|
|
579
712
|
}
|
|
580
713
|
|
|
581
714
|
// src/update.ts
|
|
582
|
-
import { existsSync as
|
|
583
|
-
import { readFile as readFile4
|
|
584
|
-
import { execSync as
|
|
585
|
-
import { join as
|
|
586
|
-
import * as
|
|
587
|
-
var NEVER_OVERWRITE = [
|
|
588
|
-
/\.env$/,
|
|
589
|
-
/\.env\.(dev|staging|prod)$/,
|
|
590
|
-
/prisma\/migrations\//,
|
|
591
|
-
/src\/migrations\/versions\//,
|
|
592
|
-
/\.projx-component$/
|
|
593
|
-
];
|
|
594
|
-
function isGitRepo(cwd) {
|
|
595
|
-
try {
|
|
596
|
-
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
597
|
-
return true;
|
|
598
|
-
} catch {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function hasUncommittedChanges(cwd) {
|
|
603
|
-
try {
|
|
604
|
-
const status = execSync2("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
605
|
-
return status.length > 0;
|
|
606
|
-
} catch {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
function branchExists(cwd, branch) {
|
|
611
|
-
try {
|
|
612
|
-
execSync2(`git show-ref --verify --quiet refs/heads/${branch}`, { cwd, stdio: "pipe" });
|
|
613
|
-
return true;
|
|
614
|
-
} catch {
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
function getCurrentBranch(cwd) {
|
|
619
|
-
return execSync2("git branch --show-current", { cwd, stdio: "pipe" }).toString().trim();
|
|
620
|
-
}
|
|
715
|
+
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
716
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
717
|
+
import { execSync as execSync3 } from "child_process";
|
|
718
|
+
import { join as join5 } from "path";
|
|
719
|
+
import * as p4 from "@clack/prompts";
|
|
621
720
|
async function update(cwd, localRepo) {
|
|
622
|
-
|
|
721
|
+
p4.intro("projx update");
|
|
623
722
|
const isLocal = !!localRepo;
|
|
624
|
-
|
|
723
|
+
if (!isGitRepo(cwd)) {
|
|
724
|
+
p4.log.error(`projx update requires a git repo. Run 'git init && git add -A && git commit -m "initial"' first.`);
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
if (hasUncommittedChanges(cwd)) {
|
|
728
|
+
p4.log.error("You have uncommitted changes. Commit or stash them first.");
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const configPath = join5(cwd, ".projx");
|
|
625
732
|
let config;
|
|
626
|
-
if (
|
|
733
|
+
if (existsSync4(configPath)) {
|
|
627
734
|
config = JSON.parse(await readFile4(configPath, "utf-8"));
|
|
628
|
-
|
|
629
|
-
`Found .projx (v${config.version}, components: ${config.components.join(", ")})`
|
|
630
|
-
);
|
|
735
|
+
p4.log.info(`Found .projx (v${config.version}, components: ${config.components.join(", ")})`);
|
|
631
736
|
} else {
|
|
632
|
-
|
|
633
|
-
const detected = COMPONENTS.filter(
|
|
634
|
-
(c) => existsSync3(join4(cwd, c))
|
|
635
|
-
);
|
|
737
|
+
p4.log.warn("No .projx file found. Detecting components from directories.");
|
|
738
|
+
const detected = COMPONENTS.filter((c) => existsSync4(join5(cwd, c)));
|
|
636
739
|
if (detected.length === 0) {
|
|
637
|
-
|
|
740
|
+
p4.log.error("No projx components found. Run 'projx init' first.");
|
|
638
741
|
process.exit(1);
|
|
639
742
|
}
|
|
640
743
|
config = {
|
|
@@ -642,171 +745,93 @@ async function update(cwd, localRepo) {
|
|
|
642
745
|
components: detected,
|
|
643
746
|
createdAt: "unknown"
|
|
644
747
|
};
|
|
645
|
-
|
|
748
|
+
p4.log.info(`Detected: ${detected.join(", ")}`);
|
|
646
749
|
}
|
|
647
750
|
const componentPaths = await discoverComponentPaths(cwd, config.components);
|
|
648
751
|
const remapped = config.components.filter((c) => componentPaths[c] !== c);
|
|
649
752
|
if (remapped.length > 0) {
|
|
650
753
|
for (const c of remapped) {
|
|
651
|
-
|
|
754
|
+
p4.log.info(`${c} \u2192 ${componentPaths[c]}/`);
|
|
652
755
|
}
|
|
653
756
|
}
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
await readFile4(join4(repoDir, "cli/package.json"), "utf-8")
|
|
673
|
-
);
|
|
674
|
-
branchName = `projx/update-v${pkg.version}`;
|
|
675
|
-
if (branchExists(cwd, branchName)) {
|
|
676
|
-
let suffix = 1;
|
|
677
|
-
while (branchExists(cwd, `${branchName}-${suffix}`)) suffix++;
|
|
678
|
-
branchName = `${branchName}-${suffix}`;
|
|
757
|
+
const dlSpinner = p4.spinner();
|
|
758
|
+
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
759
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
760
|
+
dlSpinner.stop("Failed.");
|
|
761
|
+
p4.log.error(String(err));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
});
|
|
764
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
765
|
+
try {
|
|
766
|
+
const pkg = JSON.parse(await readFile4(join5(repoDir, "cli/package.json"), "utf-8"));
|
|
767
|
+
const version = pkg.version;
|
|
768
|
+
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
769
|
+
const vars = { projectName: name, components: config.components, paths: componentPaths };
|
|
770
|
+
if (!hasBaseline(cwd)) {
|
|
771
|
+
const rebuildSpinner = p4.spinner();
|
|
772
|
+
rebuildSpinner.start("Establishing baseline (first-time migration)");
|
|
773
|
+
await reconstructBaseline(cwd, repoDir, config.components, componentPaths, vars, config.version || version);
|
|
774
|
+
rebuildSpinner.stop("Baseline established.");
|
|
679
775
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
776
|
+
const updateSpinner = p4.spinner();
|
|
777
|
+
updateSpinner.start("Updating baseline to latest template");
|
|
778
|
+
const { changed } = await updateBaseline(cwd, repoDir, config.components, componentPaths, vars, version);
|
|
779
|
+
if (!changed) {
|
|
780
|
+
updateSpinner.stop("Already up to date.");
|
|
781
|
+
p4.outro("No template changes to apply.");
|
|
782
|
+
return;
|
|
687
783
|
}
|
|
688
|
-
|
|
689
|
-
|
|
784
|
+
updateSpinner.stop("Baseline updated.");
|
|
785
|
+
const mergeSpinner = p4.spinner();
|
|
786
|
+
mergeSpinner.start("Merging template changes");
|
|
787
|
+
const result = mergeBaseline(cwd, `projx: update to template v${version}`);
|
|
788
|
+
mergeSpinner.stop("Merge complete.");
|
|
789
|
+
if (result.status === "conflicts") {
|
|
790
|
+
p4.log.warn(`Merge conflicts in ${result.conflictedFiles.length} file(s):`);
|
|
791
|
+
for (const f of result.conflictedFiles) {
|
|
792
|
+
p4.log.message(` ${f}`);
|
|
793
|
+
}
|
|
794
|
+
p4.outro(
|
|
795
|
+
"Resolve conflicts, then:\n git add . && git commit\n\nOr abort:\n git merge --abort"
|
|
796
|
+
);
|
|
797
|
+
} else {
|
|
798
|
+
p4.outro(`Updated to template v${version}. All changes merged cleanly.`);
|
|
690
799
|
}
|
|
691
|
-
|
|
692
|
-
p3.outro(
|
|
693
|
-
`Updated on branch: ${branchName}
|
|
694
|
-
|
|
695
|
-
Review changes:
|
|
696
|
-
git diff ${originalBranch}...${branchName}
|
|
697
|
-
|
|
698
|
-
Merge (resolve conflicts for files you customized):
|
|
699
|
-
git checkout ${originalBranch} && git merge --no-ff ${branchName}`
|
|
700
|
-
);
|
|
701
|
-
} else {
|
|
702
|
-
const dlSpinner = p3.spinner();
|
|
703
|
-
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
704
|
-
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
705
|
-
dlSpinner.stop("Failed.");
|
|
706
|
-
p3.log.error(String(err));
|
|
707
|
-
process.exit(1);
|
|
708
|
-
});
|
|
709
|
-
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
710
|
-
const pkg = JSON.parse(
|
|
711
|
-
await readFile4(join4(repoDir, "cli/package.json"), "utf-8")
|
|
712
|
-
);
|
|
800
|
+
} catch (err) {
|
|
713
801
|
try {
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
await cleanupRepo(repoDir, isLocal);
|
|
802
|
+
execSync3("git merge --abort", { cwd, stdio: "pipe" });
|
|
803
|
+
} catch {
|
|
717
804
|
}
|
|
718
|
-
|
|
805
|
+
p4.log.error(`Update failed: ${err}`);
|
|
806
|
+
p4.log.info("Your code is safe. Run 'git merge --abort' if needed.");
|
|
807
|
+
process.exit(1);
|
|
808
|
+
} finally {
|
|
809
|
+
await cleanupRepo(repoDir, isLocal);
|
|
719
810
|
}
|
|
720
811
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
spinner6.start(`Updating ${targetDir}/ (${component})`);
|
|
736
|
-
const componentSrc = join4(repoDir, component);
|
|
737
|
-
if (!existsSync3(componentSrc)) {
|
|
738
|
-
spinner6.stop(`${component} template not found, skipping.`);
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
const tmpDest = join4(cwd, `.projx-tmp`);
|
|
742
|
-
const files = await copyComponent(repoDir, component, tmpDest);
|
|
743
|
-
for (const file of files) {
|
|
744
|
-
const src = join4(tmpDest, component, file);
|
|
745
|
-
const destRel = `${targetDir}/${file}`;
|
|
746
|
-
const dest = join4(cwd, destRel);
|
|
747
|
-
if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
|
|
748
|
-
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
749
|
-
await mkdir3(dir, { recursive: true });
|
|
750
|
-
await cp2(src, dest, { force: true });
|
|
751
|
-
touchedFiles.push(destRel);
|
|
752
|
-
}
|
|
753
|
-
await rm2(tmpDest, { recursive: true, force: true });
|
|
754
|
-
if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
|
|
755
|
-
await writeComponentMarker(join4(cwd, targetDir), component);
|
|
756
|
-
touchedFiles.push(`${targetDir}/.projx-component`);
|
|
757
|
-
}
|
|
758
|
-
spinner6.stop(`${targetDir}/ updated.`);
|
|
759
|
-
}
|
|
760
|
-
const spinner5 = p3.spinner();
|
|
761
|
-
spinner5.start("Updating shared files");
|
|
762
|
-
const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
|
|
763
|
-
if (hasBackend || config.components.includes("frontend")) {
|
|
764
|
-
await writeFile3(join4(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
765
|
-
touchedFiles.push("docker-compose.yml");
|
|
766
|
-
await writeFile3(join4(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
767
|
-
touchedFiles.push("docker-compose.dev.yml");
|
|
768
|
-
}
|
|
769
|
-
await mkdir3(join4(cwd, ".githooks"), { recursive: true });
|
|
770
|
-
await writeFile3(join4(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
771
|
-
await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
|
|
772
|
-
touchedFiles.push(".githooks/pre-commit");
|
|
773
|
-
await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
|
|
774
|
-
await writeFile3(join4(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
775
|
-
touchedFiles.push(".github/workflows/ci.yml");
|
|
776
|
-
await writeFile3(join4(cwd, "setup.sh"), await generateSetupSh(vars));
|
|
777
|
-
await chmod2(join4(cwd, "setup.sh"), 493);
|
|
778
|
-
touchedFiles.push("setup.sh");
|
|
779
|
-
await mkdir3(join4(cwd, ".vscode"), { recursive: true });
|
|
780
|
-
await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
|
|
781
|
-
touchedFiles.push(".vscode/settings.json");
|
|
782
|
-
spinner5.stop("Shared files updated.");
|
|
783
|
-
if (config.components.includes("mobile")) {
|
|
784
|
-
const mobilePath = componentPaths.mobile ?? "mobile";
|
|
785
|
-
await replaceInDir(
|
|
786
|
-
join4(cwd, mobilePath),
|
|
787
|
-
"package:projx_mobile/",
|
|
788
|
-
`package:${nameSnake}_mobile/`,
|
|
789
|
-
".dart"
|
|
790
|
-
);
|
|
812
|
+
function isGitRepo(cwd) {
|
|
813
|
+
try {
|
|
814
|
+
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
815
|
+
return true;
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function hasUncommittedChanges(cwd) {
|
|
821
|
+
try {
|
|
822
|
+
const status = execSync3("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
823
|
+
return status.length > 0;
|
|
824
|
+
} catch {
|
|
825
|
+
return false;
|
|
791
826
|
}
|
|
792
|
-
const updatedConfig = {
|
|
793
|
-
version,
|
|
794
|
-
components: config.components,
|
|
795
|
-
createdAt: config.createdAt
|
|
796
|
-
};
|
|
797
|
-
await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
798
|
-
touchedFiles.push(".projx");
|
|
799
|
-
return touchedFiles;
|
|
800
827
|
}
|
|
801
828
|
function detectProjectName(cwd, components, componentPaths) {
|
|
802
829
|
for (const component of components) {
|
|
803
830
|
const dir = componentPaths[component] ?? component;
|
|
804
|
-
const pkgPath =
|
|
805
|
-
if (
|
|
831
|
+
const pkgPath = join5(cwd, dir, "package.json");
|
|
832
|
+
if (existsSync4(pkgPath)) {
|
|
806
833
|
try {
|
|
807
|
-
const pkg = JSON.parse(
|
|
808
|
-
readFileSync(pkgPath, "utf-8")
|
|
809
|
-
);
|
|
834
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
810
835
|
const n = pkg.name;
|
|
811
836
|
if (n && n.includes("-")) {
|
|
812
837
|
return n.substring(0, n.lastIndexOf("-"));
|
|
@@ -819,187 +844,134 @@ function detectProjectName(cwd, components, componentPaths) {
|
|
|
819
844
|
}
|
|
820
845
|
|
|
821
846
|
// src/add.ts
|
|
822
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
823
|
-
import {
|
|
824
|
-
import { join as
|
|
825
|
-
import * as
|
|
847
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
848
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
849
|
+
import { join as join6 } from "path";
|
|
850
|
+
import * as p5 from "@clack/prompts";
|
|
826
851
|
async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
827
|
-
|
|
852
|
+
p5.intro("projx add");
|
|
828
853
|
const isLocal = !!localRepo;
|
|
829
|
-
const configPath =
|
|
830
|
-
if (!
|
|
831
|
-
|
|
854
|
+
const configPath = join6(cwd, ".projx");
|
|
855
|
+
if (!existsSync5(configPath)) {
|
|
856
|
+
p5.log.error("No .projx file found. Run 'projx <name>' to create a project first.");
|
|
832
857
|
process.exit(1);
|
|
833
858
|
}
|
|
834
859
|
const config = JSON.parse(await readFile5(configPath, "utf-8"));
|
|
835
860
|
const existing = config.components;
|
|
836
861
|
const alreadyExists = newComponents.filter((c) => existing.includes(c));
|
|
837
862
|
if (alreadyExists.length > 0) {
|
|
838
|
-
|
|
863
|
+
p5.log.warn(`Already present: ${alreadyExists.join(", ")}. Skipping those.`);
|
|
839
864
|
}
|
|
840
865
|
const toAdd = newComponents.filter((c) => !existing.includes(c));
|
|
841
866
|
if (toAdd.length === 0) {
|
|
842
|
-
|
|
867
|
+
p5.log.info("Nothing new to add.");
|
|
843
868
|
process.exit(0);
|
|
844
869
|
}
|
|
845
|
-
|
|
846
|
-
const dlSpinner =
|
|
870
|
+
p5.log.info(`Adding: ${toAdd.join(", ")}`);
|
|
871
|
+
const dlSpinner = p5.spinner();
|
|
847
872
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
848
873
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
849
874
|
dlSpinner.stop("Failed.");
|
|
850
|
-
|
|
875
|
+
p5.log.error(String(err));
|
|
851
876
|
process.exit(1);
|
|
852
877
|
});
|
|
853
878
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
854
879
|
try {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
try {
|
|
900
|
-
copyFileSync2(example, env);
|
|
901
|
-
} catch {
|
|
880
|
+
const allComponents = [...existing, ...toAdd];
|
|
881
|
+
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
882
|
+
const paths = { ...existingPaths };
|
|
883
|
+
for (const c of toAdd) paths[c] = c;
|
|
884
|
+
const name = detectProjectName2(cwd, existing, paths);
|
|
885
|
+
const vars = { projectName: name, components: allComponents, paths };
|
|
886
|
+
const pkg = JSON.parse(await readFile5(join6(repoDir, "cli/package.json"), "utf-8"));
|
|
887
|
+
const version = pkg.version;
|
|
888
|
+
if (!hasBaseline(cwd)) {
|
|
889
|
+
const rebuildSpinner = p5.spinner();
|
|
890
|
+
rebuildSpinner.start("Establishing baseline");
|
|
891
|
+
await reconstructBaseline(
|
|
892
|
+
cwd,
|
|
893
|
+
repoDir,
|
|
894
|
+
existing,
|
|
895
|
+
existingPaths,
|
|
896
|
+
{ projectName: name, components: existing, paths: existingPaths },
|
|
897
|
+
config.version || version
|
|
898
|
+
);
|
|
899
|
+
rebuildSpinner.stop("Baseline established.");
|
|
900
|
+
}
|
|
901
|
+
const spinner5 = p5.spinner();
|
|
902
|
+
spinner5.start("Adding to baseline");
|
|
903
|
+
await addToBaseline(cwd, repoDir, toAdd, allComponents, paths, vars, version);
|
|
904
|
+
spinner5.stop("Baseline updated.");
|
|
905
|
+
const result = mergeBaseline(cwd, `projx: add ${toAdd.join(", ")} from template v${version}`);
|
|
906
|
+
if (result.status === "conflicts") {
|
|
907
|
+
p5.log.warn(`Merge conflicts in ${result.conflictedFiles.length} file(s):`);
|
|
908
|
+
for (const f of result.conflictedFiles) {
|
|
909
|
+
p5.log.message(` ${f}`);
|
|
910
|
+
}
|
|
911
|
+
p5.log.info("Resolve conflicts, then: git add . && git commit");
|
|
912
|
+
}
|
|
913
|
+
if (!skipInstall) {
|
|
914
|
+
await installDeps2(cwd, toAdd);
|
|
915
|
+
}
|
|
916
|
+
for (const component of toAdd) {
|
|
917
|
+
const example = join6(cwd, component, ".env.example");
|
|
918
|
+
const env = join6(cwd, component, ".env");
|
|
919
|
+
if (existsSync5(example) && !existsSync5(env)) {
|
|
920
|
+
try {
|
|
921
|
+
copyFileSync2(example, env);
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
902
924
|
}
|
|
903
925
|
}
|
|
926
|
+
} finally {
|
|
927
|
+
await cleanupRepo(repoDir, isLocal);
|
|
904
928
|
}
|
|
905
|
-
|
|
906
|
-
await readFile5(join5(repoDir, "cli/package.json"), "utf-8")
|
|
907
|
-
);
|
|
908
|
-
const updatedConfig = {
|
|
909
|
-
version: pkg.version,
|
|
910
|
-
components: allComponents,
|
|
911
|
-
createdAt: config.createdAt
|
|
912
|
-
};
|
|
913
|
-
await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
914
|
-
p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
|
|
929
|
+
p5.outro(`Added ${toAdd.join(", ")}.
|
|
915
930
|
|
|
916
931
|
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
917
932
|
}
|
|
918
|
-
async function substituteNames2(dest, components, name, nameSnake) {
|
|
919
|
-
if (components.includes("fastapi")) {
|
|
920
|
-
await replaceInFile(
|
|
921
|
-
join5(dest, "fastapi/pyproject.toml"),
|
|
922
|
-
"projx-fastapi",
|
|
923
|
-
`${name}-fastapi`
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
if (components.includes("fastify")) {
|
|
927
|
-
await replaceInFile(
|
|
928
|
-
join5(dest, "fastify/package.json"),
|
|
929
|
-
"projx-fastify",
|
|
930
|
-
`${name}-fastify`
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
if (components.includes("frontend")) {
|
|
934
|
-
await replaceInFile(
|
|
935
|
-
join5(dest, "frontend/package.json"),
|
|
936
|
-
"projx-frontend",
|
|
937
|
-
`${name}-frontend`
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
if (components.includes("e2e")) {
|
|
941
|
-
await replaceInFile(
|
|
942
|
-
join5(dest, "e2e/package.json"),
|
|
943
|
-
"projx-e2e",
|
|
944
|
-
`${name}-e2e`
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
if (components.includes("mobile")) {
|
|
948
|
-
await replaceInFile(
|
|
949
|
-
join5(dest, "mobile/pubspec.yaml"),
|
|
950
|
-
"projx_mobile",
|
|
951
|
-
`${nameSnake}_mobile`
|
|
952
|
-
);
|
|
953
|
-
await replaceInDir(
|
|
954
|
-
join5(dest, "mobile"),
|
|
955
|
-
"package:projx_mobile/",
|
|
956
|
-
`package:${nameSnake}_mobile/`,
|
|
957
|
-
".dart"
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
933
|
async function installDeps2(dest, components) {
|
|
962
934
|
for (const component of components) {
|
|
963
|
-
const spinner5 =
|
|
935
|
+
const spinner5 = p5.spinner();
|
|
964
936
|
try {
|
|
965
937
|
switch (component) {
|
|
966
938
|
case "fastapi":
|
|
967
939
|
if (hasCommand("uv")) {
|
|
968
940
|
spinner5.start("Installing FastAPI dependencies");
|
|
969
|
-
exec("uv sync --all-extras",
|
|
941
|
+
exec("uv sync --all-extras", join6(dest, "fastapi"));
|
|
970
942
|
spinner5.stop("FastAPI dependencies installed.");
|
|
971
943
|
} else {
|
|
972
|
-
|
|
944
|
+
p5.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
973
945
|
}
|
|
974
946
|
break;
|
|
975
947
|
case "fastify":
|
|
976
948
|
if (hasCommand("pnpm")) {
|
|
977
949
|
spinner5.start("Installing Fastify dependencies");
|
|
978
|
-
exec("pnpm install",
|
|
950
|
+
exec("pnpm install", join6(dest, "fastify"));
|
|
979
951
|
spinner5.stop("Fastify dependencies installed.");
|
|
980
952
|
} else {
|
|
981
953
|
spinner5.start("Installing Fastify dependencies");
|
|
982
|
-
exec("npm install",
|
|
954
|
+
exec("npm install", join6(dest, "fastify"));
|
|
983
955
|
spinner5.stop("Fastify dependencies installed.");
|
|
984
956
|
}
|
|
985
957
|
break;
|
|
986
958
|
case "frontend":
|
|
987
959
|
spinner5.start("Installing Frontend dependencies");
|
|
988
|
-
exec("npm install",
|
|
960
|
+
exec("npm install", join6(dest, "frontend"));
|
|
989
961
|
spinner5.stop("Frontend dependencies installed.");
|
|
990
962
|
break;
|
|
991
963
|
case "e2e":
|
|
992
964
|
spinner5.start("Installing E2E dependencies");
|
|
993
|
-
exec("npm install",
|
|
965
|
+
exec("npm install", join6(dest, "e2e"));
|
|
994
966
|
spinner5.stop("E2E dependencies installed.");
|
|
995
967
|
break;
|
|
996
968
|
case "mobile":
|
|
997
969
|
if (hasCommand("flutter")) {
|
|
998
970
|
spinner5.start("Installing Flutter dependencies");
|
|
999
|
-
exec("flutter pub get",
|
|
971
|
+
exec("flutter pub get", join6(dest, "mobile"));
|
|
1000
972
|
spinner5.stop("Flutter dependencies installed.");
|
|
1001
973
|
} else {
|
|
1002
|
-
|
|
974
|
+
p5.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
1003
975
|
}
|
|
1004
976
|
break;
|
|
1005
977
|
case "infra":
|
|
@@ -1013,12 +985,10 @@ async function installDeps2(dest, components) {
|
|
|
1013
985
|
function detectProjectName2(cwd, components, paths) {
|
|
1014
986
|
for (const component of components) {
|
|
1015
987
|
const dir = paths[component] ?? component;
|
|
1016
|
-
const pkgPath =
|
|
1017
|
-
if (
|
|
988
|
+
const pkgPath = join6(cwd, dir, "package.json");
|
|
989
|
+
if (existsSync5(pkgPath)) {
|
|
1018
990
|
try {
|
|
1019
|
-
const pkg = JSON.parse(
|
|
1020
|
-
readFileSync2(pkgPath, "utf-8")
|
|
1021
|
-
);
|
|
991
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1022
992
|
const n = pkg.name;
|
|
1023
993
|
if (n && n.includes("-")) {
|
|
1024
994
|
return n.substring(0, n.lastIndexOf("-"));
|
|
@@ -1031,22 +1001,22 @@ function detectProjectName2(cwd, components, paths) {
|
|
|
1031
1001
|
}
|
|
1032
1002
|
|
|
1033
1003
|
// src/init.ts
|
|
1034
|
-
import { existsSync as
|
|
1035
|
-
import { readFile as readFile6
|
|
1036
|
-
import { execSync as
|
|
1037
|
-
import { join as
|
|
1038
|
-
import * as
|
|
1004
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1005
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1006
|
+
import { execSync as execSync4 } from "child_process";
|
|
1007
|
+
import { join as join8 } from "path";
|
|
1008
|
+
import * as p6 from "@clack/prompts";
|
|
1039
1009
|
|
|
1040
1010
|
// src/detect.ts
|
|
1041
|
-
import { existsSync as
|
|
1011
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1042
1012
|
import { readdir as readdir2 } from "fs/promises";
|
|
1043
|
-
import { join as
|
|
1013
|
+
import { join as join7 } from "path";
|
|
1044
1014
|
async function detectComponents(cwd) {
|
|
1045
1015
|
const results = [];
|
|
1046
1016
|
const entries = await readdir2(cwd, { withFileTypes: true });
|
|
1047
1017
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)).map((e) => e.name);
|
|
1048
1018
|
for (const dir of dirs) {
|
|
1049
|
-
const full =
|
|
1019
|
+
const full = join7(cwd, dir);
|
|
1050
1020
|
const detections = await scanDirectory(full, dir);
|
|
1051
1021
|
results.push(...detections);
|
|
1052
1022
|
}
|
|
@@ -1054,7 +1024,7 @@ async function detectComponents(cwd) {
|
|
|
1054
1024
|
}
|
|
1055
1025
|
async function scanDirectory(dir, relPath) {
|
|
1056
1026
|
const results = [];
|
|
1057
|
-
const pyproject = await readFileOrNull(
|
|
1027
|
+
const pyproject = await readFileOrNull(join7(dir, "pyproject.toml"));
|
|
1058
1028
|
if (pyproject && /fastapi/i.test(pyproject)) {
|
|
1059
1029
|
results.push({
|
|
1060
1030
|
component: "fastapi",
|
|
@@ -1091,7 +1061,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1091
1061
|
});
|
|
1092
1062
|
}
|
|
1093
1063
|
}
|
|
1094
|
-
const pubspec = await readFileOrNull(
|
|
1064
|
+
const pubspec = await readFileOrNull(join7(dir, "pubspec.yaml"));
|
|
1095
1065
|
if (pubspec && /flutter:/i.test(pubspec)) {
|
|
1096
1066
|
results.push({
|
|
1097
1067
|
component: "mobile",
|
|
@@ -1100,7 +1070,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1100
1070
|
evidence: "pubspec.yaml has flutter dependency"
|
|
1101
1071
|
});
|
|
1102
1072
|
}
|
|
1103
|
-
const hasTf =
|
|
1073
|
+
const hasTf = existsSync6(join7(dir, "main.tf")) || existsSync6(join7(dir, "variables.tf")) || existsSync6(join7(dir, "stack/main.tf")) || existsSync6(join7(dir, "versions.tf"));
|
|
1104
1074
|
if (hasTf) {
|
|
1105
1075
|
results.push({
|
|
1106
1076
|
component: "infra",
|
|
@@ -1112,7 +1082,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1112
1082
|
return results;
|
|
1113
1083
|
}
|
|
1114
1084
|
async function readPkg(dir) {
|
|
1115
|
-
const content = await readFileOrNull(
|
|
1085
|
+
const content = await readFileOrNull(join7(dir, "package.json"));
|
|
1116
1086
|
if (!content) return null;
|
|
1117
1087
|
try {
|
|
1118
1088
|
return JSON.parse(content);
|
|
@@ -1121,83 +1091,23 @@ async function readPkg(dir) {
|
|
|
1121
1091
|
}
|
|
1122
1092
|
}
|
|
1123
1093
|
|
|
1124
|
-
// src/diff.ts
|
|
1125
|
-
function unifiedDiff(existing, template, label) {
|
|
1126
|
-
const a = existing.split("\n");
|
|
1127
|
-
const b = template.split("\n");
|
|
1128
|
-
const lines = [`--- existing ${label}`, `+++ template ${label}`];
|
|
1129
|
-
const lcs = computeLCS(a, b);
|
|
1130
|
-
let ai = 0;
|
|
1131
|
-
let bi = 0;
|
|
1132
|
-
for (const match of lcs) {
|
|
1133
|
-
while (ai < match.ai) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
|
|
1134
|
-
while (bi < match.bi) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
|
|
1135
|
-
lines.push(` ${a[ai]}`);
|
|
1136
|
-
ai++;
|
|
1137
|
-
bi++;
|
|
1138
|
-
}
|
|
1139
|
-
while (ai < a.length) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
|
|
1140
|
-
while (bi < b.length) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
|
|
1141
|
-
if (lines.length > 80) {
|
|
1142
|
-
return lines.slice(0, 80).join("\n") + `
|
|
1143
|
-
... (${lines.length - 80} more lines)`;
|
|
1144
|
-
}
|
|
1145
|
-
return lines.join("\n");
|
|
1146
|
-
}
|
|
1147
|
-
function computeLCS(a, b) {
|
|
1148
|
-
const m = a.length;
|
|
1149
|
-
const n = b.length;
|
|
1150
|
-
if (m * n > 1e5) {
|
|
1151
|
-
return simpleLCS(a, b);
|
|
1152
|
-
}
|
|
1153
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
1154
|
-
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
1155
|
-
for (let j2 = n - 1; j2 >= 0; j2--) {
|
|
1156
|
-
if (a[i2] === b[j2]) {
|
|
1157
|
-
dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
|
|
1158
|
-
} else {
|
|
1159
|
-
dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
const matches = [];
|
|
1164
|
-
let i = 0;
|
|
1165
|
-
let j = 0;
|
|
1166
|
-
while (i < m && j < n) {
|
|
1167
|
-
if (a[i] === b[j]) {
|
|
1168
|
-
matches.push({ ai: i, bi: j });
|
|
1169
|
-
i++;
|
|
1170
|
-
j++;
|
|
1171
|
-
} else if (dp[i + 1]?.[j] ?? 0 >= (dp[i]?.[j + 1] ?? 0)) {
|
|
1172
|
-
i++;
|
|
1173
|
-
} else {
|
|
1174
|
-
j++;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
return matches;
|
|
1178
|
-
}
|
|
1179
|
-
function simpleLCS(a, b) {
|
|
1180
|
-
const matches = [];
|
|
1181
|
-
let bi = 0;
|
|
1182
|
-
for (let ai = 0; ai < a.length && bi < b.length; ai++) {
|
|
1183
|
-
const idx = b.indexOf(a[ai], bi);
|
|
1184
|
-
if (idx !== -1) {
|
|
1185
|
-
matches.push({ ai, bi: idx });
|
|
1186
|
-
bi = idx + 1;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
return matches;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
1094
|
// src/init.ts
|
|
1193
1095
|
async function init(cwd, localRepo) {
|
|
1194
|
-
|
|
1096
|
+
p6.intro("projx init");
|
|
1195
1097
|
const isLocal = !!localRepo;
|
|
1196
|
-
if (
|
|
1197
|
-
|
|
1098
|
+
if (existsSync7(join8(cwd, ".projx"))) {
|
|
1099
|
+
p6.log.error("This project is already initialized. Use 'projx update' or 'projx add' instead.");
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
if (!isGitRepo2(cwd)) {
|
|
1103
|
+
p6.log.error(`projx init requires a git repo. Run 'git init && git add -A && git commit -m "initial"' first.`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
if (hasUncommittedChanges2(cwd)) {
|
|
1107
|
+
p6.log.error("You have uncommitted changes. Commit or stash them first.");
|
|
1198
1108
|
process.exit(1);
|
|
1199
1109
|
}
|
|
1200
|
-
const spinner5 =
|
|
1110
|
+
const spinner5 = p6.spinner();
|
|
1201
1111
|
spinner5.start("Scanning for components");
|
|
1202
1112
|
const detected = await detectComponents(cwd);
|
|
1203
1113
|
spinner5.stop(
|
|
@@ -1210,7 +1120,7 @@ async function init(cwd, localRepo) {
|
|
|
1210
1120
|
confirmed = await manualSelect(cwd);
|
|
1211
1121
|
}
|
|
1212
1122
|
if (confirmed.length === 0) {
|
|
1213
|
-
|
|
1123
|
+
p6.log.warn("No components selected. Nothing to do.");
|
|
1214
1124
|
process.exit(0);
|
|
1215
1125
|
}
|
|
1216
1126
|
const components = confirmed.map((c) => c.component);
|
|
@@ -1219,54 +1129,51 @@ async function init(cwd, localRepo) {
|
|
|
1219
1129
|
);
|
|
1220
1130
|
const projectName = toKebab(cwd.split("/").pop());
|
|
1221
1131
|
const vars = { projectName, components, paths };
|
|
1222
|
-
const dlSpinner =
|
|
1132
|
+
const dlSpinner = p6.spinner();
|
|
1223
1133
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
1224
1134
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1225
1135
|
dlSpinner.stop("Failed.");
|
|
1226
|
-
|
|
1136
|
+
p6.log.error(String(err));
|
|
1227
1137
|
process.exit(1);
|
|
1228
1138
|
});
|
|
1229
1139
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1230
1140
|
try {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1141
|
+
const pkg = JSON.parse(await readFile6(join8(repoDir, "cli/package.json"), "utf-8"));
|
|
1142
|
+
const version = pkg.version;
|
|
1143
|
+
const baselineSpinner = p6.spinner();
|
|
1144
|
+
baselineSpinner.start("Creating template baseline");
|
|
1145
|
+
await createBaseline(cwd, repoDir, components, paths, vars, version, "init");
|
|
1146
|
+
baselineSpinner.stop("Baseline created.");
|
|
1147
|
+
const mergeSpinner = p6.spinner();
|
|
1148
|
+
mergeSpinner.start("Merging baseline (preserving your code)");
|
|
1149
|
+
mergeBaseline(
|
|
1150
|
+
cwd,
|
|
1151
|
+
`projx: adopt template v${version} as baseline`,
|
|
1152
|
+
true,
|
|
1153
|
+
true
|
|
1241
1154
|
);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
components,
|
|
1245
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1246
|
-
};
|
|
1247
|
-
await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
1248
|
-
p5.log.success(".projx");
|
|
1249
|
-
if (isGitRepo2(cwd)) {
|
|
1155
|
+
mergeSpinner.stop("Baseline merged. Your code is preserved.");
|
|
1156
|
+
if (!existsSync7(join8(cwd, ".githooks"))) {
|
|
1250
1157
|
try {
|
|
1251
|
-
|
|
1252
|
-
|
|
1158
|
+
execSync4("git config core.hooksPath .githooks", { cwd, stdio: "pipe" });
|
|
1159
|
+
p6.log.success("Git hooks configured.");
|
|
1253
1160
|
} catch {
|
|
1254
|
-
|
|
1161
|
+
p6.log.warn("Failed to configure git hooks.");
|
|
1255
1162
|
}
|
|
1256
1163
|
}
|
|
1257
1164
|
} finally {
|
|
1258
1165
|
await cleanupRepo(repoDir, isLocal);
|
|
1259
1166
|
}
|
|
1260
|
-
|
|
1167
|
+
p6.outro("Project initialized. Run './setup.sh' to install dependencies.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx");
|
|
1261
1168
|
}
|
|
1262
1169
|
async function confirmDetections(detected) {
|
|
1263
1170
|
const confirmed = [];
|
|
1264
1171
|
for (const d of detected) {
|
|
1265
|
-
const yes = await
|
|
1172
|
+
const yes = await p6.confirm({
|
|
1266
1173
|
message: `Found ${LABELS[d.component].label} in ${d.directory}/ \u2014 register as "${d.component}"?`,
|
|
1267
1174
|
initialValue: true
|
|
1268
1175
|
});
|
|
1269
|
-
if (
|
|
1176
|
+
if (p6.isCancel(yes)) process.exit(0);
|
|
1270
1177
|
if (yes) {
|
|
1271
1178
|
confirmed.push({ component: d.component, directory: d.directory });
|
|
1272
1179
|
}
|
|
@@ -1274,7 +1181,7 @@ async function confirmDetections(detected) {
|
|
|
1274
1181
|
return confirmed;
|
|
1275
1182
|
}
|
|
1276
1183
|
async function manualSelect(cwd) {
|
|
1277
|
-
const selected = await
|
|
1184
|
+
const selected = await p6.multiselect({
|
|
1278
1185
|
message: "No components detected. Select manually:",
|
|
1279
1186
|
options: COMPONENTS.map((c) => ({
|
|
1280
1187
|
value: c,
|
|
@@ -1283,140 +1190,39 @@ async function manualSelect(cwd) {
|
|
|
1283
1190
|
})),
|
|
1284
1191
|
required: false
|
|
1285
1192
|
});
|
|
1286
|
-
if (
|
|
1193
|
+
if (p6.isCancel(selected)) process.exit(0);
|
|
1287
1194
|
const result = [];
|
|
1288
1195
|
for (const component of selected) {
|
|
1289
|
-
const dir = await
|
|
1196
|
+
const dir = await p6.text({
|
|
1290
1197
|
message: `Directory for ${LABELS[component].label}?`,
|
|
1291
1198
|
placeholder: component,
|
|
1292
1199
|
defaultValue: component
|
|
1293
1200
|
});
|
|
1294
|
-
if (
|
|
1295
|
-
if (!
|
|
1296
|
-
|
|
1201
|
+
if (p6.isCancel(dir)) process.exit(0);
|
|
1202
|
+
if (!existsSync7(join8(cwd, dir))) {
|
|
1203
|
+
p6.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
1297
1204
|
continue;
|
|
1298
1205
|
}
|
|
1299
1206
|
result.push({ component, directory: dir });
|
|
1300
1207
|
}
|
|
1301
1208
|
return result;
|
|
1302
1209
|
}
|
|
1303
|
-
async function generateSharedFiles(cwd, repoDir, vars) {
|
|
1304
|
-
const files = [];
|
|
1305
|
-
const hasBackend = vars.components.includes("fastapi") || vars.components.includes("fastify");
|
|
1306
|
-
if (hasBackend || vars.components.includes("frontend")) {
|
|
1307
|
-
files.push(
|
|
1308
|
-
{ path: "docker-compose.yml", content: await generateDockerCompose(vars) },
|
|
1309
|
-
{ path: "docker-compose.dev.yml", content: await generateDockerComposeDev(vars) }
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
files.push(
|
|
1313
|
-
{ path: "README.md", content: await generateReadme(vars) },
|
|
1314
|
-
{ path: ".githooks/pre-commit", content: await generatePreCommit(vars), mode: 493 },
|
|
1315
|
-
{ path: ".github/workflows/ci.yml", content: await generateCiYml(vars) },
|
|
1316
|
-
{ path: "setup.sh", content: await generateSetupSh(vars), mode: 493 }
|
|
1317
|
-
);
|
|
1318
|
-
for (const file of files) {
|
|
1319
|
-
const dest = join7(cwd, file.path);
|
|
1320
|
-
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
1321
|
-
if (dir !== cwd) await mkdir5(dir, { recursive: true });
|
|
1322
|
-
const existing = await readFileOrNull(dest);
|
|
1323
|
-
if (existing === null) {
|
|
1324
|
-
await writeFile5(dest, file.content);
|
|
1325
|
-
if (file.mode) await chmod4(dest, file.mode);
|
|
1326
|
-
p5.log.success(file.path);
|
|
1327
|
-
} else if (existing === file.content) {
|
|
1328
|
-
p5.log.info(`${file.path} \u2014 identical, skipped.`);
|
|
1329
|
-
} else {
|
|
1330
|
-
const action = await resolveConflict(file.path, existing, file.content);
|
|
1331
|
-
if (action === "overwrite") {
|
|
1332
|
-
await writeFile5(dest, file.content);
|
|
1333
|
-
if (file.mode) await chmod4(dest, file.mode);
|
|
1334
|
-
p5.log.success(`${file.path} \u2014 overwritten.`);
|
|
1335
|
-
} else {
|
|
1336
|
-
p5.log.info(`${file.path} \u2014 kept existing.`);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const statics = [".editorconfig"];
|
|
1341
|
-
for (const file of statics) {
|
|
1342
|
-
const src = join7(repoDir, file);
|
|
1343
|
-
const dest = join7(cwd, file);
|
|
1344
|
-
if (!existsSync6(src)) continue;
|
|
1345
|
-
if (!existsSync6(dest)) {
|
|
1346
|
-
await cp3(src, dest);
|
|
1347
|
-
p5.log.success(file);
|
|
1348
|
-
} else {
|
|
1349
|
-
const existing = await readFileOrNull(dest);
|
|
1350
|
-
const template = await readFileOrNull(src);
|
|
1351
|
-
if (existing === template) {
|
|
1352
|
-
p5.log.info(`${file} \u2014 identical, skipped.`);
|
|
1353
|
-
} else {
|
|
1354
|
-
const action = await resolveConflict(file, existing ?? "", template ?? "");
|
|
1355
|
-
if (action === "overwrite") {
|
|
1356
|
-
await cp3(src, dest, { force: true });
|
|
1357
|
-
p5.log.success(`${file} \u2014 overwritten.`);
|
|
1358
|
-
} else {
|
|
1359
|
-
p5.log.info(`${file} \u2014 kept existing.`);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
const vscodeDest = join7(cwd, ".vscode");
|
|
1365
|
-
await mkdir5(vscodeDest, { recursive: true });
|
|
1366
|
-
const settingsPath = join7(vscodeDest, "settings.json");
|
|
1367
|
-
const settingsContent = generateVscodeSettings(vars);
|
|
1368
|
-
const existingSettings = await readFileOrNull(settingsPath);
|
|
1369
|
-
if (existingSettings === null) {
|
|
1370
|
-
await writeFile5(settingsPath, settingsContent);
|
|
1371
|
-
p5.log.success(".vscode/settings.json");
|
|
1372
|
-
} else if (existingSettings !== settingsContent) {
|
|
1373
|
-
const action = await resolveConflict(".vscode/settings.json", existingSettings, settingsContent);
|
|
1374
|
-
if (action === "overwrite") {
|
|
1375
|
-
await writeFile5(settingsPath, settingsContent);
|
|
1376
|
-
p5.log.success(".vscode/settings.json \u2014 overwritten.");
|
|
1377
|
-
} else {
|
|
1378
|
-
p5.log.info(".vscode/settings.json \u2014 kept existing.");
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
const extSrc = join7(repoDir, ".vscode/extensions.json");
|
|
1382
|
-
const extDest = join7(vscodeDest, "extensions.json");
|
|
1383
|
-
if (existsSync6(extSrc) && !existsSync6(extDest)) {
|
|
1384
|
-
await cp3(extSrc, extDest);
|
|
1385
|
-
p5.log.success(".vscode/extensions.json");
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
async function resolveConflict(filePath, existing, template) {
|
|
1389
|
-
let action = await p5.select({
|
|
1390
|
-
message: `${filePath} differs from projx template`,
|
|
1391
|
-
options: [
|
|
1392
|
-
{ value: "diff", label: "View diff" },
|
|
1393
|
-
{ value: "overwrite", label: "Overwrite with template" },
|
|
1394
|
-
{ value: "skip", label: "Skip (keep existing)" }
|
|
1395
|
-
]
|
|
1396
|
-
});
|
|
1397
|
-
if (p5.isCancel(action)) process.exit(0);
|
|
1398
|
-
if (action === "diff") {
|
|
1399
|
-
const diff = unifiedDiff(existing, template, filePath);
|
|
1400
|
-
p5.log.message(diff);
|
|
1401
|
-
action = await p5.select({
|
|
1402
|
-
message: `${filePath}`,
|
|
1403
|
-
options: [
|
|
1404
|
-
{ value: "overwrite", label: "Overwrite with template" },
|
|
1405
|
-
{ value: "skip", label: "Skip (keep existing)" }
|
|
1406
|
-
]
|
|
1407
|
-
});
|
|
1408
|
-
if (p5.isCancel(action)) process.exit(0);
|
|
1409
|
-
}
|
|
1410
|
-
return action;
|
|
1411
|
-
}
|
|
1412
1210
|
function isGitRepo2(cwd) {
|
|
1413
1211
|
try {
|
|
1414
|
-
|
|
1212
|
+
execSync4("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1415
1213
|
return true;
|
|
1416
1214
|
} catch {
|
|
1417
1215
|
return false;
|
|
1418
1216
|
}
|
|
1419
1217
|
}
|
|
1218
|
+
function hasUncommittedChanges2(cwd) {
|
|
1219
|
+
try {
|
|
1220
|
+
const status = execSync4("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1221
|
+
return status.length > 0;
|
|
1222
|
+
} catch {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1420
1226
|
|
|
1421
1227
|
// src/index.ts
|
|
1422
1228
|
var args = process.argv.slice(2);
|
|
@@ -1542,7 +1348,7 @@ async function main() {
|
|
|
1542
1348
|
opts.install = options.install ?? opts.install;
|
|
1543
1349
|
}
|
|
1544
1350
|
const dest = resolve2(process.cwd(), opts.name);
|
|
1545
|
-
if (
|
|
1351
|
+
if (existsSync8(dest)) {
|
|
1546
1352
|
console.error(`Error: ${dest} already exists.`);
|
|
1547
1353
|
process.exit(1);
|
|
1548
1354
|
}
|