create-foliokit 0.0.0-stage โ†’ 1.0.1

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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/bin/index.js +6 -0
  4. package/package.json +50 -4
  5. package/src/cli.js +111 -0
  6. package/src/generator.js +133 -0
  7. package/src/prompts.js +44 -0
  8. package/src/utils/errors.js +7 -0
  9. package/src/utils/paths.js +12 -0
  10. package/src/utils/progress.js +5 -0
  11. package/src/utils/project-name.js +30 -0
  12. package/templates/default/data.ts +169 -0
  13. package/templates/default/index.html +23 -0
  14. package/templates/default/package-lock.json +1280 -0
  15. package/templates/default/package.json +28 -0
  16. package/templates/default/public/avatar-placeholder.png +0 -0
  17. package/templates/default/public/experience-logo-placeholder.png +0 -0
  18. package/templates/default/public/project-placeholder.png +0 -0
  19. package/templates/default/public/robots.txt +2 -0
  20. package/templates/default/scripts/generate-seo.mjs +34 -0
  21. package/templates/default/src/App.tsx +582 -0
  22. package/templates/default/src/BrandIcon.tsx +36 -0
  23. package/templates/default/src/ProjectMagazineGallery.css +90 -0
  24. package/templates/default/src/ProjectMagazineGallery.tsx +218 -0
  25. package/templates/default/src/main.tsx +28 -0
  26. package/templates/default/src/styles.css +501 -0
  27. package/templates/default/src/utils.ts +2 -0
  28. package/templates/default/src/vite-env.d.ts +1 -0
  29. package/templates/default/tests/portfolio.test.js +96 -0
  30. package/templates/default/tsconfig.app.json +21 -0
  31. package/templates/default/tsconfig.json +7 -0
  32. package/templates/default/tsconfig.node.json +16 -0
  33. package/templates/default/types.ts +73 -0
  34. package/templates/default/vite.config.ts +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saurabh Pandey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # ๐Ÿš€ FolioKit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/create-foliokit.svg)](https://www.npmjs.com/package/create-foliokit)
