create-epinoetics-app 1.0.0 → 1.0.2
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 +0 -0
- package/package.json +1 -1
- package/src/cloner.js +81 -5
- package/src/index.js +76 -22
- package/src/repos.js +34 -12
- package/src/scaffold.js +33 -28
package/README
ADDED
|
File without changes
|
package/package.json
CHANGED
package/src/cloner.js
CHANGED
|
@@ -4,11 +4,11 @@ import { resolve, join } from 'path';
|
|
|
4
4
|
import { simpleGit } from 'simple-git';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
|
|
7
|
+
// ── Clone all plain repos (dashboard, dotnet, frontend) ───────────
|
|
7
8
|
export async function cloneAll({ projectName, repos }) {
|
|
8
9
|
const projectDir = resolve(process.cwd(), projectName);
|
|
9
10
|
const codeDir = join(projectDir, 'code');
|
|
10
11
|
|
|
11
|
-
// Create root + code/ dirs
|
|
12
12
|
fs.mkdirSync(codeDir, { recursive: true });
|
|
13
13
|
|
|
14
14
|
for (const repo of repos) {
|
|
@@ -19,11 +19,7 @@ export async function cloneAll({ projectName, repos }) {
|
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
21
|
await simpleGit().clone(repo.url, dest, ['--depth=1']);
|
|
22
|
-
|
|
23
|
-
// Remove the .git folder so the clone becomes plain source,
|
|
24
|
-
// not a nested git repo inside the new project.
|
|
25
22
|
fs.rmSync(join(dest, '.git'), { recursive: true, force: true });
|
|
26
|
-
|
|
27
23
|
spinner.stop(`${chalk.green('✓')} ${repo.label} ${chalk.dim(`→ code/${repo.id}`)}`);
|
|
28
24
|
} catch (err) {
|
|
29
25
|
spinner.stop(`${chalk.red('✗')} ${repo.label} ${chalk.dim(err.message)}`);
|
|
@@ -31,3 +27,83 @@ export async function cloneAll({ projectName, repos }) {
|
|
|
31
27
|
}
|
|
32
28
|
}
|
|
33
29
|
}
|
|
30
|
+
|
|
31
|
+
// ── Clone NestJS db branch, then prune unselected features ────────
|
|
32
|
+
export async function cloneAndMergeNest({ projectName, database, selectedFeatures, allFeatures, repoUrl }) {
|
|
33
|
+
const projectDir = resolve(process.cwd(), projectName);
|
|
34
|
+
const dest = join(projectDir, 'code', 'api-nest');
|
|
35
|
+
|
|
36
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// 1. Clone the chosen db branch directly
|
|
39
|
+
const spinner = p.spinner();
|
|
40
|
+
spinner.start(`Cloning ${chalk.cyan('NestJS')} ${chalk.dim(`branch: ${database.branch}`)}`);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await simpleGit().clone(repoUrl, dest, ['--branch', database.branch, '--single-branch']);
|
|
44
|
+
spinner.stop(`${chalk.green('✓')} NestJS ${chalk.dim(`→ code/api-nest (${database.branch})`)}`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
spinner.stop(`${chalk.red('✗')} NestJS ${chalk.dim(err.message)}`);
|
|
47
|
+
throw new Error(`Failed to clone NestJS repo: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Remove unselected feature folders from src/features/
|
|
51
|
+
const selectedIds = new Set(selectedFeatures.map(f => f.id));
|
|
52
|
+
const toRemove = allFeatures.filter(f => !selectedIds.has(f.id));
|
|
53
|
+
|
|
54
|
+
for (const feature of toRemove) {
|
|
55
|
+
const featureDir = join(dest, 'src', 'features', feature.id);
|
|
56
|
+
if (fs.existsSync(featureDir)) {
|
|
57
|
+
fs.rmSync(featureDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (toRemove.length > 0) {
|
|
62
|
+
p.log.step(
|
|
63
|
+
`Removed unselected features: ${toRemove.map(f => chalk.dim(f.id)).join(', ')}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Prune schema.ts — remove export lines for deleted features
|
|
68
|
+
await pruneSchema({ dest, toRemove });
|
|
69
|
+
|
|
70
|
+
// 4. Strip .git
|
|
71
|
+
fs.rmSync(join(dest, '.git'), { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Remove export * lines from schema.ts for deleted features ─────
|
|
75
|
+
async function pruneSchema({ dest, toRemove }) {
|
|
76
|
+
if (toRemove.length === 0) return;
|
|
77
|
+
|
|
78
|
+
// Check common schema.ts locations
|
|
79
|
+
const candidates = [
|
|
80
|
+
join(dest, 'src', 'database', 'schema.ts'),
|
|
81
|
+
join(dest, 'src', 'db', 'schema.ts'),
|
|
82
|
+
join(dest, 'src', 'schema.ts'),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const schemaPath = candidates.find(f => fs.existsSync(f));
|
|
86
|
+
if (!schemaPath) {
|
|
87
|
+
p.log.warn('Could not find schema.ts — remove unused exports manually.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let content = fs.readFileSync(schemaPath, 'utf8');
|
|
92
|
+
const before = content;
|
|
93
|
+
|
|
94
|
+
for (const feature of toRemove) {
|
|
95
|
+
// Matches lines like:
|
|
96
|
+
// export * from '../features/posts/post.schema';
|
|
97
|
+
// export * from "../features/seo/seo.schema";
|
|
98
|
+
const pattern = new RegExp(
|
|
99
|
+
`^export \\* from ['"].*features\\/${feature.id}\\/.*['"];?\\n?`,
|
|
100
|
+
'gm'
|
|
101
|
+
);
|
|
102
|
+
content = content.replace(pattern, '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (content !== before) {
|
|
106
|
+
fs.writeFileSync(schemaPath, content, 'utf8');
|
|
107
|
+
p.log.step(`Pruned schema.ts ${chalk.dim(`(removed ${toRemove.length} export line(s))`)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { REPOS } from './repos.js';
|
|
4
|
-
import { cloneAll } from './cloner.js';
|
|
3
|
+
import { REPOS, NEST_DATABASES, NEST_FEATURES } from './repos.js';
|
|
4
|
+
import { cloneAll, cloneAndMergeNest } from './cloner.js';
|
|
5
5
|
import { writeRootFiles } from './scaffold.js';
|
|
6
6
|
import { validateProjectName } from './utils.js';
|
|
7
7
|
|
|
8
8
|
export async function run(flags) {
|
|
9
9
|
console.log('');
|
|
10
10
|
p.intro(
|
|
11
|
-
chalk.bgHex('#7c3aed').white(' create-
|
|
11
|
+
chalk.bgHex('#7c3aed').white(' create-epinoetics-app ') +
|
|
12
12
|
chalk.dim(' assemble your boilerplate')
|
|
13
13
|
);
|
|
14
14
|
|
|
@@ -37,7 +37,38 @@ export async function run(flags) {
|
|
|
37
37
|
if (p.isCancel(backendId)) return cancel();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// ── 3.
|
|
40
|
+
// ── 3. NestJS-specific prompts ────────────────────────────────────
|
|
41
|
+
let database = null;
|
|
42
|
+
let features = [];
|
|
43
|
+
|
|
44
|
+
if (backendId === 'api-nest') {
|
|
45
|
+
// Database
|
|
46
|
+
const dbId = await p.select({
|
|
47
|
+
message: 'Database',
|
|
48
|
+
options: NEST_DATABASES.map(d => ({
|
|
49
|
+
value: d.id,
|
|
50
|
+
label: d.label,
|
|
51
|
+
hint: d.hint,
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
if (p.isCancel(dbId)) return cancel();
|
|
55
|
+
database = NEST_DATABASES.find(d => d.id === dbId);
|
|
56
|
+
|
|
57
|
+
// Features
|
|
58
|
+
const featureIds = await p.multiselect({
|
|
59
|
+
message: 'Features (space to select, enter to confirm)',
|
|
60
|
+
options: NEST_FEATURES.map(f => ({
|
|
61
|
+
value: f.id,
|
|
62
|
+
label: f.label,
|
|
63
|
+
hint: f.hint,
|
|
64
|
+
})),
|
|
65
|
+
required: false,
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(featureIds)) return cancel();
|
|
68
|
+
features = NEST_FEATURES.filter(f => featureIds.includes(f.id));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 4. Frontend (only shown if repos are registered) ─────────────
|
|
41
72
|
let frontendId = flags.frontend ?? null;
|
|
42
73
|
if (REPOS.frontend?.length > 0 && !frontendId) {
|
|
43
74
|
frontendId = await p.select({
|
|
@@ -51,16 +82,24 @@ export async function run(flags) {
|
|
|
51
82
|
if (p.isCancel(frontendId)) return cancel();
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
// ── Build
|
|
55
|
-
const
|
|
85
|
+
// ── Build list of plain repos to clone ───────────────────────────
|
|
86
|
+
const plainRepos = buildPlainRepos({ backendId, frontendId });
|
|
56
87
|
|
|
57
88
|
// ── Summary ───────────────────────────────────────────────────────
|
|
58
89
|
console.log('');
|
|
90
|
+
|
|
91
|
+
const nestLines = backendId === 'api-nest' ? [
|
|
92
|
+
`${chalk.dim('database'.padEnd(16))} ${chalk.cyan(database.label)}`,
|
|
93
|
+
`${chalk.dim('features'.padEnd(16))} ${features.length ? features.map(f => chalk.cyan(f.label)).join(chalk.dim(', ')) : chalk.dim('none')}`,
|
|
94
|
+
] : [];
|
|
95
|
+
|
|
96
|
+
const repoLines = plainRepos.map(r =>
|
|
97
|
+
`${chalk.dim(r.id.padEnd(16))} ${chalk.cyan(r.label)} ${chalk.dim(r.url)}`
|
|
98
|
+
);
|
|
99
|
+
|
|
59
100
|
p.note(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.join('\n'),
|
|
63
|
-
'Repos to clone'
|
|
101
|
+
[...nestLines, ...repoLines].join('\n'),
|
|
102
|
+
'Scaffold summary'
|
|
64
103
|
);
|
|
65
104
|
|
|
66
105
|
if (flags.dryRun) {
|
|
@@ -73,34 +112,49 @@ export async function run(flags) {
|
|
|
73
112
|
if (p.isCancel(confirm) || !confirm) return cancel();
|
|
74
113
|
}
|
|
75
114
|
|
|
76
|
-
// ── Clone
|
|
77
|
-
|
|
115
|
+
// ── Clone NestJS db branch + prune unselected features ───────────
|
|
116
|
+
if (backendId === 'api-nest') {
|
|
117
|
+
const nestRepo = REPOS.backend.find(r => r.id === 'api-nest');
|
|
118
|
+
await cloneAndMergeNest({
|
|
119
|
+
projectName,
|
|
120
|
+
database,
|
|
121
|
+
selectedFeatures: features,
|
|
122
|
+
allFeatures: NEST_FEATURES,
|
|
123
|
+
repoUrl: nestRepo.url,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Clone plain repos (dashboard, dotnet, frontend) ───────────────
|
|
128
|
+
if (plainRepos.length > 0) {
|
|
129
|
+
await cloneAll({ projectName, repos: plainRepos });
|
|
130
|
+
}
|
|
78
131
|
|
|
79
|
-
// ── Write root files
|
|
80
|
-
await writeRootFiles({ projectName,
|
|
132
|
+
// ── Write root files ──────────────────────────────────────────────
|
|
133
|
+
await writeRootFiles({ projectName, plainRepos, backendId, database, features, frontendId });
|
|
81
134
|
|
|
82
|
-
// ── Done ──────────────────────────────────────────────────────────
|
|
83
135
|
p.outro(
|
|
84
136
|
chalk.green('Done!') + '\n\n' +
|
|
85
137
|
` ${chalk.dim('cd')} ${chalk.cyan(projectName)}`
|
|
86
138
|
);
|
|
87
139
|
}
|
|
88
140
|
|
|
89
|
-
function
|
|
141
|
+
function buildPlainRepos({ backendId, frontendId }) {
|
|
90
142
|
const list = [];
|
|
91
143
|
|
|
92
|
-
//
|
|
144
|
+
// Dashboard — always included
|
|
93
145
|
for (const group of Object.values(REPOS)) {
|
|
94
146
|
for (const repo of group) {
|
|
95
147
|
if (repo.fixed) list.push(repo);
|
|
96
148
|
}
|
|
97
149
|
}
|
|
98
150
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
// .NET (NestJS is handled separately)
|
|
152
|
+
if (backendId === 'api-dotnet') {
|
|
153
|
+
const dotnet = REPOS.backend.find(r => r.id === 'api-dotnet');
|
|
154
|
+
if (dotnet) list.push(dotnet);
|
|
155
|
+
}
|
|
102
156
|
|
|
103
|
-
//
|
|
157
|
+
// Frontend
|
|
104
158
|
if (frontendId && REPOS.frontend) {
|
|
105
159
|
const frontend = REPOS.frontend.find(r => r.id === frontendId);
|
|
106
160
|
if (frontend) list.push(frontend);
|
|
@@ -112,4 +166,4 @@ function buildRepoList({ backendId, frontendId }) {
|
|
|
112
166
|
function cancel() {
|
|
113
167
|
p.cancel('Cancelled.');
|
|
114
168
|
process.exit(0);
|
|
115
|
-
}
|
|
169
|
+
}
|
package/src/repos.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registry of all available boilerplate repos.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* NestJS repo branch convention:
|
|
5
|
+
* master — base helpers (filters, guards, interceptors, config)
|
|
6
|
+
* db/drizzle — Drizzle + Turso, includes src/features/* (all optional features)
|
|
7
|
+
* db/prisma — Prisma + PostgreSQL, includes src/features/* (all optional features)
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* url — GitHub clone URL
|
|
13
|
-
* fixed — if true, always included (no prompt)
|
|
9
|
+
* To add a new feature: add a folder to src/features/ in the db branches,
|
|
10
|
+
* add an export line in schema.ts, then add an entry to NEST_FEATURES.
|
|
11
|
+
* To add a new database: add a branch and an entry to NEST_DATABASES.
|
|
12
|
+
* To add a frontend: uncomment and fill in the frontend block below.
|
|
14
13
|
*/
|
|
14
|
+
|
|
15
15
|
export const REPOS = {
|
|
16
16
|
|
|
17
17
|
// ── Backend (pick one) ────────────────────────────────────────────
|
|
@@ -20,7 +20,7 @@ export const REPOS = {
|
|
|
20
20
|
id: 'api-nest',
|
|
21
21
|
label: 'NestJS',
|
|
22
22
|
hint: 'TypeScript, REST/GraphQL, modular architecture',
|
|
23
|
-
url: 'https://github.com/tyndareosmasselos/
|
|
23
|
+
url: 'https://github.com/tyndareosmasselos/boilerplate-nestjs',
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
id: 'api-dotnet',
|
|
@@ -42,8 +42,6 @@ export const REPOS = {
|
|
|
42
42
|
],
|
|
43
43
|
|
|
44
44
|
// ── Frontend (pick one) ───────────────────────────────────────────
|
|
45
|
-
// Add Next.js and Astro repos here when ready:
|
|
46
|
-
//
|
|
47
45
|
// frontend: [
|
|
48
46
|
// {
|
|
49
47
|
// id: 'web-next',
|
|
@@ -60,3 +58,27 @@ export const REPOS = {
|
|
|
60
58
|
// ],
|
|
61
59
|
|
|
62
60
|
};
|
|
61
|
+
|
|
62
|
+
// ── NestJS-specific config ─────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export const NEST_DATABASES = [
|
|
65
|
+
{
|
|
66
|
+
id: 'drizzle',
|
|
67
|
+
label: 'Drizzle + Turso',
|
|
68
|
+
hint: 'Lightweight, edge-friendly, LibSQL',
|
|
69
|
+
branch: 'db/drizzle',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'prisma',
|
|
73
|
+
label: 'Prisma + PostgreSQL',
|
|
74
|
+
hint: 'SQL, migrations, Prisma Studio',
|
|
75
|
+
branch: 'db/prisma',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export const NEST_FEATURES = [
|
|
80
|
+
{ id: 'posts', label: 'Blog / Posts', hint: 'Posts, slugs, publishing workflow' },
|
|
81
|
+
{ id: 'seo', label: 'SEO', hint: 'Sitemap, robots, meta tags' },
|
|
82
|
+
// { id: 'auth', label: 'Auth', hint: 'JWT + refresh tokens' },
|
|
83
|
+
// { id: 'mail', label: 'Mailer', hint: 'Resend / Nodemailer integration' },
|
|
84
|
+
];
|
package/src/scaffold.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
|
|
4
|
-
export async function writeRootFiles({ projectName,
|
|
4
|
+
export async function writeRootFiles({ projectName, plainRepos = [], backendId, database, features = [], frontendId }) {
|
|
5
5
|
const projectDir = resolve(process.cwd(), projectName);
|
|
6
6
|
|
|
7
7
|
// ── .gitignore ────────────────────────────────────────────────────
|
|
@@ -22,48 +22,53 @@ export async function writeRootFiles({ projectName, selected, backendId, fronten
|
|
|
22
22
|
fs.writeFileSync(join(projectDir, '.gitignore'), gitignore, 'utf8');
|
|
23
23
|
|
|
24
24
|
// ── README.md ─────────────────────────────────────────────────────
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
.join('\n');
|
|
25
|
+
const nestSection = backendId === 'api-nest' ? `
|
|
26
|
+
## NestJS backend
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
- **Database**: ${database.label}
|
|
29
|
+
- **Features**: ${features.length ? features.map(f => f.label).join(', ') : 'none'}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
cd code/api-nest
|
|
33
|
+
# follow README inside
|
|
34
|
+
\`\`\`
|
|
35
|
+
` : '';
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
const repoLines = plainRepos.map(r =>
|
|
38
|
+
`| \`code/${r.id}\` | ${r.label} | ${r.url} |`
|
|
39
|
+
).join('\n');
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
\`\`\`
|
|
41
|
+
const structureLines = [
|
|
42
|
+
backendId === 'api-nest' ? ' ├── api-nest/ ← NestJS' : null,
|
|
43
|
+
backendId === 'api-dotnet' ? ' ├── api-dotnet/ ← .NET Microservices' : null,
|
|
44
|
+
' ├── dashboard/ ← Angular admin panel',
|
|
45
|
+
frontendId ? ` ├── ${frontendId}/` : null,
|
|
46
|
+
].filter(Boolean).join('\n');
|
|
42
47
|
|
|
48
|
+
const repoTableSection = plainRepos.length > 0 ? `
|
|
43
49
|
## Repos
|
|
44
50
|
|
|
45
51
|
| Folder | Name | Source |
|
|
46
52
|
|--------|------|--------|
|
|
47
53
|
${repoLines}
|
|
54
|
+
` : '';
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
const readme = `# ${projectName}
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
> Scaffolded with \`create-epinoetics-app\`
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
# Backend
|
|
55
|
-
cd code/${backendId}
|
|
56
|
-
# follow README inside
|
|
60
|
+
## Structure
|
|
57
61
|
|
|
58
|
-
# Dashboard
|
|
59
|
-
cd code/dashboard
|
|
60
|
-
# follow README inside
|
|
61
|
-
${hasFrontend ? `
|
|
62
|
-
# Frontend
|
|
63
|
-
cd code/${frontendId}
|
|
64
|
-
# follow README inside` : ''}
|
|
65
62
|
\`\`\`
|
|
63
|
+
${projectName}/
|
|
64
|
+
└── code/
|
|
65
|
+
${structureLines}
|
|
66
|
+
\`\`\`
|
|
67
|
+
${nestSection}${repoTableSection}
|
|
68
|
+
## Getting started
|
|
69
|
+
|
|
70
|
+
Each piece is self-contained — navigate into a folder and follow its own README.
|
|
66
71
|
`;
|
|
67
72
|
|
|
68
73
|
fs.writeFileSync(join(projectDir, 'README.md'), readme, 'utf8');
|
|
69
|
-
}
|
|
74
|
+
}
|