@cellajs/create-cella 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -1,23 +1,12 @@
1
1
  # @cellajs/create-cella
2
2
 
3
- CLI tool to scaffold a new Cella project from the template.
4
-
5
- ## Overview
6
-
7
- This CLI creates a new Cella project by downloading the latest template, scaffolding your project, and configuring git with upstream tracking for future syncs. It runs no shell commands and requires no `git` binary — git operations use [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) and the template is fetched and extracted in-process.
3
+ CLI tool to scaffold a new Cella project from [the template](https://github.com/cellajs/cella).
8
4
 
9
5
  ## Usage
10
6
 
11
7
  ```bash
12
8
  pnpm create @cellajs/cella my-app
13
9
  ```
14
-
15
- Running without arguments starts interactive mode, prompting for:
16
-
17
- 1. **Project name** – Directory name and package name
18
- 2. **New branch** – Optionally create a dev branch alongside `main`
19
- 3. **Directory conflict** – If target exists, choose to cancel or continue
20
-
21
10
  ## CLI Options
22
11
 
23
12
  ```bash
@@ -34,14 +23,12 @@ pnpm create @cellajs/cella [directory] [options]
34
23
 
35
24
  ## What It Does
36
25
 
37
- 1. Downloads the latest Cella template as a GitHub tarball and extracts it in-process (no `git` binary, no shell)
38
- 2. Cleans template files (removes cella-specific docs, configs) and interpolates the project config
26
+ 1. Downloads the latest Cella template as a GitHub tarball and extracts it in-process
27
+ 2. Cleans template files to replace cella-specific config
39
28
  3. Initializes a git repository with an initial commit via isomorphic-git
40
29
  4. Creates an optional working branch
41
30
  5. Adds Cella as the upstream remote for future syncs
42
31
 
43
- Dependency installation and migration generation are **not** run automatically — after scaffolding, run `pnpm install`, then `pnpm generate` (the CLI prints these next steps on success).
44
-
45
32
  ## Development
46
33
 
47
34
  ```bash
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/create-cella-cli.ts
4
4
  import { existsSync as existsSync2 } from "fs";
5
+ import { rm as rm3 } from "fs/promises";
5
6
  import { basename as basename2, resolve as resolve3 } from "path";
6
7
  import { input, select } from "@inquirer/prompts";
7
8
  import pc6 from "picocolors";
@@ -12,7 +13,7 @@ import pc from "picocolors";
12
13
  // package.json
13
14
  var package_default = {
14
15
  name: "@cellajs/create-cella",
15
- version: "0.3.1",
16
+ version: "0.3.3",
16
17
  private: false,
17
18
  license: "MIT",
18
19
  description: "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
@@ -31,7 +32,6 @@ var package_default = {
31
32
  type: "module",
32
33
  main: "./dist/index.js",
33
34
  files: [
34
- "configs",
35
35
  "dist",
36
36
  "index.js",
37
37
  "README.md"
@@ -63,7 +63,6 @@ var package_default = {
63
63
  commander: "^15.0.0",
64
64
  ignore: "^7.0.5",
65
65
  "isomorphic-git": "^1.38.5",
66
- nanotar: "^0.3.0",
67
66
  ora: "^9.4.1",
68
67
  picocolors: "^1.1.1",
69
68
  "validate-npm-package-name": "^8.0.0"
@@ -114,23 +113,25 @@ var NAME = "create cella";
114
113
  var DIVIDER = "\u2500".repeat(60);
115
114
  var TEMPLATE_URL = "github:cellajs/cella";
116
115
  var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
116
+ var CELLA_REMOTE_NAME = "cella-upstream";
117
117
  var DESCRIPTION = package_default.description;
118
118
  var VERSION = package_default.version;
119
119
  var WEBSITE = package_default.homepage;
120
- function getHeaderLine(templateVersion) {
121
- const leftText = `\u29C8 ${NAME} \xB7 ${VERSION} \xB7 cella ${templateVersion}`;
122
- const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 ${VERSION} \xB7 cella ${templateVersion}`)}`;
120
+ function getHeaderLine() {
121
+ const leftText = `\u29C8 ${NAME} \xB7 ${VERSION}`;
122
+ const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 ${VERSION}`)}`;
123
123
  const right = pc.cyan(WEBSITE);
124
124
  const padding = Math.max(1, 60 - leftText.length - WEBSITE.length);
125
125
  return `${left}${" ".repeat(padding)}${right}`;
126
126
  }
127
- var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md"];
127
+ var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md", "./infra/Pulumi.production.yaml"];
128
128
  var TO_CLEAN = ["./backend/drizzle"];
129
129
  var TO_COPY = {
130
130
  "./frontend/.env.example": "./frontend/.env",
131
131
  "./info/QUICKSTART.md": "README.md"
132
132
  };
133
- var PLACEHOLDER_CONFIG = "configs/default-config.ts.template";
133
+ var PLACEHOLDER_CONFIG = "shared/config/config.template.ts";
134
+ var INITIAL_VERSION = "0.0.0";
134
135
  async function generateEnvFromExample(examplePath, replacements) {
135
136
  const { readFile: readFile4 } = await import("fs/promises");
136
137
  let content;
@@ -220,8 +221,8 @@ ${nameEntry}${body}
220
221
 
221
222
  // src/create.ts
222
223
  import { existsSync } from "fs";
223
- import { cp, mkdir as mkdir2, rm } from "fs/promises";
224
- import { dirname as dirname3, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
224
+ import { cp, mkdir as mkdir2, rm as rm2 } from "fs/promises";
225
+ import { dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve2 } from "path";
225
226
  import { checkbox } from "@inquirer/prompts";
226
227
  import pc5 from "picocolors";
227
228
 
@@ -255,7 +256,7 @@ async function gitRemoteRemove(repoPath, remoteName) {
255
256
  async function addRemote({
256
257
  targetFolder,
257
258
  remoteUrl = CELLA_REMOTE_URL,
258
- remoteName = "upstream",
259
+ remoteName = CELLA_REMOTE_NAME,
259
260
  silent = false
260
261
  }) {
261
262
  try {
@@ -338,13 +339,13 @@ function showAscii() {
338
339
  console.info(pc2.cyan("\u2592\u2593\u2588 \u2588\u2593\u2592 | (_| __/ | | (_| | "));
339
340
  console.info(pc2.cyan("\u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 \\___\\___|_|_|\\__,_| "));
340
341
  }
341
- function showWelcome(templateVersion) {
342
+ function showWelcome() {
342
343
  console.info();
343
344
  showAscii();
344
345
  console.info();
345
346
  console.info(pc2.dim(DESCRIPTION));
346
347
  console.info();
347
- console.info(getHeaderLine(templateVersion));
348
+ console.info(getHeaderLine());
348
349
  console.info(DIVIDER);
349
350
  }
350
351
  function confirmChoice(label, value) {
@@ -375,21 +376,8 @@ function showSuccess(projectName, _targetFolder, relativePath, needsCd, packageM
375
376
  // src/utils/clean-template.ts
376
377
  import fs2 from "fs/promises";
377
378
  import path from "path";
378
- import { fileURLToPath } from "url";
379
379
  import pc3 from "picocolors";
380
380
  var warningMark = pc3.yellow("\u26A0");
381
- async function resolvePlaceholderConfigPath() {
382
- const here = path.dirname(fileURLToPath(import.meta.url));
383
- const candidates = [path.resolve(here, "../..", PLACEHOLDER_CONFIG), path.resolve(here, "..", PLACEHOLDER_CONFIG)];
384
- for (const candidate of candidates) {
385
- try {
386
- await fs2.access(candidate);
387
- return candidate;
388
- } catch {
389
- }
390
- }
391
- return candidates[0];
392
- }
393
381
  async function cleanTemplate({
394
382
  targetFolder,
395
383
  projectName,
@@ -408,7 +396,8 @@ async function cleanTemplate({
408
396
  const destAbsolutePath = path.resolve(targetFolder, dest);
409
397
  await copyFile(srcAbsolutePath, destAbsolutePath);
410
398
  }
411
- await applyPlaceholderConfig(targetFolder, projectName);
399
+ await applyPlaceholderConfig(targetFolder, projectName, displayName);
400
+ await resetReleaseState(targetFolder, projectName);
412
401
  const backendReplacements = getBackendEnvReplacements(projectName, adminEmail, portOffset);
413
402
  const backendEnv = await generateEnvFromExample(
414
403
  path.resolve(targetFolder, "backend/.env.example"),
@@ -443,18 +432,14 @@ async function cleanTemplate({
443
432
  });
444
433
  }
445
434
  async function removeFolderContents(folderPath) {
446
- const files = await fs2.readdir(folderPath);
447
- await Promise.all(
448
- files.map(async (file) => {
449
- const filePath = path.join(folderPath, file);
450
- const stat = await fs2.lstat(filePath);
451
- if (stat.isDirectory()) {
452
- await fs2.rm(filePath, { recursive: true, force: true });
453
- } else {
454
- await fs2.rm(filePath);
455
- }
456
- })
457
- );
435
+ let files;
436
+ try {
437
+ files = await fs2.readdir(folderPath);
438
+ } catch (err) {
439
+ if (err.code === "ENOENT") return;
440
+ throw err;
441
+ }
442
+ await Promise.all(files.map((file) => fs2.rm(path.join(folderPath, file), { recursive: true, force: true })));
458
443
  }
459
444
  async function removeFileOrFolder(pathToRemove) {
460
445
  await fs2.rm(pathToRemove, { recursive: true, force: true });
@@ -473,29 +458,72 @@ ${warningMark} Source file "${src}" does not exist > Skip copy`);
473
458
  }
474
459
  }
475
460
  }
476
- async function applyPlaceholderConfig(targetFolder, projectName) {
477
- const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
478
- const src = await resolvePlaceholderConfigPath();
461
+ async function applyPlaceholderConfig(targetFolder, projectName, displayName) {
462
+ const src = path.resolve(targetFolder, PLACEHOLDER_CONFIG);
479
463
  const dest = path.resolve(targetFolder, "./shared/config/config.default.ts");
480
464
  try {
481
465
  let content = await fs2.readFile(src, "utf8");
482
466
  content = content.replaceAll("__project_name__", displayName);
483
467
  content = content.replaceAll("__project_slug__", projectName);
484
468
  await fs2.writeFile(dest, content, "utf8");
469
+ await fs2.rm(src, { force: true });
485
470
  } catch (err) {
486
471
  if (err.code === "ENOENT") {
487
472
  console.info(`
488
- ${warningMark} Placeholder config "${src}" not found > Skip`);
473
+ ${warningMark} Fork config template "${src}" not found > Skip`);
489
474
  } else {
490
475
  throw err;
491
476
  }
492
477
  }
493
478
  }
479
+ async function resetReleaseState(targetFolder, projectName) {
480
+ const pkgPath = path.resolve(targetFolder, "package.json");
481
+ try {
482
+ let pkg = await fs2.readFile(pkgPath, "utf8");
483
+ pkg = pkg.replace(/^(\s*"name":\s*)"[^"]*"/m, `$1"${projectName}"`);
484
+ pkg = pkg.replace(/^(\s*"version":\s*)"[^"]*"/m, `$1"${INITIAL_VERSION}"`);
485
+ for (const field of ["license", "repository", "description", "author", "homepage"]) {
486
+ pkg = pkg.replace(new RegExp(`^(\\s*"${field}":\\s*)"[^"]*"`, "m"), '$1""');
487
+ }
488
+ pkg = pkg.replace(/^(\s*"keywords":\s*)\[[^\]]*\]/m, "$1[]");
489
+ await fs2.writeFile(pkgPath, pkg, "utf8");
490
+ } catch (err) {
491
+ if (err.code === "ENOENT") {
492
+ console.info(`
493
+ ${warningMark} "package.json" not found > Skip version reset`);
494
+ } else {
495
+ throw err;
496
+ }
497
+ }
498
+ const configPath = path.resolve(targetFolder, ".github/release-please-config.json");
499
+ try {
500
+ const config = await fs2.readFile(configPath, "utf8");
501
+ await fs2.writeFile(configPath, config.replaceAll("info/CHANGELOG.md", "CHANGELOG.md"), "utf8");
502
+ } catch (err) {
503
+ if (err.code === "ENOENT") {
504
+ console.info(`
505
+ ${warningMark} "release-please-config.json" not found > Skip changelog path reset`);
506
+ } else {
507
+ throw err;
508
+ }
509
+ }
510
+ await fs2.writeFile(
511
+ path.resolve(targetFolder, ".github/release-please-manifest.json"),
512
+ `{
513
+ ".": "${INITIAL_VERSION}"
514
+ }
515
+ `,
516
+ "utf8"
517
+ );
518
+ await fs2.writeFile(path.resolve(targetFolder, "CHANGELOG.md"), "# Changelog\n", "utf8");
519
+ }
494
520
 
495
521
  // src/utils/download-template.ts
496
- import { mkdir, writeFile } from "fs/promises";
497
- import { dirname, join } from "path";
498
- import { parseTarGzip } from "nanotar";
522
+ import fs3 from "fs";
523
+ import { rm } from "fs/promises";
524
+ import { join } from "path";
525
+ import git2 from "isomorphic-git";
526
+ import http from "isomorphic-git/http/node";
499
527
  function parseGithubRepo(template) {
500
528
  const [owner, repo] = template.replace(/^github:/, "").split("/");
501
529
  if (!owner || !repo) {
@@ -505,22 +533,11 @@ function parseGithubRepo(template) {
505
533
  }
506
534
  async function downloadGithubTemplate(template, ref, targetFolder) {
507
535
  const { owner, repo } = parseGithubRepo(template);
508
- const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/${ref}`;
509
- const response = await fetch(url);
510
- if (!response.ok) {
511
- throw new Error(`Failed to download template ${owner}/${repo}@${ref}: ${response.status} ${response.statusText}`);
512
- }
513
- const archive = new Uint8Array(await response.arrayBuffer());
514
- const entries = await parseTarGzip(archive);
515
- for (const entry of entries) {
516
- if (entry.type !== "file" || !entry.data) continue;
517
- const relativePath = entry.name.replace(/^[^/]+\//, "");
518
- if (!relativePath) continue;
519
- const dest = join(targetFolder, relativePath);
520
- await mkdir(dirname(dest), { recursive: true });
521
- const mode = entry.attrs?.mode ? Number.parseInt(entry.attrs.mode, 8) : void 0;
522
- await writeFile(dest, entry.data, mode ? { mode } : void 0);
523
- }
536
+ const url = `https://github.com/${owner}/${repo}.git`;
537
+ await git2.clone({ fs: fs3, http, dir: targetFolder, url, ref, singleBranch: true, depth: 1 });
538
+ const baseSha = await git2.resolveRef({ fs: fs3, dir: targetFolder, ref: "HEAD" });
539
+ await rm(join(targetFolder, ".git"), { recursive: true, force: true });
540
+ return baseSha;
524
541
  }
525
542
 
526
543
  // src/utils/list-template-files.ts
@@ -605,7 +622,7 @@ function createProgress(title, silent = false) {
605
622
 
606
623
  // src/utils/scan-optional-modules.ts
607
624
  import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
608
- import { dirname as dirname2, join as join3, relative } from "path";
625
+ import { dirname, join as join3, relative } from "path";
609
626
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage"]);
610
627
  async function scanOptionalModules(targetFolder) {
611
628
  const files = [];
@@ -617,7 +634,7 @@ async function scanOptionalModules(targetFolder) {
617
634
  const name = src.match(/name:\s*['"]([^'"]+)['"]/)?.[1];
618
635
  const description = src.match(/description:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
619
636
  if (!name) continue;
620
- modules.push({ name, description, folder: relative(targetFolder, dirname2(file)) });
637
+ modules.push({ name, description, folder: relative(targetFolder, dirname(file)) });
621
638
  }
622
639
  return modules.sort((a, b) => a.name.localeCompare(b.name));
623
640
  }
@@ -643,6 +660,21 @@ async function collectModuleFiles(dir, out) {
643
660
  }
644
661
  }
645
662
 
663
+ // src/utils/write-scaffold-base.ts
664
+ import { mkdir, writeFile } from "fs/promises";
665
+ import { join as join4 } from "path";
666
+ var CELLA_DIR = ".cella";
667
+ var BASE_FILE = "base";
668
+ async function writeScaffoldBase(targetFolder, sha) {
669
+ const dir = join4(targetFolder, CELLA_DIR);
670
+ await mkdir(dir, { recursive: true });
671
+ const contents = `# Upstream cella commit this project was scaffolded from (create-cella).
672
+ # Used by \`pnpm cella sync\` to bootstrap the first merge-base. Do not edit or delete.
673
+ ${sha}
674
+ `;
675
+ await writeFile(join4(dir, BASE_FILE), contents, "utf8");
676
+ }
677
+
646
678
  // src/create.ts
647
679
  function isLocalPath(path2) {
648
680
  return path2.startsWith("/") || path2.startsWith("./") || path2.startsWith("../");
@@ -668,6 +700,7 @@ async function create({
668
700
  progress.step("creating project folder");
669
701
  await mkdir2(targetFolder, { recursive: true });
670
702
  process.chdir(targetFolder);
703
+ let baseSha = null;
671
704
  if (isLocalTemplate) {
672
705
  progress.step("copying local template");
673
706
  const sourcePath = resolve2(originalCwd, templateUrl);
@@ -681,22 +714,23 @@ async function create({
681
714
  }
682
715
  const tracked = await listTemplateFiles(sourcePath);
683
716
  for (const rel2 of tracked) {
684
- const src = join4(sourcePath, rel2);
717
+ const src = join5(sourcePath, rel2);
685
718
  if (!existsSync(src)) continue;
686
- const dest = join4(targetFolder, rel2);
687
- await mkdir2(dirname3(dest), { recursive: true });
719
+ const dest = join5(targetFolder, rel2);
720
+ await mkdir2(dirname2(dest), { recursive: true });
688
721
  await cp(src, dest);
689
722
  }
690
723
  } else {
691
724
  const ref = templateRef ?? "main";
692
725
  progress.step(`downloading cella template${templateRef ? ` (${templateRef})` : ""}`);
693
- await downloadGithubTemplate(template, ref, targetFolder);
726
+ baseSha = await downloadGithubTemplate(template, ref, targetFolder);
694
727
  }
695
728
  if (!silent) await promptOptionalModules(targetFolder);
696
729
  progress.step("cleaning template");
697
730
  const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
698
731
  await cleanTemplate({ targetFolder, projectName, displayName, portOffset, adminEmail });
699
- const gitFolderPath = join4(targetFolder, ".git");
732
+ if (baseSha) await writeScaffoldBase(targetFolder, baseSha);
733
+ const gitFolderPath = join5(targetFolder, ".git");
700
734
  if (!shouldSkipStep("git") && !existsSync(gitFolderPath)) {
701
735
  progress.step("initializing git");
702
736
  await gitInit(targetFolder);
@@ -741,14 +775,14 @@ async function promptOptionalModules(targetFolder) {
741
775
  confirmChoice("Optional modules", keep.length ? keep.join(", ") : pc5.dim("none"));
742
776
  resumeSpinner();
743
777
  const folders = modules.filter((m) => !keep.includes(m.name)).flatMap(optionalModuleFolders);
744
- await Promise.all(folders.map((f) => rm(join4(targetFolder, f), { recursive: true, force: true })));
778
+ await Promise.all(folders.map((f) => rm2(join5(targetFolder, f), { recursive: true, force: true })));
745
779
  }
746
780
 
747
781
  // src/utils/detect-used-ports.ts
748
782
  import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
749
- import { dirname as dirname4, join as join5 } from "path";
783
+ import { dirname as dirname3, join as join6 } from "path";
750
784
  async function detectUsedPorts(targetFolder) {
751
- const parentDir = dirname4(targetFolder);
785
+ const parentDir = dirname3(targetFolder);
752
786
  const used = [];
753
787
  let siblings;
754
788
  try {
@@ -757,7 +791,7 @@ async function detectUsedPorts(targetFolder) {
757
791
  return used;
758
792
  }
759
793
  for (const name of siblings) {
760
- const configPath = join5(parentDir, name, "shared/config/config.development.ts");
794
+ const configPath = join6(parentDir, name, "shared/config/config.development.ts");
761
795
  try {
762
796
  const content = await readFile3(configPath, "utf8");
763
797
  const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
@@ -785,22 +819,6 @@ function findNextOffset(usedPorts) {
785
819
  return 0;
786
820
  }
787
821
 
788
- // src/utils/extract-package-json-version-from-uri.ts
789
- async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
790
- const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
791
- const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${branch}`;
792
- try {
793
- const response = await fetch(apiUrl, {
794
- headers: { Accept: "application/vnd.github.raw+json" }
795
- });
796
- if (!response.ok) return "unknown";
797
- const packageJson = await response.json();
798
- return packageJson.version || "unknown";
799
- } catch {
800
- return "unknown";
801
- }
802
- }
803
-
804
822
  // src/utils/fetch-template-metadata.ts
805
823
  function parseRepo(repositoryUrl) {
806
824
  const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
@@ -817,7 +835,7 @@ async function fetchLatestRelease(repositoryUrl) {
817
835
  const response = await fetch(apiUrl, { headers: GITHUB_HEADERS });
818
836
  if (!response.ok) return null;
819
837
  const releases = await response.json();
820
- const release = releases.find((r) => !r.draft && !r.prerelease && /^v\d/.test(r.tag_name ?? ""));
838
+ const release = releases.find((r) => !r.draft && !r.prerelease && /^v?\d/.test(r.tag_name ?? ""));
821
839
  if (!release?.tag_name) return null;
822
840
  return { tag: release.tag_name, date: toDate(release.published_at) };
823
841
  } catch {
@@ -848,8 +866,7 @@ async function isEmptyDirectory(path2) {
848
866
 
849
867
  // src/create-cella-cli.ts
850
868
  async function main() {
851
- const templateVersion = await extractPackageJsonVersionFromUri(TEMPLATE_URL);
852
- showWelcome(templateVersion);
869
+ showWelcome();
853
870
  const promptTheme = { prefix: "", style: { answer: (text) => text } };
854
871
  const promptContext = { clearPromptOnDone: true };
855
872
  const templateRef = cli.options.template ? void 0 : await promptTemplateRef(promptTheme, promptContext);
@@ -878,7 +895,7 @@ async function main() {
878
895
  theme: promptTheme,
879
896
  choices: [
880
897
  { name: "Cancel and exit", value: "cancel" },
881
- { name: "Ignore existing files and continue", value: "ignore" }
898
+ { name: "Remove existing files and continue", value: "remove" }
882
899
  ]
883
900
  },
884
901
  promptContext
@@ -886,6 +903,9 @@ async function main() {
886
903
  if (action === "cancel") {
887
904
  process.exit(1);
888
905
  }
906
+ if (action === "remove") {
907
+ await rm3(targetFolder, { recursive: true, force: true });
908
+ }
889
909
  }
890
910
  const portOffset = cli.options.portOffset ?? await promptPortOffset(targetFolder, promptTheme, promptContext);
891
911
  confirmChoice("Port offset", formatOffset(portOffset));
@@ -925,8 +945,12 @@ async function promptTemplateRef(theme, context) {
925
945
  console.info(` ${pc6.gray(`https://github.com/${repo}/commit/${commit.sha}`)}`);
926
946
  }
927
947
  console.info();
928
- if (!release) return commit?.sha;
948
+ if (!release) return "main";
929
949
  if (!commit) return release.tag;
950
+ if (!process.stdin.isTTY) {
951
+ confirmChoice("Latest release", release.tag);
952
+ return release.tag;
953
+ }
930
954
  const choice = await select(
931
955
  {
932
956
  message: "Start from",
@@ -943,7 +967,7 @@ async function promptTemplateRef(theme, context) {
943
967
  return release.tag;
944
968
  }
945
969
  confirmChoice("Latest commit", commit.shortSha);
946
- return commit.sha;
970
+ return "main";
947
971
  }
948
972
  function formatOffset(o, suffix = "") {
949
973
  return `${o} \u2192 :${3e3 + o} / :${4e3 + o} / :${5432 + o}${suffix}`;