create-epinoetics-app 1.0.5 → 1.0.7
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 +29 -12
- package/package.json +1 -1
- package/src/adder.js +245 -0
- package/src/cloner.js +45 -19
- package/src/index.js +0 -5
- package/src/syncer.js +99 -0
|
@@ -5,6 +5,8 @@ import { readFileSync } from 'fs';
|
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { run } from '../src/index.js';
|
|
8
|
+
import { runAdd } from '../src/adder.js';
|
|
9
|
+
import { runSync } from '../src/syncer.js';
|
|
8
10
|
|
|
9
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
|
|
@@ -12,21 +14,36 @@ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8
|
|
|
12
14
|
program
|
|
13
15
|
.name('create-epinoetics-app')
|
|
14
16
|
.description('Scaffold a new project from your boilerplate repos')
|
|
15
|
-
.version(pkg.version)
|
|
16
|
-
|
|
17
|
+
.version(pkg.version);
|
|
18
|
+
|
|
19
|
+
// ── create (default) ──────────────────────────────────────────────
|
|
20
|
+
program
|
|
21
|
+
.command('create [project-name]', { isDefault: true })
|
|
22
|
+
.description('Scaffold a new project')
|
|
17
23
|
.option('-b, --backend <id>', 'Backend to use: api-nest | api-dotnet')
|
|
18
24
|
.option('-f, --frontend <id>', 'Frontend to use: web-next | web-astro (when available)')
|
|
19
25
|
.option('--dry-run', 'Preview without cloning anything')
|
|
20
26
|
.option('--yes', 'Skip confirmation prompt')
|
|
21
|
-
.
|
|
27
|
+
.action((projectName, opts) => {
|
|
28
|
+
run({
|
|
29
|
+
projectName,
|
|
30
|
+
backend: opts.backend,
|
|
31
|
+
frontend: opts.frontend,
|
|
32
|
+
dryRun: opts.dryRun ?? false,
|
|
33
|
+
yes: opts.yes ?? false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
// ── add ───────────────────────────────────────────────────────────
|
|
38
|
+
program
|
|
39
|
+
.command('add')
|
|
40
|
+
.description('Add a feature to an existing project (run from code/api-nest)')
|
|
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());
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
projectName,
|
|
28
|
-
backend: opts.backend,
|
|
29
|
-
frontend: opts.frontend,
|
|
30
|
-
dryRun: opts.dryRun ?? false,
|
|
31
|
-
yes: opts.yes ?? false,
|
|
32
|
-
});
|
|
49
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
package/src/adder.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { simpleGit } from 'simple-git';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { NEST_FEATURES, NEST_DATABASES } from './repos.js';
|
|
8
|
+
|
|
9
|
+
export async function runAdd() {
|
|
10
|
+
console.log('');
|
|
11
|
+
p.intro(
|
|
12
|
+
chalk.bgHex('#7c3aed').white(' create-epinoetics-app add ') +
|
|
13
|
+
chalk.dim(' add a feature to your project')
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
|
|
18
|
+
// ── 1. Read scaffold.config.json ──────────────────────────────────
|
|
19
|
+
const configPath = join(cwd, 'scaffold.config.json');
|
|
20
|
+
if (!fs.existsSync(configPath)) {
|
|
21
|
+
p.log.error(
|
|
22
|
+
'No scaffold.config.json found.\n' +
|
|
23
|
+
chalk.dim('Make sure you are inside the code/api-nest directory.')
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
29
|
+
const database = NEST_DATABASES.find(d => d.id === config.database);
|
|
30
|
+
|
|
31
|
+
if (!database) {
|
|
32
|
+
p.log.error(`Unknown database "${config.database}" in scaffold.config.json.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 2. Show only features not already installed ───────────────────
|
|
37
|
+
const installedIds = new Set(config.features ?? []);
|
|
38
|
+
const available = NEST_FEATURES.filter(f => !installedIds.has(f.id));
|
|
39
|
+
|
|
40
|
+
if (available.length === 0) {
|
|
41
|
+
p.outro(chalk.green('All available features are already installed!'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
p.note(
|
|
46
|
+
[
|
|
47
|
+
`${chalk.dim('database '.padEnd(14))} ${chalk.cyan(database.label)}`,
|
|
48
|
+
`${chalk.dim('installed'.padEnd(14))} ${config.features.length ? config.features.map(f => chalk.cyan(f)).join(chalk.dim(', ')) : chalk.dim('none')}`,
|
|
49
|
+
].join('\n'),
|
|
50
|
+
'Current project'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// ── 3. Pick features to add ───────────────────────────────────────
|
|
54
|
+
const featureIds = await p.multiselect({
|
|
55
|
+
message: 'Which features do you want to add?',
|
|
56
|
+
options: available.map(f => ({
|
|
57
|
+
value: f.id,
|
|
58
|
+
label: f.label,
|
|
59
|
+
hint: f.hint,
|
|
60
|
+
})),
|
|
61
|
+
required: true,
|
|
62
|
+
});
|
|
63
|
+
if (p.isCancel(featureIds)) return cancel();
|
|
64
|
+
|
|
65
|
+
const toAdd = NEST_FEATURES.filter(f => featureIds.includes(f.id));
|
|
66
|
+
|
|
67
|
+
// ── 4. Confirm ────────────────────────────────────────────────────
|
|
68
|
+
const confirm = await p.confirm({
|
|
69
|
+
message: `Add ${toAdd.map(f => chalk.cyan(f.label)).join(', ')} to this project?`,
|
|
70
|
+
});
|
|
71
|
+
if (p.isCancel(confirm) || !confirm) return cancel();
|
|
72
|
+
|
|
73
|
+
// ── 5. Clone the db branch into a temp directory ──────────────────
|
|
74
|
+
const tmpDir = join(os.tmpdir(), `epinoetics-add-${Date.now()}`);
|
|
75
|
+
const spinner = p.spinner();
|
|
76
|
+
|
|
77
|
+
spinner.start(`Fetching features from ${chalk.dim(database.branch)}...`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await simpleGit().clone(config.repoUrl, tmpDir, ['--branch', database.branch, '--single-branch', '--depth=1']);
|
|
81
|
+
spinner.stop(`${chalk.green('✓')} Fetched boilerplate`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
spinner.stop(`${chalk.red('✗')} Clone failed ${chalk.dim(err.message)}`);
|
|
84
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── 6. Copy each feature folder into the project ──────────────────
|
|
89
|
+
for (const feature of toAdd) {
|
|
90
|
+
const srcDir = join(tmpDir, 'src', 'features', feature.id);
|
|
91
|
+
const destDir = join(cwd, 'src', 'features', feature.id);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(srcDir)) {
|
|
94
|
+
p.log.warn(`Feature "${feature.id}" not found in repo — skipping.`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (fs.existsSync(destDir)) {
|
|
99
|
+
p.log.warn(`Feature "${feature.id}" already exists locally — skipping.`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.cpSync(srcDir, destDir, { recursive: true });
|
|
104
|
+
p.log.step(`${chalk.green('✓')} Copied ${chalk.cyan(feature.label)} ${chalk.dim(`→ src/features/${feature.id}`)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 7. Patch schema ───────────────────────────────────────────────
|
|
108
|
+
if (database.id === 'drizzle') {
|
|
109
|
+
addDrizzleExports({ cwd, tmpDir, toAdd });
|
|
110
|
+
} else if (database.id === 'prisma') {
|
|
111
|
+
addPrismaModels({ cwd, tmpDir, toAdd });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 8. Patch app.module.ts ────────────────────────────────────────
|
|
115
|
+
addAppModuleImports({ cwd, toAdd });
|
|
116
|
+
|
|
117
|
+
// ── 9. Cleanup temp dir ───────────────────────────────────────────
|
|
118
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
119
|
+
|
|
120
|
+
// ── 10. Update scaffold.config.json ──────────────────────────────
|
|
121
|
+
config.features = [...new Set([...config.features, ...featureIds])];
|
|
122
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
123
|
+
p.log.step(`Updated scaffold.config.json`);
|
|
124
|
+
|
|
125
|
+
p.outro(
|
|
126
|
+
chalk.green('Done!') + chalk.dim(' Run your migrations and you\'re good to go.')
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Drizzle: append export lines to schema.ts ─────────────────────
|
|
131
|
+
function addDrizzleExports({ cwd, tmpDir, toAdd }) {
|
|
132
|
+
const candidates = [
|
|
133
|
+
join(cwd, 'src', 'database', 'schema.ts'),
|
|
134
|
+
join(cwd, 'src', 'db', 'schema.ts'),
|
|
135
|
+
join(cwd, 'src', 'schema.ts'),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const schemaPath = candidates.find(f => fs.existsSync(f));
|
|
139
|
+
if (!schemaPath) {
|
|
140
|
+
p.log.warn('Could not find schema.ts — add exports manually.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get the export lines from the tmp clone's schema.ts
|
|
145
|
+
const tmpCandidates = [
|
|
146
|
+
join(tmpDir, 'src', 'database', 'schema.ts'),
|
|
147
|
+
join(tmpDir, 'src', 'db', 'schema.ts'),
|
|
148
|
+
join(tmpDir, 'src', 'schema.ts'),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const tmpSchemaPath = tmpCandidates.find(f => fs.existsSync(f));
|
|
152
|
+
if (!tmpSchemaPath) return;
|
|
153
|
+
|
|
154
|
+
const tmpContent = fs.readFileSync(tmpSchemaPath, 'utf8');
|
|
155
|
+
let additions = '';
|
|
156
|
+
|
|
157
|
+
for (const feature of toAdd) {
|
|
158
|
+
const pattern = new RegExp(
|
|
159
|
+
`^export \\* from ['"].*features\\/${feature.id}\\/.*['"];?$`,
|
|
160
|
+
'gm'
|
|
161
|
+
);
|
|
162
|
+
const matches = tmpContent.match(pattern);
|
|
163
|
+
if (matches) additions += matches.join('\n') + '\n';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (additions) {
|
|
167
|
+
fs.appendFileSync(schemaPath, '\n' + additions, 'utf8');
|
|
168
|
+
p.log.step(`Updated schema.ts ${chalk.dim(`(added ${toAdd.length} export line(s))`)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Prisma: append model blocks to schema.prisma ──────────────────
|
|
173
|
+
function addPrismaModels({ cwd, tmpDir, toAdd }) {
|
|
174
|
+
const candidates = [
|
|
175
|
+
join(cwd, 'prisma', 'schema.prisma'),
|
|
176
|
+
join(cwd, 'schema.prisma'),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const schemaPath = candidates.find(f => fs.existsSync(f));
|
|
180
|
+
if (!schemaPath) {
|
|
181
|
+
p.log.warn('Could not find schema.prisma — add models manually.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const tmpCandidates = [
|
|
186
|
+
join(tmpDir, 'prisma', 'schema.prisma'),
|
|
187
|
+
join(tmpDir, 'schema.prisma'),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const tmpSchemaPath = tmpCandidates.find(f => fs.existsSync(f));
|
|
191
|
+
if (!tmpSchemaPath) return;
|
|
192
|
+
|
|
193
|
+
const tmpContent = fs.readFileSync(tmpSchemaPath, 'utf8');
|
|
194
|
+
let additions = '';
|
|
195
|
+
|
|
196
|
+
for (const feature of toAdd) {
|
|
197
|
+
const pattern = new RegExp(
|
|
198
|
+
`\\/\\/ @feature:${feature.id}\\n[\\s\\S]*?\\/\\/ @endfeature:${feature.id}`,
|
|
199
|
+
'g'
|
|
200
|
+
);
|
|
201
|
+
const matches = tmpContent.match(pattern);
|
|
202
|
+
if (matches) additions += '\n' + matches.join('\n') + '\n';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (additions) {
|
|
206
|
+
fs.appendFileSync(schemaPath, additions, 'utf8');
|
|
207
|
+
p.log.step(`Updated schema.prisma ${chalk.dim(`(added ${toAdd.length} model block(s))`)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Add import + module to app.module.ts ──────────────────────────
|
|
212
|
+
function addAppModuleImports({ cwd, toAdd }) {
|
|
213
|
+
const appModulePath = join(cwd, 'src', 'app.module.ts');
|
|
214
|
+
if (!fs.existsSync(appModulePath)) {
|
|
215
|
+
p.log.warn('Could not find app.module.ts — add imports manually.');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let content = fs.readFileSync(appModulePath, 'utf8');
|
|
220
|
+
|
|
221
|
+
for (const feature of toAdd) {
|
|
222
|
+
const moduleName = feature.id.charAt(0).toUpperCase() + feature.id.slice(1) + 'Module';
|
|
223
|
+
|
|
224
|
+
// Add import line after the last existing import
|
|
225
|
+
const importLine = `import { ${moduleName} } from './features/${feature.id}/${feature.id}.module';`;
|
|
226
|
+
content = content.replace(
|
|
227
|
+
/(import [^\n]+\n)(?!import)/,
|
|
228
|
+
`$1${importLine}\n`
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Add module to imports array before ConfigModule
|
|
232
|
+
content = content.replace(
|
|
233
|
+
/(\/\/ internal\n)/,
|
|
234
|
+
`$1 ${moduleName},\n`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fs.writeFileSync(appModulePath, content, 'utf8');
|
|
239
|
+
p.log.step(`Updated app.module.ts ${chalk.dim(`(added ${toAdd.length} module(s))`)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cancel() {
|
|
243
|
+
p.cancel('Cancelled.');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
package/src/cloner.js
CHANGED
|
@@ -64,21 +64,61 @@ export async function cloneAndMergeNest({ projectName, database, selectedFeature
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// 3. Prune schema — Drizzle or Prisma
|
|
67
|
+
// 3. Prune schema — Drizzle or Prisma
|
|
68
68
|
if (database.id === 'drizzle') {
|
|
69
69
|
pruneDrizzleSchema({ dest, toRemove });
|
|
70
70
|
} else if (database.id === 'prisma') {
|
|
71
71
|
prunePrismaSchema({ dest, toRemove });
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// 4. Prune app.module.ts
|
|
74
|
+
// 4. Prune app.module.ts
|
|
75
75
|
pruneAppModule({ dest, toRemove });
|
|
76
76
|
|
|
77
77
|
// 5. Remove migrations directory
|
|
78
78
|
pruneMigrations({ dest, database });
|
|
79
79
|
|
|
80
|
-
// 6.
|
|
81
|
-
|
|
80
|
+
// 6. Write scaffold.config.json
|
|
81
|
+
writeScaffoldConfig({ dest, database, selectedFeatures, repoUrl });
|
|
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
|
|
87
|
+
}
|
|
88
|
+
|
|
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`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Write scaffold.config.json ────────────────────────────────────
|
|
106
|
+
function writeScaffoldConfig({ dest, database, selectedFeatures, repoUrl }) {
|
|
107
|
+
const config = {
|
|
108
|
+
database: database.id,
|
|
109
|
+
branch: database.branch,
|
|
110
|
+
features: selectedFeatures.map(f => f.id),
|
|
111
|
+
repoUrl,
|
|
112
|
+
createdAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
join(dest, 'scaffold.config.json'),
|
|
117
|
+
JSON.stringify(config, null, 2),
|
|
118
|
+
'utf8'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
p.log.step(`Written scaffold.config.json`);
|
|
82
122
|
}
|
|
83
123
|
|
|
84
124
|
// ── Drizzle: remove export * lines from schema.ts ─────────────────
|
|
@@ -101,7 +141,6 @@ function pruneDrizzleSchema({ dest, toRemove }) {
|
|
|
101
141
|
const before = content;
|
|
102
142
|
|
|
103
143
|
for (const feature of toRemove) {
|
|
104
|
-
// Removes: export * from '../features/posts/post.schema';
|
|
105
144
|
const pattern = new RegExp(
|
|
106
145
|
`^export \\* from ['"].*features\\/${feature.id}\\/.*['"];?\\n?`,
|
|
107
146
|
'gm'
|
|
@@ -116,14 +155,6 @@ function pruneDrizzleSchema({ dest, toRemove }) {
|
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
// ── Prisma: remove model blocks from schema.prisma ────────────────
|
|
119
|
-
//
|
|
120
|
-
// Convention in schema.prisma:
|
|
121
|
-
//
|
|
122
|
-
// // @feature:posts
|
|
123
|
-
// model Post { ... }
|
|
124
|
-
// model PostTag { ... }
|
|
125
|
-
// // @end:posts
|
|
126
|
-
//
|
|
127
158
|
function prunePrismaSchema({ dest, toRemove }) {
|
|
128
159
|
if (toRemove.length === 0) return;
|
|
129
160
|
|
|
@@ -142,9 +173,8 @@ function prunePrismaSchema({ dest, toRemove }) {
|
|
|
142
173
|
const before = content;
|
|
143
174
|
|
|
144
175
|
for (const feature of toRemove) {
|
|
145
|
-
// Removes everything between // @feature:posts and // @end:posts (inclusive)
|
|
146
176
|
const pattern = new RegExp(
|
|
147
|
-
`\\/\\/ @feature:${feature.id}\\n[\\s\\S]*?\\/\\/ @
|
|
177
|
+
`\\/\\/ @feature:${feature.id}\\n[\\s\\S]*?\\/\\/ @endfeature:${feature.id}\\n?`,
|
|
148
178
|
'g'
|
|
149
179
|
);
|
|
150
180
|
content = content.replace(pattern, '');
|
|
@@ -170,18 +200,14 @@ function pruneAppModule({ dest, toRemove }) {
|
|
|
170
200
|
const before = content;
|
|
171
201
|
|
|
172
202
|
for (const feature of toRemove) {
|
|
173
|
-
// e.g. posts → PostsModule, seo → SeoModule
|
|
174
203
|
const moduleName = feature.id.charAt(0).toUpperCase() + feature.id.slice(1) + 'Module';
|
|
175
204
|
|
|
176
|
-
// Remove the import line:
|
|
177
|
-
// import { PostsModule } from './features/posts/posts.module';
|
|
178
205
|
const importPattern = new RegExp(
|
|
179
206
|
`^import \\{[^}]*${moduleName}[^}]*\\} from ['"].*features\\/${feature.id}\\/.*['"];?\\n?`,
|
|
180
207
|
'gm'
|
|
181
208
|
);
|
|
182
209
|
content = content.replace(importPattern, '');
|
|
183
210
|
|
|
184
|
-
// Remove the module from the imports array
|
|
185
211
|
const refPattern = new RegExp(
|
|
186
212
|
`\\n?\\s*${moduleName},?|,?\\s*${moduleName}`,
|
|
187
213
|
'g'
|
package/src/index.js
CHANGED
|
@@ -42,7 +42,6 @@ export async function run(flags) {
|
|
|
42
42
|
let features = [];
|
|
43
43
|
|
|
44
44
|
if (backendId === 'api-nest') {
|
|
45
|
-
// Database
|
|
46
45
|
const dbId = await p.select({
|
|
47
46
|
message: 'Database',
|
|
48
47
|
options: NEST_DATABASES.map(d => ({
|
|
@@ -54,7 +53,6 @@ export async function run(flags) {
|
|
|
54
53
|
if (p.isCancel(dbId)) return cancel();
|
|
55
54
|
database = NEST_DATABASES.find(d => d.id === dbId);
|
|
56
55
|
|
|
57
|
-
// Features
|
|
58
56
|
const featureIds = await p.multiselect({
|
|
59
57
|
message: 'Features (space to select, enter to confirm)',
|
|
60
58
|
options: NEST_FEATURES.map(f => ({
|
|
@@ -141,20 +139,17 @@ export async function run(flags) {
|
|
|
141
139
|
function buildPlainRepos({ backendId, frontendId }) {
|
|
142
140
|
const list = [];
|
|
143
141
|
|
|
144
|
-
// Dashboard — always included
|
|
145
142
|
for (const group of Object.values(REPOS)) {
|
|
146
143
|
for (const repo of group) {
|
|
147
144
|
if (repo.fixed) list.push(repo);
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
|
|
151
|
-
// .NET (NestJS is handled separately)
|
|
152
148
|
if (backendId === 'api-dotnet') {
|
|
153
149
|
const dotnet = REPOS.backend.find(r => r.id === 'api-dotnet');
|
|
154
150
|
if (dotnet) list.push(dotnet);
|
|
155
151
|
}
|
|
156
152
|
|
|
157
|
-
// Frontend
|
|
158
153
|
if (frontendId && REPOS.frontend) {
|
|
159
154
|
const frontend = REPOS.frontend.find(r => r.id === frontendId);
|
|
160
155
|
if (frontend) list.push(frontend);
|
package/src/syncer.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
|
|
8
|
+
export async function runSync() {
|
|
9
|
+
console.log('');
|
|
10
|
+
p.intro(
|
|
11
|
+
chalk.bgHex('#7c3aed').white(' create-epinoetics-app sync ') +
|
|
12
|
+
chalk.dim(' pull latest boilerplate updates')
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
|
|
17
|
+
// ── 1. Read scaffold.config.json ──────────────────────────────────
|
|
18
|
+
const configPath = join(cwd, 'scaffold.config.json');
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
p.log.error(
|
|
21
|
+
'No scaffold.config.json found.\n' +
|
|
22
|
+
chalk.dim('Make sure you are inside the code/api-nest directory.')
|
|
23
|
+
);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
28
|
+
const database = NEST_DATABASES.find(d => d.id === config.database);
|
|
29
|
+
|
|
30
|
+
p.note(
|
|
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)) {
|
|
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);
|
|
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
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 4. Pull latest from the db branch ────────────────────────────
|
|
66
|
+
const pullSpinner = p.spinner();
|
|
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(() => {});
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
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.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
p.outro(chalk.green('Sync complete!'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cancel() {
|
|
97
|
+
p.cancel('Cancelled.');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|