create-projx 1.6.2 → 1.6.4

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.
@@ -13,11 +13,18 @@ import {
13
13
  toSnake,
14
14
  upsertComponentMarker,
15
15
  writeProjxConfig
16
- } from "./chunk-LTIJPVRZ.js";
16
+ } from "./chunk-6YRBHJ2V.js";
17
17
 
18
18
  // src/baseline.ts
19
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
20
- import { chmod, mkdir, writeFile, rm, readFile as readFile2, copyFile } from "fs/promises";
20
+ import {
21
+ chmod,
22
+ mkdir,
23
+ writeFile,
24
+ rm,
25
+ readFile as readFile2,
26
+ copyFile
27
+ } from "fs/promises";
21
28
  import { execSync } from "child_process";
22
29
  import { join as join2, dirname } from "path";
23
30
  import { tmpdir } from "os";
@@ -25,30 +32,60 @@ import { tmpdir } from "os";
25
32
  // src/generators/index.ts
26
33
  import { readFile } from "fs/promises";
27
34
  import { join } from "path";
35
+ function shellSafeUpper(s) {
36
+ return s.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
37
+ }
38
+ var CANONICAL_DISPLAY = {
39
+ fastapi: "FastAPI",
40
+ fastify: "Fastify",
41
+ frontend: "Frontend",
42
+ mobile: "Flutter",
43
+ e2e: "E2E",
44
+ infra: "Terraform"
45
+ };
46
+ function withInstances(vars) {
47
+ const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({
48
+ type,
49
+ path: vars.paths[type] ?? type
50
+ }));
51
+ const enriched = base.map((inst) => ({
52
+ ...inst,
53
+ upper: shellSafeUpper(inst.path),
54
+ display: inst.path === inst.type ? CANONICAL_DISPLAY[inst.type] : inst.path
55
+ }));
56
+ const byType = (type) => enriched.filter((i) => i.type === type).sort((a, b) => a.path.localeCompare(b.path));
57
+ return {
58
+ ...vars,
59
+ instances: enriched,
60
+ fastapiInstances: byType("fastapi"),
61
+ fastifyInstances: byType("fastify"),
62
+ frontendInstances: byType("frontend"),
63
+ mobileInstances: byType("mobile"),
64
+ e2eInstances: byType("e2e"),
65
+ infraInstances: byType("infra")
66
+ };
67
+ }
28
68
  async function renderShared(filename, vars) {
29
- const tpl = await readFile(
30
- join(sharedTemplateDir(), filename),
31
- "utf-8"
32
- );
69
+ const tpl = await readFile(join(sharedTemplateDir(), filename), "utf-8");
33
70
  return render(tpl, vars);
34
71
  }
35
72
  async function generateDockerCompose(vars) {
36
- return renderShared("docker-compose.yml.ejs", vars);
73
+ return renderShared("docker-compose.yml.ejs", withInstances(vars));
37
74
  }
38
75
  async function generateDockerComposeDev(vars) {
39
- return renderShared("docker-compose.dev.yml.ejs", vars);
76
+ return renderShared("docker-compose.dev.yml.ejs", withInstances(vars));
40
77
  }
41
78
  async function generatePreCommit(vars) {
42
- return renderShared("pre-commit.ejs", vars);
79
+ return renderShared("pre-commit.ejs", withInstances(vars));
43
80
  }
44
81
  async function generateSetupSh(vars) {
45
- return renderShared("setup.sh.ejs", vars);
82
+ return renderShared("setup.sh.ejs", withInstances(vars));
46
83
  }
47
84
  async function generateCiYml(vars) {
48
- return renderShared("ci.yml.ejs", vars);
85
+ return renderShared("ci.yml.ejs", withInstances(vars));
49
86
  }
50
87
  async function generateReadme(vars) {
51
- return renderShared("README.md.ejs", vars);
88
+ return renderShared("README.md.ejs", withInstances(vars));
52
89
  }
