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.
- package/bin/create-epinoetics-app.js +8 -3
- package/package.json +1 -1
- package/src/cloner.js +22 -2
- package/src/syncer.js +141 -0
|
@@ -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
|
-
|
|
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
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
|
-
|
|
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
|
+
}
|