create-epinoetics-app 1.0.8 → 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 +1 -1
- package/src/cloner.js +53 -45
- package/src/scaffold.js +13 -20
- package/src/syncer.js +81 -100
package/package.json
CHANGED
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
|
-
// ──
|
|
8
|
-
export async function
|
|
7
|
+
// ── Initialize the root project repo ─────────────────────────────
|
|
8
|
+
export async function initRootRepo({ projectName }) {
|
|
9
9
|
const projectDir = resolve(process.cwd(), projectName);
|
|
10
|
-
|
|
10
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
28
|
+
const prefix = `code/${repo.id}`;
|
|
16
29
|
const spinner = p.spinner();
|
|
17
30
|
|
|
18
|
-
spinner.start(`
|
|
31
|
+
spinner.start(`Adding ${chalk.cyan(repo.label)} ${chalk.dim(`→ ${prefix}`)}`);
|
|
19
32
|
|
|
20
33
|
try {
|
|
21
|
-
await
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
45
|
+
throw new Error(`Failed to add subtree ${repo.label}: ${err.message}`);
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
// ──
|
|
32
|
-
export async function
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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.
|
|
56
|
+
// 1. Add NestJS as subtree
|
|
39
57
|
const spinner = p.spinner();
|
|
40
|
-
spinner.start(`
|
|
58
|
+
spinner.start(`Adding ${chalk.cyan('NestJS')} ${chalk.dim(`branch: ${database.branch}`)}`);
|
|
41
59
|
|
|
42
60
|
try {
|
|
43
|
-
await
|
|
44
|
-
|
|
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
|
|
72
|
+
throw new Error(`Failed to add NestJS subtree: ${err.message}`);
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
// 2. Remove unselected feature folders
|
|
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
|
|
92
|
+
// 3. Prune schema
|
|
68
93
|
if (database.id === 'drizzle') {
|
|
69
94
|
pruneDrizzleSchema({ dest, toRemove });
|
|
70
95
|
} else if (database.id === 'prisma') {
|
|
@@ -74,34 +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
|
|
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.
|
|
84
|
-
await
|
|
85
|
-
|
|
108
|
+
// 7. Commit the pruning changes
|
|
109
|
+
await git.add('.');
|
|
110
|
+
await git.commit('chore: prune unselected features');
|
|
86
111
|
|
|
87
|
-
|
|
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
|
-
}
|
|
112
|
+
p.log.step(`Committed pruned scaffold`);
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
// ── Write scaffold.config.json ────────────────────────────────────
|
package/src/scaffold.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { join
|
|
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 === '
|
|
26
|
-
## NestJS
|
|
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/
|
|
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 === '
|
|
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}
|
|
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,20 +1,9 @@
|
|
|
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
|
-
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
7
|
|
|
19
8
|
export async function runSync() {
|
|
20
9
|
console.log('');
|
|
@@ -23,116 +12,108 @@ export async function runSync() {
|
|
|
23
12
|
chalk.dim(' pull latest boilerplate updates')
|
|
24
13
|
);
|
|
25
14
|
|
|
15
|
+
// Must be run from project root (where .git is)
|
|
26
16
|
const cwd = process.cwd();
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
const configPath = join(cwd, 'scaffold.config.json');
|
|
30
|
-
if (!fs.existsSync(configPath)) {
|
|
18
|
+
if (!fs.existsSync(join(cwd, '.git'))) {
|
|
31
19
|
p.log.error(
|
|
32
|
-
'No
|
|
33
|
-
chalk.dim('
|
|
20
|
+
'No .git found.\n' +
|
|
21
|
+
chalk.dim('Run this from your project root, not inside code/webapi.')
|
|
34
22
|
);
|
|
35
23
|
process.exit(1);
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
const
|
|
26
|
+
// ── Check for uncommitted changes ─────────────────────────────────
|
|
27
|
+
const git = simpleGit(cwd);
|
|
28
|
+
const status = await git.status();
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 });
|
|
30
|
+
if (!status.isClean()) {
|
|
31
|
+
p.log.error(
|
|
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'))
|
|
34
|
+
);
|
|
70
35
|
process.exit(1);
|
|
71
36
|
}
|
|
72
37
|
|
|
73
|
-
// ──
|
|
74
|
-
const
|
|
75
|
-
copySpinner.start('Applying updates...');
|
|
38
|
+
// ── Pick what to sync ─────────────────────────────────────────────
|
|
39
|
+
const choices = buildSyncChoices({ cwd });
|
|
76
40
|
|
|
77
|
-
|
|
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 });
|
|
41
|
+
if (choices.length === 0) {
|
|
42
|
+
p.log.error('No syncable pieces found. Make sure you are in the project root.');
|
|
85
43
|
process.exit(1);
|
|
86
44
|
}
|
|
87
45
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
|
|
58
|
+
try {
|
|
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')}`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
spinner.stop(`${chalk.red('✗')} ${item.label} ${chalk.dim(err.message)}`);
|
|
71
|
+
p.log.warn(`Sync failed for ${item.label} — resolve manually.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
90
74
|
|
|
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
|
-
);
|
|
75
|
+
p.outro(chalk.green('Sync complete!'));
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
// ──
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const destContent = destExists ? fs.readFileSync(destPath) : null;
|
|
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
|
+
}
|
|
119
98
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
112
|
}
|
|
125
113
|
}
|
|
126
114
|
}
|
|
127
115
|
|
|
128
|
-
return
|
|
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
|
-
);
|
|
116
|
+
return choices;
|
|
136
117
|
}
|
|
137
118
|
|
|
138
119
|
function cancel() {
|