53
90
  function generateVscodeSettings(vars) {
54
91
  const settings = {};
@@ -58,9 +95,15 @@ function generateVscodeSettings(vars) {
58
95
  "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
59
96
  };
60
97
  }
61
- settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
62
- settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
63
- settings["[javascript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
98
+ settings["[typescript]"] = {
99
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
100
+ };
101
+ settings["[typescriptreact]"] = {
102
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
103
+ };
104
+ settings["[javascript]"] = {
105
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
106
+ };
64
107
  settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
65
108
  settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
66
109
  settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
@@ -82,32 +125,9 @@ function generateVscodeSettings(vars) {
82
125
  }
83
126
 
84
127
  // src/baseline.ts
85
- function buildPathsUpper(paths) {
86
- const result = {};
87
- for (const [component, dir] of Object.entries(paths)) {
88
- result[component] = dir.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
89
- }
90
- return result;
91
- }
92
- var CANONICAL_DISPLAY_NAMES = {
93
- fastapi: "FastAPI",
94
- fastify: "Fastify",
95
- frontend: "Frontend",
96
- mobile: "Flutter",
97
- e2e: "E2E",
98
- infra: "Terraform"
99
- };
100
- function buildDisplayNames(paths) {
101
- const result = {};
102
- for (const [component, dir] of Object.entries(paths)) {
103
- const canonical = component;
104
- result[canonical] = dir === canonical ? CANONICAL_DISPLAY_NAMES[canonical] : dir;
105
- }
106
- return result;
107
- }
108
128
  var BASELINE_REF = "refs/projx/baseline";
109
129
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
110
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-VY5BBJBQ.js");
130
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-AVKSTHIF.js");
111
131
  for (const component of components) {
112
132
  const dir = componentPaths[component];
113
133
  const markerDir = join2(cwd, dir);
@@ -175,11 +195,17 @@ function saveBaselineRef(cwd) {
175
195
  }
176
196
  function getBaselineRef(cwd) {
177
197
  try {
178
- return execSync(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
198
+ return execSync(`git rev-parse --verify ${BASELINE_REF}`, {
199
+ cwd,
200
+ stdio: "pipe"
201
+ }).toString().trim();
179
202
  } catch {
180
203
  }
181
204
  try {
182
- const sha = execSync("git log -1 --format=%H -- .projx", { cwd, stdio: "pipe" }).toString().trim();
205
+ const sha = execSync("git log -1 --format=%H -- .projx", {
206
+ cwd,
207
+ stdio: "pipe"
208
+ }).toString().trim();
183
209
  if (sha) return sha;
184
210
  } catch {
185
211
  }
@@ -187,7 +213,10 @@ function getBaselineRef(cwd) {
187
213
  }
188
214
  function getFileAtRef(cwd, ref, filePath) {
189
215
  try {
190
- return execSync(`git show ${ref}:"${filePath}"`, { cwd, stdio: "pipe" }).toString();
216
+ return execSync(`git show ${ref}:"${filePath}"`, {
217
+ cwd,
218
+ stdio: "pipe"
219
+ }).toString();
191
220
  } catch {
192
221
  return null;
193
222
  }
@@ -258,15 +287,22 @@ async function tryThreeWayMerge(cwd, templateDir, baselineRef, componentPaths) {
258
287
  const pathFallbacks = buildPathFallbacks(componentPaths);
259
288
  for (const file of templateFiles) {
260
289
  if (file === ".projx") continue;
261
- if (file.endsWith("/.projx-component") || file === ".projx-component") continue;
290
+ const isMarker = file.endsWith("/.projx-component") || file === ".projx-component";
262
291
  const oursPath = join2(cwd, file);
292
+ if (isMarker && existsSync(oursPath)) continue;
263
293
  if (!existsSync(oursPath)) {
264
294
  await mkdir(dirname(oursPath), { recursive: true });
265
295
  await copyFile(join2(templateDir, file), oursPath);
266
296
  merged.push(file);
267
297
  continue;
268
298
  }
269
- const baseContent = lookupBaseContent(cwd, baselineRef, file, pathFallbacks);
299
+ if (isMarker) continue;
300
+ const baseContent = lookupBaseContent(
301
+ cwd,
302
+ baselineRef,
303
+ file,
304
+ pathFallbacks
305
+ );
270
306
  if (baseContent === null) continue;
271
307
  let theirsContent;
272
308
  try {
@@ -307,7 +343,10 @@ function createOrphanWorktree(cwd) {
307
343
  }
308
344
  function cleanupWorktree(cwd, worktree, branch) {
309
345
  try {
310
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
346
+ execSync(`git worktree remove "${worktree}" --force`, {
347
+ cwd,
348
+ stdio: "pipe"
349
+ });
311
350
  } catch {
312
351
  try {
313
352
  rm(worktree, { recursive: true, force: true });
@@ -332,7 +371,8 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
332
371
  await walk(full, base);
333
372
  } else if (entry.name !== ".projx-component") {
334
373
  const targetRel = rel.endsWith(".ejs") ? rel.slice(0, -".ejs".length) : rel;
335
- if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns)) continue;
374
+ if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns))
375
+ continue;
336
376
  if (realDir && !existsSync(join2(realDir, targetRel))) continue;
337
377
  await unlink(full);
338
378
  }
@@ -341,41 +381,38 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
341
381
  await walk(dir, dir);
342
382
  }
343
383
  async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, options = {}) {
344
- const { componentSkips, rootSkip, applyDefaults = false, realCwd = dest } = options;
384
+ const {
385
+ componentSkips,
386
+ rootSkip,
387
+ applyDefaults = false,
388
+ realCwd = dest,
389
+ extraInstances = [],
390
+ instancesToScaffold
391
+ } = options;
345
392
  const name = vars.projectName;
346
393
  const nameSnake = toSnake(name);
347
- for (const component of components) {
348
- const targetDir = componentPaths[component];
349
- const baseSkip = componentSkips?.[component] ?? [];
350
- const realMarker = await readComponentMarker(join2(realCwd, targetDir));
351
- const isNewMarker = !realMarker;
352
- const shouldApplyComponentDefault = isNewMarker || applyDefaults;
353
- const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[component] ?? [] : [];
354
- const skipPatterns = [.../* @__PURE__ */ new Set([...baseSkip, ...defaultSkip])];
355
- const tmpDir = join2(dest, "__cptmp__");
356
- await copyComponent(repoDir, component, tmpDir);
357
- const srcDir = join2(tmpDir, component);
358
- if (skipPatterns.length > 0) {
359
- const realComponentDir = join2(realCwd, targetDir);
360
- await removeSkippedFiles(srcDir, skipPatterns, realComponentDir);
361
- }
362
- const outDir = join2(dest, targetDir);
363
- await mkdir(outDir, { recursive: true });
364
- const { cp } = await import("fs/promises");
365
- if (existsSync(srcDir)) {
366
- await cp(srcDir, outDir, { recursive: true, force: true });
367
- }
368
- await rm(tmpDir, { recursive: true, force: true });
369
- await renderEjsInDir(outDir, vars);
370
- await upsertComponentMarker(join2(dest, targetDir), component, skipPatterns.length > 0 ? skipPatterns : void 0);
371
- }
372
- if (!vars.pathsUpper) {
373
- vars.pathsUpper = buildPathsUpper(componentPaths);
374
- }
375
- if (!vars.displayNames) {
376
- vars.displayNames = buildDisplayNames(componentPaths);
394
+ const primaryInstances = components.map((type) => ({
395
+ type,
396
+ path: componentPaths[type]
397
+ }));
398
+ const allInstances = [
399
+ ...primaryInstances,
400
+ ...extraInstances
401
+ ];
402
+ const toScaffold = instancesToScaffold ?? allInstances;
403
+ for (const inst of toScaffold) {
404
+ await writeOneInstance(inst, {
405
+ dest,
406
+ repoDir,
407
+ vars,
408
+ componentPaths,
409
+ realCwd,
410
+ applyDefaults,
411
+ baseSkip: componentSkips?.[inst.type] ?? [],
412
+ projectName: name,
413
+ nameSnake
414
+ });
377
415
  }
378
- await substituteNames(dest, components, componentPaths, name, nameSnake, vars.nameOverrides);
379
416
  const hasBackend = components.includes("fastapi") || components.includes("fastify");
380
417
  const userSkip = rootSkip ?? [];
381
418
  const defaultRootSkip = applyDefaults ? DEFAULT_ROOT_SKIP_PATTERNS : [];
@@ -386,53 +423,147 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
386
423
  };
387
424
  if (hasBackend || components.includes("frontend")) {
388
425
  if (shouldWrite("docker-compose.yml"))
389
- await writeFile(join2(dest, "docker-compose.yml"), await generateDockerCompose(vars));
426
+ await writeFile(
427
+ join2(dest, "docker-compose.yml"),
428
+ await generateDockerCompose(vars)
429
+ );
390
430
  if (shouldWrite("docker-compose.dev.yml"))
391
- await writeFile(join2(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
431
+ await writeFile(
432
+ join2(dest, "docker-compose.dev.yml"),
433
+ await generateDockerComposeDev(vars)
434
+ );
392
435
  }
393
436
  if (shouldWrite("README.md"))
394
437
  await writeFile(join2(dest, "README.md"), await generateReadme(vars));
395
438
  if (shouldWrite(".githooks/pre-commit")) {
396
439
  await mkdir(join2(dest, ".githooks"), { recursive: true });
397
- await writeFile(join2(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
440
+ await writeFile(
441
+ join2(dest, ".githooks/pre-commit"),
442
+ await generatePreCommit(vars)
443
+ );
398
444
  await chmod(join2(dest, ".githooks/pre-commit"), 493);
399
445
  }
400
446
  if (shouldWrite(".github/workflows/ci.yml")) {
401
447
  await mkdir(join2(dest, ".github/workflows"), { recursive: true });
402
- await writeFile(join2(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
448
+ await writeFile(
449
+ join2(dest, ".github/workflows/ci.yml"),
450
+ await generateCiYml(vars)
451
+ );
403
452
  }
404
- if (shouldWrite("setup.sh")) {
405
- await writeFile(join2(dest, "setup.sh"), await generateSetupSh(vars));
406
- await chmod(join2(dest, "setup.sh"), 493);
453
+ if (shouldWrite("scripts/setup.sh")) {
454
+ await mkdir(join2(dest, "scripts"), { recursive: true });
455
+ await writeFile(
456
+ join2(dest, "scripts/setup.sh"),
457
+ await generateSetupSh(vars)
458
+ );
459
+ await chmod(join2(dest, "scripts/setup.sh"), 493);
407
460
  }
408
461
  await copyStaticFiles(repoDir, dest);
409
462
  if (shouldWrite(".vscode/settings.json")) {
410
463
  await mkdir(join2(dest, ".vscode"), { recursive: true });
411
- await writeFile(join2(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
464
+ await writeFile(
465
+ join2(dest, ".vscode/settings.json"),
466
+ generateVscodeSettings(vars)
467
+ );
412
468
  }
413
469
  await writeManagedProjx(dest, version, vars, applyDefaults);
414
470
  }
415
- async function substituteNames(dest, components, paths, name, nameSnake, overrides) {
416
- if (components.includes("fastapi")) {
417
- const target = overrides?.fastapi ?? `${name}-fastapi`;
418
- await replaceInFile(join2(dest, `${paths.fastapi}/pyproject.toml`), "projx-fastapi", target);
419
- }
420
- if (components.includes("fastify")) {
421
- const target = overrides?.fastify ?? `${name}-fastify`;
422
- await replaceInFile(join2(dest, `${paths.fastify}/package.json`), "projx-fastify", target);
423
- }
424
- if (components.includes("frontend")) {
425
- const target = overrides?.frontend ?? `${name}-frontend`;
426
- await replaceInFile(join2(dest, `${paths.frontend}/package.json`), "projx-frontend", target);
427
- }
428
- if (components.includes("e2e")) {
429
- const target = overrides?.e2e ?? `${name}-e2e`;
430
- await replaceInFile(join2(dest, `${paths.e2e}/package.json`), "projx-e2e", target);
431
- }
432
- if (components.includes("mobile")) {
433
- const target = overrides?.mobile ?? `${nameSnake}_mobile`;
434
- await replaceInFile(join2(dest, `${paths.mobile}/pubspec.yaml`), "projx_mobile", target);
435
- await replaceInDir(join2(dest, `${paths.mobile}`), "package:projx_mobile/", `package:${target}/`, ".dart");
471
+ async function writeOneInstance(inst, opts) {
472
+ const {
473
+ dest,
474
+ repoDir,
475
+ vars,
476
+ componentPaths,
477
+ realCwd,
478
+ applyDefaults,
479
+ baseSkip,
480
+ projectName,
481
+ nameSnake
482
+ } = opts;
483
+ const { type, path: targetDir } = inst;
484
+ const realMarker = await readComponentMarker(join2(realCwd, targetDir));
485
+ const isNewMarker = !realMarker;
486
+ const shouldApplyComponentDefault = isNewMarker || applyDefaults;
487
+ const markerSkip = realMarker?.skip ?? [];
488
+ const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[type] ?? [] : [];
489
+ const skipPatterns = [
490
+ .../* @__PURE__ */ new Set([...baseSkip, ...markerSkip, ...defaultSkip])
491
+ ];
492
+ const tmpDir = join2(dest, "__cptmp__");
493
+ await copyComponent(repoDir, type, tmpDir);
494
+ const srcDir = join2(tmpDir, type);
495
+ if (skipPatterns.length > 0) {
496
+ await removeSkippedFiles(srcDir, skipPatterns, join2(realCwd, targetDir));
497
+ }
498
+ const outDir = join2(dest, targetDir);
499
+ await mkdir(outDir, { recursive: true });
500
+ const { cp } = await import("fs/promises");
501
+ if (existsSync(srcDir)) {
502
+ await cp(srcDir, outDir, { recursive: true, force: true });
503
+ }
504
+ await rm(tmpDir, { recursive: true, force: true });
505
+ const instancePaths = {
506
+ ...componentPaths,
507
+ [type]: targetDir
508
+ };
509
+ await renderEjsInDir(outDir, { ...vars, paths: instancePaths });
510
+ await upsertComponentMarker(
511
+ join2(dest, targetDir),
512
+ type,
513
+ skipPatterns.length > 0 ? skipPatterns : void 0
514
+ );
515
+ await substituteNamesForInstance(
516
+ inst,
517
+ dest,
518
+ projectName,
519
+ nameSnake,
520
+ vars.nameOverrides
521
+ );
522
+ }
523
+ async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides) {
524
+ const { type, path } = inst;
525
+ const isCanonical = path === type;
526
+ if (type === "fastapi") {
527
+ const target = isCanonical ? overrides?.fastapi ?? `${name}-fastapi` : `${name}-${path}`;
528
+ await replaceInFile(
529
+ join2(dest, `${path}/pyproject.toml`),
530
+ "projx-fastapi",
531
+ target
532
+ );
533
+ } else if (type === "fastify") {
534
+ const target = isCanonical ? overrides?.fastify ?? `${name}-fastify` : `${name}-${path}`;
535
+ await replaceInFile(
536
+ join2(dest, `${path}/package.json`),
537
+ "projx-fastify",
538
+ target
539
+ );
540
+ } else if (type === "frontend") {
541
+ const target = isCanonical ? overrides?.frontend ?? `${name}-frontend` : `${name}-${path}`;
542
+ await replaceInFile(
543
+ join2(dest, `${path}/package.json`),
544
+ "projx-frontend",
545
+ target
546
+ );
547
+ } else if (type === "e2e") {
548
+ const target = isCanonical ? overrides?.e2e ?? `${name}-e2e` : `${name}-${path}`;
549
+ await replaceInFile(
550
+ join2(dest, `${path}/package.json`),
551
+ "projx-e2e",
552
+ target
553
+ );
554
+ } else if (type === "mobile") {
555
+ const target = isCanonical ? overrides?.mobile ?? `${nameSnake}_mobile` : toSnake(`${nameSnake}_${path}`);
556
+ await replaceInFile(
557
+ join2(dest, `${path}/pubspec.yaml`),
558
+ "projx_mobile",
559
+ target
560
+ );
561
+ await replaceInDir(
562
+ join2(dest, path),
563
+ "package:projx_mobile/",
564
+ `package:${target}/`,
565
+ ".dart"
566
+ );
436
567
  }
437
568
  }
438
569
  async function detectPackageNameOverrides(cwd, components, componentPaths) {
@@ -484,7 +615,7 @@ async function readPubspecName(file) {
484
615
  return null;
485
616
  }
486
617
  }
487
- async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip, applyDefaults = false) {
618
+ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip, applyDefaults = false, extraInstances = [], instancesToScaffold) {
488
619
  const hasHead = (() => {
489
620
  try {
490
621
  execSync("git rev-parse HEAD", { cwd, stdio: "pipe" });
@@ -494,24 +625,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
494
625
  }
495
626
  })();
496
627
  if (!hasHead) {
497
- await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, {
498
- componentSkips,
499
- rootSkip,
500
- applyDefaults,
501
- realCwd: cwd
502
- });
628
+ await writeTemplateToDir(
629
+ cwd,
630
+ repoDir,
631
+ components,
632
+ componentPaths,
633
+ vars,
634
+ version,
635
+ {
636
+ componentSkips,
637
+ rootSkip,
638
+ applyDefaults,
639
+ realCwd: cwd,
640
+ extraInstances,
641
+ instancesToScaffold
642
+ }
643
+ );
503
644
  return { status: "clean" };
504
645
  }
505
646
  const { worktree, branch } = createOrphanWorktree(cwd);
506
647
  try {
507
- await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, {
508
- componentSkips,
509
- rootSkip,
510
- applyDefaults,
511
- realCwd: cwd
512
- });
648
+ await writeTemplateToDir(
649
+ worktree,
650
+ repoDir,
651
+ components,
652
+ componentPaths,
653
+ vars,
654
+ version,
655
+ {
656
+ componentSkips,
657
+ rootSkip,
658
+ applyDefaults,
659
+ realCwd: cwd,
660
+ extraInstances,
661
+ instancesToScaffold
662
+ }
663
+ );
513
664
  execSync("git add -A", { cwd: worktree, stdio: "pipe" });
514
- const diff = execSync("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
665
+ const diff = execSync("git diff --cached --stat", {
666
+ cwd: worktree,
667
+ stdio: "pipe"
668
+ }).toString().trim();
515
669
  if (!diff) {
516
670
  cleanupWorktree(cwd, worktree, branch);
517
671
  return { status: "clean" };
@@ -521,7 +675,10 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
521
675
  { cwd: worktree, stdio: "pipe" }
522
676
  );
523
677
  try {
524
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
678
+ execSync(`git worktree remove "${worktree}" --force`, {
679
+ cwd,
680
+ stdio: "pipe"
681
+ });
525
682
  } catch {
526
683
  try {
527
684
  await rm(worktree, { recursive: true, force: true });
@@ -547,7 +704,12 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
547
704
  } catch {
548
705
  }
549
706
  if (mergeClean) {
550
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
707
+ await migrateComponentMarkers(
708
+ cwd,
709
+ components,
710
+ componentPaths,
711
+ applyDefaults
712
+ );
551
713
  saveBaselineRef(cwd);
552
714
  return { status: "clean" };
553
715
  }
@@ -555,32 +717,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
555
717
  if (baselineRef) {
556
718
  const tmpTemplate = join2(tmpdir(), `projx-tpl-${Date.now()}`);
557
719
  await mkdir(tmpTemplate, { recursive: true });
558
- await writeTemplateToDir(tmpTemplate, repoDir, components, componentPaths, vars, version, {
559
- componentSkips,
560
- rootSkip,
561
- applyDefaults,
562
- realCwd: cwd
563
- });
564
- const result = await tryThreeWayMerge(cwd, tmpTemplate, baselineRef, componentPaths);
720
+ await writeTemplateToDir(
721
+ tmpTemplate,
722
+ repoDir,
723
+ components,
724
+ componentPaths,
725
+ vars,
726
+ version,
727
+ {
728
+ componentSkips,
729
+ rootSkip,
730
+ applyDefaults,
731
+ realCwd: cwd,
732
+ extraInstances,
733
+ instancesToScaffold
734
+ }
735
+ );
736
+ const result = await tryThreeWayMerge(
737
+ cwd,
738
+ tmpTemplate,
739
+ baselineRef,
740
+ componentPaths
741
+ );
565
742
  await rm(tmpTemplate, { recursive: true, force: true });
566
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
743
+ await migrateComponentMarkers(
744
+ cwd,
745
+ components,
746
+ componentPaths,
747
+ applyDefaults
748
+ );
567
749
  if (result.conflicted.length === 0) {
568
750
  await writeManagedProjx(cwd, version, vars, applyDefaults);
569
751
  execSync("git add -A", { cwd, stdio: "pipe" });
570
- const staged = execSync("git diff --cached --stat", { cwd, stdio: "pipe" }).toString().trim();
752
+ const staged = execSync("git diff --cached --stat", {
753
+ cwd,
754
+ stdio: "pipe"
755
+ }).toString().trim();
571
756
  if (staged) {
572
- try {
573
- execSync(
574
- `git commit -m "projx: update to template v${version} (3-way merge)"`,
575
- { cwd, stdio: "pipe" }
576
- );
577
- } catch (err) {
578
- throw new Error(
579
- `Pre-commit hook rejected the merged template content. Resolve the issues and commit manually:
580
- git commit -m "projx: update to template v${version} (3-way merge)"`,
581
- { cause: err }
582
- );
583
- }
757
+ execSync(
758
+ `git -c core.hooksPath=/dev/null commit -m "projx: update to template v${version} (3-way merge)"`,
759
+ { cwd, stdio: "pipe" }
760
+ );
584
761
  }
585
762
  saveBaselineRef(cwd);
586
763
  return result.merged.length > 0 ? { status: "merged", mergedFiles: result.merged } : { status: "clean" };
@@ -609,13 +786,28 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
609
786
  conflictedFiles: result.conflicted
610
787
  };
611
788
  }
612
- await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, {
613
- componentSkips,
614
- rootSkip,
615
- applyDefaults,
616
- realCwd: cwd
617
- });
618
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
789
+ await writeTemplateToDir(
790
+ cwd,
791
+ repoDir,
792
+ components,
793
+ componentPaths,
794
+ vars,
795
+ version,
796
+ {
797
+ componentSkips,
798
+ rootSkip,
799
+ applyDefaults,
800
+ realCwd: cwd,
801
+ extraInstances,
802
+ instancesToScaffold
803
+ }
804
+ );
805
+ await migrateComponentMarkers(
806
+ cwd,
807
+ components,
808
+ componentPaths,
809
+ applyDefaults
810
+ );
619
811
  return { status: "conflicts" };
620
812
  } catch (err) {
621
813
  cleanupWorktree(cwd, worktree, branch);
@@ -624,8 +816,6 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
624
816
  }
625
817
 
626
818
  export {
627
- buildPathsUpper,
628
- buildDisplayNames,
629
819
  BASELINE_REF,
630
820
  matchesSkip,
631
821
  saveBaselineRef,