summaryrefslogtreecommitdiff
path: root/packages/create-astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/create-astro/src')
-rw-r--r--packages/create-astro/src/components/App.tsx93
-rw-r--r--packages/create-astro/src/components/Confirm.tsx49
-rw-r--r--packages/create-astro/src/components/Emoji.tsx5
-rw-r--r--packages/create-astro/src/components/Exit.tsx9
-rw-r--r--packages/create-astro/src/components/Finalize.tsx27
-rw-r--r--packages/create-astro/src/components/Header.tsx20
-rw-r--r--packages/create-astro/src/components/Help.tsx62
-rw-r--r--packages/create-astro/src/components/Install.tsx19
-rw-r--r--packages/create-astro/src/components/ProjectName.tsx24
-rw-r--r--packages/create-astro/src/components/Select.tsx32
-rw-r--r--packages/create-astro/src/components/Spacer.tsx5
-rw-r--r--packages/create-astro/src/components/Spinner.tsx200
-rw-r--r--packages/create-astro/src/components/Template.tsx23
-rw-r--r--packages/create-astro/src/components/Version.tsx6
-rw-r--r--packages/create-astro/src/config.ts49
-rw-r--r--packages/create-astro/src/index.tsx46
-rw-r--r--packages/create-astro/src/templates/blank/README.md24
-rw-r--r--packages/create-astro/src/templates/blank/_gitignore18
-rw-r--r--packages/create-astro/src/templates/blank/meta.json4
-rw-r--r--packages/create-astro/src/templates/blank/package.json11
-rw-r--r--packages/create-astro/src/templates/blank/public/favicon.svg11
-rw-r--r--packages/create-astro/src/templates/blank/src/pages/index.astro15
-rw-r--r--packages/create-astro/src/templates/starter/README.md32
-rw-r--r--packages/create-astro/src/templates/starter/_gitignore18
-rw-r--r--packages/create-astro/src/templates/starter/astro.config.mjs16
-rw-r--r--packages/create-astro/src/templates/starter/meta.json5
-rw-r--r--packages/create-astro/src/templates/starter/package.json11
-rw-r--r--packages/create-astro/src/templates/starter/public/assets/logo.svg12
-rw-r--r--packages/create-astro/src/templates/starter/public/favicon.svg11
-rw-r--r--packages/create-astro/src/templates/starter/public/robots.txt2
-rw-r--r--packages/create-astro/src/templates/starter/public/style/global.css28
-rw-r--r--packages/create-astro/src/templates/starter/public/style/home.css38
-rw-r--r--packages/create-astro/src/templates/starter/src/components/Tour.astro82
-rw-r--r--packages/create-astro/src/templates/starter/src/pages/index.astro38
-rw-r--r--packages/create-astro/src/utils.ts147
35 files changed, 1192 insertions, 0 deletions
diff --git a/packages/create-astro/src/components/App.tsx b/packages/create-astro/src/components/App.tsx
new file mode 100644
index 000000000..fd9192bb6
--- /dev/null
+++ b/packages/create-astro/src/components/App.tsx
@@ -0,0 +1,93 @@
+import React, {FC, useEffect} from 'react';
+import { prepareTemplate, isEmpty, emptyDir } from '../utils';
+import Header from './Header';
+import Install from './Install';
+import ProjectName from './ProjectName';
+import Template from './Template';
+import Confirm from './Confirm';
+import Finalize from './Finalize';
+
+interface Context {
+ use: 'npm'|'yarn';
+ run: boolean;
+ projectExists?: boolean;
+ force?: boolean;
+ projectName?: string;
+ template?: string;
+ templates: string[];
+ ready?: boolean;
+}
+
+const getStep = ({ projectName, projectExists: exists, template, force, ready }: Context) => {
+ switch (true) {
+ case !projectName: return {
+ key: 'projectName',
+ Component: ProjectName
+ };
+ case projectName && exists === true && typeof force === 'undefined': return {
+ key: 'force',
+ Component: Confirm
+ }
+ case (exists === false || force) && !template: return {
+ key: 'template',
+ Component: Template
+ };
+ case !ready: return {
+ key: 'install',
+ Component: Install
+ };
+ default: return {
+ key: 'final',
+ Component: Finalize
+ }
+ }
+}
+
+const App: FC<{ context: Context }> = ({ context }) => {
+ const [state, setState] = React.useState(context);
+ const step = React.useRef(getStep(context));
+ const onSubmit = (value: string|boolean) => {
+ const { key } = step.current;
+ const newState = { ...state, [key]: value };
+ step.current = getStep(newState)
+ setState(newState)
+ }
+
+ useEffect(() => {
+ let isSubscribed = true
+ if (state.projectName && typeof state.projectExists === 'undefined') {
+ const newState = { ...state, projectExists: !isEmpty(state.projectName) };
+ step.current = getStep(newState)
+ if (isSubscribed) {
+ setState(newState);
+ }
+ }
+
+ if (state.projectName && (state.projectExists === false || state.force) && state.template) {
+ if (state.force) emptyDir(state.projectName);
+ prepareTemplate(context.use, state.template, state.projectName).then(() => {
+ if (isSubscribed) {
+ setState(v => {
+ const newState = {...v, ready: true };
+ step.current = getStep(newState);
+ return newState;
+ });
+ }
+ });
+ }
+
+ return () => {
+ isSubscribed = false;
+ }
+ }, [state]);
+ const { Component } = step.current;
+
+ return (
+ <>
+ <Header context={state}/>
+ <Component context={state} onSubmit={onSubmit} />
+ </>
+ )
+};
+
+export default App;
diff --git a/packages/create-astro/src/components/Confirm.tsx b/packages/create-astro/src/components/Confirm.tsx
new file mode 100644
index 000000000..1fd1aa862
--- /dev/null
+++ b/packages/create-astro/src/components/Confirm.tsx
@@ -0,0 +1,49 @@
+import React, { FC } from 'react';
+import { Box, Text, useInput, useApp } from 'ink';
+import Spacer from './Spacer';
+import Select from './Select';
+
+const Confirm: FC<{ message?: any; context: any; onSubmit: (value: boolean) => void }> = ({ message, context: { projectName }, onSubmit }) => {
+ const { exit } = useApp();
+ const handleSubmit = (v: boolean) => {
+ if (!v) return exit();
+ onSubmit(v);
+ };
+
+ return (
+ <>
+ <Box display="flex">
+ {!message ? (
+ <>
+ <Text color="#FFBE2D">{'[uh-oh]'}</Text>
+ <Text>
+ {' '}
+ It appears <Text color="#17C083">./{projectName}</Text> is not empty. Overwrite?
+ </Text>
+ </>
+ ) : (
+ message
+ )}
+ </Box>
+ <Box display="flex">
+ <Spacer width={6} />
+ <Select
+ items={[
+ {
+ value: false,
+ label: 'no'
+ },
+ {
+ value: true,
+ label: 'yes',
+ description: <Text color="#FF1639">overwrite</Text>,
+ },
+ ]}
+ onSelect={handleSubmit}
+ />
+ </Box>
+ </>
+ );
+};
+
+export default Confirm;
diff --git a/packages/create-astro/src/components/Emoji.tsx b/packages/create-astro/src/components/Emoji.tsx
new file mode 100644
index 000000000..3af9ae508
--- /dev/null
+++ b/packages/create-astro/src/components/Emoji.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+import { Text } from 'ink';
+import { isWin } from '../utils';
+
+export default ({ children }) => isWin() ? null : <Text>{children}</Text>
diff --git a/packages/create-astro/src/components/Exit.tsx b/packages/create-astro/src/components/Exit.tsx
new file mode 100644
index 000000000..cc3096705
--- /dev/null
+++ b/packages/create-astro/src/components/Exit.tsx
@@ -0,0 +1,9 @@
+import React, { FC } from 'react';
+import { Box, Text } from 'ink';
+import { isDone } from '../utils';
+
+const Exit: FC<{ didError?: boolean }> = ({ didError }) => isDone ? null : <Box marginTop={1} display="flex">
+ <Text color={didError ? "#FF1639" : "#FFBE2D"}>[abort]</Text>
+ <Text> astro cancelled</Text>
+</Box>
+export default Exit;
diff --git a/packages/create-astro/src/components/Finalize.tsx b/packages/create-astro/src/components/Finalize.tsx
new file mode 100644
index 000000000..8d2a2103a
--- /dev/null
+++ b/packages/create-astro/src/components/Finalize.tsx
@@ -0,0 +1,27 @@
+import React, { FC, useEffect } from 'react';
+import { Box, Text } from 'ink';
+import { cancelProcessListeners } from '../utils';
+
+const Finalize: FC<{ context: any }> = ({ context: { use, projectName } }) => {
+ useEffect(() => {
+ cancelProcessListeners();
+ process.exit(0);
+ }, []);
+
+ return <>
+ <Box display="flex">
+ <Text color="#17C083">{'[ yes ]'}</Text>
+ <Text> Project initialized at <Text color="#3894FF">./{projectName}</Text></Text>
+ </Box>
+ <Box display="flex" marginY={1}>
+ <Text dimColor>{'[ tip ]'}</Text>
+ <Box display="flex" marginLeft={1} flexDirection="column">
+ <Text>Get started by running</Text>
+ <Text color="#3894FF">cd ./{projectName}</Text>
+ <Text color="#3894FF">{use} start</Text>
+ </Box>
+ </Box>
+ </>;
+};
+
+export default Finalize;
diff --git a/packages/create-astro/src/components/Header.tsx b/packages/create-astro/src/components/Header.tsx
new file mode 100644
index 000000000..1d894a60e
--- /dev/null
+++ b/packages/create-astro/src/components/Header.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Box, Text } from 'ink';
+
+const getMessage = ({ projectName, template }) => {
+ switch (true) {
+ case !projectName: return <Text dimColor>Gathering mission details</Text>;
+ case !template: return <Text dimColor>Optimizing navigational system</Text>;
+ default: return <Text color="black" backgroundColor="white"> {projectName} </Text>
+ }
+}
+
+const Header: React.FC<{ context: any }> = ({ context }) => (
+ <Box width={48} display="flex" marginY={1}>
+ <Text backgroundColor="#882DE7" color="white">{' astro '}</Text>
+ <Box marginLeft={1}>
+ {getMessage(context)}
+ </Box>
+ </Box>
+)
+export default Header;
diff --git a/packages/create-astro/src/components/Help.tsx b/packages/create-astro/src/components/Help.tsx
new file mode 100644
index 000000000..88fcf5633
--- /dev/null
+++ b/packages/create-astro/src/components/Help.tsx
@@ -0,0 +1,62 @@
+import React, { FC } from 'react';
+import { Box, Text } from 'ink';
+import { ARGS, ARG } from '../config';
+
+const Type: FC<{ type: any, enum?: string[] }> = ({ type, enum: e }) => {
+ if (type === Boolean) {
+ return <>
+ <Text color="#3894FF">true</Text>
+ <Text dimColor>|</Text>
+ <Text color="#3894FF">false</Text>
+ </>
+ }
+ if (e?.length > 0) {
+ return <>
+ {e.map((item, i, { length: len}) => {
+ if (i !== len - 1) {
+ return <Box key={item}>
+ <Text color="#17C083">{item}</Text>
+ <Text dimColor>|</Text>
+ </Box>
+ }
+
+ return <Text color="#17C083" key={item}>{item}</Text>
+ })}
+ </>
+ }
+
+ return <Text color="#3894FF">string</Text>;
+}
+
+const Command: FC<{ name: string, info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => {
+ return (
+ <Box display="flex" alignItems="flex-start">
+ <Box width={24} display="flex" flexGrow={0}>
+ <Text color="whiteBright">--{name}</Text>{alias && <Text dimColor> -{alias}</Text>}
+ </Box>
+ <Box width={24}>
+ <Type type={type} enum={e} />
+ </Box>
+ <Box>
+ <Text>{description}</Text>
+ </Box>
+ </Box>
+ );
+}
+
+const Help: FC<{ context: any }> = ({ context: { templates }}) => {
+ return (
+ <>
+ <Box width={48} display="flex" marginY={1}>
+ <Text backgroundColor="#882DE7" color="white">{' astro '}</Text>
+ <Box marginLeft={1}>
+ <Text color="black" backgroundColor="white"> help </Text>
+ </Box>
+ </Box>
+ <Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column">
+ {Object.entries(ARGS).map(([name, info]) => <Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} /> )}
+ </Box>
+ </>
+ )
+};
+export default Help;
diff --git a/packages/create-astro/src/components/Install.tsx b/packages/create-astro/src/components/Install.tsx
new file mode 100644
index 000000000..d9d2bc9b6
--- /dev/null
+++ b/packages/create-astro/src/components/Install.tsx
@@ -0,0 +1,19 @@
+import React, { FC } from 'react';
+import { Box, Text } from 'ink';
+import Spacer from './Spacer';
+import Spinner from './Spinner';
+
+const Install: FC<{ context: any }> = ({ context: { use } }) => {
+ return <>
+ <Box display="flex">
+ <Spinner/>
+ <Text> Initiating launch sequence...</Text>
+ </Box>
+ <Box>
+ <Spacer />
+ <Text color="white" dimColor>(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)</Text>
+ </Box>
+ </>;
+};
+
+export default Install;
diff --git a/packages/create-astro/src/components/ProjectName.tsx b/packages/create-astro/src/components/ProjectName.tsx
new file mode 100644
index 000000000..87b976494
--- /dev/null
+++ b/packages/create-astro/src/components/ProjectName.tsx
@@ -0,0 +1,24 @@
+import React, { FC } from 'react';
+import { Box, Text } from 'ink';
+import Spacer from './Spacer';
+import TextInput from 'ink-text-input';
+// @ts-expect-error
+const { default: Input } = TextInput;
+
+const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) => {
+ const [value, setValue] = React.useState('');
+ const handleSubmit = (v: string) => onSubmit(v);
+
+ return <>
+ <Box display="flex">
+ <Text color="#17C083">{'[query]'}</Text>
+ <Text> What is your project name?</Text>
+ </Box>
+ <Box display="flex">
+ <Spacer />
+ <Input value={value} onChange={setValue} onSubmit={handleSubmit} placeholder="my-project" />
+ </Box>
+ </>;
+};
+
+export default ProjectName;
diff --git a/packages/create-astro/src/components/Select.tsx b/packages/create-astro/src/components/Select.tsx
new file mode 100644
index 000000000..acf8eb29f
--- /dev/null
+++ b/packages/create-astro/src/components/Select.tsx
@@ -0,0 +1,32 @@
+import SelectInput from 'ink-select-input';
+import React, { FC } from 'react';
+import { Text, Box } from 'ink';
+// @ts-expect-error
+const { default: Select } = SelectInput;
+
+interface Props {
+ isSelected?: boolean;
+ label: string;
+ description?: string;
+}
+const Indicator: FC<Props> = ({ isSelected }) => isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text>
+const Item: FC<Props> = ({isSelected = false, label, description }) => (
+ <Box display="flex">
+ <Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>{label}</Text>
+ {isSelected && description && typeof description === 'string' && <Text> {description}</Text>}
+ {isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>}
+ </Box>
+);
+
+interface SelectProps {
+ items: { value: string|number|boolean, label: string, description?: any }[]
+ onSelect(value: string|number|boolean): void;
+}
+const CustomSelect: FC<SelectProps> = ({ items, onSelect }) => {
+ const handleSelect = ({ value }) => onSelect(value);
+ return (
+ <Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} />
+ )
+}
+
+export default CustomSelect;
diff --git a/packages/create-astro/src/components/Spacer.tsx b/packages/create-astro/src/components/Spacer.tsx
new file mode 100644
index 000000000..1e4e14561
--- /dev/null
+++ b/packages/create-astro/src/components/Spacer.tsx
@@ -0,0 +1,5 @@
+import React, { FC } from 'react';
+import { Box } from 'ink';
+
+const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} />
+export default Spacer;
diff --git a/packages/create-astro/src/components/Spinner.tsx b/packages/create-astro/src/components/Spinner.tsx
new file mode 100644
index 000000000..2d3335e1c
--- /dev/null
+++ b/packages/create-astro/src/components/Spinner.tsx
@@ -0,0 +1,200 @@
+import React, { FC, useEffect, useState } from 'react';
+import { Box, Text } from 'ink';
+
+const Spinner: FC<{ type?: keyof typeof spinners }> = ({ type = 'countdown' }) => {
+ const { interval, frames } = spinners[type];
+ const [i, setI] = useState(0);
+ useEffect(() => {
+ const _ = setInterval(() => {
+ setI(v => (v < frames.length - 1) ? v + 1 : 0)
+ }, interval)
+
+ return () => clearInterval(_);
+ }, [])
+
+ return frames[i]
+}
+
+const spinners = {
+ countdown: {
+ interval: 80,
+ frames: [
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ <Text backgroundColor="#17C083">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ <Text backgroundColor="#23B1AF">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ <Text backgroundColor="#2CA5D2">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ <Text backgroundColor="#3894FF">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ <Text backgroundColor="#5076F9">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ <Text backgroundColor="#6858F1">{' '}</Text>
+ </Box>,
+ <Box display="flex">
+ <Text backgroundColor="#882DE7">{' '}</Text>
+ </Box>,
+ ]
+ }
+}
+
+export default Spinner;
diff --git a/packages/create-astro/src/components/Template.tsx b/packages/create-astro/src/components/Template.tsx
new file mode 100644
index 000000000..7fbab035d
--- /dev/null
+++ b/packages/create-astro/src/components/Template.tsx
@@ -0,0 +1,23 @@
+import React, { FC } from 'react';
+import { Box, Text } from 'ink';
+import Spacer from './Spacer';
+import Select from './Select';
+
+const Template: FC<{ context: any, onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => {
+ const items = templates.map(({ title: label, ...rest }) => ({ ...rest, label }));
+
+ return (
+ <>
+ <Box display="flex">
+ <Text color="#17C083">{'[query]'}</Text>
+ <Text> Which template should be used?</Text>
+ </Box>
+ <Box display="flex">
+ <Spacer width={6} />
+ <Select items={items} onSelect={onSubmit} />
+ </Box>
+ </>
+ );
+};
+
+export default Template;
diff --git a/packages/create-astro/src/components/Version.tsx b/packages/create-astro/src/components/Version.tsx
new file mode 100644
index 000000000..340952dc0
--- /dev/null
+++ b/packages/create-astro/src/components/Version.tsx
@@ -0,0 +1,6 @@
+import React, { FC } from 'react';
+import { Text } from 'ink';
+import pkg from '../../package.json';
+
+const Version: FC = () => <Text color="#17C083">v{pkg.version}</Text>;
+export default Version;
diff --git a/packages/create-astro/src/config.ts b/packages/create-astro/src/config.ts
new file mode 100644
index 000000000..3d3c07912
--- /dev/null
+++ b/packages/create-astro/src/config.ts
@@ -0,0 +1,49 @@
+import type * as arg from 'arg';
+
+export interface ARG {
+ type: any;
+ description: string;
+ enum?: string[];
+ alias?: string;
+}
+
+export const ARGS: Record<string, ARG> = {
+ 'template': {
+ type: String,
+ description: 'specifies template to use'
+ },
+ 'use': {
+ type: String,
+ enum: ['npm', 'yarn'],
+ description: 'specifies package manager to use'
+ },
+ 'run': {
+ type: Boolean,
+ description: 'should dependencies be installed automatically?'
+ },
+ 'force': {
+ type: Boolean,
+ alias: 'f',
+ description: 'should existing files be overwritten?'
+ },
+ 'version': {
+ type: Boolean,
+ alias: 'v',
+ description: 'prints current version'
+ },
+ 'help': {
+ type: Boolean,
+ alias: 'h',
+ description: 'prints this message'
+ }
+}
+
+export const args = Object.entries(ARGS).reduce((acc, [name, info]) => {
+ const key = `--${name}`;
+ const spec = { ...acc, [key]: info.type };
+
+ if (info.alias) {
+ spec[`-${info.alias}`] = key;
+ }
+ return spec
+}, {} as arg.Spec);
diff --git a/packages/create-astro/src/index.tsx b/packages/create-astro/src/index.tsx
new file mode 100644
index 000000000..0927eaae9
--- /dev/null
+++ b/packages/create-astro/src/index.tsx
@@ -0,0 +1,46 @@
+import 'source-map-support/register.js';
+import React from 'react';
+import App from './components/App';
+import Version from './components/Version';
+import Exit from './components/Exit';
+import {render} from 'ink';
+import { getTemplates, addProcessListeners } from './utils';
+import { args as argsConfig } from './config';
+import arg from 'arg';
+import Help from './components/Help';
+
+/** main `create-astro` CLI */
+export default async function createAstro() {
+ const args = arg(argsConfig);
+ const projectName = args._[0];
+ if (args['--version']) {
+ return render(<Version />);
+ }
+ const templates = await getTemplates();
+ if (args['--help']) {
+ return render(<Help context={{ templates }} />)
+ }
+
+ const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm';
+ const use = (args['--use'] ?? pkgManager) as 'npm'|'yarn';
+ const template = args['--template'];
+ const force = args['--force'];
+ const run = args['--run'] ?? true;
+
+ const app = render(<App context={{ projectName, template, templates, force, run, use }} />);
+
+ const onError = () => {
+ if (app) app.clear();
+ render(<Exit didError />);
+ }
+ const onExit = () => {
+ if (app) app.clear();
+ render(<Exit />);
+ }
+ addProcessListeners([
+ ['uncaughtException', onError],
+ ['exit', onExit],
+ ['SIGINT', onExit],
+ ['SIGTERM', onExit],
+ ])
+}
diff --git a/packages/create-astro/src/templates/blank/README.md b/packages/create-astro/src/templates/blank/README.md
new file mode 100644
index 000000000..40c5841c9
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/README.md
@@ -0,0 +1,24 @@
+# Welcome to [Astro](https://astro.build)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```
+/
+├── public/
+├── src/
+│ └── pages/
+│ └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://discord.gg/EsGdSGen).
diff --git a/packages/create-astro/src/templates/blank/_gitignore b/packages/create-astro/src/templates/blank/_gitignore
new file mode 100644
index 000000000..d436c6dad
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/_gitignore
@@ -0,0 +1,18 @@
+# build output
+dist
+
+# dependencies
+node_modules/
+.snowpack/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/packages/create-astro/src/templates/blank/meta.json b/packages/create-astro/src/templates/blank/meta.json
new file mode 100644
index 000000000..ca942e99f
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Blank",
+ "description": "a bare-bones, ultra-minimal template"
+}
diff --git a/packages/create-astro/src/templates/blank/package.json b/packages/create-astro/src/templates/blank/package.json
new file mode 100644
index 000000000..e04205726
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "TODO",
+ "version": "0.0.1",
+ "scripts": {
+ "start": "astro dev",
+ "build": "astro build"
+ },
+ "devDependencies": {
+ "astro": "0.0.9"
+ }
+}
diff --git a/packages/create-astro/src/templates/blank/public/favicon.svg b/packages/create-astro/src/templates/blank/public/favicon.svg
new file mode 100644
index 000000000..542f90aec
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/public/favicon.svg
@@ -0,0 +1,11 @@
+<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <style>
+ #flame { fill: #FF5D01; }
+ #a { fill: #000014; }
+ @media (prefers-color-scheme: dark) {
+ #a { fill: #fff; }
+ }
+ </style>
+ <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" />
+ <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" />
+</svg>
diff --git a/packages/create-astro/src/templates/blank/src/pages/index.astro b/packages/create-astro/src/templates/blank/src/pages/index.astro
new file mode 100644
index 000000000..0ad00d28f
--- /dev/null
+++ b/packages/create-astro/src/templates/blank/src/pages/index.astro
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Astro</title>
+
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
+</head>
+<body>
+ <main>
+ <h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
+ </main>
+</body>
+</html>
diff --git a/packages/create-astro/src/templates/starter/README.md b/packages/create-astro/src/templates/starter/README.md
new file mode 100644
index 000000000..59940918f
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/README.md
@@ -0,0 +1,32 @@
+# Welcome to [Astro](https://astro.build)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```
+/
+├── public/
+│ ├── robots.txt
+│ └── favicon.ico
+├── src/
+│ ├── components/
+│ │ └── Tour.astro
+│ └── pages/
+│ └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://discord.gg/EsGdSGen).
diff --git a/packages/create-astro/src/templates/starter/_gitignore b/packages/create-astro/src/templates/starter/_gitignore
new file mode 100644
index 000000000..d436c6dad
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/_gitignore
@@ -0,0 +1,18 @@
+# build output
+dist
+
+# dependencies
+node_modules/
+.snowpack/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/packages/create-astro/src/templates/starter/astro.config.mjs b/packages/create-astro/src/templates/starter/astro.config.mjs
new file mode 100644
index 000000000..de58ba1c5
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/astro.config.mjs
@@ -0,0 +1,16 @@
+export default {
+ projectRoot: '.',
+ astroRoot: './src',
+ dist: './dist',
+ public: './public',
+ extensions: {
+ '.jsx': 'react',
+ },
+ snowpack: {
+ optimize: {
+ bundle: false,
+ minify: true,
+ target: 'es2018',
+ },
+ },
+};
diff --git a/packages/create-astro/src/templates/starter/meta.json b/packages/create-astro/src/templates/starter/meta.json
new file mode 100644
index 000000000..ba775af32
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "Getting Started",
+ "description": "a friendly starting point for new astronauts",
+ "rank": 999
+}
diff --git a/packages/create-astro/src/templates/starter/package.json b/packages/create-astro/src/templates/starter/package.json
new file mode 100644
index 000000000..e04205726
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "TODO",
+ "version": "0.0.1",
+ "scripts": {
+ "start": "astro dev",
+ "build": "astro build"
+ },
+ "devDependencies": {
+ "astro": "0.0.9"
+ }
+}
diff --git a/packages/create-astro/src/templates/starter/public/assets/logo.svg b/packages/create-astro/src/templates/starter/public/assets/logo.svg
new file mode 100644
index 000000000..d751556b2
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/public/assets/logo.svg
@@ -0,0 +1,12 @@
+<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <style>
+ #flame { fill: #FF5D01; }
+ #a { fill: #000014; }
+ @media (prefers-color-scheme: dark) {
+ #a { fill: #fff; }
+ }
+ </style>
+
+ <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/>
+ <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" />
+</svg>
diff --git a/packages/create-astro/src/templates/starter/public/favicon.svg b/packages/create-astro/src/templates/starter/public/favicon.svg
new file mode 100644
index 000000000..542f90aec
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/public/favicon.svg
@@ -0,0 +1,11 @@
+<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <style>
+ #flame { fill: #FF5D01; }
+ #a { fill: #000014; }
+ @media (prefers-color-scheme: dark) {
+ #a { fill: #fff; }
+ }
+ </style>
+ <path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" />
+ <path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" />
+</svg>
diff --git a/packages/create-astro/src/templates/starter/public/robots.txt b/packages/create-astro/src/templates/starter/public/robots.txt
new file mode 100644
index 000000000..1f53798bb
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/packages/create-astro/src/templates/starter/public/style/global.css b/packages/create-astro/src/templates/starter/public/style/global.css
new file mode 100644
index 000000000..b54158f92
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/public/style/global.css
@@ -0,0 +1,28 @@
+* {
+ box-sizing: border-box;
+ margin: 0;
+}
+
+:root {
+ font-family: system-ui;
+ font-size: 1rem;
+ --user-font-scale: 1rem - 16px;
+ font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
+}
+
+body {
+ padding: 4rem 2rem;
+ width: 100vw;
+ min-height: 100vh;
+ display: grid;
+ justify-content: center;
+ background: #F9FAFB;
+ color: #111827;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #111827;
+ color: #fff;
+ }
+}
diff --git a/packages/create-astro/src/templates/starter/public/style/home.css b/packages/create-astro/src/templates/starter/public/style/home.css
new file mode 100644
index 000000000..c4271a845
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/public/style/home.css
@@ -0,0 +1,38 @@
+:root {
+ --font-mono: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
+ --color-light: #F3F4F6;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color-light: #1F2937;
+ }
+}
+
+a {
+ color: inherit;
+}
+
+header > div {
+ font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem);
+}
+
+header > div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+header h1 {
+ font-size: 1em;
+ font-weight: 500;
+}
+header img {
+ width: 2em;
+ height: 2.667em;
+}
+
+h2 {
+ font-weight: 500;
+ font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem);
+}
diff --git a/packages/create-astro/src/templates/starter/src/components/Tour.astro b/packages/create-astro/src/templates/starter/src/components/Tour.astro
new file mode 100644
index 000000000..ca0cfafbf
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/src/components/Tour.astro
@@ -0,0 +1,82 @@
+<article>
+ <div class="banner">
+ <p><strong>🧑‍🚀 Seasoned astronaut?</strong> Delete this file. Have fun!</p>
+ </div>
+
+ <section>
+ <h2>🚀 Project Structure</h2>
+ <p>Inside of your Astro project, you'll see the following folders and files:</p>
+
+ <pre><code class="tree">/
+├── public/
+│ ├── robots.txt
+│ └── favicon.ico
+├── src/
+│ ├── components/
+│ │ └── Tour.astro
+│ └── pages/
+│ └── index.astro
+└── package.json</code>
+ </pre>
+
+ <p>
+ Astro looks for <code>.astro</code> or <code>.md.astro</code> files in the <code>src/pages/</code> directory.
+ Each page is exposed as a route based on its file name.
+ </p>
+
+ <p>
+ There's nothing special about <code>src/components/</code>, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+ </p>
+
+ <p>Any static assets, like images, can be placed in the <code>public/</code> directory.</p>
+ </section>
+
+ <section>
+ <h2>👀 Want to learn more?</h2>
+ <p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://discord.gg/EsGdSGen">Discord server</a>.</p>
+ </section>
+
+</article>
+
+<style>
+ article {
+ padding-top: 2em;
+ line-height: 1.5;
+ }
+ section {
+ margin-top: 2em;
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ max-width: 70ch;
+ }
+
+ .banner {
+ text-align: center;
+ font-size: 1.2rem;
+ background: var(--color-light);
+ padding: 1em 1.5em;
+ padding-left: 0.75em;
+ border-radius: 4px;
+ }
+
+ pre,
+ code {
+ font-family: var(--font-mono);
+ background: var(--color-light);
+ border-radius: 4px;
+ }
+
+ pre {
+ padding: 1em 1.5em;
+ }
+
+ .tree {
+ line-height: 1.2;
+ }
+
+ code:not(.tree) {
+ padding: 0.125em;
+ margin: 0 -0.125em;
+ }
+</style>
diff --git a/packages/create-astro/src/templates/starter/src/pages/index.astro b/packages/create-astro/src/templates/starter/src/pages/index.astro
new file mode 100644
index 000000000..de052e9c4
--- /dev/null
+++ b/packages/create-astro/src/templates/starter/src/pages/index.astro
@@ -0,0 +1,38 @@
+---
+import Tour from '../components/Tour.astro';
+---
+
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Astro</title>
+
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
+
+ <link rel="stylesheet" href="/style/global.css">
+ <link rel="stylesheet" href="/style/home.css">
+
+ <style>
+ header {
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ max-width: min(100%, 68ch);
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <header>
+ <div>
+ <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
+ <h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
+ </div>
+ </header>
+
+ <Tour />
+ </main>
+</body>
+</html>
diff --git a/packages/create-astro/src/utils.ts b/packages/create-astro/src/utils.ts
new file mode 100644
index 000000000..790a64cf9
--- /dev/null
+++ b/packages/create-astro/src/utils.ts
@@ -0,0 +1,147 @@
+import { ChildProcess, spawn } from 'child_process';
+import { promises as fs, readdirSync, existsSync, lstatSync, rmdirSync, unlinkSync } from 'fs';
+import { basename, resolve } from 'path';
+import { fileURLToPath, URL } from 'url';
+import decompress from 'decompress';
+
+const listeners = new Map();
+
+export async function addProcessListeners(handlers: [NodeJS.Signals|string, NodeJS.SignalsListener][]) {
+ for (const [event,handler] of handlers) {
+ listeners.set(event, handler);
+ process.once(event as NodeJS.Signals, handler);
+ }
+}
+
+export async function cancelProcessListeners() {
+ for (const [event, handler] of listeners.entries()) {
+ process.off(event, handler);
+ listeners.delete(event);
+ }
+}
+
+export async function getTemplates() {
+ const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url));
+ const templateFiles = await fs.readdir(templatesRoot, 'utf8');
+ const templates = templateFiles.filter(t => t.endsWith('.tgz'));
+ const metafile = templateFiles.find(t => t.endsWith('meta.json'));
+
+ const meta = await fs.readFile(resolve(templatesRoot, metafile)).then(r => JSON.parse(r.toString()));
+
+ return templates.map(template => {
+ const value = basename(template, '.tgz');
+ if (meta[value]) return { ...meta[value], value };
+ return { value };
+ }).sort((a, b) => {
+ const aRank = a.rank ?? 0;
+ const bRank = b.rank ?? 0;
+ if (aRank > bRank) return -1;
+ if (bRank > aRank) return 1;
+ return 0;
+ });
+}
+
+const childrenProcesses: ChildProcess[] = [];
+export let isDone = false;
+
+export async function rewriteFiles(projectName: string) {
+ const dest = resolve(projectName);
+ const tasks = [];
+ tasks.push(fs.rename(resolve(dest, '_gitignore'), resolve(dest, '.gitignore')));
+ tasks.push(
+ fs.readFile(resolve(dest, 'package.json'))
+ .then(res => JSON.parse(res.toString()))
+ .then(json => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2))
+ .then(res => fs.writeFile(resolve(dest, 'package.json'), res))
+ );
+
+ return Promise.all(tasks);
+}
+
+export async function prepareTemplate(use: 'npm'|'yarn', name: string, dest: string) {
+ const projectName = dest;
+ dest = resolve(dest);
+ const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url));
+ await decompress(template, dest);
+ await rewriteFiles(projectName);
+ try {
+ await run(use, use === 'npm' ? 'i' : null, dest);
+ } catch (e) {
+ cleanup(true);
+ }
+ isDone = true;
+ return;
+}
+
+export function cleanup(didError = false) {
+ killChildren();
+ setTimeout(() => {
+ process.exit(didError ? 1 : 0);
+ }, 200);
+}
+
+export function killChildren() {
+ childrenProcesses.forEach(p => p.kill('SIGINT'));
+}
+
+export function run(pkgManager: 'npm'|'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const p = spawn(pkgManager, command ? [command] : [], {
+ shell: true,
+ stdio,
+ cwd: projectPath,
+ });
+ p.once('exit', () => resolve());
+ p.once('error', reject);
+ childrenProcesses.push(p);
+ });
+}
+
+export function isWin() {
+ return process.platform === 'win32';
+}
+
+export function isEmpty(path) {
+ try {
+ const files = readdirSync(resolve(path));
+ if (files.length > 0) {
+ return false;
+ } else {
+ return true;
+ }
+ } catch (err) {
+ if (err.code !== 'ENOENT') throw err;
+ }
+ return true;
+}
+
+export function emptyDir(dir) {
+ dir = resolve(dir);
+ if (!existsSync(dir)) {
+ return
+ }
+ for (const file of readdirSync(dir)) {
+ const abs = resolve(dir, file)
+ if (lstatSync(abs).isDirectory()) {
+ emptyDir(abs)
+ rmdirSync(abs)
+ } else {
+ unlinkSync(abs)
+ }
+ }
+}
+
+export function getValidPackageName(projectName: string) {
+ const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
+
+ if (packageNameRegExp.test(projectName)) {
+ return projectName
+ }
+
+ return projectName
+ .trim()
+ .toLowerCase()
+ .replace(/\s+/g, '-')
+ .replace(/^[._]/, '')
+ .replace(/[^a-z0-9-~]+/g, '-');
+}