@aex.is/zero 0.1.2 → 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
+ }
@@ -0,0 +1,97 @@
1
+ export const frameworks = [
2
+ {
3
+ id: 'nextjs',
4
+ label: 'Next.js',
5
+ description: 'React framework with App Router and Tailwind.',
6
+ packages: [
7
+ 'class-variance-authority',
8
+ 'clsx',
9
+ 'lucide-react',
10
+ 'tailwind-merge',
11
+ 'tailwindcss-animate',
12
+ '@radix-ui/react-slot'
13
+ ],
14
+ scaffold: {
15
+ command: 'bunx',
16
+ packageName: 'create-next-app@latest',
17
+ argSets: [
18
+ [
19
+ '--ts',
20
+ '--eslint',
21
+ '--tailwind',
22
+ '--turbo',
23
+ '--app',
24
+ '--no-src-dir',
25
+ '--import-alias',
26
+ '@/*',
27
+ '--use-bun',
28
+ '--skip-install'
29
+ ],
30
+ [
31
+ '--ts',
32
+ '--eslint',
33
+ '--tailwind',
34
+ '--turbo',
35
+ '--app',
36
+ '--no-src-dir',
37
+ '--import-alias',
38
+ '@/*',
39
+ '--use-bun'
40
+ ],
41
+ [
42
+ '--ts',
43
+ '--eslint',
44
+ '--tailwind',
45
+ '--turbo',
46
+ '--app',
47
+ '--no-src-dir',
48
+ '--import-alias',
49
+ '@/*'
50
+ ],
51
+ [
52
+ '--ts',
53
+ '--eslint',
54
+ '--tailwind',
55
+ '--app',
56
+ '--no-src-dir',
57
+ '--import-alias',
58
+ '@/*'
59
+ ]
60
+ ]
61
+ }
62
+ },
63
+ {
64
+ id: 'expo',
65
+ label: 'Expo (React Native)',
66
+ description: 'Expo app with Router and EAS configuration.',
67
+ packages: [
68
+ 'expo-router',
69
+ 'expo-font',
70
+ '@expo-google-fonts/geist-mono',
71
+ 'tamagui',
72
+ '@tamagui/config',
73
+ '@tamagui/animations-react-native',
74
+ '@tamagui/metro-plugin',
75
+ '@tamagui/babel-plugin',
76
+ 'react-native-svg'
77
+ ],
78
+ scaffold: {
79
+ command: 'bunx',
80
+ packageName: 'create-expo-app',
81
+ argSets: [
82
+ ['--template', 'expo-router', '--yes', '--no-install'],
83
+ ['--template', 'expo-router', '--yes'],
84
+ ['--yes', '--no-install'],
85
+ ['--yes'],
86
+ []
87
+ ]
88
+ }
89
+ }
90
+ ];
91
+ export function getFrameworkDefinition(id) {
92
+ const framework = frameworks.find((item) => item.id === id);
93
+ if (!framework) {
94
+ throw new Error(`Unknown framework: ${id}`);
95
+ }
96
+ return framework;
97
+ }
@@ -0,0 +1,120 @@
1
+ export const modules = [
2
+ {
3
+ id: 'neon',
4
+ label: 'Database (Neon)',
5
+ description: 'Serverless Postgres with Neon.',
6
+ envVars: [
7
+ {
8
+ key: 'DATABASE_URL',
9
+ description: 'Neon connection string',
10
+ url: 'https://neon.com/docs/get-started/connect-neon'
11
+ }
12
+ ],
13
+ packages: {
14
+ nextjs: ['@neondatabase/serverless'],
15
+ expo: ['@neondatabase/serverless']
16
+ }
17
+ },
18
+ {
19
+ id: 'clerk',
20
+ label: 'Auth (Clerk)',
21
+ description: 'Authentication with Clerk.',
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
+ ],
34
+ packages: {
35
+ nextjs: ['@clerk/nextjs'],
36
+ expo: ['@clerk/clerk-expo']
37
+ }
38
+ },
39
+ {
40
+ id: 'payload',
41
+ label: 'CMS (Payload)',
42
+ description: 'Headless CMS using Payload.',
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
+ ],
55
+ packages: {
56
+ nextjs: ['payload'],
57
+ expo: ['payload']
58
+ }
59
+ },
60
+ {
61
+ id: 'stripe',
62
+ label: 'Payments (Stripe)',
63
+ description: 'Payments via Stripe SDK.',
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
+ ],
76
+ packages: {
77
+ nextjs: ['stripe'],
78
+ expo: ['stripe']
79
+ }
80
+ }
81
+ ];
82
+ export function getModuleDefinition(id) {
83
+ const module = modules.find((item) => item.id === id);
84
+ if (!module) {
85
+ throw new Error(`Unknown module: ${id}`);
86
+ }
87
+ return module;
88
+ }
89
+ export function getModulePackages(moduleIds, framework) {
90
+ const packages = new Set();
91
+ for (const id of moduleIds) {
92
+ const module = getModuleDefinition(id);
93
+ for (const pkg of module.packages[framework]) {
94
+ packages.add(pkg);
95
+ }
96
+ }
97
+ return Array.from(packages).sort();
98
+ }
99
+ export function getModuleEnvVars(moduleIds) {
100
+ const envVars = new Set();
101
+ for (const id of moduleIds) {
102
+ const module = getModuleDefinition(id);
103
+ for (const envVar of module.envVars) {
104
+ envVars.add(envVar.key);
105
+ }
106
+ }
107
+ return Array.from(envVars).sort();
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
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'path';
2
+ import { promises as fs } from 'fs';
3
+ 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}=`);
7
+ const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
8
+ const envPath = path.join(targetDir, '.env.example');
9
+ await fs.writeFile(envPath, content, 'utf8');
10
+ }
@@ -0,0 +1,36 @@
1
+ import { execa } from 'execa';
2
+ import { getModulePackages } from '../config/modules.js';
3
+ const baseEnv = {
4
+ ...process.env,
5
+ CI: '1'
6
+ };
7
+ export async function installBaseDependencies(targetDir, extraPackages = []) {
8
+ if (extraPackages.length > 0) {
9
+ const packages = Array.from(new Set(extraPackages)).sort();
10
+ await execa('bun', ['add', ...packages], {
11
+ cwd: targetDir,
12
+ stdio: 'inherit',
13
+ env: baseEnv,
14
+ shell: false
15
+ });
16
+ return;
17
+ }
18
+ await execa('bun', ['install'], {
19
+ cwd: targetDir,
20
+ stdio: 'inherit',
21
+ env: baseEnv,
22
+ shell: false
23
+ });
24
+ }
25
+ export async function installModulePackages(framework, moduleIds, targetDir) {
26
+ const packages = getModulePackages(moduleIds, framework);
27
+ if (packages.length === 0) {
28
+ return;
29
+ }
30
+ await execa('bun', ['add', ...packages], {
31
+ cwd: targetDir,
32
+ stdio: 'inherit',
33
+ env: baseEnv,
34
+ shell: false
35
+ });
36
+ }
@@ -0,0 +1,253 @@
1
+ import path from 'path';
2
+ import { promises as fs } from 'fs';
3
+ import { execa } from 'execa';
4
+ import { getFrameworkDefinition } from '../config/frameworks.js';
5
+ import { getModuleEnvHelp } from '../config/modules.js';
6
+ import { installBaseDependencies, installModulePackages } from './installers.js';
7
+ import { writeEnvExample } from './env.js';
8
+ import { buildNextTemplateFiles, buildExpoTemplateFiles, componentsJsonTemplate } from './templates.js';
9
+ const baseEnv = {
10
+ ...process.env,
11
+ CI: '1'
12
+ };
13
+ export async function scaffoldProject(config) {
14
+ const directoryInput = config.directory.trim().length === 0 ? '.' : config.directory.trim();
15
+ const targetDir = path.resolve(process.cwd(), directoryInput);
16
+ await ensureEmptyTargetDir(targetDir);
17
+ const framework = getFrameworkDefinition(config.framework);
18
+ console.log(`Scaffolding ${framework.label}...`);
19
+ await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, directoryInput, framework.scaffold.argSets);
20
+ console.log('Applying framework templates...');
21
+ if (config.framework === 'nextjs') {
22
+ await applyNextTemplates(config, targetDir);
23
+ }
24
+ else {
25
+ await applyExpoTemplates(config, targetDir);
26
+ }
27
+ console.log('Installing base dependencies with Bun...');
28
+ await installBaseDependencies(targetDir, framework.packages);
29
+ console.log('Installing module packages...');
30
+ await installModulePackages(config.framework, config.modules, targetDir);
31
+ console.log('Generating .env.example...');
32
+ await writeEnvExample(config.modules, targetDir);
33
+ console.log('Scaffold complete.');
34
+ const cdTarget = directoryInput === '.' ? '.' : directoryInput.includes(' ') ? `"${directoryInput}"` : directoryInput;
35
+ console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
36
+ }
37
+ async function ensureEmptyTargetDir(targetDir) {
38
+ try {
39
+ const stat = await fs.stat(targetDir);
40
+ if (!stat.isDirectory()) {
41
+ throw new Error('Target path exists and is not a directory.');
42
+ }
43
+ const entries = await fs.readdir(targetDir);
44
+ if (entries.length > 0) {
45
+ throw new Error('Target directory is not empty.');
46
+ }
47
+ }
48
+ catch (error) {
49
+ if (isErrnoException(error) && error.code === 'ENOENT') {
50
+ return;
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+ async function runScaffoldCommand(command, packageName, directoryInput, argSets) {
56
+ const errors = [];
57
+ const targetArg = directoryInput === '.' ? '.' : directoryInput;
58
+ for (const args of argSets) {
59
+ try {
60
+ await execa(command, [packageName, targetArg, ...args], {
61
+ stdio: 'inherit',
62
+ env: baseEnv,
63
+ shell: false
64
+ });
65
+ return;
66
+ }
67
+ catch (error) {
68
+ errors.push(formatError(error));
69
+ }
70
+ }
71
+ const message = errors.length > 0
72
+ ? `Scaffold failed after ${errors.length} attempts:\n${errors.join('\n')}`
73
+ : 'Scaffold failed for unknown reasons.';
74
+ throw new Error(message);
75
+ }
76
+ async function applyNextTemplates(config, targetDir) {
77
+ const srcAppDir = path.join(targetDir, 'src', 'app');
78
+ const usesSrcDir = await pathExists(srcAppDir);
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');
92
+ await ensureNextTurbo(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) {
108
+ const appJsonPath = path.join(targetDir, 'app.json');
109
+ const appJson = await readJson(appJsonPath, { expo: {} });
110
+ if (!appJson.expo || typeof appJson.expo !== 'object') {
111
+ appJson.expo = {};
112
+ }
113
+ appJson.expo.name = appName;
114
+ appJson.expo.slug = toSlug(appName);
115
+ if (!Array.isArray(appJson.expo.platforms)) {
116
+ appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
117
+ }
118
+ else {
119
+ const current = appJson.expo.platforms.filter((value) => typeof value === 'string');
120
+ appJson.expo.platforms = mergeUnique(current, ['ios', 'android', 'macos', 'windows']);
121
+ }
122
+ if (!Array.isArray(appJson.expo.plugins)) {
123
+ appJson.expo.plugins = ['expo-router'];
124
+ }
125
+ else {
126
+ const current = appJson.expo.plugins.filter((value) => typeof value === 'string');
127
+ appJson.expo.plugins = mergeUnique(current, ['expo-router']);
128
+ }
129
+ await writeJson(appJsonPath, appJson);
130
+ const packageJsonPath = path.join(targetDir, 'package.json');
131
+ const packageJson = await readJson(packageJsonPath, {});
132
+ packageJson.main = 'expo-router/entry';
133
+ await writeJson(packageJsonPath, packageJson);
134
+ const easPath = path.join(targetDir, 'eas.json');
135
+ const easConfig = {
136
+ cli: {
137
+ version: '>= 8.0.0'
138
+ },
139
+ build: {
140
+ development: {
141
+ developmentClient: true,
142
+ distribution: 'internal'
143
+ },
144
+ preview: {
145
+ distribution: 'internal'
146
+ },
147
+ production: {}
148
+ },
149
+ submit: {}
150
+ };
151
+ await writeJson(easPath, easConfig);
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
+ }
178
+ function mergeUnique(values, additions) {
179
+ const set = new Set(values);
180
+ for (const value of additions) {
181
+ set.add(value);
182
+ }
183
+ return Array.from(set);
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
+ }
200
+ async function readJson(filePath, fallback) {
201
+ try {
202
+ const data = await fs.readFile(filePath, 'utf8');
203
+ return JSON.parse(data);
204
+ }
205
+ catch (error) {
206
+ if (isErrnoException(error) && error.code === 'ENOENT') {
207
+ return fallback;
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+ async function writeJson(filePath, value) {
213
+ const data = JSON.stringify(value, null, 2);
214
+ await fs.writeFile(filePath, `${data}\n`, 'utf8');
215
+ }
216
+ function formatError(error) {
217
+ if (error instanceof Error) {
218
+ return error.message;
219
+ }
220
+ return String(error);
221
+ }
222
+ function isErrnoException(error) {
223
+ return error instanceof Error && 'code' in error;
224
+ }
225
+ async function pathExists(targetPath) {
226
+ try {
227
+ await fs.stat(targetPath);
228
+ return true;
229
+ }
230
+ catch (error) {
231
+ if (isErrnoException(error) && error.code === 'ENOENT') {
232
+ return false;
233
+ }
234
+ throw error;
235
+ }
236
+ }
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';
253
+ }