@aex.is/zero 0.1.3 → 0.1.5

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
  }