@aex.is/zero 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aexis Zero
2
2
 
3
- A cross-platform interactive CLI for scaffolding modern apps with a terminal UI.
3
+ A cross-platform interactive CLI for scaffolding minimal Next.js and Expo apps with a terminal-first UI.
4
4
 
5
5
  ## Install (global)
6
6
 
@@ -24,9 +24,31 @@ zero
24
24
 
25
25
  - Bun installed and available on PATH.
26
26
 
27
+ ## Generated starters
28
+
29
+ - Next.js App Router + Tailwind + shadcn-ready config.
30
+ - Expo Router + Tamagui.
31
+ - Minimal layout, three routes, metadata, and icon generation.
32
+ - Contact form wired to `/api/contact` (Next) and mailto/endpoint fallback (Expo).
33
+
34
+ ## Environment variables
35
+
36
+ The CLI generates `.env.example` with:
37
+ - Next.js: `RESEND_API_KEY`, `EMAIL_FROM`, `EMAIL_TO`.
38
+ - Expo: `EXPO_PUBLIC_CONTACT_EMAIL`, `EXPO_PUBLIC_CONTACT_ENDPOINT`.
39
+ - Any selected module keys (Neon, Clerk, Payload, Stripe).
40
+
27
41
  ## Development
28
42
 
29
43
  ```sh
30
44
  bun install
31
45
  bun run dev
32
46
  ```
