create-epinoetics-app 1.0.7 → 1.0.9

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.7",
3
+ "version": "1.0.9",
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,32 +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. Protect src/features/ from future syncs
84
- writeGitExclude({ dest });
85
-
86
- // NOTE: .git is intentionally kept so `sync` can pull updates later
87
- }
108
+ // 7. Commit the pruning changes
109
+ await git.add('.');
110
+ await git.commit('chore: prune unselected features');
88
111
 
89
- // ── Write .git/info/exclude to protect key dirs from sync ─────────
90
- function writeGitExclude({ dest }) {
91
- const excludePath = join(dest, '.git', 'info', 'exclude');
92
- const lines = [
93
- '# Added by create-epinoetics-app — do not edit',
94
- 'src/features/',
95
- 'scaffold.config.json',
96
- '.env',
97
- '.env.local',
98
- ].join('\n');
99
-
100
- fs.mkdirSync(join(dest, '.git', 'info'), { recursive: true });
101
- fs.writeFileSync(excludePath, lines, 'utf8');
102
- p.log.step(`Protected src/features/ from sync`);
112
+ p.log.step(`Committed pruned scaffold`);
103
113
  }
104
114
 
105
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,8 +1,8 @@
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
7
 
8
8
  export async function runSync() {
@@ -12,87 +12,110 @@ export async function runSync() {
12
12
  chalk.dim(' pull latest boilerplate updates')
13
13
  );
14
14
 
15
+ // Must be run from project root (where .git is)
15
16
  const cwd = process.cwd();
16
17
 
17
- // ── 1. Read scaffold.config.json ──────────────────────────────────
18
- const configPath = join(cwd, 'scaffold.config.json');
19
- if (!fs.existsSync(configPath)) {
18
+ if (!fs.existsSync(join(cwd, '.git'))) {
20
19
  p.log.error(
21
- 'No scaffold.config.json found.\n' +
22
- 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.')
23
22
  );
24
23
  process.exit(1);
25
24
  }
26
25
 
27
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
28
- 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();
29
29
 
30
- p.note(
31
- [
32
- `${chalk.dim('branch '.padEnd(14))} ${chalk.cyan(config.branch)}`,
33
- `${chalk.dim('features'.padEnd(14))} ${config.features.length ? config.features.map(f => chalk.cyan(f)).join(chalk.dim(', ')) : chalk.dim('none')}`,
34
- ].join('\n'),
35
- 'Syncing'
36
- );
37
-
38
- const confirm = await p.confirm({
39
- message: `Pull latest changes from ${chalk.cyan(config.branch)}?`,
40
- });
41
- if (p.isCancel(confirm) || !confirm) return cancel();
42
-
43
- // ── 2. Make sure .git exists ──────────────────────────────────────
44
- const gitDir = join(cwd, '.git');
45
- if (!fs.existsSync(gitDir)) {
30
+ if (!status.isClean()) {
46
31
  p.log.error(
47
- '.git directory not found.\n' +
48
- chalk.dim('Re-scaffold this project to enable sync.')
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'))
49
34
  );
50
35
  process.exit(1);
51
36
  }
52
37
 
53
- // ── 3. Stash any local changes ────────────────────────────────────
54
- const git = simpleGit(cwd);
55
- const spinner = p.spinner();
56
-
57
- spinner.start('Stashing local changes...');
58
- try {
59
- await git.stash(['push', '--include-untracked', '-m', 'create-epinoetics-app sync']);
60
- spinner.stop(`${chalk.green('✓')} Local changes stashed`);
61
- } catch {
62
- spinner.stop(chalk.dim('Nothing to stash'));
63
- }
38
+ // ── Pick what to sync ─────────────────────────────────────────────
39
+ const choices = buildSyncChoices({ cwd });
64
40
 
65
- // ── 4. Pull latest from the db branch ────────────────────────────
66
- const pullSpinner = p.spinner();
67
- pullSpinner.start(`Pulling latest from ${chalk.cyan(config.branch)}...`);
68
-
69
- try {
70
- await git.pull('origin', config.branch, ['--rebase']);
71
- pullSpinner.stop(`${chalk.green('✓')} Pulled latest changes`);
72
- } catch (err) {
73
- pullSpinner.stop(`${chalk.red('✗')} Pull failed ${chalk.dim(err.message)}`);
74
- p.log.warn('Restoring your stashed changes...');
75
- await git.stash(['pop']).catch(() => {});
41
+ if (choices.length === 0) {
42
+ p.log.error('No syncable pieces found. Make sure you are in the project root.');
76
43
  process.exit(1);
77
44
  }
78
45
 
79
- // ── 5. Restore stashed changes ────────────────────────────────────
80
- const stashList = await git.stashList();
81
- if (stashList.total > 0) {
82
- const restoreSpinner = p.spinner();
83
- restoreSpinner.start('Restoring your local changes...');
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
+
84
58
  try {
85
- await git.stash(['pop']);
86
- restoreSpinner.stop(`${chalk.green('')} Local changes restored`);
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')}`);
87
69
  } catch (err) {
88
- restoreSpinner.stop(`${chalk.yellow('')} Conflict restoring stash — resolve manually`);
89
- p.log.warn(`Run ${chalk.cyan('git stash pop')} to restore your changes.`);
70
+ spinner.stop(`${chalk.red('')} ${item.label} ${chalk.dim(err.message)}`);
71
+ p.log.warn(`Sync failed for ${item.label} resolve manually.`);
90
72
  }
91
73
  }
92
74
 
93
75
  p.outro(chalk.green('Sync complete!'));
94
76
  }
95
77
 
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
+ }
98
+
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
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ return choices;
117
+ }
118
+
96
119
  function cancel() {
97
120
  p.cancel('Cancelled.');
98
121
  process.exit(0);