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 +1 -1
- package/src/cloner.js +20 -18
- package/src/syncer.js +81 -39
package/package.json
CHANGED
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.
|
|
84
|
-
|
|
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
|
-
// ──
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'
|
|
96
|
-
'.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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.
|
|
44
|
-
const
|
|
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(
|
|
58
|
+
spinner.start(`Fetching latest from ${chalk.dim(config.branch)}...`);
|
|
59
|
+
|
|
58
60
|
try {
|
|
59
|
-
await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
// ──
|
|
66
|
-
const
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
// ──
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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() {
|