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/bin/mongofire.cjs CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // ─── Global error handlers (Windows popup fix) ────────────────────────────────
5
- // Windows pe unhandled error -> native crash popup. Ye handlers ensure karte hain
6
- // ki har error console me aaye, popup nahi.
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 MongoFire CLI Error:', err.message || err);
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 MongoFire CLI Error:', reason?.message || reason);
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('Use only one: --esm OR --cjs');
29
+ console.error('Use only one: --esm OR --cjs');
28
30
  process.exit(1);
29
31
  }
30
32
 
31
- // ─── Load .env early ─────────────────────────────────────────────────────────
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 envPath = path.join(process.cwd(), '.env');
38
- if (!fs.existsSync(envPath)) return;
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
- // ─── Route commands ───────────────────────────────────────────────────────────
53
- if (command === 'init') {
54
- const hasFlags = flags.has('--esm') || flags.has('--cjs') || flags.has('--force') || flags.has('-f');
55
-
56
- if (!hasFlags && IS_TTY) {
57
- // Full interactive mode - sirf real TTY pe
58
- doInitInteractive().catch((err) => {
59
- console.error(' Error:', err.message);
60
- process.exit(1);
61
- });
62
- } else if (!hasFlags && !IS_TTY) {
63
- // Non-TTY (Windows piped/scripted) - direct mode with auto-detect
64
- console.log('ℹ️ Non-interactive mode (no TTY detected)');
65
- console.log(' Use flags for full control: --cjs or --esm, --force\n');
66
- doInit({ force: false, moduleSystem: null });
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
- doInit({
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
- } else if (command === 'status') {
76
- doStatus().catch((err) => {
77
- console.error('❌ Error:', err.message);
78
- process.exit(1);
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
- } else if (command === 'conflicts') {
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
- } else if (command === 'reconcile') {
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
- } else {
114
- showHelp();
115
- }
96
+ \x1b[1mCommands\x1b[0m
116
97
 
117
- // ─── Help ─────────────────────────────────────────────────────────────────────
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
- function showHelp() {
120
- console.log(`
121
- \uD83D\uDD25 MongoFire CLI v${_getVersion()}
107
+ \x1b[1mFlags for \x1b[36minit\x1b[0m
122
108
 
123
- npx mongofire init [--force] [--esm|--cjs]
124
- Setup wizard (interactive if terminal supports it)
125
- --esm Force ESM templates
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
- npx mongofire status
130
- Show pending sync operation counts
113
+ \x1b[1mFlags for \x1b[36mclean\x1b[0m
131
114
 
132
- npx mongofire clean [--days=N]
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
- npx mongofire conflicts
137
- View, retry, or dismiss unresolved conflicts
117
+ \x1b[1mFlags for \x1b[36mreconcile\x1b[0m
138
118
 
139
- npx mongofire reconcile
140
- Recover writes lost from crashes
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
- Tip: Run with MONGOFIRE_DEBUG=1 for full error stack traces
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
- // ─── Inquirer / prompt helper ─────────────────────────────────────────────────
153
- // Inquirer.js use karo agar installed hai.
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
- const inquirer = require('inquirer');
160
- // Test ke liye: inquirer v8 prompt function exist karta hai
161
- if (typeof inquirer.prompt === 'function') {
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
- async function _readlinePrompt(questions) {
172
- // Windows-safe readline wrapper
173
- // Key fixes:
174
- // 1. process.stdin.resume() call before creating interface
175
- // 2. 'close' event handler to resolve hanging promises
176
- // 3. Error handler on readline interface
177
-
178
- process.stdin.resume(); // Windows fix: stdin resume karo
179
-
180
- const readline = require('readline');
181
- const rl = readline.createInterface({
182
- input: process.stdin,
183
- output: process.stdout,
184
- terminal: IS_TTY, // Windows fix: explicit terminal flag
185
- });
186
-
187
- rl.on('error', () => {}); // Windows fix: suppress readline errors
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('| +- ' + title + '\n');
148
+ for (const line of lines) {
149
+ if (line === '') {
150
+ process.stdout.write('| |\n');
151
+ } else if (line.startsWith('✓')) {
152
+ process.stdout.write('| | ' + pad(line) + '\n');
153
+ } else if (line.startsWith('✕')) {
154
+ process.stdout.write('| | ' + pad(line) + '\n');
155
+ } else if (line.startsWith('')) {
156
+ process.stdout.write('| | ' + pad(line) + '\n');
240
157
  } else {
241
- const msgText = typeof q.message === 'function' ? q.message(prevAnswers) : q.message;
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('| | ' + pad(line) + '\n');
246
159
  }
247
- });
160
+ }
161
+ if (tip) {
162
+ process.stdout.write('| +- 💡 ' + pad(tip) + '\n');
163
+ } else {
164
+ process.stdout.write('| +-\n');
165
+ }
248
166
  }
249
167
 
250
- // ─── Interactive Init ─────────────────────────────────────────────────────────
251
-
252
- async function doInitInteractive() {
253
- const cwd = process.cwd();
254
- const exists = fs.existsSync(path.join(cwd, 'mongofire.config.js'));
255
-
256
- console.log('\n\uD83D\uDD25 MongoFire Setup Wizard\n');
257
-
258
- const answers = await _prompt([
259
- {
260
- type: 'list',
261
- name: 'moduleSystem',
262
- message: 'Which module system does your project use?',
263
- choices: [
264
- { name: 'CommonJS (require / module.exports)', value: 'cjs' },
265
- { name: 'ESM (import / export)', value: 'esm' },
266
- { name: 'Auto-detect from package.json', value: 'auto' },
168
+ async function doInitWizard() {
169
+ const p = await loadClack();
170
+ const cwd = process.cwd();
171
+
172
+ p.intro(` 🔥 MongoFire  v${_getVersion()} `);
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
+ `${existingFiles} — already exists in this directory`,
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
- default: 0,
269
- },
270
- {
271
- type: 'confirm',
272
- name: 'force',
273
- message: exists
274
- ? 'mongofire.config.js already exists. Overwrite it?'
275
- : 'Create mongofire.config.js and mongofire.js?',
276
- default: !exists,
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('Opening config wizard...');
206
+ return doConfigWizard();
207
+ }
208
+ // action === 'overwrite' — fall through to full wizard below
307
209
  }
308
210
 
309
- const wasAutoDetected = answers.moduleSystem === 'auto';
310
- const moduleSystem = wasAutoDetected
311
- ? detectModuleSystem(cwd, null)
312
- : answers.moduleSystem;
313
-
314
- const collections = String(answers.collections || 'users,products')
315
- .split(',')
316
- .map((s) => s.trim())
317
- .filter(Boolean);
318
-
319
- doInit({
320
- force: true,
321
- moduleSystem,
322
- autoDetected: wasAutoDetected,
323
- collections,
324
- realtime: !!answers.realtime,
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
- const probes = [
368
- 'index.js', 'app.js', 'server.js', 'main.js',
369
- path.join('src', 'index.js'),
370
- path.join('src', 'main.js'),
371
- ];
372
- for (const rel of probes) {
373
- const full = path.join(cwd, rel);
374
- if (!fs.existsSync(full)) continue;
375
- const code = fs.readFileSync(full, 'utf8');
376
- if (/\bimport\s.+from\s+['"]/.test(code) || /\bexport\s+default\b/.test(code)) return 'esm';
377
- if (/\brequire\(/.test(code) || /\bmodule\.exports\b/.test(code)) return 'cjs';
378
- }
379
- return 'cjs';
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
- function buildConfigTemplate(moduleSystem, opts) {
385
- const cols = (opts && opts.collections && opts.collections.length)
386
- ? opts.collections.map((c) => ` '${c}',`).join('\n')
387
- : ` 'users',\n 'products',`;
388
- const realtime = !!(opts && opts.realtime);
389
- const syncInterval = (opts && opts.syncInterval) || 30000;
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
- if (moduleSystem === 'esm') {
392
- return [
393
- '/**',
394
- ' * MongoFire Config (ESM)',
395
- ' *',
396
- ' * dotenv .env file se ATLAS_URI etc. load karta hai.',
397
- " * 'dotenv/config' import karna ZAROORI hai taake process.env variables",
398
- ' * is config ke evaluate hone SE PEHLE set ho jayein.',
399
- ' * Install: npm i dotenv',
400
- ' */',
401
- "import 'dotenv/config';",
402
- '',
403
- 'export default {',
404
- " localUri: process.env.LOCAL_URI || 'mongodb://127.0.0.1:27017',",
405
- ' atlasUri: process.env.ATLAS_URI,',
406
- " dbName: process.env.DB_NAME || 'myapp',",
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
- return [
431
- "'use strict';",
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
- 'module.exports = {',
443
- " localUri: process.env.LOCAL_URI || 'mongodb://127.0.0.1:27017',",
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
- ' collections: [',
448
- cols,
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
- ' syncInterval: ' + syncInterval + ',',
452
- ' batchSize: 200,',
453
- " syncOwner: '*',",
454
- ' realtime: ' + realtime + ',',
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
- ' onSync(result) {',
457
- ' if (result.deleted + result.downloaded + result.uploaded > 0) {',
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
- ].join('\n');
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
- function buildEntryTemplate(moduleSystem) {
470
- if (moduleSystem === 'esm') {
471
- return [
472
- "import 'dotenv/config';",
473
- "import mongofire from 'mongofire';",
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
- "mongofire.on('online', () => console.log('[MongoFire] Online'));",
479
- "mongofire.on('offline', () => console.log('[MongoFire] Offline \u2014 working locally'));",
480
- 'mongofire.on(\'sync\', (r) => {',
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
- 'export { mongofire };',
487
- '',
488
- ].join('\n');
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
- return [
492
- "'use strict';",
493
- "const mongofire = require('mongofire');",
494
- "const config = require('./mongofire.config');",
495
- '',
496
- 'const ready = mongofire.start(config);',
497
- '',
498
- "mongofire.on('online', () => console.log('[MongoFire] Online'));",
499
- "mongofire.on('offline', () => console.log('[MongoFire] Offline \u2014 working locally'));",
500
- 'mongofire.on(\'sync\', (r) => {',
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 ? '● Enabled' : '○ Disabled';
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
- if (!fs.existsSync(envPath)) {
521
- let out = '# MongoFire\n';
522
- for (const [k, v] of defaults) out += `${k}=${v}\n`;
523
- fs.writeFileSync(envPath, out, 'utf8');
524
- return { action: 'created', added: defaults.map(([k]) => k) };
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
- const env = fs.readFileSync(envPath, 'utf8');
528
- const missing = defaults.filter(([k]) => !new RegExp(`^\\s*${escapeRE(k)}\\s*=`, 'm').test(env));
529
- if (!missing.length) return { action: 'unchanged', added: [] };
530
-
531
- let patch = env.endsWith('\n') ? '' : '\n';
532
- patch += '\n# MongoFire\n';
533
- for (const [k, v] of missing) patch += `${k}=${v}\n`;
534
- fs.appendFileSync(envPath, patch, 'utf8');
535
- return { action: 'updated', added: missing.map(([k]) => k) };
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
+ '✕ ' + f.file.padEnd(24) + '' + f.error + ''
418
+ );
419
+ errLines.push('');
420
+ errLines.push('Tip: close the file in VS Code or any editor, then run init again.');
421
+ p.note(errLines.join('\n'), 'Could not write files');
422
+ p.cancel('Setup incomplete — some files could not be written.');
423
+ return;
424
+ }
537
425
 
538
- function writeFileIfNeeded(filePath, content, force) {
539
- const exists = fs.existsSync(filePath);
540
- if (exists && !force) return { action: 'skipped' };
541
- fs.writeFileSync(filePath, content, 'utf8');
542
- return { action: exists ? 'overwritten' : 'created' };
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) => '⚠ ' + h));
432
+ p.note(noteLines.join('\n'), 'Files created');
433
+
434
+ const steps = [
435
+ `Fill in ATLAS_URI inside .env`,
436
+ resolvedModule === 'esm'
437
+ ? `Add import './mongofire.js' to your app entry point`
438
+ : `Add require('./mongofire') to your app entry point`,
439
+ `Replace app.listen() with startApp(app, port):\n`+
440
+ ` ${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);"}`,
443
+ `Add Schema.plugin(plugin('name')) to each model — import plugin from mongofire.js`,
444
+ `DO NOT call mongoose.connect() — MongoFire owns the local connection`,
445
+ ];
446
+ p.note(steps.map((s, i) => `${i + 1}. ${s}`).join('\n'), 'Next steps');
544
447
 
545
- function collectPackageHints(cwd) {
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(`✓ Setup complete! ${resolvedModule.toUpperCase()} · ${wasAuto ? 'auto-detected' : 'manual'}`);
552
449
  }
553
450
 
554
- function printInitSummary(info) {
555
- console.log('');
556
- printResultLine('.env', info.envResult.action, info.envResult.added);
557
- printResultLine('mongofire.config.js', info.configResult.action);
558
- printResultLine('mongofire.js', info.entryResult.action);
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 (info.hints.length) {
580
- console.log('\nHints:');
581
- for (const h of info.hints) console.log(' \u26A0\uFE0F ' + h);
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
- function printResultLine(name, action, added) {
587
- const icon = { created: '\u2705', updated: '\u2705', overwritten: '\u267B\uFE0F', skipped: '\u26A0\uFE0F', unchanged: '\u2713' };
588
- const label = { created: 'created', updated: 'updated', overwritten: 'overwritten', skipped: 'exists (skipped)', unchanged: 'unchanged' };
589
- console.log((icon[action] || ' ') + ' ' + name + ' ' + (label[action] || action));
590
- if (added && added.length) console.log(' + ' + added.join(', '));
591
- }
592
-
593
- // ─── Interactive Clean ────────────────────────────────────────────────────────
594
-
595
- async function doCleanInteractive() {
596
- console.log('\n\uD83E\uDDF9 MongoFire Clean\n');
597
-
598
- const answers = await _prompt([
599
- {
600
- type: 'list',
601
- name: 'days',
602
- message: 'Delete synced records older than:',
603
- choices: [
604
- { name: '1 day (aggressive)', value: 1 },
605
- { name: '3 days', value: 3 },
606
- { name: '7 days (default)', value: 7 },
607
- { name: '14 days', value: 14 },
608
- { name: '30 days (conservative)', value: 30 },
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
- default: 2,
611
- },
612
- {
613
- type: 'confirm',
614
- name: 'confirm',
615
- message: 'Confirm delete? This cannot be undone.',
616
- default: false,
617
- },
618
- ]);
619
-
620
- if (!answers.confirm) {
621
- console.log('\nCancelled nothing deleted.\n');
622
- process.exit(0);
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
- flags.add('--days=' + answers.days);
626
- await doClean();
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
- console.error('❌ mongofire.config.js not found. Run: npx mongofire init');
636
- process.exit(1);
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
- console.log('\n\uD83D\uDCCA MongoFire Status');
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
- if (isNaN(days) || days < 1) {
664
- console.error('❌ Invalid --days value. Example: npx mongofire clean --days=7');
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
- console.log('\u2705 Deleted ' + count + ' old record(s)');
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
- // ─── Interactive Conflicts ────────────────────────────────────────────────────
687
-
688
- async function doConflictsInteractive() {
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
- console.error('❌ mongofire.config.js not found. Run: npx mongofire init');
693
- process.exit(1);
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
- console.log('\n\u2705 No unresolved conflicts.\n');
668
+ p.outro('\x1b[32mNo unresolved conflicts\x1b[0m');
704
669
  await mongofire.stop();
705
670
  process.exit(0);
706
671
  }
707
672
 
708
- console.log('\n\u26A0\uFE0F ' + list.length + ' unresolved conflict(s):\n');
709
- for (const c of list) {
710
- console.log(' \u2022 ' + c.collection + '/' + c.docId + ' op:' + c.type + ' v' + c.version + ' opId:' + c.opId);
711
- if (c.lastError) console.log(' Error: ' + c.lastError);
712
- }
713
-
714
- const { action } = await _prompt([
715
- {
716
- type: 'list',
717
- name: 'action',
718
- message: 'What do you want to do?',
719
- choices: [
720
- { name: 'Retry all (reset to pending, sync will re-attempt)', value: 'retry_all' },
721
- { name: 'Dismiss all (discard — accept local state)', value: 'dismiss_all' },
722
- { name: 'Pick one to retry', value: 'pick_retry' },
723
- { name: 'Pick one to dismiss', value: 'pick_dismiss' },
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
- console.log('\nNo changes made.\n');
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
- console.log('\n\u2705 Retried ' + list.length + ' conflict(s).\n');
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
- console.log('\n\u2705 Dismissed ' + list.length + ' conflict(s).\n');
700
+ s2.stop(`Dismissed ${list.length} conflict(s)`);
701
+ p.outro('\x1b[32mAll conflicts dismissed\x1b[0m');
738
702
  } else {
739
- const { opId } = await _prompt([
740
- {
741
- type: 'list',
742
- name: 'opId',
743
- message: 'Select conflict:',
744
- choices: list.map((c) => ({
745
- name: c.collection + '/' + c.docId + ' ' + c.type + ' v' + c.version,
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
- // ─── Reconcile ────────────────────────────────────────────────────────────────
765
-
766
- async function doReconcile() {
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
- console.error('❌ mongofire.config.js not found. Run: npx mongofire init');
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 results = await mongofire.reconcile({ verbose: true });
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
- console.log('\n\uD83D\uDCCA Reconciliation Results');
784
- 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');
785
- for (const r of results) {
786
- if (r.error) {
787
- console.log(' \u274C ' + r.collection + ': ' + r.error);
788
- } else {
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
- // ─── Helpers ─────────────────────────────────────────────────────────────────
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
- function requireMongofire() {
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
- return require(path.join(__dirname, '..', 'dist', 'src', 'index.cjs'));
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
- return require(path.join(__dirname, '..', 'src', 'index.cjs'));
1097
+ await localDb.collection(col).drop();
1098
+ dropped++;
807
1099
  } catch (err) {
808
- console.error('❌ MongoFire load failed:', err.message);
809
- process.exit(1);
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
- async function loadConfig(configPath, cwd) {
823
- const ext = path.extname(configPath).toLowerCase();
824
- if (ext === '.mjs') return loadESM(configPath);
825
- if (ext === '.cjs') return require(configPath);
826
- if (isESMProject(cwd)) return loadESM(configPath);
827
- try {
828
- return require(configPath);
829
- } catch (err) {
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
- const mod = await import(pathToFileURL(filePath).href);
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 isESMProject(cwd) {
847
- return readPackageJson(cwd)?.type === 'module';
848
- }
1176
+ function escapeRE(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
849
1177
 
850
- function escapeRE(s) {
851
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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
  }