create-projx 1.6.1 → 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.
@@ -6,17 +6,25 @@ import {
6
6
  readComponentMarker,
7
7
  readProjxConfig,
8
8
  render,
9
+ renderEjsInDir,
9
10
  replaceInDir,
10
11
  replaceInFile,
11
12
  sharedTemplateDir,
12
13
  toSnake,
13
14
  upsertComponentMarker,
14
15
  writeProjxConfig
15
- } from "./chunk-FTHX7ILT.js";
16
+ } from "./chunk-LYPPFXGK.js";
16
17
 
17
18
  // src/baseline.ts
18
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
19
- 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";
20
28
  import { execSync } from "child_process";
21
29
  import { join as join2, dirname } from "path";
22
30
  import { tmpdir } from "os";
@@ -24,30 +32,57 @@ import { tmpdir } from "os";
24
32
  // src/generators/index.ts
25
33
  import { readFile } from "fs/promises";
26
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
+ }
27
65
  async function renderShared(filename, vars) {
28
- const tpl = await readFile(
29
- join(sharedTemplateDir(), filename),
30
- "utf-8"
31
- );
66
+ const tpl = await readFile(join(sharedTemplateDir(), filename), "utf-8");
32
67
  return render(tpl, vars);
33
68
  }
34
69
  async function generateDockerCompose(vars) {
35
- return renderShared("docker-compose.yml.ejs", vars);
70
+ return renderShared("docker-compose.yml.ejs", withInstances(vars));
36
71
  }
37
72
  async function generateDockerComposeDev(vars) {
38
- return renderShared("docker-compose.dev.yml.ejs", vars);
73
+ return renderShared("docker-compose.dev.yml.ejs", withInstances(vars));
39
74
  }
40
75
  async function generatePreCommit(vars) {
41
- return renderShared("pre-commit.ejs", vars);
76
+ return renderShared("pre-commit.ejs", withInstances(vars));
42
77
  }
43
78
  async function generateSetupSh(vars) {
44
- return renderShared("setup.sh.ejs", vars);
79
+ return renderShared("setup.sh.ejs", withInstances(vars));
45
80
  }
46
81
  async function generateCiYml(vars) {
47
- return renderShared("ci.yml.ejs", vars);
82
+ return renderShared("ci.yml.ejs", withInstances(vars));
48
83
  }
49
84
  async function generateReadme(vars) {
50
- return renderShared("README.md.ejs", vars);
85
+ return renderShared("README.md.ejs", withInstances(vars));
51
86
  }
52
87
  function generateVscodeSettings(vars) {
53
88
  const settings = {};
@@ -57,9 +92,15 @@ function generateVscodeSettings(vars) {
57
92
  "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
58
93
  };
59
94
  }
60
- settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
61
- settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
62
- 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
+ };
63
104
  settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
64
105
  settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
65
106
  settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
@@ -81,32 +122,9 @@ function generateVscodeSettings(vars) {
81
122
  }
82
123
 
83
124
  // src/baseline.ts
84
- function buildPathsUpper(paths) {
85
- const result = {};
86
- for (const [component, dir] of Object.entries(paths)) {
87
- result[component] = dir.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
88
- }
89
- return result;
90
- }
91
- var CANONICAL_DISPLAY_NAMES = {
92
- fastapi: "FastAPI",
93
- fastify: "Fastify",
94
- frontend: "Frontend",
95
- mobile: "Flutter",
96
- e2e: "E2E",
97
- infra: "Terraform"
98
- };
99
- function buildDisplayNames(paths) {
100
- const result = {};
101
- for (const [component, dir] of Object.entries(paths)) {
102
- const canonical = component;
103
- result[canonical] = dir === canonical ? CANONICAL_DISPLAY_NAMES[canonical] : dir;
104
- }
105
- return result;
106
- }
107
125
  var BASELINE_REF = "refs/projx/baseline";
