next2point0 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.
Files changed (2) hide show
  1. package/bin/index.js +221 -0
  2. package/package.json +48 -0
package/bin/index.js ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+
8
+ const REPO_URL = 'https://github.com/ahmad2point0/next2point0';
9
+ const pkg = require('../package.json');
10
+
11
+ const c = {
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ cyan: '\x1b[36m',
19
+ };
20
+ const supportsColor = process.stdout.isTTY && process.env.TERM !== 'dumb' && !process.env.NO_COLOR;
21
+ const paint = (color, s) => (supportsColor ? `${c[color]}${s}${c.reset}` : s);
22
+
23
+ const TEMPLATES = {
24
+ frontend: { branch: 'main', label: 'Frontend' },
25
+ fullstack: { branch: 'feat/backend', label: 'Frontend + Backend' },
26
+ };
27
+
28
+ function parseArgs(argv) {
29
+ const args = { _: [], flags: {} };
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ if (a === '-h' || a === '--help') args.flags.help = true;
33
+ else if (a === '-v' || a === '--version') args.flags.version = true;
34
+ else if (a === '-t' || a === '--template') args.flags.template = argv[++i];
35
+ else if (a === '--pm' || a === '--package-manager') args.flags.pm = argv[++i];
36
+ else if (a === '--skip-install') args.flags.skipInstall = true;
37
+ else if (a === '--skip-git') args.flags.skipGit = true;
38
+ else if (a === '-y' || a === '--yes') args.flags.yes = true;
39
+ else if (a.startsWith('-')) {
40
+ console.error(paint('red', `Unknown flag: ${a}`));
41
+ process.exit(1);
42
+ } else args._.push(a);
43
+ }
44
+ return args;
45
+ }
46
+
47
+ function printHelp() {
48
+ console.log(`
49
+ ${paint('bold', 'next2point0')} ${paint('dim', `v${pkg.version}`)}
50
+ ${paint('dim', pkg.description)}
51
+
52
+ ${paint('bold', 'Usage:')}
53
+ npx next2point0 <project-name> [options]
54
+
55
+ ${paint('bold', 'Options:')}
56
+ -t, --template <name> Template: frontend | fullstack
57
+ --pm <name> Package manager: bun | pnpm | npm | yarn
58
+ --skip-install Don't install dependencies
59
+ --skip-git Don't init a new git repository
60
+ -y, --yes Accept defaults (template: frontend)
61
+ -h, --help Show this help
62
+ -v, --version Show version
63
+
64
+ ${paint('bold', 'Examples:')}
65
+ npx next2point0 my-app
66
+ npx next2point0 my-app -t fullstack --pm pnpm
67
+ npx next2point0 my-app -y
68
+ `);
69
+ }
70
+
71
+ function ask(query) {
72
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
73
+ return new Promise((resolve) => rl.question(query, (a) => { rl.close(); resolve(a); }));
74
+ }
75
+
76
+ function validateProjectName(name) {
77
+ if (!name || !name.trim()) return 'Project name cannot be empty.';
78
+ if (name.length > 214) return 'Project name is too long (max 214).';
79
+ if (/[A-Z]/.test(name)) return 'Project name must be lowercase.';
80
+ if (/^[._]/.test(name)) return 'Project name cannot start with "." or "_".';
81
+ if (/[\s~)('!*]/.test(name)) return 'Project name contains invalid characters.';
82
+ if (!/^[a-z0-9][a-z0-9\-_.]*$/.test(name)) return 'Project name has invalid characters.';
83
+ return null;
84
+ }
85
+
86
+ function detectPackageManager(preferred) {
87
+ const order = preferred ? [preferred, 'bun', 'pnpm', 'npm', 'yarn'] : ['bun', 'pnpm', 'npm', 'yarn'];
88
+ for (const pm of order) {
89
+ const r = spawnSync(pm, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
90
+ if (r.status === 0) return pm;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function checkGitAvailable() {
96
+ const r = spawnSync('git', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
97
+ return r.status === 0;
98
+ }
99
+
100
+ async function main() {
101
+ const args = parseArgs(process.argv.slice(2));
102
+
103
+ if (args.flags.help) return printHelp();
104
+ if (args.flags.version) return console.log(pkg.version);
105
+
106
+ console.log(paint('cyan', paint('bold', '\n ▲ next2point0')) + paint('dim', ` v${pkg.version}\n`));
107
+
108
+ if (!checkGitAvailable()) {
109
+ console.error(paint('red', '✖ git is required but was not found in PATH.'));
110
+ process.exit(1);
111
+ }
112
+
113
+ let projectName = args._[0];
114
+ if (!projectName) {
115
+ projectName = (await ask(paint('bold', '? ') + 'Project name: ')).trim();
116
+ }
117
+ const nameError = validateProjectName(projectName);
118
+ if (nameError) {
119
+ console.error(paint('red', `✖ ${nameError}`));
120
+ process.exit(1);
121
+ }
122
+
123
+ const targetDir = path.resolve(process.cwd(), projectName);
124
+ if (fs.existsSync(targetDir)) {
125
+ const entries = fs.readdirSync(targetDir);
126
+ if (entries.length > 0) {
127
+ console.error(paint('red', `✖ Directory "${projectName}" already exists and is not empty.`));
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ let templateKey = args.flags.template;
133
+ if (!templateKey && args.flags.yes) templateKey = 'frontend';
134
+ if (!templateKey) {
135
+ console.log('\n' + paint('bold', 'Select a template:'));
136
+ console.log(` ${paint('cyan', '1')}) Frontend ${paint('dim', '(Next.js)')}`);
137
+ console.log(` ${paint('cyan', '2')}) Frontend + Backend ${paint('dim', '(Next.js + API)')}`);
138
+ const choice = (await ask('\n' + paint('bold', '? ') + 'Choice (1 or 2) [1]: ')).trim() || '1';
139
+ templateKey = choice === '2' ? 'fullstack' : 'frontend';
140
+ }
141
+ if (!TEMPLATES[templateKey]) {
142
+ console.error(paint('red', `✖ Unknown template "${templateKey}". Use: frontend | fullstack`));
143
+ process.exit(1);
144
+ }
145
+ const template = TEMPLATES[templateKey];
146
+
147
+ let pm = args.flags.pm;
148
+ if (!pm) {
149
+ pm = detectPackageManager() || 'npm';
150
+ } else {
151
+ const available = detectPackageManager(pm);
152
+ if (available !== pm) {
153
+ console.error(paint('yellow', `⚠ "${pm}" not found, falling back to ${available || 'npm'}.`));
154
+ pm = available || 'npm';
155
+ }
156
+ }
157
+
158
+ console.log('\n' + paint('cyan', '◐') + ` Downloading ${paint('bold', template.label)} template…`);
159
+ try {
160
+ execSync(
161
+ `git clone --depth=1 --branch ${template.branch} --single-branch ${REPO_URL} "${targetDir}"`,
162
+ { stdio: 'ignore' }
163
+ );
164
+ } catch (e) {
165
+ console.error(paint('red', '✖ Failed to clone template. Check your internet connection.'));
166
+ process.exit(1);
167
+ }
168
+ console.log(paint('green', '✔') + ' Template downloaded.');
169
+
170
+ process.chdir(targetDir);
171
+ fs.rmSync(path.join(targetDir, '.git'), { recursive: true, force: true });
172
+
173
+ const projectPkgPath = path.join(targetDir, 'package.json');
174
+ if (fs.existsSync(projectPkgPath)) {
175
+ try {
176
+ const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
177
+ projectPkg.name = projectName;
178
+ projectPkg.version = '0.1.0';
179
+ fs.writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
180
+ } catch {
181
+ console.warn(paint('yellow', '⚠ Could not update template package.json name.'));
182
+ }
183
+ }
184
+
185
+ if (!args.flags.skipInstall) {
186
+ console.log(paint('cyan', '◐') + ` Installing dependencies with ${paint('bold', pm)}…`);
187
+ const install = spawnSync(pm, ['install'], { stdio: 'inherit', shell: process.platform === 'win32' });
188
+ if (install.status !== 0) {
189
+ console.error(paint('yellow', `⚠ ${pm} install failed. You can re-run it manually.`));
190
+ } else {
191
+ console.log(paint('green', '✔') + ' Dependencies installed.');
192
+ }
193
+ }
194
+
195
+ if (!args.flags.skipGit) {
196
+ try {
197
+ execSync('git init', { stdio: 'ignore' });
198
+ execSync('git add -A', { stdio: 'ignore' });
199
+ execSync('git commit -m "chore: initial commit from next2point0"', { stdio: 'ignore' });
200
+ console.log(paint('green', '✔') + ' Initialized fresh git repository.');
201
+ } catch {
202
+ // git user.name/email may not be configured — non-fatal
203
+ }
204
+ }
205
+
206
+ const runCmd = `${pm} run dev`;
207
+ console.log(`
208
+ ${paint('green', paint('bold', '✔ All set!'))}
209
+
210
+ ${paint('dim', 'Next steps:')}
211
+ ${paint('cyan', `cd ${projectName}`)}${args.flags.skipInstall ? `\n ${paint('cyan', `${pm} install`)}` : ''}
212
+ ${paint('cyan', runCmd)}
213
+
214
+ ${paint('dim', 'Docs:')} ${REPO_URL}
215
+ `);
216
+ }
217
+
218
+ main().catch((err) => {
219
+ console.error(paint('red', '\n✖ Unexpected error:'), err && err.message ? err.message : err);
220
+ process.exit(1);
221
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "next2point0",
3
+ "version": "1.1.0",
4
+ "description": "Modern Next.js starter template featuring a scalable, feature-based architecture for frontend and backend development.",
5
+ "homepage": "https://github.com/ahmad2point0/next2point0#readme",
6
+ "keywords": [
7
+ "nextjs",
8
+ "next",
9
+ "react",
10
+ "template",
11
+ "boilerplate",
12
+ "starter",
13
+ "tailwindcss",
14
+ "fullstack",
15
+ "scaffold",
16
+ "cli",
17
+ "npx",
18
+ "create-next-app"
19
+ ],
20
+ "bugs": {
21
+ "url": "https://github.com/ahmad2point0/next2point0/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ahmad2point0/next2point0.git"
26
+ },
27
+ "license": "MIT",
28
+ "author": "ahmad2point0",
29
+ "type": "commonjs",
30
+ "main": "bin/index.js",
31
+ "bin": {
32
+ "next2point0": "./bin/index.js",
33
+ "create-next2point0": "./bin/index.js"
34
+ },
35
+ "files": [
36
+ "bin",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=16"
42
+ },
43
+ "preferGlobal": true,
44
+ "scripts": {
45
+ "start": "node bin/index.js",
46
+ "test": "echo \"No tests yet\" && exit 0"
47
+ }
48
+ }