create-projx 1.6.2 → 1.6.3

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-LYPPFXGK.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,57 @@ 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) => ({ type, path: vars.paths[type] ?? type }));
48
+ const enriched = base.map((inst) => ({
49
+ ...inst,
50
+ upper: shellSafeUpper(inst.path),
51
+ display: inst.path === inst.type ? CANONICAL_DISPLAY[inst.type] : inst.path
52
+ }));
53
+ const byType = (type) => enriched.filter((i) => i.type === type).sort((a, b) => a.path.localeCompare(b.path));
54
+ return {
55
+ ...vars,
56
+ instances: enriched,
57
+ fastapiInstances: byType("fastapi"),
58
+ fastifyInstances: byType("fastify"),
59
+ frontendInstances: byType("frontend"),
60
+ mobileInstances: byType("mobile"),
61
+ e2eInstances: byType("e2e"),
62
+ infraInstances: byType("infra")
63
+ };
64
+ }
28
65
  async function renderShared(filename, vars) {
29
- const tpl = await readFile(
30
- join(sharedTemplateDir(), filename),
31
- "utf-8"
32
- );
66
+ const tpl = await readFile(join(sharedTemplateDir(), filename), "utf-8");
33
67
  return render(tpl, vars);
34
68
  }
35
69
  async function generateDockerCompose(vars) {
36
- return renderShared("docker-compose.yml.ejs", vars);
70
+ return renderShared("docker-compose.yml.ejs", withInstances(vars));
37
71
  }
38
72
  async function generateDockerComposeDev(vars) {
39
- return renderShared("docker-compose.dev.yml.ejs", vars);
73
+ return renderShared("docker-compose.dev.yml.ejs", withInstances(vars));
40
74
  }
41
75
  async function generatePreCommit(vars) {
42
- return renderShared("pre-commit.ejs", vars);
76
+ return renderShared("pre-commit.ejs", withInstances(vars));
43
77
  }
44
78
  async function generateSetupSh(vars) {
45
- return renderShared("setup.sh.ejs", vars);
79
+ return renderShared("setup.sh.ejs", withInstances(vars));
46
80
  }
47
81
  async function generateCiYml(vars) {
48
- return renderShared("ci.yml.ejs", vars);
82
+ return renderShared("ci.yml.ejs", withInstances(vars));
49
83
  }
50
84
  async function generateReadme(vars) {
51
- return renderShared("README.md.ejs", vars);
85
+ return renderShared("README.md.ejs", withInstances(vars));
52
86
  }
53
87
  function generateVscodeSettings(vars) {
54
88
  const settings = {};
@@ -58,9 +92,15 @@ function generateVscodeSettings(vars) {
58
92
  "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
59
93
  };
60
94
  }