108
126
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
109
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-OOY5OZDX.js");
127
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-BXHJP6HF.js");
110
128
  for (const component of components) {
111
129
  const dir = componentPaths[component];
112
130
  const markerDir = join2(cwd, dir);
@@ -174,11 +192,17 @@ function saveBaselineRef(cwd) {
174
192
  }
175
193
  function getBaselineRef(cwd) {
176
194
  try {
177
- 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();
178
199
  } catch {
179
200
  }
180
201
  try {
181
- 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();
182
206
  if (sha) return sha;
183
207
  } catch {
184
208
  }
@@ -186,7 +210,10 @@ function getBaselineRef(cwd) {
186
210
  }
187
211
  function getFileAtRef(cwd, ref, filePath) {
188
212
  try {
189
- return execSync(`git show ${ref}:"${filePath}"`, { cwd, stdio: "pipe" }).toString();
213
+ return execSync(`git show ${ref}:"${filePath}"`, {
214
+ cwd,
215
+ stdio: "pipe"
216
+ }).toString();
190
217
  } catch {
191
218
  return null;
192
219
  }
@@ -257,15 +284,22 @@ async function tryThreeWayMerge(cwd, templateDir, baselineRef, componentPaths) {
257
284
  const pathFallbacks = buildPathFallbacks(componentPaths);
258
285
  for (const file of templateFiles) {
259
286
  if (file === ".projx") continue;
260
- if (file.endsWith("/.projx-component") || file === ".projx-component") continue;
287
+ const isMarker = file.endsWith("/.projx-component") || file === ".projx-component";
261
288
  const oursPath = join2(cwd, file);
289
+ if (isMarker && existsSync(oursPath)) continue;
262
290
  if (!existsSync(oursPath)) {
263
291
  await mkdir(dirname(oursPath), { recursive: true });
264
292
  await copyFile(join2(templateDir, file), oursPath);
265
293
  merged.push(file);
266
294
  continue;
267
295
  }
268
- const baseContent = lookupBaseContent(cwd, baselineRef, file, pathFallbacks);
296
+ if (isMarker) continue;
297
+ const baseContent = lookupBaseContent(
298
+ cwd,
299
+ baselineRef,
300
+ file,
301
+ pathFallbacks
302
+ );
269
303
  if (baseContent === null) continue;
270
304
  let theirsContent;
271
305
  try {
@@ -306,7 +340,10 @@ function createOrphanWorktree(cwd) {
306
340
  }
307
341
  function cleanupWorktree(cwd, worktree, branch) {
308
342
  try {
309
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
343
+ execSync(`git worktree remove "${worktree}" --force`, {
344
+ cwd,
345
+ stdio: "pipe"
346
+ });
310
347
  } catch {
311
348
  try {
312
349
  rm(worktree, { recursive: true, force: true });
@@ -329,8 +366,11 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
329
366
  const rel = full.slice(base.length + 1);
330
367
  if (entry.isDirectory()) {
331
368
  await walk(full, base);
332
- } else if (entry.name !== ".projx-component" && matchesSkip(rel, skipPatterns)) {
333
- if (realDir && !existsSync(join2(realDir, rel))) continue;
369
+ } else if (entry.name !== ".projx-component") {
370
+ const targetRel = rel.endsWith(".ejs") ? rel.slice(0, -".ejs".length) : rel;
371
+ if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns))
372
+ continue;
373
+ if (realDir && !existsSync(join2(realDir, targetRel))) continue;
334
374
  await unlink(full);
335
375
  }
336
376
  }
@@ -338,40 +378,35 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
338
378
  await walk(dir, dir);
339
379
  }
340
380
  async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, options = {}) {
341
- 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;
342
389
  const name = vars.projectName;
343
390
  const nameSnake = toSnake(name);
344
- for (const component of components) {
345
- const targetDir = componentPaths[component];
346
- const baseSkip = componentSkips?.[component] ?? [];
347
- const realMarker = await readComponentMarker(join2(realCwd, targetDir));
348
- const isNewMarker = !realMarker;
349
- const shouldApplyComponentDefault = isNewMarker || applyDefaults;
350
- const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[component] ?? [] : [];
351
- const skipPatterns = [.../* @__PURE__ */ new Set([...baseSkip, ...defaultSkip])];
352
- const tmpDir = join2(dest, "__cptmp__");
353
- await copyComponent(repoDir, component, tmpDir);
354
- const srcDir = join2(tmpDir, component);
355
- if (skipPatterns.length > 0) {
356
- const realComponentDir = join2(realCwd, targetDir);
357
- await removeSkippedFiles(srcDir, skipPatterns, realComponentDir);
358
- }
359
- const outDir = join2(dest, targetDir);
360
- await mkdir(outDir, { recursive: true });
361
- const { cp } = await import("fs/promises");
362
- if (existsSync(srcDir)) {
363
- await cp(srcDir, outDir, { recursive: true, force: true });
364
- }
365
- await rm(tmpDir, { recursive: true, force: true });
366
- await upsertComponentMarker(join2(dest, targetDir), component, skipPatterns.length > 0 ? skipPatterns : void 0);
367
- }
368
- if (!vars.pathsUpper) {
369
- vars.pathsUpper = buildPathsUpper(componentPaths);
370
- }
371
- if (!vars.displayNames) {
372
- 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
+ });
373
409
  }
374
- await substituteNames(dest, components, componentPaths, name, nameSnake, vars.nameOverrides);
375
410
  const hasBackend = components.includes("fastapi") || components.includes("fastify");
376
411
  const userSkip = rootSkip ?? [];
377
412
  const defaultRootSkip = applyDefaults ? DEFAULT_ROOT_SKIP_PATTERNS : [];
@@ -382,20 +417,32 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
382
417
  };
383
418
  if (hasBackend || components.includes("frontend")) {
384
419
  if (shouldWrite("docker-compose.yml"))
385
- await writeFile(join2(dest, "docker-compose.yml"), await generateDockerCompose(vars));
420
+ await writeFile(
421
+ join2(dest, "docker-compose.yml"),
422
+ await generateDockerCompose(vars)
423
+ );
386
424
  if (shouldWrite("docker-compose.dev.yml"))
387
- 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
+ );
388
429
  }
389
430
  if (shouldWrite("README.md"))
390
431
  await writeFile(join2(dest, "README.md"), await generateReadme(vars));
391
432
  if (shouldWrite(".githooks/pre-commit")) {
392
433
  await mkdir(join2(dest, ".githooks"), { recursive: true });
393
- await writeFile(join2(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
434
+ await writeFile(
435
+ join2(dest, ".githooks/pre-commit"),
436
+ await generatePreCommit(vars)
437
+ );
394
438
  await chmod(join2(dest, ".githooks/pre-commit"), 493);
395
439
  }
396
440
  if (shouldWrite(".github/workflows/ci.yml")) {
397
441
  await mkdir(join2(dest, ".github/workflows"), { recursive: true });
398
- 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
+ );
399
446
  }
400
447
  if (shouldWrite("setup.sh")) {
401
448
  await writeFile(join2(dest, "setup.sh"), await generateSetupSh(vars));
@@ -404,31 +451,68 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
404
451
  await copyStaticFiles(repoDir, dest);
405
452
  if (shouldWrite(".vscode/settings.json")) {
406
453
  await mkdir(join2(dest, ".vscode"), { recursive: true });
407
- await writeFile(join2(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
454
+ await writeFile(
455
+ join2(dest, ".vscode/settings.json"),
456
+ generateVscodeSettings(vars)
457
+ );
408
458
  }
409
459
  await writeManagedProjx(dest, version, vars, applyDefaults);
410
460
  }
411
- async function substituteNames(dest, components, paths, name, nameSnake, overrides) {
412
- if (components.includes("fastapi")) {
413
- const target = overrides?.fastapi ?? `${name}-fastapi`;
414
- await replaceInFile(join2(dest, `${paths.fastapi}/pyproject.toml`), "projx-fastapi", target);
415
- }
416
- if (components.includes("fastify")) {
417
- const target = overrides?.fastify ?? `${name}-fastify`;
418
- await replaceInFile(join2(dest, `${paths.fastify}/package.json`), "projx-fastify", target);
419
- }
420
- if (components.includes("frontend")) {
421
- const target = overrides?.frontend ?? `${name}-frontend`;
422
- await replaceInFile(join2(dest, `${paths.frontend}/package.json`), "projx-frontend", target);
423
- }
424
- if (components.includes("e2e")) {
425
- const target = overrides?.e2e ?? `${name}-e2e`;
426
- await replaceInFile(join2(dest, `${paths.e2e}/package.json`), "projx-e2e", target);
427
- }
428
- if (components.includes("mobile")) {
429
- const target = overrides?.mobile ?? `${nameSnake}_mobile`;
430
- await replaceInFile(join2(dest, `${paths.mobile}/pubspec.yaml`), "projx_mobile", target);
431
- 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
+ );
432
516
  }
433
517
  }
434
518
  async function detectPackageNameOverrides(cwd, components, componentPaths) {
@@ -480,7 +564,7 @@ async function readPubspecName(file) {
480
564
  return null;
481
565
  }
482
566
  }
483
- 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) {
484
568
  const hasHead = (() => {
485
569
  try {
486
570
  execSync("git rev-parse HEAD", { cwd, stdio: "pipe" });
@@ -490,24 +574,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
490
574
  }
491
575
  })();
492
576
  if (!hasHead) {
493
- await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, {
494
- componentSkips,
495
- rootSkip,
496
- applyDefaults,
497
- realCwd: cwd
498
- });
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
+ );
499
593
  return { status: "clean" };
500
594
  }
501
595
  const { worktree, branch } = createOrphanWorktree(cwd);
502
596
  try {
503
- await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, {
504
- componentSkips,
505
- rootSkip,
506
- applyDefaults,
507
- realCwd: cwd
508
- });
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
+ );
509
613
  execSync("git add -A", { cwd: worktree, stdio: "pipe" });
