mongofire 6.5.3 → 6.5.6
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/CHANGELOG.md +35 -67
- package/README.md +338 -155
- package/bin/mongofire.cjs +1039 -654
- package/dist/bin/mongofire.cjs +1 -851
- package/dist/src/changetrack.js +1 -1
- package/dist/src/compactor.js +1 -0
- package/dist/src/connection.js +1 -1
- package/dist/src/device.js +1 -1
- package/dist/src/diff.js +1 -0
- package/dist/src/field-merge.js +1 -0
- package/dist/src/index.cjs +1 -1
- package/dist/src/local-manager.js +1 -0
- package/dist/src/plugin.js +1 -1
- package/dist/src/plugin.mjs +6 -0
- package/dist/src/rate-limiter.js +1 -0
- package/dist/src/reconcile.js +1 -1
- package/dist/src/schema-manager.js +1 -0
- package/dist/src/state.js +1 -1
- package/dist/src/sync.js +1 -1
- package/dist/src/utils.js +1 -1
- package/dist/types/index.d.ts +247 -281
- package/index.cjs +17 -3
- package/index.mjs +7 -2
- package/package.json +18 -16
- package/types/index.d.ts +317 -0
package/bin/mongofire.cjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Tell the MongoFire core module not to auto-start from mongofire.config.js.
|
|
5
|
+
// Without this flag, requiring the core module would fire autoStart() in the
|
|
6
|
+
// background, which conflicts with the CLI's own controlled start() calls.
|
|
7
|
+
process.env.MONGOFIRE_CLI = '1';
|
|
8
|
+
|
|
7
9
|
process.on('uncaughtException', (err) => {
|
|
8
|
-
console.error('\n
|
|
10
|
+
console.error('\n✖ MongoFire error:', err.message || err);
|
|
9
11
|
if (process.env.MONGOFIRE_DEBUG) console.error(err.stack);
|
|
10
12
|
process.exit(1);
|
|
11
13
|
});
|
|
12
14
|
process.on('unhandledRejection', (reason) => {
|
|
13
|
-
console.error('\n
|
|
15
|
+
console.error('\n✖ MongoFire error:', reason?.message || reason);
|
|
14
16
|
if (process.env.MONGOFIRE_DEBUG) console.error(reason?.stack || reason);
|
|
15
17
|
process.exit(1);
|
|
16
18
|
});
|
|
@@ -24,791 +26,1122 @@ const command = args[0];
|
|
|
24
26
|
const flags = new Set(args.slice(1));
|
|
25
27
|
|
|
26
28
|
if (flags.has('--esm') && flags.has('--cjs')) {
|
|
27
|
-
console.error('
|
|
29
|
+
console.error('✖ Use only one: --esm OR --cjs');
|
|
28
30
|
process.exit(1);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
//
|
|
32
|
-
// .env load karo BEFORE config import — warna process.env.ATLAS_URI etc. empty
|
|
33
|
-
// milte hain jab config file evaluate hoti hai. CJS config template bhi apna
|
|
34
|
-
// dotenv.config() call karta hai, lekin CLI se load karna double-safety hai
|
|
35
|
-
// aur ESM configs ke liye zaroori hai jo import hone pe env read karte hain.
|
|
33
|
+
// Load .env early
|
|
36
34
|
(function _loadDotenv() {
|
|
37
|
-
const
|
|
38
|
-
if (!fs.existsSync(
|
|
39
|
-
try {
|
|
40
|
-
require('dotenv').config({ path: envPath });
|
|
41
|
-
} catch (_) {
|
|
42
|
-
// dotenv not installed — env vars must be set manually in the shell
|
|
43
|
-
}
|
|
35
|
+
const p = path.join(process.cwd(), '.env');
|
|
36
|
+
if (!fs.existsSync(p)) return;
|
|
37
|
+
try { require('dotenv').config({ path: p }); } catch (_) {}
|
|
44
38
|
})();
|
|
45
39
|
|
|
46
|
-
// ─── TTY Detection (Windows fix) ─────────────────────────────────────────────
|
|
47
|
-
// Windows PowerShell/CMD me process.stdin.isTTY undefined hota hai.
|
|
48
|
-
// Agar TTY nahi hai to interactive prompts crash karte hain -> Windows popup.
|
|
49
|
-
// Solution: non-TTY environment me automatically non-interactive mode use karo.
|
|
50
40
|
const IS_TTY = !!process.stdin.isTTY;
|
|
51
41
|
|
|
52
|
-
// ───
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
} else if (
|
|
63
|
-
|
|
64
|
-
console.log('
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
// ─── Router ───────────────────────────────────────────────────────────────────
|
|
43
|
+
(async () => {
|
|
44
|
+
if (command === 'init') {
|
|
45
|
+
const hasFlags = flags.has('--esm') || flags.has('--cjs') || flags.has('--force') || flags.has('-f');
|
|
46
|
+
if (!hasFlags && IS_TTY) {
|
|
47
|
+
await doInitWizard();
|
|
48
|
+
} else {
|
|
49
|
+
if (!hasFlags) process.stdout.write('ℹ Non-interactive mode — use --cjs, --esm, --force for control\n\n');
|
|
50
|
+
doInitDirect({ force: flags.has('--force') || flags.has('-f'), moduleSystem: flags.has('--esm') ? 'esm' : flags.has('--cjs') ? 'cjs' : null, flagForced: flags.has('--esm') || flags.has('--cjs') });
|
|
51
|
+
}
|
|
52
|
+
} else if (command === 'config') {
|
|
53
|
+
if (IS_TTY) await doConfigWizard();
|
|
54
|
+
else { console.log('ℹ config requires an interactive terminal.'); process.exit(0); }
|
|
55
|
+
} else if (command === 'status') {
|
|
56
|
+
await doStatus();
|
|
57
|
+
} else if (command === 'clean') {
|
|
58
|
+
const hasDaysFlag = [...flags].some((f) => f.startsWith('--days='));
|
|
59
|
+
if (!hasDaysFlag && IS_TTY) await doCleanWizard();
|
|
60
|
+
else await doClean();
|
|
61
|
+
} else if (command === 'conflicts') {
|
|
62
|
+
if (IS_TTY) await doConflictsWizard();
|
|
63
|
+
else { console.log('ℹ conflicts requires an interactive terminal.'); process.exit(0); }
|
|
64
|
+
} else if (command === 'reconcile') {
|
|
65
|
+
const noFullScan = flags.has('--no-full-scan');
|
|
66
|
+
const colArg = [...flags].find((f) => f.startsWith('--collection='));
|
|
67
|
+
const singleCol = colArg ? colArg.split('=')[1] : null;
|
|
68
|
+
await doReconcile({ fullScan: !noFullScan, collection: singleCol });
|
|
69
|
+
} else if (command === 'compact') {
|
|
70
|
+
await doCompact();
|
|
71
|
+
} else if (command === 'reset-local') {
|
|
72
|
+
if (IS_TTY) await doResetLocal();
|
|
73
|
+
else {
|
|
74
|
+
console.log('ℹ reset-local requires an interactive terminal (safety guard).');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
67
77
|
} else {
|
|
68
|
-
|
|
69
|
-
force: flags.has('--force') || flags.has('-f'),
|
|
70
|
-
moduleSystem: flags.has('--esm') ? 'esm' : flags.has('--cjs') ? 'cjs' : null,
|
|
71
|
-
flagForced: flags.has('--esm') || flags.has('--cjs'),
|
|
72
|
-
});
|
|
78
|
+
showHelp();
|
|
73
79
|
}
|
|
80
|
+
})().catch((err) => {
|
|
81
|
+
console.error('\n✖ MongoFire error:', err.message || err);
|
|
82
|
+
if (process.env.MONGOFIRE_DEBUG) console.error(err.stack);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
} else if (command === 'clean') {
|
|
82
|
-
const hasDaysFlag = [...flags].some((f) => f.startsWith('--days='));
|
|
83
|
-
if (!hasDaysFlag && IS_TTY) {
|
|
84
|
-
doCleanInteractive().catch((err) => {
|
|
85
|
-
console.error('❌ Error:', err.message);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
doClean().catch((err) => {
|
|
90
|
-
console.error('❌ Error:', err.message);
|
|
91
|
-
process.exit(1);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
86
|
+
// ─── Help ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
function showHelp() {
|
|
88
|
+
const v = _getVersion();
|
|
89
|
+
console.log(`
|
|
90
|
+
\x1b[33m🔥 MongoFire\x1b[0m \x1b[2mv${v}\x1b[0m
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
if (IS_TTY) {
|
|
97
|
-
doConflictsInteractive().catch((err) => {
|
|
98
|
-
console.error('❌ Error:', err.message);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
});
|
|
101
|
-
} else {
|
|
102
|
-
console.log('ℹ️ conflicts command requires an interactive terminal (TTY).');
|
|
103
|
-
console.log(' Run directly in PowerShell or CMD window.\n');
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}
|
|
92
|
+
\x1b[1mUsage\x1b[0m
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
doReconcile().catch((err) => {
|
|
109
|
-
console.error('❌ Error:', err.message);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
});
|
|
94
|
+
\x1b[36mnpx mongofire\x1b[0m \x1b[2m<command> [flags]\x1b[0m
|
|
112
95
|
|
|
113
|
-
|
|
114
|
-
showHelp();
|
|
115
|
-
}
|
|
96
|
+
\x1b[1mCommands\x1b[0m
|
|
116
97
|
|
|
117
|
-
|
|
98
|
+
\x1b[36minit\x1b[0m Interactive setup wizard
|
|
99
|
+
\x1b[36mconfig\x1b[0m Update an existing configuration \x1b[2m(TTY only)\x1b[0m
|
|
100
|
+
\x1b[36mstatus\x1b[0m Show pending sync counts
|
|
101
|
+
\x1b[36mclean\x1b[0m Delete old sync records
|
|
102
|
+
\x1b[36mconflicts\x1b[0m View and resolve conflicts \x1b[2m(TTY only)\x1b[0m
|
|
103
|
+
\x1b[36mreconcile\x1b[0m Recover writes lost from crashes
|
|
104
|
+
\x1b[36mcompact\x1b[0m Compact the sync changelog (60\u201380% size reduction)
|
|
105
|
+
\x1b[36mreset-local\x1b[0m Safely wipe local DB + all MongoFire state \x1b[2m(TTY only)\x1b[0m
|
|
118
106
|
|
|
119
|
-
|
|
120
|
-
console.log(`
|
|
121
|
-
\uD83D\uDD25 MongoFire CLI v${_getVersion()}
|
|
107
|
+
\x1b[1mFlags for \x1b[36minit\x1b[0m
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
--cjs Force CommonJS templates
|
|
127
|
-
--force Overwrite existing config files
|
|
109
|
+
\x1b[2m--esm\x1b[0m Force ESM module format
|
|
110
|
+
\x1b[2m--cjs\x1b[0m Force CommonJS module format
|
|
111
|
+
\x1b[2m--force, -f\x1b[0m Overwrite existing config files
|
|
128
112
|
|
|
129
|
-
|
|
130
|
-
Show pending sync operation counts
|
|
113
|
+
\x1b[1mFlags for \x1b[36mclean\x1b[0m
|
|
131
114
|
|
|
132
|
-
|
|
133
|
-
Delete old synced records (default: 7 days)
|
|
134
|
-
Example: npx mongofire clean --days=3
|
|
115
|
+
\x1b[2m--days=N\x1b[0m Delete records older than N days (1–3650, default 7)
|
|
135
116
|
|
|
136
|
-
|
|
137
|
-
View, retry, or dismiss unresolved conflicts
|
|
117
|
+
\x1b[1mFlags for \x1b[36mreconcile\x1b[0m
|
|
138
118
|
|
|
139
|
-
|
|
140
|
-
|
|
119
|
+
\x1b[2m--no-full-scan\x1b[0m Skip Phase 2 (only check orphaned docmeta)
|
|
120
|
+
\x1b[2m--collection=NAME\x1b[0m Scan a single collection instead of all
|
|
141
121
|
|
|
142
|
-
|
|
122
|
+
\x1b[2mTip: set MONGOFIRE_DEBUG=1 for full error stack traces\x1b[0m
|
|
143
123
|
`);
|
|
144
124
|
}
|
|
145
125
|
|
|
146
126
|
function _getVersion() {
|
|
147
|
-
try {
|
|
148
|
-
return require(path.join(__dirname, '..', 'package.json')).version;
|
|
149
|
-
} catch { return '?'; }
|
|
127
|
+
try { return require(path.join(__dirname, '..', 'package.json')).version; } catch { return '?'; }
|
|
150
128
|
}
|
|
151
129
|
|
|
152
|
-
// ───
|
|
153
|
-
|
|
154
|
-
// Fallback: apna readline-based prompter jo Windows pe bhi kaam karta hai.
|
|
155
|
-
|
|
156
|
-
async function _prompt(questions) {
|
|
157
|
-
// Try inquirer first
|
|
130
|
+
// ─── Load clack ───────────────────────────────────────────────────────────────
|
|
131
|
+
async function loadClack() {
|
|
158
132
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return await inquirer.prompt(questions);
|
|
163
|
-
}
|
|
164
|
-
} catch (_) {
|
|
165
|
-
// inquirer not installed — use readline fallback
|
|
133
|
+
return await import('@clack/prompts');
|
|
134
|
+
} catch {
|
|
135
|
+
return buildFallbackClack();
|
|
166
136
|
}
|
|
167
|
-
|
|
168
|
-
return _readlinePrompt(questions);
|
|
169
137
|
}
|
|
170
138
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
process.
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const answers = {};
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
for (const q of questions) {
|
|
193
|
-
answers[q.name] = await _askOne(rl, q, answers);
|
|
194
|
-
}
|
|
195
|
-
} finally {
|
|
196
|
-
rl.close();
|
|
197
|
-
process.stdin.pause(); // Windows fix: stdin pause after done
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return answers;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function _askOne(rl, q, prevAnswers) {
|
|
204
|
-
return new Promise((resolve) => {
|
|
205
|
-
|
|
206
|
-
// 'close' event = stdin closed unexpectedly (Windows pipe issue)
|
|
207
|
-
// Use default value instead of hanging
|
|
208
|
-
const onClose = () => resolve(q.default ?? '');
|
|
209
|
-
rl.once('close', onClose);
|
|
210
|
-
|
|
211
|
-
const done = (val) => {
|
|
212
|
-
rl.removeListener('close', onClose);
|
|
213
|
-
resolve(val);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
if (q.type === 'list') {
|
|
217
|
-
const choices = q.choices || [];
|
|
218
|
-
const lines = choices
|
|
219
|
-
.map((c, i) => ` ${i + 1}. ${typeof c === 'object' ? c.name : c}`)
|
|
220
|
-
.join('\n');
|
|
221
|
-
const defIdx = typeof q.default === 'number' ? q.default : 0;
|
|
222
|
-
|
|
223
|
-
const msg = q.message + (typeof q.message === 'function' ? q.message(prevAnswers) : '');
|
|
224
|
-
rl.question(`\n${msg}\n${lines}\nEnter number [${defIdx + 1}]: `, (ans) => {
|
|
225
|
-
const trimmed = ans.trim();
|
|
226
|
-
const idx = trimmed ? Math.max(0, parseInt(trimmed, 10) - 1) : defIdx;
|
|
227
|
-
const choice = choices[idx] ?? choices[defIdx] ?? choices[0];
|
|
228
|
-
done(typeof choice === 'object' ? choice.value : choice);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
} else if (q.type === 'confirm') {
|
|
232
|
-
const defVal = q.default !== false;
|
|
233
|
-
const hint = defVal ? 'Y/n' : 'y/N';
|
|
234
|
-
const msgText = typeof q.message === 'function' ? q.message(prevAnswers) : q.message;
|
|
235
|
-
rl.question(`\n${msgText} (${hint}): `, (ans) => {
|
|
236
|
-
const trimmed = ans.trim().toLowerCase();
|
|
237
|
-
done(trimmed === '' ? defVal : trimmed === 'y' || trimmed === 'yes');
|
|
238
|
-
});
|
|
239
|
-
|
|
139
|
+
// ─── Init Wizard ──────────────────────────────────────────────────────────────
|
|
140
|
+
const DEFAULT_COLLECTIONS = 'orders, products, users';
|
|
141
|
+
|
|
142
|
+
// ─── Modern info block ────────────────────────────────────────────────────────
|
|
143
|
+
function _info(title, lines, tip) {
|
|
144
|
+
const W = 58;
|
|
145
|
+
const pad = (s) => s.length > W ? s.slice(0, W - 1) + '...' : s;
|
|
146
|
+
process.stdout.write('|\n');
|
|
147
|
+
process.stdout.write('[2m| +- [0m[1m' + title + '[0m\n');
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (line === '') {
|
|
150
|
+
process.stdout.write('[2m| |[0m\n');
|
|
151
|
+
} else if (line.startsWith('✓')) {
|
|
152
|
+
process.stdout.write('[2m| | [32m' + pad(line) + '[0m\n');
|
|
153
|
+
} else if (line.startsWith('✕')) {
|
|
154
|
+
process.stdout.write('[2m| | [2m' + pad(line) + '[0m\n');
|
|
155
|
+
} else if (line.startsWith('→')) {
|
|
156
|
+
process.stdout.write('[2m| | [36m' + pad(line) + '[0m\n');
|
|
240
157
|
} else {
|
|
241
|
-
|
|
242
|
-
const defHint = q.default != null ? ` (${q.default})` : '';
|
|
243
|
-
rl.question(`\n${msgText}${defHint}: `, (ans) => {
|
|
244
|
-
done(ans.trim() || q.default || '');
|
|
245
|
-
});
|
|
158
|
+
process.stdout.write('[2m| | ' + pad(line) + '[0m\n');
|
|
246
159
|
}
|
|
247
|
-
}
|
|
160
|
+
}
|
|
161
|
+
if (tip) {
|
|
162
|
+
process.stdout.write('[2m| +- 💡 ' + pad(tip) + '[0m\n');
|
|
163
|
+
} else {
|
|
164
|
+
process.stdout.write('[2m| +-[0m\n');
|
|
165
|
+
}
|
|
248
166
|
}
|
|
249
167
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
168
|
+
async function doInitWizard() {
|
|
169
|
+
const p = await loadClack();
|
|
170
|
+
const cwd = process.cwd();
|
|
171
|
+
|
|
172
|
+
p.intro(`[33m 🔥 MongoFire [0m[2m v${_getVersion()} [0m`);
|
|
173
|
+
|
|
174
|
+
// ── Existing config check (Vite-style) ───────────────────────────────────────
|
|
175
|
+
const cfgPath = path.join(cwd, 'mongofire.config.js');
|
|
176
|
+
const entryPath = path.join(cwd, 'mongofire.js');
|
|
177
|
+
const cfgExists = fs.existsSync(cfgPath);
|
|
178
|
+
const entryExists = fs.existsSync(entryPath);
|
|
179
|
+
|
|
180
|
+
if (cfgExists) {
|
|
181
|
+
const existingFiles = [
|
|
182
|
+
cfgExists && 'mongofire.config.js',
|
|
183
|
+
entryExists && 'mongofire.js',
|
|
184
|
+
].filter(Boolean).join(', ');
|
|
185
|
+
|
|
186
|
+
p.note(
|
|
187
|
+
`[33m${existingFiles}[0m [2m— already exists in this directory[0m`,
|
|
188
|
+
'Existing setup found'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const action = await p.select({
|
|
192
|
+
message: 'What would you like to do?',
|
|
193
|
+
options: [
|
|
194
|
+
{ value: 'overwrite', label: 'Overwrite', hint: 'replace with fresh config — start from scratch' },
|
|
195
|
+
{ value: 'config', label: 'Update config', hint: 'change collections, interval, or toggle options' },
|
|
196
|
+
{ value: 'cancel', label: 'Cancel', hint: 'keep existing files unchanged' },
|
|
267
197
|
],
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
type: 'confirm',
|
|
280
|
-
name: 'realtime',
|
|
281
|
-
message: 'Enable real-time sync? (Requires Atlas or replica set)',
|
|
282
|
-
default: false,
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
type: 'input',
|
|
286
|
-
name: 'collections',
|
|
287
|
-
message: 'Collections to sync (comma-separated)',
|
|
288
|
-
default: 'users,products',
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
type: 'list',
|
|
292
|
-
name: 'syncInterval',
|
|
293
|
-
message: 'Polling interval',
|
|
294
|
-
choices: [
|
|
295
|
-
{ name: '5s (dev / aggressive)', value: 5000 },
|
|
296
|
-
{ name: '15s (balanced)', value: 15000 },
|
|
297
|
-
{ name: '30s (default)', value: 30000 },
|
|
298
|
-
{ name: '60s (conservative)', value: 60000 },
|
|
299
|
-
],
|
|
300
|
-
default: 2,
|
|
301
|
-
},
|
|
302
|
-
]);
|
|
303
|
-
|
|
304
|
-
if (!answers.force) {
|
|
305
|
-
console.log('\nSetup cancelled — no files changed.\n');
|
|
306
|
-
process.exit(0);
|
|
198
|
+
initialValue: 'overwrite',
|
|
199
|
+
});
|
|
200
|
+
if (p.isCancel(action) || action === 'cancel') {
|
|
201
|
+
p.cancel('Cancelled — existing files kept.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (action === 'config') {
|
|
205
|
+
p.outro('[2mOpening config wizard...[0m');
|
|
206
|
+
return doConfigWizard();
|
|
207
|
+
}
|
|
208
|
+
// action === 'overwrite' — fall through to full wizard below
|
|
307
209
|
}
|
|
308
210
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
syncInterval: Number(answers.syncInterval) || 30000,
|
|
211
|
+
// ── Step 1: Module system ────────────────────────────────────────────────────
|
|
212
|
+
_info('Module System', [
|
|
213
|
+
'How your project loads and shares code between files.',
|
|
214
|
+
'',
|
|
215
|
+
'→ Auto-detect reads package.json + scans your source files',
|
|
216
|
+
'→ CommonJS you write require() / module.exports',
|
|
217
|
+
'→ ESM you write import / export',
|
|
218
|
+
], 'Not sure? Pick Auto-detect — correct for most projects.');
|
|
219
|
+
const moduleSystem = await p.select({
|
|
220
|
+
message: 'Module system',
|
|
221
|
+
options: [
|
|
222
|
+
{ value: 'auto', label: 'Auto-detect', hint: 'recommended — reads your project automatically' },
|
|
223
|
+
{ value: 'cjs', label: 'CommonJS', hint: 'require() / module.exports' },
|
|
224
|
+
{ value: 'esm', label: 'ESM', hint: 'import / export' },
|
|
225
|
+
],
|
|
226
|
+
initialValue: 'auto',
|
|
326
227
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
function doInit(options) {
|
|
332
|
-
const cwd = process.cwd();
|
|
333
|
-
const moduleSystem = detectModuleSystem(cwd, options.moduleSystem);
|
|
334
|
-
// moduleSource:
|
|
335
|
-
// 'auto-detected' — user picked "Auto-detect" in wizard, OR no flag passed
|
|
336
|
-
// 'forced' — user explicitly passed --esm / --cjs flag
|
|
337
|
-
// 'auto-detected (from package.json)' — auto picked esm/cjs from pkg.type
|
|
338
|
-
const moduleSource = options.autoDetected
|
|
339
|
-
? `auto-detected (${moduleSystem} from package.json)`
|
|
340
|
-
: options.flagForced
|
|
341
|
-
? `forced (--${moduleSystem})`
|
|
342
|
-
: 'auto-detected';
|
|
343
|
-
|
|
344
|
-
const envResult = ensureEnvFile(path.join(cwd, '.env'));
|
|
345
|
-
const configResult = writeFileIfNeeded(
|
|
346
|
-
path.join(cwd, 'mongofire.config.js'),
|
|
347
|
-
buildConfigTemplate(moduleSystem, options),
|
|
348
|
-
!!options.force
|
|
349
|
-
);
|
|
350
|
-
const entryResult = writeFileIfNeeded(
|
|
351
|
-
path.join(cwd, 'mongofire.js'),
|
|
352
|
-
buildEntryTemplate(moduleSystem),
|
|
353
|
-
!!options.force
|
|
354
|
-
);
|
|
355
|
-
const hints = collectPackageHints(cwd);
|
|
356
|
-
|
|
357
|
-
printInitSummary({ moduleSystem, moduleSource, force: !!options.force, envResult, configResult, entryResult, hints });
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function detectModuleSystem(cwd, manualOverride) {
|
|
361
|
-
if (manualOverride && manualOverride !== 'auto') return manualOverride;
|
|
362
|
-
|
|
363
|
-
const pkg = readPackageJson(cwd);
|
|
364
|
-
if (pkg?.type === 'module') return 'esm';
|
|
365
|
-
if (pkg?.type === 'commonjs') return 'cjs';
|
|
228
|
+
if (p.isCancel(moduleSystem)) return _cancelled(p);
|
|
366
229
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// ─── Template builders ────────────────────────────────────────────────────────
|
|
230
|
+
// ── Step 2: Real-time sync ───────────────────────────────────────────────────
|
|
231
|
+
_info('Real-Time Sync', [
|
|
232
|
+
'Controls how fast changes appear across all connected devices.',
|
|
233
|
+
'',
|
|
234
|
+
'✓ Enabled Atlas Change Streams — changes push within milliseconds',
|
|
235
|
+
'✕ Disabled Polling only — devices check every syncInterval',
|
|
236
|
+
'',
|
|
237
|
+
'If Change Streams are unavailable, falls back to polling silently.',
|
|
238
|
+
], 'Recommended ON for most apps.');
|
|
239
|
+
const enableRealtime = await p.confirm({
|
|
240
|
+
message: 'Enable real-time sync?',
|
|
241
|
+
hint: 'ON = instant push OFF = polling only',
|
|
242
|
+
initialValue: true,
|
|
243
|
+
});
|
|
244
|
+
if (p.isCancel(enableRealtime)) return _cancelled(p);
|
|
383
245
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
246
|
+
// ── Step 3: Multi-tenant ─────────────────────────────────────────────────────
|
|
247
|
+
_info('Multi-Tenant Mode', [
|
|
248
|
+
'Controls whether each user syncs only their own private data.',
|
|
249
|
+
'',
|
|
250
|
+
'✕ OFF Cafe system, team app, single business',
|
|
251
|
+
' All users share the same data — simple and fast',
|
|
252
|
+
'',
|
|
253
|
+
'✓ ON SaaS platform, per-user notes, multi-business app',
|
|
254
|
+
' Each user sees only their own documents',
|
|
255
|
+
], 'Keep OFF unless users must have isolated private data.');
|
|
256
|
+
const enableMultitenant = await p.confirm({
|
|
257
|
+
message: 'Enable multi-tenant mode?',
|
|
258
|
+
hint: 'OFF = shared data (default) ON = per-user isolation',
|
|
259
|
+
initialValue: false,
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(enableMultitenant)) return _cancelled(p);
|
|
390
262
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
'',
|
|
408
|
-
' collections: [',
|
|
409
|
-
cols,
|
|
410
|
-
' ],',
|
|
411
|
-
'',
|
|
412
|
-
' syncInterval: ' + syncInterval + ',',
|
|
413
|
-
' batchSize: 200,',
|
|
414
|
-
" syncOwner: '*',",
|
|
415
|
-
' realtime: ' + realtime + ',',
|
|
416
|
-
'',
|
|
417
|
-
' onSync(result) {',
|
|
418
|
-
' if (result.deleted + result.downloaded + result.uploaded > 0) {',
|
|
419
|
-
' console.log(`[MongoFire] Synced: \u2191${result.uploaded} \u2193${result.downloaded} DEL:${result.deleted}`);',
|
|
420
|
-
' }',
|
|
421
|
-
' },',
|
|
422
|
-
' onError(err) {',
|
|
423
|
-
" console.error('[MongoFire] Sync error:', err.message);",
|
|
424
|
-
' },',
|
|
425
|
-
'};',
|
|
426
|
-
'',
|
|
427
|
-
].join('\n');
|
|
428
|
-
}
|
|
263
|
+
// ── Step 4: TypeScript hints ─────────────────────────────────────────────────
|
|
264
|
+
_info('TypeScript Hints', [
|
|
265
|
+
'Adds @ts-check + JSDoc types to mongofire.config.js.',
|
|
266
|
+
'',
|
|
267
|
+
'✓ Enabled VS Code shows autocomplete and catches typos',
|
|
268
|
+
' Works perfectly in plain JavaScript projects',
|
|
269
|
+
' No TypeScript compiler or tsconfig needed',
|
|
270
|
+
'',
|
|
271
|
+
'✕ Disabled Plain config file, no type annotations',
|
|
272
|
+
], 'Recommended ON — free autocomplete, zero setup cost.');
|
|
273
|
+
const enableTypescript = await p.confirm({
|
|
274
|
+
message: 'Add TypeScript hints?',
|
|
275
|
+
hint: 'ON = smart autocomplete in VS Code OFF = plain JS',
|
|
276
|
+
initialValue: true,
|
|
277
|
+
});
|
|
278
|
+
if (p.isCancel(enableTypescript)) return _cancelled(p);
|
|
429
279
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
'
|
|
433
|
-
' * MongoFire Config (CommonJS)',
|
|
434
|
-
' *',
|
|
435
|
-
' * dotenv — .env file se ATLAS_URI etc. load karta hai.',
|
|
436
|
-
' * require("dotenv").config() ZAROORI hai taake process.env variables',
|
|
437
|
-
' * is config ke evaluate hone SE PEHLE set ho jayein.',
|
|
438
|
-
' * Install: npm i dotenv',
|
|
439
|
-
' */',
|
|
440
|
-
"try { require('dotenv').config(); } catch (_) {}",
|
|
280
|
+
// ── Step 5: Verbose logging ──────────────────────────────────────────────────
|
|
281
|
+
_info('Verbose Logging', [
|
|
282
|
+
'Controls how much MongoFire prints to your console.',
|
|
441
283
|
'',
|
|
442
|
-
'
|
|
443
|
-
|
|
444
|
-
' atlasUri: process.env.ATLAS_URI,',
|
|
445
|
-
" dbName: process.env.DB_NAME || 'myapp',",
|
|
284
|
+
'✕ OFF Logs only when real changes happen (uploads/downloads)',
|
|
285
|
+
' Clean output — good for production',
|
|
446
286
|
'',
|
|
447
|
-
'
|
|
448
|
-
|
|
449
|
-
|
|
287
|
+
'✓ ON Logs every sync cycle even when nothing changed',
|
|
288
|
+
' Useful when debugging sync issues during development',
|
|
289
|
+
], 'Keep OFF in production. Enable temporarily to debug.');
|
|
290
|
+
const enableVerbose = await p.confirm({
|
|
291
|
+
message: 'Enable verbose logging?',
|
|
292
|
+
hint: 'OFF = clean logs (recommended) ON = log every cycle',
|
|
293
|
+
initialValue: false,
|
|
294
|
+
});
|
|
295
|
+
if (p.isCancel(enableVerbose)) return _cancelled(p);
|
|
296
|
+
|
|
297
|
+
const featureSet = new Set([
|
|
298
|
+
enableRealtime && 'realtime',
|
|
299
|
+
enableMultitenant && 'multitenant',
|
|
300
|
+
enableTypescript && 'typescript',
|
|
301
|
+
enableVerbose && 'verbose',
|
|
302
|
+
].filter(Boolean));
|
|
303
|
+
|
|
304
|
+
// ── Step 6: Collections ──────────────────────────────────────────────────────
|
|
305
|
+
_info('Collections to Sync', [
|
|
306
|
+
'MongoDB collection names MongoFire will keep in sync.',
|
|
450
307
|
'',
|
|
451
|
-
'
|
|
452
|
-
'
|
|
453
|
-
|
|
454
|
-
|
|
308
|
+
'→ Enter your actual collection names, comma-separated',
|
|
309
|
+
'→ Must match exactly what your Mongoose models use',
|
|
310
|
+
'→ Example: orders, products, users, categories',
|
|
311
|
+
], 'Press Enter to accept defaults — edit in mongofire.config.js anytime.');
|
|
312
|
+
const rawCols = await p.text({
|
|
313
|
+
message: 'Collections to sync',
|
|
314
|
+
placeholder: DEFAULT_COLLECTIONS,
|
|
315
|
+
defaultValue: DEFAULT_COLLECTIONS,
|
|
316
|
+
validate: () => undefined,
|
|
317
|
+
});
|
|
318
|
+
if (p.isCancel(rawCols)) return _cancelled(p);
|
|
319
|
+
const colsRaw = (rawCols || '').trim() || DEFAULT_COLLECTIONS;
|
|
320
|
+
const collections = colsRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
321
|
+
|
|
322
|
+
// ── Step 7: Sync interval ────────────────────────────────────────────────────
|
|
323
|
+
const realtimeOn = featureSet.has('realtime');
|
|
324
|
+
_info('Sync Interval', [
|
|
325
|
+
'How often MongoFire polls Atlas for new changes.',
|
|
455
326
|
'',
|
|
456
|
-
'
|
|
457
|
-
'
|
|
458
|
-
' console.log(`[MongoFire] Synced: \u2191${result.uploaded} \u2193${result.downloaded} DEL:${result.deleted}`);',
|
|
459
|
-
' }',
|
|
460
|
-
' },',
|
|
461
|
-
' onError(err) {',
|
|
462
|
-
" console.error('[MongoFire] Sync error:', err.message);",
|
|
463
|
-
' },',
|
|
464
|
-
'};',
|
|
327
|
+
'→ Lower changes appear faster, more bandwidth used',
|
|
328
|
+
'→ Higher less resource usage, slight delay in updates',
|
|
465
329
|
'',
|
|
466
|
-
|
|
467
|
-
|
|
330
|
+
realtimeOn
|
|
331
|
+
? '✓ Realtime is ON — polling is just a safety net. 5s is ideal.'
|
|
332
|
+
: '→ No realtime — 30s is the sweet spot for most apps.',
|
|
333
|
+
], 'You can change this in mongofire.config.js at any time.');
|
|
334
|
+
const intervalOptions = realtimeOn
|
|
335
|
+
? [
|
|
336
|
+
{ value: 2000, label: '2s', hint: 'aggressive — high bandwidth usage' },
|
|
337
|
+
{ value: 5000, label: '5s', hint: 'recommended with Change Streams' },
|
|
338
|
+
{ value: 15000, label: '15s', hint: 'light fallback polling' },
|
|
339
|
+
]
|
|
340
|
+
: [
|
|
341
|
+
{ value: 5000, label: '5s', hint: 'fast — great for dev and testing' },
|
|
342
|
+
{ value: 15000, label: '15s', hint: 'balanced — low traffic apps' },
|
|
343
|
+
{ value: 30000, label: '30s', hint: 'recommended for most apps' },
|
|
344
|
+
{ value: 60000, label: '60s', hint: 'conservative — minimal DB load' },
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const syncInterval = await p.select({
|
|
348
|
+
message: 'Sync interval',
|
|
349
|
+
options: intervalOptions,
|
|
350
|
+
initialValue: realtimeOn ? 5000 : 30000,
|
|
351
|
+
});
|
|
352
|
+
if (p.isCancel(syncInterval)) return _cancelled(p);
|
|
468
353
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"import config from './mongofire.config.js';",
|
|
475
|
-
'',
|
|
476
|
-
'export const ready = mongofire.start(config);',
|
|
354
|
+
// ── Step 8: Owner field (only if multi-tenant) ───────────────────────────────
|
|
355
|
+
let ownerField = '*';
|
|
356
|
+
if (featureSet.has('multitenant')) {
|
|
357
|
+
_info('Owner Field', [
|
|
358
|
+
'The document field that identifies who owns each record.',
|
|
477
359
|
'',
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
'
|
|
481
|
-
' if (r.deleted + r.downloaded + r.uploaded > 0) {',
|
|
482
|
-
' console.log(`[MongoFire] Synced: \u2191${r.uploaded} \u2193${r.downloaded} DEL:${r.deleted}`);',
|
|
483
|
-
' }',
|
|
484
|
-
'});',
|
|
360
|
+
'→ Must exist on EVERY document in every synced collection',
|
|
361
|
+
'→ MongoFire only syncs docs where this field = current user',
|
|
362
|
+
'→ You must set this field when creating documents in your app',
|
|
485
363
|
'',
|
|
486
|
-
'
|
|
487
|
-
|
|
488
|
-
|
|
364
|
+
'Common names: userId, ownerId, createdBy, accountId',
|
|
365
|
+
], 'Set req.user._id into this field on every document you create.');
|
|
366
|
+
const ownerRaw = await p.text({
|
|
367
|
+
message: 'Owner field name',
|
|
368
|
+
placeholder: 'userId',
|
|
369
|
+
defaultValue: 'userId',
|
|
370
|
+
hint: 'must exist on every synced document',
|
|
371
|
+
});
|
|
372
|
+
if (p.isCancel(ownerRaw)) return _cancelled(p);
|
|
373
|
+
ownerField = (ownerRaw || '').trim() || 'userId';
|
|
489
374
|
}
|
|
490
375
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
' if (r.deleted + r.downloaded + r.uploaded > 0) {',
|
|
502
|
-
' console.log(`[MongoFire] Synced: \u2191${r.uploaded} \u2193${r.downloaded} DEL:${r.deleted}`);',
|
|
503
|
-
' }',
|
|
504
|
-
'});',
|
|
505
|
-
'',
|
|
506
|
-
'module.exports = { mongofire, ready };',
|
|
507
|
-
'',
|
|
508
|
-
].join('\n');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// ─── .env helper ─────────────────────────────────────────────────────────────
|
|
512
|
-
|
|
513
|
-
function ensureEnvFile(envPath) {
|
|
514
|
-
const defaults = [
|
|
515
|
-
['ATLAS_URI', 'mongodb+srv://USERNAME:PASSWORD@cluster0.xxxxx.mongodb.net/'],
|
|
516
|
-
['LOCAL_URI', 'mongodb://127.0.0.1:27017'],
|
|
517
|
-
['DB_NAME', 'myapp'],
|
|
376
|
+
// ── Step 9: Summary ───────────────────────────────────────────────────────────
|
|
377
|
+
const on = (v) => v ? '[32m● Enabled[0m' : '[2m○ Disabled[0m';
|
|
378
|
+
const summaryLines = [
|
|
379
|
+
`Module system ${moduleSystem === 'auto' ? 'Auto-detect' : moduleSystem.toUpperCase()}`,
|
|
380
|
+
`Real-time sync ${on(enableRealtime)}`,
|
|
381
|
+
`Multi-tenant ${on(enableMultitenant)}`,
|
|
382
|
+
`TypeScript hints ${on(enableTypescript)}`,
|
|
383
|
+
`Verbose logging ${on(enableVerbose)}`,
|
|
384
|
+
`Sync interval ${syncInterval}ms`,
|
|
385
|
+
`Collections ${collections.join(', ')}`,
|
|
518
386
|
];
|
|
387
|
+
if (featureSet.has('multitenant')) summaryLines.push(`Owner field ${ownerField}`);
|
|
388
|
+
p.note(summaryLines.join('\n'), 'Configuration Summary');
|
|
519
389
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
390
|
+
const proceed = await p.confirm({
|
|
391
|
+
message: 'Looks good — create these files?',
|
|
392
|
+
initialValue: true,
|
|
393
|
+
});
|
|
394
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
395
|
+
p.cancel('Cancelled — no files changed.');
|
|
396
|
+
return;
|
|
525
397
|
}
|
|
526
398
|
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
399
|
+
// ── Write files ──────────────────────────────────────────────────────────────
|
|
400
|
+
const wasAuto = moduleSystem === 'auto';
|
|
401
|
+
const resolvedModule = wasAuto ? detectModuleSystem(cwd, null) : moduleSystem;
|
|
402
|
+
const opts = { collections, realtime: enableRealtime, syncInterval, ownerField, verbose: enableVerbose, typescript: enableTypescript };
|
|
403
|
+
|
|
404
|
+
const envResult = ensureEnvFile(path.join(cwd, '.env'));
|
|
405
|
+
const cfgResult = writeFileIfNeeded(path.join(cwd, 'mongofire.config.js'), buildConfigTemplate(resolvedModule, opts), true);
|
|
406
|
+
const entryResult = writeFileIfNeeded(path.join(cwd, 'mongofire.js'), buildEntryTemplate(resolvedModule, opts), true);
|
|
407
|
+
const hints = collectPackageHints(cwd);
|
|
408
|
+
|
|
409
|
+
// Show clear per-file error if write failed (Windows permission issues etc)
|
|
410
|
+
const failures = [
|
|
411
|
+
cfgResult.action === 'failed' && { file: 'mongofire.config.js', error: cfgResult.error },
|
|
412
|
+
entryResult.action === 'failed' && { file: 'mongofire.js', error: entryResult.error },
|
|
413
|
+
].filter(Boolean);
|
|
414
|
+
|
|
415
|
+
if (failures.length > 0) {
|
|
416
|
+
const errLines = failures.map((f) =>
|
|
417
|
+
'[31m✕[0m ' + f.file.padEnd(24) + '[2m' + f.error + '[0m'
|
|
418
|
+
);
|
|
419
|
+
errLines.push('');
|
|
420
|
+
errLines.push('[2mTip: close the file in VS Code or any editor, then run init again.[0m');
|
|
421
|
+
p.note(errLines.join('\n'), '[31mCould not write files[0m');
|
|
422
|
+
p.cancel('Setup incomplete — some files could not be written.');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
537
425
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
426
|
+
const noteLines = [
|
|
427
|
+
_fileRow('.env', envResult.action),
|
|
428
|
+
_fileRow('mongofire.config.js', cfgResult.action),
|
|
429
|
+
_fileRow('mongofire.js', entryResult.action),
|
|
430
|
+
];
|
|
431
|
+
if (hints.length) noteLines.push('', ...hints.map((h) => '[33m⚠[0m ' + h));
|
|
432
|
+
p.note(noteLines.join('\n'), 'Files created');
|
|
433
|
+
|
|
434
|
+
const steps = [
|
|
435
|
+
`Fill in [36mATLAS_URI[0m inside [2m.env[0m`,
|
|
436
|
+
resolvedModule === 'esm'
|
|
437
|
+
? `Add [2mimport './mongofire.js'[0m to your app entry point`
|
|
438
|
+
: `Add [2mrequire('./mongofire')[0m to your app entry point`,
|
|
439
|
+
`Replace [2mapp.listen()[0m with [32mstartApp(app, port)[0m:\n`+
|
|
440
|
+
` [2m${resolvedModule === 'esm'
|
|
441
|
+
? "import { startApp } from './mongofire.js'; startApp(app, process.env.PORT || 3000);"
|
|
442
|
+
: "const { startApp } = require('./mongofire'); startApp(app, process.env.PORT || 3000);"}[0m`,
|
|
443
|
+
`Add [2mSchema.plugin(plugin('name'))[0m to each model — import plugin from mongofire.js`,
|
|
444
|
+
`[31mDO NOT[0m call [2mmongoose.connect()[0m — MongoFire owns the local connection`,
|
|
445
|
+
];
|
|
446
|
+
p.note(steps.map((s, i) => `${i + 1}. ${s}`).join('\n'), 'Next steps');
|
|
544
447
|
|
|
545
|
-
|
|
546
|
-
const pkg = readPackageJson(cwd);
|
|
547
|
-
const all = Object.assign({}, pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies);
|
|
548
|
-
const hints = [];
|
|
549
|
-
if (!all.mongoose) hints.push('mongoose not found — install: npm i mongoose');
|
|
550
|
-
if (!all.dotenv) hints.push('dotenv not installed — REQUIRED for .env loading: npm i dotenv');
|
|
551
|
-
return hints;
|
|
448
|
+
p.outro(`[32m✓ Setup complete![0m [2m${resolvedModule.toUpperCase()} · ${wasAuto ? 'auto-detected' : 'manual'}[0m`);
|
|
552
449
|
}
|
|
553
450
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
console.log('\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501');
|
|
561
|
-
console.log('\uD83D\uDD25 MongoFire ready (' + info.moduleSystem.toUpperCase() + ', ' + info.moduleSource + ')');
|
|
562
|
-
|
|
563
|
-
if (!info.force && (info.configResult.action === 'skipped' || info.entryResult.action === 'skipped')) {
|
|
564
|
-
console.log('\u2139\uFE0F Existing files kept. Use --force to regenerate.');
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
console.log('\nNext steps:');
|
|
568
|
-
console.log(' 1. Set ATLAS_URI in .env');
|
|
569
|
-
console.log(' 2. Install dotenv if not already: npm i dotenv');
|
|
570
|
-
console.log(' 3. Add your collections to mongofire.config.js');
|
|
571
|
-
if (info.moduleSystem === 'esm') {
|
|
572
|
-
console.log(" 4. Schema plugin: userSchema.plugin(mongofire.plugin('users'))");
|
|
573
|
-
console.log(" 5. App entry: import { ready } from './mongofire.js'; await ready;");
|
|
574
|
-
} else {
|
|
575
|
-
console.log(" 4. Schema plugin: UserSchema.plugin(mongofire.plugin('users'))");
|
|
576
|
-
console.log(" 5. App entry: await require('./mongofire').ready");
|
|
577
|
-
}
|
|
451
|
+
// ─── Config Wizard ────────────────────────────────────────────────────────────
|
|
452
|
+
async function doConfigWizard() {
|
|
453
|
+
const p = await loadClack();
|
|
454
|
+
const cwd = process.cwd();
|
|
455
|
+
const cfgPath = path.join(cwd, 'mongofire.config.js');
|
|
578
456
|
|
|
579
|
-
if (
|
|
580
|
-
console.log('\
|
|
581
|
-
|
|
457
|
+
if (!fs.existsSync(cfgPath)) {
|
|
458
|
+
console.log('\n✖ No mongofire.config.js found — run npx mongofire init first.\n');
|
|
459
|
+
process.exit(1);
|
|
582
460
|
}
|
|
583
|
-
console.log('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n');
|
|
584
|
-
}
|
|
585
461
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
462
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m config \x1b[0m`);
|
|
463
|
+
|
|
464
|
+
const section = await p.select({
|
|
465
|
+
message: 'What do you want to change?',
|
|
466
|
+
options: [
|
|
467
|
+
{ value: 'collections', label: 'Collections to sync' },
|
|
468
|
+
{ value: 'interval', label: 'Sync interval' },
|
|
469
|
+
{ value: 'realtime', label: 'Real-time sync toggle' },
|
|
470
|
+
{ value: 'multitenant', label: 'Multi-tenant owner field' },
|
|
471
|
+
{ value: 'regenerate', label: 'Regenerate all files', hint: 'runs full wizard' },
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
if (p.isCancel(section)) return _cancelled(p);
|
|
475
|
+
if (section === 'regenerate') { p.outro(''); return doInitWizard(); }
|
|
476
|
+
|
|
477
|
+
let content = '';
|
|
478
|
+
try { content = fs.readFileSync(cfgPath, 'utf8'); } catch (_) {}
|
|
479
|
+
|
|
480
|
+
if (section === 'collections') {
|
|
481
|
+
const match = content.match(/collections:\s*\[([\s\S]*?)\]/);
|
|
482
|
+
const current = match ? match[1].replace(/['"\s]/g, '').split(',').filter(Boolean).join(', ') : 'users, products';
|
|
483
|
+
const rawCols = await p.text({ message: 'Collections to sync', defaultValue: current, placeholder: current });
|
|
484
|
+
if (p.isCancel(rawCols)) return _cancelled(p);
|
|
485
|
+
const cols = rawCols.split(',').map((s) => s.trim()).filter(Boolean);
|
|
486
|
+
const newList = cols.map((c) => ` '${c}',`).join('\n');
|
|
487
|
+
fs.writeFileSync(cfgPath, content.replace(/collections:\s*\[([\s\S]*?)\]/, `collections: [\n${newList}\n ]`), 'utf8');
|
|
488
|
+
p.outro(`\x1b[32mUpdated\x1b[0m collections → ${cols.join(', ')}`);
|
|
489
|
+
|
|
490
|
+
} else if (section === 'interval') {
|
|
491
|
+
const syncInterval = await p.select({
|
|
492
|
+
message: 'New sync interval',
|
|
493
|
+
options: [
|
|
494
|
+
{ value: 5000, label: '5s', hint: 'dev' },
|
|
495
|
+
{ value: 15000, label: '15s', hint: 'balanced' },
|
|
496
|
+
{ value: 30000, label: '30s', hint: 'recommended' },
|
|
497
|
+
{ value: 60000, label: '60s', hint: 'conservative' },
|
|
609
498
|
],
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
499
|
+
initialValue: 30000,
|
|
500
|
+
});
|
|
501
|
+
if (p.isCancel(syncInterval)) return _cancelled(p);
|
|
502
|
+
const updated = content.replace(/syncInterval:\s*\d+/, `syncInterval: ${syncInterval}`);
|
|
503
|
+
if (updated === content) {
|
|
504
|
+
p.cancel('✖ Could not find syncInterval in config — edit manually.');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
fs.writeFileSync(cfgPath, updated, 'utf8');
|
|
508
|
+
p.outro(`\x1b[32mUpdated\x1b[0m syncInterval → ${syncInterval}ms`);
|
|
509
|
+
|
|
510
|
+
} else if (section === 'realtime') {
|
|
511
|
+
const isOn = /realtime:\s*true/.test(content);
|
|
512
|
+
const realtime = await p.confirm({ message: 'Enable real-time sync?', initialValue: !isOn });
|
|
513
|
+
if (p.isCancel(realtime)) return _cancelled(p);
|
|
514
|
+
const updated = content.replace(/realtime:\s*(true|false)/, `realtime: ${realtime}`);
|
|
515
|
+
if (updated === content) {
|
|
516
|
+
p.cancel('✖ Could not find realtime field in config — edit manually.');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
fs.writeFileSync(cfgPath, updated, 'utf8');
|
|
520
|
+
p.outro(`\x1b[32mUpdated\x1b[0m realtime → ${realtime}`);
|
|
521
|
+
|
|
522
|
+
} else if (section === 'multitenant') {
|
|
523
|
+
const owner = await p.text({ message: "Owner field path (use '*' to disable)", defaultValue: 'userId', placeholder: 'userId' });
|
|
524
|
+
if (p.isCancel(owner)) return _cancelled(p);
|
|
525
|
+
const updated = content.replace(/syncOwner:\s*['"][^'"]*['"]/, `syncOwner: '${owner}'`);
|
|
526
|
+
if (updated === content) {
|
|
527
|
+
p.cancel('✖ Could not find syncOwner in config — edit manually.');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
fs.writeFileSync(cfgPath, updated, 'utf8');
|
|
531
|
+
p.outro(`\x1b[32mUpdated\x1b[0m syncOwner → ${owner}`);
|
|
623
532
|
}
|
|
533
|
+
}
|
|
624
534
|
|
|
625
|
-
|
|
626
|
-
|
|
535
|
+
// ─── Init direct (flag mode) ──────────────────────────────────────────────────
|
|
536
|
+
function doInitDirect(options) {
|
|
537
|
+
const cwd = process.cwd();
|
|
538
|
+
const ms = detectModuleSystem(cwd, options.moduleSystem);
|
|
539
|
+
const source = options.flagForced ? `--${ms}` : 'auto-detected';
|
|
540
|
+
const opts = { collections: ['users', 'products'], realtime: false, syncInterval: 30000, ownerField: '*' };
|
|
541
|
+
|
|
542
|
+
const envResult = ensureEnvFile(path.join(cwd, '.env'));
|
|
543
|
+
const cfgResult = writeFileIfNeeded(path.join(cwd, 'mongofire.config.js'), buildConfigTemplate(ms, opts), !!options.force);
|
|
544
|
+
const entryResult = writeFileIfNeeded(path.join(cwd, 'mongofire.js'), buildEntryTemplate(ms, {}), !!options.force);
|
|
545
|
+
const hints = collectPackageHints(cwd);
|
|
546
|
+
|
|
547
|
+
console.log(`\x1b[33m 🔥 MongoFire\x1b[0m \x1b[2mv${_getVersion()}\x1b[0m\n`);
|
|
548
|
+
console.log(_fileRow('.env', envResult.action));
|
|
549
|
+
console.log(_fileRow('mongofire.config.js', cfgResult.action));
|
|
550
|
+
console.log(_fileRow('mongofire.js', entryResult.action));
|
|
551
|
+
if (!options.force && (cfgResult.action === 'skipped' || entryResult.action === 'skipped'))
|
|
552
|
+
console.log('\n\x1b[2m Existing files kept — use --force to regenerate.\x1b[0m');
|
|
553
|
+
if (hints.length) { console.log(''); hints.forEach((h) => console.log(`\x1b[33m ⚠\x1b[0m ${h}`)); }
|
|
554
|
+
console.log(`\n\x1b[32m Done!\x1b[0m \x1b[2m${ms.toUpperCase()} · ${source}\x1b[0m\n`);
|
|
627
555
|
}
|
|
628
556
|
|
|
629
557
|
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
630
|
-
|
|
631
558
|
async function doStatus() {
|
|
559
|
+
const p = await loadClack();
|
|
632
560
|
const cwd = process.cwd();
|
|
633
561
|
const cfgPath = resolveConfigPath(cwd);
|
|
634
|
-
if (!cfgPath)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
562
|
+
if (!cfgPath) return _noConfig(p);
|
|
563
|
+
|
|
564
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m status \x1b[0m`);
|
|
638
565
|
|
|
566
|
+
const spin = p.spinner();
|
|
567
|
+
spin.start('Connecting…');
|
|
639
568
|
const config = await loadConfig(cfgPath, cwd);
|
|
640
569
|
const mongofire = requireMongofire();
|
|
641
|
-
|
|
642
570
|
await mongofire.start(config);
|
|
643
571
|
const s = await mongofire.status();
|
|
572
|
+
spin.stop('Connected');
|
|
573
|
+
|
|
574
|
+
const onlineLabel = s.online ? '\x1b[32m● online\x1b[0m' : '\x1b[31m● offline\x1b[0m';
|
|
575
|
+
p.note(
|
|
576
|
+
[
|
|
577
|
+
`Atlas ${onlineLabel}`,
|
|
578
|
+
``,
|
|
579
|
+
`Pending \x1b[1m${s.pending}\x1b[0m \x1b[2munsynced operations\x1b[0m`,
|
|
580
|
+
` Creates ${s.creates}`,
|
|
581
|
+
` Updates ${s.updates}`,
|
|
582
|
+
` Deletes ${s.deletes}`,
|
|
583
|
+
].join('\n'),
|
|
584
|
+
'Sync state'
|
|
585
|
+
);
|
|
644
586
|
|
|
645
|
-
|
|
646
|
-
console.log('\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500');
|
|
647
|
-
console.log(' Online: ' + (s.online ? '\uD83D\uDFE2 Yes' : '\uD83D\uDD34 No (offline)'));
|
|
648
|
-
console.log(' Pending: ' + s.pending + ' total unsynced');
|
|
649
|
-
console.log(' Creates: ' + s.creates);
|
|
650
|
-
console.log(' Updates: ' + s.updates);
|
|
651
|
-
console.log(' Deletes: ' + s.deletes + '\n');
|
|
587
|
+
p.outro(s.pending === 0 ? '\x1b[32mAll caught up\x1b[0m' : `\x1b[33m${s.pending} operation(s) pending\x1b[0m`);
|
|
652
588
|
|
|
653
589
|
await mongofire.stop();
|
|
654
590
|
process.exit(0);
|
|
655
591
|
}
|
|
656
592
|
|
|
657
593
|
// ─── Clean ────────────────────────────────────────────────────────────────────
|
|
594
|
+
async function doCleanWizard() {
|
|
595
|
+
const p = await loadClack();
|
|
596
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m clean \x1b[0m`);
|
|
597
|
+
|
|
598
|
+
const days = await p.select({
|
|
599
|
+
message: 'Delete records synced longer than',
|
|
600
|
+
options: [
|
|
601
|
+
{ value: 1, label: '1 day', hint: 'aggressive' },
|
|
602
|
+
{ value: 3, label: '3 days' },
|
|
603
|
+
{ value: 7, label: '7 days', hint: 'recommended' },
|
|
604
|
+
{ value: 14, label: '14 days' },
|
|
605
|
+
{ value: 30, label: '30 days', hint: 'conservative' },
|
|
606
|
+
],
|
|
607
|
+
initialValue: 7,
|
|
608
|
+
});
|
|
609
|
+
if (p.isCancel(days)) return _cancelled(p);
|
|
610
|
+
|
|
611
|
+
const ok = await p.confirm({ message: `Delete records older than ${days} day(s) — this cannot be undone`, initialValue: false });
|
|
612
|
+
if (p.isCancel(ok) || !ok) { p.cancel('Cancelled — nothing deleted.'); return; }
|
|
613
|
+
|
|
614
|
+
// Pass days directly — no outer-scope flags mutation needed.
|
|
615
|
+
await _runClean(p, days);
|
|
616
|
+
}
|
|
658
617
|
|
|
659
618
|
async function doClean() {
|
|
619
|
+
const p = await loadClack();
|
|
620
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m clean \x1b[0m`);
|
|
660
621
|
const daysArg = [...flags].find((f) => f.startsWith('--days='));
|
|
661
622
|
const days = daysArg ? parseInt(daysArg.split('=')[1], 10) : 7;
|
|
623
|
+
await _runClean(p, days);
|
|
624
|
+
}
|
|
662
625
|
|
|
663
|
-
|
|
664
|
-
|
|
626
|
+
async function _runClean(p, days) {
|
|
627
|
+
// Validate: must be integer, 1–3650 (10 years max to guard against typos).
|
|
628
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650) {
|
|
629
|
+
console.error('✖ Invalid --days value (must be 1–3650)');
|
|
665
630
|
process.exit(1);
|
|
666
631
|
}
|
|
667
632
|
|
|
668
633
|
const cwd = process.cwd();
|
|
669
634
|
const cfgPath = resolveConfigPath(cwd);
|
|
670
|
-
if (!cfgPath)
|
|
671
|
-
console.error('❌ mongofire.config.js not found. Run: npx mongofire init');
|
|
672
|
-
process.exit(1);
|
|
673
|
-
}
|
|
635
|
+
if (!cfgPath) return _noConfig(p);
|
|
674
636
|
|
|
637
|
+
const spin = p.spinner();
|
|
638
|
+
spin.start(`Deleting records older than ${days} day(s)…`);
|
|
675
639
|
const config = await loadConfig(cfgPath, cwd);
|
|
676
640
|
const mongofire = requireMongofire();
|
|
677
|
-
|
|
678
641
|
await mongofire.start(config);
|
|
679
|
-
console.log('\uD83E\uDDF9 Cleaning records older than ' + days + ' days...');
|
|
680
642
|
const count = await mongofire.clean(days);
|
|
681
|
-
|
|
643
|
+
spin.stop(`Deleted ${count} record(s)`);
|
|
644
|
+
|
|
645
|
+
p.outro(count > 0 ? `\x1b[32m${count} record(s) removed\x1b[0m` : '\x1b[2mNothing to clean\x1b[0m');
|
|
682
646
|
await mongofire.stop();
|
|
683
647
|
process.exit(0);
|
|
684
648
|
}
|
|
685
649
|
|
|
686
|
-
// ───
|
|
687
|
-
|
|
688
|
-
|
|
650
|
+
// ─── Conflicts ────────────────────────────────────────────────────────────────
|
|
651
|
+
async function doConflictsWizard() {
|
|
652
|
+
const p = await loadClack();
|
|
689
653
|
const cwd = process.cwd();
|
|
690
654
|
const cfgPath = resolveConfigPath(cwd);
|
|
691
|
-
if (!cfgPath)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
655
|
+
if (!cfgPath) return _noConfig(p);
|
|
656
|
+
|
|
657
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m conflicts \x1b[0m`);
|
|
695
658
|
|
|
659
|
+
const spin = p.spinner();
|
|
660
|
+
spin.start('Loading conflicts…');
|
|
696
661
|
const config = await loadConfig(cfgPath, cwd);
|
|
697
662
|
const mongofire = requireMongofire();
|
|
698
|
-
|
|
699
663
|
await mongofire.start(config);
|
|
700
664
|
const list = await mongofire.conflicts();
|
|
665
|
+
spin.stop(`${list.length} conflict(s) found`);
|
|
701
666
|
|
|
702
667
|
if (!list.length) {
|
|
703
|
-
|
|
668
|
+
p.outro('\x1b[32mNo unresolved conflicts\x1b[0m');
|
|
704
669
|
await mongofire.stop();
|
|
705
670
|
process.exit(0);
|
|
706
671
|
}
|
|
707
672
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
{ name: 'Do nothing', value: 'nothing' },
|
|
725
|
-
],
|
|
726
|
-
default: 0,
|
|
727
|
-
},
|
|
728
|
-
]);
|
|
673
|
+
const rows = list.map((c) =>
|
|
674
|
+
[`\x1b[1m${c.collection}/${c.docId}\x1b[0m \x1b[2mop:${c.type} v${c.version}\x1b[0m`, c.lastError ? ` \x1b[31m${c.lastError}\x1b[0m` : ''].filter(Boolean).join('\n')
|
|
675
|
+
).join('\n\n');
|
|
676
|
+
p.note(rows, `${list.length} unresolved conflict(s)`);
|
|
677
|
+
|
|
678
|
+
const action = await p.select({
|
|
679
|
+
message: 'How do you want to resolve?',
|
|
680
|
+
options: [
|
|
681
|
+
{ value: 'retry_all', label: 'Retry all', hint: 'reset to pending, sync re-attempts' },
|
|
682
|
+
{ value: 'dismiss_all', label: 'Dismiss all', hint: 'discard — keep local state' },
|
|
683
|
+
{ value: 'pick_retry', label: 'Pick one to retry' },
|
|
684
|
+
{ value: 'pick_dismiss', label: 'Pick one to dismiss' },
|
|
685
|
+
{ value: 'nothing', label: 'Do nothing' },
|
|
686
|
+
],
|
|
687
|
+
});
|
|
688
|
+
if (p.isCancel(action)) return _cancelled(p);
|
|
729
689
|
|
|
730
690
|
if (action === 'nothing') {
|
|
731
|
-
|
|
691
|
+
p.outro('\x1b[2mNo changes made\x1b[0m');
|
|
732
692
|
} else if (action === 'retry_all') {
|
|
693
|
+
const s2 = p.spinner(); s2.start('Retrying…');
|
|
733
694
|
for (const c of list) await mongofire.retryConflict(c.opId);
|
|
734
|
-
|
|
695
|
+
s2.stop(`Retried ${list.length} conflict(s)`);
|
|
696
|
+
p.outro('\x1b[32mAll conflicts reset to pending\x1b[0m');
|
|
735
697
|
} else if (action === 'dismiss_all') {
|
|
698
|
+
const s2 = p.spinner(); s2.start('Dismissing…');
|
|
736
699
|
for (const c of list) await mongofire.dismissConflict(c.opId);
|
|
737
|
-
|
|
700
|
+
s2.stop(`Dismissed ${list.length} conflict(s)`);
|
|
701
|
+
p.outro('\x1b[32mAll conflicts dismissed\x1b[0m');
|
|
738
702
|
} else {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
value: c.opId,
|
|
747
|
-
})),
|
|
748
|
-
default: 0,
|
|
749
|
-
},
|
|
750
|
-
]);
|
|
751
|
-
if (action === 'pick_retry') {
|
|
752
|
-
await mongofire.retryConflict(opId);
|
|
753
|
-
console.log('\n\u2705 Conflict reset to pending.\n');
|
|
754
|
-
} else {
|
|
755
|
-
await mongofire.dismissConflict(opId);
|
|
756
|
-
console.log('\n\u2705 Conflict dismissed.\n');
|
|
757
|
-
}
|
|
703
|
+
const opId = await p.select({
|
|
704
|
+
message: 'Select conflict',
|
|
705
|
+
options: list.map((c) => ({ value: c.opId, label: `${c.collection}/${c.docId}`, hint: `${c.type} v${c.version}` })),
|
|
706
|
+
});
|
|
707
|
+
if (p.isCancel(opId)) return _cancelled(p);
|
|
708
|
+
if (action === 'pick_retry') { await mongofire.retryConflict(opId); p.outro('\x1b[32mConflict reset to pending\x1b[0m'); }
|
|
709
|
+
else { await mongofire.dismissConflict(opId); p.outro('\x1b[32mConflict dismissed\x1b[0m'); }
|
|
758
710
|
}
|
|
759
711
|
|
|
760
712
|
await mongofire.stop();
|
|
761
713
|
process.exit(0);
|
|
762
714
|
}
|
|
763
715
|
|
|
764
|
-
// ───
|
|
765
|
-
|
|
766
|
-
|
|
716
|
+
// ─── Compact ──────────────────────────────────────────────────────────────────
|
|
717
|
+
async function doCompact() {
|
|
718
|
+
const p = await loadClack();
|
|
767
719
|
const cwd = process.cwd();
|
|
768
720
|
const cfgPath = resolveConfigPath(cwd);
|
|
769
|
-
if (!cfgPath)
|
|
770
|
-
|
|
721
|
+
if (!cfgPath) return _noConfig(p);
|
|
722
|
+
|
|
723
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m compact \x1b[0m`);
|
|
724
|
+
|
|
725
|
+
const daysArg = [...flags].find((f) => f.startsWith('--days='));
|
|
726
|
+
const days = daysArg ? parseInt(daysArg.split('=')[1], 10) : 7;
|
|
727
|
+
const colArg = [...flags].find((f) => f.startsWith('--collection='));
|
|
728
|
+
const col = colArg ? colArg.split('=')[1] : null;
|
|
729
|
+
|
|
730
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650) {
|
|
731
|
+
console.error('✖ Invalid --days value (must be 1–3650)');
|
|
771
732
|
process.exit(1);
|
|
772
733
|
}
|
|
773
734
|
|
|
735
|
+
const spin = p.spinner();
|
|
736
|
+
spin.start(`Compacting changelog (retention: ${days} day(s)${col ? `, collection: ${col}` : ''})…`);
|
|
737
|
+
|
|
774
738
|
const config = await loadConfig(cfgPath, cwd);
|
|
775
739
|
const mongofire = requireMongofire();
|
|
740
|
+
await mongofire.start(config);
|
|
741
|
+
const result = await mongofire.compact({ retentionDays: days, collection: col || undefined, verbose: false });
|
|
742
|
+
spin.stop(`Done — scanned ${result.scanned}, deleted ${result.deleted}, kept ${result.kept} (${result.durationMs}ms)`);
|
|
776
743
|
|
|
744
|
+
p.outro(
|
|
745
|
+
result.deleted > 0
|
|
746
|
+
? `\x1b[32m${result.deleted} superseded row(s) removed\x1b[0m`
|
|
747
|
+
: '\x1b[2mNothing to compact — changelog is already lean\x1b[0m'
|
|
748
|
+
);
|
|
749
|
+
await mongofire.stop();
|
|
750
|
+
process.exit(0);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ─── Reconcile ────────────────────────────────────────────────────────────────
|
|
754
|
+
async function doReconcile(opts = {}) {
|
|
755
|
+
const p = await loadClack();
|
|
756
|
+
const cwd = process.cwd();
|
|
757
|
+
const cfgPath = resolveConfigPath(cwd);
|
|
758
|
+
if (!cfgPath) return _noConfig(p);
|
|
759
|
+
|
|
760
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m reconcile \x1b[0m`);
|
|
761
|
+
|
|
762
|
+
const fullScan = opts.fullScan !== false;
|
|
763
|
+
const scanLabel = fullScan ? 'full scan' : 'Phase 1 only (--no-full-scan)';
|
|
764
|
+
|
|
765
|
+
const spin = p.spinner();
|
|
766
|
+
spin.start(`Scanning for lost writes… \x1b[2m(${scanLabel})\x1b[0m`);
|
|
767
|
+
const config = await loadConfig(cfgPath, cwd);
|
|
768
|
+
const mongofire = requireMongofire();
|
|
777
769
|
await mongofire.start(config);
|
|
778
|
-
console.log('\n\uD83D\uDD0D Running reconciliation scan...\n');
|
|
779
770
|
|
|
780
|
-
const
|
|
771
|
+
const reconcileArg = opts.collection
|
|
772
|
+
? opts.collection
|
|
773
|
+
: { fullScan, verbose: true };
|
|
774
|
+
|
|
775
|
+
const results = await mongofire.reconcile(reconcileArg, { fullScan, verbose: true });
|
|
781
776
|
const total = results.reduce((s, r) => s + (r.totalQueued || 0), 0);
|
|
777
|
+
spin.stop('Scan complete');
|
|
782
778
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
console.log(' ' + r.collection + ': P1=' + r.phase1.queued + ' P2=' + r.phase2.queued + ' queued');
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
console.log('\n Total re-queued: ' + total);
|
|
793
|
-
console.log(total > 0 ? ' \u2705 Lost writes recovered.\n' : ' \u2705 No untracked operations found.\n');
|
|
779
|
+
const rows = results.map((r) => {
|
|
780
|
+
if (r.error) return `\x1b[31m✖\x1b[0m ${r.collection} \x1b[2m${r.error}\x1b[0m`;
|
|
781
|
+
const q = (r.phase1?.queued || 0) + (r.phase2?.queued || 0);
|
|
782
|
+
return `${q > 0 ? '\x1b[33m↻\x1b[0m' : '\x1b[32m✓\x1b[0m'} \x1b[1m${r.collection}\x1b[0m \x1b[2mP1:${r.phase1?.queued || 0} P2:${r.phase2?.queued || 0} re-queued\x1b[0m`;
|
|
783
|
+
}).join('\n');
|
|
784
|
+
p.note(rows, 'Results');
|
|
794
785
|
|
|
786
|
+
p.outro(total > 0 ? `\x1b[33m${total} lost write(s) recovered\x1b[0m` : '\x1b[32mNo untracked operations found\x1b[0m');
|
|
795
787
|
await mongofire.stop();
|
|
796
788
|
process.exit(0);
|
|
797
789
|
}
|
|
798
790
|
|
|
799
|
-
// ───
|
|
791
|
+
// ─── Config templates ─────────────────────────────────────────────────────────
|
|
792
|
+
function buildConfigTemplate(ms, opts = {}) {
|
|
793
|
+
const cols = (opts.collections?.length ? opts.collections : ['orders', 'products', 'users']).map((c) => ` '${c}',`).join('\n');
|
|
794
|
+
const realtime = !!opts.realtime;
|
|
795
|
+
const syncInterval = opts.syncInterval || 30000;
|
|
796
|
+
const ownerField = (opts.ownerField && opts.ownerField !== '*') ? opts.ownerField : '*';
|
|
797
|
+
const tsCheck = opts.typescript ? '\n// @ts-check\n/** @type {import("mongofire").SyncConfig} */' : '';
|
|
798
|
+
const verbose = !!opts.verbose;
|
|
799
|
+
const syncLog = verbose
|
|
800
|
+
? " console.log(`[MongoFire] ↑${result.uploaded} ↓${result.downloaded} DEL:${result.deleted}`);"
|
|
801
|
+
: " if (result.deleted + result.downloaded + result.uploaded > 0) {\n console.log(`[MongoFire] Synced: ↑${result.uploaded} ↓${result.downloaded} DEL:${result.deleted}`);\n }";
|
|
802
|
+
|
|
803
|
+
const body = [
|
|
804
|
+
` // ── Required ─────────────────────────────────────────────────────────`,
|
|
805
|
+
` localUri: process.env.LOCAL_URI || 'mongodb://127.0.0.1:27017',`,
|
|
806
|
+
` atlasUri: process.env.ATLAS_URI,`,
|
|
807
|
+
` dbName: process.env.DB_NAME || 'myapp',`,
|
|
808
|
+
``,
|
|
809
|
+
` collections: [`,
|
|
810
|
+
cols,
|
|
811
|
+
` ],`,
|
|
812
|
+
``,
|
|
813
|
+
` // ── Sync behaviour ───────────────────────────────────────────────────`,
|
|
814
|
+
` syncInterval: ${syncInterval}, // ms — minimum 500`,
|
|
815
|
+
` batchSize: 200, // docs per upload batch (1–10000)`,
|
|
816
|
+
` realtime: ${realtime}, // Atlas Change Streams — instant push`,
|
|
817
|
+
` syncOwner: '${ownerField}', // '*' = all data · 'userId' = per-user isolation`,
|
|
818
|
+
``,
|
|
819
|
+
` // ── Maintenance (safe defaults — change when needed) ─────────────────`,
|
|
820
|
+
` cleanDays: 7, // auto-delete synced records older than N days`,
|
|
821
|
+
` reconcileOnStart: true, // repair writes lost to crashes on startup`,
|
|
822
|
+
` verbose: ${String(verbose).padEnd(5)}, // log every sync cycle (useful for debugging)`,
|
|
823
|
+
``,
|
|
824
|
+
` // ── Advanced (disabled by default — uncomment to enable) ─────────────`,
|
|
825
|
+
` // autoCompact: true, // compact changelog (60–80% size reduction)`,
|
|
826
|
+
` // compactEvery: 100, // run compaction every N sync cycles`,
|
|
827
|
+
` // compactRetentionDays: 7, // keep rows newer than this after compaction`,
|
|
828
|
+
` // rateLimiter: { // protect Atlas from sync storms`,
|
|
829
|
+
` // tokenBucket: { capacity: 100, refillRate: 50 },`,
|
|
830
|
+
` // circuitBreaker: { failureThreshold: 5, timeout: 30000 },`,
|
|
831
|
+
` // },`,
|
|
832
|
+
ownerField === '*'
|
|
833
|
+
? ` // syncOwner: 'userId', // uncomment for multi-tenant (per-user sync)`
|
|
834
|
+
: ` // multi-tenant ON: only syncs docs where document.${ownerField} === syncOwner value`,
|
|
835
|
+
``,
|
|
836
|
+
` // ── Callbacks ────────────────────────────────────────────────────────`,
|
|
837
|
+
` onSync(result) {`,
|
|
838
|
+
syncLog,
|
|
839
|
+
` },`,
|
|
840
|
+
` onError(err) {`,
|
|
841
|
+
` console.error('[MongoFire] Sync error:', err.message);`,
|
|
842
|
+
` },`,
|
|
843
|
+
].join('\n');
|
|
800
844
|
|
|
801
|
-
|
|
845
|
+
if (ms === 'esm') {
|
|
846
|
+
return `${tsCheck}\nimport 'dotenv/config';\n\n// MongoFire configuration (ESM)\nexport default {\n${body}\n};\n`;
|
|
847
|
+
}
|
|
848
|
+
return `${tsCheck}\n'use strict';\ntry { require('dotenv').config(); } catch (_) {}\n\n// MongoFire configuration (CommonJS)\nmodule.exports = {\n${body}\n};\n`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function buildEntryTemplate(ms, opts = {}) {
|
|
852
|
+
const syncLog = opts.verbose
|
|
853
|
+
? `_mongofire.on('sync', (r) => console.log(\`🔄 [MongoFire] Synced: ↑\${r.uploaded} ↓\${r.downloaded} DEL:\${r.deleted}\`));`
|
|
854
|
+
: `_mongofire.on('sync', (r) => {\n if (r.uploaded + r.downloaded + r.deleted > 0) {\n console.log(\`🔄 [MongoFire] Sync complete — ↑\${r.uploaded} uploaded ↓\${r.downloaded} downloaded 🗑 \${r.deleted} deleted\`);\n }\n});`;
|
|
855
|
+
|
|
856
|
+
if (ms === 'esm') {
|
|
857
|
+
return [
|
|
858
|
+
`// mongofire.js — ESM production entry point`,
|
|
859
|
+
`//`,
|
|
860
|
+
`// ⚠️ DO NOT call mongoose.connect() — MongoFire handles this automatically.`,
|
|
861
|
+
`//`,
|
|
862
|
+
`// server.js usage:`,
|
|
863
|
+
`// import { startApp } from './mongofire.js';`,
|
|
864
|
+
`// startApp(app, process.env.PORT || 3000);`,
|
|
865
|
+
`//`,
|
|
866
|
+
`// model.js usage:`,
|
|
867
|
+
`// import { plugin } from './mongofire.js';`,
|
|
868
|
+
`// UserSchema.plugin(plugin('users'));`,
|
|
869
|
+
``,
|
|
870
|
+
`import { createRequire } from 'module';`,
|
|
871
|
+
`import { fileURLToPath, pathToFileURL } from 'url';`,
|
|
872
|
+
`import path from 'path';`,
|
|
873
|
+
`import fs from 'fs';`,
|
|
874
|
+
``,
|
|
875
|
+
`const require = createRequire(import.meta.url);`,
|
|
876
|
+
`const __dirname = path.dirname(fileURLToPath(import.meta.url));`,
|
|
877
|
+
``,
|
|
878
|
+
`// Load .env synchronously — dotenv is CJS, always safe to require()`,
|
|
879
|
+
`try {`,
|
|
880
|
+
` const e = [path.join(process.cwd(),'.env'), path.join(__dirname,'.env')].find(p => fs.existsSync(p));`,
|
|
881
|
+
` if (e) require('dotenv').config({ path: e });`,
|
|
882
|
+
`} catch (_) {}`,
|
|
883
|
+
``,
|
|
884
|
+
`const _mongofire = require('mongofire');`,
|
|
885
|
+
``,
|
|
886
|
+
`_mongofire.on('localReady', () => console.log('✅ [MongoFire] Local MongoDB connected'));`,
|
|
887
|
+
`_mongofire.on('online', () => console.log('🌐 [MongoFire] Atlas connected — sync active'));`,
|
|
888
|
+
`_mongofire.on('offline', () => console.log('📴 [MongoFire] Atlas offline — changes queued locally'));`,
|
|
889
|
+
syncLog,
|
|
890
|
+
`_mongofire.on('error', (err) => console.error('❌ [MongoFire] Error:', err?.message || err));`,
|
|
891
|
+
``,
|
|
892
|
+
`// loadConfigFile() auto-detects CJS/ESM — safe on Node 22+ (no ERR_INTERNAL_ASSERTION)`,
|
|
893
|
+
`async function _loadAndStart() {`,
|
|
894
|
+
` const result = await _mongofire.loadConfigFile([process.cwd(), __dirname]);`,
|
|
895
|
+
` if (!result) { console.error('\\n❌ mongofire.config.js not found. Run: npx mongofire init\\n'); process.exit(1); }`,
|
|
896
|
+
` if (!result.config?.collections?.length) { console.error('\\n❌ mongofire.config.js has no collections.\\n'); process.exit(1); }`,
|
|
897
|
+
` return _mongofire.start(result.config);`,
|
|
898
|
+
`}`,
|
|
899
|
+
`const _startPromise = _loadAndStart();`,
|
|
900
|
+
``,
|
|
901
|
+
`export async function startApp(app, port = 3000) {`,
|
|
902
|
+
` await _startPromise.catch(() => {});`,
|
|
903
|
+
` try {`,
|
|
904
|
+
` await _mongofire.localReady;`,
|
|
905
|
+
` } catch (err) {`,
|
|
906
|
+
` console.error('\\n❌ [MongoFire] Local MongoDB failed to connect.\\n' +`,
|
|
907
|
+
` \` Reason: \${err?.message || err}\\n\` +`,
|
|
908
|
+
` ' Server will NOT start in a broken state.\\n');`,
|
|
909
|
+
` process.exit(1);`,
|
|
910
|
+
` }`,
|
|
911
|
+
` return new Promise((resolve, reject) => {`,
|
|
912
|
+
` const server = app.listen(Number(port) || 3000, (err) => {`,
|
|
913
|
+
` if (err) { console.error('❌ Server listen failed:', err.message); reject(err); process.exit(1); }`,
|
|
914
|
+
` console.log(\`🚀 [MongoFire] Server ready on port \${port}\`);`,
|
|
915
|
+
` resolve(server);`,
|
|
916
|
+
` });`,
|
|
917
|
+
` server.on('error', (err) => { console.error('❌ Server error:', err.message); reject(err); process.exit(1); });`,
|
|
918
|
+
` });`,
|
|
919
|
+
`}`,
|
|
920
|
+
``,
|
|
921
|
+
`export function plugin(collectionName, options = {}) {`,
|
|
922
|
+
` return _mongofire.plugin(collectionName, options);`,
|
|
923
|
+
`}`,
|
|
924
|
+
``,
|
|
925
|
+
`export const localReady = _mongofire.localReady;`,
|
|
926
|
+
`export const ready = _startPromise;`,
|
|
927
|
+
`export { _mongofire as mongofire };`,
|
|
928
|
+
`export default _mongofire;`,
|
|
929
|
+
``,
|
|
930
|
+
].join('\n');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return [
|
|
934
|
+
`// mongofire.js — CJS production entry point`,
|
|
935
|
+
`//`,
|
|
936
|
+
`// ⚠️ DO NOT call mongoose.connect() — MongoFire handles this automatically.`,
|
|
937
|
+
`//`,
|
|
938
|
+
`// server.js usage:`,
|
|
939
|
+
`// const { startApp } = require('./mongofire');`,
|
|
940
|
+
`// startApp(app, process.env.PORT || 3000);`,
|
|
941
|
+
`//`,
|
|
942
|
+
`// model.js usage:`,
|
|
943
|
+
`// const { plugin } = require('./mongofire');`,
|
|
944
|
+
`// UserSchema.plugin(plugin('users'));`,
|
|
945
|
+
``,
|
|
946
|
+
`'use strict';`,
|
|
947
|
+
`const _mongofire = require('mongofire');`,
|
|
948
|
+
`const _config = require('./mongofire.config');`,
|
|
949
|
+
``,
|
|
950
|
+
`_mongofire.on('localReady', () => console.log('✅ [MongoFire] Local MongoDB connected'));`,
|
|
951
|
+
`_mongofire.on('online', () => console.log('🌐 [MongoFire] Atlas connected — sync active'));`,
|
|
952
|
+
`_mongofire.on('offline', () => console.log('📴 [MongoFire] Atlas offline — changes queued locally'));`,
|
|
953
|
+
syncLog,
|
|
954
|
+
`_mongofire.on('error', (err) => console.error('❌ [MongoFire] Error:', err?.message || err));`,
|
|
955
|
+
``,
|
|
956
|
+
`const _startPromise = _mongofire.start(_config);`,
|
|
957
|
+
``,
|
|
958
|
+
`async function startApp(app, port = 3000) {`,
|
|
959
|
+
` try {`,
|
|
960
|
+
` await _mongofire.localReady;`,
|
|
961
|
+
` } catch (err) {`,
|
|
962
|
+
` console.error('\\n❌ [MongoFire] Local MongoDB failed to connect.\\n' +`,
|
|
963
|
+
` \` Reason: \${err?.message || err}\\n\` +`,
|
|
964
|
+
` ' Server will NOT start in a broken state.\\n');`,
|
|
965
|
+
` process.exit(1);`,
|
|
966
|
+
` }`,
|
|
967
|
+
` return new Promise((resolve, reject) => {`,
|
|
968
|
+
` const server = app.listen(Number(port) || 3000, (err) => {`,
|
|
969
|
+
` if (err) { console.error('❌ Server listen failed:', err.message); reject(err); process.exit(1); }`,
|
|
970
|
+
` console.log(\`🚀 [MongoFire] Server ready on port \${port}\`);`,
|
|
971
|
+
` resolve(server);`,
|
|
972
|
+
` });`,
|
|
973
|
+
` server.on('error', (err) => { console.error('❌ Server error:', err.message); reject(err); process.exit(1); });`,
|
|
974
|
+
` });`,
|
|
975
|
+
`}`,
|
|
976
|
+
``,
|
|
977
|
+
`function plugin(collectionName, options = {}) {`,
|
|
978
|
+
` return _mongofire.plugin(collectionName, options);`,
|
|
979
|
+
`}`,
|
|
980
|
+
``,
|
|
981
|
+
`module.exports = { startApp, plugin, mongofire: _mongofire, ready: _startPromise, localReady: _mongofire.localReady };`,
|
|
982
|
+
``,
|
|
983
|
+
].join('\n');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ─── File helpers ─────────────────────────────────────────────────────────────
|
|
987
|
+
function ensureEnvFile(envPath) {
|
|
988
|
+
const defaults = [
|
|
989
|
+
['ATLAS_URI', 'mongodb+srv://USERNAME:PASSWORD@cluster0.xxxxx.mongodb.net/'],
|
|
990
|
+
['LOCAL_URI', 'mongodb://127.0.0.1:27017'],
|
|
991
|
+
['DB_NAME', 'myapp'],
|
|
992
|
+
];
|
|
993
|
+
if (!fs.existsSync(envPath)) {
|
|
994
|
+
fs.writeFileSync(envPath, '# MongoFire\n' + defaults.map(([k, v]) => `${k}=${v}`).join('\n') + '\n', 'utf8');
|
|
995
|
+
return { action: 'created' };
|
|
996
|
+
}
|
|
997
|
+
const env = fs.readFileSync(envPath, 'utf8');
|
|
998
|
+
const missing = defaults.filter(([k]) => !new RegExp(`^\\s*${escapeRE(k)}\\s*=`, 'm').test(env));
|
|
999
|
+
if (!missing.length) return { action: 'unchanged' };
|
|
1000
|
+
fs.appendFileSync(envPath, '\n# MongoFire\n' + missing.map(([k, v]) => `${k}=${v}`).join('\n') + '\n', 'utf8');
|
|
1001
|
+
return { action: 'updated' };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function writeFileIfNeeded(filePath, content, force) {
|
|
1005
|
+
const exists = fs.existsSync(filePath);
|
|
1006
|
+
if (exists && !force) return { action: 'skipped' };
|
|
1007
|
+
|
|
1008
|
+
// Write to a temp file first, then rename — avoids Windows permission
|
|
1009
|
+
// errors when overwriting a file that may be open in another process.
|
|
1010
|
+
const tmpPath = filePath + '.mftmp';
|
|
1011
|
+
try {
|
|
1012
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
1013
|
+
// On Windows, renameSync fails if dest exists — remove it first
|
|
1014
|
+
if (exists) {
|
|
1015
|
+
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
1016
|
+
}
|
|
1017
|
+
fs.renameSync(tmpPath, filePath);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
// Fallback: direct write (works on most systems)
|
|
1020
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
1021
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8', flag: 'w' });
|
|
1022
|
+
}
|
|
1023
|
+
return { action: exists ? 'overwritten' : 'created' };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function collectPackageHints(cwd) {
|
|
1027
|
+
const pkg = readPackageJson(cwd);
|
|
1028
|
+
const all = Object.assign({}, pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies);
|
|
1029
|
+
const out = [];
|
|
1030
|
+
if (!all.mongoose) out.push('mongoose not installed — npm i mongoose');
|
|
1031
|
+
if (!all.dotenv) out.push('dotenv not installed — npm i dotenv');
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function _fileRow(name, action) {
|
|
1036
|
+
const icons = { created: '\x1b[32m+\x1b[0m', overwritten: '\x1b[33m~\x1b[0m', skipped: '\x1b[2m–\x1b[0m', unchanged: '\x1b[2m–\x1b[0m', updated: '\x1b[32m+\x1b[0m' };
|
|
1037
|
+
const labels = { created: '\x1b[2mcreated\x1b[0m', overwritten: '\x1b[33mupdated\x1b[0m', skipped: '\x1b[2mexists\x1b[0m', unchanged: '\x1b[2munchanged\x1b[0m', updated: '\x1b[2mupdated\x1b[0m' };
|
|
1038
|
+
return ` ${icons[action] || ' '} ${name.padEnd(24)}${labels[action] || action}`;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ─── Reset Local ──────────────────────────────────────────────────────────────
|
|
1042
|
+
// Safe interactive wipe of the local MongoDB database.
|
|
1043
|
+
// Drops all _mf_* internal collections plus the configured data collections so
|
|
1044
|
+
// that the next startup triggers a clean bootstrap from Atlas, with no stale ops.
|
|
1045
|
+
async function doResetLocal() {
|
|
1046
|
+
const p = await loadClack();
|
|
1047
|
+
const cwd = process.cwd();
|
|
1048
|
+
const cfgPath = resolveConfigPath(cwd);
|
|
1049
|
+
if (!cfgPath) return _noConfig(p);
|
|
1050
|
+
|
|
1051
|
+
p.intro(`\x1b[33m 🔥 MongoFire \x1b[0m\x1b[2m reset-local \x1b[0m`);
|
|
1052
|
+
|
|
1053
|
+
p.note(
|
|
1054
|
+
[
|
|
1055
|
+
'\x1b[1mThis will permanently delete:\x1b[0m',
|
|
1056
|
+
' • All MongoFire internal collections (_mf_changetrack, _mf_docmeta,',
|
|
1057
|
+
' _mf_sync_state, _mf_ops)',
|
|
1058
|
+
' • All configured data collections listed in mongofire.config.js',
|
|
1059
|
+
'',
|
|
1060
|
+
'The next startup will re-bootstrap from Atlas.',
|
|
1061
|
+
'\x1b[31mAny unsynced local changes will be lost.\x1b[0m',
|
|
1062
|
+
].join('\n'),
|
|
1063
|
+
'WARNING'
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const ok = await p.confirm({
|
|
1067
|
+
message: 'Are you sure you want to reset the entire local database?',
|
|
1068
|
+
initialValue: false,
|
|
1069
|
+
});
|
|
1070
|
+
if (p.isCancel(ok) || !ok) { p.cancel('Cancelled — nothing was deleted.'); return; }
|
|
1071
|
+
|
|
1072
|
+
const spin = p.spinner();
|
|
1073
|
+
spin.start('Connecting to local MongoDB…');
|
|
1074
|
+
|
|
1075
|
+
let config;
|
|
802
1076
|
try {
|
|
803
|
-
|
|
804
|
-
} catch (
|
|
1077
|
+
config = await loadConfig(cfgPath, cwd);
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
spin.stop('Failed to load config');
|
|
1080
|
+
p.cancel(`Config error: ${err.message}`);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const mongofire = requireMongofire();
|
|
1085
|
+
await mongofire.start(config);
|
|
1086
|
+
|
|
1087
|
+
const localDb = mongofire._conn?.local || mongofire.conn?.local;
|
|
1088
|
+
const userCols = Array.isArray(config.collections) ? config.collections : [];
|
|
1089
|
+
const mfCols = ['_mf_changetrack', '_mf_docmeta', '_mf_sync_state', '_mf_ops', '_mf_devices'];
|
|
1090
|
+
const allToDrop = [...new Set([...mfCols, ...userCols])];
|
|
1091
|
+
|
|
1092
|
+
spin.start(`Dropping ${allToDrop.length} collection(s)…`);
|
|
1093
|
+
|
|
1094
|
+
let dropped = 0, failed = 0;
|
|
1095
|
+
for (const col of allToDrop) {
|
|
805
1096
|
try {
|
|
806
|
-
|
|
1097
|
+
await localDb.collection(col).drop();
|
|
1098
|
+
dropped++;
|
|
807
1099
|
} catch (err) {
|
|
808
|
-
|
|
809
|
-
|
|
1100
|
+
// collection may not exist — that is fine
|
|
1101
|
+
if (!err.message?.includes('ns not found') && !err.codeName?.includes('NamespaceNotFound')) {
|
|
1102
|
+
failed++;
|
|
1103
|
+
console.error(`\n✖ Failed to drop "${col}":`, err.message);
|
|
1104
|
+
}
|
|
810
1105
|
}
|
|
811
1106
|
}
|
|
1107
|
+
|
|
1108
|
+
spin.stop(`Dropped ${dropped} collection(s)${failed ? `, ${failed} error(s)` : ''}`);
|
|
1109
|
+
|
|
1110
|
+
p.outro(
|
|
1111
|
+
failed === 0
|
|
1112
|
+
? '\x1b[32mLocal database reset complete.\x1b[0m Start your app to re-bootstrap from Atlas.'
|
|
1113
|
+
: `\x1b[33mReset finished with ${failed} error(s).\x1b[0m Check logs above.`
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
await mongofire.stop();
|
|
1117
|
+
process.exit(0);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ─── Shared utils ─────────────────────────────────────────────────────────────
|
|
1121
|
+
function _cancelled(p) { p.cancel('Cancelled.'); process.exit(0); }
|
|
1122
|
+
function _noConfig(p) { p.cancel('No mongofire.config.js found — run npx mongofire init first.'); process.exit(1); }
|
|
1123
|
+
|
|
1124
|
+
function detectModuleSystem(cwd, override) {
|
|
1125
|
+
if (override && override !== 'auto') return override;
|
|
1126
|
+
const pkg = readPackageJson(cwd);
|
|
1127
|
+
if (pkg?.type === 'module') return 'esm';
|
|
1128
|
+
if (pkg?.type === 'commonjs') return 'cjs';
|
|
1129
|
+
const probes = ['index.js', 'app.js', 'server.js', 'main.js', path.join('src', 'index.js'), path.join('src', 'main.js')];
|
|
1130
|
+
for (const rel of probes) {
|
|
1131
|
+
const full = path.join(cwd, rel);
|
|
1132
|
+
if (!fs.existsSync(full)) continue;
|
|
1133
|
+
const code = fs.readFileSync(full, 'utf8');
|
|
1134
|
+
if (/\bimport\s.+from\s+['"]/.test(code) || /\bexport\s+default\b/.test(code)) return 'esm';
|
|
1135
|
+
if (/\brequire\(/.test(code) || /\bmodule\.exports\b/.test(code)) return 'cjs';
|
|
1136
|
+
}
|
|
1137
|
+
return 'cjs';
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function requireMongofire() {
|
|
1141
|
+
try { return require(path.join(__dirname, '..', 'dist', 'src', 'index.cjs')); } catch (_) {}
|
|
1142
|
+
try { return require(path.join(__dirname, '..', 'src', 'index.cjs')); } catch (err) {
|
|
1143
|
+
console.error('✖ MongoFire core load failed:', err.message); process.exit(1);
|
|
1144
|
+
}
|
|
812
1145
|
}
|
|
813
1146
|
|
|
814
1147
|
function resolveConfigPath(cwd) {
|
|
@@ -819,22 +1152,19 @@ function resolveConfigPath(cwd) {
|
|
|
819
1152
|
return null;
|
|
820
1153
|
}
|
|
821
1154
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (err && err.code === 'ERR_REQUIRE_ESM') return loadESM(configPath);
|
|
831
|
-
throw err;
|
|
832
|
-
}
|
|
1155
|
+
// AUTO-DETECT: always use dynamic import() for config files.
|
|
1156
|
+
// This works for CJS projects, ESM projects ("type":"module"), .cjs, .mjs,
|
|
1157
|
+
// and all Node 14+ versions without ERR_INTERNAL_ASSERTION or ERR_REQUIRE_ESM.
|
|
1158
|
+
// import() wraps CJS module.exports as { default: ... } — we unwrap it below.
|
|
1159
|
+
// There is no longer any need to branch on extension or package.json "type".
|
|
1160
|
+
async function loadConfig(configPath) {
|
|
1161
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
1162
|
+
return (mod.default !== undefined) ? mod.default : mod;
|
|
833
1163
|
}
|
|
834
1164
|
|
|
1165
|
+
// Kept as alias for any internal callers that still reference loadESM directly.
|
|
835
1166
|
async function loadESM(filePath) {
|
|
836
|
-
|
|
837
|
-
return mod.default || mod;
|
|
1167
|
+
return loadConfig(filePath);
|
|
838
1168
|
}
|
|
839
1169
|
|
|
840
1170
|
function readPackageJson(cwd) {
|
|
@@ -843,10 +1173,65 @@ function readPackageJson(cwd) {
|
|
|
843
1173
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
844
1174
|
}
|
|
845
1175
|
|
|
846
|
-
function
|
|
847
|
-
return readPackageJson(cwd)?.type === 'module';
|
|
848
|
-
}
|
|
1176
|
+
function escapeRE(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
849
1177
|
|
|
850
|
-
|
|
851
|
-
|
|
1178
|
+
// ─── Minimal fallback (if @clack/prompts not available) ───────────────────────
|
|
1179
|
+
function buildFallbackClack() {
|
|
1180
|
+
const readline = require('readline');
|
|
1181
|
+
let rl;
|
|
1182
|
+
function getRL() {
|
|
1183
|
+
if (!rl) {
|
|
1184
|
+
process.stdin.resume();
|
|
1185
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: IS_TTY });
|
|
1186
|
+
rl.on('error', () => {});
|
|
1187
|
+
}
|
|
1188
|
+
return rl;
|
|
1189
|
+
}
|
|
1190
|
+
function ask(q) {
|
|
1191
|
+
return new Promise((res) => {
|
|
1192
|
+
const r = getRL();
|
|
1193
|
+
r.once('close', () => res(''));
|
|
1194
|
+
r.question(q, (a) => res(a.trim()));
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
isCancel: () => false,
|
|
1199
|
+
intro: (t) => console.log(`\n┌ ${t}\n│`),
|
|
1200
|
+
outro: (m) => { console.log(`│\n└ ${m}\n`); if (rl) rl.close(); },
|
|
1201
|
+
cancel: (m) => { console.log(`│\n└ ${m}\n`); if (rl) rl.close(); },
|
|
1202
|
+
note: (body, title) => { if (title) console.log(`│\n│ ${title}`); body.split('\n').forEach((l) => console.log(`│ ${l}`)); },
|
|
1203
|
+
spinner: () => ({ start: (m) => process.stdout.write(`│\n│ ${m} `), stop: (m) => console.log(`→ ${m}`) }),
|
|
1204
|
+
async select({ message, options, initialValue }) {
|
|
1205
|
+
console.log(`│\n◇ ${message}`);
|
|
1206
|
+
options.forEach((o, i) => {
|
|
1207
|
+
const hint = o.hint ? `\x1b[2m — ${o.hint}\x1b[0m` : '';
|
|
1208
|
+
const mark = o.value === initialValue ? ' \x1b[33m(default)\x1b[0m' : '';
|
|
1209
|
+
console.log(`│ ${i + 1}. ${o.label}${mark}${hint}`);
|
|
1210
|
+
});
|
|
1211
|
+
const def = options.findIndex((o) => o.value === initialValue);
|
|
1212
|
+
const ans = await ask(`│ Enter number [${def >= 0 ? def + 1 : 1}]: `);
|
|
1213
|
+
const idx = ans ? Math.max(0, parseInt(ans, 10) - 1) : (def >= 0 ? def : 0);
|
|
1214
|
+
return options[idx]?.value ?? options[0]?.value;
|
|
1215
|
+
},
|
|
1216
|
+
async multiselect({ message, options }) {
|
|
1217
|
+
console.log(`│\n◇ ${message} (comma-separated numbers, blank = none)`);
|
|
1218
|
+
options.forEach((o, i) => console.log(`│ ${i + 1}. ${o.label}${o.hint ? ' (' + o.hint + ')' : ''}`));
|
|
1219
|
+
const ans = await ask('│ Numbers: ');
|
|
1220
|
+
if (!ans.trim()) return [];
|
|
1221
|
+
return ans.split(',').map((s) => options[parseInt(s.trim(), 10) - 1]?.value).filter(Boolean);
|
|
1222
|
+
},
|
|
1223
|
+
async text({ message, defaultValue, placeholder }) {
|
|
1224
|
+
const hint = defaultValue ? ` (default: ${defaultValue})` : placeholder ? ` e.g. ${placeholder}` : '';
|
|
1225
|
+
const ans = await ask(`│\n◇ ${message}${hint}\n│ `);
|
|
1226
|
+
return (ans || '').trim() || defaultValue || '';
|
|
1227
|
+
},
|
|
1228
|
+
async confirm({ message, initialValue, hint }) {
|
|
1229
|
+
const defaultYes = initialValue !== false;
|
|
1230
|
+
if (hint) console.log(`│ \x1b[2m${hint}\x1b[0m`);
|
|
1231
|
+
const ans = await ask(`│\n◇ ${message} (${defaultYes ? 'Y/n' : 'y/N'}) `);
|
|
1232
|
+
const t = (ans || '').trim().toLowerCase();
|
|
1233
|
+
if (t === '') return defaultYes;
|
|
1234
|
+
return t === 'y' || t === 'yes';
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
852
1237
|
}
|