4
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.12.0-brightgreen.svg)](package.json)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ FolioKit is a CLI for creating a customizable developer portfolio. It scaffolds a responsive **React 19**, **TypeScript**, and **Vite** website with a dark grid background, framed sections, social links, and an interactive project gallery.
8
+
9
+ Created and maintained by [Saurabh Pandey](https://github.com/saurabhhdev) ยท [GitHub repository](https://github.com/saurabhhdev/foliokit)
10
+
11
+ ---
12
+
13
+ ## โœจ Make It Yours
14
+
15
+ Most portfolio content is edited in one file: `data.ts` in the generated project.
16
+
17
+ | What | Where |
18
+ | --- | --- |
19
+ | Name, headline, bio, photo, location, time zone, resume | `data.ts` โ†’ `profile` |
20
+ | Social profiles and links | `data.ts` โ†’ `socialIcons` |
21
+ | Work history and organization logos | `data.ts` โ†’ `experience` |
22
+ | Schools, programs, dates, locations | `data.ts` โ†’ `education` |
23
+ | Skill names, icons, and categories | `data.ts` โ†’ `skills` |
24
+ | Project details, screenshots, technologies, URLs | `data.ts` โ†’ `projects` |
25
+ | Recommendations | `data.ts` โ†’ `testimonials` |
26
+ | Contact email | `data.ts` โ†’ `contact` |
27
+ | Browser and social preview metadata | `data.ts` โ†’ `seo` |
28
+
29
+ Copy images into `public/` and use paths such as `'/my-photo.jpg'` in `data.ts`. The template includes avatar, project, and experience logo placeholders. Leave an optional section array empty (`[]`) to hide that section.
30
+
31
+ Interface labels are beside the components in `src/App.tsx` and `src/ProjectMagazineGallery.tsx`. Colors, grid, spacing, and responsive styles are in `src/styles.css` and `src/ProjectMagazineGallery.css`.
32
+
33
+ ---
34
+
35
+ ## ๐Ÿ›  Run Locally
36
+
37
+ You need Node.js **22.12.0 or newer** to run the CLI. No global installation is required.
38
+
39
+ ```bash
40
+ npx create-foliokit@latest
41
+ ```
42
+
43
+ Choose a project folder name and whether to install dependencies. Then:
44
+
45
+ ```bash
46
+ cd my-portfolio
47
+ npm run dev
48
+ ```
49
+
50
+ Open the local address printed by Vite, usually `http://localhost:5173`. If you skipped dependency installation, run `npm install` in the project folder first.
51
+
52
+ To install the CLI globally instead:
53
+
54
+ ```bash
55
+ npm install --global create-foliokit
56
+ foliokit
57
+ ```
58
+
59
+ ### Available commands
60
+
61
+ | Command | Action |
62
+ | --- | --- |
63
+ | `foliokit` | Start the setup wizard. |
64
+ | `foliokit create` | Start the wizard with the explicit `create` command. |
65
+ | `foliokit --help` | Show CLI help. |
66
+ | `foliokit --version` | Print the installed version. |
67
+
68
+ ---
69
+
70
+ ## ๐Ÿ“ฆ Build and Deploy
71
+
72
+ Run the generated portfolio's checks and create a production build:
73
+
74
+ ```bash
75
+ npm test
76
+ npm run build
77
+ npm run preview
78
+ ```
79
+
80
+ Vite writes the static site to `dist/`. Deploy it to a static host such as **Vercel** or **Netlify** with build command `npm run build` and output directory `dist`.
81
+
82
+ For canonical URLs and sitemap generation, set `SITE_URL` in the hosting environment to your HTTPS origin, for example `https://portfolio.example` (no path or query string). You can also set `seo.siteUrl` in `data.ts`.
83
+
84
+ ---
85
+
86
+ ## ๐Ÿงฐ Stack and Project Files
87
+
88
+ The generated site uses React, TypeScript, Vite, Lucide icons, and CSS. The CLI package uses Commander, Inquirer, Chalk, and Ora.
89
+
90
+ ```text
91
+ my-portfolio/
92
+ โ”œโ”€โ”€ data.ts # Edit your portfolio content
93
+ โ”œโ”€โ”€ types.ts # Shared TypeScript types
94
+ โ”œโ”€โ”€ public/ # Images and robots.txt
95
+ โ”œโ”€โ”€ scripts/generate-seo.mjs
96
+ โ”œโ”€โ”€ tests/portfolio.test.js
97
+ โ””โ”€โ”€ src/
98
+ โ”œโ”€โ”€ App.tsx
99
+ โ”œโ”€โ”€ BrandIcon.tsx
100
+ โ”œโ”€โ”€ ProjectMagazineGallery.tsx
101
+ โ”œโ”€โ”€ ProjectMagazineGallery.css
102
+ โ”œโ”€โ”€ styles.css
103
+ โ””โ”€โ”€ main.tsx
104
+ ```
105
+
106
+ ---
107
+
108
+ ## ๐Ÿ™Œ Contributing and License
109
+
110
+ For CLI development, run `npm install` and `npm test` at the repository root. To check the bundled website template, run `npm test` and `npm run build` inside `templates/default/`.
111
+
112
+ FolioKit is open source under the [MIT License](LICENSE).
package/bin/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from '../src/cli.js'
4
+ import { reportCliError } from '../src/utils/errors.js'
5
+
6
+ runCli().catch(reportCliError)
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "create-foliokit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Generate customizable React and TypeScript developer portfolios",
5
+ "type": "module",
6
+ "main": "./src/cli.js",
7
+ "exports": {
8
+ ".": "./src/cli.js",
9
+ "./generator": "./src/generator.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "author": "Saurabh Pandey",
13
+ "scripts": {
14
+ "test": "node --test tests"
15
+ },
16
+ "bin": {
17
+ "foliokit": "./bin/index.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "src/",
22
+ "templates/",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=22.12.0"
28
+ },
29
+ "keywords": [
30
+ "portfolio",
31
+ "developer-portfolio",
32
+ "react",
33
+ "typescript",
34
+ "cli",
35
+ "generator"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/saurabhhdev/foliokit.git"
40
+ },
41
+ "homepage": "https://github.com/saurabhhdev/foliokit#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/saurabhhdev/foliokit/issues"
44
+ },
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "chalk": "^6.0.1",
48
+ "commander": "^15.0.0",
49
+ "inquirer": "^14.2.5",
50
+ "ora": "^9.4.1"
51
+ }
52
+ }
package/src/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { spawn } from 'node:child_process'
3
+ import { Command } from 'commander'
4
+ import chalk from 'chalk'
5
+ import inquirer from 'inquirer'
6
+ import { generatePortfolio } from './generator.js'
7
+ import { promptForProjectOptions } from './prompts.js'
8
+ import { getPackagePath } from './utils/paths.js'
9
+ import { createProgress } from './utils/progress.js'
10
+
11
+ const packageMetadata = JSON.parse(readFileSync(getPackagePath('package.json'), 'utf8'))
12
+
13
+ function showWelcome() {
14
+ console.log(chalk.bold.cyan('FOLIOKIT'))
15
+ console.log(chalk.dim('Developer Portfolio Generator'))
16
+ console.log()
17
+ }
18
+
19
+ async function startSetup() {
20
+ showWelcome()
21
+ const options = await promptForProjectOptions()
22
+
23
+ if (!options) {
24
+ console.log(chalk.yellow('Setup cancelled.'))
25
+ return
26
+ }
27
+
28
+ let projectPath
29
+ try {
30
+ projectPath = await generatePortfolio(options)
31
+ } catch (error) {
32
+ if (!(error instanceof Error) || error.code !== 'EEXIST') throw error
33
+ let overwrite
34
+ try {
35
+ ({ overwrite } = await inquirer.prompt([{
36
+ type: 'confirm',
37
+ name: 'overwrite',
38
+ message: `The folder ${options.projectName} already exists. Replace it?`,
39
+ default: false,
40
+ }]))
41
+ } catch (promptError) {
42
+ if (promptError instanceof Error && promptError.name === 'ExitPromptError') {
43
+ console.log(chalk.yellow('Setup cancelled. Existing project was left unchanged.'))
44
+ return
45
+ }
46
+ throw promptError
47
+ }
48
+ if (!overwrite) {
49
+ console.log(chalk.yellow('Existing project was left unchanged.'))
50
+ return
51
+ }
52
+ projectPath = await generatePortfolio({ ...options, overwrite: true })
53
+ }
54
+
55
+ let dependenciesInstalled = false
56
+ if (options.installDependencies) {
57
+ const windows = process.platform === 'win32'
58
+ const install = spawn(windows ? (process.env.ComSpec || 'cmd.exe') : 'npm', windows ? ['/d', '/s', '/c', 'npm.cmd install'] : ['install'], {
59
+ cwd: projectPath,
60
+ stdio: 'inherit',
61
+ shell: false,
62
+ })
63
+ const installation = createProgress('Installing dependencies...').start()
64
+ const result = await new Promise((resolve) => {
65
+ install.once('error', (error) => resolve({ code: 1, error }))
66
+ install.once('close', (code) => resolve({ code: code ?? 1 }))
67
+ })
68
+ dependenciesInstalled = result.code === 0
69
+ if (dependenciesInstalled) installation.succeed('Dependencies installed')
70
+ else installation.fail('Dependency installation failed')
71
+ if (!dependenciesInstalled) {
72
+ console.error(chalk.red('Dependency installation failed. Your generated project is still available.'))
73
+ if (result.error) console.error(chalk.dim(result.error.message))
74
+ console.error(`Run ${chalk.bold('npm install')} inside ${options.projectName} to try again.`)
75
+ }
76
+ }
77
+
78
+ const configuration = { ...options, projectPath }
79
+ console.log()
80
+ console.log(chalk.green('โœ” Portfolio generated successfully!'))
81
+ console.log()
82
+ console.log(chalk.bold('Next steps:'))
83
+ console.log(` cd ${configuration.projectName}`)
84
+ if (!dependenciesInstalled) console.log(' npm install')
85
+ console.log(' npm run dev')
86
+ console.log()
87
+ console.log(`Customize your portfolio by editing ${chalk.bold('data.ts')}.`)
88
+ if (options.installDependencies && !dependenciesInstalled) process.exitCode = 1
89
+ }
90
+
91
+ export function createCli() {
92
+ const program = new Command()
93
+
94
+ program
95
+ .name('foliokit')
96
+ .description('Generate customizable developer portfolio websites')
97
+ .version(packageMetadata.version, '-v, --version')
98
+ .showHelpAfterError()
99
+ .action(startSetup)
100
+
101
+ program
102
+ .command('create')
103
+ .description('Create a portfolio project')
104
+ .action(startSetup)
105
+
106
+ return program
107
+ }
108
+
109
+ export async function runCli(argv = process.argv) {
110
+ await createCli().parseAsync(argv)
111
+ }
@@ -0,0 +1,133 @@
1
+ import { copyFile, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
2
+ import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
3
+ import { getDefaultTemplatePath } from './utils/paths.js'
4
+ import { validateProjectName } from './utils/project-name.js'
5
+ import { createProgress } from './utils/progress.js'
6
+
7
+ const excludedDirectories = new Set([
8
+ '.angular', '.cache', '.git', '.netlify', '.next', '.parcel-cache', '.svelte-kit',
9
+ '.turbo', '.vercel', '.vite', 'build', 'coverage', 'dist', 'node_modules', 'tmp', 'temp',
10
+ ])
11
+ const excludedExtensions = new Set([
12
+ '.bak', '.log', '.orig', '.pem', '.p12', '.pfx', '.key', '.secret', '.token',
13
+ '.swp', '.swo', '.tsbuildinfo',
14
+ ])
15
+ const excludedFiles = new Set([
16
+ '.ds_store', '.envrc', '.npmignore', '.npmrc', '.netrc', '.pypirc', 'credentials', 'credentials.json',
17
+ 'id_ed25519', 'id_rsa', 'secrets.json', 'thumbs.db',
18
+ ])
19
+
20
+ function isWithin(parent, child) {
21
+ const path = relative(parent, child)
22
+ return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path))
23
+ }
24
+
25
+ function shouldExclude(name, isDirectory) {
26
+ const lower = name.toLowerCase()
27
+ if (isDirectory && excludedDirectories.has(lower)) return true
28
+ if (excludedFiles.has(lower) || excludedExtensions.has(extname(lower))) return true
29
+ if (lower === '.env' || (lower.startsWith('.env.') && lower !== '.env.example')) return true
30
+ if (/\.(?:tmp|temp|swp|swo|orig|bak|log)$/.test(lower)) return true
31
+ return false
32
+ }
33
+
34
+ export function resolveProjectDestination(projectName, workingDirectory = process.cwd()) {
35
+ const validation = validateProjectName(projectName)
36
+ if (validation !== true) throw new Error(`Invalid project name: ${validation}`)
37
+ const base = resolve(workingDirectory)
38
+ const destination = resolve(base, projectName)
39
+ if (!isWithin(base, destination) || destination === base) {
40
+ throw new Error('The project destination must be a new child of the working directory.')
41
+ }
42
+ return destination
43
+ }
44
+
45
+ async function copyTree(source, destination) {
46
+ await mkdir(destination, { recursive: true })
47
+ for (const entry of await readdir(source, { withFileTypes: true })) {
48
+ if (shouldExclude(entry.name, entry.isDirectory())) continue
49
+ const from = join(source, entry.name)
50
+ const to = join(destination, entry.name)
51
+ if (entry.isSymbolicLink()) throw new Error(`Template contains an unsupported symbolic link: ${from}`)
52
+ if (entry.isDirectory()) {
53
+ await copyTree(from, to)
54
+ } else if (entry.isFile()) {
55
+ await copyFile(from, to)
56
+ }
57
+ }
58
+ }
59
+
60
+ async function updatePackageNames(destination, projectName) {
61
+ const packagePath = join(destination, 'package.json')
62
+ const packageData = JSON.parse(await readFile(packagePath, 'utf8'))
63
+ packageData.name = projectName
64
+ await writeFile(packagePath, `${JSON.stringify(packageData, null, 2)}\n`)
65
+
66
+ const lockPath = join(destination, 'package-lock.json')
67
+ try {
68
+ const lock = JSON.parse(await readFile(lockPath, 'utf8'))
69
+ lock.name = projectName
70
+ if (lock.packages?.['']) lock.packages[''].name = projectName
71
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
72
+ } catch (error) {
73
+ if (error.code !== 'ENOENT') throw error
74
+ }
75
+ }
76
+
77
+ export function getDefaultTemplateDirectory() {
78
+ return getDefaultTemplatePath()
79
+ }
80
+
81
+ export async function generatePortfolio(options, context = {}) {
82
+ const projectName = options?.projectName
83
+ const workingDirectory = resolve(context.workingDirectory ?? process.cwd())
84
+ const templatePath = resolve(context.templateDirectory ?? getDefaultTemplateDirectory())
85
+
86
+ const [workingStats, templateStats] = await Promise.all([lstat(workingDirectory), lstat(templatePath)])
87
+ if (!workingStats.isDirectory()) throw new Error(`Working directory is not a directory: ${workingDirectory}`)
88
+ if (!templateStats.isDirectory()) throw new Error(`Portfolio template directory was not found: ${templatePath}`)
89
+ const [realDestinationParent, realTemplate] = await Promise.all([realpath(workingDirectory), realpath(templatePath)])
90
+ const destination = resolveProjectDestination(projectName, realDestinationParent)
91
+ if (isWithin(realTemplate, destination) || isWithin(destination, realTemplate)) {
92
+ throw new Error('The project destination cannot overlap the bundled template directory.')
93
+ }
94
+
95
+ const creating = createProgress('Creating project...').start()
96
+ try {
97
+ await mkdir(destination)
98
+ } catch (error) {
99
+ creating.fail('Could not create project directory')
100
+ if (error.code === 'EEXIST' && options.overwrite === true) {
101
+ const existing = await lstat(destination)
102
+ if (!existing.isDirectory() || existing.isSymbolicLink()) {
103
+ throw new Error(`The destination exists and is not a regular directory: ${destination}`)
104
+ }
105
+ await rm(destination, { recursive: true })
106
+ await mkdir(destination)
107
+ } else if (error.code === 'EEXIST') {
108
+ const conflict = new Error(`The destination already exists: ${destination}`)
109
+ conflict.code = 'EEXIST'
110
+ conflict.projectPath = destination
111
+ throw conflict
112
+ } else {
113
+ throw error
114
+ }
115
+ }
116
+ creating.succeed('Project directory created')
117
+
118
+ const progress = createProgress('Copying portfolio template...').start()
119
+ let configuring
120
+ try {
121
+ await copyTree(templatePath, destination)
122
+ progress.succeed('Copying portfolio template')
123
+ configuring = createProgress('Configuring package.json...').start()
124
+ await updatePackageNames(destination, projectName)
125
+ configuring.succeed('Configuring package.json')
126
+ return destination
127
+ } catch (error) {
128
+ progress.fail('Could not create portfolio')
129
+ configuring?.fail('Could not configure project')
130
+ await rm(destination, { recursive: true, force: true })
131
+ throw error
132
+ }
133
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,44 @@
1
+ import inquirer from 'inquirer'
2
+ import {
3
+ DEFAULT_PROJECT_NAME,
4
+ normalizeProjectName,
5
+ validateProjectName,
6
+ } from './utils/project-name.js'
7
+
8
+ export function createProjectQuestions() {
9
+ return [
10
+ {
11
+ type: 'input',
12
+ name: 'projectName',
13
+ message: 'What should your project be called?',
14
+ default: DEFAULT_PROJECT_NAME,
15
+ filter: normalizeProjectName,
16
+ validate: validateProjectName,
17
+ },
18
+ {
19
+ type: 'confirm',
20
+ name: 'installDependencies',
21
+ message: 'Install dependencies?',
22
+ default: true,
23
+ },
24
+ ]
25
+ }
26
+
27
+ export async function promptForProjectOptions(prompt = (questions) => inquirer.prompt(questions)) {
28
+ try {
29
+ const answers = await prompt(createProjectQuestions())
30
+ if (!answers) return null
31
+
32
+ const projectName = normalizeProjectName(answers.projectName)
33
+ const validation = validateProjectName(projectName)
34
+ if (validation !== true) throw new Error(validation)
35
+
36
+ return {
37
+ projectName,
38
+ installDependencies: Boolean(answers.installDependencies),
39
+ }
40
+ } catch (error) {
41
+ if (error instanceof Error && error.name === 'ExitPromptError') return null
42
+ throw error
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ import chalk from 'chalk'
2
+
3
+ export function reportCliError(error) {
4
+ const message = error instanceof Error ? error.message : String(error)
5
+ process.stderr.write(`${chalk.red('FolioKit error:')} ${message}\n`)
6
+ process.exitCode = 1
7
+ }
@@ -0,0 +1,12 @@
1
+ import { dirname, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
5
+
6
+ export function getPackagePath(...segments) {
7
+ return resolve(packageRoot, ...segments)
8
+ }
9
+
10
+ export function getDefaultTemplatePath() {
11
+ return getPackagePath('templates', 'default')
12
+ }
@@ -0,0 +1,5 @@
1
+ import ora from 'ora'
2
+
3
+ export function createProgress(message) {
4
+ return ora(message)
5
+ }
@@ -0,0 +1,30 @@
1
+ export const DEFAULT_PROJECT_NAME = 'my-portfolio'
2
+
3
+ const projectNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
4
+ const reservedNames = new Set([
5
+ 'node_modules',
6
+ 'con',
7
+ 'prn',
8
+ 'aux',
9
+ 'nul',
10
+ ...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
11
+ ...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`),
12
+ ])
13
+
14
+ export function normalizeProjectName(value) {
15
+ if (typeof value !== 'string') return DEFAULT_PROJECT_NAME
16
+ return value.trim() || DEFAULT_PROJECT_NAME
17
+ }
18
+
19
+ export function validateProjectName(value) {
20
+ const name = typeof value === 'string' ? value.trim() : ''
21
+
22
+ if (!name) return 'Enter a project name.'
23
+ if (name.length > 214) return 'Project names must be 214 characters or fewer.'
24
+ if (!projectNamePattern.test(name)) {
25
+ return 'Use lowercase letters and numbers separated by single hyphens.'
26
+ }
27
+ if (reservedNames.has(name)) return 'That name is reserved by the operating system or npm.'
28
+
29
+ return true
30
+ }