create-foliokit 0.0.0-stage → 1.0.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -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/.gitignore +20 -0
  13. package/templates/default/data.ts +227 -0
  14. package/templates/default/index.html +23 -0
  15. package/templates/default/package-lock.json +1280 -0
  16. package/templates/default/package.json +28 -0
  17. package/templates/default/public/avatar-placeholder.svg +10 -0
  18. package/templates/default/public/footer-heart.png +0 -0
  19. package/templates/default/public/love-always-wins.png +0 -0
  20. package/templates/default/public/project-placeholder.svg +10 -0
  21. package/templates/default/public/robots.txt +2 -0
  22. package/templates/default/public/stars-foreground-landscape.svg +202 -0
  23. package/templates/default/public/stars-foreground-portrait.svg +202 -0
  24. package/templates/default/public/stars-landscape.svg +322 -0
  25. package/templates/default/public/stars-portrait.svg +322 -0
  26. package/templates/default/scripts/generate-seo.mjs +34 -0
  27. package/templates/default/src/App.tsx +552 -0
  28. package/templates/default/src/BrandIcon.tsx +36 -0
  29. package/templates/default/src/ProjectMagazineGallery.css +90 -0
  30. package/templates/default/src/ProjectMagazineGallery.tsx +210 -0
  31. package/templates/default/src/main.tsx +10 -0
  32. package/templates/default/src/styles.css +536 -0
  33. package/templates/default/src/utils.ts +2 -0
  34. package/templates/default/src/vite-env.d.ts +1 -0
  35. package/templates/default/tests/portfolio.test.js +96 -0
  36. package/templates/default/tsconfig.app.json +21 -0
  37. package/templates/default/tsconfig.json +7 -0
  38. package/templates/default/tsconfig.node.json +16 -0
  39. package/templates/default/vite.config.ts +38 -0
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ node_modules/
2
+ dist/
3
+ .vite/
4
+ .netlify/
5
+ .vercel/
6
+ coverage/
7
+ *.tsbuildinfo
8
+ *.log
9
+ npm-debug.log*
10
+ .DS_Store
11
+ Thumbs.db
12
+ .env
13
+ .env.*
14
+ secrets/
15
+ *.pem
16
+ *.key
17
+ *.p12
18
+ *.pfx
19
+ *.credentials
20
+ *.secret
@@ -0,0 +1,227 @@
1
+ /*
2
+ * FolioKit Portfolio Configuration
3
+ *
4
+ * Customize your portfolio by editing the information in this file.
5
+ * Replace the sample content with your own details.
6
+ *
7
+ * You generally do not need to edit the React components or other source
8
+ * files to update your personal information, projects, skills, or links.
9
+ *
10
+ * Icon Reference Links:
11
+ * - Technical Skills: https://skillicons.dev
12
+ * - Social & Brand Icons: https://simpleicons.org
13
+ */
14
+
15
+ export interface Skill {
16
+ name: string
17
+ icon: string
18
+ category?: string
19
+ }
20
+
21
+ export interface Project {
22
+ name: string
23
+ category?: string
24
+ description: string
25
+ image?: string
26
+ imageAlt?: string
27
+ technologies: Skill[]
28
+ liveUrl?: string
29
+ githubUrl?: string
30
+ featured?: boolean
31
+ }
32
+
33
+ export interface ContentItem {
34
+ title: string
35
+ organization?: string
36
+ date?: string
37
+ description?: string
38
+ image?: string
39
+ links?: { label: string; url: string }[]
40
+ }
41
+
42
+ export interface Testimonial {
43
+ name: string
44
+ role: string
45
+ company?: string
46
+ message: string
47
+ image?: string
48
+ }
49
+
50
+ export interface SocialIcon {
51
+ label: string
52
+ icon: string
53
+ url?: string
54
+ color?: string
55
+ }
56
+
57
+ export interface PortfolioConfig {
58
+ profile: {
59
+ name: string
60
+ headline: string
61
+ introduction: string
62
+ about: string
63
+ location?: string
64
+ timeZone?: string
65
+ timeZoneLabel?: string
66
+ profileImage?: string
67
+ resumeUrl?: string
68
+ quote?: string
69
+ }
70
+ seo: {
71
+ title: string
72
+ description: string
73
+ siteUrl?: string
74
+ image?: string
75
+ }
76
+ socialIcons: SocialIcon[]
77
+ skills: Skill[]
78
+ projects: Project[]
79
+ experience: ContentItem[]
80
+ education: ContentItem[]
81
+ testimonials: Testimonial[]
82
+ contact: {
83
+ email?: string
84
+ }
85
+ }
86
+
87
+ export const portfolioConfig: PortfolioConfig = {
88
+ // Personal Information
89
+ profile: {
90
+ name: 'Your Name',
91
+ headline: 'Full Stack Developer',
92
+ introduction: "Hey, there! I'm a",
93
+ about: `Hey, there! I'm a developer who loves building products that people actually use. Write a couple of sentences here about who you are, what you do, and what you're currently working on.
94
+
95
+ Use this space to talk about your experience, the kinds of problems you enjoy solving, and the technologies you reach for most often.
96
+
97
+ Wrap up with what you're excited about right now — side projects, open source, or whatever you're learning next`,
98
+ location: 'Delhi, India',
99
+ timeZone: 'Asia/Kolkata', // IANA time zone for the live clock
100
+ timeZoneLabel: 'IST',
101
+ profileImage: '/avatar-placeholder.svg', // Path relative to public/ directory
102
+ quote: 'Build with curiosity. Share what you learn.',
103
+ },
104
+
105
+ // SEO & Metadata
106
+ seo: {
107
+ title: '{{name}} — {{headline}}',
108
+ description: '{{name}} is a {{headline}} building useful, thoughtful web experiences.',
109
+ },
110
+
111
+ // Social Links
112
+ // Add, edit, remove, or reorder your social media profiles.
113
+ // Find brand icons at: https://simpleicons.org (built-in icons: 'mail', 'file-text')
114
+ socialIcons: [
115
+ { label: 'GitHub', icon: 'github', url: 'https://github.com/your-username', color: '#f2f0e9' },
116
+ { label: 'LinkedIn', icon: 'linkedin', url: 'https://linkedin.com/in/your-username', color: '#0a66c2' },
117
+ { label: 'Instagram', icon: 'instagram', url: 'https://instagram.com/your-username', color: '#e4405f' },
118
+ { label: 'X', icon: 'x', url: 'https://x.com/your-username', color: '#f2f0e9' },
119
+ ],
120
+
121
+ // Skills
122
+ // Add, edit, remove, or reorder skills.
123
+ // Find skill icon identifiers at: https://skillicons.dev
124
+ skills: [
125
+ { name: 'HTML', icon: 'html', category: 'Frontend' },
126
+ { name: 'CSS', icon: 'css', category: 'Frontend' },
127
+ { name: 'JavaScript', icon: 'js', category: 'Frontend' },
128
+ { name: 'React', icon: 'react', category: 'Frontend' },
129
+ { name: 'TypeScript', icon: 'ts', category: 'Frontend' },
130
+ { name: 'Tailwind CSS', icon: 'tailwind', category: 'Frontend' },
131
+ { name: 'Vite', icon: 'vite', category: 'Tools' },
132
+ { name: 'Redux', icon: 'redux', category: 'Frontend' },
133
+ { name: 'Markdown', icon: 'md', category: 'Tools' },
134
+ { name: 'Node.js', icon: 'nodejs', category: 'Backend' },
135
+ { name: 'Express.js', icon: 'express', category: 'Backend' },
136
+ { name: 'MongoDB', icon: 'mongodb', category: 'Backend' },
137
+ { name: 'MySQL', icon: 'mysql', category: 'Backend' },
138
+ { name: 'PostgreSQL', icon: 'postgres', category: 'Backend' },
139
+ { name: 'C', icon: 'c', category: 'Languages' },
140
+ { name: 'Python', icon: 'python', category: 'Languages' },
141
+ { name: 'Java', icon: 'java', category: 'Languages' },
142
+ { name: 'Postman', icon: 'postman', category: 'Tools' },
143
+ { name: 'VS Code', icon: 'vscode', category: 'Tools' },
144
+ { name: 'Git', icon: 'git', category: 'Tools' },
145
+ { name: 'GitHub', icon: 'github', category: 'Tools' },
146
+ { name: 'npm', icon: 'npm', category: 'Tools' },
147
+ { name: 'GitHub Actions', icon: 'githubactions', category: 'Tools' },
148
+ ],
149
+
150
+ // Projects
151
+ // Add, edit, remove, or reorder projects. Set featured: true to highlight in the 3D gallery.
152
+ // Technology icon identifiers also come from: https://skillicons.dev
153
+ projects: [
154
+ {
155
+ name: 'Project One',
156
+ category: 'PROJECT CATEGORY',
157
+ description: 'Add a short description of this project, the problem it solves, and what you built.',
158
+ image: '/project-placeholder.svg',
159
+ imageAlt: 'Project image placeholder, 1200 by 675 pixels',
160
+ technologies: [{ name: 'React', icon: 'react' }, { name: 'TypeScript', icon: 'ts' }],
161
+ featured: true, // Highlights project in the 3D magazine gallery
162
+ },
163
+ {
164
+ name: 'Project Two',
165
+ category: 'PROJECT CATEGORY',
166
+ description: 'Describe another project here. Replace the title, image, and technology stack in data.ts.',
167
+ image: '/project-placeholder.svg',
168
+ imageAlt: 'Project image placeholder, 1200 by 675 pixels',
169
+ technologies: [{ name: 'Node.js', icon: 'nodejs' }, { name: 'Express.js', icon: 'express' }, { name: 'MongoDB', icon: 'mongodb' }],
170
+ },
171
+ {
172
+ name: 'Project Three',
173
+ category: 'PROJECT CATEGORY',
174
+ description: 'Use this space to explain what you made and the tools or technologies you used.',
175
+ image: '/project-placeholder.svg',
176
+ imageAlt: 'Project image placeholder, 1200 by 675 pixels',
177
+ technologies: [{ name: 'Python', icon: 'python' }, { name: 'C', icon: 'c' }, { name: 'Java', icon: 'java' }],
178
+ },
179
+ ],
180
+
181
+ // Work Experience
182
+ // Add, edit, remove, or reorder work history. Leave empty ([]) to hide this section.
183
+ experience: [
184
+ {
185
+ title: 'Your Role',
186
+ organization: 'Company Name',
187
+ date: 'Month YYYY – Present',
188
+ description: 'Replace this sample with your role, company, and dates.',
189
+ },
190
+ ],
191
+
192
+ // Education
193
+ // Add, edit, remove, or reorder education. Leave empty ([]) to hide this section.
194
+ education: [
195
+ {
196
+ title: 'Degree or Program',
197
+ organization: 'Your University',
198
+ date: 'YYYY – YYYY',
199
+ description: 'Add your major, score, or another education detail here.',
200
+ },
201
+ ],
202
+
203
+ // Testimonials
204
+ // Add, edit, remove, or reorder testimonials. Leave empty ([]) to hide this section.
205
+ testimonials: [
206
+ {
207
+ name: 'Person One',
208
+ role: 'Role or designation',
209
+ message: 'Replace this placeholder with a genuine testimonial before publishing.',
210
+ },
211
+ {
212
+ name: 'Person Two',
213
+ role: 'Role or designation',
214
+ message: 'Replace this placeholder with a genuine testimonial before publishing.',
215
+ },
216
+ {
217
+ name: 'Person Three',
218
+ role: 'Role or designation',
219
+ message: 'Replace this placeholder with a genuine testimonial before publishing.',
220
+ },
221
+ ],
222
+
223
+ // Contact
224
+ contact: {
225
+ email: 'hello@example.com',
226
+ },
227
+ }
@@ -0,0 +1,23 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <meta name="description" content="A developer portfolio." />
8
+ <meta property="og:type" content="website" />
9
+ <meta property="og:title" content="Developer Portfolio" />
10
+ <meta property="og:description" content="A developer portfolio." />
11
+ <meta name="twitter:card" content="summary_large_image" />
12
+ <meta name="twitter:title" content="Developer Portfolio" />
13
+ <meta name="twitter:description" content="A developer portfolio." />
14
+ <title>Developer Portfolio</title>
15
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ <script type="module" src="/src/main.tsx"></script>
22
+ </body>
23
+ </html>