create-epinoetics-app 1.0.7 → 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 -43
- package/src/scaffold.js +13 -20
- package/src/syncer.js +81 -58
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,32 +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
|
-
|
|
85
|
-
|
|
86
|
-
// NOTE: .git is intentionally kept so `sync` can pull updates later
|
|
87
|
-
}
|
|
108
|
+
// 7. Commit the pruning changes
|
|
109
|
+
await git.add('.');
|
|
110
|
+
await git.commit('chore: prune unselected features');
|
|
88
111
|
|
|
89
|
-
|
|
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`);
|
|
112
|
+
p.log.step(`Committed pruned scaffold`);
|
|
103
113
|
}
|
|
104
114
|
|
|
105
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,8 +1,8 @@
|
|
|
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
7
|
|
|
8
8
|
export async function runSync() {
|
|
@@ -12,87 +12,110 @@ export async function runSync() {
|
|
|
12
12
|
chalk.dim(' pull latest boilerplate updates')
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
+
// Must be run from project root (where .git is)
|
|
15
16
|
const cwd = process.cwd();
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
const configPath = join(cwd, 'scaffold.config.json');
|
|
19
|
-
if (!fs.existsSync(configPath)) {
|
|
18
|
+
if (!fs.existsSync(join(cwd, '.git'))) {
|
|
20
19
|
p.log.error(
|
|
21
|
-
'No
|
|
22
|
-
chalk.dim('
|
|
20
|
+
'No .git found.\n' +
|
|
21
|
+
chalk.dim('Run this from your project root, not inside code/webapi.')
|
|
23
22
|
);
|
|
24
23
|
process.exit(1);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
const
|
|
26
|
+
// ── Check for uncommitted changes ─────────────────────────────────
|
|
27
|
+
const git = simpleGit(cwd);
|
|
28
|
+
const status = await git.status();
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
[
|
|
32
|
-
`${chalk.dim('branch '.padEnd(14))} ${chalk.cyan(config.branch)}`,
|
|
33
|
-
`${chalk.dim('features'.padEnd(14))} ${config.features.length ? config.features.map(f => chalk.cyan(f)).join(chalk.dim(', ')) : chalk.dim('none')}`,
|
|
34
|
-
].join('\n'),
|
|
35
|
-
'Syncing'
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const confirm = await p.confirm({
|
|
39
|
-
message: `Pull latest changes from ${chalk.cyan(config.branch)}?`,
|
|
40
|
-
});
|
|
41
|
-
if (p.isCancel(confirm) || !confirm) return cancel();
|
|
42
|
-
|
|
43
|
-
// ── 2. Make sure .git exists ──────────────────────────────────────
|
|
44
|
-
const gitDir = join(cwd, '.git');
|
|
45
|
-
if (!fs.existsSync(gitDir)) {
|
|
30
|
+
if (!status.isClean()) {
|
|
46
31
|
p.log.error(
|
|
47
|
-
'.
|
|
48
|
-
chalk.dim(
|
|
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'))
|
|
49
34
|
);
|
|
50
35
|
process.exit(1);
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
// ──
|
|
54
|
-
const
|
|
55
|
-
const spinner = p.spinner();
|
|
56
|
-
|
|
57
|
-
spinner.start('Stashing local changes...');
|
|
58
|
-
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'));
|
|
63
|
-
}
|
|
38
|
+
// ── Pick what to sync ─────────────────────────────────────────────
|
|
39
|
+
const choices = buildSyncChoices({ cwd });
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pullSpinner.start(`Pulling latest from ${chalk.cyan(config.branch)}...`);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
await git.pull('origin', config.branch, ['--rebase']);
|
|
71
|
-
pullSpinner.stop(`${chalk.green('✓')} Pulled latest changes`);
|
|
72
|
-
} 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(() => {});
|
|
41
|
+
if (choices.length === 0) {
|
|
42
|
+
p.log.error('No syncable pieces found. Make sure you are in the project root.');
|
|
76
43
|
process.exit(1);
|
|
77
44
|
}
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
|
|
84
58
|
try {
|
|
85
|
-
await git.
|
|
86
|
-
|
|
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')}`);
|
|
87
69
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
p.log.warn(`
|
|
70
|
+
spinner.stop(`${chalk.red('✗')} ${item.label} ${chalk.dim(err.message)}`);
|
|
71
|
+
p.log.warn(`Sync failed for ${item.label} — resolve manually.`);
|
|
90
72
|
}
|
|
91
73
|
}
|
|
92
74
|
|
|
93
75
|
p.outro(chalk.green('Sync complete!'));
|
|
94
76
|
}
|
|
95
77
|
|
|
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
|
+
}
|
|
98
|
+
|
|
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
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return choices;
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
function cancel() {
|
|
97
120
|
p.cancel('Cancelled.');
|
|
98
121
|
process.exit(0);
|