create-epinoetics-app 1.0.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-epinoetics-app",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Scaffold a new project from your boilerplate repos",
5
5
  "type": "module",
6
6
  "bin": {
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-myapp ') +
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. Frontend (only shown if repos are registered) ─────────────
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 the final list of repos to clone ────────────────────────
55
- const selected = buildRepoList({ backendId, frontendId });
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
- selected
61
- .map(r => `${chalk.dim(r.id.padEnd(16))} ${chalk.cyan(r.label)} ${chalk.dim(r.url)}`)
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
- await cloneAll({ projectName, repos: selected });
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 (.gitignore, README) ─────────────────────────
80
- await writeRootFiles({ projectName, selected, backendId, frontendId });
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 buildRepoList({ backendId, frontendId }) {
141
+ function buildPlainRepos({ backendId, frontendId }) {
90
142
  const list = [];
91
143
 
92
- // Fixed repos (dashboard — always included)
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
- // Chosen backend
100
- const backend = REPOS.backend.find(r => r.id === backendId);
101
- if (backend) list.push(backend);
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
- // Chosen frontend (optional until repos are added)
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
- * To add a new repo (e.g. Astro or Next.js frontend):
5
- * 1. Add an entry to the relevant group below.
6
- * 2. That's it the CLI picks it up automatically.
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
- * Fields:
9
- * id — internal key, used as the folder name under code/
10
- * label — shown in the prompt
11
- * hint — shown as a subtitle in the prompt
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/Boilerplater.NestJS',
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
+ ];