510
- 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();
511
618
  if (!diff) {
512
619
  cleanupWorktree(cwd, worktree, branch);
513
620
  return { status: "clean" };
@@ -517,7 +624,10 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
517
624
  { cwd: worktree, stdio: "pipe" }
518
625
  );
519
626
  try {
520
- execSync(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
627
+ execSync(`git worktree remove "${worktree}" --force`, {
628
+ cwd,
629
+ stdio: "pipe"
630
+ });
521
631
  } catch {
522
632
  try {
523
633
  await rm(worktree, { recursive: true, force: true });
@@ -543,7 +653,12 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
543
653
  } catch {
544
654
  }
545
655
  if (mergeClean) {
546
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
656
+ await migrateComponentMarkers(
657
+ cwd,
658
+ components,
659
+ componentPaths,
660
+ applyDefaults
661
+ );
547
662
  saveBaselineRef(cwd);
548
663
  return { status: "clean" };
549
664
  }
@@ -551,32 +666,47 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
551
666
  if (baselineRef) {
552
667
  const tmpTemplate = join2(tmpdir(), `projx-tpl-${Date.now()}`);
553
668
  await mkdir(tmpTemplate, { recursive: true });
554
- await writeTemplateToDir(tmpTemplate, repoDir, components, componentPaths, vars, version, {
555
- componentSkips,
556
- rootSkip,
557
- applyDefaults,
558
- realCwd: cwd
559
- });
560
- 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
+ );
561
691
  await rm(tmpTemplate, { recursive: true, force: true });
562
- await migrateComponentMarkers(cwd, components, componentPaths, applyDefaults);
692
+ await migrateComponentMarkers(
693
+ cwd,
694
+ components,
695
+ componentPaths,
696
+ applyDefaults
697
+ );
563
698
  if (result.conflicted.length === 0) {
564
699
  await writeManagedProjx(cwd, version, vars, applyDefaults);
565
700
  execSync("git add -A", { cwd, stdio: "pipe" });
566
- 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();
567
705
  if (staged) {
568
- try {
569
- execSync(
570
- `git commit -m "projx: update to template v${version} (3-way merge)"`,
571
- { cwd, stdio: "pipe" }
572
- );
573
- } catch (err) {
574
- throw new Error(
575
- `Pre-commit hook rejected the merged template content. Resolve the issues and commit manually:
576
- git commit -m "projx: update to template v${version} (3-way merge)"`,
577
- { cause: err }
578
- );
579
- }
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
+ );
580
710
  }
581
711
  saveBaselineRef(cwd);
582
712
  return result.merged.length > 0 ? { status: "merged", mergedFiles: result.merged } : { status: "clean" };
@@ -605,13 +735,28 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
605
735
  conflictedFiles: result.conflicted
606
736
  };
607
737
  }
608
- await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, {
609
- componentSkips,
610
- rootSkip,
611
- applyDefaults,
612
- realCwd: cwd
613
- });
614
- 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
+ );
615
760
  return { status: "conflicts" };
616
761
  } catch (err) {
617
762
  cleanupWorktree(cwd, worktree, branch);
@@ -620,8 +765,6 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
620
765
  }
621
766
 
622
767
  export {
623
- buildPathsUpper,
624
- buildDisplayNames,
625
768
  BASELINE_REF,
626
769
  matchesSkip,
627
770
  saveBaselineRef,