61
- settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
62
- settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
63
- settings["[javascript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
95
+ settings["[typescript]"] = {
96
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
97
+ };
98
+ settings["[typescriptreact]"] = {
99
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
100
+ };
101
+ settings["[javascript]"] = {
102
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
103
+ };
64
104
  settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
65
105
  settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
66
106
  settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
@@ -82,32 +122,9 @@ function generateVscodeSettings(vars) {
82
122
  }
83
123
 
84
124
  // 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
125
  var BASELINE_REF = "refs/projx/baseline";
109
126
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
110
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-VY5BBJBQ.js");
127
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-BXHJP6HF.js");
111
128
  for (const component of components) {
112
129
  const dir = componentPaths[component];
113
130
  const markerDir = join2(cwd, dir);
@@ -175,11 +192,17 @@ function saveBaselineRef(cwd) {
175
192
  }
176
193
  function getBaselineRef(cwd) {
177
194
  try {
178
- return execSync(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
195
+ return execSync(`git rev-parse --verify ${BASELINE_REF}`, {
196
+ cwd,
197
+ stdio: "pipe"
198
+ }).toString().trim();
179
199
  } catch {
180
200
  }
181
201
  try {
182
- const sha = execSync("git log -1 --format=%H -- .projx", { cwd, stdio: "pipe" }).toString().trim();
202
+ const sha = execSync("git log -1 --format=%H -- .projx", {
203
+ cwd,
204
+ stdio: "pipe"
205
+ }).toString().trim();
183
206
  if (sha) return sha;
184
207
  } catch {
185
208
  }
@@ -187,7 +210,10 @@ function getBaselineRef(cwd) {
187
210
  }
188
211
  function getFileAtRef(cwd, ref, filePath) {
189
212
  try {
190
- return execSync(`git show ${ref}:"${filePath}"`, { cwd, stdio: "pipe" }).toString();
213
+ return execSync(`git show ${ref}:"${filePath}"`, {
214
+ cwd,
215
+ stdio: "pipe"
216
+ }).toString();
191
217
  } catch {
192
218
  return null;
193
219
  }
@@ -258,15 +284,22 @@ async function tryThreeWayMerge(cwd, templateDir, baselineRef, componentPaths) {
258
284
  const pathFallbacks = buildPathFallbacks(componentPaths);
259
285
  for (const file of templateFiles) {
260
286
  if (file === ".projx") continue;
261
- if (file.endsWith("/.projx-component") || file === ".projx-component") continue;
287
+ const isMarker = file.endsWith("/.projx-component") || file === ".projx-component";
262
288
  const oursPath = join2(cwd, file);
289
+ if (isMarker && existsSync(oursPath)) continue;
263
290
  if (!existsSync(oursPath)) {
264
291
  await mkdir(dirname(oursPath), { recursive: true });
265
292
  await copyFile(join2(templateDir, file), oursPath);
266
293
  merged.push(file);
267
294
  continue;
268
295
  }
269
- const baseContent = lookupBaseContent(cwd, baselineRef, file, pathFallbacks);
296
+ if (isMarker) continue;
297
+ const baseContent = lookupBaseContent(
298
+ cwd,
299
+ baselineRef,
300
+ file,
301
+ pathFallbacks
302
+ );
270
303
  if (baseContent === null) continue;
271
304
  let theirsContent;
272
305
  try {
@@ -307,7 +340,10 @@ function createOrphanWorktree(cwd) {
307
340
  }
308
341
  function cleanupWorktree(cwd, worktree, branch) {
309
342
  try {
310
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
343
+ execSync(`git worktree remove "${worktree}" --force`, {
344
+ cwd,
345
+ stdio: "pipe"
346
+ });
311
347
  } catch {
312
348
  try {
313
349
  rm(worktree, { recursive: true, force: true });
@@ -332,7 +368,8 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
332
368
  await walk(full, base);
333
369
  } else if (entry.name !== ".projx-component") {
334
370
  const targetRel = rel.endsWith(".ejs") ? rel.slice(0, -".ejs".length) : rel;
335
- if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns)) continue;
371
+ if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns))
372
+ continue;
336
373
  if (realDir && !existsSync(join2(realDir, targetRel))) continue;
337
374
  await unlink(full);
338
375
  }
@@ -341,41 +378,35 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
341
378
  await walk(dir, dir);
342
379
  }
343
380
  async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, options = {}) {
344
- const { componentSkips, rootSkip, applyDefaults = false, realCwd = dest } = options;
381
+ const {
382
+ componentSkips,
383
+ rootSkip,
384
+ applyDefaults = false,
385
+ realCwd = dest,
386
+ extraInstances = [],
387
+ instancesToScaffold
388
+ } = options;
345
389
  const name = vars.projectName;
346
390
  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);
391
+ const primaryInstances = components.map((type) => ({
392
+ type,
393
+ path: componentPaths[type]
394
+ }));
395
+ const allInstances = [...primaryInstances, ...extraInstances];
396
+ const toScaffold = instancesToScaffold ?? allInstances;
397
+ for (const inst of toScaffold) {
398
+ await writeOneInstance(inst, {
399
+ dest,
400
+ repoDir,
401
+ vars,
402
+ componentPaths,
403
+ realCwd,
404
+ applyDefaults,
405
+ baseSkip: componentSkips?.[inst.type] ?? [],
406
+ projectName: name,
407
+ nameSnake
408
+ });
377
409
  }
378
- await substituteNames(dest, components, componentPaths, name, nameSnake, vars.nameOverrides);
379
410
  const hasBackend = components.includes("fastapi") || components.includes("fastify");
380
411
  const userSkip = rootSkip ?? [];
381
412
  const defaultRootSkip = applyDefaults ? DEFAULT_ROOT_SKIP_PATTERNS : [];
@@ -386,20 +417,32 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
386
417
  };
387
418
  if (hasBackend || components.includes("frontend")) {
388
419
  if (shouldWrite("docker-compose.yml"))
389
- await writeFile(join2(dest, "docker-compose.yml"), await generateDockerCompose(vars));
420
+ await writeFile(
421
+ join2(dest, "docker-compose.yml"),
422
+ await generateDockerCompose(vars)
423
+ );
390
424
  if (shouldWrite("docker-compose.dev.yml"))
391
- await writeFile(join2(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
425
+ await writeFile(
426
+ join2(dest, "docker-compose.dev.yml"),
427
+ await generateDockerComposeDev(vars)
428
+ );
392
429
  }
393
430
  if (shouldWrite("README.md"))
394
431
  await writeFile(join2(dest, "README.md"), await generateReadme(vars));
395
432
  if (shouldWrite(".githooks/pre-commit")) {
396
433
  await mkdir(join2(dest, ".githooks"), { recursive: true });
397
- await writeFile(join2(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
434
+ await writeFile(
435
+ join2(dest, ".githooks/pre-commit"),
436
+ await generatePreCommit(vars)
437
+ );
398
438
  await chmod(join2(dest, ".githooks/pre-commit"), 493);
399
439
  }
400
440
  if (shouldWrite(".github/workflows/ci.yml")) {
401
441
  await mkdir(join2(dest, ".github/workflows"), { recursive: true });
402
- await writeFile(join2(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
442
+ await writeFile(
443
+ join2(dest, ".github/workflows/ci.yml"),
444
+ await generateCiYml(vars)
445
+ );
403
446
  }
404
447
  if (shouldWrite("setup.sh")) {
405
448
  await writeFile(join2(dest, "setup.sh"), await generateSetupSh(vars));
@@ -408,31 +451,68 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
408
451
  await copyStaticFiles(repoDir, dest);
409
452
  if (shouldWrite(".vscode/settings.json")) {
410
453
  await mkdir(join2(dest, ".vscode"), { recursive: true });
411
- await writeFile(join2(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
454
+ await writeFile(
455
+ join2(dest, ".vscode/settings.json"),
456
+ generateVscodeSettings(vars)
457
+ );
412
458
  }
413
459
  await writeManagedProjx(dest, version, vars, applyDefaults);
414
460
  }
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");
461
+ async function writeOneInstance(inst, opts) {
462
+ const { dest, repoDir, vars, componentPaths, realCwd, applyDefaults, baseSkip, projectName, nameSnake } = opts;
463
+ const { type, path: targetDir } = inst;
464
+ const realMarker = await readComponentMarker(join2(realCwd, targetDir));
465
+ const isNewMarker = !realMarker;
466
+ const shouldApplyComponentDefault = isNewMarker || applyDefaults;
467
+ const markerSkip = realMarker?.skip ?? [];
468
+ const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[type] ?? [] : [];
469
+ const skipPatterns = [.../* @__PURE__ */ new Set([...baseSkip, ...markerSkip, ...defaultSkip])];
470
+ const tmpDir = join2(dest, "__cptmp__");
471
+ await copyComponent(repoDir, type, tmpDir);
472
+ const srcDir = join2(tmpDir, type);
473
+ if (skipPatterns.length > 0) {
474
+ await removeSkippedFiles(srcDir, skipPatterns, join2(realCwd, targetDir));
475
+ }
476
+ const outDir = join2(dest, targetDir);
477
+ await mkdir(outDir, { recursive: true });
478
+ const { cp } = await import("fs/promises");
479
+ if (existsSync(srcDir)) {
480
+ await cp(srcDir, outDir, { recursive: true, force: true });
481
+ }
482
+ await rm(tmpDir, { recursive: true, force: true });
483
+ const instancePaths = { ...componentPaths, [type]: targetDir };
484
+ await renderEjsInDir(outDir, { ...vars, paths: instancePaths });
485
+ await upsertComponentMarker(
486
+ join2(dest, targetDir),
487
+ type,
488
+ skipPatterns.length > 0 ? skipPatterns : void 0
489
+ );
490
+ await substituteNamesForInstance(inst, dest, projectName, nameSnake, vars.nameOverrides);
491
+ }
492
+ async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides) {
493
+ const { type, path } = inst;
494
+ const isCanonical = path === type;
495
+ if (type === "fastapi") {
496
+ const target = isCanonical ? overrides?.fastapi ?? `${name}-fastapi` : `${name}-${path}`;
497
+ await replaceInFile(join2(dest, `${path}/pyproject.toml`), "projx-fastapi", target);
498
+ } else if (type === "fastify") {
499
+ const target = isCanonical ? overrides?.fastify ?? `${name}-fastify` : `${name}-${path}`;
500
+ await replaceInFile(join2(dest, `${path}/package.json`), "projx-fastify", target);
501
+ } else if (type === "frontend") {
502
+ const target = isCanonical ? overrides?.frontend ?? `${name}-frontend` : `${name}-${path}`;
503
+ await replaceInFile(join2(dest, `${path}/package.json`), "projx-frontend", target);
504
+ } else if (type === "e2e") {
505
+ const target = isCanonical ? overrides?.e2e ?? `${name}-e2e` : `${name}-${path}`;
506
+ await replaceInFile(join2(dest, `${path}/package.json`), "projx-e2e", target);
507
+ } else if (type === "mobile") {
508
+ const target = isCanonical ? overrides?.mobile ?? `${nameSnake}_mobile` : toSnake(`${nameSnake}_${path}`);
509
+ await replaceInFile(join2(dest, `${path}/pubspec.yaml`), "projx_mobile", target);
510
+ await replaceInDir(
511
+ join2(dest, path),
512
+ "package:projx_mobile/",
513
+ `package:${target}/`,
514
+ ".dart"
515
+ );
436
516
  }
437
517
  }
438
518
  async function detectPackageNameOverrides(cwd, components, componentPaths) {
@@ -484,7 +564,7 @@ async function readPubspecName(file) {
484
564
  return null;
485
565
  }
486
566
  }
487
- async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip, applyDefaults = false) {
567
+ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip, applyDefaults = false, extraInstances = [], instancesToScaffold) {
488
568
  const hasHead = (() => {
489
569
  try {
490
570
  execSync("git rev-parse HEAD", { cwd, stdio: "pipe" });
@@ -494,24 +574,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
494
574
  }
495
575
  })();
496
576
  if (!hasHead) {
497
- await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, {
498
- componentSkips,
499
- rootSkip,
500
- applyDefaults,
501
- realCwd: cwd
502
- });
577
+ await writeTemplateToDir(
578
+ cwd,
579
+ repoDir,
580
+ components,
581
+ componentPaths,
582
+ vars,
583
+ version,
584
+ {
585
+ componentSkips,
586
+ rootSkip,
587
+ applyDefaults,
588
+ realCwd: cwd,
589
+ extraInstances,
590
+ instancesToScaffold
591
+ }
592
+ );
503
593
  return { status: "clean" };
504
594
  }
505
595
  const { worktree, branch } = createOrphanWorktree(cwd);
506
596
  try {
507
- await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, {
508
- componentSkips,
509
- rootSkip,
510
- applyDefaults,
511
- realCwd: cwd
512
- });
597
+ await writeTemplateToDir(
598
+ worktree,
599
+ repoDir,
600
+ components,
601
+ componentPaths,
602
+ vars,
603
+ version,
604
+ {
605
+ componentSkips,
606
+ rootSkip,
607
+ applyDefaults,
608
+ realCwd: cwd,
609
+ extraInstances,
610
+ instancesToScaffold
611
+ }
612
+ );
513
613
  execSync("git add -A", { cwd: worktree, stdio: "pipe" });
514
- const diff = execSync("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
614
+ const diff = execSync("git diff --cached --stat", {
615
+ cwd: worktree,
616
+ stdio: "pipe"
617
+ }).toString().trim();
515
618
  if (!diff) {
516
619
  cleanupWorktree(cwd, worktree, branch);
517
620
  return { status: "clean" };
@@ -521,7 +624,10 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
521
624
  { cwd: worktree, stdio: "pipe" }
522
625
  );
523
626
  try {
524
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
627
+ execSync(`git worktree remove "${worktree}" --force`, {
628
+ cwd,
629
+ stdio: "pipe"
630
+ });
525
631
  } catch {
526
632
  try {
527
633
  await rm(worktree, { recursive: true, force: true });
@@ -547,7 +653,12 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
547
653
  } catch {
548
654
  }
549
655
  if (mergeClean) {
550
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
656
+ await migrateComponentMarkers(
657
+ cwd,
658
+ components,
659
+ componentPaths,
660
+ applyDefaults
661
+ );
551
662
  saveBaselineRef(cwd);
552
663
  return { status: "clean" };
553
664
  }
@@ -555,32 +666,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
555
666
  if (baselineRef) {
556
667
  const tmpTemplate = join2(tmpdir(), `projx-tpl-${Date.now()}`);
557
668
  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);
669
+ await writeTemplateToDir(
670
+ tmpTemplate,
671
+ repoDir,
672
+ components,
673
+ componentPaths,
674
+ vars,
675
+ version,
676
+ {
677
+ componentSkips,
678
+ rootSkip,
679
+ applyDefaults,
680
+ realCwd: cwd,
681
+ extraInstances,
682
+ instancesToScaffold
683
+ }
684
+ );
685
+ const result = await tryThreeWayMerge(
686
+ cwd,
687
+ tmpTemplate,
688
+ baselineRef,
689
+ componentPaths
690
+ );
565
691
  await rm(tmpTemplate, { recursive: true, force: true });
566
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
692
+ await migrateComponentMarkers(
693
+ cwd,
694
+ components,
695
+ componentPaths,
696
+ applyDefaults
697
+ );
567
698
  if (result.conflicted.length === 0) {
568
699
  await writeManagedProjx(cwd, version, vars, applyDefaults);
569
700
  execSync("git add -A", { cwd, stdio: "pipe" });
570
- const staged = execSync("git diff --cached --stat", { cwd, stdio: "pipe" }).toString().trim();
701
+ const staged = execSync("git diff --cached --stat", {
702
+ cwd,
703
+ stdio: "pipe"
704
+ }).toString().trim();
571
705
  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
- }
706
+ execSync(
707
+ `git -c core.hooksPath=/dev/null commit -m "projx: update to template v${version} (3-way merge)"`,
708
+ { cwd, stdio: "pipe" }
709
+ );
584
710
  }
585
711
  saveBaselineRef(cwd);
586
712
  return result.merged.length > 0 ? { status: "merged", mergedFiles: result.merged } : { status: "clean" };
@@ -609,13 +735,28 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
609
735
  conflictedFiles: result.conflicted
610
736
  };
611
737
  }
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);
738
+ await writeTemplateToDir(
739
+ cwd,
740
+ repoDir,
741
+ components,
742
+ componentPaths,
743
+ vars,
744
+ version,
745
+ {
746
+ componentSkips,
747
+ rootSkip,
748
+ applyDefaults,
749
+ realCwd: cwd,
750
+ extraInstances,
751
+ instancesToScaffold
752
+ }
753
+ );
754
+ await migrateComponentMarkers(
755
+ cwd,
756
+ components,
757
+ componentPaths,
758
+ applyDefaults
759
+ );
619
760
  return { status: "conflicts" };
620
761
  } catch (err) {
621
762
  cleanupWorktree(cwd, worktree, branch);
@@ -624,8 +765,6 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
624
765
  }
625
766
 
626
767
  export {
627
- buildPathsUpper,
628
- buildDisplayNames,
629
768
  BASELINE_REF,
630
769
  matchesSkip,
631
770
  saveBaselineRef,