create-epinoetics-app 1.0.8 → 1.0.10

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.8",
3
+ "version": "1.0.10",
4
4
  "description": "Scaffold a new project from your boilerplate repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cloner.js CHANGED
@@ -4,50 +4,75 @@ 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) ───────────
8
- export async function cloneAll({ projectName, repos }) {
7
+ // ── Initialize the root project repo ─────────────────────────────
8
+ export async function initRootRepo({ projectName }) {
9
9
  const projectDir = resolve(process.cwd(), projectName);
10
- const codeDir = join(projectDir, 'code');
10
+ fs.mkdirSync(projectDir, { recursive: true });
11
11
 
12
- fs.mkdirSync(codeDir, { recursive: true });
12
+ const git = simpleGit(projectDir);
13
+ await git.init();
14
+
15
+ // Initial empty commit so subtree add works
16
+ await git.commit('chore: init project', { '--allow-empty': null });
17
+
18
+ p.log.step(`Initialized git repo ${chalk.dim(`→ ${projectName}`)}`);
19
+
20
+ return projectDir;
21
+ }
22
+
23
+ // ── Add a plain repo as a subtree (dashboard, dotnet, frontend) ───
24
+ export async function addSubtrees({ projectDir, repos }) {
25
+ const git = simpleGit(projectDir);
13
26
 
14
27
  for (const repo of repos) {
15
- const dest = join(codeDir, repo.id);
28
+ const prefix = `code/${repo.id}`;
16
29
  const spinner = p.spinner();
17
30
 
18
- spinner.start(`Cloning ${chalk.cyan(repo.label)} ${chalk.dim(repo.url)}`);
31
+ spinner.start(`Adding ${chalk.cyan(repo.label)} ${chalk.dim(`→ ${prefix}`)}`);
19
32
 
20
33
  try {
21
- await simpleGit().clone(repo.url, dest, ['--depth=1']);
22
- fs.rmSync(join(dest, '.git'), { recursive: true, force: true });
23
- spinner.stop(`${chalk.green('')} ${repo.label} ${chalk.dim(`→ code/${repo.id}`)}`);
34
+ await git.raw([
35
+ 'subtree', 'add',
36
+ '--prefix', prefix,
37
+ repo.url,
38
+ 'master',
39
+ '--squash',
40
+ ]);
41
+
42
+ spinner.stop(`${chalk.green('✓')} ${repo.label} ${chalk.dim(`→ ${prefix}`)}`);
24
43
  } catch (err) {
25
44
  spinner.stop(`${chalk.red('✗')} ${repo.label} ${chalk.dim(err.message)}`);
26
- throw new Error(`Failed to clone ${repo.label}: ${err.message}`);
45
+ throw new Error(`Failed to add subtree ${repo.label}: ${err.message}`);
27
46
  }
28
47
  }
29
48
  }
30
49
 
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 });
50
+ // ── Add NestJS as subtree, then prune unselected features ─────────
51
+ export async function addNestSubtree({ projectDir, database, selectedFeatures, allFeatures, repoUrl }) {
52
+ const git = simpleGit(projectDir);
53
+ const prefix = 'code/webapi';
54
+ const dest = join(projectDir, prefix);
37
55
 
38
- // 1. Clone the chosen db branch directly
56
+ // 1. Add NestJS as subtree
39
57
  const spinner = p.spinner();
40
- spinner.start(`Cloning ${chalk.cyan('NestJS')} ${chalk.dim(`branch: ${database.branch}`)}`);
58
+ spinner.start(`Adding ${chalk.cyan('NestJS')} ${chalk.dim(`branch: ${database.branch}`)}`);
41
59
 
42
60
  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})`)}`);
61
+ await git.raw([
62
+ 'subtree', 'add',
63
+ '--prefix', prefix,
64
+ repoUrl,
65
+ database.branch,
66
+ '--squash',
67
+ ]);
68
+
69
+ spinner.stop(`${chalk.green('✓')} NestJS ${chalk.dim(`→ ${prefix} (${database.branch})`)}`);
45
70
  } catch (err) {
46
71
  spinner.stop(`${chalk.red('✗')} NestJS ${chalk.dim(err.message)}`);
47
- throw new Error(`Failed to clone NestJS repo: ${err.message}`);
72
+ throw new Error(`Failed to add NestJS subtree: ${err.message}`);
48
73
  }
49
74
 
50
- // 2. Remove unselected feature folders from src/features/
75
+ // 2. Remove unselected feature folders
51
76
  const selectedIds = new Set(selectedFeatures.map(f => f.id));
52
77
  const toRemove = allFeatures.filter(f => !selectedIds.has(f.id));
53
78
 
@@ -64,7 +89,7 @@ export async function cloneAndMergeNest({ projectName, database, selectedFeature
64
89
  );
65
90
  }
66
91
 