47
+
48
+ ### Build requirements
49
+
50
+ - Go (for the Bubble Tea TUI binary build).
51
+ - Brand assets in `assets/`:
52
+ - `assets/icon.svg`
53
+ - `assets/icon.png`
54
+ - `assets/social.png`
Binary file
@@ -0,0 +1,4 @@
1
+ <svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="900" height="900" fill="#1E1E1E"/>
3
+ <path d="M340.641 622.501L541.133 273.593L559.359 277.499L358.867 626.407L340.641 622.501ZM450 691.501C419.188 691.501 391.849 681.303 367.981 660.907C344.547 640.077 326.32 611.652 313.301 575.633C300.282 539.18 293.773 497.519 293.773 450.651C293.773 403.349 300.282 361.471 313.301 325.018C326.32 288.565 344.547 260.14 367.981 239.744C391.849 218.914 419.188 208.499 450 208.499C481.245 208.499 508.585 218.914 532.019 239.744C555.453 260.14 573.68 288.565 586.699 325.018C599.718 361.471 606.227 403.349 606.227 450.651C606.227 497.519 599.718 539.18 586.699 575.633C573.68 611.652 555.453 640.077 532.019 660.907C508.585 681.303 481.245 691.501 450 691.501ZM450 670.671C476.906 670.671 500.34 661.341 520.302 642.68C540.265 624.02 555.887 598.199 567.17 565.218C578.453 531.802 584.095 493.613 584.095 450.651C584.095 407.255 578.453 369.066 567.17 336.084C555.887 302.669 540.265 276.631 520.302 257.971C500.34 238.876 476.906 229.329 450 229.329C423.528 229.329 400.094 238.876 379.698 257.971C359.735 276.631 344.113 302.669 332.83 336.084C321.546 369.066 315.905 407.255 315.905 450.651C315.905 493.613 321.546 531.802 332.83 565.218C344.113 598.199 359.735 624.02 379.698 642.68C400.094 661.341 423.528 670.671 450 670.671Z" fill="#E7E5E4"/>
4
+ </svg>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,57 @@
1
+ import { execa } from 'execa';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { promises as fs } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ export async function runWizard() {
7
+ const binPath = resolveTuiBinary();
8
+ try {
9
+ await fs.stat(binPath);
10
+ }
11
+ catch {
12
+ throw new Error(`TUI binary not found. Expected ${binPath}. Reinstall the CLI or run npm run build:tui.`);
13
+ }
14
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aexis-zero-'));
15
+ const outputPath = path.join(tmpDir, 'result.json');
16
+ try {
17
+ await execa(binPath, ['-output', outputPath], {
18
+ stdio: 'inherit',
19
+ shell: false
20
+ });
21
+ }
22
+ catch (error) {
23
+ return null;
24
+ }
25
+ try {
26
+ const content = await fs.readFile(outputPath, 'utf8');
27
+ if (!content.trim()) {
28
+ return null;
29
+ }
30
+ return JSON.parse(content);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ function resolveTuiBinary() {
37
+ const platform = process.platform;
38
+ const arch = process.arch;
39
+ let target = '';
40
+ if (platform === 'darwin') {
41
+ target = arch === 'arm64' ? 'darwin-arm64' : 'darwin-amd64';
42
+ }
43
+ else if (platform === 'linux') {
44
+ target = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
45
+ }
46
+ else if (platform === 'win32') {
47
+ target = 'windows-amd64';
48
+ }
49
+ else {
50
+ throw new Error(`Unsupported platform: ${platform}`);
51
+ }
52
+ const exe = platform === 'win32' ? '.exe' : '';
53
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
54
+ const binDir = path.resolve(currentDir, '..', '..', 'bin');
55
+ const binPath = path.join(binDir, `zero-${target}${exe}`);
56
+ return binPath;
57
+ }
@@ -0,0 +1,31 @@
1
+ const nextBaseEnv = [
2
+ {
3
+ key: 'RESEND_API_KEY',
4
+ description: 'Resend API key',
5
+ url: 'https://resend.com'
6
+ },
7
+ {
8
+ key: 'EMAIL_FROM',
9
+ description: 'Verified sender email address'
10
+ },
11
+ {
12
+ key: 'EMAIL_TO',
13
+ description: 'Destination email address'
14
+ }
15
+ ];
16
+ const expoBaseEnv = [
17
+ {
18
+ key: 'EXPO_PUBLIC_CONTACT_EMAIL',
19
+ description: 'Email address used for contact form'
20
+ },
21
+ {
22
+ key: 'EXPO_PUBLIC_CONTACT_ENDPOINT',
23
+ description: 'Optional contact API endpoint'
24
+ }
25
+ ];
26
+ export function getBaseEnvHelp(framework) {
27
+ return framework === 'nextjs' ? nextBaseEnv : expoBaseEnv;
28
+ }
29
+ export function getBaseEnvVars(framework) {
30
+ return getBaseEnvHelp(framework).map((item) => item.key);
31
+ }
@@ -7,6 +7,7 @@ export const frameworks = [
7
7
  'class-variance-authority',
8
8
  'clsx',
9
9
  'lucide-react',
10
+ 'resend',
10
11
  'tailwind-merge',
11
12
  'tailwindcss-animate',
12
13
  '@radix-ui/react-slot'
@@ -0,0 +1,54 @@
1
+ import path from 'path';
2
+ import { promises as fs } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import sharp from 'sharp';
5
+ export function resolveAssetSources() {
6
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
7
+ const rootDir = path.resolve(currentDir, '..', '..');
8
+ const assetsDir = path.join(rootDir, 'assets');
9
+ return {
10
+ iconSvg: path.join(assetsDir, 'icon.svg'),
11
+ iconPng: path.join(assetsDir, 'icon.png'),
12
+ socialPng: path.join(assetsDir, 'social.png')
13
+ };
14
+ }
15
+ export async function assertAssetSources(sources) {
16
+ const missing = [];
17
+ for (const [key, value] of Object.entries(sources)) {
18
+ try {
19
+ await fs.stat(value);
20
+ }
21
+ catch {
22
+ missing.push(`${key}: ${value}`);
23
+ }
24
+ }
25
+ if (missing.length > 0) {
26
+ throw new Error(`Missing asset files. Add them to the CLI package under /assets:\n${missing.join('\n')}`);
27
+ }
28
+ }
29
+ export async function generateNextAssets(sources, targets) {
30
+ await fs.mkdir(targets.appDir, { recursive: true });
31
+ await fs.mkdir(targets.publicDir, { recursive: true });
32
+ await fs.copyFile(sources.iconSvg, path.join(targets.appDir, 'icon.svg'));
33
+ await resizePng(sources.iconPng, path.join(targets.appDir, 'apple-icon.png'), 180, 180);
34
+ await resizePng(sources.socialPng, path.join(targets.appDir, 'opengraph-image.png'), 1200, 630);
35
+ await resizePng(sources.socialPng, path.join(targets.appDir, 'twitter-image.png'), 1200, 630);
36
+ await resizePng(sources.iconPng, path.join(targets.publicDir, 'favicon-16x16.png'), 16, 16);
37
+ await resizePng(sources.iconPng, path.join(targets.publicDir, 'favicon-32x32.png'), 32, 32);
38
+ await resizePng(sources.iconPng, path.join(targets.publicDir, 'apple-touch-icon.png'), 180, 180);
39
+ await resizePng(sources.iconPng, path.join(targets.publicDir, 'android-chrome-192x192.png'), 192, 192);
40
+ await resizePng(sources.iconPng, path.join(targets.publicDir, 'android-chrome-512x512.png'), 512, 512);
41
+ }
42
+ export async function generateExpoAssets(sources, targets) {
43
+ await fs.mkdir(targets.assetsDir, { recursive: true });
44
+ await resizePng(sources.iconPng, path.join(targets.assetsDir, 'icon.png'), 1024, 1024);
45
+ await resizePng(sources.iconPng, path.join(targets.assetsDir, 'adaptive-icon.png'), 1024, 1024);
46
+ await resizePng(sources.iconPng, path.join(targets.assetsDir, 'favicon.png'), 48, 48);
47
+ await resizePng(sources.socialPng, path.join(targets.assetsDir, 'splash.png'), 1200, 630);
48
+ }
49
+ async function resizePng(source, destination, width, height) {
50
+ await sharp(source)
51
+ .resize(width, height, { fit: 'cover' })
52
+ .png()
53
+ .toFile(destination);
54
+ }
@@ -1,9 +1,11 @@
1
1
  import path from 'path';
2
2
  import { promises as fs } from 'fs';
3
+ import { getBaseEnvVars } from '../config/base-env.js';
3
4
  import { getModuleEnvVars } from '../config/modules.js';
4
- export async function writeEnvExample(moduleIds, targetDir) {
5
- const envVars = getModuleEnvVars(moduleIds);
6
- const lines = envVars.map((key) => `${key}=`);
5
+ export async function writeEnvExample(moduleIds, framework, targetDir) {
6
+ const envVars = [...getBaseEnvVars(framework), ...getModuleEnvVars(moduleIds)];
7
+ const unique = Array.from(new Set(envVars));
8
+ const lines = unique.map((key) => `${key}=`);
7
9
  const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
8
10
  const envPath = path.join(targetDir, '.env.example');
9
11
  await fs.writeFile(envPath, content, 'utf8');
@@ -2,9 +2,11 @@ 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 { getBaseEnvHelp } from '../config/base-env.js';
5
6
  import { getModuleEnvHelp } from '../config/modules.js';
6
7
  import { installBaseDependencies, installModulePackages } from './installers.js';
7
8
  import { writeEnvExample } from './env.js';
9
+ import { assertAssetSources, generateExpoAssets, generateNextAssets, resolveAssetSources } from './assets.js';
8
10
  import { buildNextTemplateFiles, buildExpoTemplateFiles, componentsJsonTemplate } from './templates.js';
9
11
  const baseEnv = {
10
12
  ...process.env,
@@ -15,6 +17,8 @@ export async function scaffoldProject(config) {
15
17
  const targetDir = path.resolve(process.cwd(), directoryInput);
16
18
  await ensureEmptyTargetDir(targetDir);
17
19
  const framework = getFrameworkDefinition(config.framework);
20
+ const sources = resolveAssetSources();
21
+ await assertAssetSources(sources);
18
22
  console.log(`Scaffolding ${framework.label}...`);
19
23
  await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, directoryInput, framework.scaffold.argSets);
20
24
  console.log('Applying framework templates...');
@@ -24,12 +28,29 @@ export async function scaffoldProject(config) {
24
28
  else {
25
29
  await applyExpoTemplates(config, targetDir);
26
30
  }
31
+ console.log('Generating brand assets...');
32
+ if (config.framework === 'nextjs') {
33
+ const usesSrcDir = await pathExists(path.join(targetDir, 'src', 'app'));
34
+ const appDir = usesSrcDir ? path.join(targetDir, 'src', 'app') : path.join(targetDir, 'app');
35
+ await generateNextAssets(sources, {
36
+ appDir,
37
+ publicDir: path.join(targetDir, 'public'),
38
+ assetsDir: path.join(targetDir, 'assets')
39
+ });
40
+ }
41
+ else {
42
+ await generateExpoAssets(sources, {
43
+ appDir: path.join(targetDir, 'app'),
44
+ publicDir: path.join(targetDir, 'public'),
45
+ assetsDir: path.join(targetDir, 'assets')
46
+ });
47
+ }
27
48
  console.log('Installing base dependencies with Bun...');
28
49
  await installBaseDependencies(targetDir, framework.packages);
29
50
  console.log('Installing module packages...');
30
51
  await installModulePackages(config.framework, config.modules, targetDir);
31
52
  console.log('Generating .env.example...');
32
- await writeEnvExample(config.modules, targetDir);
53
+ await writeEnvExample(config.modules, config.framework, targetDir);
33
54
  console.log('Scaffold complete.');
34
55
  const cdTarget = directoryInput === '.' ? '.' : directoryInput.includes(' ') ? `"${directoryInput}"` : directoryInput;
35
56
  console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
@@ -79,7 +100,7 @@ async function applyNextTemplates(config, targetDir) {
79
100
  const srcAppDir = path.join(targetDir, 'src', 'app');
80
101
  const usesSrcDir = await pathExists(srcAppDir);
81
102
  const basePath = usesSrcDir ? 'src' : '';
82
- const envHelp = getModuleEnvHelp(config.modules);
103
+ const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
83
104
  const files = buildNextTemplateFiles({
84
105
  appName: config.appName,
85
106
  domain: config.domain,
@@ -95,7 +116,7 @@ async function applyNextTemplates(config, targetDir) {
95
116
  await ensurePackageName(targetDir, config.appName);
96
117
  }
97
118
  async function applyExpoTemplates(config, targetDir) {
98
- const envHelp = getModuleEnvHelp(config.modules);
119
+ const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
99
120
  const files = buildExpoTemplateFiles({
100
121
  appName: config.appName,
101
122
  domain: config.domain,
@@ -114,6 +135,12 @@ async function ensureExpoConfig(targetDir, appName) {
114
135
  }
115
136
  appJson.expo.name = appName;
116
137
  appJson.expo.slug = toSlug(appName);
138
+ appJson.expo.icon = './assets/icon.png';
139
+ appJson.expo.splash = {
140
+ image: './assets/splash.png',
141
+ resizeMode: 'contain',
142
+ backgroundColor: '#E7E5E4'
143
+ };
117
144
  if (!Array.isArray(appJson.expo.platforms)) {
118
145
  appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
119
146
  }
@@ -128,6 +155,24 @@ async function ensureExpoConfig(targetDir, appName) {
128
155
  const current = appJson.expo.plugins.filter((value) => typeof value === 'string');
129
156
  appJson.expo.plugins = mergeUnique(current, ['expo-router']);
130
157
  }
158
+ if (!appJson.expo.android || typeof appJson.expo.android !== 'object') {
159
+ appJson.expo.android = {};
160
+ }
161
+ appJson.expo.android.adaptiveIcon = {
162
+ foregroundImage: './assets/adaptive-icon.png',
163
+ backgroundColor: '#E7E5E4'
164
+ };
165
+ if (!appJson.expo.ios || typeof appJson.expo.ios !== 'object') {
166
+ appJson.expo.ios = {};
167
+ }
168
+ appJson.expo.ios.icon = './assets/icon.png';
169
+ if (!appJson.expo.web || typeof appJson.expo.web !== 'object') {
170
+ appJson.expo.web = {};
171
+ }
172
+ appJson.expo.web.favicon = './assets/favicon.png';
173
+ if (!Array.isArray(appJson.expo.assetBundlePatterns)) {
174
+ appJson.expo.assetBundlePatterns = ['**/*'];
175
+ }
131
176
  await writeJson(appJsonPath, appJson);
132
177
  const packageJsonPath = path.join(targetDir, 'package.json');
133
178
  const packageJson = await readJson(packageJsonPath, {});
@@ -164,6 +209,17 @@ async function ensureNextTurbo(targetDir) {
164
209
  }
165
210
  await writeJson(packageJsonPath, packageJson);
166
211
  }
212
+ function mergeEnvHelp(...lists) {
213
+ const map = new Map();
214
+ for (const list of lists) {
215
+ for (const item of list) {
216
+ if (!map.has(item.key)) {
217
+ map.set(item.key, item);
218
+ }
219
+ }
220
+ }
221
+ return Array.from(map.values());
222
+ }
167
223
  async function ensurePackageName(targetDir, appName) {
168
224
  const packageJsonPath = path.join(targetDir, 'package.json');
169
225
  const packageJson = await readJson(packageJsonPath, {});