@aex.is/zero 0.1.1 → 0.1.3

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.
@@ -1,296 +0,0 @@
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
- shadcnUtilsTemplate,
12
- componentsJsonTemplate,
13
- tamaguiConfigTemplate,
14
- metroConfigTemplate,
15
- expoLayoutTemplate,
16
- expoIndexTemplate
17
- } from './templates.js';
18
-
19
- const baseEnv = {
20
- ...process.env,
21
- CI: '1'
22
- };
23
-
24
- export async function scaffoldProject(config: ProjectConfig): Promise<void> {
25
- const targetDir = path.resolve(process.cwd(), config.appName);
26
- await ensureEmptyTargetDir(targetDir);
27
-
28
- const framework = getFrameworkDefinition(config.framework);
29
-
30
- console.log(`Scaffolding ${framework.label}...`);
31
- await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, config.appName, framework.scaffold.argSets);
32
-
33
- console.log('Applying framework templates...');
34
- if (config.framework === 'nextjs') {
35
- await applyNextTemplates(targetDir);
36
- } else {
37
- await applyExpoTemplates(targetDir);
38
- }
39
-
40
- console.log('Installing base dependencies with Bun...');
41
- await installBaseDependencies(targetDir, framework.packages);
42
-
43
- console.log('Installing module packages...');
44
- await installModulePackages(config.framework, config.modules, targetDir);
45
-
46
- console.log('Generating .env.example...');
47
- await writeEnvExample(config.modules, targetDir);
48
-
49
- console.log('Scaffold complete.');
50
- const cdTarget = config.appName.includes(' ') ? `"${config.appName}"` : config.appName;
51
- console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
52
- }
53
-
54
- async function ensureEmptyTargetDir(targetDir: string): Promise<void> {
55
- try {
56
- const stat = await fs.stat(targetDir);
57
- if (!stat.isDirectory()) {
58
- throw new Error('Target path exists and is not a directory.');
59
- }
60
- const entries = await fs.readdir(targetDir);
61
- if (entries.length > 0) {
62
- throw new Error('Target directory is not empty.');
63
- }
64
- } catch (error) {
65
- if (isErrnoException(error) && error.code === 'ENOENT') {
66
- return;
67
- }
68
- throw error;
69
- }
70
- }
71
-
72
- async function runScaffoldCommand(
73
- command: string,
74
- packageName: string,
75
- appName: string,
76
- argSets: string[][]
77
- ): Promise<void> {
78
- const errors: string[] = [];
79
- for (const args of argSets) {
80
- try {
81
- await execa(command, [packageName, appName, ...args], {
82
- stdio: 'inherit',
83
- env: baseEnv,
84
- shell: false
85
- });
86
- return;
87
- } catch (error) {
88
- errors.push(formatError(error));
89
- }
90
- }
91
- const message = errors.length > 0
92
- ? `Scaffold failed after ${errors.length} attempts:\n${errors.join('\n')}`
93
- : 'Scaffold failed for unknown reasons.';
94
- throw new Error(message);
95
- }
96
-
97
- async function applyNextTemplates(targetDir: string): Promise<void> {
98
- const rootAppDir = path.join(targetDir, 'app');
99
- const srcAppDir = path.join(targetDir, 'src', 'app');
100
- const usesSrcDir = await pathExists(srcAppDir);
101
- const appDir = usesSrcDir ? srcAppDir : rootAppDir;
102
- const projectSrcBase = usesSrcDir ? path.join(targetDir, 'src') : targetDir;
103
- await fs.mkdir(appDir, { recursive: true });
104
- await fs.writeFile(path.join(appDir, 'layout.tsx'), nextLayoutTemplate, 'utf8');
105
- await fs.writeFile(path.join(appDir, 'page.tsx'), nextPageTemplate, 'utf8');
106
- await ensureShadcnSetup(targetDir, projectSrcBase, usesSrcDir);
107
- await ensureNextTurbo(targetDir);
108
- }
109
-
110
- async function applyExpoTemplates(targetDir: string): Promise<void> {
111
- const appDir = path.join(targetDir, 'app');
112
- await fs.mkdir(appDir, { recursive: true });
113
- await fs.writeFile(path.join(appDir, '_layout.tsx'), expoLayoutTemplate, 'utf8');
114
- await fs.writeFile(path.join(appDir, 'index.tsx'), expoIndexTemplate, 'utf8');
115
- await ensureExpoConfig(targetDir);
116
- await ensureExpoTamagui(targetDir);
117
- }
118
-
119
- async function ensureExpoConfig(targetDir: string): Promise<void> {
120
- const appJsonPath = path.join(targetDir, 'app.json');
121
- const appJson = await readJson<Record<string, any>>(appJsonPath, { expo: {} });
122
- if (!appJson.expo || typeof appJson.expo !== 'object') {
123
- appJson.expo = {};
124
- }
125
-
126
- if (!Array.isArray(appJson.expo.platforms)) {
127
- appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
128
- } else {
129
- const current = appJson.expo.platforms.filter((value: unknown) => typeof value === 'string');
130
- appJson.expo.platforms = mergeUnique(current, ['ios', 'android', 'macos', 'windows']);
131
- }
132
-
133
- if (!Array.isArray(appJson.expo.plugins)) {
134
- appJson.expo.plugins = ['expo-router'];
135
- } else {
136
- const current = appJson.expo.plugins.filter((value: unknown) => typeof value === 'string');
137
- appJson.expo.plugins = mergeUnique(current, ['expo-router']);
138
- }
139
-
140
- await writeJson(appJsonPath, appJson);
141
-
142
- const packageJsonPath = path.join(targetDir, 'package.json');
143
- const packageJson = await readJson<Record<string, any>>(packageJsonPath, {});
144
- packageJson.main = 'expo-router/entry';
145
- await writeJson(packageJsonPath, packageJson);
146
-
147
- const easPath = path.join(targetDir, 'eas.json');
148
- const easConfig = {
149
- cli: {
150
- version: '>= 8.0.0'
151
- },
152
- build: {
153
- development: {
154
- developmentClient: true,
155
- distribution: 'internal'
156
- },
157
- preview: {
158
- distribution: 'internal'
159
- },
160
- production: {}
161
- },
162
- submit: {}
163
- };
164
- await writeJson(easPath, easConfig);
165
- }
166
-
167
- function mergeUnique(values: string[], additions: string[]): string[] {
168
- const set = new Set(values);
169
- for (const value of additions) {
170
- set.add(value);
171
- }
172
- return Array.from(set);
173
- }
174
-
175
- async function readJson<T>(filePath: string, fallback: T): Promise<T> {
176
- try {
177
- const data = await fs.readFile(filePath, 'utf8');
178
- return JSON.parse(data) as T;
179
- } catch (error) {
180
- if (isErrnoException(error) && error.code === 'ENOENT') {
181
- return fallback;
182
- }
183
- throw error;
184
- }
185
- }
186
-
187
- async function writeJson(filePath: string, value: unknown): Promise<void> {
188
- const data = JSON.stringify(value, null, 2);
189
- await fs.writeFile(filePath, `${data}\n`, 'utf8');
190
- }
191
-
192
- function formatError(error: unknown): string {
193
- if (error instanceof Error) {
194
- return error.message;
195
- }
196
- return String(error);
197
- }
198
-
199
- function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
200
- return error instanceof Error && 'code' in error;
201
- }
202
-
203
- async function pathExists(targetPath: string): Promise<boolean> {
204
- try {
205
- await fs.stat(targetPath);
206
- return true;
207
- } catch (error) {
208
- if (isErrnoException(error) && error.code === 'ENOENT') {
209
- return false;
210
- }
211
- throw error;
212
- }
213
- }
214
-
215
- async function ensureNextTurbo(targetDir: string): Promise<void> {
216
- const packageJsonPath = path.join(targetDir, 'package.json');
217
- const packageJson = await readJson<Record<string, any>>(packageJsonPath, {});
218
- if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
219
- packageJson.scripts = {};
220
- }
221
- const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
222
- if (!currentDev.includes('--turbo')) {
223
- packageJson.scripts.dev = `${currentDev} --turbo`;
224
- }
225
- await writeJson(packageJsonPath, packageJson);
226
- }
227
-
228
- async function ensureShadcnSetup(targetDir: string, projectSrcBase: string, usesSrcDir: boolean): Promise<void> {
229
- const libDir = path.join(projectSrcBase, 'lib');
230
- await fs.mkdir(libDir, { recursive: true });
231
- await fs.writeFile(path.join(libDir, 'utils.ts'), shadcnUtilsTemplate, 'utf8');
232
-
233
- const globalsPath = usesSrcDir ? 'src/app/globals.css' : 'app/globals.css';
234
- const tailwindConfigPath = await detectTailwindConfig(targetDir);
235
- const componentsJson = componentsJsonTemplate(globalsPath, tailwindConfigPath ?? 'tailwind.config.ts');
236
- await fs.writeFile(path.join(targetDir, 'components.json'), componentsJson, 'utf8');
237
- }
238
-
239
- async function detectTailwindConfig(targetDir: string): Promise<string | null> {
240
- const candidates = [
241
- 'tailwind.config.ts',
242
- 'tailwind.config.js',
243
- 'tailwind.config.cjs',
244
- 'tailwind.config.mjs'
245
- ];
246
- for (const filename of candidates) {
247
- const fullPath = path.join(targetDir, filename);
248
- if (await pathExists(fullPath)) {
249
- return filename;
250
- }
251
- }
252
- return null;
253
- }
254
-
255
- async function ensureExpoTamagui(targetDir: string): Promise<void> {
256
- const configPath = path.join(targetDir, 'tamagui.config.ts');
257
- await fs.writeFile(configPath, tamaguiConfigTemplate, 'utf8');
258
-
259
- const metroPath = path.join(targetDir, 'metro.config.js');
260
- await fs.writeFile(metroPath, metroConfigTemplate, 'utf8');
261
-
262
- await ensureBabelTamagui(targetDir);
263
- }
264
-
265
- async function ensureBabelTamagui(targetDir: string): Promise<void> {
266
- const babelPath = path.join(targetDir, 'babel.config.js');
267
- let content = '';
268
- if (await pathExists(babelPath)) {
269
- content = await fs.readFile(babelPath, 'utf8');
270
- }
271
-
272
- if (!content) {
273
- 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`;
274
- await fs.writeFile(babelPath, defaultConfig, 'utf8');
275
- return;
276
- }
277
-
278
- if (content.includes('@tamagui/babel-plugin')) {
279
- return;
280
- }
281
-
282
- const pluginSnippet = `[\n '@tamagui/babel-plugin',\n {\n config: './tamagui.config.ts',\n components: ['tamagui']\n }\n ]`;
283
-
284
- const pluginsRegex = /plugins:\\s*\\[(.*)\\]/s;
285
- if (pluginsRegex.test(content)) {
286
- content = content.replace(pluginsRegex, (match, inner) => {
287
- const trimmed = inner.trim();
288
- const updatedInner = trimmed.length > 0 ? `${trimmed},\n ${pluginSnippet}` : pluginSnippet;
289
- return `plugins: [${updatedInner}]`;
290
- });
291
- } else {
292
- content = content.replace(/return\\s*\\{/, (match) => `${match}\n plugins: [${pluginSnippet}],`);
293
- }
294
-
295
- await fs.writeFile(babelPath, content, 'utf8');
296
- }
package/src/env/detect.ts DELETED
@@ -1,40 +0,0 @@
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 DELETED
@@ -1,48 +0,0 @@
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 DELETED
@@ -1,12 +0,0 @@
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 DELETED
@@ -1,139 +0,0 @@
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
- }
@@ -1,114 +0,0 @@
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
- }