create-projx 1.0.1 → 1.1.1

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/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 existsSync5 } from "fs";
4
+ import { existsSync as existsSync7 } from "fs";
5
5
  import { resolve as resolve2 } from "path";
6
6
 
7
7
  // src/utils.ts
@@ -122,7 +122,7 @@ async function copyComponent(repoDir, component, dest) {
122
122
  async function copyStaticFiles(repoDir, dest) {
123
123
  const manifest = [];
124
124
  const tpl = repoDir;
125
- const statics = [".editorconfig", "LICENSE"];
125
+ const statics = [".editorconfig"];
126
126
  for (const file of statics) {
127
127
  const src = join(tpl, file);
128
128
  if (existsSync(src)) {
@@ -135,10 +135,11 @@ async function copyStaticFiles(repoDir, dest) {
135
135
  await cp(gitignore, join(dest, ".gitignore"));
136
136
  manifest.push(".gitignore");
137
137
  }
138
- const vscode = join(tpl, ".vscode");
139
- if (existsSync(vscode)) {
140
- await cp(vscode, join(dest, ".vscode"), { recursive: true });
141
- manifest.push(".vscode/settings.json", ".vscode/extensions.json");
138
+ const extensionsJson = join(tpl, ".vscode/extensions.json");
139
+ if (existsSync(extensionsJson)) {
140
+ await mkdir(join(dest, ".vscode"), { recursive: true });
141
+ await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
142
+ manifest.push(".vscode/extensions.json");
142
143
  }
143
144
  const scripts = join(tpl, "scripts");
144
145
  if (existsSync(scripts)) {
@@ -176,6 +177,65 @@ async function replaceInDir(dir, find, replace, ext) {
176
177
  }
177
178
  }
178
179
  }
180
+ var COMPONENT_MARKER = ".projx-component";
181
+ async function readFileOrNull(path) {
182
+ try {
183
+ return await readFile(path, "utf-8");
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ async function writeComponentMarker(dir, component) {
189
+ const markerPath = join(dir, COMPONENT_MARKER);
190
+ let components = [component];
191
+ const existing = await readFileOrNull(markerPath);
192
+ if (existing) {
193
+ try {
194
+ const data = JSON.parse(existing);
195
+ const prev = data.components ?? (data.component ? [data.component] : []);
196
+ if (!prev.includes(component)) {
197
+ components = [...prev, component];
198
+ } else {
199
+ return;
200
+ }
201
+ } catch {
202
+ }
203
+ }
204
+ await writeFile(
205
+ markerPath,
206
+ JSON.stringify({ components }, null, 2) + "\n"
207
+ );
208
+ }
209
+ async function discoverComponentPaths(cwd, components) {
210
+ const paths = {};
211
+ const scan = async (dir) => {
212
+ const entries = await readdir(dir, { withFileTypes: true });
213
+ for (const entry of entries) {
214
+ if (!entry.isDirectory()) continue;
215
+ if (EXCLUDE.has(entry.name)) continue;
216
+ if (entry.name.startsWith(".")) continue;
217
+ const full = join(dir, entry.name);
218
+ const marker = join(full, COMPONENT_MARKER);
219
+ if (existsSync(marker)) {
220
+ try {
221
+ const data = JSON.parse(await readFile(marker, "utf-8"));
222
+ const markerComponents = data.components ?? (data.component ? [data.component] : []);
223
+ for (const mc of markerComponents) {
224
+ if (components.includes(mc)) {
225
+ paths[mc] = entry.name;
226
+ }
227
+ }
228
+ } catch {
229
+ }
230
+ }
231
+ }
232
+ };
233
+ await scan(cwd);
234
+ for (const c of components) {
235
+ if (!paths[c]) paths[c] = c;
236
+ }
237
+ return paths;
238
+ }
179
239
  function render(template, vars) {
180
240
  const components = vars.components;
181
241
  const projectName = vars.projectName;
@@ -201,8 +261,15 @@ function render(template, vars) {
201
261
  }
202
262
  if (stack.length > 0 && stack.some((v) => !v)) continue;
203
263
  const replaced = line.replace(
204
- /<%=\s*(\w+)\s*%>/g,
205
- (_, key) => String(vars[key] ?? "")
264
+ /<%=\s*([\w.]+)\s*%>/g,
265
+ (_, expr) => {
266
+ const parts = expr.split(".");
267
+ let val = vars;
268
+ for (const p6 of parts) {
269
+ val = val?.[p6];
270
+ }
271
+ return String(val ?? "");
272
+ }
206
273
  );
207
274
  output.push(replaced);
208
275
  }
@@ -283,12 +350,45 @@ async function generateCiYml(vars) {
283
350
  async function generateReadme(vars) {
284
351
  return renderShared("README.md.ejs", vars);
285
352
  }
353
+ function generateVscodeSettings(vars) {
354
+ const settings = {};
355
+ if (vars.components.includes("fastapi")) {
356
+ settings["[python]"] = {
357
+ "editor.defaultFormatter": "charliermarsh.ruff",
358
+ "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
359
+ };
360
+ }
361
+ settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
362
+ settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
363
+ settings["[javascript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
364
+ settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
365
+ settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
366
+ settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
367
+ settings["editor.formatOnSave"] = true;
368
+ settings["editor.codeActionsOnSave"] = { "source.fixAll.eslint": "explicit" };
369
+ settings["eslint.useFlatConfig"] = true;
370
+ const prettierComponent = ["frontend", "fastify", "e2e"].find(
371
+ (c) => vars.components.includes(c)
372
+ );
373
+ if (prettierComponent) {
374
+ settings["prettier.configPath"] = `${vars.paths[prettierComponent]}/.prettierrc`;
375
+ }
376
+ if (vars.components.includes("fastapi")) {
377
+ settings["ruff.lineLength"] = 120;
378
+ settings["python.analysis.extraPaths"] = [`${vars.paths.fastapi}/src`];
379
+ settings["python.analysis.importFormat"] = "absolute";
380
+ }
381
+ return JSON.stringify(settings, null, 2) + "\n";
382
+ }
286
383
 
287
384
  // src/scaffold.ts
288
385
  async function scaffold(opts, dest, localRepo) {
289
386
  const name = toKebab(opts.name);
290
387
  const nameSnake = toSnake(opts.name);
291
- const vars = { projectName: name, components: opts.components };
388
+ const paths = Object.fromEntries(
389
+ opts.components.map((c) => [c, c])
390
+ );
391
+ const vars = { projectName: name, components: opts.components, paths };
292
392
  const isLocal = !!localRepo;
293
393
  await mkdir2(dest, { recursive: true });
294
394
  const dlSpinner = p2.spinner();
@@ -307,42 +407,30 @@ async function scaffold(opts, dest, localRepo) {
307
407
  }
308
408
  async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
309
409
  p2.log.info(`Scaffolding project in ${dest}`);
310
- const manifest = [];
311
410
  for (const component of opts.components) {
312
- const spinner4 = p2.spinner();
313
- spinner4.start(`Copying ${component}/`);
314
- const files = await copyComponent(repoDir, component, dest);
315
- manifest.push(...files.map((f) => `${component}/${f}`));
316
- spinner4.stop(`${component}/`);
411
+ const spinner5 = p2.spinner();
412
+ spinner5.start(`Copying ${component}/`);
413
+ await copyComponent(repoDir, component, dest);
414
+ await writeComponentMarker(join3(dest, component), component);
415
+ spinner5.stop(`${component}/`);
317
416
  }
318
417
  await substituteNames(dest, opts.components, name, nameSnake);
319
418
  const hasBackend = opts.components.includes("fastapi") || opts.components.includes("fastify");
320
419
  if (hasBackend || opts.components.includes("frontend")) {
321
- const dc = await generateDockerCompose(vars);
322
- await writeFile2(join3(dest, "docker-compose.yml"), dc);
323
- manifest.push("docker-compose.yml");
324
- const dcDev = await generateDockerComposeDev(vars);
325
- await writeFile2(join3(dest, "docker-compose.dev.yml"), dcDev);
326
- manifest.push("docker-compose.dev.yml");
327
- }
328
- const readme = await generateReadme(vars);
329
- await writeFile2(join3(dest, "README.md"), readme);
330
- manifest.push("README.md");
420
+ await writeFile2(join3(dest, "docker-compose.yml"), await generateDockerCompose(vars));
421
+ await writeFile2(join3(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
422
+ }
423
+ await writeFile2(join3(dest, "README.md"), await generateReadme(vars));
331
424
  await mkdir2(join3(dest, ".githooks"), { recursive: true });
332
- const preCommit = await generatePreCommit(vars);
333
- await writeFile2(join3(dest, ".githooks/pre-commit"), preCommit);
425
+ await writeFile2(join3(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
334
426
  await chmod(join3(dest, ".githooks/pre-commit"), 493);
335
- manifest.push(".githooks/pre-commit");
336
427
  await mkdir2(join3(dest, ".github/workflows"), { recursive: true });
337
- const lintYml = await generateCiYml(vars);
338
- await writeFile2(join3(dest, ".github/workflows/ci.yml"), lintYml);
339
- manifest.push(".github/workflows/ci.yml");
340
- const setupSh = await generateSetupSh(vars);
341
- await writeFile2(join3(dest, "setup.sh"), setupSh);
428
+ await writeFile2(join3(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
429
+ await writeFile2(join3(dest, "setup.sh"), await generateSetupSh(vars));
342
430
  await chmod(join3(dest, "setup.sh"), 493);
343
- manifest.push("setup.sh");
344
- const staticFiles = await copyStaticFiles(repoDir, dest);
345
- manifest.push(...staticFiles);
431
+ await copyStaticFiles(repoDir, dest);
432
+ await mkdir2(join3(dest, ".vscode"), { recursive: true });
433
+ await writeFile2(join3(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
346
434
  const pkg = JSON.parse(
347
435
  await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
348
436
  );
@@ -350,7 +438,7 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
350
438
  version: pkg.version,
351
439
  components: opts.components,
352
440
  createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
353
- files: manifest.sort()
441
+ paths: vars.paths
354
442
  };
355
443
  await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2));
356
444
  if (opts.git) {
@@ -377,7 +465,9 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
377
465
  p2.outro(`Done! Next steps:
378
466
 
379
467
  cd ${name}
380
- ./setup.sh`);
468
+ ./setup.sh
469
+
470
+ Like projx? Star it: https://github.com/ukanhaupa/projx`);
381
471
  }
382
472
  async function substituteNames(dest, components, name, nameSnake) {
383
473
  if (components.includes("fastapi")) {
@@ -424,44 +514,44 @@ async function substituteNames(dest, components, name, nameSnake) {
424
514
  }
425
515
  async function installDeps(dest, components) {
426
516
  for (const component of components) {
427
- const spinner4 = p2.spinner();
517
+ const spinner5 = p2.spinner();
428
518
  try {
429
519
  switch (component) {
430
520
  case "fastapi":
431
521
  if (hasCommand("uv")) {
432
- spinner4.start("Installing FastAPI dependencies (uv sync)");
522
+ spinner5.start("Installing FastAPI dependencies (uv sync)");
433
523
  exec("uv sync --all-extras", join3(dest, "fastapi"));
434
- spinner4.stop("FastAPI dependencies installed.");
524
+ spinner5.stop("FastAPI dependencies installed.");
435
525
  } else {
436
526
  p2.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
437
527
  }
438
528
  break;
439
529
  case "fastify":
440
530
  if (hasCommand("pnpm")) {
441
- spinner4.start("Installing Fastify dependencies (pnpm install)");
531
+ spinner5.start("Installing Fastify dependencies (pnpm install)");
442
532
  exec("pnpm install", join3(dest, "fastify"));
443
- spinner4.stop("Fastify dependencies installed.");
533
+ spinner5.stop("Fastify dependencies installed.");
444
534
  } else {
445
- spinner4.start("Installing Fastify dependencies (npm install)");
535
+ spinner5.start("Installing Fastify dependencies (npm install)");
446
536
  exec("npm install", join3(dest, "fastify"));
447
- spinner4.stop("Fastify dependencies installed.");
537
+ spinner5.stop("Fastify dependencies installed.");
448
538
  }
449
539
  break;
450
540
  case "frontend":
451
- spinner4.start("Installing Frontend dependencies (npm install)");
541
+ spinner5.start("Installing Frontend dependencies (npm install)");
452
542
  exec("npm install", join3(dest, "frontend"));
453
- spinner4.stop("Frontend dependencies installed.");
543
+ spinner5.stop("Frontend dependencies installed.");
454
544
  break;
455
545
  case "e2e":
456
- spinner4.start("Installing E2E dependencies (npm install)");
546
+ spinner5.start("Installing E2E dependencies (npm install)");
457
547
  exec("npm install", join3(dest, "e2e"));
458
- spinner4.stop("E2E dependencies installed.");
548
+ spinner5.stop("E2E dependencies installed.");
459
549
  break;
460
550
  case "mobile":
461
551
  if (hasCommand("flutter")) {
462
- spinner4.start("Installing Flutter dependencies");
552
+ spinner5.start("Installing Flutter dependencies");
463
553
  exec("flutter pub get", join3(dest, "mobile"));
464
- spinner4.stop("Flutter dependencies installed.");
554
+ spinner5.stop("Flutter dependencies installed.");
465
555
  } else {
466
556
  p2.log.warn(
467
557
  "Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
@@ -472,7 +562,7 @@ async function installDeps(dest, components) {
472
562
  break;
473
563
  }
474
564
  } catch {
475
- spinner4.stop(`Failed to install ${component} dependencies.`);
565
+ spinner5.stop(`Failed to install ${component} dependencies.`);
476
566
  }
477
567
  }
478
568
  }
@@ -499,7 +589,8 @@ var NEVER_OVERWRITE = [
499
589
  /\.env$/,
500
590
  /\.env\.(dev|staging|prod)$/,
501
591
  /prisma\/migrations\//,
502
- /src\/migrations\/versions\//
592
+ /src\/migrations\/versions\//,
593
+ /\.projx-component$/
503
594
  ];
504
595
  function isGitRepo(cwd) {
505
596
  try {
@@ -550,11 +641,17 @@ async function update(cwd, localRepo) {
550
641
  config = {
551
642
  version: "0.0.0",
552
643
  components: detected,
553
- createdAt: "unknown",
554
- files: []
644
+ createdAt: "unknown"
555
645
  };
556
646
  p3.log.info(`Detected: ${detected.join(", ")}`);
557
647
  }
648
+ const componentPaths = await discoverComponentPaths(cwd, config.components);
649
+ const remapped = config.components.filter((c) => componentPaths[c] !== c);
650
+ if (remapped.length > 0) {
651
+ for (const c of remapped) {
652
+ p3.log.info(`${c} \u2192 ${componentPaths[c]}/`);
653
+ }
654
+ }
558
655
  const useGitBranch = isGitRepo(cwd);
559
656
  let branchName;
560
657
  let originalBranch;
@@ -584,7 +681,7 @@ async function update(cwd, localRepo) {
584
681
  execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
585
682
  p3.log.info(`Created branch: ${branchName}`);
586
683
  try {
587
- await doUpdate(cwd, config, repoDir, pkg.version);
684
+ await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
588
685
  } finally {
589
686
  await cleanupRepo(repoDir, isLocal);
590
687
  }
@@ -612,76 +709,75 @@ async function update(cwd, localRepo) {
612
709
  await readFile4(join4(repoDir, "cli/package.json"), "utf-8")
613
710
  );
614
711
  try {
615
- await doUpdate(cwd, config, repoDir, pkg.version);
712
+ await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
616
713
  } finally {
617
714
  await cleanupRepo(repoDir, isLocal);
618
715
  }
619
716
  p3.outro(`Updated to v${pkg.version}. Review changes before committing.`);
620
717
  }
621
718
  }
622
- async function doUpdate(cwd, config, repoDir, version) {
623
- const name = detectProjectName(cwd, config.components);
719
+ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
720
+ const name = detectProjectName(cwd, config.components, componentPaths);
624
721
  const nameSnake = toSnake(name);
625
- const vars = { projectName: name, components: config.components };
626
- const newManifest = [];
722
+ const vars = { projectName: name, components: config.components, paths: componentPaths };
627
723
  for (const component of config.components) {
628
- const spinner5 = p3.spinner();
629
- spinner5.start(`Updating ${component}/ template files`);
724
+ const targetDir = componentPaths[component];
725
+ const spinner6 = p3.spinner();
726
+ spinner6.start(`Updating ${targetDir}/ (${component})`);
630
727
  const componentSrc = join4(repoDir, component);
631
728
  if (!existsSync3(componentSrc)) {
632
- spinner5.stop(`${component}/ template not found, skipping.`);
729
+ spinner6.stop(`${component} template not found, skipping.`);
633
730
  continue;
634
731
  }
635
732
  const tmpDest = join4(cwd, `.projx-tmp`);
636
733
  const files = await copyComponent(repoDir, component, tmpDest);
637
734
  for (const file of files) {
638
- const rel = `${component}/${file}`;
639
735
  const src = join4(tmpDest, component, file);
640
- const dest = join4(cwd, rel);
641
- if (NEVER_OVERWRITE.some((re) => re.test(rel))) continue;
642
- if (config.files.length > 0 && !config.files.includes(rel)) continue;
736
+ const destRel = `${targetDir}/${file}`;
737
+ const dest = join4(cwd, destRel);
738
+ if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
643
739
  const dir = dest.substring(0, dest.lastIndexOf("/"));
644
740
  await mkdir3(dir, { recursive: true });
645
741
  await cp2(src, dest, { force: true });
646
- newManifest.push(rel);
647
742
  }
648
743
  await rm2(tmpDest, { recursive: true, force: true });
649
- spinner5.stop(`${component}/ updated.`);
744
+ if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
745
+ await writeComponentMarker(join4(cwd, targetDir), component);
746
+ }
747
+ spinner6.stop(`${targetDir}/ updated.`);
650
748
  }
651
- const spinner4 = p3.spinner();
652
- spinner4.start("Updating shared files");
749
+ const spinner5 = p3.spinner();
750
+ spinner5.start("Updating shared files");
653
751
  const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
654
752
  if (hasBackend || config.components.includes("frontend")) {
655
753
  await writeFile3(
656
754
  join4(cwd, "docker-compose.yml"),
657
755
  await generateDockerCompose(vars)
658
756
  );
659
- newManifest.push("docker-compose.yml");
660
757
  await writeFile3(
661
758
  join4(cwd, "docker-compose.dev.yml"),
662
759
  await generateDockerComposeDev(vars)
663
760
  );
664
- newManifest.push("docker-compose.dev.yml");
665
761
  }
666
762
  await mkdir3(join4(cwd, ".githooks"), { recursive: true });
667
763
  const preCommit = await generatePreCommit(vars);
668
764
  await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
669
765
  await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
670
- newManifest.push(".githooks/pre-commit");
671
766
  await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
672
767
  await writeFile3(
673
768
  join4(cwd, ".github/workflows/ci.yml"),
674
769
  await generateCiYml(vars)
675
770
  );
676
- newManifest.push(".github/workflows/ci.yml");
677
771
  const setupSh = await generateSetupSh(vars);
678
772
  await writeFile3(join4(cwd, "setup.sh"), setupSh);
679
773
  await chmod2(join4(cwd, "setup.sh"), 493);
680
- newManifest.push("setup.sh");
681
- spinner4.stop("Shared files updated.");
774
+ await mkdir3(join4(cwd, ".vscode"), { recursive: true });
775
+ await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
776
+ spinner5.stop("Shared files updated.");
682
777
  if (config.components.includes("mobile")) {
778
+ const mobilePath = componentPaths.mobile ?? "mobile";
683
779
  await replaceInDir(
684
- join4(cwd, "mobile"),
780
+ join4(cwd, mobilePath),
685
781
  "package:projx_mobile/",
686
782
  `package:${nameSnake}_mobile/`,
687
783
  ".dart"
@@ -691,13 +787,14 @@ async function doUpdate(cwd, config, repoDir, version) {
691
787
  version,
692
788
  components: config.components,
693
789
  createdAt: config.createdAt,
694
- files: [.../* @__PURE__ */ new Set([...config.files, ...newManifest])].sort()
790
+ paths: componentPaths
695
791
  };
696
792
  await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
697
793
  }
698
- function detectProjectName(cwd, components) {
794
+ function detectProjectName(cwd, components, componentPaths) {
699
795
  for (const component of components) {
700
- const pkgPath = join4(cwd, component, "package.json");
796
+ const dir = componentPaths[component] ?? component;
797
+ const pkgPath = join4(cwd, dir, "package.json");
701
798
  if (existsSync3(pkgPath)) {
702
799
  try {
703
800
  const pkg = JSON.parse(
@@ -754,44 +851,37 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
754
851
  }
755
852
  }
756
853
  async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
757
- const name = detectProjectName2(cwd, config.components);
758
- const nameSnake = toSnake(name);
759
854
  const allComponents = [...config.components, ...toAdd];
760
- const vars = { projectName: name, components: allComponents };
761
- const newFiles = [];
855
+ const existingPaths = await discoverComponentPaths(cwd, config.components);
856
+ const paths = { ...existingPaths };
857
+ for (const c of toAdd) paths[c] = c;
858
+ const name = detectProjectName2(cwd, config.components, paths);
859
+ const nameSnake = toSnake(name);
860
+ const vars = { projectName: name, components: allComponents, paths };
762
861
  for (const component of toAdd) {
763
- const spinner5 = p4.spinner();
764
- spinner5.start(`Adding ${component}/`);
765
- const files = await copyComponent(repoDir, component, cwd);
766
- newFiles.push(...files.map((f) => `${component}/${f}`));
767
- spinner5.stop(`${component}/`);
862
+ const spinner6 = p4.spinner();
863
+ spinner6.start(`Adding ${component}/`);
864
+ await copyComponent(repoDir, component, cwd);
865
+ await writeComponentMarker(join5(cwd, component), component);
866
+ spinner6.stop(`${component}/`);
768
867
  }
769
868
  await substituteNames2(cwd, toAdd, name, nameSnake);
770
- const spinner4 = p4.spinner();
771
- spinner4.start("Regenerating shared files");
869
+ const spinner5 = p4.spinner();
870
+ spinner5.start("Regenerating shared files");
772
871
  const hasBackend = allComponents.includes("fastapi") || allComponents.includes("fastify");
773
872
  if (hasBackend || allComponents.includes("frontend")) {
774
- await writeFile4(
775
- join5(cwd, "docker-compose.yml"),
776
- await generateDockerCompose(vars)
777
- );
778
- await writeFile4(
779
- join5(cwd, "docker-compose.dev.yml"),
780
- await generateDockerComposeDev(vars)
781
- );
873
+ await writeFile4(join5(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
874
+ await writeFile4(join5(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
782
875
  }
783
876
  await writeFile4(join5(cwd, "README.md"), await generateReadme(vars));
784
877
  await mkdir4(join5(cwd, ".githooks"), { recursive: true });
785
878
  await writeFile4(join5(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
786
879
  await chmod3(join5(cwd, ".githooks/pre-commit"), 493);
787
880
  await mkdir4(join5(cwd, ".github/workflows"), { recursive: true });
788
- await writeFile4(
789
- join5(cwd, ".github/workflows/ci.yml"),
790
- await generateCiYml(vars)
791
- );
881
+ await writeFile4(join5(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
792
882
  await writeFile4(join5(cwd, "setup.sh"), await generateSetupSh(vars));
793
883
  await chmod3(join5(cwd, "setup.sh"), 493);
794
- spinner4.stop("Shared files regenerated.");
884
+ spinner5.stop("Shared files regenerated.");
795
885
  if (!skipInstall) {
796
886
  await installDeps2(cwd, toAdd);
797
887
  }
@@ -812,10 +902,12 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
812
902
  version: pkg.version,
813
903
  components: allComponents,
814
904
  createdAt: config.createdAt,
815
- files: [.../* @__PURE__ */ new Set([...config.files, ...newFiles])].sort()
905
+ paths
816
906
  };
817
907
  await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
818
- p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.`);
908
+ p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
909
+
910
+ Like projx? Star it: https://github.com/ukanhaupa/projx`);
819
911
  }
820
912
  async function substituteNames2(dest, components, name, nameSnake) {
821
913
  if (components.includes("fastapi")) {
@@ -862,44 +954,44 @@ async function substituteNames2(dest, components, name, nameSnake) {
862
954
  }
863
955
  async function installDeps2(dest, components) {
864
956
  for (const component of components) {
865
- const spinner4 = p4.spinner();
957
+ const spinner5 = p4.spinner();
866
958
  try {
867
959
  switch (component) {
868
960
  case "fastapi":
869
961
  if (hasCommand("uv")) {
870
- spinner4.start("Installing FastAPI dependencies");
962
+ spinner5.start("Installing FastAPI dependencies");
871
963
  exec("uv sync --all-extras", join5(dest, "fastapi"));
872
- spinner4.stop("FastAPI dependencies installed.");
964
+ spinner5.stop("FastAPI dependencies installed.");
873
965
  } else {
874
966
  p4.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
875
967
  }
876
968
  break;
877
969
  case "fastify":
878
970
  if (hasCommand("pnpm")) {
879
- spinner4.start("Installing Fastify dependencies");
971
+ spinner5.start("Installing Fastify dependencies");
880
972
  exec("pnpm install", join5(dest, "fastify"));
881
- spinner4.stop("Fastify dependencies installed.");
973
+ spinner5.stop("Fastify dependencies installed.");
882
974
  } else {
883
- spinner4.start("Installing Fastify dependencies");
975
+ spinner5.start("Installing Fastify dependencies");
884
976
  exec("npm install", join5(dest, "fastify"));
885
- spinner4.stop("Fastify dependencies installed.");
977
+ spinner5.stop("Fastify dependencies installed.");
886
978
  }
887
979
  break;
888
980
  case "frontend":
889
- spinner4.start("Installing Frontend dependencies");
981
+ spinner5.start("Installing Frontend dependencies");
890
982
  exec("npm install", join5(dest, "frontend"));
891
- spinner4.stop("Frontend dependencies installed.");
983
+ spinner5.stop("Frontend dependencies installed.");
892
984
  break;
893
985
  case "e2e":
894
- spinner4.start("Installing E2E dependencies");
986
+ spinner5.start("Installing E2E dependencies");
895
987
  exec("npm install", join5(dest, "e2e"));
896
- spinner4.stop("E2E dependencies installed.");
988
+ spinner5.stop("E2E dependencies installed.");
897
989
  break;
898
990
  case "mobile":
899
991
  if (hasCommand("flutter")) {
900
- spinner4.start("Installing Flutter dependencies");
992
+ spinner5.start("Installing Flutter dependencies");
901
993
  exec("flutter pub get", join5(dest, "mobile"));
902
- spinner4.stop("Flutter dependencies installed.");
994
+ spinner5.stop("Flutter dependencies installed.");
903
995
  } else {
904
996
  p4.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
905
997
  }
@@ -908,13 +1000,14 @@ async function installDeps2(dest, components) {
908
1000
  break;
909
1001
  }
910
1002
  } catch {
911
- spinner4.stop(`Failed to install ${component} dependencies.`);
1003
+ spinner5.stop(`Failed to install ${component} dependencies.`);
912
1004
  }
913
1005
  }
914
1006
  }
915
- function detectProjectName2(cwd, components) {
1007
+ function detectProjectName2(cwd, components, paths) {
916
1008
  for (const component of components) {
917
- const pkgPath = join5(cwd, component, "package.json");
1009
+ const dir = paths[component] ?? component;
1010
+ const pkgPath = join5(cwd, dir, "package.json");
918
1011
  if (existsSync4(pkgPath)) {
919
1012
  try {
920
1013
  const pkg = JSON.parse(
@@ -931,6 +1024,395 @@ function detectProjectName2(cwd, components) {
931
1024
  return toKebab(cwd.split("/").pop());
932
1025
  }
933
1026
 
1027
+ // src/init.ts
1028
+ import { existsSync as existsSync6 } from "fs";
1029
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5, chmod as chmod4, cp as cp3 } from "fs/promises";
1030
+ import { execSync as execSync3 } from "child_process";
1031
+ import { join as join7 } from "path";
1032
+ import * as p5 from "@clack/prompts";
1033
+
1034
+ // src/detect.ts
1035
+ import { existsSync as existsSync5 } from "fs";
1036
+ import { readdir as readdir2 } from "fs/promises";
1037
+ import { join as join6 } from "path";
1038
+ async function detectComponents(cwd) {
1039
+ const results = [];
1040
+ const entries = await readdir2(cwd, { withFileTypes: true });
1041
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)).map((e) => e.name);
1042
+ for (const dir of dirs) {
1043
+ const full = join6(cwd, dir);
1044
+ const detections = await scanDirectory(full, dir);
1045
+ results.push(...detections);
1046
+ }
1047
+ return results;
1048
+ }
1049
+ async function scanDirectory(dir, relPath) {
1050
+ const results = [];
1051
+ const pyproject = await readFileOrNull(join6(dir, "pyproject.toml"));
1052
+ if (pyproject && /fastapi/i.test(pyproject)) {
1053
+ results.push({
1054
+ component: "fastapi",
1055
+ directory: relPath,
1056
+ confidence: "high",
1057
+ evidence: "pyproject.toml has fastapi dependency"
1058
+ });
1059
+ }
1060
+ const pkg = await readPkg(dir);
1061
+ if (pkg) {
1062
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1063
+ if (allDeps.fastify) {
1064
+ results.push({
1065
+ component: "fastify",
1066
+ directory: relPath,
1067
+ confidence: "high",
1068
+ evidence: "package.json has fastify dependency"
1069
+ });
1070
+ }
1071
+ if (allDeps.react || allDeps["react-dom"]) {
1072
+ results.push({
1073
+ component: "frontend",
1074
+ directory: relPath,
1075
+ confidence: "high",
1076
+ evidence: "package.json has react dependency"
1077
+ });
1078
+ }
1079
+ if (allDeps["@playwright/test"] || allDeps.playwright) {
1080
+ results.push({
1081
+ component: "e2e",
1082
+ directory: relPath,
1083
+ confidence: "high",
1084
+ evidence: "package.json has playwright dependency"
1085
+ });
1086
+ }
1087
+ }
1088
+ const pubspec = await readFileOrNull(join6(dir, "pubspec.yaml"));
1089
+ if (pubspec && /flutter:/i.test(pubspec)) {
1090
+ results.push({
1091
+ component: "mobile",
1092
+ directory: relPath,
1093
+ confidence: "high",
1094
+ evidence: "pubspec.yaml has flutter dependency"
1095
+ });
1096
+ }
1097
+ const hasTf = existsSync5(join6(dir, "main.tf")) || existsSync5(join6(dir, "variables.tf")) || existsSync5(join6(dir, "stack/main.tf")) || existsSync5(join6(dir, "versions.tf"));
1098
+ if (hasTf) {
1099
+ results.push({
1100
+ component: "infra",
1101
+ directory: relPath,
1102
+ confidence: "high",
1103
+ evidence: "Terraform .tf files found"
1104
+ });
1105
+ }
1106
+ return results;
1107
+ }
1108
+ async function readPkg(dir) {
1109
+ const content = await readFileOrNull(join6(dir, "package.json"));
1110
+ if (!content) return null;
1111
+ try {
1112
+ return JSON.parse(content);
1113
+ } catch {
1114
+ return null;
1115
+ }
1116
+ }
1117
+
1118
+ // src/diff.ts
1119
+ function unifiedDiff(existing, template, label) {
1120
+ const a = existing.split("\n");
1121
+ const b = template.split("\n");
1122
+ const lines = [`--- existing ${label}`, `+++ template ${label}`];
1123
+ const lcs = computeLCS(a, b);
1124
+ let ai = 0;
1125
+ let bi = 0;
1126
+ for (const match of lcs) {
1127
+ while (ai < match.ai) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
1128
+ while (bi < match.bi) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
1129
+ lines.push(` ${a[ai]}`);
1130
+ ai++;
1131
+ bi++;
1132
+ }
1133
+ while (ai < a.length) lines.push(`\x1B[31m- ${a[ai++]}\x1B[0m`);
1134
+ while (bi < b.length) lines.push(`\x1B[32m+ ${b[bi++]}\x1B[0m`);
1135
+ if (lines.length > 80) {
1136
+ return lines.slice(0, 80).join("\n") + `
1137
+ ... (${lines.length - 80} more lines)`;
1138
+ }
1139
+ return lines.join("\n");
1140
+ }
1141
+ function computeLCS(a, b) {
1142
+ const m = a.length;
1143
+ const n = b.length;
1144
+ if (m * n > 1e5) {
1145
+ return simpleLCS(a, b);
1146
+ }
1147
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
1148
+ for (let i2 = m - 1; i2 >= 0; i2--) {
1149
+ for (let j2 = n - 1; j2 >= 0; j2--) {
1150
+ if (a[i2] === b[j2]) {
1151
+ dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
1152
+ } else {
1153
+ dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1154
+ }
1155
+ }
1156
+ }
1157
+ const matches = [];
1158
+ let i = 0;
1159
+ let j = 0;
1160
+ while (i < m && j < n) {
1161
+ if (a[i] === b[j]) {
1162
+ matches.push({ ai: i, bi: j });
1163
+ i++;
1164
+ j++;
1165
+ } else if (dp[i + 1]?.[j] ?? 0 >= (dp[i]?.[j + 1] ?? 0)) {
1166
+ i++;
1167
+ } else {
1168
+ j++;
1169
+ }
1170
+ }
1171
+ return matches;
1172
+ }
1173
+ function simpleLCS(a, b) {
1174
+ const matches = [];
1175
+ let bi = 0;
1176
+ for (let ai = 0; ai < a.length && bi < b.length; ai++) {
1177
+ const idx = b.indexOf(a[ai], bi);
1178
+ if (idx !== -1) {
1179
+ matches.push({ ai, bi: idx });
1180
+ bi = idx + 1;
1181
+ }
1182
+ }
1183
+ return matches;
1184
+ }
1185
+
1186
+ // src/init.ts
1187
+ async function init(cwd, localRepo) {
1188
+ p5.intro("projx init");
1189
+ const isLocal = !!localRepo;
1190
+ if (existsSync6(join7(cwd, ".projx"))) {
1191
+ p5.log.error("This project is already initialized. Use 'projx update' or 'projx add' instead.");
1192
+ process.exit(1);
1193
+ }
1194
+ const spinner5 = p5.spinner();
1195
+ spinner5.start("Scanning for components");
1196
+ const detected = await detectComponents(cwd);
1197
+ spinner5.stop(
1198
+ detected.length > 0 ? `Found ${detected.length} component(s).` : "No components detected."
1199
+ );
1200
+ let confirmed;
1201
+ if (detected.length > 0) {
1202
+ confirmed = await confirmDetections(detected);
1203
+ } else {
1204
+ confirmed = await manualSelect(cwd);
1205
+ }
1206
+ if (confirmed.length === 0) {
1207
+ p5.log.warn("No components selected. Nothing to do.");
1208
+ process.exit(0);
1209
+ }
1210
+ const components = confirmed.map((c) => c.component);
1211
+ const paths = Object.fromEntries(
1212
+ confirmed.map((c) => [c.component, c.directory])
1213
+ );
1214
+ const projectName = toKebab(cwd.split("/").pop());
1215
+ const vars = { projectName, components, paths };
1216
+ const dlSpinner = p5.spinner();
1217
+ dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
1218
+ const repoDir = await downloadRepo(localRepo).catch((err) => {
1219
+ dlSpinner.stop("Failed.");
1220
+ p5.log.error(String(err));
1221
+ process.exit(1);
1222
+ });
1223
+ dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
1224
+ try {
1225
+ for (const { component, directory } of confirmed) {
1226
+ const dir = join7(cwd, directory);
1227
+ if (existsSync6(dir)) {
1228
+ await writeComponentMarker(dir, component);
1229
+ p5.log.success(`${directory}/.projx-component`);
1230
+ }
1231
+ }
1232
+ await generateSharedFiles(cwd, repoDir, vars);
1233
+ const pkg = JSON.parse(
1234
+ await readFile6(join7(repoDir, "cli/package.json"), "utf-8")
1235
+ );
1236
+ const projxConfig = {
1237
+ version: pkg.version,
1238
+ components,
1239
+ createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1240
+ paths
1241
+ };
1242
+ await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
1243
+ p5.log.success(".projx");
1244
+ if (isGitRepo2(cwd)) {
1245
+ try {
1246
+ execSync3("git config core.hooksPath .githooks", { cwd, stdio: "pipe" });
1247
+ p5.log.success("Git hooks configured.");
1248
+ } catch {
1249
+ p5.log.warn("Failed to configure git hooks.");
1250
+ }
1251
+ }
1252
+ } finally {
1253
+ await cleanupRepo(repoDir, isLocal);
1254
+ }
1255
+ p5.outro("Project initialized. Run './setup.sh' to install dependencies.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx");
1256
+ }
1257
+ async function confirmDetections(detected) {
1258
+ const confirmed = [];
1259
+ for (const d of detected) {
1260
+ const yes = await p5.confirm({
1261
+ message: `Found ${LABELS[d.component].label} in ${d.directory}/ \u2014 register as "${d.component}"?`,
1262
+ initialValue: true
1263
+ });
1264
+ if (p5.isCancel(yes)) process.exit(0);
1265
+ if (yes) {
1266
+ confirmed.push({ component: d.component, directory: d.directory });
1267
+ }
1268
+ }
1269
+ return confirmed;
1270
+ }
1271
+ async function manualSelect(cwd) {
1272
+ const selected = await p5.multiselect({
1273
+ message: "No components detected. Select manually:",
1274
+ options: COMPONENTS.map((c) => ({
1275
+ value: c,
1276
+ label: LABELS[c].label,
1277
+ hint: LABELS[c].hint
1278
+ })),
1279
+ required: false
1280
+ });
1281
+ if (p5.isCancel(selected)) process.exit(0);
1282
+ const result = [];
1283
+ for (const component of selected) {
1284
+ const dir = await p5.text({
1285
+ message: `Directory for ${LABELS[component].label}?`,
1286
+ placeholder: component,
1287
+ defaultValue: component
1288
+ });
1289
+ if (p5.isCancel(dir)) process.exit(0);
1290
+ if (!existsSync6(join7(cwd, dir))) {
1291
+ p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
1292
+ continue;
1293
+ }
1294
+ result.push({ component, directory: dir });
1295
+ }
1296
+ return result;
1297
+ }
1298
+ async function generateSharedFiles(cwd, repoDir, vars) {
1299
+ const files = [];
1300
+ const hasBackend = vars.components.includes("fastapi") || vars.components.includes("fastify");
1301
+ if (hasBackend || vars.components.includes("frontend")) {
1302
+ files.push(
1303
+ { path: "docker-compose.yml", content: await generateDockerCompose(vars) },
1304
+ { path: "docker-compose.dev.yml", content: await generateDockerComposeDev(vars) }
1305
+ );
1306
+ }
1307
+ files.push(
1308
+ { path: "README.md", content: await generateReadme(vars) },
1309
+ { path: ".githooks/pre-commit", content: await generatePreCommit(vars), mode: 493 },
1310
+ { path: ".github/workflows/ci.yml", content: await generateCiYml(vars) },
1311
+ { path: "setup.sh", content: await generateSetupSh(vars), mode: 493 }
1312
+ );
1313
+ for (const file of files) {
1314
+ const dest = join7(cwd, file.path);
1315
+ const dir = dest.substring(0, dest.lastIndexOf("/"));
1316
+ if (dir !== cwd) await mkdir5(dir, { recursive: true });
1317
+ const existing = await readFileOrNull(dest);
1318
+ if (existing === null) {
1319
+ await writeFile5(dest, file.content);
1320
+ if (file.mode) await chmod4(dest, file.mode);
1321
+ p5.log.success(file.path);
1322
+ } else if (existing === file.content) {
1323
+ p5.log.info(`${file.path} \u2014 identical, skipped.`);
1324
+ } else {
1325
+ const action = await resolveConflict(file.path, existing, file.content);
1326
+ if (action === "overwrite") {
1327
+ await writeFile5(dest, file.content);
1328
+ if (file.mode) await chmod4(dest, file.mode);
1329
+ p5.log.success(`${file.path} \u2014 overwritten.`);
1330
+ } else {
1331
+ p5.log.info(`${file.path} \u2014 kept existing.`);
1332
+ }
1333
+ }
1334
+ }
1335
+ const statics = [".editorconfig"];
1336
+ for (const file of statics) {
1337
+ const src = join7(repoDir, file);
1338
+ const dest = join7(cwd, file);
1339
+ if (!existsSync6(src)) continue;
1340
+ if (!existsSync6(dest)) {
1341
+ await cp3(src, dest);
1342
+ p5.log.success(file);
1343
+ } else {
1344
+ const existing = await readFileOrNull(dest);
1345
+ const template = await readFileOrNull(src);
1346
+ if (existing === template) {
1347
+ p5.log.info(`${file} \u2014 identical, skipped.`);
1348
+ } else {
1349
+ const action = await resolveConflict(file, existing ?? "", template ?? "");
1350
+ if (action === "overwrite") {
1351
+ await cp3(src, dest, { force: true });
1352
+ p5.log.success(`${file} \u2014 overwritten.`);
1353
+ } else {
1354
+ p5.log.info(`${file} \u2014 kept existing.`);
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+ const vscodeDest = join7(cwd, ".vscode");
1360
+ await mkdir5(vscodeDest, { recursive: true });
1361
+ const settingsPath = join7(vscodeDest, "settings.json");
1362
+ const settingsContent = generateVscodeSettings(vars);
1363
+ const existingSettings = await readFileOrNull(settingsPath);
1364
+ if (existingSettings === null) {
1365
+ await writeFile5(settingsPath, settingsContent);
1366
+ p5.log.success(".vscode/settings.json");
1367
+ } else if (existingSettings !== settingsContent) {
1368
+ const action = await resolveConflict(".vscode/settings.json", existingSettings, settingsContent);
1369
+ if (action === "overwrite") {
1370
+ await writeFile5(settingsPath, settingsContent);
1371
+ p5.log.success(".vscode/settings.json \u2014 overwritten.");
1372
+ } else {
1373
+ p5.log.info(".vscode/settings.json \u2014 kept existing.");
1374
+ }
1375
+ }
1376
+ const extSrc = join7(repoDir, ".vscode/extensions.json");
1377
+ const extDest = join7(vscodeDest, "extensions.json");
1378
+ if (existsSync6(extSrc) && !existsSync6(extDest)) {
1379
+ await cp3(extSrc, extDest);
1380
+ p5.log.success(".vscode/extensions.json");
1381
+ }
1382
+ }
1383
+ async function resolveConflict(filePath, existing, template) {
1384
+ let action = await p5.select({
1385
+ message: `${filePath} differs from projx template`,
1386
+ options: [
1387
+ { value: "diff", label: "View diff" },
1388
+ { value: "overwrite", label: "Overwrite with template" },
1389
+ { value: "skip", label: "Skip (keep existing)" }
1390
+ ]
1391
+ });
1392
+ if (p5.isCancel(action)) process.exit(0);
1393
+ if (action === "diff") {
1394
+ const diff = unifiedDiff(existing, template, filePath);
1395
+ p5.log.message(diff);
1396
+ action = await p5.select({
1397
+ message: `${filePath}`,
1398
+ options: [
1399
+ { value: "overwrite", label: "Overwrite with template" },
1400
+ { value: "skip", label: "Skip (keep existing)" }
1401
+ ]
1402
+ });
1403
+ if (p5.isCancel(action)) process.exit(0);
1404
+ }
1405
+ return action;
1406
+ }
1407
+ function isGitRepo2(cwd) {
1408
+ try {
1409
+ execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
1410
+ return true;
1411
+ } catch {
1412
+ return false;
1413
+ }
1414
+ }
1415
+
934
1416
  // src/index.ts
935
1417
  var args = process.argv.slice(2);
936
1418
  function parseArgs() {
@@ -949,6 +1431,10 @@ function parseArgs() {
949
1431
  command = "add";
950
1432
  continue;
951
1433
  }
1434
+ if (arg === "init" && !name) {
1435
+ command = "init";
1436
+ continue;
1437
+ }
952
1438
  if (arg === "--components") {
953
1439
  const val = args[++i];
954
1440
  if (val) {
@@ -992,6 +1478,7 @@ function printHelp() {
992
1478
  console.log(`
993
1479
  Usage:
994
1480
  projx <name> [options] Create a new project
1481
+ projx init Adopt existing project into projx
995
1482
  projx add <components...> Add components to existing project
996
1483
  projx update Update scaffolding to latest
997
1484
 
@@ -1013,6 +1500,10 @@ function printHelp() {
1013
1500
  }
1014
1501
  async function main() {
1015
1502
  const { command, name, options, localRepo, extraArgs } = parseArgs();
1503
+ if (command === "init") {
1504
+ await init(process.cwd(), localRepo);
1505
+ return;
1506
+ }
1016
1507
  if (command === "update") {
1017
1508
  await update(process.cwd(), localRepo);
1018
1509
  return;
@@ -1046,7 +1537,7 @@ async function main() {
1046
1537
  opts.install = options.install ?? opts.install;
1047
1538
  }
1048
1539
  const dest = resolve2(process.cwd(), opts.name);
1049
- if (existsSync5(dest)) {
1540
+ if (existsSync7(dest)) {
1050
1541
  console.error(`Error: ${dest} already exists.`);
1051
1542
  process.exit(1);
1052
1543
  }