create-epinoetics-app 1.0.6 → 1.0.8

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.
@@ -6,6 +6,7 @@ import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { run } from '../src/index.js';
8
8
  import { runAdd } from '../src/adder.js';
9
+ import { runSync } from '../src/syncer.js';
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
@@ -37,8 +38,12 @@ program
37
38
  program
38
39
  .command('add')
39
40
  .description('Add a feature to an existing project (run from code/api-nest)')
40
- .action(() => {
41
- runAdd();
42
- });
41
+ .action(() => runAdd());
42
+
43
+ // ── sync ──────────────────────────────────────────────────────────
44
+ program
45
+ .command('sync')
46
+ .description('Pull latest boilerplate updates (run from code/api-nest)')
47
+ .action(() => runSync());
43
48
 
44
49
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-epinoetics-app",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Scaffold a new project from your boilerplate repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cloner.js CHANGED
@@ -80,8 +80,28 @@ export async function cloneAndMergeNest({ projectName, database, selectedFeature
80
80
  // 6. Write scaffold.config.json
81
81
  writeScaffoldConfig({ dest, database, selectedFeatures, repoUrl });
82
82
 
83
- // 7. Strip .git
84
- fs.rmSync(join(dest, '.git'), { recursive: true, force: true });
83
+ // 7. Strip boilerplate .git and init a fresh one
84
+ await initFreshGit({ dest });
85
+ }
86
+
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
+ }
85
105
  }
86
106
 
87
107
  // ── Write scaffold.config.json ────────────────────────────────────
package/src/syncer.js ADDED
@@ -0,0 +1,141 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { join } from 'path';
4
+ import { simpleGit } from 'simple-git';
5
+ import { NEST_DATABASES } from './repos.js';
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
+
19
+ export async function runSync() {
20
+ console.log('');
21
+ p.intro(
22
+ chalk.bgHex('#7c3aed').white(' create-epinoetics-app sync ') +
23
+ chalk.dim(' pull latest boilerplate updates')
24
+ );
25
+
26
+ const cwd = process.cwd();
27
+
28
+ // ── 1. Read scaffold.config.json ──────────────────────────────────
29
+ const configPath = join(cwd, 'scaffold.config.json');
30
+ if (!fs.existsSync(configPath)) {
31
+ p.log.error(
32
+ 'No scaffold.config.json found.\n' +
33
+ chalk.dim('Make sure you are inside the code/api-nest directory.')
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
39
+ const database = NEST_DATABASES.find(d => d.id === config.database);
40
+
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 });
70
+ process.exit(1);
71
+ }
72
+
73
+ // ── 3. Copy changed files, skipping protected paths ───────────────
74
+ const copySpinner = p.spinner();
75
+ copySpinner.start('Applying updates...');
76
+
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 });
85
+ process.exit(1);
86
+ }
87
+
88
+ // ── 4. Cleanup temp dir ───────────────────────────────────────────
89
+ fs.rmSync(tmpDir, { recursive: true, force: true });
90
+
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
+ );
96
+ }
97
+
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;
119
+
120
+ // Only write if file changed
121
+ if (!destContent || !srcContent.equals(destContent)) {
122
+ fs.writeFileSync(destPath, srcContent);
123
+ count++;
124
+ }
125
+ }
126
+ }
127
+
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
+ );
136
+ }
137
+
138
+ function cancel() {
139
+ p.cancel('Cancelled.');
140
+ process.exit(0);
141
+ }