@aex.is/zero 0.1.3 → 0.1.4

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.
@@ -0,0 +1,163 @@
1
+ import * as p from '@clack/prompts';
2
+ import { frameworks } from '../config/frameworks.js';
3
+ import { modules } from '../config/modules.js';
4
+ const introArt = [
5
+ ' _____',
6
+ ' / ___ \\\\',
7
+ ' / / _ \\\\ \\\\',
8
+ ' | |/ /| |',
9
+ ' \\\\ \\\\_/ / /',
10
+ ' \\\\___/_/'
11
+ ].join('\n');
12
+ export async function runWizard() {
13
+ p.intro(`${introArt}\nAexis Zero`);
14
+ let directory = '.';
15
+ let appName = '';
16
+ let domain = '';
17
+ let framework = 'nextjs';
18
+ let selectedModules = [];
19
+ let step = 'directory';
20
+ while (true) {
21
+ if (step === 'directory') {
22
+ const value = await p.text({
23
+ message: 'Project directory',
24
+ placeholder: '.',
25
+ validate: (input) => {
26
+ if (typeof input !== 'string') {
27
+ return 'Enter a directory.';
28
+ }
29
+ return undefined;
30
+ }
31
+ });
32
+ if (isCancelled(value))
33
+ return null;
34
+ const normalized = String(value).trim();
35
+ directory = normalized.length === 0 ? '.' : normalized;
36
+ step = 'name';
37
+ continue;
38
+ }
39
+ if (step === 'name') {
40
+ const value = await p.text({
41
+ message: 'App name',
42
+ placeholder: 'my-app',
43
+ validate: (input) => {
44
+ if (typeof input !== 'string' || input.trim().length === 0) {
45
+ return 'App name is required.';
46
+ }
47
+ return undefined;
48
+ }
49
+ });
50
+ if (isCancelled(value))
51
+ return null;
52
+ appName = String(value).trim();
53
+ step = 'domain';
54
+ continue;
55
+ }
56
+ if (step === 'domain') {
57
+ const value = await p.text({
58
+ message: 'Domain (optional)',
59
+ placeholder: 'example.com'
60
+ });
61
+ if (isCancelled(value))
62
+ return null;
63
+ domain = String(value).trim();
64
+ step = 'framework';
65
+ continue;
66
+ }
67
+ if (step === 'framework') {
68
+ const value = await p.select({
69
+ message: 'Framework',
70
+ options: frameworks.map((item) => ({
71
+ value: item.id,
72
+ label: item.label,
73
+ hint: item.description
74
+ }))
75
+ });
76
+ if (isCancelled(value))
77
+ return null;
78
+ framework = value;
79
+ step = 'modules';
80
+ continue;
81
+ }
82
+ if (step === 'modules') {
83
+ const value = await p.multiselect({
84
+ message: 'Modules',
85
+ options: modules.map((item) => ({
86
+ value: item.id,
87
+ label: item.label,
88
+ hint: item.description
89
+ })),
90
+ required: false
91
+ });
92
+ if (isCancelled(value))
93
+ return null;
94
+ selectedModules = value;
95
+ step = 'confirm';
96
+ continue;
97
+ }
98
+ if (step === 'confirm') {
99
+ const frameworkLabel = frameworks.find((item) => item.id === framework)?.label ?? framework;
100
+ const moduleLabels = selectedModules
101
+ .map((id) => modules.find((item) => item.id === id)?.label ?? id)
102
+ .join(', ');
103
+ p.note([
104
+ `Directory: ${directory}`,
105
+ `App name: ${appName}`,
106
+ `Domain: ${domain || 'None'}`,
107
+ `Framework: ${frameworkLabel}`,
108
+ `Modules: ${moduleLabels || 'None'}`
109
+ ].join('\n'), 'Review');
110
+ const action = await p.select({
111
+ message: 'Next step',
112
+ options: [
113
+ { value: 'continue', label: 'Continue' },
114
+ { value: 'edit-directory', label: 'Edit directory' },
115
+ { value: 'edit-name', label: 'Edit name' },
116
+ { value: 'edit-domain', label: 'Edit domain' },
117
+ { value: 'edit-framework', label: 'Edit framework' },
118
+ { value: 'edit-modules', label: 'Edit modules' },
119
+ { value: 'cancel', label: 'Cancel' }
120
+ ]
121
+ });
122
+ if (isCancelled(action))
123
+ return null;
124
+ switch (action) {
125
+ case 'continue':
126
+ return {
127
+ directory,
128
+ appName,
129
+ domain,
130
+ framework,
131
+ modules: selectedModules
132
+ };
133
+ case 'edit-directory':
134
+ step = 'directory';
135
+ continue;
136
+ case 'edit-name':
137
+ step = 'name';
138
+ continue;
139
+ case 'edit-domain':
140
+ step = 'domain';
141
+ continue;
142
+ case 'edit-framework':
143
+ step = 'framework';
144
+ continue;
145
+ case 'edit-modules':
146
+ step = 'modules';
147
+ continue;
148
+ case 'cancel':
149
+ p.cancel('Cancelled.');
150
+ return null;
151
+ default:
152
+ return null;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ function isCancelled(value) {
158
+ if (p.isCancel(value)) {
159
+ p.cancel('Cancelled.');
160
+ return true;
161
+ }
162
+ return false;
163
+ }
@@ -66,6 +66,8 @@ export const frameworks = [
66
66
  description: 'Expo app with Router and EAS configuration.',
67
67
  packages: [
68
68
  'expo-router',
69
+ 'expo-font',
70
+ '@expo-google-fonts/geist-mono',
69
71
  'tamagui',
70
72
  '@tamagui/config',
71
73
  '@tamagui/animations-react-native',
@@ -3,7 +3,13 @@ export const modules = [
3
3
  id: 'neon',
4
4
  label: 'Database (Neon)',
5
5
  description: 'Serverless Postgres with Neon.',
6
- envVars: ['DATABASE_URL'],
6
+ envVars: [
7
+ {
8
+ key: 'DATABASE_URL',
9
+ description: 'Neon connection string',
10
+ url: 'https://neon.com/docs/get-started/connect-neon'
11
+ }
12
+ ],
7
13
  packages: {
8
14
  nextjs: ['@neondatabase/serverless'],
9
15
  expo: ['@neondatabase/serverless']
@@ -13,7 +19,18 @@ export const modules = [
13
19
  id: 'clerk',
14
20
  label: 'Auth (Clerk)',
15
21
  description: 'Authentication with Clerk.',
16
- envVars: ['CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY'],
22
+ envVars: [
23
+ {
24
+ key: 'CLERK_PUBLISHABLE_KEY',
25
+ description: 'Clerk publishable key',
26
+ url: 'https://dashboard.clerk.com'
27
+ },
28
+ {
29
+ key: 'CLERK_SECRET_KEY',
30
+ description: 'Clerk secret key',
31
+ url: 'https://dashboard.clerk.com'
32
+ }
33
+ ],
17
34
  packages: {
18
35
  nextjs: ['@clerk/nextjs'],
19
36
  expo: ['@clerk/clerk-expo']
@@ -23,7 +40,18 @@ export const modules = [
23
40
  id: 'payload',
24
41
  label: 'CMS (Payload)',
25
42
  description: 'Headless CMS using Payload.',
26
- envVars: ['PAYLOAD_SECRET', 'DATABASE_URL'],
43
+ envVars: [
44
+ {
45
+ key: 'PAYLOAD_SECRET',
46
+ description: 'Payload secret (generate a long random string)',
47
+ url: 'https://payloadcms.com/docs'
48
+ },
49
+ {
50
+ key: 'DATABASE_URL',
51
+ description: 'Database connection string',
52
+ url: 'https://payloadcms.com/docs'
53
+ }
54
+ ],
27
55
  packages: {
28
56
  nextjs: ['payload'],
29
57
  expo: ['payload']
@@ -33,7 +61,18 @@ export const modules = [
33
61
  id: 'stripe',
34
62
  label: 'Payments (Stripe)',
35
63
  description: 'Payments via Stripe SDK.',
36
- envVars: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'],
64
+ envVars: [
65
+ {
66
+ key: 'STRIPE_SECRET_KEY',
67
+ description: 'Stripe secret key',
68
+ url: 'https://dashboard.stripe.com/apikeys'
69
+ },
70
+ {
71
+ key: 'STRIPE_WEBHOOK_SECRET',
72
+ description: 'Stripe webhook signing secret',
73
+ url: 'https://dashboard.stripe.com/webhooks'
74
+ }
75
+ ],
37
76
  packages: {
38
77
  nextjs: ['stripe'],
39
78
  expo: ['stripe']
@@ -62,8 +101,20 @@ export function getModuleEnvVars(moduleIds) {
62
101
  for (const id of moduleIds) {
63
102
  const module = getModuleDefinition(id);
64
103
  for (const envVar of module.envVars) {
65
- envVars.add(envVar);
104
+ envVars.add(envVar.key);
66
105
  }
67
106
  }
68
107
  return Array.from(envVars).sort();
69
108
  }
109
+ export function getModuleEnvHelp(moduleIds) {
110
+ const map = new Map();
111
+ for (const id of moduleIds) {
112
+ const module = getModuleDefinition(id);
113
+ for (const envVar of module.envVars) {
114
+ if (!map.has(envVar.key)) {
115
+ map.set(envVar.key, envVar);
116
+ }
117
+ }
118
+ }
119
+ return Array.from(map.values());
120
+ }
@@ -2,25 +2,27 @@ import path from 'path';
2
2
  import { promises as fs } from 'fs';
3
3
  import { execa } from 'execa';
4
4
  import { getFrameworkDefinition } from '../config/frameworks.js';
5
+ import { getModuleEnvHelp } from '../config/modules.js';
5
6
  import { installBaseDependencies, installModulePackages } from './installers.js';
6
7
  import { writeEnvExample } from './env.js';
7
- import { nextLayoutTemplate, nextPageTemplate, shadcnUtilsTemplate, componentsJsonTemplate, tamaguiConfigTemplate, metroConfigTemplate, expoLayoutTemplate, expoIndexTemplate } from './templates.js';
8
+ import { buildNextTemplateFiles, buildExpoTemplateFiles, componentsJsonTemplate } from './templates.js';
8
9
  const baseEnv = {
9
10
  ...process.env,
10
11
  CI: '1'
11
12
  };
12
13
  export async function scaffoldProject(config) {
13
- const targetDir = path.resolve(process.cwd(), config.appName);
14
+ const directoryInput = config.directory.trim().length === 0 ? '.' : config.directory.trim();
15
+ const targetDir = path.resolve(process.cwd(), directoryInput);
14
16
  await ensureEmptyTargetDir(targetDir);
15
17
  const framework = getFrameworkDefinition(config.framework);
16
18
  console.log(`Scaffolding ${framework.label}...`);
17
- await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, config.appName, framework.scaffold.argSets);
19
+ await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, directoryInput, framework.scaffold.argSets);
18
20
  console.log('Applying framework templates...');
19
21
  if (config.framework === 'nextjs') {
20
- await applyNextTemplates(targetDir);
22
+ await applyNextTemplates(config, targetDir);
21
23
  }
22
24
  else {
23
- await applyExpoTemplates(targetDir);
25
+ await applyExpoTemplates(config, targetDir);
24
26
  }
25
27
  console.log('Installing base dependencies with Bun...');
26
28
  await installBaseDependencies(targetDir, framework.packages);
@@ -29,7 +31,7 @@ export async function scaffoldProject(config) {
29
31
  console.log('Generating .env.example...');
30
32
  await writeEnvExample(config.modules, targetDir);
31
33
  console.log('Scaffold complete.');
32
- const cdTarget = config.appName.includes(' ') ? `"${config.appName}"` : config.appName;
34
+ const cdTarget = directoryInput === '.' ? '.' : directoryInput.includes(' ') ? `"${directoryInput}"` : directoryInput;
33
35
  console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
34
36
  }
35
37
  async function ensureEmptyTargetDir(targetDir) {
@@ -50,11 +52,12 @@ async function ensureEmptyTargetDir(targetDir) {
50
52
  throw error;
51
53
  }
52
54
  }
53
- async function runScaffoldCommand(command, packageName, appName, argSets) {
55
+ async function runScaffoldCommand(command, packageName, directoryInput, argSets) {
54
56
  const errors = [];
57
+ const targetArg = directoryInput === '.' ? '.' : directoryInput;
55
58
  for (const args of argSets) {
56
59
  try {
57
- await execa(command, [packageName, appName, ...args], {
60
+ await execa(command, [packageName, targetArg, ...args], {
58
61
  stdio: 'inherit',
59
62
  env: baseEnv,
60
63
  shell: false
@@ -70,32 +73,45 @@ async function runScaffoldCommand(command, packageName, appName, argSets) {
70
73
  : 'Scaffold failed for unknown reasons.';
71
74
  throw new Error(message);
72
75
  }
73
- async function applyNextTemplates(targetDir) {
74
- const rootAppDir = path.join(targetDir, 'app');
76
+ async function applyNextTemplates(config, targetDir) {
75
77
  const srcAppDir = path.join(targetDir, 'src', 'app');
76
78
  const usesSrcDir = await pathExists(srcAppDir);
77
- const appDir = usesSrcDir ? srcAppDir : rootAppDir;
78
- const projectSrcBase = usesSrcDir ? path.join(targetDir, 'src') : targetDir;
79
- await fs.mkdir(appDir, { recursive: true });
80
- await fs.writeFile(path.join(appDir, 'layout.tsx'), nextLayoutTemplate, 'utf8');
81
- await fs.writeFile(path.join(appDir, 'page.tsx'), nextPageTemplate, 'utf8');
82
- await ensureShadcnSetup(targetDir, projectSrcBase, usesSrcDir);
79
+ const basePath = usesSrcDir ? 'src' : '';
80
+ const envHelp = getModuleEnvHelp(config.modules);
81
+ const files = buildNextTemplateFiles({
82
+ appName: config.appName,
83
+ domain: config.domain,
84
+ envVars: envHelp,
85
+ basePath
86
+ });
87
+ await writeTemplateFiles(targetDir, files);
88
+ const globalsPath = usesSrcDir ? 'src/app/globals.css' : 'app/globals.css';
89
+ const tailwindConfig = await detectTailwindConfig(targetDir);
90
+ const componentsJson = componentsJsonTemplate(globalsPath, tailwindConfig ?? 'tailwind.config.ts');
91
+ await fs.writeFile(path.join(targetDir, 'components.json'), componentsJson, 'utf8');
83
92
  await ensureNextTurbo(targetDir);
84
- }
85
- async function applyExpoTemplates(targetDir) {
86
- const appDir = path.join(targetDir, 'app');
87
- await fs.mkdir(appDir, { recursive: true });
88
- await fs.writeFile(path.join(appDir, '_layout.tsx'), expoLayoutTemplate, 'utf8');
89
- await fs.writeFile(path.join(appDir, 'index.tsx'), expoIndexTemplate, 'utf8');
90
- await ensureExpoConfig(targetDir);
91
- await ensureExpoTamagui(targetDir);
92
- }
93
- async function ensureExpoConfig(targetDir) {
93
+ await ensurePackageName(targetDir, config.appName);
94
+ }
95
+ async function applyExpoTemplates(config, targetDir) {
96
+ const envHelp = getModuleEnvHelp(config.modules);
97
+ const files = buildExpoTemplateFiles({
98
+ appName: config.appName,
99
+ domain: config.domain,
100
+ envVars: envHelp,
101
+ basePath: ''
102
+ });
103
+ await writeTemplateFiles(targetDir, files);
104
+ await ensureExpoConfig(targetDir, config.appName);
105
+ await ensurePackageName(targetDir, config.appName);
106
+ }
107
+ async function ensureExpoConfig(targetDir, appName) {
94
108
  const appJsonPath = path.join(targetDir, 'app.json');
95
109
  const appJson = await readJson(appJsonPath, { expo: {} });
96
110
  if (!appJson.expo || typeof appJson.expo !== 'object') {
97
111
  appJson.expo = {};
98
112
  }
113
+ appJson.expo.name = appName;
114
+ appJson.expo.slug = toSlug(appName);
99
115
  if (!Array.isArray(appJson.expo.platforms)) {
100
116
  appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
101
117
  }
@@ -134,6 +150,31 @@ async function ensureExpoConfig(targetDir) {
134
150
  };
135
151
  await writeJson(easPath, easConfig);
136
152
  }
153
+ async function ensureNextTurbo(targetDir) {
154
+ const packageJsonPath = path.join(targetDir, 'package.json');
155
+ const packageJson = await readJson(packageJsonPath, {});
156
+ if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
157
+ packageJson.scripts = {};
158
+ }
159
+ const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
160
+ if (!currentDev.includes('--turbo')) {
161
+ packageJson.scripts.dev = `${currentDev} --turbo`;
162
+ }
163
+ await writeJson(packageJsonPath, packageJson);
164
+ }
165
+ async function ensurePackageName(targetDir, appName) {
166
+ const packageJsonPath = path.join(targetDir, 'package.json');
167
+ const packageJson = await readJson(packageJsonPath, {});
168
+ packageJson.name = toPackageName(appName);
169
+ await writeJson(packageJsonPath, packageJson);
170
+ }
171
+ async function writeTemplateFiles(targetDir, files) {
172
+ for (const file of files) {
173
+ const fullPath = path.join(targetDir, ...file.path.split('/'));
174
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
175
+ await fs.writeFile(fullPath, file.content, 'utf8');
176
+ }
177
+ }
137
178
  function mergeUnique(values, additions) {
138
179
  const set = new Set(values);
139
180
  for (const value of additions) {
@@ -141,6 +182,21 @@ function mergeUnique(values, additions) {
141
182
  }
142
183
  return Array.from(set);
143
184
  }
185
+ async function detectTailwindConfig(targetDir) {
186
+ const candidates = [
187
+ 'tailwind.config.ts',
188
+ 'tailwind.config.js',
189
+ 'tailwind.config.cjs',
190
+ 'tailwind.config.mjs'
191
+ ];
192
+ for (const filename of candidates) {
193
+ const fullPath = path.join(targetDir, filename);
194
+ if (await pathExists(fullPath)) {
195
+ return filename;
196
+ }
197
+ }
198
+ return null;
199
+ }
144
200
  async function readJson(filePath, fallback) {
145
201
  try {
146
202
  const data = await fs.readFile(filePath, 'utf8');
@@ -178,74 +234,20 @@ async function pathExists(targetPath) {
178
234
  throw error;
179
235
  }
180
236
  }
181
- async function ensureNextTurbo(targetDir) {
182
- const packageJsonPath = path.join(targetDir, 'package.json');
183
- const packageJson = await readJson(packageJsonPath, {});
184
- if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
185
- packageJson.scripts = {};
186
- }
187
- const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
188
- if (!currentDev.includes('--turbo')) {
189
- packageJson.scripts.dev = `${currentDev} --turbo`;
190
- }
191
- await writeJson(packageJsonPath, packageJson);
192
- }
193
- async function ensureShadcnSetup(targetDir, projectSrcBase, usesSrcDir) {
194
- const libDir = path.join(projectSrcBase, 'lib');
195
- await fs.mkdir(libDir, { recursive: true });
196
- await fs.writeFile(path.join(libDir, 'utils.ts'), shadcnUtilsTemplate, 'utf8');
197
- const globalsPath = usesSrcDir ? 'src/app/globals.css' : 'app/globals.css';
198
- const tailwindConfigPath = await detectTailwindConfig(targetDir);
199
- const componentsJson = componentsJsonTemplate(globalsPath, tailwindConfigPath ?? 'tailwind.config.ts');
200
- await fs.writeFile(path.join(targetDir, 'components.json'), componentsJson, 'utf8');
201
- }
202
- async function detectTailwindConfig(targetDir) {
203
- const candidates = [
204
- 'tailwind.config.ts',
205
- 'tailwind.config.js',
206
- 'tailwind.config.cjs',
207
- 'tailwind.config.mjs'
208
- ];
209
- for (const filename of candidates) {
210
- const fullPath = path.join(targetDir, filename);
211
- if (await pathExists(fullPath)) {
212
- return filename;
213
- }
214
- }
215
- return null;
216
- }
217
- async function ensureExpoTamagui(targetDir) {
218
- const configPath = path.join(targetDir, 'tamagui.config.ts');
219
- await fs.writeFile(configPath, tamaguiConfigTemplate, 'utf8');
220
- const metroPath = path.join(targetDir, 'metro.config.js');
221
- await fs.writeFile(metroPath, metroConfigTemplate, 'utf8');
222
- await ensureBabelTamagui(targetDir);
223
- }
224
- async function ensureBabelTamagui(targetDir) {
225
- const babelPath = path.join(targetDir, 'babel.config.js');
226
- let content = '';
227
- if (await pathExists(babelPath)) {
228
- content = await fs.readFile(babelPath, 'utf8');
229
- }
230
- if (!content) {
231
- const defaultConfig = `module.exports = function (api) {\n api.cache(true);\n return {\n presets: ['babel-preset-expo'],\n plugins: [\n 'expo-router/babel',\n [\n '@tamagui/babel-plugin',\n {\n config: './tamagui.config.ts',\n components: ['tamagui']\n }\n ]\n ]\n };\n};\n`;
232
- await fs.writeFile(babelPath, defaultConfig, 'utf8');
233
- return;
234
- }
235
- if (content.includes('@tamagui/babel-plugin')) {
236
- return;
237
- }
238
- const pluginSnippet = `[\n '@tamagui/babel-plugin',\n {\n config: './tamagui.config.ts',\n components: ['tamagui']\n }\n ]`;
239
- const pluginsRegex = /plugins:\\s*\\[(.*)\\]/s;
240
- if (pluginsRegex.test(content)) {
241
- content = content.replace(pluginsRegex, (match, inner) => {
242
- const trimmed = inner.trim();
243
- const updatedInner = trimmed.length > 0 ? `${trimmed},\n ${pluginSnippet}` : pluginSnippet;
244
- return `plugins: [${updatedInner}]`;
245
- });
246
- }
247
- else {
248
- content = content.replace(/return\\s*\\{/, (match) => `${match}\n plugins: [${pluginSnippet}],`);
249
- }
250
- await fs.writeFile(babelPath, content, 'utf8');
237
+ function toPackageName(name) {
238
+ const cleaned = name
239
+ .trim()
240
+ .toLowerCase()
241
+ .replace(/[^a-z0-9-._]/g, '-')
242
+ .replace(/-+/g, '-')
243
+ .replace(/^[-_.]+|[-_.]+$/g, '');
244
+ return cleaned || 'aexis-zero-app';
245
+ }
246
+ function toSlug(name) {
247
+ return name
248
+ .trim()
249
+ .toLowerCase()
250
+ .replace(/[^a-z0-9-]/g, '-')
251
+ .replace(/-+/g, '-')
252
+ .replace(/^-|-$/g, '') || 'aexis-zero-app';
251
253
  }
@@ -1,7 +1,125 @@
1
- export const nextLayoutTemplate = `import type { ReactNode } from 'react';
1
+ export function componentsJsonTemplate(globalsPath, tailwindConfigPath) {
2
+ return `{
3
+ "$schema": "https://ui.shadcn.com/schema.json",
4
+ "style": "default",
5
+ "rsc": true,
6
+ "tsx": true,
7
+ "tailwind": {
8
+ "config": "${tailwindConfigPath}",
9
+ "css": "${globalsPath}",
10
+ "baseColor": "slate",
11
+ "cssVariables": true
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils"
16
+ }
17
+ }
18
+ `;
19
+ }
20
+ export function buildNextTemplateFiles(data) {
21
+ const base = data.basePath ? `${data.basePath}/` : '';
22
+ const envList = renderNextEnvList(data.envVars);
23
+ return [
24
+ {
25
+ path: `${base}app/layout.tsx`,
26
+ content: nextLayoutTemplate(data.appName, data.domain)
27
+ },
28
+ {
29
+ path: `${base}app/page.tsx`,
30
+ content: nextHomeTemplate(data.appName, data.domain, envList)
31
+ },
32
+ {
33
+ path: `${base}app/about/page.tsx`,
34
+ content: nextRouteTemplate('About', 'A concise overview of your project.')
35
+ },
36
+ {
37
+ path: `${base}app/guide/page.tsx`,
38
+ content: nextRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
39
+ },
40
+ {
41
+ path: `${base}app/globals.css`,
42
+ content: nextGlobalsCss()
43
+ },
44
+ {
45
+ path: `${base}components/site-header.tsx`,
46
+ content: nextHeaderTemplate(data.appName)
47
+ },
48
+ {
49
+ path: `${base}components/site-footer.tsx`,
50
+ content: nextFooterTemplate(data.domain)
51
+ },
52
+ {
53
+ path: `${base}components/env-list.tsx`,
54
+ content: nextEnvListTemplate(envList)
55
+ },
56
+ {
57
+ path: `${base}lib/utils.ts`,
58
+ content: nextUtilsTemplate()
59
+ }
60
+ ];
61
+ }
62
+ export function buildExpoTemplateFiles(data) {
63
+ const envItems = renderExpoEnvItems(data.envVars);
64
+ return [
65
+ {
66
+ path: 'app/_layout.tsx',
67
+ content: expoLayoutTemplate()
68
+ },
69
+ {
70
+ path: 'app/index.tsx',
71
+ content: expoHomeTemplate(data.appName, data.domain, envItems)
72
+ },
73
+ {
74
+ path: 'app/about.tsx',
75
+ content: expoRouteTemplate('About', 'A concise overview of your project.')
76
+ },
77
+ {
78
+ path: 'app/guide.tsx',
79
+ content: expoRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
80
+ },
81
+ {
82
+ path: 'components/theme.ts',
83
+ content: expoThemeTemplate()
84
+ },
85
+ {
86
+ path: 'components/site-header.tsx',
87
+ content: expoHeaderTemplate(data.appName)
88
+ },
89
+ {
90
+ path: 'components/site-footer.tsx',
91
+ content: expoFooterTemplate(data.domain)
92
+ },
93
+ {
94
+ path: 'components/env-list.tsx',
95
+ content: expoEnvListTemplate(envItems)
96
+ },
97
+ {
98
+ path: 'components/page-shell.tsx',
99
+ content: expoPageShellTemplate()
100
+ },
101
+ {
102
+ path: 'tamagui.config.ts',
103
+ content: tamaguiConfigTemplate()
104
+ },
105
+ {
106
+ path: 'metro.config.js',
107
+ content: metroConfigTemplate()
108
+ },
109
+ {
110
+ path: 'babel.config.js',
111
+ content: babelConfigTemplate()
112
+ }
113
+ ];
114
+ }
115
+ function nextLayoutTemplate(appName, domain) {
116
+ return `import type { ReactNode } from 'react';
117
+ import './globals.css';
118
+ import { SiteHeader } from '@/components/site-header';
119
+ import { SiteFooter } from '@/components/site-footer';
2
120
 
3
121
  export const metadata = {
4
- title: 'Aexis Zero App',
122
+ title: '${escapeTemplate(appName)}',
5
123
  description: 'Scaffolded by Aexis Zero.'
6
124
  };
7
125
 
@@ -12,74 +130,520 @@ export default function RootLayout({
12
130
  }) {
13
131
  return (
14
132
  <html lang="en">
15
- <body>{children}</body>
133
+ <body className="min-h-screen bg-[var(--bg)] text-[var(--fg)]">
134
+ <div className="flex min-h-screen flex-col">
135
+ <SiteHeader appName="${escapeTemplate(appName)}" />
136
+ <main className="flex-1">{children}</main>
137
+ <SiteFooter domain="${escapeTemplate(domain)}" />
138
+ </div>
139
+ </body>
16
140
  </html>
17
141
  );
18
142
  }
19
143
  `;
20
- export const nextPageTemplate = `export default function Home() {
21
- return <main />;
144
+ }
145
+ function nextHomeTemplate(appName, domain, envList) {
146
+ return `import { EnvList } from '@/components/env-list';
147
+
148
+ export default function Home() {
149
+ return (
150
+ <section className="mx-auto flex w-full max-w-3xl flex-col gap-8 px-6 py-12">
151
+ <div className="flex flex-col gap-3">
152
+ <p className="text-xs uppercase tracking-[0.4em]">Hello World</p>
153
+ <h1 className="text-3xl font-thin">${escapeTemplate(appName)}</h1>
154
+ <p className="text-sm">
155
+ ${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'No domain configured yet.'}
156
+ </p>
157
+ </div>
158
+ <div className="rounded-xl border border-[var(--fg)] p-6">
159
+ <h2 className="text-lg font-medium">Environment variables</h2>
160
+ <p className="text-sm">Set these in your <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">.env</code>.</p>
161
+ <EnvList />
162
+ </div>
163
+ <div className="rounded-xl border border-[var(--fg)] p-6">
164
+ <h2 className="text-lg font-medium">Routes</h2>
165
+ <p className="text-sm">Explore <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code> and <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code>.</p>
166
+ </div>
167
+ </section>
168
+ );
169
+ }
170
+ `;
171
+ }
172
+ function nextRouteTemplate(title, body) {
173
+ return `export default function Page() {
174
+ return (
175
+ <section className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-12">
176
+ <h1 className="text-3xl font-thin">${title}</h1>
177
+ <p className="text-sm">${body}</p>
178
+ </section>
179
+ );
180
+ }
181
+ `;
182
+ }
183
+ function nextHeaderTemplate(appName) {
184
+ return `import Link from 'next/link';
185
+
186
+ const links = [
187
+ { href: '/', label: 'Home' },
188
+ { href: '/about', label: 'About' },
189
+ { href: '/guide', label: 'Guide' }
190
+ ];
191
+
192
+ export function SiteHeader({ appName }: { appName: string }) {
193
+ return (
194
+ <header className="border-b border-[var(--fg)]">
195
+ <div className="mx-auto flex w-full max-w-4xl items-center justify-between px-6 py-4">
196
+ <div className="flex items-baseline gap-3">
197
+ <span className="text-xl font-thin tracking-[0.3em]">ZER0</span>
198
+ <span className="text-xs uppercase tracking-[0.2em]">{appName}</span>
199
+ </div>
200
+ <nav className="hidden items-center gap-6 text-sm sm:flex">
201
+ {links.map((link) => (
202
+ <Link key={link.href} href={link.href} className="underline-offset-4 hover:underline">
203
+ {link.label}
204
+ </Link>
205
+ ))}
206
+ </nav>
207
+ <details className="sm:hidden">
208
+ <summary className="cursor-pointer text-sm">Menu</summary>
209
+ <div className="mt-3 flex flex-col gap-3 text-sm">
210
+ {links.map((link) => (
211
+ <Link key={link.href} href={link.href} className="underline-offset-4 hover:underline">
212
+ {link.label}
213
+ </Link>
214
+ ))}
215
+ </div>
216
+ </details>
217
+ </div>
218
+ </header>
219
+ );
220
+ }
221
+ `;
222
+ }
223
+ function nextFooterTemplate(domain) {
224
+ const domainLabel = escapeTemplate(domain).trim().length > 0
225
+ ? `Domain: ${escapeTemplate(domain)}`
226
+ : 'Domain: not set';
227
+ return `export function SiteFooter({ domain }: { domain?: string }) {
228
+ return (
229
+ <footer className=\"border-t border-[var(--fg)]\">
230
+ <div className=\"mx-auto flex w-full max-w-4xl flex-col gap-2 px-6 py-4 text-xs\">
231
+ <span>${domainLabel}</span>
232
+ <span>Generated by Aexis Zero.</span>
233
+ </div>
234
+ </footer>
235
+ );
22
236
  }
23
237
  `;
24
- export const shadcnUtilsTemplate = `import { clsx, type ClassValue } from 'clsx';
238
+ }
239
+ function nextEnvListTemplate(envList) {
240
+ return `export function EnvList() {
241
+ return (
242
+ <div className="mt-4 flex flex-col gap-4">
243
+ ${envList}
244
+ </div>
245
+ );
246
+ }
247
+ `;
248
+ }
249
+ function nextUtilsTemplate() {
250
+ return `import { clsx, type ClassValue } from 'clsx';
25
251
  import { twMerge } from 'tailwind-merge';
26
252
 
27
253
  export function cn(...inputs: ClassValue[]) {
28
254
  return twMerge(clsx(inputs));
29
255
  }
30
256
  `;
31
- export const componentsJsonTemplate = (globalsPath, tailwindConfigPath) => `{
32
- "$schema": "https://ui.shadcn.com/schema.json",
33
- "style": "default",
34
- "rsc": true,
35
- "tsx": true,
36
- "tailwind": {
37
- "config": "${tailwindConfigPath}",
38
- "css": "${globalsPath}",
39
- "baseColor": "slate",
40
- "cssVariables": true
41
- },
42
- "aliases": {
43
- "components": "@/components",
44
- "utils": "@/lib/utils"
257
+ }
258
+ function nextGlobalsCss() {
259
+ return `@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100;200;300;400;500;600;700;800;900&display=swap');
260
+
261
+ @tailwind base;
262
+ @tailwind components;
263
+ @tailwind utilities;
264
+
265
+ :root {
266
+ --bg: #E7E5E4;
267
+ --fg: #1C1917;
268
+ }
269
+
270
+ @media (prefers-color-scheme: dark) {
271
+ :root {
272
+ --bg: #1C1917;
273
+ --fg: #E7E5E4;
45
274
  }
46
275
  }
47
- `;
48
- export const tamaguiConfigTemplate = `import { config } from '@tamagui/config/v3';
49
276
 
50
- export default config;
51
- `;
52
- export const metroConfigTemplate = `const { getDefaultConfig } = require('expo/metro-config');
53
- const { withTamagui } = require('@tamagui/metro-plugin');
277
+ * {
278
+ border-color: var(--fg);
279
+ }
54
280
 
55
- const config = getDefaultConfig(__dirname);
281
+ html,
282
+ body {
283
+ min-height: 100%;
284
+ background: var(--bg);
285
+ color: var(--fg);
286
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
287
+ }
56
288
 
57
- module.exports = withTamagui(config, {
58
- components: ['tamagui'],
59
- config: './tamagui.config.ts',
60
- outputCSS: './tamagui-web.css'
61
- });
289
+ a {
290
+ color: inherit;
291
+ text-decoration: underline;
292
+ text-underline-offset: 4px;
293
+ }
294
+
295
+ code {
296
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
297
+ }
62
298
  `;
63
- export const expoLayoutTemplate = `import { Stack } from 'expo-router';
64
- import { TamaguiProvider } from 'tamagui';
299
+ }
300
+ function renderNextEnvList(envVars) {
301
+ if (envVars.length === 0) {
302
+ return '<p className="text-sm">No environment variables required.</p>';
303
+ }
304
+ return envVars
305
+ .map((item) => {
306
+ return `
307
+ <div className="rounded-lg border border-[var(--fg)] p-4">
308
+ <div className="flex flex-wrap items-center gap-2">
309
+ <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">${escapeTemplate(item.key)}</code>
310
+ <span className="text-sm">${escapeTemplate(item.description)}</span>
311
+ </div>
312
+ <a
313
+ className="mt-2 inline-flex text-sm underline underline-offset-4"
314
+ href="${escapeAttribute(item.url)}"
315
+ target="_blank"
316
+ rel="noreferrer"
317
+ >
318
+ Get keys →
319
+ </a>
320
+ </div>`;
321
+ })
322
+ .join('\n');
323
+ }
324
+ function expoLayoutTemplate() {
325
+ return `import { Stack } from 'expo-router';
326
+ import { TamaguiProvider, Theme } from 'tamagui';
327
+ import { useColorScheme } from 'react-native';
328
+ import { useFonts, GeistMono_100Thin } from '@expo-google-fonts/geist-mono';
65
329
  import config from '../tamagui.config';
66
330
 
67
331
  export default function RootLayout() {
332
+ const scheme = useColorScheme();
333
+ const [loaded] = useFonts({ GeistMono_100Thin });
334
+
335
+ if (!loaded) {
336
+ return null;
337
+ }
338
+
68
339
  return (
69
340
  <TamaguiProvider config={config}>
70
- <Stack screenOptions={{ headerShown: false }} />
341
+ <Theme name={scheme === 'dark' ? 'dark' : 'light'}>
342
+ <Stack screenOptions={{ headerShown: false }} />
343
+ </Theme>
71
344
  </TamaguiProvider>
72
345
  );
73
346
  }
74
347
  `;
75
- export const expoIndexTemplate = `import { Button, H1, YStack } from 'tamagui';
348
+ }
349
+ function expoHomeTemplate(appName, domain, envItems) {
350
+ return `import { Text, YStack } from 'tamagui';
351
+ import { PageShell } from '../components/page-shell';
352
+ import { EnvList } from '../components/env-list';
353
+ import { FONT_FAMILY, useThemeColors } from '../components/theme';
76
354
 
77
355
  export default function Home() {
356
+ const { fg } = useThemeColors();
357
+
78
358
  return (
79
- <YStack flex={1} alignItems=\"center\" justifyContent=\"center\" gap=\"$4\">
80
- <H1>Aexis Zero</H1>
81
- <Button>Start building</Button>
359
+ <PageShell
360
+ title="${escapeTemplate(appName)}"
361
+ subtitle="${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'No domain configured yet.'}"
362
+ badge="Hello World"
363
+ >
364
+ <YStack borderWidth={1} borderColor={fg} padding="$4" borderRadius="$4" gap="$3">
365
+ <Text fontFamily={FONT_FAMILY} fontSize="$4" color={fg}>
366
+ Environment variables
367
+ </Text>
368
+ <EnvList />
369
+ </YStack>
370
+ <YStack borderWidth={1} borderColor={fg} padding="$4" borderRadius="$4" gap="$3">
371
+ <Text fontFamily={FONT_FAMILY} fontSize="$4" color={fg}>
372
+ Routes
373
+ </Text>
374
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
375
+ Visit /about and /guide.
376
+ </Text>
377
+ </YStack>
378
+ </PageShell>
379
+ );
380
+ }
381
+ `;
382
+ }
383
+ function expoRouteTemplate(title, body) {
384
+ return `import { PageShell } from '../components/page-shell';
385
+
386
+ export default function Page() {
387
+ return (
388
+ <PageShell title="${title}" subtitle="${body}">
389
+ <></>
390
+ </PageShell>
391
+ );
392
+ }
393
+ `;
394
+ }
395
+ function expoThemeTemplate() {
396
+ return `import { useColorScheme } from 'react-native';
397
+
398
+ export const COLORS = {
399
+ light: {
400
+ bg: '#E7E5E4',
401
+ fg: '#1C1917'
402
+ },
403
+ dark: {
404
+ bg: '#1C1917',
405
+ fg: '#E7E5E4'
406
+ }
407
+ };
408
+
409
+ export const FONT_FAMILY = 'GeistMono_100Thin';
410
+
411
+ export function useThemeColors() {
412
+ const scheme = useColorScheme();
413
+ const mode = scheme === 'dark' ? 'dark' : 'light';
414
+ return {
415
+ mode,
416
+ bg: COLORS[mode].bg,
417
+ fg: COLORS[mode].fg
418
+ };
419
+ }
420
+ `;
421
+ }
422
+ function expoHeaderTemplate(appName) {
423
+ return `import { useState } from 'react';
424
+ import { Link } from 'expo-router';
425
+ import { Button, Text, XStack, YStack } from 'tamagui';
426
+ import { FONT_FAMILY, useThemeColors } from './theme';
427
+
428
+ const links = [
429
+ { href: '/', label: 'Home' },
430
+ { href: '/about', label: 'About' },
431
+ { href: '/guide', label: 'Guide' }
432
+ ];
433
+
434
+ export function SiteHeader() {
435
+ const [open, setOpen] = useState(false);
436
+ const { bg, fg } = useThemeColors();
437
+
438
+ return (
439
+ <YStack backgroundColor={bg} paddingHorizontal="$5" paddingVertical="$4" borderBottomWidth={1} borderColor={fg}>
440
+ <XStack alignItems="center" justifyContent="space-between">
441
+ <XStack alignItems="center" gap="$3">
442
+ <Text fontFamily={FONT_FAMILY} fontWeight="100" fontSize="$6" color={fg}>
443
+ ZER0
444
+ </Text>
445
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" textTransform="uppercase" color={fg}>
446
+ ${escapeTemplate(appName)}
447
+ </Text>
448
+ </XStack>
449
+ <Button
450
+ backgroundColor={fg}
451
+ color={bg}
452
+ fontFamily={FONT_FAMILY}
453
+ size="$2"
454
+ onPress={() => setOpen((prev) => !prev)}
455
+ >
456
+ Menu
457
+ </Button>
458
+ </XStack>
459
+ {open ? (
460
+ <YStack marginTop="$3" gap="$2">
461
+ {links.map((link) => (
462
+ <Link key={link.href} href={link.href} asChild>
463
+ <Text fontFamily={FONT_FAMILY} textDecorationLine="underline" color={fg}>
464
+ {link.label}
465
+ </Text>
466
+ </Link>
467
+ ))}
468
+ </YStack>
469
+ ) : null}
470
+ </YStack>
471
+ );
472
+ }
473
+ `;
474
+ }
475
+ function expoFooterTemplate(domain) {
476
+ return `import { Text, YStack } from 'tamagui';
477
+ import { FONT_FAMILY, useThemeColors } from './theme';
478
+
479
+ export function SiteFooter() {
480
+ const { bg, fg } = useThemeColors();
481
+
482
+ return (
483
+ <YStack backgroundColor={bg} paddingHorizontal="$5" paddingVertical="$4" borderTopWidth={1} borderColor={fg}>
484
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
485
+ ${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'Domain: not set'}
486
+ </Text>
487
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
488
+ Generated by Aexis Zero.
489
+ </Text>
82
490
  </YStack>
83
491
  );
84
492
  }
85
493
  `;
494
+ }
495
+ function expoEnvListTemplate(envItems) {
496
+ return `import { Linking } from 'react-native';
497
+ import { Text, YStack } from 'tamagui';
498
+ import { FONT_FAMILY, useThemeColors } from './theme';
499
+
500
+ const envItems = ${envItems};
501
+
502
+ export function EnvList() {
503
+ const { bg, fg } = useThemeColors();
504
+
505
+ if (envItems.length === 0) {
506
+ return (
507
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
508
+ No environment variables required.
509
+ </Text>
510
+ );
511
+ }
512
+
513
+ return (
514
+ <YStack gap="$3">
515
+ {envItems.map((item) => (
516
+ <YStack key={item.key} borderWidth={1} borderColor={fg} padding="$3" borderRadius="$4">
517
+ <Text fontFamily={FONT_FAMILY} color={fg}>{item.description}</Text>
518
+ <Text
519
+ fontFamily={FONT_FAMILY}
520
+ backgroundColor={fg}
521
+ color={bg}
522
+ paddingHorizontal="$2"
523
+ paddingVertical="$1"
524
+ borderRadius="$2"
525
+ marginTop="$2"
526
+ >
527
+ {item.key}
528
+ </Text>
529
+ <Text
530
+ fontFamily={FONT_FAMILY}
531
+ textDecorationLine="underline"
532
+ color={fg}
533
+ marginTop="$2"
534
+ onPress={() => Linking.openURL(item.url)}
535
+ >
536
+ Get keys →
537
+ </Text>
538
+ </YStack>
539
+ ))}
540
+ </YStack>
541
+ );
542
+ }
543
+ `;
544
+ }
545
+ function expoPageShellTemplate() {
546
+ return `import type { ReactNode } from 'react';
547
+ import { ScrollView, Text, YStack } from 'tamagui';
548
+ import { SiteHeader } from './site-header';
549
+ import { SiteFooter } from './site-footer';
550
+ import { FONT_FAMILY, useThemeColors } from './theme';
551
+
552
+ interface PageShellProps {
553
+ title: string;
554
+ subtitle: string;
555
+ badge?: string;
556
+ children: ReactNode;
557
+ }
558
+
559
+ export function PageShell({ title, subtitle, badge, children }: PageShellProps) {
560
+ const { bg, fg } = useThemeColors();
561
+
562
+ return (
563
+ <YStack flex={1} backgroundColor={bg}>
564
+ <SiteHeader />
565
+ <ScrollView contentContainerStyle={{ padding: 24 }}>
566
+ <YStack gap="$4">
567
+ {badge ? (
568
+ <Text fontFamily={FONT_FAMILY} fontSize="$2" textTransform="uppercase" letterSpacing={2} color={fg}>
569
+ {badge}
570
+ </Text>
571
+ ) : null}
572
+ <Text fontFamily={FONT_FAMILY} fontSize="$7" color={fg}>
573
+ {title}
574
+ </Text>
575
+ <Text fontFamily={FONT_FAMILY} fontSize="$3" color={fg}>
576
+ {subtitle}
577
+ </Text>
578
+ {children}
579
+ </YStack>
580
+ </ScrollView>
581
+ <SiteFooter />
582
+ </YStack>
583
+ );
584
+ }
585
+ `;
586
+ }
587
+ function tamaguiConfigTemplate() {
588
+ return `import { config } from '@tamagui/config/v3';
589
+
590
+ export default config;
591
+ `;
592
+ }
593
+ function metroConfigTemplate() {
594
+ return `const { getDefaultConfig } = require('expo/metro-config');
595
+ const { withTamagui } = require('@tamagui/metro-plugin');
596
+
597
+ const config = getDefaultConfig(__dirname);
598
+
599
+ module.exports = withTamagui(config, {
600
+ components: ['tamagui'],
601
+ config: './tamagui.config.ts',
602
+ outputCSS: './tamagui-web.css'
603
+ });
604
+ `;
605
+ }
606
+ function babelConfigTemplate() {
607
+ return `module.exports = function (api) {
608
+ api.cache(true);
609
+ return {
610
+ presets: ['babel-preset-expo'],
611
+ plugins: [
612
+ 'expo-router/babel',
613
+ [
614
+ '@tamagui/babel-plugin',
615
+ {
616
+ config: './tamagui.config.ts',
617
+ components: ['tamagui']
618
+ }
619
+ ]
620
+ ]
621
+ };
622
+ };
623
+ `;
624
+ }
625
+ function renderExpoEnvItems(envVars) {
626
+ if (envVars.length === 0) {
627
+ return '[]';
628
+ }
629
+ const items = envVars.map((item) => {
630
+ return `{
631
+ key: '${escapeTemplate(item.key)}',
632
+ description: '${escapeTemplate(item.description)}',
633
+ url: '${escapeTemplate(item.url)}'
634
+ }`;
635
+ });
636
+ return `[
637
+ ${items.join(',\n ')}
638
+ ]`;
639
+ }
640
+ function escapeTemplate(value) {
641
+ return value
642
+ .replace(/`/g, '\\`')
643
+ .replace(/\$/g, '\\$')
644
+ .replace(/"/g, '\\"')
645
+ .replace(/'/g, "\\'");
646
+ }
647
+ function escapeAttribute(value) {
648
+ return value.replace(/"/g, '&quot;');
649
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { render } from 'ink';
4
- import { App } from './ui/App.js';
2
+ import { runWizard } from './cli/prompts.js';
5
3
  import { assertBunAvailable } from './env/detect.js';
6
4
  import { scaffoldProject } from './engine/scaffold.js';
7
5
  async function main() {
@@ -14,13 +12,8 @@ async function main() {
14
12
  process.exitCode = 1;
15
13
  return;
16
14
  }
17
- let config = null;
18
- const { waitUntilExit } = render(_jsx(App, { onComplete: (result) => {
19
- config = result;
20
- } }));
21
- await waitUntilExit();
15
+ const config = await runWizard();
22
16
  if (!config) {
23
- console.log('Cancelled.');
24
17
  return;
25
18
  }
26
19
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aex.is/zero",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Aexis Zero scaffolding CLI",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -18,22 +18,17 @@
18
18
  },
19
19
  "scripts": {
20
20
  "build": "tsc -p tsconfig.json",
21
- "dev": "bun run src/index.tsx",
21
+ "dev": "bun run src/index.ts",
22
22
  "start": "node dist/index.js",
23
23
  "prepublishOnly": "npm run build"
24
24
  },
25
25
  "dependencies": {
26
+ "@clack/prompts": "^0.7.0",
26
27
  "execa": "^8.0.1",
27
- "ink": "^4.4.1",
28
- "ink-text-input": "^5.0.1",
29
- "react": "^18.3.1",
30
- "react-dom": "^18.3.1",
31
28
  "which": "^4.0.0"
32
29
  },
33
30
  "devDependencies": {
34
31
  "@types/node": "^20.11.30",
35
- "@types/react": "^18.3.3",
36
- "@types/react-dom": "^18.3.0",
37
32
  "@types/which": "^3.0.3",
38
33
  "typescript": "^5.5.4"
39
34
  }