create-epinoetics-app 1.0.7 → 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.
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.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,26 +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. Protect src/features/ from future syncs
84
- writeGitExclude({ dest });
85
-
86
- // NOTE: .git is intentionally kept so `sync` can pull updates later
83
+ // 7. Strip boilerplate .git and init a fresh one
84
+ await initFreshGit({ dest });
87
85
  }
88
86
 
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`);
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
+ }
103
105
  }
104
106
 
105
107
  // ── Write scaffold.config.json ────────────────────────────────────
package/src/syncer.js CHANGED
@@ -4,6 +4,17 @@ import { join } from 'path';
4
4
  import { simpleGit } from 'simple-git';
5
5
  import { NEST_DATABASES } 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
+ ];
7
18
 
8
19
  export async function runSync() {
9
20
  console.log('');
@@ -32,65 +43,96 @@ export async function runSync() {
32
43
  `${chalk.dim('branch '.padEnd(14))} ${chalk.cyan(config.branch)}`,
33
44
  `${chalk.dim('features'.padEnd(14))} ${config.features.length ? config.features.map(f => chalk.cyan(f)).join(chalk.dim(', ')) : chalk.dim('none')}`,
34
45
  ].join('\n'),
35
- 'Syncing'
46
+ 'Syncing from boilerplate'
36
47
  );
37
48
 
38
49
  const confirm = await p.confirm({
39
- message: `Pull latest changes from ${chalk.cyan(config.branch)}?`,
50
+ message: `Pull latest from ${chalk.cyan(config.branch)}? Your code in src/features/ will not be touched.`,
40
51
  });
41
52
  if (p.isCancel(confirm) || !confirm) return cancel();
42
53
 
43
- // ── 2. Make sure .git exists ──────────────────────────────────────
44
- const gitDir = join(cwd, '.git');
45
- if (!fs.existsSync(gitDir)) {
46
- p.log.error(
47
- '.git directory not found.\n' +
48
- chalk.dim('Re-scaffold this project to enable sync.')
49
- );
50
- process.exit(1);
51
- }
52
-
53
- // ── 3. Stash any local changes ────────────────────────────────────
54
- const git = simpleGit(cwd);
54
+ // ── 2. Clone latest into temp dir ─────────────────────────────────
55
+ const tmpDir = join(os.tmpdir(), `epinoetics-sync-${Date.now()}`);
55
56
  const spinner = p.spinner();
56
57
 
57
- spinner.start('Stashing local changes...');
58
+ spinner.start(`Fetching latest from ${chalk.dim(config.branch)}...`);
59
+
58
60
  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'));
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);
63
71
  }
64
72
 
65
- // ── 4. Pull latest from the db branch ────────────────────────────
66
- const pullSpinner = p.spinner();
67
- pullSpinner.start(`Pulling latest from ${chalk.cyan(config.branch)}...`);
73
+ // ── 3. Copy changed files, skipping protected paths ───────────────
74
+ const copySpinner = p.spinner();
75
+ copySpinner.start('Applying updates...');
76
+
77
+ let updated = 0;
68
78
 
69
79
  try {
70
- await git.pull('origin', config.branch, ['--rebase']);
71
- pullSpinner.stop(`${chalk.green('✓')} Pulled latest changes`);
80
+ updated = syncDirs({ src: tmpDir, dest: cwd });
81
+ copySpinner.stop(`${chalk.green('✓')} Applied updates ${chalk.dim(`(${updated} file(s) updated)`)}`);
72
82
  } 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(() => {});
83
+ copySpinner.stop(`${chalk.red('✗')} Sync failed ${chalk.dim(err.message)}`);
84
+ fs.rmSync(tmpDir, { recursive: true, force: true });
76
85
  process.exit(1);
77
86
  }
78
87
 
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...');
84
- try {
85
- await git.stash(['pop']);
86
- restoreSpinner.stop(`${chalk.green('✓')} Local changes restored`);
87
- } 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.`);
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
+ }
90
125
  }
91
126
  }
92
127
 
93
- p.outro(chalk.green('Sync complete!'));
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
+ );
94
136
  }
95
137
 
96
138
  function cancel() {