67
- // 3. Prune schema — Drizzle or Prisma
92
+ // 3. Prune schema
68
93
  if (database.id === 'drizzle') {
69
94
  pruneDrizzleSchema({ dest, toRemove });
70
95
  } else if (database.id === 'prisma') {
@@ -74,34 +99,17 @@ export async function cloneAndMergeNest({ projectName, database, selectedFeature
74
99
  // 4. Prune app.module.ts
75
100
  pruneAppModule({ dest, toRemove });
76
101
 
77
- // 5. Remove migrations directory
102
+ // 5. Remove migrations
78
103
  pruneMigrations({ dest, database });
79
104
 
80
105
  // 6. Write scaffold.config.json
81
106
  writeScaffoldConfig({ dest, database, selectedFeatures, repoUrl });
82
107
 
83
- // 7. Strip boilerplate .git and init a fresh one
84
- await initFreshGit({ dest });
85
- }
108
+ // 7. Commit the pruning changes
109
+ await git.add('.');
110
+ await git.commit('chore: prune unselected features');
86
111
 
87
- // ── Strip boilerplate .git, init fresh repo, initial commit ───────
88
- async function initFreshGit({ dest }) {
89
- const spinner = p.spinner();
90
- spinner.start('Initializing fresh git repo...');
91
-
92
- try {
93
- // Remove the boilerplate's .git
94
- fs.rmSync(join(dest, '.git'), { recursive: true, force: true });
95
-
96
- const git = simpleGit(dest);
97
- await git.init();
98
- await git.add('.');
99
- await git.commit('chore: initial scaffold via create-epinoetics-app');
100
-
101
- spinner.stop(`${chalk.green('✓')} Fresh git repo initialized`);
102
- } catch (err) {
103
- spinner.stop(`${chalk.yellow('⚠')} Git init failed ${chalk.dim(err.message)}`);
104
- }
112
+ p.log.step(`Committed pruned scaffold`);
105
113
  }
106
114
 
107
115
  // ── Write scaffold.config.json ────────────────────────────────────
package/src/scaffold.js CHANGED
@@ -1,8 +1,7 @@
1
- import { join, resolve } from 'path';
1
+ import { join } from 'path';
2
2
  import fs from 'fs';
3
3
 
4
- export async function writeRootFiles({ projectName, plainRepos = [], backendId, database, features = [], frontendId }) {
5
- const projectDir = resolve(process.cwd(), projectName);
4
+ export async function writeRootFiles({ projectName, projectDir, plainRepos = [], backendId, database, features = [], frontendId }) {
6
5
 
7
6
  // ── .gitignore ────────────────────────────────────────────────────
8
7
  const gitignore = [
@@ -22,37 +21,25 @@ export async function writeRootFiles({ projectName, plainRepos = [], backendId,
22
21
  fs.writeFileSync(join(projectDir, '.gitignore'), gitignore, 'utf8');
23
22
 
24
23
  // ── README.md ─────────────────────────────────────────────────────
25
- const nestSection = backendId === 'api-nest' ? `
26
- ## NestJS backend
24
+ const nestSection = backendId === 'webapi' ? `
25
+ ## NestJS Web API
27
26
 
28
27
  - **Database**: ${database.label}
29
28
  - **Features**: ${features.length ? features.map(f => f.label).join(', ') : 'none'}
30
29
 
31
30
  \`\`\`bash
32
- cd code/api-nest
31
+ cd code/webapi
33
32
  # follow README inside
34
33
  \`\`\`
35
34
  ` : '';
36
35
 
37
- const repoLines = plainRepos.map(r =>
38
- `| \`code/${r.id}\` | ${r.label} | ${r.url} |`
39
- ).join('\n');
40
-
41
36
  const structureLines = [
42
- backendId === 'api-nest' ? ' ├── api-nest/ ← NestJS' : null,
37
+ backendId === 'webapi' ? ' ├── webapi/ ← NestJS' : null,
43
38
  backendId === 'api-dotnet' ? ' ├── api-dotnet/ ← .NET Microservices' : null,
44
39
  ' ├── dashboard/ ← Angular admin panel',
45
40
  frontendId ? ` ├── ${frontendId}/` : null,
46
41
  ].filter(Boolean).join('\n');
47
42
 
48
- const repoTableSection = plainRepos.length > 0 ? `
49
- ## Repos
50
-
51
- | Folder | Name | Source |
52
- |--------|------|--------|
53
- ${repoLines}
54
- ` : '';
55
-
56
43
  const readme = `# ${projectName}
57
44
 
58
45
  > Scaffolded with \`create-epinoetics-app\`
@@ -64,7 +51,13 @@ ${projectName}/
64
51
  └── code/
65
52
  ${structureLines}
66
53
  \`\`\`
67
- ${nestSection}${repoTableSection}
54
+ ${nestSection}
55
+ ## Pulling boilerplate updates
56
+
57
+ \`\`\`bash
58
+ npx create-epinoetics-app sync
59
+ \`\`\`
60
+
68
61
  ## Getting started
69
62
 
70
63
  Each piece is self-contained — navigate into a folder and follow its own README.
package/src/syncer.js CHANGED
@@ -1,20 +1,9 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import chalk from 'chalk';
3
- import { join } from 'path';
3
+ import { join, resolve } from 'path';
4
4
  import { simpleGit } from 'simple-git';
5
- import { NEST_DATABASES } from './repos.js';
5
+ import { NEST_DATABASES, REPOS } from './repos.js';
6
6
  import fs from 'fs';
7
- import os from 'os';
8
- import path from 'path';
9
-
10
- // Paths that are never overwritten during sync
11
- const PROTECTED = [
12
- 'src/features',
13
- 'scaffold.config.json',
14
- '.env',
15
- '.env.local',
16
- '.git',
17
- ];
18
7
 
19
8
  export async function runSync() {
20
9
  console.log('');
@@ -23,116 +12,108 @@ export async function runSync() {
23
12
  chalk.dim(' pull latest boilerplate updates')
24
13
  );
25
14
 
15
+ // Must be run from project root (where .git is)
26
16
  const cwd = process.cwd();
27
17
 
28
- // ── 1. Read scaffold.config.json ──────────────────────────────────
29
- const configPath = join(cwd, 'scaffold.config.json');
30
- if (!fs.existsSync(configPath)) {
18
+ if (!fs.existsSync(join(cwd, '.git'))) {
31
19
  p.log.error(
32
- 'No scaffold.config.json found.\n' +
33
- chalk.dim('Make sure you are inside the code/api-nest directory.')
20
+ 'No .git found.\n' +
21
+ chalk.dim('Run this from your project root, not inside code/webapi.')
34
22
  );
35
23
  process.exit(1);
36
24
  }
37
25
 
38
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
39
- const database = NEST_DATABASES.find(d => d.id === config.database);
26
+ // ── Check for uncommitted changes ─────────────────────────────────
27
+ const git = simpleGit(cwd);
28
+ const status = await git.status();
40
29
 
41
- p.note(
42
- [
43
- `${chalk.dim('branch '.padEnd(14))} ${chalk.cyan(config.branch)}`,
44
- `${chalk.dim('features'.padEnd(14))} ${config.features.length ? config.features.map(f => chalk.cyan(f)).join(chalk.dim(', ')) : chalk.dim('none')}`,
45
- ].join('\n'),
46
- 'Syncing from boilerplate'
47
- );
48
-
49
- const confirm = await p.confirm({
50
- message: `Pull latest from ${chalk.cyan(config.branch)}? Your code in src/features/ will not be touched.`,
51
- });
52
- if (p.isCancel(confirm) || !confirm) return cancel();
53
-
54
- // ── 2. Clone latest into temp dir ─────────────────────────────────
55
- const tmpDir = join(os.tmpdir(), `epinoetics-sync-${Date.now()}`);
56
- const spinner = p.spinner();
57
-
58
- spinner.start(`Fetching latest from ${chalk.dim(config.branch)}...`);
59
-
60
- try {
61
- await simpleGit().clone(config.repoUrl, tmpDir, [
62
- '--branch', config.branch,
63
- '--single-branch',
64
- '--depth=1',
65
- ]);
66
- spinner.stop(`${chalk.green('✓')} Fetched latest boilerplate`);
67
- } catch (err) {
68
- spinner.stop(`${chalk.red('✗')} Fetch failed ${chalk.dim(err.message)}`);
69
- fs.rmSync(tmpDir, { recursive: true, force: true });
30
+ if (!status.isClean()) {
31
+ p.log.error(
32
+ 'You have uncommitted changes. Commit or stash them first:\n' +
33
+ chalk.dim(status.files.map(f => ` ${f.working_dir} ${f.path}`).join('\n'))
34
+ );
70
35
  process.exit(1);
71
36
  }
72
37
 
73
- // ── 3. Copy changed files, skipping protected paths ───────────────
74
- const copySpinner = p.spinner();
75
- copySpinner.start('Applying updates...');
38
+ // ── Pick what to sync ─────────────────────────────────────────────
39
+ const choices = buildSyncChoices({ cwd });
76
40
 
77
- let updated = 0;
78
-
79
- try {
80
- updated = syncDirs({ src: tmpDir, dest: cwd });
81
- copySpinner.stop(`${chalk.green('✓')} Applied updates ${chalk.dim(`(${updated} file(s) updated)`)}`);
82
- } catch (err) {
83
- copySpinner.stop(`${chalk.red('✗')} Sync failed ${chalk.dim(err.message)}`);
84
- fs.rmSync(tmpDir, { recursive: true, force: true });
41
+ if (choices.length === 0) {
42
+ p.log.error('No syncable pieces found. Make sure you are in the project root.');
85
43
  process.exit(1);
86
44
  }
87
45
 
88
- // ── 4. Cleanup temp dir ───────────────────────────────────────────
89
- fs.rmSync(tmpDir, { recursive: true, force: true });
46
+ const toSync = await p.multiselect({
47
+ message: 'Which pieces do you want to sync?',
48
+ options: choices,
49
+ required: true,
50
+ });
51
+ if (p.isCancel(toSync)) return cancel();
52
+
53
+ // ── Sync each selected piece ──────────────────────────────────────
54
+ for (const item of toSync) {
55
+ const spinner = p.spinner();
56
+ spinner.start(`Syncing ${chalk.cyan(item.label)} ${chalk.dim(`${item.prefix} ← ${item.branch}`)}`);
57
+
58
+ try {
59
+ await git.raw([
60
+ 'subtree', 'pull',
61
+ '--prefix', item.prefix,
62
+ item.url,
63
+ item.branch,
64
+ '--squash',
65
+ '-m', `chore: sync ${item.prefix} from boilerplate`,
66
+ ]);
67
+
68
+ spinner.stop(`${chalk.green('✓')} ${item.label} ${chalk.dim('synced')}`);
69
+ } catch (err) {
70
+ spinner.stop(`${chalk.red('✗')} ${item.label} ${chalk.dim(err.message)}`);
71
+ p.log.warn(`Sync failed for ${item.label} — resolve manually.`);
72
+ }
73
+ }
90
74
 
91
- p.outro(
92
- chalk.green('Sync complete!') + '\n\n' +
93
- chalk.dim(' Your src/features/ and .env were not touched.\n') +
94
- chalk.dim(' Review changes, then commit.')
95
- );
75
+ p.outro(chalk.green('Sync complete!'));
96
76
  }
97
77
 
98
- // ── Recursively copy src dest, skipping protected paths ─────────
99
- function syncDirs({ src, dest, relative = '' }) {
100
- let count = 0;
101
- const entries = fs.readdirSync(src, { withFileTypes: true });
102
-
103
- for (const entry of entries) {
104
- const relPath = relative ? `${relative}/${entry.name}` : entry.name;
105
-
106
- // Skip protected paths
107
- if (isProtected(relPath)) continue;
108
-
109
- const srcPath = join(src, entry.name);
110
- const destPath = join(dest, entry.name);
111
-
112
- if (entry.isDirectory()) {
113
- fs.mkdirSync(destPath, { recursive: true });
114
- count += syncDirs({ src: srcPath, dest: destPath, relative: relPath });
115
- } else {
116
- const srcContent = fs.readFileSync(srcPath);
117
- const destExists = fs.existsSync(destPath);
118
- const destContent = destExists ? fs.readFileSync(destPath) : null;
78
+ // ── Build list of syncable pieces from what exists on disk ────────
79
+ function buildSyncChoices({ cwd }) {
80
+ const choices = [];
81
+
82
+ // NestJS webapi — read config from scaffold.config.json
83
+ const configPath = join(cwd, 'code', 'webapi', 'scaffold.config.json');
84
+ if (fs.existsSync(configPath)) {
85
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
86
+ const database = NEST_DATABASES.find(d => d.id === config.database);
87
+ if (database) {
88
+ choices.push({
89
+ value: 'webapi',
90
+ label: 'NestJS Web API',
91
+ hint: `branch: ${config.branch}`,
92
+ prefix: 'code/webapi',
93
+ url: config.repoUrl,
94
+ branch: config.branch,
95
+ });
96
+ }
97
+ }
119
98
 
120
- // Only write if file changed
121
- if (!destContent || !srcContent.equals(destContent)) {
122
- fs.writeFileSync(destPath, srcContent);
123
- count++;
99
+ // Dashboard and other fixed repos
100
+ for (const group of Object.values(REPOS)) {
101
+ for (const repo of group) {
102
+ const dir = join(cwd, 'code', repo.id);
103
+ if (fs.existsSync(dir)) {
104
+ choices.push({
105
+ value: repo.id,
106
+ label: repo.label,
107
+ hint: repo.url,
108
+ prefix: `code/${repo.id}`,
109
+ url: repo.url,
110
+ branch: 'master',
111
+ });
124
112
  }
125
113
  }
126
114
  }
127
115
 
128
- return count;
129
- }
130
-
131
- // ── Check if a relative path is protected ─────────────────────────
132
- function isProtected(relPath) {
133
- return PROTECTED.some(p =>
134
- relPath === p || relPath.startsWith(p + '/') || relPath.startsWith(p + path.sep)
135
- );
116
+ return choices;
136
117
  }
137
118
 
138
119
  function cancel() {