@aex.is/zero 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (c) 2026 Aexis Zero
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. No part of the Software may be copied, modified, distributed,
7
+ published, sublicensed, or sold without prior written permission from the copyright
8
+ holder.
9
+
10
+ The Software is provided "AS IS", without warranty of any kind, express or
11
+ implied, including but not limited to the warranties of merchantability, fitness
12
+ for a particular purpose and noninfringement. In no event shall the authors or
13
+ copyright holders be liable for any claim, damages or other liability, whether in
14
+ an action of contract, tort or otherwise, arising from, out of or in connection
15
+ with the Software or the use or other dealings in the Software.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Aexis Zero
2
+
3
+ A cross-platform interactive CLI for scaffolding modern apps with a terminal UI.
4
+
5
+ ## Install (global)
6
+
7
+ ```sh
8
+ npm install -g @aex.is/zero
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```sh
14
+ zero
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - Bun installed and available on PATH.
20
+
21
+ ## Development
22
+
23
+ ```sh
24
+ bun install
25
+ bun run dev
26
+ ```
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@aex.is/zero",
3
+ "version": "0.1.0",
4
+ "description": "Aexis Zero scaffolding CLI",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "zero": "src/index.ts"
9
+ },
10
+ "main": "dist/index.js",
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "dev": "bun run src/index.ts",
23
+ "start": "node dist/index.js"
24
+ },
25
+ "dependencies": {
26
+ "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
+ "which": "^4.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.11.30",
35
+ "@types/react": "^18.3.3",
36
+ "@types/react-dom": "^18.3.0",
37
+ "typescript": "^5.5.4"
38
+ }
39
+ }
@@ -0,0 +1,95 @@
1
+ import type { FrameworkId } from '../types.js';
2
+
3
+ export interface FrameworkDefinition {
4
+ id: FrameworkId;
5
+ label: string;
6
+ description: string;
7
+ packages: string[];
8
+ scaffold: {
9
+ command: string;
10
+ packageName: string;
11
+ argSets: string[][];
12
+ };
13
+ }
14
+
15
+ export const frameworks: FrameworkDefinition[] = [
16
+ {
17
+ id: 'nextjs',
18
+ label: 'Next.js',
19
+ description: 'React framework with App Router and Tailwind.',
20
+ packages: [],
21
+ scaffold: {
22
+ command: 'bunx',
23
+ packageName: 'create-next-app@latest',
24
+ argSets: [
25
+ [
26
+ '--ts',
27
+ '--eslint',
28
+ '--tailwind',
29
+ '--turbo',
30
+ '--app',
31
+ '--no-src-dir',
32
+ '--import-alias',
33
+ '@/*',
34
+ '--use-bun',
35
+ '--skip-install'
36
+ ],
37
+ [
38
+ '--ts',
39
+ '--eslint',
40
+ '--tailwind',
41
+ '--turbo',
42
+ '--app',
43
+ '--no-src-dir',
44
+ '--import-alias',
45
+ '@/*',
46
+ '--use-bun'
47
+ ],
48
+ [
49
+ '--ts',
50
+ '--eslint',
51
+ '--tailwind',
52
+ '--turbo',
53
+ '--app',
54
+ '--no-src-dir',
55
+ '--import-alias',
56
+ '@/*'
57
+ ],
58
+ [
59
+ '--ts',
60
+ '--eslint',
61
+ '--tailwind',
62
+ '--app',
63
+ '--no-src-dir',
64
+ '--import-alias',
65
+ '@/*'
66
+ ]
67
+ ]
68
+ }
69
+ },
70
+ {
71
+ id: 'expo',
72
+ label: 'Expo (React Native)',
73
+ description: 'Expo app with Router and EAS configuration.',
74
+ packages: ['expo-router'],
75
+ scaffold: {
76
+ command: 'bunx',
77
+ packageName: 'create-expo-app',
78
+ argSets: [
79
+ ['--template', 'expo-router', '--yes', '--no-install'],
80
+ ['--template', 'expo-router', '--yes'],
81
+ ['--yes', '--no-install'],
82
+ ['--yes'],
83
+ []
84
+ ]
85
+ }
86
+ }
87
+ ];
88
+
89
+ export function getFrameworkDefinition(id: FrameworkId): FrameworkDefinition {
90
+ const framework = frameworks.find((item) => item.id === id);
91
+ if (!framework) {
92
+ throw new Error(`Unknown framework: ${id}`);
93
+ }
94
+ return framework;
95
+ }
@@ -0,0 +1,82 @@
1
+ import type { FrameworkId, ModuleId } from '../types.js';
2
+
3
+ export interface ModuleDefinition {
4
+ id: ModuleId;
5
+ label: string;
6
+ description: string;
7
+ envVars: string[];
8
+ packages: Record<FrameworkId, string[]>;
9
+ }
10
+
11
+ export const modules: ModuleDefinition[] = [
12
+ {
13
+ id: 'neon',
14
+ label: 'Database (Neon)',
15
+ description: 'Serverless Postgres with Neon.',
16
+ envVars: ['DATABASE_URL'],
17
+ packages: {
18
+ nextjs: ['@neondatabase/serverless'],
19
+ expo: ['@neondatabase/serverless']
20
+ }
21
+ },
22
+ {
23
+ id: 'clerk',
24
+ label: 'Auth (Clerk)',
25
+ description: 'Authentication with Clerk.',
26
+ envVars: ['CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY'],
27
+ packages: {
28
+ nextjs: ['@clerk/nextjs'],
29
+ expo: ['@clerk/clerk-expo']
30
+ }
31
+ },
32
+ {
33
+ id: 'payload',
34
+ label: 'CMS (Payload)',
35
+ description: 'Headless CMS using Payload.',
36
+ envVars: ['PAYLOAD_SECRET', 'DATABASE_URL'],
37
+ packages: {
38
+ nextjs: ['payload'],
39
+ expo: ['payload']
40
+ }
41
+ },
42
+ {
43
+ id: 'stripe',
44
+ label: 'Payments (Stripe)',
45
+ description: 'Payments via Stripe SDK.',
46
+ envVars: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'],
47
+ packages: {
48
+ nextjs: ['stripe'],
49
+ expo: ['stripe']
50
+ }
51
+ }
52
+ ];
53
+
54
+ export function getModuleDefinition(id: ModuleId): ModuleDefinition {
55
+ const module = modules.find((item) => item.id === id);
56
+ if (!module) {
57
+ throw new Error(`Unknown module: ${id}`);
58
+ }
59
+ return module;
60
+ }
61
+
62
+ export function getModulePackages(moduleIds: ModuleId[], framework: FrameworkId): string[] {
63
+ const packages = new Set<string>();
64
+ for (const id of moduleIds) {
65
+ const module = getModuleDefinition(id);
66
+ for (const pkg of module.packages[framework]) {
67
+ packages.add(pkg);
68
+ }
69
+ }
70
+ return Array.from(packages).sort();
71
+ }
72
+
73
+ export function getModuleEnvVars(moduleIds: ModuleId[]): string[] {
74
+ const envVars = new Set<string>();
75
+ for (const id of moduleIds) {
76
+ const module = getModuleDefinition(id);
77
+ for (const envVar of module.envVars) {
78
+ envVars.add(envVar);
79
+ }
80
+ }
81
+ return Array.from(envVars).sort();
82
+ }
@@ -0,0 +1,12 @@
1
+ import path from 'path';
2
+ import { promises as fs } from 'fs';
3
+ import type { ModuleId } from '../types.js';
4
+ import { getModuleEnvVars } from '../config/modules.js';
5
+
6
+ export async function writeEnvExample(moduleIds: ModuleId[], targetDir: string): Promise<void> {
7
+ const envVars = getModuleEnvVars(moduleIds);
8
+ const lines = envVars.map((key) => `${key}=`);
9
+ const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
10
+ const envPath = path.join(targetDir, '.env.example');
11
+ await fs.writeFile(envPath, content, 'utf8');
12
+ }
@@ -0,0 +1,44 @@
1
+ import { execa } from 'execa';
2
+ import type { FrameworkId, ModuleId } from '../types.js';
3
+ import { getModulePackages } from '../config/modules.js';
4
+
5
+ const baseEnv = {
6
+ ...process.env,
7
+ CI: '1'
8
+ };
9
+
10
+ export async function installBaseDependencies(targetDir: string, extraPackages: string[] = []): Promise<void> {
11
+ if (extraPackages.length > 0) {
12
+ const packages = Array.from(new Set(extraPackages)).sort();
13
+ await execa('bun', ['add', ...packages], {
14
+ cwd: targetDir,
15
+ stdio: 'inherit',
16
+ env: baseEnv,
17
+ shell: false
18
+ });
19
+ return;
20
+ }
21
+ await execa('bun', ['install'], {
22
+ cwd: targetDir,
23
+ stdio: 'inherit',
24
+ env: baseEnv,
25
+ shell: false
26
+ });
27
+ }
28
+
29
+ export async function installModulePackages(
30
+ framework: FrameworkId,
31
+ moduleIds: ModuleId[],
32
+ targetDir: string
33
+ ): Promise<void> {
34
+ const packages = getModulePackages(moduleIds, framework);
35
+ if (packages.length === 0) {
36
+ return;
37
+ }
38
+ await execa('bun', ['add', ...packages], {
39
+ cwd: targetDir,
40
+ stdio: 'inherit',
41
+ env: baseEnv,
42
+ shell: false
43
+ });
44
+ }
@@ -0,0 +1,218 @@
1
+ import path from 'path';
2
+ import { promises as fs } from 'fs';
3
+ import { execa } from 'execa';
4
+ import type { ProjectConfig } from '../types.js';
5
+ import { getFrameworkDefinition } from '../config/frameworks.js';
6
+ import { installBaseDependencies, installModulePackages } from './installers.js';
7
+ import { writeEnvExample } from './env.js';
8
+ import {
9
+ nextLayoutTemplate,
10
+ nextPageTemplate,
11
+ expoLayoutTemplate,
12
+ expoIndexTemplate
13
+ } from './templates.js';
14
+
15
+ const baseEnv = {
16
+ ...process.env,
17
+ CI: '1'
18
+ };
19
+
20
+ export async function scaffoldProject(config: ProjectConfig): Promise<void> {
21
+ const targetDir = path.resolve(process.cwd(), config.appName);
22
+ await ensureEmptyTargetDir(targetDir);
23
+
24
+ const framework = getFrameworkDefinition(config.framework);
25
+
26
+ console.log(`Scaffolding ${framework.label}...`);
27
+ await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, config.appName, framework.scaffold.argSets);
28
+
29
+ console.log('Applying framework templates...');
30
+ if (config.framework === 'nextjs') {
31
+ await applyNextTemplates(targetDir);
32
+ } else {
33
+ await applyExpoTemplates(targetDir);
34
+ }
35
+
36
+ console.log('Installing base dependencies with Bun...');
37
+ await installBaseDependencies(targetDir, framework.packages);
38
+
39
+ console.log('Installing module packages...');
40
+ await installModulePackages(config.framework, config.modules, targetDir);
41
+
42
+ console.log('Generating .env.example...');
43
+ await writeEnvExample(config.modules, targetDir);
44
+
45
+ console.log('Scaffold complete.');
46
+ const cdTarget = config.appName.includes(' ') ? `"${config.appName}"` : config.appName;
47
+ console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
48
+ }
49
+
50
+ async function ensureEmptyTargetDir(targetDir: string): Promise<void> {
51
+ try {
52
+ const stat = await fs.stat(targetDir);
53
+ if (!stat.isDirectory()) {
54
+ throw new Error('Target path exists and is not a directory.');
55
+ }
56
+ const entries = await fs.readdir(targetDir);
57
+ if (entries.length > 0) {
58
+ throw new Error('Target directory is not empty.');
59
+ }
60
+ } catch (error) {
61
+ if (isErrnoException(error) && error.code === 'ENOENT') {
62
+ return;
63
+ }
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ async function runScaffoldCommand(
69
+ command: string,
70
+ packageName: string,
71
+ appName: string,
72
+ argSets: string[][]
73
+ ): Promise<void> {
74
+ const errors: string[] = [];
75
+ for (const args of argSets) {
76
+ try {
77
+ await execa(command, [packageName, appName, ...args], {
78
+ stdio: 'inherit',
79
+ env: baseEnv,
80
+ shell: false
81
+ });
82
+ return;
83
+ } catch (error) {
84
+ errors.push(formatError(error));
85
+ }
86
+ }
87
+ const message = errors.length > 0
88
+ ? `Scaffold failed after ${errors.length} attempts:\n${errors.join('\n')}`
89
+ : 'Scaffold failed for unknown reasons.';
90
+ throw new Error(message);
91
+ }
92
+
93
+ async function applyNextTemplates(targetDir: string): Promise<void> {
94
+ const rootAppDir = path.join(targetDir, 'app');
95
+ const srcAppDir = path.join(targetDir, 'src', 'app');
96
+ const appDir = (await pathExists(srcAppDir)) ? srcAppDir : rootAppDir;
97
+ await fs.mkdir(appDir, { recursive: true });
98
+ await fs.writeFile(path.join(appDir, 'layout.tsx'), nextLayoutTemplate, 'utf8');
99
+ await fs.writeFile(path.join(appDir, 'page.tsx'), nextPageTemplate, 'utf8');
100
+ await ensureNextTurbo(targetDir);
101
+ }
102
+
103
+ async function applyExpoTemplates(targetDir: string): Promise<void> {
104
+ const appDir = path.join(targetDir, 'app');
105
+ await fs.mkdir(appDir, { recursive: true });
106
+ await fs.writeFile(path.join(appDir, '_layout.tsx'), expoLayoutTemplate, 'utf8');
107
+ await fs.writeFile(path.join(appDir, 'index.tsx'), expoIndexTemplate, 'utf8');
108
+ await ensureExpoConfig(targetDir);
109
+ }
110
+
111
+ async function ensureExpoConfig(targetDir: string): Promise<void> {
112
+ const appJsonPath = path.join(targetDir, 'app.json');
113
+ const appJson = await readJson<Record<string, any>>(appJsonPath, { expo: {} });
114
+ if (!appJson.expo || typeof appJson.expo !== 'object') {
115
+ appJson.expo = {};
116
+ }
117
+
118
+ if (!Array.isArray(appJson.expo.platforms)) {
119
+ appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
120
+ } else {
121
+ const current = appJson.expo.platforms.filter((value: unknown) => typeof value === 'string');
122
+ appJson.expo.platforms = mergeUnique(current, ['ios', 'android', 'macos', 'windows']);
123
+ }
124
+
125
+ if (!Array.isArray(appJson.expo.plugins)) {
126
+ appJson.expo.plugins = ['expo-router'];
127
+ } else {
128
+ const current = appJson.expo.plugins.filter((value: unknown) => typeof value === 'string');
129
+ appJson.expo.plugins = mergeUnique(current, ['expo-router']);
130
+ }
131
+
132
+ await writeJson(appJsonPath, appJson);
133
+
134
+ const packageJsonPath = path.join(targetDir, 'package.json');
135
+ const packageJson = await readJson<Record<string, any>>(packageJsonPath, {});
136
+ packageJson.main = 'expo-router/entry';
137
+ await writeJson(packageJsonPath, packageJson);
138
+
139
+ const easPath = path.join(targetDir, 'eas.json');
140
+ const easConfig = {
141
+ cli: {
142
+ version: '>= 8.0.0'
143
+ },
144
+ build: {
145
+ development: {
146
+ developmentClient: true,
147
+ distribution: 'internal'
148
+ },
149
+ preview: {
150
+ distribution: 'internal'
151
+ },
152
+ production: {}
153
+ },
154
+ submit: {}
155
+ };
156
+ await writeJson(easPath, easConfig);
157
+ }
158
+
159
+ function mergeUnique(values: string[], additions: string[]): string[] {
160
+ const set = new Set(values);
161
+ for (const value of additions) {
162
+ set.add(value);
163
+ }
164
+ return Array.from(set);
165
+ }
166
+
167
+ async function readJson<T>(filePath: string, fallback: T): Promise<T> {
168
+ try {
169
+ const data = await fs.readFile(filePath, 'utf8');
170
+ return JSON.parse(data) as T;
171
+ } catch (error) {
172
+ if (isErrnoException(error) && error.code === 'ENOENT') {
173
+ return fallback;
174
+ }
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ async function writeJson(filePath: string, value: unknown): Promise<void> {
180
+ const data = JSON.stringify(value, null, 2);
181
+ await fs.writeFile(filePath, `${data}\n`, 'utf8');
182
+ }
183
+
184
+ function formatError(error: unknown): string {
185
+ if (error instanceof Error) {
186
+ return error.message;
187
+ }
188
+ return String(error);
189
+ }
190
+
191
+ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
192
+ return error instanceof Error && 'code' in error;
193
+ }
194
+
195
+ async function pathExists(targetPath: string): Promise<boolean> {
196
+ try {
197
+ await fs.stat(targetPath);
198
+ return true;
199
+ } catch (error) {
200
+ if (isErrnoException(error) && error.code === 'ENOENT') {
201
+ return false;
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ async function ensureNextTurbo(targetDir: string): Promise<void> {
208
+ const packageJsonPath = path.join(targetDir, 'package.json');
209
+ const packageJson = await readJson<Record<string, any>>(packageJsonPath, {});
210
+ if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
211
+ packageJson.scripts = {};
212
+ }
213
+ const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
214
+ if (!currentDev.includes('--turbo')) {
215
+ packageJson.scripts.dev = `${currentDev} --turbo`;
216
+ }
217
+ await writeJson(packageJsonPath, packageJson);
218
+ }
@@ -0,0 +1,42 @@
1
+ export const nextLayoutTemplate = `import type { ReactNode } from 'react';
2
+
3
+ export const metadata = {
4
+ title: 'Aexis Zero App',
5
+ description: 'Scaffolded by Aexis Zero.'
6
+ };
7
+
8
+ export default function RootLayout({
9
+ children
10
+ }: {
11
+ children: ReactNode;
12
+ }) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }
19
+ `;
20
+
21
+ export const nextPageTemplate = `export default function Home() {
22
+ return <main />;
23
+ }
24
+ `;
25
+
26
+ export const expoLayoutTemplate = `import { Stack } from 'expo-router';
27
+
28
+ export default function RootLayout() {
29
+ return <Stack screenOptions={{ headerShown: false }} />;
30
+ }
31
+ `;
32
+
33
+ export const expoIndexTemplate = `import { Text, View } from 'react-native';
34
+
35
+ export default function Home() {
36
+ return (
37
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
38
+ <Text>Aexis Zero</Text>
39
+ </View>
40
+ );
41
+ }
42
+ `;
@@ -0,0 +1,40 @@
1
+ import which from 'which';
2
+ import type { Platform, ShellFlavor } from '../types.js';
3
+
4
+ export function detectPlatform(): Platform {
5
+ switch (process.platform) {
6
+ case 'darwin':
7
+ return 'macos';
8
+ case 'win32':
9
+ return 'windows';
10
+ case 'linux':
11
+ return 'linux';
12
+ default:
13
+ return 'linux';
14
+ }
15
+ }
16
+
17
+ export function detectShell(platform: Platform = detectPlatform()): ShellFlavor {
18
+ return platform === 'windows' ? 'powershell' : 'posix';
19
+ }
20
+
21
+ export async function isBunAvailable(): Promise<boolean> {
22
+ try {
23
+ await which('bun');
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export async function assertBunAvailable(): Promise<void> {
31
+ const available = await isBunAvailable();
32
+ if (!available) {
33
+ const platform = detectPlatform();
34
+ const shell = detectShell(platform);
35
+ const hint = platform === 'windows'
36
+ ? 'Install Bun for Windows and reopen PowerShell.'
37
+ : 'Install Bun and ensure it is on your PATH.';
38
+ throw new Error(`Bun is required but was not found in PATH. (${platform}/${shell}) ${hint}`);
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bun
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from './ui/App.js';
5
+ import { assertBunAvailable } from './env/detect.js';
6
+ import { scaffoldProject } from './engine/scaffold.js';
7
+ import type { ProjectConfig } from './types.js';
8
+
9
+ async function main(): Promise<void> {
10
+ try {
11
+ await assertBunAvailable();
12
+ } catch (error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ console.error(message);
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+
19
+ let config: ProjectConfig | null = null;
20
+ const { waitUntilExit } = render(
21
+ <App
22
+ onComplete={(result) => {
23
+ config = result;
24
+ }}
25
+ />
26
+ );
27
+
28
+ await waitUntilExit();
29
+
30
+ if (!config) {
31
+ console.log('Cancelled.');
32
+ return;
33
+ }
34
+
35
+ try {
36
+ await scaffoldProject(config);
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ console.error(`Error: ${message}`);
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+
44
+ main().catch((error) => {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ console.error(`Error: ${message}`);
47
+ process.exitCode = 1;
48
+ });
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type Platform = 'macos' | 'linux' | 'windows';
2
+ export type ShellFlavor = 'posix' | 'powershell';
3
+
4
+ export type FrameworkId = 'nextjs' | 'expo';
5
+ export type ModuleId = 'neon' | 'clerk' | 'payload' | 'stripe';
6
+
7
+ export interface ProjectConfig {
8
+ appName: string;
9
+ domain: string;
10
+ framework: FrameworkId;
11
+ modules: ModuleId[];
12
+ }
package/src/ui/App.tsx ADDED
@@ -0,0 +1,139 @@
1
+ import React, { useCallback, useRef, useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import type { FrameworkId, ModuleId, ProjectConfig } from '../types.js';
4
+ import { Intro } from './screens/Intro.js';
5
+ import { NamePrompt } from './screens/NamePrompt.js';
6
+ import { DomainPrompt } from './screens/DomainPrompt.js';
7
+ import { FrameworkSelect } from './screens/FrameworkSelect.js';
8
+ import { ModuleSelect } from './screens/ModuleSelect.js';
9
+ import { Confirm, ConfirmAction } from './screens/Confirm.js';
10
+
11
+ export type Step = 'intro' | 'name' | 'domain' | 'framework' | 'modules' | 'confirm';
12
+
13
+ interface AppProps {
14
+ onComplete: (config: ProjectConfig | null) => void;
15
+ }
16
+
17
+ export function App({ onComplete }: AppProps) {
18
+ const { exit } = useApp();
19
+ const [step, setStep] = useState<Step>('intro');
20
+ const [resumeStep, setResumeStep] = useState<Step | null>(null);
21
+ const [name, setName] = useState('');
22
+ const [domain, setDomain] = useState('');
23
+ const [framework, setFramework] = useState<FrameworkId>('nextjs');
24
+ const [modules, setModules] = useState<ModuleId[]>([]);
25
+ const completedRef = useRef(false);
26
+
27
+ const finish = useCallback(
28
+ (config: ProjectConfig | null) => {
29
+ if (completedRef.current) {
30
+ return;
31
+ }
32
+ completedRef.current = true;
33
+ onComplete(config);
34
+ exit();
35
+ },
36
+ [exit, onComplete]
37
+ );
38
+
39
+ const goNext = useCallback(
40
+ (next: Step) => {
41
+ if (resumeStep) {
42
+ setStep(resumeStep);
43
+ setResumeStep(null);
44
+ return;
45
+ }
46
+ setStep(next);
47
+ },
48
+ [resumeStep]
49
+ );
50
+
51
+ const handleConfirmAction = useCallback(
52
+ (action: ConfirmAction) => {
53
+ switch (action) {
54
+ case 'continue':
55
+ finish({
56
+ appName: name,
57
+ domain,
58
+ framework,
59
+ modules
60
+ });
61
+ return;
62
+ case 'cancel':
63
+ finish(null);
64
+ return;
65
+ case 'edit-name':
66
+ setResumeStep('confirm');
67
+ setStep('name');
68
+ return;
69
+ case 'edit-domain':
70
+ setResumeStep('confirm');
71
+ setStep('domain');
72
+ return;
73
+ case 'edit-framework':
74
+ setResumeStep('confirm');
75
+ setStep('framework');
76
+ return;
77
+ case 'edit-modules':
78
+ setResumeStep('confirm');
79
+ setStep('modules');
80
+ return;
81
+ default:
82
+ return;
83
+ }
84
+ },
85
+ [domain, finish, framework, modules, name]
86
+ );
87
+
88
+ return (
89
+ <Box flexDirection="column">
90
+ {step === 'intro' ? null : (
91
+ <Text color="cyan">Aexis Zero</Text>
92
+ )}
93
+ {step === 'intro' ? (
94
+ <Intro onContinue={() => setStep('name')} />
95
+ ) : null}
96
+ {step === 'name' ? (
97
+ <NamePrompt
98
+ initialValue={name}
99
+ onSubmit={(value) => {
100
+ setName(value);
101
+ goNext('domain');
102
+ }}
103
+ />
104
+ ) : null}
105
+ {step === 'domain' ? (
106
+ <DomainPrompt
107
+ initialValue={domain}
108
+ onSubmit={(value) => {
109
+ setDomain(value);
110
+ goNext('framework');
111
+ }}
112
+ />
113
+ ) : null}
114
+ {step === 'framework' ? (
115
+ <FrameworkSelect
116
+ value={framework}
117
+ onChange={setFramework}
118
+ onConfirm={() => goNext('modules')}
119
+ />
120
+ ) : null}
121
+ {step === 'modules' ? (
122
+ <ModuleSelect
123
+ value={modules}
124
+ onChange={setModules}
125
+ onConfirm={() => goNext('confirm')}
126
+ />
127
+ ) : null}
128
+ {step === 'confirm' ? (
129
+ <Confirm
130
+ name={name}
131
+ domain={domain}
132
+ framework={framework}
133
+ modules={modules}
134
+ onAction={handleConfirmAction}
135
+ />
136
+ ) : null}
137
+ </Box>
138
+ );
139
+ }
@@ -0,0 +1,114 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ export interface SelectItem {
5
+ id: string;
6
+ label: string;
7
+ description?: string;
8
+ }
9
+
10
+ interface SelectListProps {
11
+ items: SelectItem[];
12
+ selectedIds: string[];
13
+ onChange: (next: string[]) => void;
14
+ onSubmit: () => void;
15
+ multi?: boolean;
16
+ hint?: string;
17
+ }
18
+
19
+ export function SelectList({
20
+ items,
21
+ selectedIds,
22
+ onChange,
23
+ onSubmit,
24
+ multi = false,
25
+ hint
26
+ }: SelectListProps) {
27
+ const [index, setIndex] = useState(0);
28
+
29
+ const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
30
+
31
+ const move = useCallback(
32
+ (delta: number) => {
33
+ setIndex((current) => {
34
+ const next = current + delta;
35
+ if (next < 0) {
36
+ return items.length - 1;
37
+ }
38
+ if (next >= items.length) {
39
+ return 0;
40
+ }
41
+ return next;
42
+ });
43
+ },
44
+ [items.length]
45
+ );
46
+
47
+ const toggle = useCallback(
48
+ (itemId: string) => {
49
+ if (multi) {
50
+ const next = new Set(selectedSet);
51
+ if (next.has(itemId)) {
52
+ next.delete(itemId);
53
+ } else {
54
+ next.add(itemId);
55
+ }
56
+ onChange(Array.from(next));
57
+ } else {
58
+ onChange([itemId]);
59
+ }
60
+ },
61
+ [multi, onChange, selectedSet]
62
+ );
63
+
64
+ useInput((input, key) => {
65
+ if (key.upArrow) {
66
+ move(-1);
67
+ return;
68
+ }
69
+ if (key.downArrow) {
70
+ move(1);
71
+ return;
72
+ }
73
+ if (input === ' ') {
74
+ const item = items[index];
75
+ if (item) {
76
+ toggle(item.id);
77
+ }
78
+ return;
79
+ }
80
+ if (key.return) {
81
+ onSubmit();
82
+ }
83
+ });
84
+
85
+ return (
86
+ <Box flexDirection="column">
87
+ <Box flexDirection="column">
88
+ {items.map((item, idx) => {
89
+ const isSelected = selectedSet.has(item.id);
90
+ const isActive = idx === index;
91
+ const prefix = isActive ? '>' : ' ';
92
+ const marker = multi
93
+ ? isSelected
94
+ ? '[x]'
95
+ : '[ ]'
96
+ : isSelected
97
+ ? '(x)'
98
+ : '( )';
99
+ return (
100
+ <Box key={item.id} flexDirection="column">
101
+ <Text color={isActive ? 'cyan' : undefined}>
102
+ {prefix} {marker} {item.label}
103
+ </Text>
104
+ {item.description ? (
105
+ <Text color="gray"> {item.description}</Text>
106
+ ) : null}
107
+ </Box>
108
+ );
109
+ })}
110
+ </Box>
111
+ {hint ? <Text color="gray">{hint}</Text> : null}
112
+ </Box>
113
+ );
114
+ }
@@ -0,0 +1,62 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { SelectList, SelectItem } from '../components/SelectList.js';
4
+ import { frameworks } from '../../config/frameworks.js';
5
+ import { modules } from '../../config/modules.js';
6
+ import type { FrameworkId, ModuleId } from '../../types.js';
7
+
8
+ export type ConfirmAction =
9
+ | 'continue'
10
+ | 'edit-name'
11
+ | 'edit-domain'
12
+ | 'edit-framework'
13
+ | 'edit-modules'
14
+ | 'cancel';
15
+
16
+ interface ConfirmProps {
17
+ name: string;
18
+ domain: string;
19
+ framework: FrameworkId;
20
+ modules: ModuleId[];
21
+ onAction: (action: ConfirmAction) => void;
22
+ }
23
+
24
+ const actionItems: SelectItem[] = [
25
+ { id: 'continue', label: 'Continue' },
26
+ { id: 'edit-name', label: 'Edit name' },
27
+ { id: 'edit-domain', label: 'Edit domain' },
28
+ { id: 'edit-framework', label: 'Edit framework' },
29
+ { id: 'edit-modules', label: 'Edit modules' },
30
+ { id: 'cancel', label: 'Cancel' }
31
+ ];
32
+
33
+ export function Confirm({ name, domain, framework, modules: selectedModules, onAction }: ConfirmProps) {
34
+ const [selection, setSelection] = useState<ConfirmAction>('continue');
35
+
36
+ const frameworkLabel = frameworks.find((item) => item.id === framework)?.label ?? framework;
37
+ const moduleLabels = selectedModules
38
+ .map((id) => modules.find((item) => item.id === id)?.label ?? id)
39
+ .join(', ');
40
+
41
+ return (
42
+ <Box flexDirection="column">
43
+ <Text color="cyan">Confirm</Text>
44
+ <Box flexDirection="column" marginTop={1}>
45
+ <Text>App name: {name}</Text>
46
+ <Text>Domain: {domain || 'None'}</Text>
47
+ <Text>Framework: {frameworkLabel}</Text>
48
+ <Text>Modules: {moduleLabels || 'None'}</Text>
49
+ </Box>
50
+ <Box marginTop={1}>
51
+ <SelectList
52
+ items={actionItems}
53
+ selectedIds={[selection]}
54
+ onChange={(next) => setSelection(next[0] as ConfirmAction)}
55
+ onSubmit={() => onAction(selection)}
56
+ multi={false}
57
+ hint="Select an action and press Enter."
58
+ />
59
+ </Box>
60
+ </Box>
61
+ );
62
+ }
@@ -0,0 +1,29 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+
5
+ interface DomainPromptProps {
6
+ initialValue: string;
7
+ onSubmit: (value: string) => void;
8
+ }
9
+
10
+ export function DomainPrompt({ initialValue, onSubmit }: DomainPromptProps) {
11
+ const [value, setValue] = useState(initialValue);
12
+
13
+ const handleSubmit = () => {
14
+ onSubmit(value.trim());
15
+ };
16
+
17
+ return (
18
+ <Box flexDirection="column">
19
+ <Text color="cyan">Domain (optional)</Text>
20
+ <Text color="gray">Press Enter to continue or leave blank.</Text>
21
+ <TextInput
22
+ value={value}
23
+ onChange={setValue}
24
+ onSubmit={handleSubmit}
25
+ placeholder="example.com"
26
+ />
27
+ </Box>
28
+ );
29
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { SelectList, SelectItem } from '../components/SelectList.js';
4
+ import { frameworks } from '../../config/frameworks.js';
5
+ import type { FrameworkId } from '../../types.js';
6
+
7
+ interface FrameworkSelectProps {
8
+ value: FrameworkId;
9
+ onChange: (value: FrameworkId) => void;
10
+ onConfirm: () => void;
11
+ }
12
+
13
+ export function FrameworkSelect({ value, onChange, onConfirm }: FrameworkSelectProps) {
14
+ const items: SelectItem[] = frameworks.map((framework) => ({
15
+ id: framework.id,
16
+ label: framework.label,
17
+ description: framework.description
18
+ }));
19
+
20
+ return (
21
+ <Box flexDirection="column">
22
+ <Text color="cyan">Framework</Text>
23
+ <Text color="gray">Use arrows to move, space to select, Enter to confirm.</Text>
24
+ <SelectList
25
+ items={items}
26
+ selectedIds={[value]}
27
+ onChange={(next) => onChange(next[0] as FrameworkId)}
28
+ onSubmit={onConfirm}
29
+ multi={false}
30
+ hint="( ) radio select"
31
+ />
32
+ </Box>
33
+ );
34
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ interface IntroProps {
5
+ onContinue: () => void;
6
+ }
7
+
8
+ const introArt = [
9
+ ' _____',
10
+ ' / ___ \\ ',
11
+ ' / / _ \\ \\ ',
12
+ ' | |/ /| |',
13
+ ' \\ \\_/ / /',
14
+ ' \\___/_/'
15
+ ].join('\n');
16
+
17
+ export function Intro({ onContinue }: IntroProps) {
18
+ useInput((input, key) => {
19
+ if (key.return || input === ' ') {
20
+ onContinue();
21
+ }
22
+ });
23
+
24
+ return (
25
+ <Box flexDirection="column">
26
+ <Text>{introArt}</Text>
27
+ <Text color="cyan">Aexis Zero</Text>
28
+ <Text color="gray">Press Enter to begin.</Text>
29
+ </Box>
30
+ );
31
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { SelectList, SelectItem } from '../components/SelectList.js';
4
+ import { modules } from '../../config/modules.js';
5
+ import type { ModuleId } from '../../types.js';
6
+
7
+ interface ModuleSelectProps {
8
+ value: ModuleId[];
9
+ onChange: (value: ModuleId[]) => void;
10
+ onConfirm: () => void;
11
+ }
12
+
13
+ export function ModuleSelect({ value, onChange, onConfirm }: ModuleSelectProps) {
14
+ const items: SelectItem[] = modules.map((module) => ({
15
+ id: module.id,
16
+ label: module.label,
17
+ description: module.description
18
+ }));
19
+
20
+ return (
21
+ <Box flexDirection="column">
22
+ <Text color="cyan">Modules</Text>
23
+ <Text color="gray">Use arrows to move, space to toggle, Enter to confirm.</Text>
24
+ <SelectList
25
+ items={items}
26
+ selectedIds={value}
27
+ onChange={(next) => onChange(next as ModuleId[])}
28
+ onSubmit={onConfirm}
29
+ multi
30
+ hint="[ ] multi-select"
31
+ />
32
+ </Box>
33
+ );
34
+ }
@@ -0,0 +1,37 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+
5
+ interface NamePromptProps {
6
+ initialValue: string;
7
+ onSubmit: (value: string) => void;
8
+ }
9
+
10
+ export function NamePrompt({ initialValue, onSubmit }: NamePromptProps) {
11
+ const [value, setValue] = useState(initialValue);
12
+ const [error, setError] = useState('');
13
+
14
+ const handleSubmit = () => {
15
+ const trimmed = value.trim();
16
+ if (!trimmed) {
17
+ setError('App name is required.');
18
+ return;
19
+ }
20
+ setError('');
21
+ onSubmit(trimmed);
22
+ };
23
+
24
+ return (
25
+ <Box flexDirection="column">
26
+ <Text color="cyan">App name</Text>
27
+ <Text color="gray">Required. Press Enter to continue.</Text>
28
+ <TextInput
29
+ value={value}
30
+ onChange={setValue}
31
+ onSubmit={handleSubmit}
32
+ placeholder="my-app"
33
+ />
34
+ {error ? <Text color="red">{error}</Text> : null}
35
+ </Box>
36
+ );
37
+ }