aboutsummaryrefslogtreecommitdiff
path: root/packages/create-astro/src/actions
diff options
context:
space:
mode:
authorGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
committerGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
commite586d7d704d475afe3373a1de6ae20d504f79d6d (patch)
tree7e3fa24807cebd48a86bd40f866d792181191ee9 /packages/create-astro/src/actions
downloadastro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.gz
astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.zst
astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.zip
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'packages/create-astro/src/actions')
-rw-r--r--packages/create-astro/src/actions/context.ts146
-rw-r--r--packages/create-astro/src/actions/dependencies.ts109
-rw-r--r--packages/create-astro/src/actions/git.ts64
-rw-r--r--packages/create-astro/src/actions/help.ts24
-rw-r--r--packages/create-astro/src/actions/intro.ts29
-rw-r--r--packages/create-astro/src/actions/next-steps.ts25
-rw-r--r--packages/create-astro/src/actions/project-name.ts74
-rw-r--r--packages/create-astro/src/actions/shared.ts59
-rw-r--r--packages/create-astro/src/actions/template.ts169
-rw-r--r--packages/create-astro/src/actions/verify.ts40
10 files changed, 739 insertions, 0 deletions
diff --git a/packages/create-astro/src/actions/context.ts b/packages/create-astro/src/actions/context.ts
new file mode 100644
index 000000000..ce4c067fd
--- /dev/null
+++ b/packages/create-astro/src/actions/context.ts
@@ -0,0 +1,146 @@
+import os from 'node:os';
+import { type Task, prompt } from '@astrojs/cli-kit';
+import { random } from '@astrojs/cli-kit/utils';
+import arg from 'arg';
+
+import getSeasonalData from '../data/seasonal.js';
+import { getName, getVersion } from '../messages.js';
+
+export interface Context {
+ help: boolean;
+ prompt: typeof prompt;
+ cwd: string;
+ packageManager: string;
+ username: Promise<string>;
+ version: Promise<string>;
+ skipHouston: boolean;
+ fancy?: boolean;
+ add?: string[];
+ dryRun?: boolean;
+ yes?: boolean;
+ projectName?: string;
+ template?: string;
+ ref: string;
+ install?: boolean;
+ git?: boolean;
+ typescript?: string;
+ stdin?: typeof process.stdin;
+ stdout?: typeof process.stdout;
+ exit(code: number): never;
+ welcome?: string;
+ hat?: string;
+ tie?: string;
+ tasks: Task[];
+}
+
+function getPackageTag(packageSpecifier: string | undefined): string | undefined {
+ switch (packageSpecifier) {
+ case 'alpha':
+ case 'beta':
+ case 'rc':
+ return packageSpecifier;
+ // Will fallback to latest
+ case undefined:
+ default:
+ return undefined;
+ }
+}
+
+export async function getContext(argv: string[]): Promise<Context> {
+ const packageSpecifier = argv
+ .find((argItem) => /^(astro|create-astro)@/.exec(argItem))
+ ?.split('@')[1];
+
+ const flags = arg(
+ {
+ '--template': String,
+ '--ref': String,
+ '--yes': Boolean,
+ '--no': Boolean,
+ '--install': Boolean,
+ '--no-install': Boolean,
+ '--git': Boolean,
+ '--no-git': Boolean,
+ '--skip-houston': Boolean,
+ '--dry-run': Boolean,
+ '--help': Boolean,
+ '--fancy': Boolean,
+ '--add': [String],
+
+ '-y': '--yes',
+ '-n': '--no',
+ '-h': '--help',
+ },
+ { argv, permissive: true },
+ );
+
+ const packageManager = detectPackageManager() ?? 'npm';
+ let cwd = flags['_'][0];
+ let {
+ '--help': help = false,
+ '--template': template,
+ '--no': no,
+ '--yes': yes,
+ '--install': install,
+ '--no-install': noInstall,
+ '--git': git,
+ '--no-git': noGit,
+ '--fancy': fancy,
+ '--skip-houston': skipHouston,
+ '--dry-run': dryRun,
+ '--ref': ref,
+ '--add': add,
+ } = flags;
+ let projectName = cwd;
+
+ if (no) {
+ yes = false;
+ if (install == undefined) install = false;
+ if (git == undefined) git = false;
+ }
+
+ skipHouston =
+ ((os.platform() === 'win32' && !fancy) || skipHouston) ??
+ [yes, no, install, git].some((v) => v !== undefined);
+
+ const { messages, hats, ties } = getSeasonalData({ fancy });
+
+ const context: Context = {
+ help,
+ prompt,
+ packageManager,
+ username: getName(),
+ version: getVersion(
+ packageManager,
+ 'astro',
+ getPackageTag(packageSpecifier),
+ process.env.ASTRO_VERSION,
+ ),
+ skipHouston,
+ fancy,
+ add,
+ dryRun,
+ projectName,
+ template,
+ ref: ref ?? 'latest',
+ welcome: random(messages),
+ hat: hats ? random(hats) : undefined,
+ tie: ties ? random(ties) : undefined,
+ yes,
+ install: install ?? (noInstall ? false : undefined),
+ git: git ?? (noGit ? false : undefined),
+ cwd,
+ exit(code) {
+ process.exit(code);
+ },
+ tasks: [],
+ };
+ return context;
+}
+
+function detectPackageManager() {
+ if (!process.env.npm_config_user_agent) return;
+ const specifier = process.env.npm_config_user_agent.split(' ')[0];
+ const name = specifier.substring(0, specifier.lastIndexOf('/'));
+ return name === 'npminstall' ? 'cnpm' : name;
+}
diff --git a/packages/create-astro/src/actions/dependencies.ts b/packages/create-astro/src/actions/dependencies.ts
new file mode 100644
index 000000000..d4990a8fb
--- /dev/null
+++ b/packages/create-astro/src/actions/dependencies.ts
@@ -0,0 +1,109 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { color } from '@astrojs/cli-kit';
+import { error, info, title } from '../messages.js';
+import { shell } from '../shell.js';
+import type { Context } from './context.js';
+
+export async function dependencies(
+ ctx: Pick<
+ Context,
+ 'install' | 'yes' | 'prompt' | 'packageManager' | 'cwd' | 'dryRun' | 'tasks' | 'add'
+ >,
+) {
+ let deps = ctx.install ?? ctx.yes;
+ if (deps === undefined) {
+ ({ deps } = await ctx.prompt({
+ name: 'deps',
+ type: 'confirm',
+ label: title('deps'),
+ message: `Install dependencies?`,
+ hint: 'recommended',
+ initial: true,
+ }));
+ ctx.install = deps;
+ }
+
+ ctx.add = ctx.add?.reduce<string[]>((acc, item) => acc.concat(item.split(',')), []);
+
+ if (ctx.dryRun) {
+ await info(
+ '--dry-run',
+ `Skipping dependency installation${ctx.add ? ` and adding ${ctx.add.join(', ')}` : ''}`,
+ );
+ } else if (deps) {
+ ctx.tasks.push({
+ pending: 'Dependencies',
+ start: `Dependencies installing with ${ctx.packageManager}...`,
+ end: 'Dependencies installed',
+ onError: (e) => {
+ error('error', e);
+ error(
+ 'error',
+ `Dependencies failed to install, please run ${color.bold(
+ ctx.packageManager + ' install',
+ )} to install them manually after setup.`,
+ );
+ },
+ while: () => install({ packageManager: ctx.packageManager, cwd: ctx.cwd }),
+ });
+
+ let add = ctx.add;
+
+ if (add) {
+ ctx.tasks.push({
+ pending: 'Integrations',
+ start: `Adding integrations with astro add`,
+ end: 'Integrations added',
+ onError: (e) => {
+ error('error', e);
+ error(
+ 'error',
+ `Failed to add integrations, please run ${color.bold(
+ `astro add ${add.join(' ')}`,
+ )} to install them manually after setup.`,
+ );
+ },
+ while: () =>
+ astroAdd({ integrations: add, packageManager: ctx.packageManager, cwd: ctx.cwd }),
+ });
+ }
+ } else {
+ await info(
+ ctx.yes === false ? 'deps [skip]' : 'No problem!',
+ 'Remember to install dependencies after setup.',
+ );
+ }
+}
+
+async function astroAdd({
+ integrations,
+ packageManager,
+ cwd,
+}: { integrations: string[]; packageManager: string; cwd: string }) {
+ if (packageManager === 'yarn') await ensureYarnLock({ cwd });
+ return shell(
+ packageManager === 'npm' ? 'npx' : `${packageManager} dlx`,
+ ['astro add', integrations.join(' '), '-y'],
+ { cwd, timeout: 90_000, stdio: 'ignore' },
+ );
+}
+
+async function install({ packageManager, cwd }: { packageManager: string; cwd: string }) {
+ if (packageManager === 'yarn') await ensureYarnLock({ cwd });
+ return shell(packageManager, ['install'], { cwd, timeout: 90_000, stdio: 'ignore' });
+}
+
+/**
+ * Yarn Berry (PnP) versions will throw an error if there isn't an existing `yarn.lock` file
+ * If a `yarn.lock` file doesn't exist, this function writes an empty `yarn.lock` one.
+ * Unfortunately this hack is required to run `yarn install`.
+ *
+ * The empty `yarn.lock` file is immediately overwritten by the installation process.
+ * See https://github.com/withastro/astro/pull/8028
+ */
+async function ensureYarnLock({ cwd }: { cwd: string }) {
+ const yarnLock = path.join(cwd, 'yarn.lock');
+ if (fs.existsSync(yarnLock)) return;
+ return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' });
+}
diff --git a/packages/create-astro/src/actions/git.ts b/packages/create-astro/src/actions/git.ts
new file mode 100644
index 000000000..ebedb8701
--- /dev/null
+++ b/packages/create-astro/src/actions/git.ts
@@ -0,0 +1,64 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import type { Context } from './context.js';
+
+import { color } from '@astrojs/cli-kit';
+import { error, info, title } from '../messages.js';
+import { shell } from '../shell.js';
+
+export async function git(
+ ctx: Pick<Context, 'cwd' | 'git' | 'yes' | 'prompt' | 'dryRun' | 'tasks'>,
+) {
+ if (fs.existsSync(path.join(ctx.cwd, '.git'))) {
+ await info('Nice!', `Git has already been initialized`);
+ return;
+ }
+ let _git = ctx.git ?? ctx.yes;
+ if (_git === undefined) {
+ ({ git: _git } = await ctx.prompt({
+ name: 'git',
+ type: 'confirm',
+ label: title('git'),
+ message: `Initialize a new git repository?`,
+ hint: 'optional',
+ initial: true,
+ }));
+ }
+
+ if (ctx.dryRun) {
+ await info('--dry-run', `Skipping Git initialization`);
+ } else if (_git) {
+ ctx.tasks.push({
+ pending: 'Git',
+ start: 'Git initializing...',
+ end: 'Git initialized',
+ while: () =>
+ init({ cwd: ctx.cwd }).catch((e) => {
+ error('error', e);
+ process.exit(1);
+ }),
+ });
+ } else {
+ await info(
+ ctx.yes === false ? 'git [skip]' : 'Sounds good!',
+ `You can always run ${color.reset('git init')}${color.dim(' manually.')}`,
+ );
+ }
+}
+
+async function init({ cwd }: { cwd: string }) {
+ try {
+ await shell('git', ['init'], { cwd, stdio: 'ignore' });
+ await shell('git', ['add', '-A'], { cwd, stdio: 'ignore' });
+ await shell(
+ 'git',
+ [
+ 'commit',
+ '-m',
+ '"Initial commit from Astro"',
+ '--author="houston[bot] <astrobot-houston@users.noreply.github.com>"',
+ ],
+ { cwd, stdio: 'ignore' },
+ );
+ } catch {}
+}
diff --git a/packages/create-astro/src/actions/help.ts b/packages/create-astro/src/actions/help.ts
new file mode 100644
index 000000000..1d5c7f609
--- /dev/null
+++ b/packages/create-astro/src/actions/help.ts
@@ -0,0 +1,24 @@
+import { printHelp } from '../messages.js';
+
+export function help() {
+ printHelp({
+ commandName: 'create-astro',
+ usage: '[dir] [...flags]',
+ headline: 'Scaffold Astro projects.',
+ tables: {
+ Flags: [
+ ['--help (-h)', 'See all available flags.'],
+ ['--template <name>', 'Specify your template.'],
+ ['--install / --no-install', 'Install dependencies (or not).'],
+ ['--add <integrations>', 'Add integrations.'],
+ ['--git / --no-git', 'Initialize git repo (or not).'],
+ ['--yes (-y)', 'Skip all prompts by accepting defaults.'],
+ ['--no (-n)', 'Skip all prompts by declining defaults.'],
+ ['--dry-run', 'Walk through steps without executing.'],
+ ['--skip-houston', 'Skip Houston animation.'],
+ ['--ref', 'Choose astro branch (default: latest).'],
+ ['--fancy', 'Enable full Unicode support for Windows.'],
+ ],
+ },
+ });
+}
diff --git a/packages/create-astro/src/actions/intro.ts b/packages/create-astro/src/actions/intro.ts
new file mode 100644
index 000000000..0249e63c8
--- /dev/null
+++ b/packages/create-astro/src/actions/intro.ts
@@ -0,0 +1,29 @@
+import type { Context } from './context.js';
+
+import { color, label } from '@astrojs/cli-kit';
+import { banner, say } from '../messages.js';
+
+export async function intro(
+ ctx: Pick<Context, 'skipHouston' | 'welcome' | 'hat' | 'tie' | 'version' | 'username' | 'fancy'>,
+) {
+ banner();
+
+ if (!ctx.skipHouston) {
+ const { welcome, hat, tie } = ctx;
+ await say(
+ [
+ [
+ 'Welcome',
+ 'to',
+ label('astro', color.bgGreen, color.black),
+ Promise.resolve(ctx.version).then(
+ (version) => (version ? color.green(`v${version}`) : '') + ',',
+ ),
+ Promise.resolve(ctx.username).then((username) => `${username}!`),
+ ],
+ welcome ?? "Let's build something awesome!",
+ ] as string[],
+ { clear: true, hat, tie },
+ );
+ }
+}
diff --git a/packages/create-astro/src/actions/next-steps.ts b/packages/create-astro/src/actions/next-steps.ts
new file mode 100644
index 000000000..91536cc46
--- /dev/null
+++ b/packages/create-astro/src/actions/next-steps.ts
@@ -0,0 +1,25 @@
+import path from 'node:path';
+import type { Context } from './context.js';
+
+import { nextSteps, say } from '../messages.js';
+
+export async function next(
+ ctx: Pick<Context, 'hat' | 'tie' | 'cwd' | 'packageManager' | 'skipHouston'>,
+) {
+ let projectDir = path.relative(process.cwd(), ctx.cwd);
+
+ const commandMap: { [key: string]: string } = {
+ npm: 'npm run dev',
+ bun: 'bun run dev',
+ yarn: 'yarn dev',
+ pnpm: 'pnpm dev',
+ };
+
+ const devCmd = commandMap[ctx.packageManager as keyof typeof commandMap] || 'npm run dev';
+ await nextSteps({ projectDir, devCmd });
+
+ if (!ctx.skipHouston) {
+ await say(['Good luck out there, astronaut! 🚀'], { hat: ctx.hat, tie: ctx.tie });
+ }
+ return;
+}
diff --git a/packages/create-astro/src/actions/project-name.ts b/packages/create-astro/src/actions/project-name.ts
new file mode 100644
index 000000000..26938de80
--- /dev/null
+++ b/packages/create-astro/src/actions/project-name.ts
@@ -0,0 +1,74 @@
+import type { Context } from './context.js';
+
+import path from 'node:path';
+import { color, generateProjectName } from '@astrojs/cli-kit';
+import { info, log, title } from '../messages.js';
+
+import { isEmpty, toValidName } from './shared.js';
+
+export async function projectName(
+ ctx: Pick<Context, 'cwd' | 'yes' | 'dryRun' | 'prompt' | 'projectName' | 'exit'>,
+) {
+ await checkCwd(ctx.cwd);
+
+ if (!ctx.cwd || !isEmpty(ctx.cwd)) {
+ if (!isEmpty(ctx.cwd)) {
+ await info('Hmm...', `${color.reset(`"${ctx.cwd}"`)}${color.dim(` is not empty!`)}`);
+ }
+
+ if (ctx.yes) {
+ ctx.projectName = generateProjectName();
+ ctx.cwd = `./${ctx.projectName}`;
+ await info('dir', `Project created at ./${ctx.projectName}`);
+ return;
+ }
+
+ const { name } = await ctx.prompt({
+ name: 'name',
+ type: 'text',
+ label: title('dir'),
+ message: 'Where should we create your new project?',
+ initial: `./${generateProjectName()}`,
+ validate(value: string) {
+ if (!isEmpty(value)) {
+ return `Directory is not empty!`;
+ }
+ // Check for non-printable characters
+ if (value.match(/[^\x20-\x7E]/g) !== null)
+ return `Invalid non-printable character present!`;
+ return true;
+ },
+ });
+
+ ctx.cwd = name!.trim();
+ ctx.projectName = toValidName(name!);
+ if (ctx.dryRun) {
+ await info('--dry-run', 'Skipping project naming');
+ return;
+ }
+ } else {
+ let name = ctx.cwd;
+ if (name === '.' || name === './') {
+ const parts = process.cwd().split(path.sep);
+ name = parts[parts.length - 1];
+ } else if (name.startsWith('./') || name.startsWith('../')) {
+ const parts = name.split('/');
+ name = parts[parts.length - 1];
+ }
+ ctx.projectName = toValidName(name);
+ }
+
+ if (!ctx.cwd) {
+ ctx.exit(1);
+ }
+}
+
+async function checkCwd(cwd: string | undefined) {
+ const empty = cwd && isEmpty(cwd);
+ if (empty) {
+ log('');
+ await info('dir', `Using ${color.reset(cwd)}${color.dim(' as project directory')}`);
+ }
+
+ return empty;
+}
diff --git a/packages/create-astro/src/actions/shared.ts b/packages/create-astro/src/actions/shared.ts
new file mode 100644
index 000000000..da19677d0
--- /dev/null
+++ b/packages/create-astro/src/actions/shared.ts
@@ -0,0 +1,59 @@
+import fs from 'node:fs';
+
+// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory.
+// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934
+const VALID_PROJECT_DIRECTORY_SAFE_LIST = [
+ '.DS_Store',
+ '.git',
+ '.gitkeep',
+ '.gitattributes',
+ '.gitignore',
+ '.gitlab-ci.yml',
+ '.hg',
+ '.hgcheck',
+ '.hgignore',
+ '.idea',
+ '.npmignore',
+ '.travis.yml',
+ '.yarn',
+ '.yarnrc.yml',
+ 'docs',
+ 'LICENSE',
+ 'mkdocs.yml',
+ 'Thumbs.db',
+ /\.iml$/,
+ /^npm-debug\.log/,
+ /^yarn-debug\.log/,
+ /^yarn-error\.log/,
+];
+
+export function isEmpty(dirPath: string) {
+ if (!fs.existsSync(dirPath)) {
+ return true;
+ }
+
+ const conflicts = fs.readdirSync(dirPath).filter((content) => {
+ return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => {
+ return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content);
+ });
+ });
+
+ return conflicts.length === 0;
+}
+
+function isValidName(projectName: string) {
+ return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
+}
+
+export function toValidName(projectName: string) {
+ if (isValidName(projectName)) return projectName;
+
+ return projectName
+ .trim()
+ .toLowerCase()
+ .replace(/\s+/g, '-')
+ .replace(/^[._]/, '')
+ .replace(/[^a-z\d\-~]+/g, '-')
+ .replace(/^-+/, '')
+ .replace(/-+$/, '');
+}
diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts
new file mode 100644
index 000000000..cf58d90a8
--- /dev/null
+++ b/packages/create-astro/src/actions/template.ts
@@ -0,0 +1,169 @@
+import type { Context } from './context.js';
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { color } from '@astrojs/cli-kit';
+import { downloadTemplate } from '@bluwy/giget-core';
+import { error, info, title } from '../messages.js';
+
+export async function template(
+ ctx: Pick<Context, 'template' | 'prompt' | 'yes' | 'dryRun' | 'exit' | 'tasks'>,
+) {
+ if (!ctx.template && ctx.yes) ctx.template = 'basics';
+
+ if (ctx.template) {
+ await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`);
+ } else {
+ const { template: tmpl } = await ctx.prompt({
+ name: 'template',
+ type: 'select',
+ label: title('tmpl'),
+ message: 'How would you like to start your new project?',
+ initial: 'basics',
+ choices: [
+ { value: 'basics', label: 'A basic, helpful starter project', hint: '(recommended)' },
+ { value: 'blog', label: 'Use blog template' },
+ { value: 'starlight', label: 'Use docs (Starlight) template' },
+ { value: 'minimal', label: 'Use minimal (empty) template' },
+ ],
+ });
+ ctx.template = tmpl;
+ }
+
+ if (ctx.dryRun) {
+ await info('--dry-run', `Skipping template copying`);
+ } else if (ctx.template) {
+ ctx.tasks.push({
+ pending: 'Template',
+ start: 'Template copying...',
+ end: 'Template copied',
+ while: () =>
+ copyTemplate(ctx.template!, ctx as Context).catch((e) => {
+ if (e instanceof Error) {
+ error('error', e.message);
+ process.exit(1);
+ } else {
+ error('error', 'Unable to clone template.');
+ process.exit(1);
+ }
+ }),
+ });
+ } else {
+ ctx.exit(1);
+ }
+}
+
+// some files are only needed for online editors when using astro.new. Remove for create-astro installs.
+const FILES_TO_REMOVE = ['CHANGELOG.md', '.codesandbox'];
+const FILES_TO_UPDATE = {
+ 'package.json': (file: string, overrides: { name: string }) =>
+ fs.promises.readFile(file, 'utf-8').then((value) => {
+ // Match first indent in the file or fallback to `\t`
+ const indent = /(^\s+)/m.exec(value)?.[1] ?? '\t';
+ return fs.promises.writeFile(
+ file,
+ JSON.stringify(
+ Object.assign(JSON.parse(value), Object.assign(overrides, { private: undefined })),
+ null,
+ indent,
+ ),
+ 'utf-8',
+ );
+ }),
+};
+
+export function getTemplateTarget(tmpl: string, ref = 'latest') {
+ // Handle Starlight templates
+ if (tmpl.startsWith('starlight')) {
+ const [, starter = 'basics'] = tmpl.split('/');
+ return `github:withastro/starlight/examples/${starter}`;
+ }
+
+ // Handle third-party templates
+ const isThirdParty = tmpl.includes('/');
+ if (isThirdParty) return tmpl;
+
+ // Handle Astro templates
+ if (ref === 'latest') {
+ // `latest` ref is specially handled to route to a branch specifically
+ // to allow faster downloads. Otherwise giget has to download the entire
+ // repo and only copy a sub directory
+ return `github:withastro/astro#examples/${tmpl}`;
+ } else {
+ return `github:withastro/astro/examples/${tmpl}#${ref}`;
+ }
+}
+
+async function copyTemplate(tmpl: string, ctx: Context) {
+ const templateTarget = getTemplateTarget(tmpl, ctx.ref);
+ // Copy
+ if (!ctx.dryRun) {
+ try {
+ await downloadTemplate(templateTarget, {
+ force: true,
+ cwd: ctx.cwd,
+ dir: '.',
+ });
+
+ // Modify the README file to reflect the correct package manager
+ if (ctx.packageManager !== 'npm') {
+ const readmePath = path.resolve(ctx.cwd, 'README.md');
+ const readme = fs.readFileSync(readmePath, 'utf8');
+
+ // `run` is removed since it's optional in other package managers
+ const updatedReadme = readme
+ .replace(/\bnpm run\b/g, ctx.packageManager)
+ .replace(/\bnpm\b/g, ctx.packageManager);
+
+ fs.writeFileSync(readmePath, updatedReadme);
+ }
+ } catch (err: any) {
+ // Only remove the directory if it's most likely created by us.
+ if (ctx.cwd !== '.' && ctx.cwd !== './' && !ctx.cwd.startsWith('../')) {
+ try {
+ fs.rmdirSync(ctx.cwd);
+ } catch (_) {
+ // Ignore any errors from removing the directory,
+ // make sure we throw and display the original error.
+ }
+ }
+
+ if (err.message?.includes('404')) {
+ throw new Error(`Template ${color.reset(tmpl)} ${color.dim('does not exist!')}`);
+ }
+
+ if (err.message) {
+ error('error', err.message);
+ }
+ try {
+ // The underlying error is often buried deep in the `cause` property
+ // This is in a try/catch block in case of weirdnesses in accessing the `cause` property
+ if ('cause' in err) {
+ // This is probably included in err.message, but we can log it just in case it has extra info
+ error('error', err.cause);
+ if ('cause' in err.cause) {
+ // Hopefully the actual fetch error message
+ error('error', err.cause?.cause);
+ }
+ }
+ } catch {}
+ throw new Error(`Unable to download template ${color.reset(tmpl)}`);
+ }
+
+ // Post-process in parallel
+ const removeFiles = FILES_TO_REMOVE.map(async (file) => {
+ const fileLoc = path.resolve(path.join(ctx.cwd, file));
+ if (fs.existsSync(fileLoc)) {
+ return fs.promises.rm(fileLoc, { recursive: true });
+ }
+ });
+ const updateFiles = Object.entries(FILES_TO_UPDATE).map(async ([file, update]) => {
+ const fileLoc = path.resolve(path.join(ctx.cwd, file));
+ if (fs.existsSync(fileLoc)) {
+ return update(fileLoc, { name: ctx.projectName! });
+ }
+ });
+
+ await Promise.all([...removeFiles, ...updateFiles]);
+ }
+}
diff --git a/packages/create-astro/src/actions/verify.ts b/packages/create-astro/src/actions/verify.ts
new file mode 100644
index 000000000..7f446c869
--- /dev/null
+++ b/packages/create-astro/src/actions/verify.ts
@@ -0,0 +1,40 @@
+import type { Context } from './context.js';
+
+import dns from 'node:dns/promises';
+import { color } from '@astrojs/cli-kit';
+import { verifyTemplate } from '@bluwy/giget-core';
+import { bannerAbort, error, info, log } from '../messages.js';
+import { getTemplateTarget } from './template.js';
+
+export async function verify(
+ ctx: Pick<Context, 'version' | 'dryRun' | 'template' | 'ref' | 'exit'>,
+) {
+ if (!ctx.dryRun) {
+ const online = await isOnline();
+ if (!online) {
+ bannerAbort();
+ log('');
+ error('error', `Unable to connect to the internet.`);
+ ctx.exit(1);
+ }
+ }
+
+ if (ctx.template) {
+ const target = getTemplateTarget(ctx.template, ctx.ref);
+ const ok = await verifyTemplate(target);
+ if (!ok) {
+ bannerAbort();
+ log('');
+ error('error', `Template ${color.reset(ctx.template)} ${color.dim('could not be found!')}`);
+ await info('check', 'https://astro.build/examples');
+ ctx.exit(1);
+ }
+ }
+}
+
+function isOnline(): Promise<boolean> {
+ return dns.lookup('github.com').then(
+ () => true,
+ () => false,
+ );
+}