zero-to-app 1.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/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # zero-to-app CLI
2
+
3
+ CLI tool to install the zero-to-app design system package into your project.
4
+
5
+ ## Installation
6
+
7
+ You can use the CLI directly with npx (no installation needed):
8
+
9
+ ```bash
10
+ npx zero-to-app
11
+ ```
12
+
13
+ Or install it globally:
14
+
15
+ ```bash
16
+ npm install -g zero-to-app
17
+ ```
18
+
19
+ Then run:
20
+
21
+ ```bash
22
+ zero-to-app
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ zero-to-app [options]
29
+ ```
30
+
31
+ ### Options
32
+
33
+ - `-f, --force` - Overwrite existing directory if it exists
34
+ - `--skip-install` - Skip dependency installation
35
+ - `-p, --package-manager <manager>` - Specify package manager (npm, yarn, pnpm). Default: auto-detect
36
+ - `-h, --help` - Display help
37
+ - `-V, --version` - Display version
38
+
39
+ ### Examples
40
+
41
+ ```bash
42
+ # Install to ./zero-to-app directory
43
+ npx zero-to-app
44
+
45
+ # Force overwrite existing directory
46
+ npx zero-to-app --force
47
+
48
+ # Skip dependency installation
49
+ npx zero-to-app --skip-install
50
+
51
+ # Use specific package manager
52
+ npx zero-to-app --package-manager yarn
53
+ ```
54
+
55
+ ## What it does
56
+
57
+ 1. Downloads the `package/` directory from [https://github.com/Alex-Amayo/zero-to-app](https://github.com/Alex-Amayo/zero-to-app) (master branch)
58
+ 2. Copies all files from the downloaded package to `./zero-to-app` in your current directory
59
+ 3. Checks your project's `package.json` for required dependencies
60
+ 4. Installs only missing dependencies in your project root (not in zero-to-app/)
61
+ 5. Uses `npx expo install` for Expo packages and npm/yarn/pnpm for regular packages
62
+
63
+ ### How it gets the package files
64
+
65
+ The CLI always downloads from GitHub:
66
+ - **GitHub source**: Always fetches from the official repository at `Alex-Amayo/zero-to-app` on the `master` branch
67
+ - **Caching**: Downloads are cached locally for faster subsequent runs
68
+ - **Requirements**: Requires git and internet connection
69
+
70
+ ### Required Dependencies
71
+
72
+ The CLI checks for and installs these dependencies if missing:
73
+ - `react-hook-form` - Form validation
74
+ - `@hookform/resolvers` - Form validation resolvers
75
+ - `zod` - Schema validation
76
+ - `react-native-reanimated-carousel` - Carousel component
77
+ - `expo-blur` - Blur effects
78
+ - `expo-glass-effect` - Glass effect
79
+
80
+ **Note:** If all dependencies are already present in your `package.json`, the CLI will skip installation.
81
+
82
+ ## Development
83
+
84
+ ```bash
85
+ # Install dependencies
86
+ npm install
87
+
88
+ # Build (bundles package files)
89
+ npm run build
90
+
91
+ # Run tests
92
+ npm test
93
+
94
+ # Link locally for testing
95
+ npm link
96
+ ```
97
+
98
+
99
+ ## Testing
100
+
101
+ ```bash
102
+ npm test
103
+ ```
104
+
105
+ ## Publishing
106
+
107
+ The CLI automatically downloads package files from GitHub when used. No build step is required before publishing.
108
+
109
+ Simply publish to npm:
110
+ ```bash
111
+ npm publish
112
+ ```
113
+
114
+ The CLI will fetch the latest package files from GitHub when users run it.
115
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../src/index.js');
4
+
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "zero-to-app",
3
+ "version": "1.1.0",
4
+ "description": "CLI tool to install the zero-to-app design system package",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "zero-to-app": "./bin/zero-to-app"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "test:watch": "jest --watch"
12
+ },
13
+ "keywords": [
14
+ "react-native",
15
+ "design-system",
16
+ "cli",
17
+ "zero-to-app"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "chalk": "^4.1.2",
23
+ "commander": "^11.1.0",
24
+ "fs-extra": "^11.2.0"
25
+ },
26
+ "devDependencies": {
27
+ "jest": "^29.7.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=14.0.0"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "src",
35
+ "package.json",
36
+ "README.md"
37
+ ]
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const chalk = require('chalk');
7
+ const { program } = require('commander');
8
+ const readline = require('readline');
9
+
10
+ // Get the directory where the CLI package is installed
11
+ const getCliPackagePath = () => {
12
+ // When installed via npm/npx, __dirname will be in node_modules/zero-to-app/src
13
+ // We need to find the root of this CLI package
14
+ let currentDir = __dirname;
15
+ const rootDir = path.parse(currentDir).root;
16
+
17
+ // Go up from src/ to find the package root
18
+ while (currentDir !== rootDir) {
19
+ const packageJsonPath = path.join(currentDir, 'package.json');
20
+ if (fs.existsSync(packageJsonPath)) {
21
+ try {
22
+ const pkg = require(packageJsonPath);
23
+ if (pkg.name === 'zero-to-app' && pkg.bin && pkg.bin['zero-to-app']) {
24
+ return currentDir;
25
+ }
26
+ } catch (e) {
27
+ // Continue searching if package.json is invalid
28
+ }
29
+ }
30
+ const parentDir = path.dirname(currentDir);
31
+ if (parentDir === currentDir) break;
32
+ currentDir = parentDir;
33
+ }
34
+
35
+ // Fallback: assume we're in development
36
+ return path.resolve(__dirname, '..');
37
+ };
38
+
39
+ // Get the source package directory
40
+ const getSourcePackagePath = async () => {
41
+ // Always download from GitHub
42
+ return await downloadFromGitHub();
43
+ };
44
+
45
+ // Download package files from GitHub
46
+ const downloadFromGitHub = async () => {
47
+ const { execSync } = require('child_process');
48
+ const GITHUB_REPO = 'Alex-Amayo/zero-to-app';
49
+ const GITHUB_BRANCH = 'master'; // Default branch per GitHub repo
50
+ const GITHUB_URL = `https://github.com/${GITHUB_REPO}.git`;
51
+
52
+ const tempDir = path.join(require('os').tmpdir(), 'zero-to-app-cli');
53
+ const repoDir = path.join(tempDir, 'zero-to-app');
54
+ const packageDir = path.join(repoDir, 'package');
55
+
56
+ // Check if already cloned and up to date
57
+ if (fs.existsSync(packageDir)) {
58
+ try {
59
+ // Try to update if it's a git repo
60
+ if (fs.existsSync(path.join(repoDir, '.git'))) {
61
+ console.log(chalk.gray('Updating package files from GitHub...'));
62
+ execSync('git pull', { cwd: repoDir, stdio: 'ignore' });
63
+ console.log(chalk.gray('Using cached package files from GitHub...'));
64
+ return packageDir;
65
+ }
66
+ } catch (e) {
67
+ // If update fails, continue with existing files
68
+ console.log(chalk.gray('Using cached package files from GitHub...'));
69
+ return packageDir;
70
+ }
71
+ }
72
+
73
+ console.log(chalk.blue('Downloading package files from GitHub...'));
74
+ console.log(chalk.gray(`Repository: ${GITHUB_REPO}`));
75
+ console.log(chalk.gray(`Branch: ${GITHUB_BRANCH}`));
76
+
77
+ try {
78
+ await fs.ensureDir(tempDir);
79
+
80
+ // Try git clone first (fastest and most reliable)
81
+ try {
82
+ if (fs.existsSync(repoDir)) {
83
+ await fs.remove(repoDir);
84
+ }
85
+ execSync(`git clone --depth 1 --branch ${GITHUB_BRANCH} ${GITHUB_URL} "${repoDir}"`, {
86
+ stdio: 'inherit',
87
+ });
88
+ console.log(chalk.green('✓ Package files downloaded successfully'));
89
+ return packageDir;
90
+ } catch (gitError) {
91
+ // If git is not available, try downloading zip
92
+ console.log(chalk.yellow('Git not available, trying alternative download method...'));
93
+ throw gitError;
94
+ }
95
+ } catch (error) {
96
+ console.error(chalk.red(`Failed to download from GitHub: ${error.message}`));
97
+ throw new Error(`Could not download package files from GitHub. Please ensure git is installed or check your internet connection. Error: ${error.message}`);
98
+ }
99
+ };
100
+
101
+ // Copy directory recursively, excluding node_modules and other ignored files
102
+ const copyDirectory = async (src, dest, excludes = []) => {
103
+ await fs.ensureDir(dest);
104
+
105
+ const items = await fs.readdir(src);
106
+
107
+ for (const item of items) {
108
+ const srcPath = path.join(src, item);
109
+ const destPath = path.join(dest, item);
110
+
111
+ // Skip excluded items
112
+ if (excludes.includes(item)) {
113
+ continue;
114
+ }
115
+
116
+ const stat = await fs.stat(srcPath);
117
+
118
+ if (stat.isDirectory()) {
119
+ await copyDirectory(srcPath, destPath, excludes);
120
+ } else {
121
+ await fs.copy(srcPath, destPath);
122
+ }
123
+ }
124
+ };
125
+
126
+ // Get required dependencies that are not included with Expo by default
127
+ const getRequiredDependencies = () => {
128
+ return {
129
+ // Regular npm packages
130
+ regular: [
131
+ 'react-hook-form',
132
+ '@hookform/resolvers',
133
+ 'zod',
134
+ 'react-native-reanimated-carousel',
135
+ ],
136
+ // Expo packages (should use expo install)
137
+ expo: [
138
+ 'expo-blur',
139
+ 'expo-glass-effect',
140
+ ],
141
+ };
142
+ };
143
+
144
+ // Check user's package.json for missing dependencies
145
+ const checkDependencies = (packageJsonPath) => {
146
+ if (!fs.existsSync(packageJsonPath)) {
147
+ return { missing: getRequiredDependencies(), allPresent: false };
148
+ }
149
+
150
+ try {
151
+ const packageJson = fs.readJsonSync(packageJsonPath);
152
+ const allDeps = {
153
+ ...(packageJson.dependencies || {}),
154
+ ...(packageJson.devDependencies || {}),
155
+ };
156
+
157
+ const required = getRequiredDependencies();
158
+ const missing = {
159
+ regular: [],
160
+ expo: [],
161
+ };
162
+
163
+ // Check regular dependencies
164
+ for (const dep of required.regular) {
165
+ if (!allDeps[dep]) {
166
+ missing.regular.push(dep);
167
+ }
168
+ }
169
+
170
+ // Check Expo dependencies
171
+ for (const dep of required.expo) {
172
+ if (!allDeps[dep]) {
173
+ missing.expo.push(dep);
174
+ }
175
+ }
176
+
177
+ const allPresent = missing.regular.length === 0 && missing.expo.length === 0;
178
+
179
+ return { missing, allPresent };
180
+ } catch (error) {
181
+ console.warn(chalk.yellow(`Warning: Could not read package.json: ${error.message}`));
182
+ return { missing: getRequiredDependencies(), allPresent: false };
183
+ }
184
+ };
185
+
186
+ // Install missing dependencies
187
+ const installMissingDependencies = async (missing, packageManager = 'npm', projectRoot) => {
188
+ if (missing.regular.length === 0 && missing.expo.length === 0) {
189
+ return;
190
+ }
191
+
192
+ try {
193
+ // Install Expo packages first using expo install
194
+ if (missing.expo.length > 0) {
195
+ console.log(chalk.blue(`Installing Expo packages: ${missing.expo.join(', ')}...`));
196
+ const expoPackages = missing.expo.join(' ');
197
+ execSync(`npx expo install ${expoPackages}`, {
198
+ cwd: projectRoot,
199
+ stdio: 'inherit',
200
+ });
201
+ console.log(chalk.green('✓ Expo packages installed successfully'));
202
+ }
203
+
204
+ // Install regular packages using detected package manager
205
+ if (missing.regular.length > 0) {
206
+ console.log(chalk.blue(`Installing packages: ${missing.regular.join(', ')}...`));
207
+
208
+ let command;
209
+ if (packageManager === 'yarn') {
210
+ command = `yarn add ${missing.regular.join(' ')}`;
211
+ } else if (packageManager === 'pnpm') {
212
+ command = `pnpm add ${missing.regular.join(' ')}`;
213
+ } else {
214
+ command = `npm install ${missing.regular.join(' ')}`;
215
+ }
216
+
217
+ execSync(command, {
218
+ cwd: projectRoot,
219
+ stdio: 'inherit',
220
+ });
221
+
222
+ console.log(chalk.green('✓ Packages installed successfully'));
223
+ }
224
+ } catch (error) {
225
+ console.error(chalk.red('✗ Failed to install dependencies'));
226
+ throw error;
227
+ }
228
+ };
229
+
230
+ // Prompt user for confirmation
231
+ const promptOverwrite = (targetDir) => {
232
+ return new Promise((resolve) => {
233
+ const rl = readline.createInterface({
234
+ input: process.stdin,
235
+ output: process.stdout,
236
+ });
237
+
238
+ rl.question(chalk.yellow(`Directory ${targetDir} already exists. Overwrite? (y/N): `), (answer) => {
239
+ rl.close();
240
+ const shouldOverwrite = answer.toLowerCase().trim() === 'y' || answer.toLowerCase().trim() === 'yes';
241
+ resolve(shouldOverwrite);
242
+ });
243
+ });
244
+ };
245
+
246
+ // Main install function
247
+ const install = async (options) => {
248
+ const targetDir = path.resolve(process.cwd(), 'zero-to-app');
249
+
250
+ console.log(chalk.blue('Installing zero-to-app design system...'));
251
+ console.log(chalk.gray(`Target directory: ${targetDir}`));
252
+
253
+ // Check if target directory already exists
254
+ let shouldOverwrite = options.force;
255
+ if (fs.existsSync(targetDir) && !options.force) {
256
+ shouldOverwrite = await promptOverwrite(targetDir);
257
+ if (!shouldOverwrite) {
258
+ console.log(chalk.gray('Installation cancelled.'));
259
+ process.exit(0);
260
+ }
261
+ }
262
+
263
+ try {
264
+ // Get source package path (may download from GitHub if not found locally)
265
+ const sourcePath = await getSourcePackagePath();
266
+ console.log(chalk.gray(`Source directory: ${sourcePath}`));
267
+
268
+ if (!fs.existsSync(sourcePath)) {
269
+ throw new Error(`Source package directory not found: ${sourcePath}`);
270
+ }
271
+
272
+ // Remove existing directory if force is used or user confirmed
273
+ if (shouldOverwrite && fs.existsSync(targetDir)) {
274
+ console.log(chalk.yellow('Removing existing directory...'));
275
+ await fs.remove(targetDir);
276
+ }
277
+
278
+ // Copy package files
279
+ console.log(chalk.blue('Copying package files...'));
280
+ const excludes = ['node_modules', '.git', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'];
281
+ await copyDirectory(sourcePath, targetDir, excludes);
282
+ console.log(chalk.green('✓ Files copied successfully'));
283
+
284
+ // Check and install dependencies in user's project root
285
+ if (!options.skipInstall) {
286
+ const projectRoot = process.cwd();
287
+ const packageJsonPath = path.join(projectRoot, 'package.json');
288
+
289
+ console.log(chalk.blue('Checking dependencies...'));
290
+ const { missing, allPresent } = checkDependencies(packageJsonPath);
291
+
292
+ if (allPresent) {
293
+ console.log(chalk.green('✓ All required dependencies are already installed'));
294
+ } else {
295
+ const packageManager = options.packageManager || detectPackageManager();
296
+ await installMissingDependencies(missing, packageManager, projectRoot);
297
+ }
298
+ } else {
299
+ console.log(chalk.yellow('Skipping dependency installation (--skip-install)'));
300
+ }
301
+
302
+ console.log(chalk.green('\n✓ zero-to-app installed successfully!'));
303
+ console.log(chalk.blue(`\nNext steps:`));
304
+ console.log(chalk.gray(` Import components: import { Button, Card } from './zero-to-app'`));
305
+
306
+ } catch (error) {
307
+ console.error(chalk.red(`\n✗ Installation failed: ${error.message}`));
308
+ if (error.stack) {
309
+ console.error(chalk.gray(error.stack));
310
+ }
311
+ process.exit(1);
312
+ }
313
+ };
314
+
315
+ // Detect package manager from current directory
316
+ const detectPackageManager = () => {
317
+ if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) {
318
+ return 'yarn';
319
+ }
320
+ if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) {
321
+ return 'pnpm';
322
+ }
323
+ return 'npm';
324
+ };
325
+
326
+ // CLI setup
327
+ program
328
+ .name('zero-to-app')
329
+ .description('Install the zero-to-app design system package')
330
+ .version('1.0.0')
331
+ .option('-f, --force', 'Overwrite existing directory', false)
332
+ .option('--skip-install', 'Skip dependency installation', false)
333
+ .option('-p, --package-manager <manager>', 'Package manager to use (npm, yarn, pnpm)', 'npm')
334
+ .action(install);
335
+
336
+ program.parse();
337
+