fivocell 4.2.4 → 4.3.1

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/dist/cli.js CHANGED
@@ -45,6 +45,7 @@ const figlet_1 = __importDefault(require("figlet"));
45
45
  const gradient_string_1 = __importDefault(require("gradient-string"));
46
46
  const database_1 = require("./core/database");
47
47
  const layers_1 = require("./layers");
48
+ const cli_repl_1 = require("./cli-repl");
48
49
  (0, database_1.initializeDatabase)();
49
50
  const C = {
50
51
  primary: chalk_1.default.hex('#FF6B35'),
@@ -99,6 +100,13 @@ switch (cmd) {
99
100
  case 'stop':
100
101
  doStop();
101
102
  break;
103
+ case 'setup':
104
+ doSetup().catch((e) => {
105
+ const msg = e instanceof Error ? e.message : String(e);
106
+ console.log(C.warn(' Setup crashed: ' + msg));
107
+ process.exit(1);
108
+ });
109
+ break;
102
110
  case 'scan':
103
111
  doScan();
104
112
  break;
@@ -132,13 +140,24 @@ switch (cmd) {
132
140
  case 'mcp-config':
133
141
  doMcpConfig();
134
142
  break;
143
+ case 'repl':
144
+ case '-i':
145
+ case '--interactive':
146
+ doRepl();
147
+ break;
135
148
  case 'help':
136
149
  case '--help':
137
150
  case '-h':
138
151
  doHelp();
139
152
  break;
140
153
  default:
141
- doStatus();
154
+ // If no command and TTY → open interactive REPL; else show status (safe default for scripts/automation)
155
+ if (!cmd && process.stdin.isTTY) {
156
+ doRepl();
157
+ }
158
+ else {
159
+ doStatus();
160
+ }
142
161
  break;
143
162
  }
144
163
  // ─── cell start ─────────────────────────────────────────────────────────────
@@ -152,6 +171,37 @@ function doStart() {
152
171
  firstRunGreeting();
153
172
  }
154
173
  catch { }
174
+ // 1. Check if daemon is already running (cheap /health probe) — if so,
175
+ // skip restart and just print status. This makes `cell start` re-run safe.
176
+ const http = require('http');
177
+ const probeReq = http.request({
178
+ hostname: '127.0.0.1', port: 9876, path: '/health', method: 'GET', timeout: 1000,
179
+ }, (res) => {
180
+ if (res.statusCode === 200) {
181
+ let data = '';
182
+ res.on('data', (chunk) => { data += chunk ? chunk.toString() : ''; });
183
+ res.on('end', () => {
184
+ let up = 0;
185
+ try {
186
+ up = JSON.parse(data).uptimeSeconds || 0;
187
+ }
188
+ catch { }
189
+ const upH = Math.floor(up / 3600);
190
+ const upM = Math.floor((up % 3600) / 60);
191
+ const upStr = upH > 0 ? `${upH}h ${upM}m` : upM > 0 ? `${upM}m` : `${up}s`;
192
+ console.log(C.success(` Daemon: already running on port 9876 (uptime ${upStr})`));
193
+ printProjectStatusAndExit();
194
+ });
195
+ }
196
+ else {
197
+ startDaemon();
198
+ }
199
+ });
200
+ probeReq.on('error', () => { startDaemon(); });
201
+ probeReq.on('timeout', () => { probeReq.destroy(); startDaemon(); });
202
+ probeReq.end();
203
+ }
204
+ function startDaemon() {
155
205
  // 1. Kill any stale daemon (clean restart)
156
206
  try {
157
207
  const { stopDaemon } = require('./daemon/lifecycle');
@@ -196,64 +246,45 @@ function doStart() {
196
246
  console.log(C.dim(' Daemon + MCP: http://localhost:9876'));
197
247
  console.log(C.dim(' MCP endpoint: POST http://localhost:9876/mcp'));
198
248
  console.log();
199
- // 3. Auto-detect project + offer to start watcher (so @cell live events
200
- // work immediately without a manual `cell watch start`).
201
- try {
202
- const projectName = path.basename(process.cwd());
203
- const httpMod = require('http');
204
- const body = JSON.stringify({
205
- method: 'tools/call',
206
- params: { name: 'cell_watch_status', arguments: { project: projectName } },
207
- });
208
- const req = httpMod.request({
209
- hostname: '127.0.0.1', port: 9876, path: '/mcp', method: 'POST',
210
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
211
- }, (res) => {
212
- let data = '';
213
- res.on('data', (c) => { data += c || ''; });
214
- res.on('end', () => {
215
- try {
216
- const parsed = JSON.parse(data);
217
- const status = parsed?.result?.status;
218
- if (status && status.active) {
219
- console.log(C.success(` Watcher: ${projectName} LIVE (${status.eventCount} events so far)`));
220
- }
221
- else {
222
- console.log(C.dim(` Watcher: ${projectName} not active`));
223
- console.log(C.primary(` Tip: run \`cell watch start ${projectName}\` to track file saves for @cell`));
224
- }
225
- }
226
- catch { /* ignore */ }
227
- console.log();
228
- console.log(C.dim(' Next: cell scan (scan codebase + build layers)'));
229
- console.log(C.dim(' Then: cell status'));
230
- console.log();
231
- process.exit(0);
232
- });
233
- });
234
- req.on('error', () => {
235
- console.log(C.dim(' Next: cell scan (scan codebase + build layers)'));
236
- console.log(C.dim(' Then: cell status'));
237
- console.log();
238
- process.exit(0);
239
- });
240
- req.setTimeout(3000, () => { req.destroy(); process.exit(0); });
241
- req.write(body);
242
- req.end();
243
- return;
249
+ // Give the daemon a moment to bind to the port, then print project status.
250
+ setTimeout(() => { printProjectStatusAndExit(); }, 500);
251
+ }
252
+ catch (e) {
253
+ console.log(C.warn(' Start failed: ' + String(e)));
254
+ process.exit(1);
255
+ }
256
+ }
257
+ function printProjectStatusAndExit() {
258
+ try {
259
+ const { detectProject, checkScanState, checkWatchState, checkMcpState, isDaemonRunning } = require('./core/setup');
260
+ const httpMod = require('http');
261
+ const project = detectProject(process.cwd());
262
+ const scan = checkScanState(process.cwd());
263
+ const watch = checkWatchState(project.name);
264
+ const mcp = checkMcpState(os.homedir());
265
+ console.log();
266
+ console.log(C.bold(' Project:'), C.num(project.name), C.dim(`(${project.source})`));
267
+ console.log();
268
+ console.log(` ${scan.done ? C.success('OK') : C.dim('--')} scan: ${scan.reason}`);
269
+ console.log(` ${watch.done ? C.success('OK') : C.dim('--')} watch: ${watch.reason}`);
270
+ console.log(` ${mcp.done === mcp.total ? C.success('OK') : C.dim('--')} mcp: ${mcp.done}/${mcp.total} IDEs wired${mcp.needsWrite.length > 0 ? C.warn(' (need: ' + mcp.needsWrite.join(', ') + ')') : ''}`);
271
+ console.log();
272
+ const allReady = scan.done && watch.done && mcp.done === mcp.total;
273
+ if (allReady) {
274
+ console.log(C.dim(' Cell is fully ready. Use @cell in your AI chat.'));
244
275
  }
245
- catch {
246
- // fall through
276
+ else {
277
+ console.log(C.primary(' Run: cell setup (one-time, idempotent) to finish setup'));
247
278
  }
279
+ console.log();
280
+ process.exit(0);
281
+ }
282
+ catch {
248
283
  console.log(C.dim(' Next: cell scan (scan codebase + build layers)'));
249
284
  console.log(C.dim(' Then: cell status'));
250
285
  console.log();
251
286
  process.exit(0);
252
287
  }
253
- catch (e) {
254
- console.log(C.warn(' Start failed: ' + String(e)));
255
- process.exit(1);
256
- }
257
288
  }
258
289
  // ─── cell stop ──────────────────────────────────────────────────────────────
259
290
  function doStop() {
@@ -401,6 +432,129 @@ function doScan() {
401
432
  console.log();
402
433
  }
403
434
  }
435
+ // ─── cell setup (idempotent one-time project setup) ─────────────────────────
436
+ /**
437
+ * One-command setup for a new project. Runs scan + watcher + MCP config
438
+ * with full idempotency — safe to re-run any number of times. Skips steps
439
+ * that are already done and only does the missing pieces.
440
+ *
441
+ * Flags:
442
+ * --project <name> Override project name (default: package.json name or folder basename)
443
+ * --no-rules Skip writing AGENTS.md / .cursorrules
444
+ * --force-rules Overwrite existing AGENTS.md / .cursorrules
445
+ * --skip-scan Don't run scan
446
+ * --skip-watch Don't start watcher
447
+ * --skip-mcp Don't write MCP configs
448
+ * --dir <path> Watch dir (default: cwd)
449
+ * --dry-run Print what would happen, don't change anything
450
+ */
451
+ async function doSetup() {
452
+ const banner = figlet_1.default.textSync('CELL', { font: 'ANSI Shadow', horizontalLayout: 'fitted' });
453
+ console.log((0, gradient_string_1.default)(['#FF6B35', '#FFAB91'])(banner));
454
+ console.log(C.dim(' Setting up project (idempotent)...\n'));
455
+ // ─── Flag parsing ─────────────────────────────────────────────────────
456
+ const noRules = args.includes('--no-rules');
457
+ const forceRules = args.includes('--force-rules');
458
+ const skipScan = args.includes('--skip-scan');
459
+ const skipWatch = args.includes('--skip-watch');
460
+ const skipMcp = args.includes('--skip-mcp');
461
+ const dryRun = args.includes('--dry-run');
462
+ const projectIdx = args.indexOf('--project');
463
+ const projectName = projectIdx > 0 ? args[projectIdx + 1] : undefined;
464
+ const dirIdx = args.indexOf('--dir');
465
+ const dir = dirIdx > 0 ? args[dirIdx + 1] : undefined;
466
+ try {
467
+ const { runSetup, detectProject, checkScanState, checkWatchState, checkMcpState, isDaemonRunning, } = require('./core/setup');
468
+ const cwd = process.cwd();
469
+ const project = detectProject(cwd, projectName);
470
+ console.log(C.bold(` Project: ${C.num(project.name)}`));
471
+ console.log(C.dim(` Source: ${project.source}${project.packageJsonPath ? ' (' + project.packageJsonPath + ')' : ''}`));
472
+ console.log(C.dim(` Cwd: ${cwd}`));
473
+ if (dryRun)
474
+ console.log(C.warn(' Mode: DRY RUN (no changes will be made)'));
475
+ console.log();
476
+ // ─── Daemon check ────────────────────────────────────────────────────
477
+ const daemonUp = await isDaemonRunning();
478
+ if (daemonUp) {
479
+ console.log(C.success(' [daemon] running on http://localhost:9876'));
480
+ }
481
+ else {
482
+ console.log(C.warn(' [daemon] not running — watcher will be skipped'));
483
+ console.log(C.dim(' run `cell start` to bring it up, then re-run `cell setup`'));
484
+ }
485
+ console.log();
486
+ // ─── Pre-flight state report ────────────────────────────────────────
487
+ if (!dryRun) {
488
+ const scan = checkScanState(cwd);
489
+ const watch = daemonUp ? checkWatchState(project.name) : { done: false, reason: 'daemon not running' };
490
+ const mcp = checkMcpState(os.homedir());
491
+ console.log(C.bold(' Pre-flight:'));
492
+ console.log(` scan: ${scan.done ? C.success('done') : C.dim('missing')} (${scan.reason})`);
493
+ console.log(` watch: ${watch.done ? C.success('active') : C.dim('inactive')} (${watch.reason})`);
494
+ console.log(` mcp: ${mcp.done}/${mcp.total} IDEs wired${mcp.needsWrite.length > 0 ? C.warn(' (need: ' + mcp.needsWrite.join(', ') + ')') : ''}`);
495
+ console.log();
496
+ const allDone = scan.done && watch.done && mcp.done === mcp.total;
497
+ if (allDone) {
498
+ console.log(C.success(' Already fully set up — nothing to do.'));
499
+ console.log(C.dim(' Tip: re-run anytime; it is safe and idempotent.'));
500
+ console.log();
501
+ return;
502
+ }
503
+ }
504
+ // ─── Run setup ───────────────────────────────────────────────────────
505
+ const start = Date.now();
506
+ const result = await runSetup({
507
+ cwd,
508
+ homedir: os.homedir(),
509
+ noRules,
510
+ forceRules,
511
+ projectName,
512
+ skipScan,
513
+ skipWatch: skipWatch || !daemonUp,
514
+ skipMcp,
515
+ dir,
516
+ dryRun,
517
+ });
518
+ const ms = Date.now() - start;
519
+ // ─── Result summary ─────────────────────────────────────────────────
520
+ console.log(C.bold(' Result:'));
521
+ console.log(` ${result.scan.done ? C.success('OK') : C.warn('FAIL')} scan: ${result.scan.reason}`);
522
+ console.log(` ${result.watch.done ? C.success('OK') : C.warn('FAIL')} watch: ${result.watch.reason}`);
523
+ const mcpOk = result.mcp.done === result.mcp.total;
524
+ console.log(` ${mcpOk ? C.success('OK') : C.warn('PARTIAL')} mcp: ${result.mcp.done}/${result.mcp.total} IDEs wired${result.mcp.written.length > 0 ? ' (' + result.mcp.written.length + ' just written)' : ''}`);
525
+ console.log();
526
+ if (result.warnings.length > 0) {
527
+ console.log(C.warn(' Warnings:'));
528
+ for (const w of result.warnings)
529
+ console.log(C.warn(` ! ${w}`));
530
+ console.log();
531
+ }
532
+ if (result.errors.length > 0) {
533
+ console.log(C.warn(' Errors:'));
534
+ for (const e of result.errors)
535
+ console.log(C.warn(` x ${e}`));
536
+ console.log();
537
+ }
538
+ // ─── Final verdict ──────────────────────────────────────────────────
539
+ const allGood = result.scan.done && result.watch.done && mcpOk && result.errors.length === 0;
540
+ if (allGood) {
541
+ console.log(C.success(` Setup complete in ${ms}ms. Cell is ready.`));
542
+ }
543
+ else {
544
+ console.log(C.warn(` Setup finished in ${ms}ms with warnings (see above).`));
545
+ }
546
+ console.log();
547
+ console.log(C.dim(' Run: cell status Check what Cell knows about you'));
548
+ console.log(C.dim(' Run: cell context Inject @cell block into your AI prompt'));
549
+ console.log();
550
+ }
551
+ catch (e) {
552
+ const msg = e instanceof Error ? e.message : String(e);
553
+ console.log(C.warn(' Setup failed: ' + msg));
554
+ console.log();
555
+ }
556
+ process.exit(0);
557
+ }
404
558
  // ─── cell status ────────────────────────────────────────────────────────────
405
559
  function semiLabel(s) {
406
560
  if (s === 'with semicolons' || s === 'always')
@@ -1362,14 +1516,154 @@ function doMcpConfig() {
1362
1516
  console.log(C.dim(' Make sure `cell start` is running for tools to connect.'));
1363
1517
  console.log();
1364
1518
  }
1519
+ // ─── cell repl (interactive mode with tab completion) ────────────────────────
1520
+ /**
1521
+ * Interactive REPL — opened by `cell` (no args) in a TTY, or by `cell repl`.
1522
+ * User types a command (with or without leading `/`), press Enter to run.
1523
+ * Type `/` then Tab to see all commands. Type `exit`/`quit` to leave.
1524
+ *
1525
+ * Tab completion: when the input starts with `/`, returns matching commands.
1526
+ * Otherwise no completion (user is typing a command name without prefix).
1527
+ */
1528
+ function showReplHelp() {
1529
+ console.log();
1530
+ console.log(C.bold(' Commands:'));
1531
+ console.log(C.dim(' ────────'));
1532
+ for (const [name, desc] of cli_repl_1.REPL_COMMANDS) {
1533
+ const padded = name.padEnd(14);
1534
+ console.log(` ${C.primary('/' + padded)} ${C.dim(desc)}`);
1535
+ }
1536
+ console.log();
1537
+ console.log(C.dim(' Type a command and press Enter. Use /<TAB> to filter.'));
1538
+ console.log(C.dim(' Tab completion works after `/` — try typing `/s` then Tab.'));
1539
+ console.log();
1540
+ }
1541
+ function doRepl() {
1542
+ // Only run if stdin is a TTY. If piped (e.g. `echo help | cell`), fall back to status.
1543
+ if (!process.stdin.isTTY) {
1544
+ doStatus();
1545
+ return;
1546
+ }
1547
+ const readline = require('readline');
1548
+ const { spawn } = require('child_process');
1549
+ const pathMod = require('path');
1550
+ // Show banner
1551
+ console.log();
1552
+ const banner = figlet_1.default.textSync('CELL', { font: 'ANSI Shadow', horizontalLayout: 'fitted' });
1553
+ console.log((0, gradient_string_1.default)(['#FF6B35', '#FFAB91'])(banner));
1554
+ console.log(C.dim(' Interactive REPL — type a command, / for list, exit to quit'));
1555
+ console.log();
1556
+ // First-run greeting
1557
+ try {
1558
+ const { firstRunGreeting } = require('./first-run');
1559
+ firstRunGreeting();
1560
+ }
1561
+ catch { }
1562
+ const rl = readline.createInterface({
1563
+ input: process.stdin,
1564
+ output: process.stdout,
1565
+ prompt: C.primary('cell> '),
1566
+ completer: cli_repl_1.replCompleter,
1567
+ terminal: true,
1568
+ });
1569
+ // Track command history
1570
+ const historyFile = pathMod.join(os.homedir(), '.fivo', 'cell', 'repl_history');
1571
+ try {
1572
+ const fsMod = require('fs');
1573
+ if (fsMod.existsSync(historyFile)) {
1574
+ const lines = fsMod.readFileSync(historyFile, 'utf8').split('\n').filter(Boolean);
1575
+ // readline.history is an array; pushing to it adds to up-arrow memory
1576
+ for (const l of lines)
1577
+ rl.history.push(l);
1578
+ }
1579
+ }
1580
+ catch { }
1581
+ const persistHistory = () => {
1582
+ try {
1583
+ const fsMod = require('fs');
1584
+ const pathMod2 = require('path');
1585
+ fsMod.mkdirSync(pathMod2.dirname(historyFile), { recursive: true });
1586
+ // Keep last 100 commands
1587
+ const recent = rl.history.slice(-100);
1588
+ fsMod.writeFileSync(historyFile, recent.join('\n') + '\n', 'utf8');
1589
+ }
1590
+ catch { }
1591
+ };
1592
+ rl.prompt();
1593
+ rl.on('line', (line) => {
1594
+ const trimmed = line.trim();
1595
+ if (!trimmed) {
1596
+ rl.prompt();
1597
+ return;
1598
+ }
1599
+ // Handle exit
1600
+ if (trimmed === 'exit' || trimmed === 'quit' || trimmed === 'q') {
1601
+ persistHistory();
1602
+ console.log(C.dim(' Bye!'));
1603
+ rl.close();
1604
+ return;
1605
+ }
1606
+ // Strip leading `/` if present
1607
+ const cmd = (0, cli_repl_1.stripSlash)(trimmed);
1608
+ // In-REPL help (no spawn overhead)
1609
+ if (cmd === 'help' || cmd === '?') {
1610
+ showReplHelp();
1611
+ rl.prompt();
1612
+ return;
1613
+ }
1614
+ // In-REPL version (avoid child spawn for trivial info)
1615
+ if (cmd === 'version' || cmd === '--version' || cmd === '-v') {
1616
+ try {
1617
+ const pkg = require('../package.json');
1618
+ console.log(`${C.primary('cell')} v${C.num(pkg.version)}`);
1619
+ }
1620
+ catch { }
1621
+ rl.prompt();
1622
+ return;
1623
+ }
1624
+ // Validate the command is known (so typos get a hint instead of a silent no-op)
1625
+ const firstToken = cmd.split(/\s+/)[0];
1626
+ if (!(0, cli_repl_1.isKnownCommand)(firstToken)) {
1627
+ console.log(C.warn(` Unknown command: ${firstToken}`));
1628
+ console.log(C.dim(' Type `help` for the list, or `/` then Tab.'));
1629
+ console.log();
1630
+ rl.prompt();
1631
+ return;
1632
+ }
1633
+ // Spawn cell <cmd> as child process (shares stdio so output is real-time)
1634
+ const cellPath = pathMod.join(__dirname, 'cli.js');
1635
+ const childArgs = cmd.split(/\s+/);
1636
+ const child = spawn(process.execPath, [cellPath, ...childArgs], {
1637
+ stdio: 'inherit',
1638
+ });
1639
+ child.on('exit', (code) => {
1640
+ if (code !== 0 && code !== null) {
1641
+ console.log(C.dim(` (exit ${code})`));
1642
+ }
1643
+ console.log();
1644
+ rl.prompt();
1645
+ });
1646
+ child.on('error', (err) => {
1647
+ console.log(C.warn(` Failed to run command: ${err.message}`));
1648
+ console.log();
1649
+ rl.prompt();
1650
+ });
1651
+ });
1652
+ rl.on('close', () => {
1653
+ persistHistory();
1654
+ console.log();
1655
+ process.exit(0);
1656
+ });
1657
+ }
1365
1658
  // ─── cell help ──────────────────────────────────────────────────────────────
1366
1659
  function doHelp() {
1367
1660
  console.log();
1368
1661
  console.log(C.bold(' Cell Commands'));
1369
1662
  console.log(C.dim(' ─────────────'));
1370
1663
  console.log();
1371
- console.log(` ${C.primary('cell start')} Start daemon + MCP (port 9876)`);
1664
+ console.log(` ${C.primary('cell start')} Start daemon + MCP (port 9876) — re-run safe`);
1372
1665
  console.log(` ${C.primary('cell stop')} Stop daemon`);
1666
+ console.log(` ${C.primary('cell setup')} One-time project setup: scan + watcher + MCP (idempotent)`);
1373
1667
  console.log(` ${C.primary('cell --version')} Print version + daemon status`);
1374
1668
  console.log(` ${C.primary('cell mcp-config')} Auto-register cell MCP in Cursor/Antigravity/Codex/OpenCode`);
1375
1669
  console.log(` ${C.primary('cell scan')} Scan codebase + build layers`);
@@ -1385,6 +1679,7 @@ function doHelp() {
1385
1679
  console.log(` ${C.primary('cell watch daemon [proj] [dir]')} Run as long-lived watcher (for .bat)`);
1386
1680
  console.log(` ${C.primary('cell blindspots [dir] [maxFiles]')} Scan for blind spots (15+ types)`);
1387
1681
  console.log(` ${C.primary('cell context [project] [tool]')} Inject @cell context block`);
1682
+ console.log(` ${C.primary('cell repl')} Interactive REPL with / completion (or just run \`cell\` in a TTY)`);
1388
1683
  console.log(` ${C.primary('cell help')} Show this help`);
1389
1684
  console.log();
1390
1685
  console.log(C.dim(' Install: npm i -g fivocell'));